記一次springboot專案結合arthas排查ClassNotFoundException問題

語言: CN / TW / HK

前言

前陣子業務部門的專案出現了一個很奇怪的問題,有個class明明存在,本地idea執行也沒問題,然後一發佈線上就出現ClassNotFoundException問題,而且線上這個class確實是存在的。本文就通過一個demo示例來複現這麼一個情況

demo示例

注:本文的專案框架為springboot2。本文僅演示ClassNotFoundException相關內容,並不模擬業務流

業務服務A

package com.example.helloloader.service;

import org.springframework.stereotype.Service;

@Service
public class HelloService {

    public String hello(){
        return "hello loader";
    }
}

元件B

@Component
public class HelloServiceLoaderUtils implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    public String invoke(String className){
        try {
            ClassLoader classLoader = ClassLoader.getSystemClassLoader();
            Class clz = classLoader.loadClass(className);
            Object bean = applicationContext.getBean(clz);
            Method method = clz.getMethod("hello");
            return (String) method.invoke(bean);

        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

服務A呼叫元件B

@SpringBootApplication(scanBasePackages = "com.example")
public class HelloLoaderApplication implements ApplicationRunner {

    @Autowired
    private HelloServiceLoaderUtils helloServiceLoaderUtils;

    public static void main(String[] args) {
        SpringApplication.run(HelloLoaderApplication.class, args);
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println(helloServiceLoaderUtils.invoke(HelloService.class.getName()));
    }
}

異常復現

如果通過本地idea進行呼叫,控制檯會正常打印出

hello loader

將業務服務A打包,通過

java -jar hello-loader-0.0.1-SNAPSHOT.jar

啟動訪問

出現了ClassNotFoundException異常

異常排查

class存在,卻找不到class,要麼就是類載入器錯了,要麼是class的位置錯了。因此通過arthas進行排查。對arthas不瞭解的朋友,可以檢視如下文章

java應用線上診斷神器--Arthas

我們通過如下命令檢視com.example.helloloader.service.HelloService載入器

sc -d com.example.helloloader.service.HelloService

從圖片可以看出打包後的HelloService的類載入器為spring封裝過的載入器,因此用appClassLoader是載入不到HelloService

解決方法

1、方法一將appClassLoader改成spring封裝的載入器

做法就是將ClassLoader.getSystemClassLoader()改成

Thread.currentThread().getContextClassLoader()即可

改好重新打包。此時重新執行,觀察控制檯

當前類載入器:org.springframework.boot.loader.Laun[email protected]
hello loader

2、方法二修改打包方式

將打包外掛由

<build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

切換成

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.6</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                            <mainClass>com.example.helloloader.HelloLoaderApplication</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>2.10</version>
                <executions>
                    <execution>
                        <id>copy</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>
                                ${project.build.directory}/lib
                            </outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

切換打包方式後,重新執行

當前類載入器:[email protected]
hello loader

此時正常輸出,且載入器為AppClassLoader。我們可以通過

sc -d com.example.helloloader.service.HelloService

觀察HelloService的類載入器

此時的HelloService的類載入器為AppClassLoader

總結

1、如果專案採用springboot的打包外掛,他的class會放在/BOOT-INF,且該目錄下的class類載入器為

org.springframework.boot.loader.LaunchedURLClassLoader

2、arthas是個好東西,誰用誰知道

3、當時業務排查的時候,過程是比我文章示例還要複雜一點。因為專案是部署到k8s中,當本地專案啟動沒問題時,業務方的研發就一直把問題聚焦在k8s中,一直覺得是k8s引發的問題。

後面他們業務方找到我,叫我幫忙排查,我第一反應就是可能打包出了問題,於是我讓業務方打個包,本地以java -jar試下,但業務方的研發又很肯定的說,他試過本地打jar執行也沒問題。因為業務方的程式碼我這邊是沒許可權訪問的,沒辦法進行驗證。後面只能建議他們安裝arthas,最終結合arthas解決了問題