springboot打包发布原理

如何打包发布一个springboot项目?

SpringBoot 提供了 Maven 插件 spring-boot-maven-plugin,将 Spring Boot 项目打成 jar 包或者 war 包。
只需要在pom.xml文件中加入下面这个插件配置,再通过mvn clean package获取jar包即可。


    ...
    
        ...
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

打包后 通过下面的命令即可启动一个服务。

java -jar  **.jar 

Jar包如何运行并启动SpringBoot项目?

springboot jar 的目录结构

image.png

可以看到,主要有三个大目录META-INF,BOOT-INF以及org,

META-INF

比较重要的是MAINIFEST.MF文件:

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: jarlearn
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.jsvc.jarlearn.JarlearnApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.5.5
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher

该文件声明了Main-Class 配置项:可以理解为jar包的启动类,这里设置为 spring-boot-loader 项目的 JarLauncher类,进行 Spring Boot 应用的启动。
还有一个Start-Class 配置项:配置的内容是我们springboot项目的主启动类。

BOOT-INF

classes文件中保存了 Java 类所编译的 .class文件以及配置文件等。
lib目录中保存了我们项目所依赖的jar包。


image.png

org

该文件中即springboot为我们提供的jar包启动类,亦即JarLauncher.class

当使用 java -jar filename.jar 命令启动时,会执行封装在 JAR 文件中的程序。JAR 文件需包含 manifest,其中一行格式为 Main-Class:classname,指定了一个包含 public static void main(String[] args) 方法的类,作为该程序的启动点。

为什么不可以直接Main-Class 放置为springboot的启动类呢?

对应在示例的这个项目,问题可以翻译为为什么不可以直接使用com.jsvc.jarlearn.JarlearnApplication类作为启动类?

主要是因为,Java 没有提供任何加载嵌套 jar 文件的标准方法(即加载本身包含在 jar 中的 jar 文件)。当需要分发一个可以从命令行运行而不需要解压缩的自包含应用程序时 , 会出现问题。

同时,我试了下,直接运行application类的话,是找不到主类的:

java -classpath /Users/sensu/jarl/jarlearn-0.0.1-SNAPSHOT.jar com.jsvc.jarlearn.JarlearnApplication

错误: 找不到或无法加载主类 com.jsvc.jarlearn.JarlearnApplication

因为在文件目录中,JarlearnApplication实际上是在META-INF/maven/... 中的,所以会找不到。

所以,springboot以org.springframework.boot.loader.JarLauncher为启动类,
又自定义了LaunchedURLClassLoader用来加载BOOT-INF中的class文件以及BOOT-INF/lib中的嵌套jar包。

关于JarLaunch

我这边通过引入spring-boot-loader模块来看下JarLaunch的源码:

//JarLauncher

    public static void main(String[] args) throws Exception {
        new JarLauncher().launch(args);
    }

可以看到main方法中,执行了launch方法,改方法由JarLaunch的父类Launcher提供:

    private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";

    /**
     * Launch the application. This method is the initial entry point that should be
     * called by a subclass {@code public static void main(String[] args)} method.
     * @param args the incoming arguments
     * @throws Exception if the application fails to launch
     */
    protected void launch(String[] args) throws Exception {
        if (!isExploded()) {
            JarFile.registerUrlProtocolHandler();
        }
        ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
        String jarMode = System.getProperty("jarmode");
        String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
        launch(args, launchClass, classLoader);
    }

launch方法主要分为三步:

  1. 在launch方法中,首先调用了registerUrlProtocolHandler方法注册URLStreamHandler类用于jar包的解析.
  2. 调用了createClassLoader方法,创建自定义的ClassLoader,用于从jar中加载类。
  3. 执行Application启动类。

registerUrlProtocolHandler

