springboot如何通过java -jar启动的?fatjar有什么缺点如何解决?

2021/11/14

springboot支持用java -jar 方式启动程序,和以往用tomcat启动spring mvc比起来,非常方便省事。那么springboot是如何实现的呢?

springboot打出来的jar包也称为fat jar,是指把所有的代码、依赖都打包到一个单独的文件中的jar,单独的jar易于传输使用。

要了解实现原理,我们分为包结构、启动过程两部分来理解。

springboot jar的结构

在springboot maven项目中,会通过spring-boot-maven-plugin插件来把项目打包成一个fat jar,可以通过jar -xvf xxx.jar(xxx是你的jar包的名字)解压jar包。

jar包解压后的文件夹结构如下

.
├── BOOT-INF
│   ├── classes
│   │   ├── application.yml
│   │   ├── com
│   │   │   └── xxx
│   │   │       ├── XXXX.class  // 工程中的代码class文件,例如controller类等等
│   └── lib
│       ├── HikariCP-xxx.jar // 依赖的jar包
│       ├── activation-xxx.jar
│       ├── adapter-guava-xxx.jar
├── META-INF
│   ├── MANIFEST.MF
└── org
    └── springframework
        └── boot
            └── loader
                ├── ExecutableArchiveLauncher.class // spring的启动相关类
                ├── JarLauncher.class
                └── util
                    └── SystemPropertyUtils.class
                    ...

springboot启动流程

java -jar xxx.jar时,java会在这个jar包的META-INFO/MANIFEST.MF中寻找Main-Class,然后运行这个Main-Class指向的类的main(args[])方法。

Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.xxx.XXXApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/

Main-Class是JarLauncher,MANIFEST.MF中还定义了spring需要的Start-Class, Spring-Boot-Classes, SPring-Boot-Lib等信息,Start-CLass是用户的springboot main方法代码入口,Boot-Classes和Lib是需要加入classpath的路径。 JarLauncher类如下,会调用new JarLauncher().launch(args)方法

public class JarLauncher extends ExecutableArchiveLauncher {
	public JarLauncher() {
	}

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

launch会找到所有的classpath的jar包,然后创建一个特定的ClassLoader,把jar包作为ClassLoader的classpath传入禁区。

protected void launch(String[] args) throws Exception {
    // 设置java.protocol.handler.pkgs属性, URLStreamHandler 能够处理jar包里的URL
    JarFile.registerUrlProtocolHandler();
    // 创建LaunchedURLClassLoader,传入getClassPathArchives(),getClassPathArchives返回所有的
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    // 用LaunchedURLClassLoader launcher启动main方法
    launch(args, getMainClass(), classLoader);
}

org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchives 这个方法会返回一个Archieve,里面包含内嵌Archieve

ExecutableArchiveLauncher在创建的时候,会创建createArchive,表示java -jar启动的那个jar包

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));
}

getNestedArchives(EntryFilter) ExecutableArchiveLauncher会过滤出BOOT-INF/classes文件夹和BOOT-INF/lib包下的所有的jar包

protected boolean isNestedArchive(Archive.Entry entry) {
    if (entry.isDirectory()) {
        return entry.getName().equals(BOOT_INF_CLASSES);
    }
    return entry.getName().startsWith(BOOT_INF_LIB);
}

然后用这个BOOT-INF/classes和BOOT-INF/lib包下的所有的jar包作为LaunchedURLClassLoader的classpath,LaunchedURLClassLoader继承与 URLClassLoader,classes文件和jar包就会作为url classpath,类加载的时候就能够按照classname找到对应的class文件。

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]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
    return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

springboot除了使用fatjar,还可以怎么启动

fatjar虽然方便,但是在容器镜像使用中却存在一些缺点。

FROM openjdk:8-jdk-alpine
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

例如使用上述的Dockerfile来生成我们的springboot项目镜像,有如下缺点

缺点1: 每次变更下载的镜像比较多,因为springboot中镜像是层级结构,每次新的镜像相比上次的app.jar都不相同不能服用,因此每次都需要拉取app.jar的镜像层次 缺点2: 启动较慢,fatjar在启动的时候,扫描class、解析jar较慢,相比exploded(解压之后使用)方式启动慢一些,在运行时没有区别。

解决方法,不使用fatjar。把lib依赖作为单独的一层,这样因为依赖在大部分情况下都是不会变化的,所以在拉取镜像时能够复用上一次拉取的结果。拉取速度加快。 启动时,扫描加载类会变快,因此启动速度相比fatjar也有提升。

FROM openjdk:8-jdk-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
ARG DEPENDENCY=target/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

Post Directory