SpringBootjavaagent

编程

SpringBoot 提供了一个插件spring-boot-maven-plugin用于把程序打包成一个可执行的 jar 包。在 pom 文件里加入这个插件即可:

1

2

3

4

5

6

7

8

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

打包完生成的executable-jar-1.0-SNAPSHOT.jar内部的结构如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

├── META-INF

│ ├── MANIFEST.MF

│ └── maven

│ └── tech.iooo

│ └── executable-jar

│ ├── pom.properties

│ └── pom.xml

├── lib

│ ├── aopalliance-1.0.jar

│ ├── classmate-1.1.0.jar

│ ├── spring-boot-1.3.5.RELEASE.jar

│ ├── spring-boot-autoconfigure-1.3.5.RELEASE.jar

│ ├── ...

├── org

│ └── springframework

│ └── boot

│ └── loader

│ ├── ExecutableArchiveLauncher$1.class

│ ├── ...

└── tech

└── iooo

└── executablejar

└── ExecutableJarApplication.class

然后可以直接执行 jar 包就能启动程序了:

1

java -jar executable-jar-1.0-SNAPSHOT.jar

打包出来 fat jar 内部有 4 种文件类型:

  1. META-INF 文件夹:程序入口,其中 MANIFEST.MF 用于描述 jar 包的信息
  2. lib 目录:放置第三方依赖的 jar 包,比如 springboot 的一些 jar 包
  3. spring boot loader 相关的代码
  4. 模块自身的代码

MANIFEST.MF文件的内容:

1

2

3

4

5

6

7

8

9

10

11

12

Manifest-Version: 1.0

Implementation-Title: executable-jar

Implementation-Version: 1.0-SNAPSHOT

Archiver-Version: Plexus Archiver

Built-By: Format

Start-Class: tech.iooo.executablejar.ExecutableJarApplication

Implementation-Vendor-Id: tech.iooo

Spring-Boot-Version: 1.3.5.RELEASE

Created-By: Apache Maven 3.5.3

Build-Jdk: 1.8.0_172

Implementation-Vendor: Pivotal Software, Inc.

Main-Class: org.springframework.boot.loader.JarLauncher

我们看到,它的 Main-Class 是org.springframework.boot.loader.JarLauncher,当我们使用java -jar执行 jar 包的时候会调用JarLauncher的 main 方法,而不是我们编写的 SpringApplication。

那么 JarLauncher 这个类是的作用是什么的?

它是 SpringBoot 内部提供的工具 Spring Boot Loader 提供的一个用于执行 Application 类的工具类 (fat jar 内部有 spring loader 相关的代码就是因为这里用到了)。相当于 Spring Boot Loader 提供了一套标准用于执行 SpringBoot 打包出来的 jar。

Spring Boot Loader 抽象的一些类

  • 抽象类Launcher:各种 Launcher 的基础抽象类,用于启动应用程序;跟Archive配合使用;目前有 3 种实现,分别是JarLauncherWarLauncher以及PropertiesLauncher

  • Archive:归档文件的基础抽象类。JarFileArchive就是 jar 包文件的抽象。它提供了一些方法比如getUrl会返回这个Archive对应的 URL;getManifest方法会获得Manifest数据等。ExplodedArchive是文件目录的抽象

  • JarFile:对 jar 包的封装,每个JarFileArchive都会对应一个JarFileJarFile被构造的时候会解析内部结构,去获取 jar 包里的各个文件或文件夹,这些文件或文件夹会被封装到 Entry 中,也存储在JarFileArchive中。如果 Entry 是个 jar,会解析成JarFileArchive

比如一个 JarFileArchive 对应的 URL 为:

1

jar:file:/Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/

它对应的 JarFile 为:

1

/Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar

这个 JarFile 有很多 Entry,比如:

1

2

3

4

5

6

7

8

9

META-INF/

META-INF/MANIFEST.MF

tech/

tech/iooo/

....

tech/iooo/executablejar/ExecutableJarApplication.class

lib/spring-boot-starter-1.3.5.RELEASE.jar