private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";


    /**
     * Register a {@literal 'java.protocol.handler.pkgs'} property so that a
     * {@link URLStreamHandler} will be located to deal with jar URLs.
     */
    public static void registerUrlProtocolHandler() {
        Handler.captureJarContextUrl();
        String handlers = System.getProperty(PROTOCOL_HANDLER, "");
        System.setProperty(PROTOCOL_HANDLER,
                ((handlers == null || handlers.isEmpty()) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
        resetCachedUrlHandlers();
    }

基本思路就是将org.springframework.boot.loader包路径添加到java.protocol.handler.pkgs环境变量中,从而使用自定义的 URLStreamHandler 实现类 Handler处理 jar: 协议的 URL。
关于handler 可以自行百度下。

createClassLoader

这里有两个主要方法:

ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());

也就是 getClassPathArchivesIterator 以及createClassLoader
首先是 getClassPathArchivesIterator

// ExecutableArchiveLauncher

    @Override
    protected Iterator getClassPathArchivesIterator() throws Exception {
        Archive.EntryFilter searchFilter = this::isSearchCandidate;
        Iterator archives = this.archive.getNestedArchives(searchFilter,
                (entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
        if (isPostProcessingClassPathArchives()) {
            archives = applyClassPathArchivePostProcessing(archives);
        }
        return archives;
    }

首先是isSearchCandidate,在JarLaunch中实现:

//JarLaunch 

    @Override
    protected boolean isSearchCandidate(Archive.Entry entry) {
        return entry.getName().startsWith("BOOT-INF/");
    }

可以看出是只处理BOOT-INF/文件夹下的内容。
然后会通过getNestedArchives获取到嵌套的Archive,其中的isNestedArchive 方法也由JarLaunch实现:

//JarLaunch 

    static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
        if (entry.isDirectory()) {
            return entry.getName().equals("BOOT-INF/classes/");
        }
        return entry.getName().startsWith("BOOT-INF/lib/");
    };

    @Override
    protected boolean isNestedArchive(Archive.Entry entry) {
        return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
    }

基本就是获取 BOOT-INF/classes/下的目录以及BOOT-INF/lib/ 下的jar文件,最终通过getNestedArchives将其封装为对应的Archive并返回。

然后就是createClassLoader方法:


    /**
     * Create a classloader for the specified archives.
     * @param archives the archives
     * @return the classloader
     * @throws Exception if the classloader cannot be created
     * @since 2.3.0
     */
    protected ClassLoader createClassLoader(Iterator archives) throws Exception {
        List urls = new ArrayList<>(50);
        while (archives.hasNext()) {
            urls.add(archives.next().getUrl());
        }
        return createClassLoader(urls.toArray(new URL[0]));
    }

基本上就是通过archives获取到所有的URL,然后创建处理这些URL的ClassLoader。

执行Application启动类方法

主要就是通过getMainClass方法获取到manifest文件中配置的Start-Class:

// ExecutableArchiveLauncher
    private static final String START_CLASS_ATTRIBUTE = "Start-Class";

    @Override
    protected String getMainClass() throws Exception {
        Manifest manifest = this.archive.getManifest();
        String mainClass = null;
        if (manifest != null) {
            mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
        }
        if (mainClass == null) {
            throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
        }
        return mainClass;
    }

然后通过另一个launch方法,开始执行:

// Launcher

    /**
     * Launch the application given the archive file and a fully configured classloader.
     * @param args the incoming arguments
     * @param launchClass the launch class to run
     * @param classLoader the classloader
     * @throws Exception if the launch fails
     */
    protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
        Thread.currentThread().setContextClassLoader(classLoader);
        createMainMethodRunner(launchClass, args, classLoader).run();
    }

这里createMainMethodRunner创建出来的是什么呢?

// MainMethodRunner

    private final String mainClassName;

    private final String[] args;

    /**
     * Create a new {@link MainMethodRunner} instance.
     * @param mainClass the main class
     * @param args incoming arguments
     */
    public MainMethodRunner(String mainClass, String[] args) {
        this.mainClassName = mainClass;
        this.args = (args != null) ? args.clone() : null;
    }

    public void run() throws Exception {
        Class mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
        Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        mainMethod.setAccessible(true);
        mainMethod.invoke(null, new Object[] { this.args });
    }

最终调用的其实就是MainMethodRunner的run方法了,其实也就是通过反射调用Application的main方法了。

你可能感兴趣的:(springboot打包发布原理)