getResourceAsStream 探究

項目中經常會使用 properties 文件定義一些配置變量,相應的就需要寫一個類來加載此配置。
常用的方式是使用 class 或者 classLoader 對象的getResourceAsStream 來加載properties文件。
eg:

GlobalConfig.class.getResourceAsStream("/properties/globalConfig.properties")
GlobalConfig.class.getClassLoader().getResourceAsStream("properties/globalConfig.properties")

用Class或者ClassLoader 讀取的區別是什麼呢?相信眼尖的讀者已經看出來了,就是指定的路徑差了個 /

路徑是否有 “/” 到底有什麼影響,能不能使用 GlobalConfig.class.getResourceAsStream(“properties/globalConfig.properties”)
或者 GlobalConfig.class.getClassLoader().getResourceAsStream("/properties/globalConfig.properties") 來讀取配置呢?

帶着種種疑問,下面就來探究一下這裏面有什麼貓膩。
測試環境:sping-boot項目(jar包)
項目結構
在這裏插入圖片描述
深入源碼探究

  1. Class類的 getResourceAsStream方法 有啥貓膩?

查看Class類getResourceAsStream方法的源碼,如下:

     public InputStream getResourceAsStream(String name) {
        name = resolveName(name);
        ClassLoader cl = getClassLoader0();
        //...
        return cl.getResourceAsStream(name);
    }

哦呵,看出來了吧,Class的getResourceAsStream方法調用的是ClassLoader的getResourceAsStream,Class這傢伙真是夠懶的。不過呢,調用之前也不是什麼都不做,在調用前對name做了一些操作,即resolveName(name)。看來也不是真懶。
我們看看 resolveName 方法做了什麼呢。

    /**
     * Add a package name prefix if the name is not absolute 
     * Remove leading "/" if name is absolute
     */
    private String resolveName(String name) {
        //...
        if (!name.startsWith("/")) { // name不是以 "/" 開頭
            Class<?> c = this;
            while (c.isArray()) {
                c = c.getComponentType();
            }
            String baseName = c.getName(); // 當前類的全限定名。eg:com.markix.config.GlobalConfig
            int index = baseName.lastIndexOf('.');
            if (index != -1) {
                name = baseName.substring(0, index).replace('.', '/') +"/"+name;
            }
        } else { // name以 "/" 開頭
            name = name.substring(1);
        }
        return name;
    }

其實,resolveName方法上的註釋已說明一切,硬翻譯一波:如果name不是絕對的,則添加包名前綴;如果name是絕對的,則刪除最前面的"/"。
通過代碼我們也能得出此結論,當name以‘/’開頭,則執行 name = name.substring(1); 也就是截取掉開頭的’/’。當name不是以“/”開頭,則獲取當前類class對象的name(即類的全路徑名,包括包名),再截取包名替換成路徑形式拼接到name的前綴。

舉個栗子直觀描述上面說的一坨東西:

  • 絕對路徑:GlobalConfig.class.getResourceAsStream("/properties/config.properties")
    name 經過 resolveName 方法從 /properties/config.properties 變成 properties/config.properties
    進而調用 ClassLoader類的getResourceAsStream(“properties/config.properties”)

  • 相對路徑:GlobalConfig類class.getResourceAsStream(“properties/config.properties”)
    name 經過 resolveName 方法從原本的 properties/config.properties 變成 com/markix/config/properties/config.properties
    進而調用 ClassLoader類的getResourceAsStream(“com/markix/config/properties/config.properties”)

通過上述分析,得出結論

  • 調用 類名.class.getResourceAsStream("/路徑") 等價於調用 類名.class.getClassLoader().getResourceAsStream("路徑")
  • 調用 類名.class.getResourceAsStream("路徑") 等價於調用 類名.class.getClassLoader().getResourceAsStream("類路徑 + 路徑")

過渡一句,class的getResourceAsStream本質就是調用classLoader的getResourceAsStream,下面探究下classLoader的getResourceAsStream。

  1. ClassLoader類的getResourceAsStream方法 有啥貓膩?

查看ClassLoader類getResourceAsStream方法的源碼,呃,遇到難題了,有多個實現類重寫了getResourceAsStream,到底是哪個類?
getResourceAsStream
這裏關乎Java類加載器的知識,不瞭解的請先自覺補姿勢。博主直接拋結論啦,一般我們應用類運行都是使用 AppClassLoader 加載的,其繼承自 URLClassLoader,所以我們查看URLClassLoader的getResourceAsStream方法,如下:

    public InputStream getResourceAsStream(String name) {
        URL url = getResource(name);
        try {
            if (url == null) {
                return null;
            }
            URLConnection urlc = url.openConnection();
            InputStream is = urlc.getInputStream();
            //...
            return is;
        } catch (IOException e) {
            return null;
        }
    }

核心就是調用了 getResource 方法,接着獲取流就返回了。繼續看getResource方法,在ClassLoader類中:

	public URL getResource(String name) {
        URL url;
        if (parent != null) {
            url = parent.getResource(name);
        } else {
            url = getBootstrapResource(name);
        }
        if (url == null) {
            url = findResource(name);
        }
        return url;
    }

和類加載類似,採用雙親委託。如果有parent,就先調用父加載器的方法。
我們的資源在我們項目中,其實最終調用的是findResource方法進行查找,代碼如下:

public URL findResource(final String name) {
        /*
         * The same restriction to finding classes applies to resources
         */
        URL url = AccessController.doPrivileged(
            new PrivilegedAction<URL>() {
                public URL run() {
                    return ucp.findResource(name, true);
                }
            }, acc);

        return url != null ? ucp.checkURL(url) : null;
    }

點到爲止哈哈,有興趣自行debug哈(再深入編不下去了哈哈)。通過debug調試,總結一下結論。
結論:

  • 調用 類名.class.getClassLoader().getResourceAsStream("/路徑") 總是返回 null。’/’ 不可訪問。
  • 調用 類名.class.getClassLoader().getResourceAsStream("路徑") ,會在運行環境的所有加載目錄(包括jar)中查找路徑。

舉個栗子:
我的項目存放在 E:\WorkSpace\IDEA\spring-boot-demo,編譯目錄爲 E:\WorkSpace\IDEA\spring-boot-demo\target\classes\。我是直接在IDEA運行的,運行時會加載編譯目錄的內容,當調用
GlobalConfig.class.getClassLoader().getResourceAsStream(“properties/globalConfig.properties”) 時,也就會在 E:\WorkSpace\IDEA\spring-boot-demo\target\classes\ 目錄查找一下 properties\globalConfig.properties 文件是否存在,找不到就去其他目錄找,找得到就返回了。

另:
在tomcat容器環境中,調用類名.class.getClassLoader().getResourceAsStream("/路徑") 並不是返回null。原因在於tomcat重寫了ClassLoader機制,war項目運行時並不是使用AppClassLoader加載,而是使用tomcat自定義的WebappClassLoader類,其父類WebappClassLoaderBase重寫了getResourceAsStream方法,對路徑做了特殊處理,最終實現了調用 類名.class.getClassLoader().getResourceAsStream("/路徑") 等價於調用 類名.class.getClassLoader().getResourceAsStream("路徑")
詳見 WebappClassLoaderBase的getResourceAsStream方法。

所以,還是推薦使用如下形式:

GlobalConfig.class.getResourceAsStream("/properties/globalConfig.properties")
// 或者
GlobalConfig.class.getClassLoader().getResourceAsStream("properties/globalConfig.properties")

end

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章