TDDL與Spring Boot集成Version報錯——跟蹤與解決


先說背景:公司采用diamond+tddl,這套技術來做web管理。本人處於好奇率先體驗了下spring-boot,於是就有了spring-boot+tddl的組合。但是jar包上線后,屢屢發現一條error日志不痛不癢的出現在日志文件中,處於程序員的本能,怎么能允許error日志出現在我的系統中呢!

於是,展開了一段tddl與spring-boot的愛恨之旅...

掙扎期

首先看錯誤提示:

2017-09-27 11:15:58,428 [main] ERROR com.taobao.tddl.common.utils.version.Version:74 -  [TDDL] check guava version is recommend <= 15.0(the minimum version), please upgrade guava jar version, tddl version: recommend
java.lang.IllegalStateException: check guava version is recommend <= 15.0(the minimum version), please upgrade guava jar version
	at com.taobao.tddl.common.utils.version.Version.validVersion(Version.java:122)
	at com.taobao.tddl.common.utils.version.Version.<clinit>(Version.java:36)
	at com.taobao.tddl.monitor.logger.LoggerInit.initTddlLog(LoggerInit.java:42)
	at com.taobao.tddl.monitor.logger.LoggerInit.<clinit>(LoggerInit.java:32)
	at com.taobao.tddl.group.jdbc.TGroupDataSource.doInit(TGroupDataSource.java:117)
	at com.taobao.tddl.common.model.lifecycle.AbstractLifecycle.init(AbstractLifecycle.java:21)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)

太明顯了!jar包版本不對嘛!——so easy!

於是取mvn里面,調查版本,發現沒啥問題呀!guava(21.0);druid(1.1.2)。嘗試了maven的diagram圖和mvn dependency:tree,各種分析都沒啥問題!

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>20.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.2</version>
</dependency>

於是,我迷茫了!!!!!!!

突破期

作為一個合格的程序員,不放棄,不拋棄是我們的原則!我想起來實習的時候,公司一位技術大牛教過我,怎么在不下載完整源碼的情況下,直接調試第三方jar。

首先需要一點背景知識,就是java的類加載機制!

類加載

最基本的知識就是類加載的模型和父類委托機制:

Bootstrap ClassLoader:加載JAVA_HOME\lib下的jar
        |
Extension ClassLoader:加載JAVA_HOME\ext\lib下的jar
        |
Application ClassLoader:加載用戶路徑上指定的類

父類委托機制,其實就是每次加載的時候,會優先把加載的任務交給上一層,上一層再交給上一層。如果上一層找到,就使用這個jar;如果找不到再由下一層尋找。也就是說,如果同樣的兩個jar,放在JAVA_HOME\lib里面的會優先使用。

有些人可能會有疑問,這根本地調試有什么關系?別着急,在開發環境或者應用部署環境中,應用類的加載也是有一定的順序的。比如會先讀取classes下面的文件,找不到的話,在讀取lib中的文件。

了解這個classes和lib的優先級,就足夠我們調試用了!

spring-boot jar目錄結構

另外一個背景知識,就是需要了解spring-boot jar的目錄結構。

這樣我們編譯出的class會進入到classes目錄下,而其他的第三方的jar會進入lib目錄。

開始源碼跟蹤

根據error日志的跟蹤,可以發現最終出錯的位置是在Version的validVersion方法。所以我們直接看一下源碼,並且根據源碼直接拷貝出來放入我們自己的工程:

public final class Version {
    ...
    // 這里是檢查常用jar包版本的地方,可以看到tddl強烈要求druid、daimond、guava的版本
    static {
        ...
        // 檢查下經常性沖突的兩個包
        Version.checkDuplicate("com/alibaba/druid/pool/DruidDataSource.class", false);
        validVersion("druid", "com/alibaba/druid/pool/DruidDataSource.class", "1.0.6");
        Version.checkDuplicate("com/taobao/diamond/client/Diamond.class", false);
        validVersion("diamond", "com/taobao/diamond/client/Diamond.class", "3.6.8");
        Version.checkDuplicate("com/google/common/collect/MapMaker.class", false);
        validVersion("guava", "com/google/common/collect/MapMaker.class", "15.0");
    }

