1. 程式人生 > >SpringBoot之class is not visible from class loader

SpringBoot之class is not visible from class loader

一、前言

最近在搭建SpringBoot的新應用,遇到個有意思的問題,如題就是在載入某一個類時候丟擲了class is not visible from class loader, 下面就帶大家看看是如何產生的。

二、問題產生

  • 首先有如下bean的定義:
public class TestProxy implements TestService {

   private TestService testService;


   public void init() throws Exception {
       RemoteConsumerProxy<TestService> proxy =
               RemoteConsumerProxy()
                       .setInterfaceClass(TestService.class)
                       .build();
       testService = proxy.getService();
   }
   。。。
}

如上程式碼代理類TestProxy繼承了TestService類,並且在init方法裡面消費了介面TestService的提供的遠端服務。RemoteConsumerProxy類做了兩件事,首先是生成介面TestService的遠端服務bean這裡假設為beanRemote,然後對beanRemote進行JDK代理生成代理類beanRemoteProxy,代理的作用是執行具體遠端服務方法前進行統一限流處理或者指定呼叫的ip等。並且RemoteConsumerProxy是通過二方庫方式引入。

  • 然後引入了SpringBoot的開發工具模組spring-boot-devtools。
  • 滿足上面兩個條件後注入TestProxy到IOC容器,執行Spring-boot工程的main函式(注意打成jar,然後執行jar則不會有這個問題),就會丟擲:
    TestService is not visible from class loader

從呼叫堆疊看是java.lang.reflect.Proxy的apply方法丟擲的異常。

image.png

三、問題分析

既然是Proxy的apply方法丟擲了異常,那麼就看什麼情況下會丟擲異常,從Proxy的程式碼看是 interfaceClass != intf時候丟擲異常。

這裡intf是通過 RemoteConsumerProxy<TestService> proxy = RemoteConsumerProxy() .setInterfaceClass(TestService.class)傳遞的,

而interfaceClass則是使用

try {
                   interfaceClass = Class.forName(intf.getName(), false
, loader); } catch (ClassNotFoundException e) { }

建立的。

到這裡對類載入器比較熟悉的童鞋應該會有所思了,同一個類兩次載入後的Class物件不一樣,那只有一種情況,那就是使用了兩個類載入器載入了同一個類。

為了證明這個,可以在init方法裡面新增如下程式碼:

   System.out.println("TestProxy classloader:" + MassTopicQueryProxy.class.getClassLoader());
   System.out.println("TestService classloader:" + MassTopicQueryService.class.getClassLoader());
   System.out.println("RemoteConsumerProxy classloader:" + ACCSHSFConsumerProxy.class.getClassLoader());

執行後輸出為:

TestProxy classloader:org.sprin[email protected]63e66532
TestService classloader:org.sprin[email protected]63e66532
RemoteConsumerProxy classloader:[email protected]
  • 從結果可知TestProxy和TestService是使用RestartClassLoader類載入器載入的,所以呼叫程式碼 RemoteConsumerProxy.setInterfaceClass(TestService)傳遞的時候傳遞的是RestartClassLoader載入的Class物件。
  • 從結果可知RemoteConsumerProxy.setInterfaceClass是AppClassLoader載入的,所以ACCSHSFConsumerProxy內部執行程式碼
               try {
                   interfaceClass = Class.forName(intf.getName(), false, loader);
               } catch (ClassNotFoundException e) {
               }

時候也是使用AppClassLoader載入的,也就是這裡的TestService是AppClassLoader載入的,所以同一個介面由兩個類載入器載入,所以兩個Class物件不相等。

另外通過debug可以發現RestartClassLoader的父載入器就是AppClassLoader。

那麼RestartClassLoader又是什麼那?從何而來?
經查閱資料的20.2 Automatic Restart章節可知,SpringBoot使用spring-boot-devtools模組實現當classpath下的檔案被修改後自動重啟的功能。這是通過使用兩個類載入器來實現的,一些不需要的改變的類比如三方jar是使用base類載入器載入的(這裡值AppClassloader),開發中一些需要修改的類則使用restart classloader進行重新載入。

總結:在IDE裡面main函式方式執行時候由於會編譯類,classpath下的內容會發生變化,所以會觸發restart,從而導致丟擲異常。而首先通過mvn clean package 打包,然後在java -jar jar方式由於jar內部不會變了所以不會觸發restart,所以執行正常。

四、如何解決

  • 方案一,排查掉spring-boot-devtools模組模組的maven引入可以解決,這時候所有類都是使用APPClassloader載入。
  • 方案二,可以引入spring-boot-devtools模組,但是禁用禁用reStart功能
public static void main( String[] args )
   {
       System.setProperty("spring.devtools.restart.enabled", "false");

       SpringApplication.run(Application.class, args);
   }

五、總結

雖然是同一個類,但是使用不同的類載入器載入後得到的Class物件是不一樣的,區分一個Class物件是否相等要看包名+類名,也要看是否是同一個類載入器。另外SpringBoot的spring-boot-devtools模組的restart功能在IDE裡面執行main函式時候應該有bug。歡迎大家批評指正。

歡迎關注微信公眾號 ‘技術原始積累’
image.png


加多

加多

高階 Java 攻城獅 at 阿里巴巴加多,目前就職於阿里巴巴,熱衷併發程式設計、ClassLoader,Spring等開源框架,分散式RPC框架dubbo,springcloud等;愛好音樂,運動。微信公眾號:技術原始積累。知識星球賬號:技術原始積累