lib/spring-boot-1.3.5.RELEASE.jar

...

JarFileArchive内部的一些依赖 jar 对应的 URL(SpringBoot 使用org.springframework.boot.loader.jar.Handler处理器来处理这些 URL):

1

2

3

jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-starter-web-1.3.5.RELEASE.jar!/

jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class

我们看到如果有 jar 包中包含 jar,或者 jar 包中包含 jar 包里面的 class 文件,那么会使用 !/ 分隔开,这种方式只有org.springframework.boot.loader.jar.Handler能处理,它是 SpringBoot 内部扩展出来的一种 URL 协议。

JarLauncher 的执行过程

JarLauncher的 main 方法:

1

2

3

4

public static void main(String[] args) {

// 构造JarLauncher,然后调用它的launch方法。参数是控制台传递的

new JarLauncher().launch(args);

}

JarLauncher被构造的时候会调用父类ExecutableArchiveLauncher的构造方法。

ExecutableArchiveLauncher的构造方法内部会去构造Archive,这里构造了JarFileArchive。构造JarFileArchive的过程中还会构造很多东西,比如JarFileEntry …

JarLauncherlaunch方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

protected void launch(String[] args) {

try {

// 在系统属性中设置注册了自定义的URL处理器:org.springframework.boot.loader.jar.Handler。如果URL中没有指定处理器,会去系统属性中查询

JarFile.registerUrlProtocolHandler();

// getClassPathArchives方法在会去找lib目录下对应的第三方依赖JarFileArchive,同时也会项目自身的JarFileArchive

// 根据getClassPathArchives得到的JarFileArchive集合去创建类加载器ClassLoader。这里会构造一个LaunchedURLClassLoader类加载器,这个类加载器继承URLClassLoader,并使用这些JarFileArchive集合的URL构造成URLClassPath

// LaunchedURLClassLoader类加载器的父类加载器是当前执行类JarLauncher的类加载器

ClassLoader classLoader = createClassLoader(getClassPathArchives());

// getMainClass方法会去项目自身的Archive中的Manifest中找出key为Start-Class的类

// 调用重载方法launch

launch(args, getMainClass(), classLoader);

}

catch (Exception ex) {

ex.printStackTrace();

System.exit(1);

}

}

// Archive的getMainClass方法

// 这里会找出spring.study.executablejar.ExecutableJarApplication这个类

public String getMainClass() throws Exception {

Manifest manifest = getManifest();

String mainClass = null;

if (manifest != null) {

mainClass = manifest.getMainAttributes().getValue("Start-Class");

}

if (mainClass == null) {

throw new IllegalStateException(

"No "Start-Class" manifest entry specified in " + this);

}

return mainClass;

}

// launch重载方法

protected void launch(String[] args, String mainClass, ClassLoader classLoader)

throws Exception {

// 创建一个MainMethodRunner,并把args和Start-Class传递给它

Runnable runner = createMainMethodRunner(mainClass, args, classLoader);

// 构造新线程

Thread runnerThread = new Thread(runner);

// 线程设置类加载器以及名字,然后启动

runnerThread.setContextClassLoader(classLoader);

runnerThread.setName(Thread.currentThread().getName());

runnerThread.start();

}

MainMethodRunner的 run 方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

@Override

public void run() {

try {

// 根据Start-Class进行实例化

Class<?> mainClass = Thread.currentThread().getContextClassLoader()

.loadClass(this.mainClassName);

// 找出main方法

Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);

// 如果main方法不存在,抛出异常

if (mainMethod == null) {

throw new IllegalStateException(

this.mainClassName + " does not have a main method");

}

// 调用

mainMethod.invoke(null, new Object[] { this.args });

}

catch (Exception ex) {

UncaughtExceptionHandler handler = Thread.currentThread()

.getUncaughtExceptionHandler();

if (handler != null) {

handler.uncaughtException(Thread.currentThread(), ex);

}

throw new RuntimeException(ex);

}

}

Start-Class 的 main 方法调用之后,内部会构造 Spring 容器,启动内置 Servlet 容器等过程。 这些过程我们都已经分析过了。

