spring boot的六大特性之一就是开发独立的应用,原因就是在spring boot中使用了内置容器,在开发web应用时不用再采用将应用打包成war包发布到外部容器中,实现了对应用生命周期的完全自主的控制。
开发spring应用
1、创建spring应用
在开发一个spring应用时由多种创建方式,最简单的就是从https://spring.io/的方式创建一个spring应用模板然后再根据业务需求添加相应的逻辑。
2、根据环境运行spring应用
最常见的环境分为两类:生产环境和开发环境,在开发环境中通过运行idea中的run方式即可运行该应用,也是在开发过程中最常见的一种方式,也可以采用在启动类的根目录下运行命令函参数java -jar的方式运行,当然第二种方式需要将应用单独打包然后根据jar包中的元信息(MATE-INF/MANIFEST.MF)加载Main-class指定的启动类:jarlauncher去启动start-class指定的spring应用。
1、Jarlauncher的作用
JarLauncher作为引导类,当执行java -jar命令去运行一个可执行的spring boot应用时,META -INF/资源的Main-Class属性调用main(String[] args)方法,而实际上调用的是Jarlauncher #launch方法。Jarlauncher作为jar的启动类用于装载项目引导类并添加其依赖,其所在的JAR文件的Maven GAV信息为org.springframework.boot:spring-boot-loader:2.0.2.RELEASE。
按照java官方文档规定,java -jar命令引导的具体启动类必须配置在MANIFEST.MF中的Main-class属性中。
而打包后的Main-class属性为:org.springframework.boot.loader.JarLauncher
public class JarLauncher extends ExecutableArchiveLauncher {
private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx";
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/");
};
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
// Only needed for exploded archives, regular ones already have a defined order
if (archive instanceof ExplodedArchive) {
String location = getClassPathIndexFileLocation(archive);
return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
}
return super.getClassPathIndex(archive);
}
private String getClassPathIndexFileLocation(Archive archive) throws IOException {
Manifest manifest = archive.getManifest();
Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
return (location != null) ? location : DEFAULT_CLASSPATH_INDEX_LOCATION;
}
@Override
protected boolean isPostProcessingClassPathArchives() {
return false;
}
@Override
protected boolean isSearchCandidate(Archive.Entry entry) {
return entry.getName().startsWith("BOOT-INF/");
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
通过new一个JarLauncher().launch(args)方式进行启动。
2、JarLauncher启动逻辑分析
2.1Jarlauncher类图
2.2 处理流程图
2.3 代码解读
public class Jarlauncher{
...
private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";
...
protected void launch(String[] args) throws Exception {
//判断是否以一个分解模式的方式运行,如果是则运行,否则只支持规范的jar文件从而选择跳过
if (!isExploded()) {
JarFile.registerUrlProtocolHandler();
}
//获取类加载器
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
//获取系统中的jar模型
String jarMode = System.getProperty("jarmode");
//加载启动引导类
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER :
getMainClass();
//启动应用
launch(args, launchClass, classLoader);
}
}
在JDK内部提供了一系列协议的实现用来支持文件(file)、HTTP、JAR等协议,这些实现存放在sun.net.www.protocol包下,类名都是以Handler结尾,而JarFile.registerUrlProtocolHandler();要做的就是用org.springframework.boot.loader.jar.Handler覆盖jdk中内建的对jar协议的实现。
覆盖原因:spring boot fat jar中除了包含传统的java jar中的资源外,还包含依赖的jar包。因此当执行java- jar命令时无法把spring boot fat jar文件当成class path,所以需要用springframework中提供的jar.Handler来进行覆盖。
进入JarFile.registerUrlProtocolHandler()
public static void registerUrlProtocolHandler() {
Handler.captureJarContextUrl();
//获取handers的属性信息
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
//如果handlers不存在或者为空则将springframework内部提供的handler加入进去进行,
//否则用|进行隔离后加入到该属性中,覆盖的方式就是如果存在自定义的handler处理某
//个协议就不采用内建的实现
System.setProperty(PROTOCOL_HANDLER,
((handlers == null || handlers.isEmpty()) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
//重置缓存
resetCachedUrlHandlers();
}
在将springframework提供的关于jar协议实现加入到Handlers中后,在获取类的加载器。获取的方式是通过迭代的方式从类路径中获取。在Launcher中声明,并在ExecutableAchiveLauncher中实现。
public class ExecutableArchiveLauncher{
...
@Override
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
Archive.EntryFilter searchFilter = this::isSearchCandidate;
Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
(entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
if (isPostProcessingClassPathArchives()) {
archives = applyClassPathArchivePostProcessing(archives);
}
return archives;
}
...
}
在isNestedAcgive(entry)方法中调用Jarlauncher#isNestedArchive()方法获取类路径和依赖的第三方jar包。
public class JarLauncher extends ExecutableArchiveLauncher {
...
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);
}
...
}
然后通过属性jarMode是否为空来获取启动引导类:
private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";
...
String jarMode = System.getProperty("jarmode");
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
public class ExecutableAchiveLauncher{
...
@Override
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
//关联Meta-INF中的start-class属性
mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
...
}
最后启动应用:
public class Launcher{
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(launchClass, args, classLoader).run();
}
}