【源码分析】springboot具体启动过程
1.将项目打包成jar包并运行
- 将应用打包成jar文件
在终端中运行./gradlew bootRun将应用打包成一个jar文件:demo-0.0.1-SNAPSHOT.jar
- 命令行运行一个jar项目
进入 jar文件所在目录,运行一下命令启动项目
java -jar demo-0.0.1-SNAPSHOT.jar
- 在命令行运行命令,查看jar包文件目录路
unzip demo-0.0.1-SNAPSHOT.jar
可以看到生成的jar包文件目录如下:
- BOOT-INF
- classes
- com
- application.properties
- lib
- classes
- META-INF
- MANIFEST.MF
- org
注意:
BOOT-INF:该目录是项目代码,源代码,配置文件和依赖包,这个可以理解
META-INF:内容如下
image.png
meta一般都是项目元信息,表示项目版本,名称等 description信息。为什么会有这6个记录呢?先保留疑问。
org:顺着目录打开,可以看到该org目录是org.springframework.boot.loader下的包,解压缩后被放置到了jar包中。
疑问:回到上边疑问,Start-Class
在项目我们知道是main方法所在类,但是Main-Class
呢?JarLancher的作用又是干什么?字面意思感觉也是项目开始类的意思。以及另外两个属性path也有疑问。
2. spring boot项目启动源码分析
- 在idea中扩展包中找到spring-boot-loader包,打开JarLancher类,
/**
* {@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 Wilkinson
*/
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);
}
}
分析: 该类继承了ExecutableArchiveLauncher抽象类,两个静态成员变量属性,原来jar包部分目录是JarLancher指定的,最后main方法,实际上是JarLancher对象将main方法的参数通过lanch方法加载。我们再往上看ExecutableArchiveLauncher抽象类,及跳转launch方法。
**
* Base class for executable archive {@link Launcher}s.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public abstract class ExecutableArchiveLauncher extends Launcher {
private final Archive archive;
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
protected ExecutableArchiveLauncher(Archive archive) {
this.archive = archive;
}
protected final Archive getArchive() {
return this.archive;
}
@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;
}
@Override
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> archives = new ArrayList<>(
this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives);
return archives;
}
/**
* Determine if the specified {@link JarEntry} is a nested item that should be added
* to the classpath. The method is called once for each entry.
* @param entry the jar entry
* @return {@code true} if the entry is a nested item (jar or folder)
*/
protected abstract boolean isNestedArchive(Archive.Entry entry);
/**
* Called to post-process archive entries before they are used. Implementations can
* add and remove entries.
* @param archives the archives
* @throws Exception if the post processing fails
*/
protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
}
}
分析:该类也是继承一个Lancher的抽象类,launch()方法就是在最顶层抽象类实现,查看该类的实现类,如下,这时候忽然明白,原来gradle 打包工具,jar包和war包是在这里定义的。
**
* Base class for launchers that can start an application with a fully configured
* classpath backed by one or more {@link Archive}s.
*
* @author Phillip Webb
* @author Dave Syer
*/
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);
}
/**
* Create a classloader for the specified archives.
* @param archives the archives
* @return the classloader
* @throws Exception if the classloader cannot be created
*/
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]));
}
/**
* Create a classloader for the specified URLs.
* @param urls the URLs
* @return the classloader
* @throws Exception if the classloader cannot be created
*/
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
/**
* Launch the application given the archive file and a fully configured classloader.
* @param args the incoming arguments
* @param mainClass the main class to run
* @param classLoader the classloader
* @throws Exception if the launch fails
*/
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
/**
* Create the {@code MainMethodRunner} used to launch the application.
* @param mainClass the main class
* @param args the incoming arguments
* @param classLoader the classloader
* @return the main method runner
*/
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
/**
* Returns the main class that should be launched.
* @return the name of the main class
* @throws Exception if the main class cannot be obtained
*/
protected abstract String getMainClass() throws Exception;
/**
* Returns the archives that will be used to construct the class path.
* @return the class path archives
* @throws Exception if the class path archives cannot be obtained
*/
protected abstract List<Archive> getClassPathArchives() throws Exception;
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));
}
}
解析:launch()中,ClassLoader classLoader = createClassLoader(getClassPathArchives());
该行代码主要做的就是将类归档文件作为参数,并加载到类加载器中。
@Override
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> archives = new ArrayList<>(
this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives);
return archives;
}
解析:Archiev代表jar文件归档对象,此时就是获取嵌套的归档对象即我们的项目文件,然后作为classloader的参数。
我们看到spring-boot-loader并没有直接将jar包复制到目录中,因为jar打包规范是将所有的jar包都解压缩后,不允许嵌套,如果将class文件打包成一个jar文件,该方式的缺点是文件目录混乱,重名时就会进行覆盖。但是我们看到在项目源文件中,有lib目录,这些是spring boot通过classloader达到了目的。
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
自定义类加载器加载BOOT-INF文件夹过程
1)getClassPathArchieves()
将子文件夹中的嵌套的jar文件获取到硬盘位置url
2)ClassLoader classLoader = createClassLoader(getClassPathArchives());
装载到自定义类加载器中,返回jar文件的类加载器对象,具体过程如下:
- 将class文件url放置到一个array中。
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]));
}
-
createClassLoader
负责通过url加载,进入到这个方法中,一个参数是我们的jar url,另一个是系统类加载器的class对象。真正加载的类加载器就是new LaunchedURLClassLoader()
,再进入到这个方法
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
- 可以看到都是在调用系统类加载器,
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
super进入:
public URLClassLoader(URL[] urls, ClassLoader parent) {
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.acc = AccessController.getContext();
ucp = new URLClassPath(urls, acc);
}
再进入super:
protected SecureClassLoader(ClassLoader parent) {
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
initialized = true;
}
再进入super,最终是系统类加载器
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
3)拿到加载class的classLoader对象后,下一步如何将这个加载器对象执行呢?就是第三行代码,launch()方法
参数:args,MANIFEST.MF属性的·Start-Class·属性,即我们main方法入口类;自定义类加载器
protected void launch(String[] args, String mainClass, ClassLoader classLoader):
throws Exception {
Thread.currentThread().setContextClassLoader(classLoader); ##这句话就是将类加载器替换为自定义类加载器
createMainMethodRunner(mainClass, args, classLoader).run();
}
获取到MANIFEST.MFStart-Class
属性,作为main class参数,
@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;
}
4)Thread.currentThread().setContextClassLoader(classLoader)
; 这句就是将类加载器替换为自定义类加载器
-
createMainMethodRunner(mainClass, args, classLoader).run();
做了什么呢?主要在run方法。
-
MainMethodRunner
只做了变量替换而已。
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;
}
-
run()
:[重点] :可以看到spring boot通过自定义classloader后,在Thread中获取到,通过反射的方式来运行对应的“main”方法,这种巧妙的办法加载应用入口其实不叫“main”也可以,只要入口名对应就行,之所以取“main”,因此可以在IDE中通过run的方式运行,否则取其他名字则不能。
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 });
}
以上就是spring boot加载类的启动过程,这也就是为什么org下是将spring-boot-loader复制到了org文件夹下,不能嵌套jar文件,而BOOT-INF却可以,就是自定义了类加载器。