    // 這里是檢查版本的地方
    public static boolean validVersion(String name, String path, String minVersion) {
        try {
            if (minVersion == null) {
                return true;
            }

            Long minv = convertVersion(minVersion);
            Enumeration<URL> urls = Version.class.getClassLoader().getResources(path);
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                if (url != null) {
                    String file = url.getFile();
                    if (file != null && file.length() > 0) {
                        // 主要的點是這里!根據file的名字獲取版本
                        String version = getVerionByPath(file);
                        // 為了方便運行期調試,直接輸出關鍵的信息 System.out.println("------------------------------------");
                        System.out.println("完整的路徑為:"+file);
                        System.out.println("檢測到version為:"+version);
                        System.out.println("------------------------------------");
                        if (checkVersionNecessary(version)) {
                            Long ver = convertVersion(version);
                            if (ver < minv) {
                                throw new IllegalStateException("check " + name + " version is " + version + " <= "
                                        + minVersion + "(the minimum version), please upgrade "
                                        + name + " jar version");
                            }
                        }
                    }
                }
            }
        } catch (Throwable e) { // 防御性容錯
            logger.error(e.getMessage(), e);
        }

        return true;
    }
    
}

目錄結構如下:

運行后就發現,得到下面的輸出:

------------------------------------
完整的路徑為:file:/Users/xingoo/IdeaProjects/test-recommend/target/recommend.jar!/BOOT-INF/lib/guava-20.0.jar!/com/google/common/collect/MapMaker.class
檢測到version為:recommend
------------------------------------

呵呵噠!原來error日志已經提示了版本為recommend,如果有經驗的話,應該直接就能發現版本是錯的。不過對於新手來說,真是看不出來啊。

那么接下來就可以分析一下為什么版本會出錯了!

talk is cheap, show me your code!

public static String getVerionByPath(String file) {
    if (file != null && file.length() > 0 && StringUtils.contains(file, ".jar")) {
        int index = StringUtils.indexOf(file, ".jar");
        file = file.substring(0, index);
        int i = file.lastIndexOf('/');
        if (i >= 0) {
            file = file.substring(i + 1);
        }
        i = file.indexOf("-");
        if (i >= 0) {
            file = file.substring(i + 1);
        }
        while (file.length() > 0 && !Character.isDigit(file.charAt(0))) {
            i = file.indexOf("-");
            if (i >= 0) {
                file = file.substring(i + 1);
            } else {
                break;
            }
        }
        return file;
    } else {
        return null;
    }
}

這里用的StringUtils.indexOf獲得第一個jar前面的部分,由於我的spring-boot打出來的jar包,是嵌套jar的。所以獲取第一個就出錯了!

修改方法, 把indexOf替換成lastIndexOf就行了:

...
int index = StringUtils.lastIndexOf(file, ".jar");
...

最終解決辦法

想要一條error日志也沒有,可以參考下面的方法:

  • 把下面的代碼考入工程,記住不要修改包名,直接放在com.tddl.common.utils.version下!
  • 並且關閉tddl的各種版本檢查,

不過因為兩個error報錯,就把整個tddl的版本檢測給摘掉好像太暴力。所以最好還是等官網來修復吧!:

public final class Version {

    private static final Logger logger = LoggerFactory.getLogger(Version.class);
    private static final Package myPackage = VersionAnnotation.class.getPackage();
    private static final VersionAnnotation va = myPackage.getAnnotation(VersionAnnotation.class);
    private static final String VERSION = getVersion(Version.class, "5.1.7");

    static {
        // 檢查是否存在重復的jar包
        // Version為tddl5.x之后的版本檢測類
        // 如果Version檢測沒有發現重復,再和tddl3.x系列進行檢查,ThreadLocalMap兼容了tddl3.x的類路徑
//        if (!Version.checkDuplicate(Version.class)) {
//            Version.checkDuplicate(ThreadLocalMap.class);
//        }

        // 檢查下經常性沖突的兩個包
//        Version.checkDuplicate("com/alibaba/druid/pool/DruidDataSource.class", false);
//        validVersion("druid", "com/alibaba/druid/pool/DruidDataSource.class", "1.0.6");
//        Version.checkDuplicate("com/taobao/diamond/client/Diamond.class", false);
//        validVersion("diamond", "com/taobao/diamond/client/Diamond.class", "3.6.8");
//        Version.checkDuplicate("com/google/common/collect/MapMaker.class", false);
//        validVersion("guava", "com/google/common/collect/MapMaker.class", "15.0");
    }

    private Version() {
    }
    ...
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM