不得不讲SpringBoot 使用起来太方便了,它的外表轻巧简单,在企业级的应用系统中非常流行,已经成为java开发者必备技能。而它采用的one-jar的方案已经深入人心,其实one-jar技术早在2004年就已经被提出,除此之外spring boot的强大的自动配置类也是非常的受用,总之用过的童靴都会很感觉一个字“爽”。但是 SpringBoot它内部实现却非常的复杂,它常常把爱研究源码的读者绕的晕头转向。
目录
1. spring-boot-maven-plugin是个什么鬼
2. maven-shade-plugin和spring-boot-maven-plugin有何区别
3. JarLauncher
4. Archive
5. LaunchedURLClassLoader
6. WarLauncher
7. SpringBootApplication
1. spring-boot-maven-plugin是个什么鬼
如果你不知道从哪里开始,就按作者的思路向下看吧!SpringBoot在于打包时它使用了one-jar (也有很多人叫 FatJar)技术,它就是将所有的依赖 jar 包一起放进了最终的 jar 包中的 BOOT-INF/lib 目录中,当前应用系统的 class 被统一放到了 BOOT-INF/classes 目录中。
简单的讲:它可以实现将所有的依赖 jar 包及class 文件塞进统一的 jar 包中。如果你还不了解,但有一个onejar-maven-plugin你可能听说过,是不是有一种旧瓶换新药的感觉。
这里提个问题:META-INF目录和org/springframework/boot/loader目录有什么特别之处呢?
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
springboot-exaple-1.0-SNAPSHOT.jar
├── BOOT-INF
│ ├── classes
│ │ └── com
│ └── lib
│ ├── classmate-1.3.4.jar
│ ├── jackson-datatype-jdk8-2.9.6.jar
│ ├── jackson-datatype-jsr310-2.9.6.jar
│ ├── snakeyaml-1.19.jar
│ ├── spring-aop-5.0.9.RELEASE.jar
│ ├── spring-beans-5.0.9.RELEASE.jar
│ ├── spring-boot-2.0.5.RELEASE.jar
│ ├── spring-boot-autoconfigure-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-json-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-logging-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-tomcat-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-web-2.0.5.RELEASE.jar
│ ├── spring-context-5.0.9.RELEASE.jar
│ ├── spring-core-5.0.9.RELEASE.jar
│ ├── spring-expression-5.0.9.RELEASE.jar
│ ├── spring-jcl-5.0.9.RELEASE.jar
│ ├── spring-web-5.0.9.RELEASE.jar
│ ├── spring-webmvc-5.0.9.RELEASE.jar
│ ├── tomcat-embed-core-8.5.34.jar
│ ├── tomcat-embed-el-8.5.34.jar
│ ├── tomcat-embed-websocket-8.5.34.jar
│ └── validation-api-2.0.1.Final.jar
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── org.springframework
└── org
└── springframework
└── boot
└── loader
└── Launcher.class
└── JarLauncher.class
2. maven-shade-plugin和spring-boot-maven-plugin有何区别
META-INF 目录一定不陌生吧!基础知识提问:MANIFEST 文件有何用?
借助MANIFEST 文件可以直接使用 java -jar springboot-exaple-1.0-SNAPSHOT.jar 运行,而不用 java -classpath jar1:jar2:jar3... mainClassName 这么复杂的语法格式运行。
如果你对maven-shade-plugin有一定的了解(不了解的可以去看dubbo中的运用),二者本身没有可比性,这里讲的区别主要指的是:两者的 MANIFEST 文件的差异性。
// Generated by Maven Shade Plugin
Manifest-Version: 1.0
Implementation-Title: gs-spring-boot
Implementation-Version: 0.1.0
Built-By: qianwp
Implementation-Vendor-Id: org.springframework
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_191
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/gs-spring-boot
Main-Class: com.kxtx.Application// Generated by SpringBootLoader Plugin
Manifest-Version: 1.0
Implementation-Title: gs-spring-boot
Implementation-Version: 0.1.0
Built-By: qianwp
Implementation-Vendor-Id: org.springframework
Spring-Boot-Version: 2.0.5.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.kxtx.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_191
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/gs-spring-boot
but,spring-boot并没有指定classpath啊,它是怎么做的的呢?
对,这里要说道说道了,首先运行环境一般有两种:
- IDE中运行,你完全感知不到打包逻辑的,它会自动帮你下载Jar包、指定classpath(细心的会发现启动控制台有答案)
- 直接执行java -jar的话,这就是springboot自己的JarLauncher奥秘,它进行解包、用依赖包配置ClassLoader、用反射调用实际main函数
具体是如何做的的呢?
SpringBoot 将 jar 包中的 Main-Class 进行了替换,换成了 JarLauncher,还增加了一个 Start-Class 参数,这个参数对应的类才是真正的业务 main 方法入口。其实jar包在启动时实际上启动的是springboot自己的JarLauncher,通过这个JarLauncher去加载 lib 下的依赖,然后去启动 Start-Class 配置对应下的类。
Start-Class是怎么找到呢?
spring-boot-maven-plugin执行时会去找有@SpringBootApplication注解的类,如果找不到,那么就检测所有的类中有Main函数的,如果找到且只找到一个就皆大欢喜,否则就报错给你看。
public abstract class MainClassFinder {
private String getMainClassName() {
Set<MainClass> matchingMainClasses = new LinkedHashSet<MainClass>();
if (this.annotationName != null) {
for (MainClass mainClass : this.mainClasses) {
if (mainClass.getAnnotationNames().contains(this.annotationName)) {
matchingMainClasses.add(mainClass);
}
}
}
if (matchingMainClasses.isEmpty()) {
matchingMainClasses.addAll(this.mainClasses);
}
if (matchingMainClasses.size() > 1) {
throw new IllegalStateException(
"Unable to find a single main class from the following candidates "
+ matchingMainClasses);
}
return (matchingMainClasses.isEmpty() ? null
: matchingMainClasses.iterator().next().getName());
}
}
}
public class Repackager {
private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class";
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version";
private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib";
private static final String BOOT_CLASSES_ATTRIBUTE = "Spring-Boot-Classes";
private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 };
private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";
protected String findMainMethod(JarFile source) throws IOException {
return MainClassFinder.findSingleMainClass(source,
this.layout.getClassesLocation(), SPRING_BOOT_APPLICATION_CLASS_NAME);
}
}
3. JarLauncher
Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a /BOOT-INF/lib directory and that application classes are included inside a /BOOT-INF/classes directory.
Main-Class的启动类是JarLaucher(源于org/springframework/boot/loader目录) ,它创建了一个特殊的 ClassLoader,然后由这个 ClassLoader
这里提个问题:Archive(红色的部分)又是个什么鬼?
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
//源于org.springframework.boot.loader;
public class JarLauncher extends ExecutableArchiveLauncher {
protected JarLauncher(Archive archive) {
super(archive);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
public abstract class Launcher {
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
// 生成自定义ClassLoader
ClassLoader classLoader = this.createClassLoader(this.getClassPathArchives());
// 启动应用
this.launch(args, this.getMainClass(), classLoader);
}
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList(archives.size());
Iterator var3 = archives.iterator();
while(var3.hasNext()) {
Archive archive = (Archive)var3.next();
urls.add(archive.getUrl());
}
return this.createClassLoader((URL[])urls.toArray(new URL[urls.size()]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, this.getClass().getClassLoader());
}
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
//设置为当前线程上下文类加载器
Thread.currentThread().setContextClassLoader(classLoader);
this.createMainMethodRunner(mainClass, args, classLoader).run();
}
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
}
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = args != null ? (String[])args.clone() : null;
}
public void run() throws Exception {
Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
// 调用业务系统类(startClass)的main方法
mainMethod.invoke((Object)null, this.args);
}
}
4. Archive
Archive是spring boot中特有的对象,可以理解为:
- 归档文件,通常为tar/zip等格式压缩包,jar为zip格式归档文件
- 一个Archive可以是jar(JarFileArchive),可以是一个文件目录(ExplodedArchive),可以抽象为统一访问资源的逻辑层
public interface Archive extends Iterable<Archive.Entry> {
// 获取该归档的url
URL getUrl() throws MalformedURLException;
// 获取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF
Manifest getManifest() throws IOException;
// 获取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jar
List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
interface Entry {
boolean isDirectory();
String getName();
}
}
public class JarFileArchive implements Archive {
@Override
public URL getUrl() throws MalformedURLException {
if (this.url != null) {
return this.url;
}
return this.jarFile.getUrl();
}
@Override
public Manifest getManifest() throws IOException {
return this.jarFile.getManifest();
}
@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
List<Archive> nestedArchives = new ArrayList<>();
for (Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
}
}
public class ExplodedArchive implements Archive {}
springboot-exaple-1.0-SNAPSHOT.jar 既为一个JarFileArchive,springboot-exaple-1.0-SNAPSHOT.jar!/BOOT-INF/lib下的每一个jar包也是一个JarFileArchive。将springboot-exaple-1.0-SNAPSHOT.jar解压到目录springboot-exaple-1.0-SNAPSHOT后,则该目录就是一个ExplodedArchive。
创建archive在ExecutableArchiveLauncher ,它是JarLauncher 和WarLauncher的父类。
public abstract class ExecutableArchiveLauncher extends Launcher {
// 在自己所在的jar,并创建Archive
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
@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;
}
// 获取/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录对应的archive
@Override
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> archives = new ArrayList<>(
this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives);
return archives;
}
}
public abstract class Launcher {
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));
}
}
5. LaunchedURLClassLoader
新的问题来了,当 JVM 遇到一个不认识的类,BOOT-INF/lib 目录里又有那么多 jar 包,它是如何知道去哪个 jar 包里加载呢?
java中定义了URL的概念,对应的URLConnection,可以灵活地获取多种URL协议(http、 file、 ftp、 jar )下的资源,具体的可以看我之前分享的URL拓展协议。
每个jar都会对应一个url,如:jar:file:/data/springboot-exaple-1.0-SNAPSHOT/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/
jar中的资源对应的url,并以'!/'分割,如:jar:file:/data/springboot-exaple-1.0-SNAPSHOT/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class
关于jar中的资源对应的url,原始的 java.util.jar.JarFile 只支持一个'!/',而SpringBoot扩展了此协议,使其支持多个'!/',以实现jar in jar的资源。自定义URL的类格式为[pkgs].[protocol].Handler,具体实现参考JarFile.registerUrlProtocolHandler()。
spring如何读取SpringProxy.class呢?
会循环处理'!/'分隔符,从最上层出发,先构造springboot-exaple-1.0-SNAPSHOT.jar的JarFile,再构造spring-aop-5.0.4.RELEASE.jar的JarFile,最后构造指向SpringProxy.class的JarURLConnection,通过JarURLConnection的getInputStream方法获取SpringProxy.class内容。
ClassLoader 会在本地缓存包名和 jar包路径的映射关系。
public class LaunchedURLClassLoader extends URLClassLoader {
//入口
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Handler.setUseFastConnectionExceptions(true);
Class var3;
try {
try {
this.definePackageIfNecessary(name);
} catch (IllegalArgumentException var7) {
if (this.getPackage(name) == null) {
throw new AssertionError("Package " + name + " has already been defined but it could not be found");
}
}
var3 = super.loadClass(name, resolve);
} finally {
Handler.setUseFastConnectionExceptions(false);
}
return var3;
}
private void definePackageIfNecessary(String className) {
int lastDot = className.lastIndexOf(46);
if (lastDot >= 0) {
String packageName = className.substring(0, lastDot);
if (this.getPackage(packageName) == null) {
try {
this.definePackage(className, packageName);
} catch (IllegalArgumentException var5) {
if (this.getPackage(packageName) == null) {
throw new AssertionError("Package " + packageName + " has already been defined but it could not be found");
}
}
}
}
}
private void definePackage(String className, String packageName) {
try {
AccessController.doPrivileged(() -> {
String packageEntryName = packageName.replace('.', '/') + "/";
String classEntryName = className.replace('.', '/') + ".class";
URL[] var5 = this.getURLs();
int var6 = var5.length;
for(int var7 = 0; var7 < var6; ++var7) {
URL url = var5[var7];
try {
URLConnection connection = url.openConnection();
if (connection instanceof JarURLConnection) {
JarFile jarFile = ((JarURLConnection)connection).getJarFile();
if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null && jarFile.getManifest() != null) {
this.definePackage(packageName, jarFile.getManifest(), url);
return null;
}
}
} catch (IOException var11) {
}
}
return null;
}, AccessController.getContext());
} catch (PrivilegedActionException var4) {
}
}
public void clearCache() {
URL[] var1 = this.getURLs();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
URL url = var1[var3];
try {
URLConnection connection = url.openConnection();
if (connection instanceof JarURLConnection) {
this.clearCache(connection);
}
} catch (IOException var6) {
}
}
}
}
// java.util.jar.JarFile的拓展体,可以访问内部任何目录项或jar文件
public class JarFile extends java.util.jar.JarFile {
private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
private static final AsciiBytes META_INF = new AsciiBytes("META-INF/");
// 注册一个'java.protocol.handler.pkgs'属性以便URLStreamHandler可以处理jar urls
public static void registerUrlProtocolHandler() {
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
: handlers + "|" + HANDLERS_PACKAGE));
resetCachedUrlHandlers();
}
}
//jar-url处理器
public class Handler extends URLStreamHandler {
private static final String JAR_PROTOCOL = "jar:";
private static final String FILE_PROTOCOL = "file:";
private static final String SEPARATOR = "!/";//在处理如下URL时,会循环处理'!/'分隔符
private static final String CURRENT_DIR = "/./";
private static final Pattern CURRENT_DIR_PATTERN = Pattern.compile("/./");
private static final String PARENT_DIR = "/../";
private static final String[] FALLBACK_HANDLERS = new String[]{"sun.net.www.protocol.jar.Handler"};
private static final Method OPEN_CONNECTION_METHOD;
private static SoftReference<Map<File, JarFile>> rootFileCache;
private final JarFile jarFile;
private URLStreamHandler fallbackHandler;
}
//jar-url 读取文件
final class JarURLConnection extends java.net.JarURLConnection {
private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal();
private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException("Jar file or entry not found");
private static final IllegalStateException NOT_FOUND_CONNECTION_EXCEPTION;
private static final String SEPARATOR = "!/";
private static final URL EMPTY_JAR_URL;
private static final JarURLConnection.JarEntryName EMPTY_JAR_ENTRY_NAME;
private static final String READ_ACTION = "read";
}
这里思考一个问题:war是一个怎样的处理流程呢?
6. WarLauncher
首先将内嵌容器相关依赖设为provided,再重写SpringBootServletInitializer的configure方法。
@SpringBootApplication
public class WebApp extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(WebApp.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(WebApp.class);
}
}
它构建处理的结构大概是这样的:
springboot-exaple-1.0-SNAPSHOT.war
├── META-INF
│ └── MANIFEST.MF
├── WEB-INF
│ ├── classes
│ │ └── 应用程序
│ └── lib
│ └── 第三方依赖jar
│ └── lib-provided
│ └── 与内嵌容器相关的第三方依赖jar
└── org
└── springframework
└── boot
└── loader
└── springboot启动程序
MANIFEST.MF内容为:
Manifest-Version: 1.0
Start-Class: com.kxtx.Application
Main-Class: org.springframework.boot.loader.WarLauncher
WarLauncher实现,其实与JarLauncher并无太大差别。差别仅在于:JarLauncher在构建LauncherURLClassLoader时,会搜索BOOT-INF/classes目录及BOOT-INF/lib目录下jar,WarLauncher在构建LauncherURLClassLoader时,则会搜索WEB-INFO/classes目录及WEB-INFO/lib和WEB-INFO/lib-provided两个目录下的jar。
因此构建出的war便支持两种启动方式:
- 直接运行./springboot-exaple-1.0-SNAPSHOT.war start
- 部署到Tomcat容器中
spring boot提供的除了JarLauncher、WarLauncher 之外,还提供更为轻量的PropretiesLauncher。
7. SpringBootApplication
我们知道SpringBoot 深度依赖注解来完成配置的自动装配工作,它发明了几十个注解,你需要仔细阅读文档才能知道它是用来干嘛的。@SpringBootApplication
是一个复合注解,包括@ComponentScan
,和@SpringBootConfiguration
,@EnableAutoConfiguration。
@ComponentScan的功能其实就是自动扫描并加载符合条件的组件(比如@Component和@Repository等)或者bean定义,最终将这些bean定义加载到IoC容器中。可以通过basePackages等属性来细粒度的定制自动扫描的范围,因为默认不指定basePackages,这也就是为什么SpringBoot的启动类最好是放在root package下的原因。
@SpringBootApplication(scanBasePackages = {"com.example"})
public class WebApp {
public static void main(String[] args) {
SpringApplication.run(WebApp.class, args);
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration // springboot的大神器之一,其借助@import的帮助
@ComponentScan(excludeFilters = { // 扫描路径设置
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}
// 继承了Configuration,表示当前是注解类
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}
总结,EnableAutoConfiguration的强大神奇之处,三言两语无法道尽,在下一篇中工作原理介绍。