springboot使用StringRedisTemplate springboot thinjar_spring boot

写在前面

源码这里。本文分析的是通过java -jarjar包方式启动,关于直接运行main函数启动过程可以参考这里

1:创建helloworld程序

1.1:创建maven项目

file->new->project,然后选择左侧的maven,选择jdk的版本为8,直接next创建。

1.2:配置依赖

<packaging>jar</packaging>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.10.RELEASE</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

<!-- 这个插件,可以将应用打包成一个可执行的jar包;-->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>2.2.5.RELEASE</version>
        </plugin>
    </plugins>
 </build>

我们最终要探究的可执行jar包就是通过插件spring-boot-maven-plugin打出来的。

1.3:创建启动类

@SpringBootApplication
public class HelloWorldMainApplication {

    public static void main(String[] args) {
        SpringApplication.run(HelloWorldMainApplication.class, args);
    }
}

1.4:创建测试controller

@Controller
public class HelloController {

    @ResponseBody
    @RequestMapping("/hello")
    public String hello(){
        return "Hello World!";
    }
}

2:生成并运行jar

2.1:生成jar

按照下图生成:

springboot使用StringRedisTemplate springboot thinjar_jar_02

2.2:运行jar

$ java -jar springboot-helloworld-1.0-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::       (v2.2.10.RELEASE)

2020-12-02 13:50:55.004  INFO 25496 --- [           main] dongshi.daddy.HelloWorldMainApplication  : Starting HelloWorldMainApplication v1.0-SNAPSHOT on jhp with PID 25496 (C:\Users\Administrator\Desktop\temp\springboot-helloworld-1.0-SNAPSHOT.jar started by Administrator in C:\Users\Administrator\Desktop\temp)
2020-12-02 13:50:55.006  INFO 25496 --- [           main] dongshi.daddy.HelloWorldMainApplication  : No active profile set, falling back to default profiles: default
2020-12-02 13:50:55.744  INFO 25496 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8083 (http)
2020-12-02 13:50:55.752  INFO 25496 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-12-02 13:50:55.753  INFO 25496 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.38]
2020-12-02 13:50:55.796  INFO 25496 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-12-02 13:50:55.796  INFO 25496 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 760 ms
2020-12-02 13:50:55.926  INFO 25496 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-12-02 13:50:56.039  INFO 25496 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8083 (http) with context path ''
2020-12-02 13:50:56.041  INFO 25496 --- [           main] dongshi.daddy.HelloWorldMainApplication  : Started HelloWorldMainApplication in 1.285 seconds (JVM running for 1.564)

2.3:访问测试

D:\program_files\Redis-x64-2.8.2402>curl http://localhost:8083/hello
Hello World!

3:jar包启动分析

3.1:入口分析

这里生成jar包的清单文件MENIFEST.MF如下:

Manifest-Version: 1.0
Implementation-Title: springboot-helloworld
Implementation-Version: 1.0-SNAPSHOT
Start-Class: dongshi.daddy.HelloWorldMainApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.2.5.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher

可以看到这里Main-Classs的值是org.springframework.boot.loader.JarLauncher,这就是通过jar包方式启动程序的入口了。其顶层的抽象父类是org.springframework.boot.loader.Launcher,该抽象类定义了如何通过一个归档文件(如jar包)来启动应用程序。

3.2:主函数

源码如下:

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

其中调用的launch(args)方法是在org.springframework.boot.loader.Launcher父类中,源码如下:

protected void launch(String[] args) throws Exception {
	// <1>
	JarFile.registerUrlProtocolHandler();
	// <2>
	ClassLoader classLoader = createClassLoader(getClassPathArchives());
	// <3>
	launch(args, getMainClass(), classLoader);
}

<1>处代码注册协议处理器,处理jar:的协议,用于最终类加载器从jar包归档文件中加载依赖的.class文件,<2>处代码创建类加载器,用于加载相关类,会使用到<1>中设置的协议处理器(在jarFileUrl.openConnection()的时候使用到)<3>处代码调用启动类,启动应用程序,启动类最终会通过反射调用应用程序的main函数启动应用程序(后面会分析到)

