[英]How to catch unhandled exceptions in the Room persistence library
背景:
我在我的 Android (Java) 項目中使用 Room 持久性庫來支持本地數據緩存。 Room 在查詢或保存數據時在專用線程上運行。
問題:
如果在 Room 管理的這些線程之一中拋出異常,那么整個應用程序就會崩潰。 這可能發生在數據不一致的情況下,例如數據與當前模式不匹配。 這是非常成問題的。 我寧願自己處理此類異常並擦除本地數據庫中的所有數據 - 這比讓用戶使用完全損壞且無法修復的應用程序要好。
示例異常:
2020-01-22 12:45:08.252 9159-11043/com.xyz E/AndroidRuntime: FATAL EXCEPTION: arch_disk_io_1
Process: com.xyz, PID: 9159
java.lang.RuntimeException: Exception while computing database live data.
at androidx.room.RoomTrackingLiveData$1.run(RoomTrackingLiveData.java:92)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)
Caused by: java.lang.RuntimeException: com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "primary" (class com.xyz.model.remotedatasource.sampleApi.entities.ProfileImage), not marked as ignorable (2 known properties: "isPrimary", "url"])
at [Source: (byte[])":)
... -1, column: 402] (through reference chain: com.xyz.model.remotedatasource.sampleApi.entities.Candidate["profileImages"]->java.util.ArrayList[0]->com.xyz.model.remotedatasource.sampleApi.entities.ProfileImage["primary"])
at com.xyz.model.localdatasource.Converters.deserialize(Converters.java:113)
at com.xyz.model.localdatasource.Converters.toCandidate(Converters.java:73)
at com.xyz.model.localdatasource.LocalDao_Impl$4.call(LocalDao_Impl.java:270)
at com.xyz.model.localdatasource.LocalDao_Impl$4.call(LocalDao_Impl.java:217)
at androidx.room.RoomTrackingLiveData$1.run(RoomTrackingLiveData.java:90)
... 3 more
示例數據訪問對象 (DAO):
public interface LocalDao {
@Query("SELECT * FROM Match")
LiveData<List<Match>> getMatches();
@Insert(onConflict = REPLACE)
void saveMatches(List<Match> matches);
}
題
由於 Room 在后台線程中執行許多操作,我希望有一種注冊自定義錯誤處理程序的方法。 你知道如何實現這一目標嗎? 如果沒有,您對如何在發生此類異常時自動擦除數據庫有任何其他建議嗎?
該目標可以通過注冊一個執行具有自定義異常處理程序的自定義線程來實現。
我想出了以下解決方案:
public abstract class LocalDatabase extends RoomDatabase {
private static final String TAG = LocalDatabase.class.getSimpleName();
private static final Object syncObj = new Object();
private static LocalDatabase localDatabase;
private static ConcurrentHashMap<Integer, String> dbToInstanceId = new ConcurrentHashMap<>();
private static ConcurrentHashMap<Long, String> threadToInstanceId = new ConcurrentHashMap<>();
public abstract LocalDao getDao();
public static LocalDatabase getInstance() {
if (localDatabase == null) {
localDatabase = buildDb();
}
return localDatabase;
}
private static LocalDatabase buildDb() {
// keep track of which thread belongs to which local database
final String instanceId = UUID.randomUUID().toString();
// custom thread with an exception handler strategy
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool(runnable -> {
ThreadFactory defaultThreadFactory = Executors.defaultThreadFactory();
Thread thread = defaultThreadFactory.newThread(runnable);
thread.setUncaughtExceptionHandler(resetDatabaseOnUnhandledException);
threadToInstanceId.put(thread.getId(), instanceId);
return thread;
});
LocalDatabase localDatabase = Room.databaseBuilder(App.getInstance().getApplicationContext(),
LocalDatabase.class, "LocalDatabase")
.fallbackToDestructiveMigration()
.setQueryExecutor(executor)
.build();
dbToInstanceId.put(localDatabase.hashCode(), instanceId);
return localDatabase;
}
static Thread.UncaughtExceptionHandler resetDatabaseOnUnhandledException = new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
Log.e("", "uncaught exception in a LocalDatabase thread, resetting the database", throwable);
synchronized (syncObj) {
// there is no active local database to clean up
if (localDatabase == null) return;
String instanceIdOfThread = threadToInstanceId.get(thread.getId());
String instanceIdOfActiveLocalDb = dbToInstanceId.get(localDatabase.hashCode());
if(instanceIdOfThread == null || !instanceIdOfThread.equals(instanceIdOfActiveLocalDb)) {
// the active local database instance is not the one that caused this thread to fail, so leave it as is
return;
}
localDatabase.tryResetDatabase();
}
}
};
public void tryResetDatabase() {
try {
String dbName = this.getOpenHelper().getDatabaseName();
// try closing existing connections
try {
if(this.getOpenHelper().getWritableDatabase().isOpen()) {
this.getOpenHelper().getWritableDatabase().close();
}
if(this.getOpenHelper().getReadableDatabase().isOpen()) {
this.getOpenHelper().getReadableDatabase().close();
}
if (this.isOpen()) {
this.close();
}
if(this == localDatabase) localDatabase = null;
} catch (Exception ex) {
Log.e(TAG, "Could not close LocalDatabase", ex);
}
// try deleting database file
File f = App.getContext().getDatabasePath(dbName);
if (f.exists()) {
boolean deleteSucceeded = SQLiteDatabase.deleteDatabase(f);
if (!deleteSucceeded) {
Log.e(TAG, "Could not delete LocalDatabase");
}
}
LocalDatabase tmp = buildDb();
tmp.query("SELECT * from Match", null);
tmp.close();
this.getOpenHelper().getReadableDatabase();
this.getOpenHelper().getWritableDatabase();
this.query("SELECT * from Match", null);
} catch (Exception ex) {
Log.e("", "Could not reset LocalDatabase", ex);
}
}
我不認為你可以對這些做些什么,它假設模式是正確的,但如果數據或模式有問題,那么你可以在插入或對它們進行任何操作時處理這樣的事情。
只需將 DAO 函數標記為可拋出的方法並在調用方處理可能的錯誤。
public interface LocalDao {
@Query("SELECT * FROM Match")
LiveData<List<Match>> getMatches() throws Exception;
@Insert(onConflict = REPLACE)
void saveMatches(List<Match> matches) throws Exception;
@Query("DELETE FROM Match")
public void nukeTable();
}
現在在調用方只調用這個處理異常。
public getMatches(){
//... some code
try{
//some code goes here
dao.getMatches();
}catch(Exception exp){
//if something happens bad then nuke the table on background thread
dao.nukeTable()
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.