Springboot - Fat Jar

2019-11-06  本文已影响0人  rock_fish

传送:

Spring大观园,我有过的困惑或许你也有!


导读:

springboot FatJar 的设计,打破了标准jar的结构,在jar包内携带了其所依赖的jar包,通过jar中的main方法创建自己的类加载器,来识别加载运行其不规范的目录下的代码和依赖.

初识jar包结构

# JAR包中的MANIFEST.MF文件详解以及编写规范

打开Java的JAR文件我们经常可以看到文件中包含着一个META-INF目录,这个目录下会有一些文件,其中必有一个MANIFEST.MF,这个文件描述了该Jar文件的很多信息
其中 Main-Class 定义jar文件的入口类,该类必须是一个可执行的类,一旦定义了该属性即可通过 java -jar x.jar来运行该jar文件。

Spring-Boot-Version: 2.1.3.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.rock.springbootlearn.SpringbootLearnApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk: 1.8.0_131

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

运行时执行JarLauncher的Main方法

非开发阶段,是使用java -jar xxx.jar的方式来运行springboot程序.
这种情况下,springboot应用真实的启动类并不是我们所定义的带有main方法的类,而是
JarLauncher类.这个类在spring-boot-loader-***.jar包中,在项目中加入maven依赖,以便查看源码和远程调试.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-loader</artifactId>
</dependency>

可找到这个JarLauncher类的源码.


image.png

这个spring-boot-loader包中的内容与springboot的运行jar包中的一部分内容几乎一样.
JarLauncher在jar中的位置如下:


image.png

因jar规范要求Main-Class所指定的类,必须位于jar包的顶层目录下,即org.springframework.boot.loader.JarLauncher 这个org必须位于jar包中的第一级目录,不能嵌套的其他的目录下.所以只能将spring-boot-loader这个jar包的内容拷贝出来,而不是整个jar直接放置于执行Jar中.

程序依赖的携带
image.png

上边JarLauncher的这个org.xxx 以及 META-INF 这两个目录是符合jar包规范的.但是BOOT-INF这个目录里边类似我们开发中的一些用法,程序.class文件在classes目录下,依赖jar包在lib目录下.
按照jar包规范.jar中不能有jar包的情况下,.class文件应该按照spring-boot-loader这样顶级目录的方式,
从包内携带依赖角度来梳理,类似maven-shade-plugin 的做法,对于重名(全限定名)的类会出现覆盖.为了避免覆盖的情况,依赖原始jar包;要想让这些代码能够正常运行,一定是需要自己做classLoader来加载.

运行流程

自定义类加载器的常规处理,1指定资源,2指定委托关系,3指定线程上下文类加载器,4调用逻辑入口方法.
1.指定资源
1.1 构造方法中,基于jar包的文件系统信息,构造Archive对象

public ExecutableArchiveLauncher() {
    this.archive = createArchive();
}

protected final Archive createArchive() throws Exception {
    ProtectionDomain protectionDomain = getClass().getProtectionDomain();
    CodeSource codeSource = protectionDomain.getCodeSource();
    URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
    String path = (location != null) ? location.getSchemeSpecificPart() : null;
    if (path == null) {
        throw new IllegalStateException("Unable to determine code source archive");
    }
    File root = new File(path);
    if (!root.exists()) {
        throw new IllegalStateException(
                "Unable to determine code source archive from " + root);
    }
    return (root.isDirectory() ? new ExplodedArchive(root)
            : new JarFileArchive(root));
}

1.2 采集jar包中的classes 和lib目录下的归档文件.后边创建ClassLoader的时候作为参数传入

@Override
protected List<Archive> getClassPathArchives() throws Exception {
    List<Archive> archives = new ArrayList<>(
            this.archive.getNestedArchives(this::isNestedArchive));
    postProcessClassPathArchives(archives);
    return archives;
}

protected boolean isNestedArchive(Archive.Entry entry) {
    if (entry.isDirectory()) {
        return entry.getName().equals(BOOT_INF_CLASSES);
    }
    return entry.getName().startsWith(BOOT_INF_LIB);
}  
public static void main(String[] args) throws Exception {
    new JarLauncher().launch(args);
}

2创建ClassLoader

protected void launch(String[] args) throws Exception {
    JarFile.registerUrlProtocolHandler();
        //创建类加载器, 并指定归档文件
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    launch(args, getMainClass(), classLoader);
}
//创建类加载器, 将归档文件转换为URL
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
    List<URL> urls = new ArrayList<>(archives.size());
    for (Archive archive : archives) {
        urls.add(archive.getUrl());
    }
    return createClassLoader(urls.toArray(new URL[0]));
}
//父加载器是AppClassLoader
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
        //getClass().getClassLoader() 是系统类加载器,因为默认情况下main方法所在类是由SystemClassLoader加载的,默认情况下是AppClassLoader.
    return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

3,4 设置线程上下文类加载器,调用逻辑入口方法

public static void main(String[] args) throws Exception {
    new JarLauncher().launch(args);
}
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
        throws Exception {
        //设置线程上下文类加载器
    Thread.currentThread().setContextClassLoader(classLoader);
    //调用MANIFEST.MF 中配置的Start-Class: xxx的main方法,还带入了参数
        createMainMethodRunner(mainClass, args, classLoader).run();
}

# Java 打包 FatJar 方法小结

上一篇下一篇

猜你喜欢

热点阅读