SpringBoot深入理解
項目打包SpringBoot啟動過程
當使用打包時,會下載org-springframework-boot-loader的jar,並且不會放在lib存放的第三方jar包文件中,該jar包中有個JarLauncher.class文件中設置了jar包運行時的入口和打包后文件的結構(定義了BOOT-INF,META-INF中放什么數據)
使用java -jar 啟動項目時,因為META-INF中文件記載了啟動的順序
Manifest-Version: 1.0 #版本
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Start-Class: com.zyy.gradletest1.GradleTest1Application #項目的啟動器
Spring-Boot-Classes: BOOT-INF/classes/ #記錄編譯后文件存放地址
Spring-Boot-Lib: BOOT-INF/lib/ #記錄第三方jar包存放地址
Spring-Boot-Version: 2.3.0.RELEASE #SpringBoot版本
Main-Class: org.springframework.boot.loader.JarLauncher #標記了程序的入口,程序的入口定義類加載器去加載項目的啟動器
所以程序會直接進入JarLauncher.class中執行main方法
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);
}
}
JarLancher繼承了ExecutableArchiveLauncher,ExecutableArchiveLauncher抽象類是歸檔啟動器的父類,它有兩個子類
Lancher:是整個歸檔的基類(抽象類)
|
ExecutableArchiveLauncher
| |
JarLancher WarLancher
顧名思義jarLancher就是打包成jar的歸檔啟動類,WarLancher是War打包方式的歸檔啟動類。
當執行JarLancher的main方法時,會調用Lancher基類的launch方法,該方法注釋為:啟動應用程序。 此方法是初始入口點應該由一個子類被稱為public static void main(String[] args)方法。
使用SpringBoot自定義類加載器(LaunchedURLClassLoader)來加載打包好的Jar文件中BOOT-INF里面的類和lib
ClassLoader classLoader = createClassLoader(getClassPathArchives());
getClassPathArchives()方法:返回嵌套Archive S表示匹配指定過濾器條目。
意思就是:獲得需要自定義類加載器需要加載的類的路徑並返回(歸檔文件)
public abstract class ExecutableArchiveLauncher extends Launcher {
@Override
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> archives = new ArrayList<>(
/*this::isNestedArchive:確定指定JarEntry是應添加到類路徑嵌套項。 該方法被調用一次為每個條目。
說人話就是查看當前需要加載的路徑是否符合,如果符合返回true,這個方法調用的就是JarLancher類 中的isNestedArchive方法*/
this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives);
//返回Archive類型的集合,Archive是可以由{@link Launcher}啟動的檔案類,里面記錄的需要加載類的路徑
return archives;
}
}
補充知識:
正常的jar包加載的方式就是直接找到classpath下的頂層路徑加載(例如:org.springframework)
傳統的打包方式是將第三方包和自己寫的代碼打包在一塊,這就有可能出現,包名沖突之類的。為了解決這種問題,Spring的打包方式就是把第三方包和自己的代碼分開。但是此時就出現了一個問題,不能正常找到頂層的加載文件,那么spring就出現了org-springframework-boot-loader包,spring默認規定該包在打包時將該包放到classpath下(頂層路徑),首先加載該類中對應的Lancher類,然后通過該類創建的自定義類加載器去加載項目中其他的類。
准備去加載項目的主運行程序
launch(args, getMainClass(), classLoader);
getMainClass():獲得ExecutableArchiveLauncher.Archive類中的清單,Archive在創建自定義類加載器時,被構造方法初始化,初始化的路徑就是classpath路徑下的META-INF文件中的信息然后讀取對應的主程序入口的路徑(也就是META-INF文件中的信息)key為Start-Class
的值(就是項目主程序的啟動器)
public abstract class ExecutableArchiveLauncher extends Launcher {
private final Archive 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;
}
}
當所有參數准備完畢之后,進入launch方法中
public abstract class Launcher {
/**
* 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();
}
}
創建一個運行Main的縣城,將運行產參數和類加載器傳入進去
createMainMethodRunner(mainClass, args, classLoader)
/**
* Create the {@code MainMethodRunner} used to launch the application.
* @param mainClass the main class main方法運行的路徑
* @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);
}
當線程創建好之后!!!重點來了!!!!運行run方法
createMainMethodRunner(mainClass, args, classLoader).run();
public void run() throws Exception {
//獲得線程上下文取出類加載器,然后獲得要運行的main方法的路徑
Class<?> mainClass = Thread.currentThread().getContextClassLoader()
.loadClass(this.mainClassName);
//通過反射定位到main方法的位置
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
//調用對應的main方法(此時調用的就是SpringBoot項目的啟動器)
mainMethod.invoke(null, new Object[] { this.args });
}
注意:mainMethod.invoke(null, new Object[] { this.args });
為什么invoke第一個參數是空,因為main方法是static的,static不歸存與類中,它只是把類當做寄存的場所而已,原因是所有的方法必須存在與類中
jar包啟動時,使用-agentlib:jdwp遠程debug
什么是jdwep?
JDWP 是 Java Debug Wire Protocol 的縮寫,它定義了調試器(debugger)和被調試的 Java 虛擬機(target vm)之間的通信協議,可以項目在線運行時,遠程debug查看運行的流程,如果項目是部署在tomcat中運行,需要在tomcat中配置相關的啟動,才能使用遠程debug
啟動jar包時,使用命令
然后在idea中配置
注意:這種調試需要在配置之前在idea中有相同的代碼,然后打上斷點
注解
java.lang.annotation 提供了四種元注解,專門注解其他的注解(在自定義注解的時候,需要使用到元注解):
@Documented – 注解是否將包含在JavaDoc中
@Retention – 什么時候使用該注解
@Target – 注解用於什么地方
@Inherited – 是否允許子類繼承該注解
1.)@Retention – 定義該注解的生命周期
RetentionPolicy.SOURCE : 在編譯階段丟棄。這些注解在編譯結束之后就不再有任何意義,所以它們不會寫入字節碼。@Override, @SuppressWarnings都屬於這類注解。
RetentionPolicy.CLASS : 在類加載的時候丟棄。在字節碼文件的處理中有用。注解默認使用這種方式
RetentionPolicy.RUNTIME : 始終不會丟棄,運行期也保留該注解,因此可以使用反射機制讀取該注解的信息。我們自定義的注解通常使用這種方式。
2.)Target – 表示該注解用於什么地方。默認值為任何元素,表示該注解用於什么地方。可用的ElementType 參數包括
ElementType.CONSTRUCTOR: 用於描述構造器
ElementType.FIELD: 成員變量、對象、屬性(包括enum實例)
ElementType.LOCAL_VARIABLE: 用於描述局部變量
ElementType.METHOD: 用於描述方法
ElementType.PACKAGE: 用於描述包
ElementType.PARAMETER: 用於描述參數
ElementType.TYPE: 用於描述類、接口(包括注解類型) 或enum聲明
3.)@Documented – 一個簡單的Annotations 標記注解,表示是否將注解信息添加在java 文檔中。
4.)@Inherited – 定義該注釋和子類的關系
@Inherited 元注解是一個標記注解,@Inherited 闡述了某個被標注的類型是被繼承的。如果一個使用了@Inherited 修飾的annotation 類型被用於一個class,則這個annotation 將被用於該class 的子類。
@Configuration官方文檔
修飾一個類為配置類,將Bean注入到Spring容器中,在運行期間生成bean的定義
@Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
// instantiate, configure and return bean ...
}
}
@Configuration類通常使用AnnotationConfigApplicationContext或者支持web的AnnotationConfigWebApplicationContext的引導,來使用@Bean
//在test類中可以使用,或者直接使用@Autowired自動注入
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class);
ctx.refresh();
MyBean myBean = ctx.getBean(MyBean.class);
// use myBean ...
@PropertySource
@Configuration類可以使用@PropertySource注釋將屬性源貢獻給環境對象:
@Configuration
@PropertySource("classpath:/com/acme/app.properties")
public class AppConfig {
@Inject Environment env;
@Bean
public MyBean myBean() {
return new MyBean(env.getProperty("bean.name"));
}
}
@ComponentScan
@Configuration類不僅可以使用組件掃描引導,還可以自己使用@ComponentScan注釋配置組件掃描:
@Configuration
@ComponentScan("com.acme.app.services")
public class AppConfig {
// various @Bean definitions ...
}
@Import
@Configuration類可以使用@Import注釋組合,因為@Configuration對象在容器中被管理為Spring bean,所以導入的配置可能會被注入——例如,通過構造函數
注入:
@Configuration
public class DatabaseConfig {
@Bean
public DataSource dataSource() {
// instantiate, configure and return DataSource
}
}
@Configuration
@Import(DatabaseConfig.class)
public class AppConfig {
private final DatabaseConfig dataConfig;
public AppConfig(DatabaseConfig dataConfig) {
this.dataConfig = dataConfig;
}
@Bean
public MyBean myBean() {
// reference the dataSource() bean method
return new MyBean(dataConfig.dataSource());
}
}
@ContextConfiguration
組合多個@Configuration配置組件
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {AppConfig.class, DatabaseConfig.class})
public class MyTests {
@Autowired MyBean myBean;
@Autowired DataSource dataSource;
@Test
public void test() {
// assertions against myBean ...
}
}
@Component
修飾一個類,表明一個注解的類是“部件”。@Configuration就是基於該注解。
@Autowired
自動注入,按照類型來匹配。
@Profile
分別開發環境和生產環境的配置
application.properties 配置文件
spring.profiles.active=development
測試環境
@Profile("development")
@Configuration
public class EmbeddedDatabaseConfig {
@Bean
public DataSource dataSource() {
// instantiate, configure and return embedded DataSource
}
}
生產環境
@Profile("production")
@Configuration
public class ProductionDatabaseConfig {
@Bean
public DataSource dataSource() {
// instantiate, configure and return production DataSource
}
}
第二種方法
@Configuration
public class ProfileDatabaseConfig {
//測試環境
@Bean("dataSource")
@Profile("development")
public DataSource embeddedDatabase() { ... }
//生產環境
@Bean("dataSource")
@Profile("production")
public DataSource productionDatabase() { ... }
}
@ImportResource
導入Spring的配置文件,讓配置文件里面的內容生效;Spring Boot里面沒有Spring的配置文件,我們自己編寫的配置文件
-
例:@ImportResource(locations = {"classpath:beans.xml"})
-
但是SpringBoott推薦給容器中添加組件的方式;推薦使用全注解的方式
@EnableAutoConfiguration
啟用Spring應用程序上下文的自動配置(開啟自動配置spring),當你不需要加載某個自定義類時可以在yml文件中:spring.autoconfigure.exclude: XXXX 來排除某個類。
@ComponentScan
配置使用組件掃描指令與@ Configuration類。 提供與Spring XML的支持並行context:component-scan元件。它包含了annotation-config這個屬性。
SpringBoot啟動過程
從main中執行SpringApplication.run(GradleTest1Application.class, args);
@SpringBootApplication
public class GradleTest1Application {
public static void main(String[] args) {
SpringApplication.run(GradleTest1Application.class, args);
}
}
進入到SpirngApplication類中,然后在run方法中間接調用new SpringApplication的構造方法
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
SpringApplication(primarySources):源碼開始
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}
從這里開始,有SpringApplication構造方法中嵌套的源碼
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader; //資源加載器,因為上一次調用賦值為null,所以之這里時null
Assert.notNull(primarySources, "PrimarySources must not be null"); //斷言這里是否傳入的SpringBoot的啟動器是否為空
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); //將SpringBoot啟動器放入一個Set<Class<?>>中
/*WebApplicationType:Web應用程序可能類型的枚舉,里面有三個枚舉參數(查看Web是那種類型),deduceFromClasspath()方法中使用CLassUtils的class工具查看對應 的classpath路徑下是否有對應的配置,如果有返回對應的Web類型
注意:源碼在本代碼塊后第一個代碼塊中
*/
this.webApplicationType = WebApplicationType.deduceFromClasspath();
/*加載默認配置類(ApplicationContextInitializer是初始化所有自動配置類的類),掃描所有自動配置jar包中“META-INF/spring.factories”的配置類jar包(
主要有org.springframework.boot.autoconfigure、org.springframework.boot.context、org.springframework.boot.beans)
找到實現ApplicationContextInitializer接口的類,並實例化他們
提示:源碼在本代碼塊后第二個代碼塊中
*/
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
//和上一行代碼操作一致,只不過是在三個jar包中找到實現ApplicationListener.class類並實例化它們
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
//找到主要的應用程序類
this.mainApplicationClass = deduceMainApplicationClass();
/**
deduceMainApplicationClass();源碼說明:
private Class<?> deduceMainApplicationClass() {
try {
//直接new出一個運行異常,然后獲得堆信息
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
//在堆信息中找到main方法
for (StackTraceElement stackTraceElement : stackTrace) {
if ("main".equals(stackTraceElement.getMethodName())) {
//加載main方法,並返回class對象
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
}
*/
}
this.webApplicationType = WebApplicationType.deduceFromClasspath();源碼:
WebApplicationType:Web應用程序可能類型的枚舉,里面有三個枚舉參數(查看Web是那種類型),deduceFromClasspath()方法中使用CLassUtils的class工具查看對應 的classpath路徑下是否有對應的配置,如果有返回對應的Web類型
/**
* An enumeration of possible types of web application.
*
* @author Andy Wilkinson
* @author Brian Clozel
* @since 2.0.0
*/
public enum WebApplicationType {
/**
* The application should not run as a web application and should not start an
* embedded web server.
應用程序不應作為web應用程序運行,也不應該啟動嵌入式web服務器。
*/
NONE,
/**
* The application should run as a servlet-based web application and should start an
* embedded servlet web server.
應用程序應作為基於servlet的web應用程序運行,並應啟動嵌入式servlet web服務器。
*/
SERVLET,
/**
* The application should run as a reactive web application and should start an
* embedded reactive web server.
應用程序應該作為一個反應性web應用程序運行,並啟動一個嵌入式反應性web服務器。當前反應性web服務器在Spring5引進,它可以支持servlet容器,也可以支持servlet職位的容器
*/
REACTIVE;
static WebApplicationType deduceFromClasspath() {
//判斷類路徑是否存在,存在:true 不存在:false
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
}
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); 源碼:
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return getSpringFactoriesInstances(type, new Class<?>[] {});
}
/**
重點!!!!!!!!!
這個類是返回在org.springframework.boot.autoconfigure、org.springframework.boot.context、org.springframework.boot.beans這三個包中繼承type類型的所有類,
這個類在SpringApplication中經常使用!!!!!!!
*/
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
//獲得類加載器
ClassLoader classLoader = getClassLoader();
//獲得所有的自動配置類的路徑信息
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
/* SpringFactoriesLoader.loadFactoryNames(type, classLoader)說明和源碼
| | |
使用set集合為了確保惟一以防止重復,因為這里存放的是所有自動配置類(AutoConfiguration)
loadFactoryNames(type, classLoader));
參數說明: (type = ApplicationContextInitializer.class(記錄被加載類的配置信息,並初始化) classLoader = 系統加載器)
方法調用了loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader)的同名方法,
loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader)源碼:
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
//loadSpringFactories(classLoader):找到所有“META-INF/spring.factories”配置文件中的類
//getOrDefault(factoryTypeName, Collections.emptyList()); 過濾所有的類找出實現factoryTypeName的類並放入集合中 (factoryTypeName=ApplicationContextInitializer.class
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
在return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());中獲得所有的自動配置類的路徑信息
*/
//在獲得所有需要初始化的自動配置類后,初始化他們
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
//對類排序
AnnotationAwareOrderComparator.sort(instances);
//返回所有排序后的自動配置類集合
return instances;
}
到這里結束
run(args):源碼開始
/**
* Run the Spring application, creating and refreshing a new
運行Spring應用程序,創建並刷新一個新應用程序
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
//計時器(Spring創始人2001年寫的)
StopWatch stopWatch = new StopWatch();
//啟動計時器
stopWatch.start();
//ApplicationContext子類,封裝了配置和生命周期,防止ApplicationContext客戶端代碼看到它們
ConfigurableApplicationContext context = null;
//存放SpringApplication啟動運行時的異常
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
//以服務器模式運行,在系統可能缺少顯示設備、鍵盤或鼠標這些外設的情況下可以使用該模式,激活該模式:System.setProperty("java.awt.headless", "true");
configureHeadlessProperty();
/*SpringApplication運行時的監視器,每運行一次都會創建一個SpringApplicationRunListener的子類放入SpringApplicationRunListeners類中的List<SpringApplicationRunListener> 的集合中*/
SpringApplicationRunListeners listeners = getRunListeners(args);
/** | | | 源碼:
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args)); //找到繼承SpringApplicationRunListener類的所有類
}
*/
//開始啟動所有繼承SpringApplicationRunListener的監視器
listeners.starting();
try {
//加載默認參數,只是把args賦值給DefaultApplicationArguments類中的成員變量
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//配置環境,傳入所有監聽器和應用程序的參數
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
//配置忽略一些Bean信息
configureIgnoreBeanInfo(environment);
//向控制台打印Banner(就是那個大的Spring字體,我們可以在classpath目錄下創建一個banner.txt文件,把想要打印的東西放入進去就可以在程序啟動時打印出來)
Banner printedBanner = printBanner(environment);
//創建應用上下文
context = createApplicationContext();
//獲取Spring工廠的實例
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
//准備上下文,並輸出某些日志
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//刷新上下文,並輸出某個日志
refreshContext(context);
//刷新上下問后置處理
afterRefresh(context, applicationArguments);
//計時停止了
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}