3.3:JarFile.registerUrlProtocolHandler

源码:

org.springframework.boot.loader.jar.JarFile#registerUrlProtocolHandler
public static void registerUrlProtocolHandler() {
	// <1>
	String handlers = System.getProperty(PROTOCOL_HANDLER, "");
	// <2>
	// <3>
	System.setProperty(PROTOCOL_HANDLER,
			("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
	// <4>
	resetCachedUrlHandlers();
}

先简单说下java.protocol.handler.pkgs环境变量的作用,我们知道,有很多的通信协议,比如http://,jar://,file://,ftp://等等,不同的协议对应的资源都会有一个地址,在jdk中使用类java.net.URL类来表示的,而不同协议对应的资源都需要专门的处理器类来进行获取和处理,比如我们这里就是要处理jar://对应的jar包资源。我们可以自定义处理器类,定义完毕之后通过环境变量java.protocol.handler.pkgs来设置自定义处理器类所在的包路径,而下面所作的事情就是设置处理jar://协议资源的处理器类所在的包,自定义处理器也比较简单,只需要继承java.net.URLStreamHandler抽象类就可以了,springboot的处理器其实就是可执行jar包中的org.springframework.boot.loader.jar.Handler。如下图:

springboot使用StringRedisTemplate springboot thinjar_spring boot_03

<1>处代码是获取名称为java.protocol.handler.pkgs的环境变量,其中第二个参数为默认值,<2>处代码是如果没有名称为java.protocol.handler.pkgs的属性则设置其值为org.springframework.boot.loader,该值正是上图资源处理器所在的包,<3>设置协议处理器,多个通过|分割。<4>处代码重置协议处理器,防止存在的jar:协议处理器覆盖自定义的协议处理器。源码如下:

private static void resetCachedUrlHandlers() {
	try {
		URL.setURLStreamHandlerFactory(null);
	}
	catch (Error ex) {
		// Ignore
	}
}

接下来我们继续看创建类加载器。

3.4:createClassLoader(getClassPathArchives())

所在的代码为:

org.springframework.boot.loader.Launcher#launch(java.lang.String[])
protected void launch(String[] args) throws Exception {
	...
	ClassLoader classLoader = createClassLoader(getClassPathArchives());
	...
}

源码:

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

我们先看方法getClassPathArchives()

3.4.1:getClassPathArchives()

该方法用于获取BOOT-INF/lib下所有的jar,以及BOOT-INF/classes下所有的应用程序.class和资源配置文件。位置在org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchives,源码如下:

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

<1>处代码使用的是java8的lambda表达式用法,其中代码this::isNestedArchive相当于代码:

new Archive.EntryFilter() {
	@Override
	public boolean matches(Archive.Entry entry) {
		return isNestedArchive(entry);
	}
}

其中isNestedArchive(entry)调用的代码如下:

org.springframework.boot.loader.JarLauncher#isNestedArchive
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
	// static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
	// 如果是目录只过滤BOOT-INF/classes/
	if (entry.isDirectory()) {
		return entry.getName().equals(BOOT_INF_CLASSES);
	}
	// 当这里不是目录,那就是文件了
	// 如果是文件则必须是BOOT-INF/lib/目录下的(jar包)
	return entry.getName().startsWith(BOOT_INF_LIB);
}

List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));这行代码的整体意思就是通过filterthis::isNestedArchive过滤this.archive获取最终符合要求的集合结果。其中getNestedArchives源码如下:

org.springframework.boot.loader.archive.JarFileArchive#getNestedArchives
// 这里的参数EntryFilter filter是通过lambda表达式this::isNestedArchive定义的匿名类
@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
	List<Archive> nestedArchives = new ArrayList<>();
	// this代表的是this.archive.getNestedArchives(this::isNestedArchive)中的
	// this.archive
	// 在循环中通过filter过滤entry,这里this可以循环的原因是该对象是可迭代的
	for (Entry entry : this) {
		if (filter.matches(entry)) {
			nestedArchives.add(getNestedArchive(entry));
		}
	}
	return Collections.unmodifiableList(nestedArchives);
}

