為什么要理解類加載?遇到這種問題就知道書到用時(shí)方恨少了
1、問題背景
我們項(xiàng)目中引入了sharding-jdbc,本機(jī)運(yùn)行、開發(fā)環(huán)境運(yùn)行、測試環(huán)境運(yùn)行都沒有問題,結(jié)果到了預(yù)發(fā)布環(huán)境發(fā)生了一個(gè)異常:
Cannot support database type 'MySQL' at org.apache.shardingsphere.sql.parser.core.parser.SQLParserFactory.newInstance(SQLParserFactory.java:55)
at org.apache.shardingsphere.sql.parser.core.parser.SQLParserExecutor.towPhaseParse(SQLParserExecutor.java:55)
at org.apache.shardingsphere.sql.parser.core.parser.SQLParserExecutor.execute(SQLParserExecutor.java:47)
at org.apache.shardingsphere.sql.parser.SQLParserEngine.parse0(SQLParserEngine.java:79)
at org.apache.shardingsphere.sql.parser.SQLParserEngine.parse(SQLParserEngine.java:61)
at org.apache.shardingsphere.underlying.route.DataNodeRouter.createRouteContext(DataNodeRouter.java:97)
at org.apache.shardingsphere.underlying.route.DataNodeRouter.executeRoute(DataNodeRouter.java:89)
at org.apache.shardingsphere.underlying.route.DataNodeRouter.route(DataNodeRouter.java:76)
at org.apache.shardingsphere.underlying.pluggble.prepare.PreparedQueryPrepareEngine.route(PreparedQueryPrepareEngine.java:54)
而我們除了本機(jī)環(huán)境各人使用上有些差異外,開發(fā)環(huán)境運(yùn)行、測試環(huán)境運(yùn)行和預(yù)發(fā)布環(huán)境上只有MySQL服務(wù)端版本是不同的,雖然是報(bào)錯(cuò)上看和MySQL服務(wù)端并沒有直接關(guān)系,但我們還是在開發(fā)環(huán)境還原了預(yù)發(fā)布環(huán)境的MySQL服務(wù)端版本,還原之后開發(fā)環(huán)境并沒有復(fù)現(xiàn)問題。
這就非常詭異了。也給我們解決帶來了一定的技術(shù)挑戰(zhàn):不能通過本地調(diào)試或者加JVM參數(shù)來做進(jìn)一步驗(yàn)證。
以下就是我們的排查過程。
2、源碼分析
既然有明確的報(bào)錯(cuò)日志,首先要進(jìn)行代碼分析:
SQLParserFactory.newInstance(SQLParserFactory.java:55)
跟進(jìn)這一行報(bào)錯(cuò)的源碼:
public static SQLParser newInstance(final String databaseTypeName, final String sql) {
for (SQLParserConfiguration each : NewInstanceServiceLoader.newServiceInstances(SQLParserConfiguration.class)) {
if (each.getDatabaseTypeName().equals(databaseTypeName)) {
return createSQLParser(sql, each);
}
}
throw new UnsupportedOperationException(String.format("Cannot support database type '%s'", databaseTypeName));
}
第7行拋出了日志中的異常。這說明問題就發(fā)生在2、3、4這三行中的一行。
究竟是哪一行呢?本地可以調(diào)試的話很簡單,Debug跟蹤一下,但是預(yù)發(fā)布環(huán)境不能Debug呀!當(dāng)然其實(shí)有些公司網(wǎng)是通的,可以做遠(yuǎn)程Debug,更多的是一個(gè)規(guī)范的問題。
在不能Debug的前提下,我把這三行代碼拷貝出來,分步打日志,再放到預(yù)發(fā)布環(huán)境運(yùn)行:
try{
log.warn("ShardingDebug test=1==============================begin");
for (SQLParserConfiguration each : NewInstanceServiceLoader.newServiceInstances(SQLParserConfiguration.class)) {
log.warn("ShardingDebug test=2==============================each:{}", each);
if (each.getDatabaseTypeName().equals("MySQL")) {
log.warn("ShardingDebug test=3==============================equals:{}", each);
CodePointCharStream codePointCharStream = CharStreams.fromString("select version()");
log.warn("ShardingDebug test=4==============================codePointCharStream:{}", codePointCharStream);
// 這次存在
Lexer lexer = null;
try {
log.warn("ShardingDebug test=5.0==============================MySQLLexer:{}", each.getLexerClass().getName());
log.warn("ShardingDebug test=5.1==============================MySQLLexer:{}", each.getLexerClass().getConstructor(CharStream.class).getName());
SQLLexer sqlLexer = each.getLexerClass().getConstructor(CharStream.class).newInstance(codePointCharStream);
log.warn("ShardingDebug test=5.2==============================sqlLexer:{}, isInstance:{}", sqlLexer, sqlLexer instanceof Lexer);
lexer = (Lexer) each.getLexerClass().getConstructor(CharStream.class).newInstance(codePointCharStream);
log.warn("ShardingDebug test=5==============================lexer:{}", lexer);
} catch (InstantiationException e) {
log.error("ShardingDebug test=6==============================lexer:{}", lexer, e);
} catch (IllegalAccessException e) {
log.error("ShardingDebug test=7==============================lexer:{}", lexer, e);
} catch (InvocationTargetException e) {
log.error("ShardingDebug test=8==============================lexer:{}", lexer, e);
} catch (NoSuchMethodException e) {
log.error("ShardingDebug test=9==============================lexer:{}", lexer, e);
}
CommonTokenStream lexerCommonTokenStream = new CommonTokenStream(lexer);
log.warn("ShardingDebug test=10==============================lexerCommonTokenStream:{}", lexerCommonTokenStream);
SQLParser sqlParser = null;
try {
log.warn("ShardingDebug test=11.0==============================sqlParser:{}", each.getParserClass());
log.warn("ShardingDebug test=11.1==============================sqlParser:{}", each.getParserClass().getConstructor(TokenStream.class));
sqlParser = each.getParserClass().getConstructor(TokenStream.class).newInstance(lexerCommonTokenStream);
log.warn("ShardingDebug test=11==============================sqlParser:{}", sqlParser);
} catch (InstantiationException e) {
log.warn("ShardingDebug test=12==============================sqlParser:{}", sqlParser, e);
} catch (IllegalAccessException e) {
log.warn("ShardingDebug test=13==============================sqlParser:{}", sqlParser, e);
} catch (InvocationTargetException e) {
log.warn("ShardingDebug test=14==============================sqlParser:{}", sqlParser, e);
} catch (NoSuchMethodException e) {
log.warn("ShardingDebug test=15==============================sqlParser:{}", sqlParser, e);
}
break;
}
}
} catch (Exception ex) {
log.error("ShardDebugJob failed", ex);
}
}
我把這三行代碼拆解的非常細(xì),希望盡量減少發(fā)布,排查出問題的原因。
結(jié)果日志只打印了第一行,剩下的都沒打印。說明沒有進(jìn)入for循環(huán)。也就說明了。
NewInstanceServiceLoader.newServiceInstances(SQLParserConfiguration.class)
沒有加載到東西。再看這一行的源碼:
public static <T> Collection<T> newServiceInstances(final Class<T> service) {
Collection<T> result = new LinkedList<>();
if (null == SERVICE_MAP.get(service)) {
return result;
}
for (Class<?> each : SERVICE_MAP.get(service)) {
result.add((T) each.newInstance());
}
return result;
}
這說明SERVICE_MAP里沒有對應(yīng)的實(shí)現(xiàn)類。再看SERVICE_MAP賦值的源碼:
public static <T> void register(final Class<T> service) {
for (T each : ServiceLoader.load(service)) {
registerServiceClass(service, each);
}
}
private static <T> void registerServiceClass(final Class<T> service, final T instance) {
Collection<Class<?>> serviceClasses = SERVICE_MAP.get(service);
if (null == serviceClasses) {
serviceClasses = new LinkedHashSet<>();
}
serviceClasses.add(instance.getClass());
SERVICE_MAP.put(service, serviceClasses);
}
本質(zhì)上值都是ServiceLoader.load(service)加載來的。這就要考察Java功力了。
這行代碼本質(zhì)是什么呢?
3、原理分析
本質(zhì)是使用了Java的SPI功能。
Java SPI(Service Provider Interface)是一種服務(wù)發(fā)現(xiàn)機(jī)制,它允許服務(wù)提供者為API定義標(biāo)準(zhǔn)接口,而實(shí)現(xiàn)者可以通過配置文件來注冊自己的實(shí)現(xiàn)。如果在使用SPI時(shí)出現(xiàn)“java SPI沒有加載到實(shí)現(xiàn)類”的錯(cuò)誤,通常意味著以下幾種情況之一:
- 實(shí)現(xiàn)類沒有正確地被打包到j(luò)ar中,或者沒有被放置在正確的目錄下。
- 配置文件(通常是META-INF/services/接口全限定名)中沒有列出實(shí)現(xiàn)類的全限定名。
- 類加載器沒有正確加載到實(shí)現(xiàn)類的路徑。
解決方法:
- 確保實(shí)現(xiàn)類的jar包已經(jīng)被正確打包,并且實(shí)現(xiàn)類的包結(jié)構(gòu)和接口包結(jié)構(gòu)一致。
- 檢查META-INF/services目錄下對應(yīng)接口的文件中是否有實(shí)現(xiàn)類的全限定名。
- 如果是在web容器或者OSGi環(huán)境中,確保類加載器的路徑設(shè)置正確,實(shí)現(xiàn)類應(yīng)該可見。
- 如果使用的是第三方庫,確保依賴已經(jīng)正確引入。
- 清除可能存在的緩存,比如重新編譯或重啟應(yīng)用。
這次的問題是屬于哪一種呢?很遺憾,都不是。我為了確認(rèn)問題,將預(yù)發(fā)布環(huán)境打的運(yùn)行jar包下載到本地,解壓查看確認(rèn),都是沒有問題的。
為了確認(rèn)可以加載到,我再一次發(fā)布預(yù)發(fā)布環(huán)境,這一次手動(dòng)執(zhí)行加載看看:
Class<?> mySQLParserConfiguration = Thread.currentThread().getContextClassLoader().loadClass(MySQLParserConfiguration.class.getName());
log.info("ShardingDebug test=0.0==============================loadClass:{}", mySQLParserConfiguration);
結(jié)果正常打印了實(shí)現(xiàn)類的全限定名。
這里為什么我會(huì)想到Thread.currentThread().getContextClassLoader()這個(gè)類加載器呢?很簡單。這個(gè)類加載器就是ServiceLoader.load源碼里使用的類加載器。
@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
4、問題解決
到這里,解決方案也呼之欲出:既然是可以加載到的,那應(yīng)該就是沒有在注冊服務(wù)代碼執(zhí)行前加載。手動(dòng)讓類加載在注冊服務(wù)前運(yùn)行即可。
Class<?> mySQLParserConfiguration = Thread.currentThread().getContextClassLoader().loadClass(MySQLParserConfiguration.class.getName());
log.info("ShardingDebug test=0.0==============================loadClass:{}", mySQLParserConfiguration);
try {
NewInstanceServiceLoader.register(SQLParserConfiguration.class);
} catch (Throwable e) {
log.error("ShardingDebug test=0.011==============================register", e);
}
先執(zhí)行這個(gè),再執(zhí)行最初的:
NewInstanceServiceLoader.newServiceInstances(SQLParserConfiguration.class)
就可以加載到對應(yīng)的實(shí)例了。
4、分析總結(jié)
這次問題出現(xiàn)在sharding-jdbc的SQL解析階段,可以通過源碼上下文看到問題發(fā)生在與MySQL服務(wù)端交互之前,可排除受服務(wù)端的影響。并且可以確定問題發(fā)生在JVM內(nèi)部。
可通過ServiceLoader.load(service)確定是使用了Java的SPI機(jī)制時(shí)發(fā)生問題。SPI的本質(zhì)是通過META-INF/services目錄下對應(yīng)接口的文件找到實(shí)現(xiàn)類。
驗(yàn)證實(shí)現(xiàn)類可被JVM正常加載我使用了與源碼相同的類加載器并發(fā)布到預(yù)發(fā)布環(huán)境進(jìn)行驗(yàn)證。因?yàn)椴煌惣虞d器有不同的使用條件。比如:
ClassLoader.getSystemClassLoader()
在本機(jī)會(huì)正常運(yùn)行,但是服務(wù)器上會(huì)因?yàn)檫\(yùn)行的是打好的 jar 包,路徑發(fā)生變化,服務(wù)器上運(yùn)行報(bào)「找不到類」異常。
整個(gè)排查過程也有一些怎樣搜索答案的思考,比如只是根據(jù)最初的異常來搜索,發(fā)現(xiàn)網(wǎng)上搜的都不是本質(zhì)問題。后來雖然我用更接近本質(zhì)的問題: