美文网首页
Springboot - Fat Jar

Springboot - Fat Jar

作者: rock_fish | 来源:发表于2019-11-06 19:01 被阅读0次

    传送:

    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 方法小结

    相关文章

      网友评论

          本文标题:Springboot - Fat Jar

          本文链接:https://www.haomeiwen.com/subject/fndxbctx.html