其中归档文件archive的获取是通过属性org.springframework.boot.loader.ExecutableArchiveLauncher#archive来的,那么接下来的问题就变成了这个属性到底是怎么初始化的呢?我们单起一小部分来看下这个问题。

3.4.2:this.archives的初始化

初始化是通过org.springframework.boot.loader.ExecutableArchiveLauncher的构造函数完成的,源码如下:

org.springframework.boot.loader.ExecutableArchiveLauncher#ExecutableArchiveLauncher()
public ExecutableArchiveLauncher() {
	try {
		this.archive = createArchive();
	}
	catch (Exception ex) {
		throw new IllegalStateException(ex);
	}
}

其中方法createArchive();是在父类org.springframework.boot.loader.Launcher中的,源码如下:

protected final Archive createArchive() throws Exception {
	/* 获取root区域开始 */
	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);
	/* 获取root区域结束 */
	if (!root.exists()) {
		throw new IllegalStateException("Unable to determine code source archive from " + root);
	}
	// <1>
	return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}

我们来验证下获取root区域中最终获取的root到底是什么,首先在我们的helloworld应用程序中增加Launcher的依赖:

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

然后修改HelloWorldMainApplication,修改后代码如下:

@SpringBootApplication
public class HelloWorldMainApplication {

    public static void main(String[] args) throws URISyntaxException {
        ProtectionDomain protectionDomain = Launcher.class.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);
        System.out.println("root is: ");
        System.out.println(root);
        SpringApplication.run(HelloWorldMainApplication.class, args);

    }
}

之后重新打jar包,然后执行:

$ java -jar springboot-helloworld-1.0-SNAPSHOT.jar
root is:
C:\Users\Administrator\Desktop\springboot-helloworld-1.0-SNAPSHOT.jar
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::       (v2.2.10.RELEASE)
 ...

可以看到输出的结果是C:\Users\Administrator\Desktop\springboot-helloworld-1.0-SNAPSHOT.jar其实就是jar包自己。那么这个Archive又是什么呢?其实就是 <1>处代码的ExplodedArchive或者是JarFileArchive。我们来进一步验证代码org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchives中的代码List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));最终获取的归档文件都有哪些,执行如下操作,将HelloWorldMainApplication修改为如下代码:

@SpringBootApplication
public class HelloWorldMainApplication {

    public static void main(String[] args) throws URISyntaxException, IOException {
        ProtectionDomain protectionDomain = Launcher.class.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);
        System.out.println("root is: ");
        System.out.println(root);
        Archive archive = new JarFileArchive(root);
        //  List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
        /*List<Archive> archives = new ArrayList<>(archive.getNestedArchives(new Archive.EntryFilter() {
            @Override
            public boolean matches(Archive.Entry entry) {
                return HelloWorldMainApplication.isNestedArchive(entry);
            }
        }));*/
        List<Archive> archives
                = new ArrayList<>(archive.getNestedArchives(HelloWorldMainApplication::isNestedArchive));
        System.out.println("所有的归档信息是:");
        for (int i = 0; i < archives.size(); i++) {
            System.out.println(archives.get(i));
        }
        SpringApplication.run(HelloWorldMainApplication.class, args);

    }
    // 在可执行jar包中开发人员编写的代码编译而成的.class文件以及配置文件所在的目录
    // 原始对应的就是src/main/java和src/main/resources目录下的文件
    static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
    //在可执行jar包中的依赖jar所在的目录
    static final String BOOT_INF_LIB = "BOOT-INF/lib/";

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

}

同样重新生成jar包,然后执行,如下:

C:\Users\Administrator\Desktop\temp\springboot-helloworld-1.0-SNAPSHOT.jar
所有的归档信息是:
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/classes!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-starter-web-2.2.10.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-starter-2.2.10.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-2.2.10.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-autoconfigure-2.2.10.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-starter-logging-2.2.10.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/logback-classic-1.2.3.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/logback-core-1.2.3.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/slf4j-api-1.7.30.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/log4j-to-slf4j-2.12.1.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/log4j-api-2.12.1.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/jul-to-slf4j-1.7.30.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/jakarta.annotation-api-1.3.5.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-core-5.2.9.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-jcl-5.2.9.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/snakeyaml-1.25.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-starter-json-2.2.10.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/jackson-databind-2.10.5.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/jackson-annotations-2.10.5.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/jackson-core-2.10.5.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/jackson-datatype-jdk8-2.10.5.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/jackson-datatype-jsr310-2.10.5.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/jackson-module-parameter-names-2.10.5.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-starter-tomcat-2.2.10.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/tomcat-embed-core-9.0.38.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/tomcat-embed-el-9.0.38.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/tomcat-embed-websocket-9.0.38.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-starter-validation-2.2.10.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/jakarta.validation-api-2.0.2.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/hibernate-validator-6.0.20.Final.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/jboss-logging-3.4.1.Final.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/classmate-1.5.1.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-web-5.2.9.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-beans-5.2.9.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-webmvc-5.2.9.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-aop-5.2.9.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-context-5.2.9.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-expression-5.2.9.RELEASE.jar!/
jar:file:/C:/Users/Administrator/Desktop/temp/springboot-helloworld-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-loader-2.2.10.RELEASE.jar!/

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::       (v2.2.10.RELEASE)
 ...

从结果可以看到BOOT-INF/classes下的应用程序类和资源文件被封装为一个ArchiveBOOT-INF/lib下的每一个jar文件被封装为一个Archive。现在我们已经将所有的归档文件列表全部获取到了,接下就是本小结的主题,创建自定义类加器了。

3.4.3:createClassLoader

springboot使用StringRedisTemplate springboot thinjar_jar_04


源码如下:

protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
    // <1>
	List<URL> urls = new ArrayList<>(archives.size());
	// <2>
	for (Archive archive : archives) {
		urls.add(archive.getUrl());
	}
	// <3>
	return createClassLoader(urls.toArray(new URL[0]));
}

<1><2>处代码是将归档文件资源文件地址URLjar:协议的地址存储到list集合中,<3>使用URL集合来创建自定义的类加载器。其中createClassLoader源码如下:

protected ClassLoader createClassLoader(URL[] urls) throws Exception {
	return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

3.5:launch(args, getMainClass(), classLoader)

被调用位置:

org.springframework.boot.loader.Launcher#launch(java.lang.String[])
protected void launch(String[] args) throws Exception {
	...
	launch(args, getMainClass(), classLoader);
}

先来看下getMainClass(),源码如下:

org.springframework.boot.loader.ExecutableArchiveLauncher#getMainClass
@Override
protected String getMainClass() throws Exception {
	Manifest manifest = this.archive.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;
}

代码比较简单就是获取jar包中的MENIFEST.MF清单文件中的Start-Class,如下:

...
Start-Class: dongshi.daddy.HelloWorldMainApplication
...

最终结果是dongshi.daddy.HelloWorldMainApplication.接着来看launch方法.

3.5.1:launch

位置:

org.springframework.boot.loader.Launcher#launch(java.lang.String[])
protected void launch(String[] args) throws Exception {
	...
	launch(args, getMainClass(), classLoader);
}

源码如下:

org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
	// <1>
	Thread.currentThread().setContextClassLoader(classLoader);
	// <2>
	createMainMethodRunner(mainClass, args, classLoader).run();
}

<1>处代码设置类加载器为自定义的类加载器。<2>处代码是创建用于启动应用程序的类org.springframework.boot.loader.MainMethodRunner,并调用其run方法启动,源码如下:

org.springframework.boot.loader.Launcher#createMainMethodRunner
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
	return new MainMethodRunner(mainClass, args);
}

继续看构造函数:

public MainMethodRunner(String mainClass, String[] args) {
	// 赋值启动主类名称到全局变量
	this.mainClassName = mainClass;
	// 启动参数
	this.args = (args != null) ? args.clone() : null;
}

启动方法源码如下:

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

直接通过自定义的类加载器加载启动类dongshi.daddy.HelloWorldMainApplication,并通过反射调用main方法,最终启动。