关于自定义的类加载器 LaunchedURLClassLoader

LaunchedURLClassLoader重写了 loadClass 方法,也就是说它修改了默认的类加载方式 (先看该类是否已加载这部分不变,后面真正去加载类的规则改变了,不再是直接从父类加载器中去加载)。LaunchedURLClassLoader定义了自己的类加载规则:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

private Class<?> doLoadClass(String name) throws ClassNotFoundException {

// 1) Try the root class loader

try {

if (this.rootClassLoader != null) {

return this.rootClassLoader.loadClass(name);

}

}

catch (Exception ex) {

// Ignore and continue

}

// 2) Try to find locally

try {

findPackage(name);

Class<?> cls = findClass(name);

return cls;

}

catch (Exception ex) {

// Ignore and continue

}

// 3) Use standard loading

return super.loadClass(name, false);

}

加载规则:

  1. 如果根类加载器存在,调用它的加载方法。这里是根类加载是 ExtClassLoader
  2. 调用 LaunchedURLClassLoader 自身的 findClass 方法,也就是 URLClassLoader 的 findClass 方法
  3. 调用父类的 loadClass 方法,也就是执行默认的类加载顺序 (从 BootstrapClassLoader 开始从下往下寻找)

LaunchedURLClassLoader自身的findClass方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

protected Class<?> findClass(final String name)

throws ClassNotFoundException

{

try {

return AccessController.doPrivileged(

new PrivilegedExceptionAction<Class<?>>() {

public Class<?> run() throws ClassNotFoundException {

// 把类名解析成路径并加上.class后缀

String path = name.replace(".", "/").concat(".class");

// 基于之前得到的第三方jar包依赖以及自己的jar包得到URL数组,进行遍历找出对应类名的资源

// 比如path是org/springframework/boot/loader/JarLauncher.class,它在jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/中被找出

// 那么找出的资源对应的URL为jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class

Resource res = ucp.getResource(path, false);

if (res != null) { // 找到了资源

try {

return defineClass(name, res);

} catch (IOException e) {

throw new ClassNotFoundException(name, e);

}

} else { // 找不到资源的话直接抛出ClassNotFoundException异常

throw new ClassNotFoundException(name);

}

}

}, acc);

} catch (java.security.PrivilegedActionException pae) {

throw (ClassNotFoundException) pae.getException();

}

}

下面是LaunchedURLClassLoader的一个测试:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// 注册org.springframework.boot.loader.jar.Handler URL协议处理器

JarFile.registerUrlProtocolHandler();

// 构造LaunchedURLClassLoader类加载器,这里使用了2个URL,分别对应jar包中依赖包spring-boot-loader和spring-boot,使用 "!/" 分开,需要org.springframework.boot.loader.jar.Handler处理器处理

LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader(

new URL[] {

new URL("jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/")

, new URL("jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-1.3.5.RELEASE.jar!/")

},

LaunchedURLClassLoaderTest.class.getClassLoader());

// 加载类

// 这2个类都会在第二步本地查找中被找出(URLClassLoader的findClass方法)

classLoader.loadClass("org.springframework.boot.loader.JarLauncher");

classLoader.loadClass("org.springframework.boot.SpringApplication");

// 在第三步使用默认的加载顺序在ApplicationClassLoader中被找出

classLoader.loadClass("org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration");

Spring Boot Loader 的作用

SpringBoot 在可执行 jar 包中定义了自己的一套规则,比如第三方依赖 jar 包在 / lib 目录下,jar 包的 URL 路径使用自定义的规则并且这个规则需要使用org.springframework.boot.loader.jar.Handler处理器处理。它的 Main-Class 使用JarLauncher,如果是 war 包,使用WarLauncher执行。这些Launcher内部都会另起一个线程启动自定义的SpringApplication类。

这些特性通过spring-boot-maven-plugin插件打包完成。

javaagent 扩展

