spring-boot-loader模块让你的springboot应用具备打包为可执行jar或war文件的能力。只需要引入Maven插件或者Gradle插件就可以自动生成。
Java中并没有标准的方法加载嵌入式的jar文件,通常都是在一个jar文件中。这种情况下,如果你要通过命令行的形式发布一个没有打包的独立程序的话,可能会出现问题。
为了解决这种问题,很多开发人员将所有的class文件都打包为一个jar文件,然后依赖其他的jar文件。但是这种方式下,开发人员很难去判断哪个依赖的文件库是被程序真正使用到的。更普遍的问题是,在不同的jar文件中,如果有相同名称的文件则会冲突。(这里说明一下,在传统的可执行jar文件中会有/META-INF/MANIFEST.MF文件,这里主要介绍两个属性:Main-Class和classpath,Main-Class是可执行jar的启动类,classpath则可以指定依赖的类库。)
读者可以自行编写一个简单的springboot应用,然后用Maven插件打包。
example.jar
|
+-META-INF
| +-MANIFEST.MF
+-org
| +-springframework
| +-boot
| +-loader
| +-<spring boot loader classes>
+-BOOT-INF
+-classes
| +-mycompany
| +-project
| +-YourClasses.class
+-lib
+-dependency1.jar
+-dependency2.jar
熟悉Java EE的读者可能会发现,其中目录BOOT-INF下的classes和lib和WEB-INF下的classes和lib相似。但是为什么可以通过命令行的方式去执行Jar文件呢?
springboot应用可执行jar文件被java -jar 命令执行时,还是要按照Java官方文档的规定,命令的启动类必须配置在MANIFEST.MF文件的Main-class属性。
查看springboot应用jar包中的MANIFEST.MF发现:
Main-Class: org.springframework.boot.loader.JarLauncher
首先,在pom.xml中添加spring-boot-loader依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<scope>provided</scope>
</dependency>
在IDEA中,双击shift搜索Jarlauncher
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);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
从上面代码可以看出,BOOT- INF/classes/和BOOT-INF/lib/路径分别用常量BOOT INF_ CLASSES 和BOOT INF_ LIB表示,并且用于isNestedArchive(Archive. Entry)方法判断,从该方法的实现分析,方法参数Archive . Entry对象看似为JAR文件中的资源,比如application. properties,不过该对象与Java标准的java . util. jar . JarEntry对象类似,其name属性( getName()方法)为JAR资源的相对路径。当application. properties资源位于FAT JAR时,实际的Archive . Entry#getName()为/BOOT- INF/classes/application. properties,故符合entry. getName(). startsWith(BOOT_ INF_ LIB) 的判断,即isNestedArchive(Archive. Entry)方法返回true.反之,该方法返回false时,说明FAT JAR被解压至文件目录,因此从侧面说明了Spring Boot应用能直接通过java org. springframework. boot . loader .JarLauncher
命令启动的原因。换言之,Archive. Entry与java.util.jar .JarEntry也存在差异,它也可表示文件或目录。实际上,Archive . Entry存在两种实现,其中一种为 JarFileArchive.JarFileEntry,基于java.util. jar . JarEntry实现,表示FATJAR嵌入资源:
public class JarFileArchive implements Archive {
....
/**
* {@link Archive.Entry} implementation backed by a {@link JarEntry}.
*/
private static class JarFileEntry implements Entry {
private final JarEntry jarEntry;
JarFileEntry(JarEntry jarEntry) {
this.jarEntry = jarEntry;
}
public JarEntry getJarEntry() {
return this.jarEntry;
}
@Override
public boolean isDirectory() {
return this.jarEntry.isDirectory();
}
@Override
public String getName() {
return this.jarEntry.getName();
}
}
}
另一种实现是ExplodedArchive.FileEntry,基于文件系统实现:
public class ExplodedArchive implements Archive {
......
/**
* {@link Entry} backed by a File.
*/
private static class FileEntry implements Entry {
private final String name;
private final File file;
FileEntry(String name, File file) {
this.name = name;
this.file = file;
}
public File getFile() {
return this.file;
}
@Override
public boolean isDirectory() {
return this.file.isDirectory();
}
@Override
public String getName() {
return this.name;
}
}
}
从上面可以知道JarLauncher支持Jar和文件系统的两种启动方式。
同时,JarLauncher 同样作为引导类,当执行java -jar 命令时,/META-INF/ 资源的Main-Class属性将调用其main(String[])方法,实际上调用的是JarLauncher#launch(args)方法,而该方法继承于基类org. springframework . boot . loader .
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();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
这里主要看launch方法
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
该方法的执行者为MainMethodRunner#run()方法:
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 });
}
MainMethodRunner对象需要关联mainClass及main方法参数args,而mainClass来自于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;
}
到这里就可以知道,主要的mainMethodRunner关联的是MANIFEST.MF文件中Start-Class属性指定的类。
最后总结一下,首先是按照Java规范指定main-class(Spring boot中的JarLauncher),然后指定类加载器,加载并执行MANIFEST.MF文件中Start-Class属性指定类的main方法。