Spring Boot 自動(dòng)裝配原理以及實(shí)踐
在當(dāng)今的軟件開(kāi)發(fā)領(lǐng)域,Spring Boot 以其強(qiáng)大的功能和便捷性成為了眾多開(kāi)發(fā)者的首選框架。而其中最為關(guān)鍵且令人著迷的特性之一,便是自動(dòng)裝配。自動(dòng)裝配猶如一把神奇的鑰匙,開(kāi)啟了高效開(kāi)發(fā)的大門(mén)。
在這篇文章中,我們將深入探究 Spring Boot 自動(dòng)裝配背后的原理。了解它是如何巧妙地將各種組件和功能無(wú)縫整合到我們的應(yīng)用程序中,使得開(kāi)發(fā)過(guò)程變得如此輕松和高效。同時(shí),我們也將通過(guò)實(shí)際的案例和實(shí)踐,親身體驗(yàn)自動(dòng)裝配在項(xiàng)目中的具體應(yīng)用和強(qiáng)大威力。讓我們一同踏上這場(chǎng)探索 Spring Boot 自動(dòng)裝配的精彩旅程,揭開(kāi)其神秘面紗,掌握這一核心技術(shù),為我們的開(kāi)發(fā)工作注入新的活力和效率。
一、自動(dòng)裝配兩個(gè)核心
1. @Import注解的作用
@Import說(shuō)Spring框架經(jīng)常會(huì)看到的注解,它可用于導(dǎo)入一個(gè)或者多個(gè)組件,是與<import/>配置等效的一個(gè)注解:
- 導(dǎo)入@Configuration類(lèi)下所有的@bean方法中創(chuàng)建的bean。
- 導(dǎo)入該注解指定的bean,例如@Import(AService.class),就會(huì)生成AService的bean,并將其導(dǎo)入到Spring容器中。
- 結(jié)合ImportSelector接口類(lèi)導(dǎo)入指定類(lèi),這個(gè)比較重點(diǎn)后文會(huì)會(huì)展開(kāi)介紹。
Indicates one or more component classes to import — typically @Configuration classes. Provides functionality equivalent to theelement in Spring XML. Allows for importing @Configuration classes, ImportSelector and ImportBeanDefinitionRegistrar implementations, as well as regular component classes (as of 4.2; analogous to AnnotationConfigApplicationContext. register).
2. 詳解ImportSelector
ImportSelector接口則是@Import的輔助者,如果我們希望可以選擇性的導(dǎo)入一些類(lèi),我們就可以繼承ImportSelector接口編寫(xiě)一個(gè)ImportSelector類(lèi),告知容器需要導(dǎo)入的類(lèi)。 我們以Spring Boot源碼中@EnableAutoConfiguration為例講解一下它的使用,它基于Import注解將AutoConfigurationImportSelector導(dǎo)入容器中:
//......
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
//......
}
這樣在IOC階段,Spring就會(huì)調(diào)用其selectImports方法獲取需要導(dǎo)入的類(lèi)的字符串?dāng)?shù)組并將這些類(lèi)導(dǎo)入容器中:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
//返回需要導(dǎo)入的類(lèi)的字符串?dāng)?shù)組
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
3. ImportSelector使用示例
可能上文的原理對(duì)沒(méi)有接觸源碼的讀者比較模糊,所以我們不妨寫(xiě)一個(gè)demo來(lái)了解一下這個(gè)注解。我們現(xiàn)在有一個(gè)需求,希望通過(guò)import注解按需將Student類(lèi)或者User類(lèi)導(dǎo)入容器中。首先我們看看user類(lèi)代碼,沒(méi)有任何實(shí)現(xiàn),代碼示例如下:
public class User {
}
Student 類(lèi)代碼同理,沒(méi)有任何實(shí)現(xiàn)僅僅做測(cè)試使用
public class Student {
}
完成測(cè)試類(lèi)的創(chuàng)建之后,我們就以用戶(hù)類(lèi)為例,創(chuàng)建UserConfig 代碼如下:
@Configuration
public class UserConfig {
@Bean
public User getUser() {
return new User();
}
}
然后編寫(xiě)ImportSelector 首先類(lèi),編寫(xiě)自己的導(dǎo)入邏輯,可以看到筆者簡(jiǎn)單實(shí)現(xiàn)了一個(gè)selectImports方法返回UserConfig的類(lèi)路徑。
public class CustomImportSelector implements ImportSelector {
privatestatic Logger logger = LoggerFactory.getLogger(CustomImportSelector.class);
/**
* importingClassMetadata:被修飾的類(lèi)注解信息
*/
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
logger.info("獲取到的注解類(lèi)型:{}",importingClassMetadata.getAnnotationTypes().toArray());
// 如果被CustomImportSelector導(dǎo)入的組件是類(lèi),那么我們就實(shí)例化UserConfig
if (!importingClassMetadata.isInterface()) {
returnnew String[] { "com.example.UserConfig" };
}
// 此處不要返回null
returnnew String[] { "com.example.StudentConfig" };
}
}
完成這些步驟我們就要來(lái)到最關(guān)鍵的一步了,在Spring Boot啟動(dòng)類(lèi)中使用@Import導(dǎo)入CustomImportSelector:
@SpringBootApplication
@Configuration
@Import(CustomImportSelector.class)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
為了測(cè)試我們編寫(xiě)這樣一個(gè)controller看看bean是否會(huì)導(dǎo)入到容器中
@RestController
publicclass MyController {
privatestatic Logger logger = LoggerFactory.getLogger(MyController.class);
@Autowired
private User user;
@RequestMapping("hello")
public String hello() {
logger.info("user:{}", user);
return"hello";
}
}
結(jié)果測(cè)試我們發(fā)現(xiàn)user不為空,說(shuō)明CustomImportSelector確實(shí)將UserConfig導(dǎo)入到容器中,并將User導(dǎo)入到容器中了。
4. 從源碼角度了解ImportSelector工作原理
我們以上文筆者所給出的UserConfig導(dǎo)入作為示例講解一下源碼的工作流程:
- 在Spring初始化容器階段,AbstractApplicationContext執(zhí)行invokeBeanFactoryPostProcessors開(kāi)始調(diào)用上下文中關(guān)于BeanFactory的處理器。
- 執(zhí)行到BeanDefinitionRegistryPostProcessor的處理,在循環(huán)過(guò)程中就會(huì)得到一個(gè)ConfigurationClassPostProcessor處理器它會(huì)拿到所有帶有@Import注解的類(lèi)
- 得到我們的啟動(dòng)類(lèi)由此執(zhí)行到我們所實(shí)現(xiàn)的CustomImportSelector得到要注入的配置類(lèi)。
- 將其放入beanDefinitionMap中讓Spring完成后續(xù)java bean的創(chuàng)建和注入:
對(duì)此我們給出入口源碼即AbstractApplicationContext的refresh()方法,它會(huì)調(diào)用一個(gè)invokeBeanFactoryPostProcessors(beanFactory);進(jìn)行bean工廠后置操作:
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
.........
//執(zhí)行bean工廠后置操作
invokeBeanFactoryPostProcessors(beanFactory);
........
}
}
步入代碼,可以看到容器會(huì)不斷遍歷各個(gè)postProcessor 即容器后置處理器,然后執(zhí)行他們的邏輯
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
.....
//執(zhí)行各個(gè)postProcessor 的邏輯
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
}
重點(diǎn)來(lái)了,遍歷過(guò)程中得到一個(gè)ConfigurationClassPostProcessor,這個(gè)類(lèi)就會(huì)得到我們的CustomImportSelector,然后執(zhí)行selectImports獲取需要導(dǎo)入的類(lèi)信息,最終會(huì)生成一個(gè)Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
如下圖所示可以看到configClasses就包含UserConfig
sharkChili
總結(jié)一下核心流程的時(shí)序圖
完成上述步驟后ConfigurationClassPostProcessor就會(huì)通過(guò)這個(gè)set集合執(zhí)行l(wèi)oadBeanDefinitions方法將需要的bean導(dǎo)入到容器中,進(jìn)行后續(xù)IOC操作:
//configClasses 中就包含了UserConfig類(lèi)
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);
//執(zhí)行 loadBeanDefinitions
this.reader.loadBeanDefinitions(configClasses);
二、Spring Boot自動(dòng)裝配原理(重點(diǎn))
了解了import原理后,我們了解Spring Boot自動(dòng)裝配原理也很簡(jiǎn)單了,我們不妨看看Spring Boot的@SpringBootApplication這個(gè)注解中包含一個(gè)@EnableAutoConfiguration注解,我們不妨點(diǎn)入看看,可以看到它包含一個(gè)@Import(AutoConfigurationImportSelector.class)注解,從名字上我們可以知曉這是一個(gè)ImportSelector的實(shí)現(xiàn)類(lèi)。
所以我們不妨看看它的selectImports邏輯,可以看到它會(huì)通過(guò)getAutoConfigurationEntry方法獲取需要裝配的類(lèi),然后通過(guò)StringUtils.toStringArray切割返回。所以我們不妨看看getAutoConfigurationEntry
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
查看getAutoConfigurationEntry方法,我們可以看到它通過(guò)getCandidateConfigurations獲取各個(gè)xxxxAutoConfigure,并返回結(jié)果:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//獲取所有xxxxAutoConfigure
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
//移除不需要的
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
//返回結(jié)果
returnnew AutoConfigurationEntry(configurations, exclusions);
}
而getCandidateConfigurations實(shí)際上是會(huì)通過(guò)一個(gè)loadSpringFactories方法,如下所示遍歷獲取所有含有META-INF/spring.factories的jar包
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
//解析這個(gè)配置文件獲取所有配置類(lèi)然后返回
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
.....
return result;
} catch (IOException var14) {
thrownew IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
最終結(jié)果過(guò)濾解析,回到我們上文說(shuō)的beanDefinitionMap中,最終通過(guò)IOC完成自動(dòng)裝配。
三、(實(shí)踐)落地通用日志組件
1. 需求介紹
微服務(wù)項(xiàng)目中,基于日志排查問(wèn)題是非常重要的手段,而日志屬于非功能范疇的一個(gè)職責(zé),所以我們希望將日志打印和功能解耦。AOP就是非常不錯(cuò)的手段,但是在每個(gè)服務(wù)中都編寫(xiě)一個(gè)切面顯然是非常不可取的。 所以我們希望通過(guò)某種手段會(huì)編寫(xiě)一個(gè)通用日志打印工具,只需一個(gè)注解即可實(shí)現(xiàn)對(duì)方法的請(qǐng)求響應(yīng)進(jìn)行日志打印。 所以我們這個(gè)例子仍然是利用自動(dòng)裝配原理編寫(xiě)一個(gè)通用日志組件。
2. 實(shí)現(xiàn)步驟
(1) 搭建工程
cloud-component-logging-starter,并引入我們需要的依賴(lài),如下所示,因?yàn)楣P者要對(duì)spring-web應(yīng)用進(jìn)行攔截所以用到的starter-web和aop模塊,以及為了打印響應(yīng)結(jié)果,筆者也用到hutool,完整的依賴(lài)配置如下所示:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
(2) 編寫(xiě)日志注解
如下所示,該注解的value用于記錄當(dāng)前方法要執(zhí)行的操作,例如某方法上@SysLog("獲取用戶(hù)信息"),當(dāng)我們的aop攔截到之后,就基于該注解的value打印該方法的功能。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SysLog {
/**
* 記錄方法要執(zhí)行的操作
*
* @return
*/
String value();
}
(3) 編寫(xiě)環(huán)繞切面邏輯
邏輯非常簡(jiǎn)單,攔截到了切面后若報(bào)錯(cuò)則打印報(bào)錯(cuò)的邏輯,反之打印正常請(qǐng)求響應(yīng)結(jié)果:
@Aspect
publicclass SysLogAspect {
privatestatic Logger logger = LoggerFactory.getLogger(SysLogAspect.class);
@Pointcut("@annotation(com.sharkChili.annotation.SysLog)")
public void logPointCut() {
}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//類(lèi)名
String className = joinPoint.getTarget().getClass().getName();
//方法名
String methodName = signature.getName();
SysLog syslog = method.getAnnotation(SysLog.class);
//獲取當(dāng)前方法進(jìn)行的操作
String operator =syslog.value();
long beginTime = System.currentTimeMillis();
Object returnValue = null;
Exception ex = null;
try {
returnValue = joinPoint.proceed();
return returnValue;
} catch (Exception e) {
ex = e;
throw e;
} finally {
long cost = System.currentTimeMillis() - beginTime;
if (ex != null) {
logger.error("業(yè)務(wù)請(qǐng)求:[類(lèi)名: {}][執(zhí)行方法: {}][執(zhí)行操作: {}][耗時(shí): {}ms][請(qǐng)求參數(shù): {}][發(fā)生異常]",
className, methodName, operator, joinPoint.getArgs(), ex);
} else {
logger.info("業(yè)務(wù)請(qǐng)求:[類(lèi)名: {}][執(zhí)行方法: {}][執(zhí)行操作: {}][耗時(shí): {}ms][請(qǐng)求參數(shù): {}][響應(yīng)結(jié)果: {}]",
className, methodName, operator, cost, joinPoint.getArgs(), JSONUtil.toJsonStr(returnValue));
}
}
}
}
(4) 編寫(xiě)配置類(lèi)
最后我們給出后續(xù)自動(dòng)裝配會(huì)掃描到的配置類(lèi),并基于bean注解創(chuàng)建SysLogAspect切面:
@Configuration
public class SysLogAutoConfigure {
@Bean
public SysLogAspect getSysLogAspect() {
return new SysLogAspect();
}
}
(5) 新建spring.factories
該配置文件,告知要導(dǎo)入Spring容器的類(lèi),內(nèi)容如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.sharkChili.config.SysLogAutoConfigure
(6) 服務(wù)測(cè)試
服務(wù)引入進(jìn)行測(cè)試,以筆者為例,方法如下
@SysLog("獲取用戶(hù)信息")
@GetMapping("getByCode/{accountCode}")
public ResultData<AccountDTO> getByCode(@PathVariable(value = "accountCode") String accountCode) {
log.info("遠(yuǎn)程調(diào)用feign接口,請(qǐng)求參數(shù):{}", accountCode);
return accountFeign.getByCode(accountCode);
}
請(qǐng)求之后輸出結(jié)果如下:
2023-02-16 00:08:08,085 INFO SysLogAspect:58 - 業(yè)務(wù)請(qǐng)求:[類(lèi)名: com.sharkChili.order.controller.OrderController][執(zhí)行方法: getByCode][執(zhí)行操作: 獲取用戶(hù)信息][耗時(shí): 892ms][請(qǐng)求參數(shù): [sharkChili]][響應(yīng)結(jié)果: {"data":{"accountCode":"sharkChili","amount":10000,"accountName":"sharkChili","id":1},"message":"操作成功","success":true,"status":100,"timestamp":1676477287856}]