对于使用 spring boot 框架的应用,直接在 IDE 里面执行 main 方法运行自定义 agent 工作 ok,一旦打成 uber jar 方式后加上 agent 启动,就会出现 agent 中引用的第三方包中的类报ClassNotFoundException。这也是因为 springboot 的 uber jar 机制导致的。
spring boot 使用自己的 main 类org.springframework.boot.loader.JarLauncher启动应用,并自定义一个LaunchedURLClassLoader,再由它加载应用的 main 类。而LaunchedURLClassLoader在初始化 classpath 搜索路径时特意把 javaagent jar 包排除在外,所以 javaagent jar 包中的类是不能被LaunchedURLClassLoader定义的,所以 javaagent 中的辅助类如果引用了某个第三方包中的类,而这个类是被LaunchedURLClassLoader定义的,简单引用就会出现ClassNotFoundException

所以,必须让 javaagent 中用到的辅助类也由定义当前正在增强的 Class 的LaunchedURLClassLoader定义。

1

2

3

4

5

6

7

8

9

public interface ClassFileTransformer {

byte[] transform(

ClassLoader loader,

String className,

Class<?> classBeingRedefined,

ProtectionDomain protectionDomain,

byte[] classfileBuffer)

throws IllegalClassFormatException;

}

更确切地说,应该让每一个被ClassFileTransformer修改的类所用到的自定义以及第三方辅助类都由ClassFileTransformer#transform()方法第一个参数的ClassLoader定义。这样,不管应用的ClassLoader采用了何种类查找策略,都可以保证辅助类可以正常加载到。

那么,如何做到让增强类所用到的辅助类都被同一个类定义呢。一种办法是显示地用对应的ClassLoader定义所有用到的辅助类,这样需要手动注入所有辅助类,比较繁琐;另一种办法是将一组功能相关的辅助类打成 jar 包,注入到对应的ClassLoader中,这样就不需要一个一个类手动注入。推荐采用的是第二种方案,将一组功能相关的增强辅助类做成一个插件,并打成一个 jar 包,然后在增强类的时候,将对应的 jar 包注入到当前执行增强的ClassLoader中。

不同类型的ClassLoader的注入方式有所不同,方法如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

public class ClassInjector {

private static Method DEFINE_CLASS;

private static Method ADD_URL;

static {

try {

DEFINE_CLASS = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);

DEFINE_CLASS.setAccessible(true);

ADD_URL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);

ADD_URL.setAccessible(true);

} catch (NoSuchMethodException e) {

throw new IllegalStateException(e);

}

}

/** * 注入到非URLClassLoader的非引导类ClassLoader * @param classLoader * @param className * @param bytes 类定义 * @throws InvocationTargetException * @throws IllegalAccessException */

public static void defineClass(ClassLoader classLoader, String className, byte[] bytes) throws InvocationTargetException, IllegalAccessException {

if (classLoader != null) {

DEFINE_CLASS.invoke(classLoader, className, bytes, 0, bytes.length);

}

}

/** * 注入到URLClassLoader类加载器 * @param classLoader * @param url * @throws InvocationTargetException * @throws IllegalAccessException */

public static void addURL(URLClassLoader classLoader, URL url) throws InvocationTargetException, IllegalAccessException {

ADD_URL.invoke(classLoader, url);

}

/** * 注入到引导类加载器 * @param instrumentation * @param jarFile */

public static void addURL(Instrumentation instrumentation, JarFile jarFile) {

instrumentation.appendToBootstrapClassLoaderSearch(jarFile);

}

}

采用上面的思路将 javaagent 用插件的方式实现,会导致每个插件都是一个 jar 包,不方便部署。可以用maven-assembly-plugin将 javaagent 核心代码和插件打成一个 jar 包,agent 加载时,再将 jar 包解压,取出内嵌的插件包。

或者,也可以借鉴 spring-boot uber jar 的处理方式,自定义 Jar 包结构和 URL handler,这样就可以直接加载内嵌的 jar 包,不需要先解压。

以上是 SpringBootjavaagent 的全部内容, 来源链接: utcz.com/z/511562.html

回到顶部