独立的Spring应用
之前的Spring web项目,都是Spring被动监听Tomcat(ContextLoaderListener
监听Servlet容器的启动),然后启动上下文。而Spring Boot搭建的应用却是自己当家做主,将Tomcat作为内嵌容器使用。
我们运行Spring Boot应用时,只需要通过如下两种方式:
-
开发环境
mvn spring-boot:run
-
线上环境
mvn package
获取可执行jar/war包java -jar ....war/jar
执行jar
从如上的描述,我们可以明显得知,Spring Boot应用是独立的Spring应用,Spring Boot应用尽在一个war/jar包中,一切都由Spring当家做主,而之前的Spring程序都是被打成一个war包放入Servlet容器中运行。
JarLauncher
我们将mvn package后的jar包解压,可得
这目录本身像极了一个web程序
我们主要看一下META-INF下的MANIFEST.MF文件
Manifest-Version: 1.0
Implementation-Title: first-app-by-gui
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: thinkinginspringboot.firstappbygui.FirstAppByGuiApplicati
on
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.7.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher
复制代码
-
Start-Class
对应的是Spring Boot程序的项目引导类 -
Main-Class
是可执行jar的启动器可执行jar 也就是执行了
mvn package
得到的jar包
org.springframework.boot.loader.JarLauncher
需要我们仔细研究一下
/**
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
* included inside a {@code /BOOT-INF/lib} directory and that application classes are
* included inside a {@code /BOOT-INF/classes} directory.
*
* @author Phillip Webb
* @author Andy Wilkinso
* @since 1.0.0n
*/
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
// entry.getName()获取的是jar文件夹中的相对路径
return entry.getName().startsWith(BOOT_INF_LIB);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
复制代码
Archive.Entry
可以被认为是JAR文件中的资源
其实重点在于JarLauncher
的main方法,它是什么时候被调用的。
在我们调用java -jar executable***.jar
命令时,/META-INF 中的资源文件的MAIN-CLASS(例如:
Main-Class: org.springframework.boot.loader.JarLauncher
)对应的类将调用自身的main(String[] args);方法。
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
复制代码
实际上JarLauncher
是调用了父类的父类的launch()
方法
public abstract class Launcher {
/**
* 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 {
JarFile.registerUrlProtocolHandler(); // URL协议的扩展
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
复制代码
JarFile.registerUrlProtocolHandler();
对于URL协议的扩展,不太了解,但是我们可以看下JarFile.registerUrlProtocolHandler()
方法
public static void registerUrlProtocolHandler() {
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
System.setProperty(PROTOCOL_HANDLER,
("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
resetCachedUrlHandlers();
}
复制代码
搜寻下resetCachedUrlHandlers();
方法:
private static void resetCachedUrlHandlers() {
try {
URL.setURLStreamHandlerFactory(null);
}
catch (Error ex) {
// Ignore
}
}
复制代码
继续往下:
public static void setURLStreamHandlerFactory(URLStreamHandlerFactory fac) {
// private static Object streamHandlerLock = new Object();
synchronized (streamHandlerLock) {
// static URLStreamHandlerFactory factory;
if (factory != null) {
throw new Error("factory already defined");
}
// SecurityManager是一个允许应用程序实现安全策略的类。
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkSetFactory();
}
handlers.clear();
factory = fac;
}
}
复制代码
这是synchronized来实现了线程安全。
Launcher.createClassLoader(getClassPathArchives());
创建一个类加载器
// Launcher
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]));
}
复制代码
ExecutableArchiveLauncher.getClassPathArchiives()
// ExecutableArchiveLauncher
@Override
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives);
return archives;
}
复制代码
在这里首先看下org.springframework.boot.loader.ExecutableArchiveLauncher
类中的this.archive
private final Archive archive;
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
复制代码
archive引用的对象是通过Launcher#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));
}
复制代码
Launcher.launch(args, getMainClass(), classLoader);
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
// 为当前线程设置一个ClassLoader
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
复制代码
在给定Archive File 和完全配置的ClassLoader情况下启动应用程序。
该方法的实际执行者为createMainMethodRunner(mainClass, args, classLoader).run();
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
复制代码
再去看MainMethodRunner
这个类
public class 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 = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
}
复制代码
总结:
其实到这里就是很简单,JarLauncher
就是对我们的jar包进行解析,先创建一个ClassLoader
,然后MainMethodRunner
这个工具类,调用其run()方法,使用创建好的ClassLoader类加载器加载我们Spring Boot程序中的引导类,然后获取其main方法,调用main()方法使之运行。这就是为什么一个Spring Boot程序为什么可以只写一个默认的main()方法就可以运行了.
@SpringBootApplication
public class FirstAppByGuiApplication {
public static void main(String[] args) {
SpringApplication.run(FirstAppByGuiApplication.class, args);
}
}
复制代码
至于Spring Boot引导启动类的启动流程,在此处先不做深入了解。
关于ClassLoader的创建以及其他原理,会在后续学习中将其理解。
ExecutableArchiveLauncher#getMainClass()
这里我们还需要啰嗦的是ExecutableArchiveLauncher#getMainClass()
方法,因为该方法的返回值会被作为MainMethodRunner
的构造函数的入参初始化一个MainMethodRunner
对象。
这是Launcher
的getMainClass()
方法,是一个空方法。
protected abstract String getMainClass() throws Exception;
我们看下子类ExecutableArchiveLauncher
中的实现
@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;
}
复制代码
Manifest-Version: 1.0
Implementation-Title: first-app-by-gui
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: thinkinginspringboot.firstappbygui.FirstAppByGuiApplicati
on
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.7.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher
复制代码
其实本质很简单,就是到META-INF下的资源文件中获取Start-Class: thinkinginspringboot.firstappbygui.FirstAppByGuiApplicati on
对应的启动类,也就是FirstAppByGuiApplication
。
总结
通过以上讨论,不难发现,Spring Boot为了逃离Tomcat等Servlet容器,直接将应用通过spring-boot-maven-plugin
插件打包成一个可执行war/jar包,然后java -jar执行的时候,是调用的JarLauncher
/WarLauncher
来引导启动的。