在当今的软件开发领域,Spring Boot 以其强大的功能和便捷性成为了众多开发者的首选框架。而其中最为关键且令人着迷的特性之一,便是自动装配。自动装配犹如一把神奇的钥匙,开启了高效开发的大门。
在这篇文章中,我们将深入探究 Spring Boot 自动装配背后的原理。了解它是如何巧妙地将各种组件和功能无缝整合到我们的应用程序中,使得开发过程变得如此轻松和高效。同时,我们也将通过实际的案例和实践,亲身体验自动装配在项目中的具体应用和强大威力。让我们一同踏上这场探索 Spring Boot 自动装配的精彩旅程,揭开其神秘面纱,掌握这一核心技术,为我们的开发工作注入新的活力和效率。
一、自动装配两个核心
1. @Import注解的作用
@Import说Spring框架经常会看到的注解,它可用于导入一个或者多个组件,是与<import/>配置等效的一个注解:
- 导入@Configuration类下所有的@bean方法中创建的bean。
- 导入该注解指定的bean,例如@Import(AService.class),就会生成AService的bean,并将其导入到Spring容器中。
- 结合ImportSelector接口类导入指定类,这个比较重点后文会会展开介绍。
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的辅助者,如果我们希望可以选择性的导入一些类,我们就可以继承ImportSelector接口编写一个ImportSelector类,告知容器需要导入的类。 我们以Spring Boot源码中@EnableAutoConfiguration为例讲解一下它的使用,它基于Import注解将AutoConfigurationImportSelector导入容器中:
//......
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
//......
}
这样在IOC阶段,Spring就会调用其selectImports方法获取需要导入的类的字符串数组并将这些类导入容器中:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
//返回需要导入的类的字符串数组
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
3. ImportSelector使用示例
可能上文的原理对没有接触源码的读者比较模糊,所以我们不妨写一个demo来了解一下这个注解。我们现在有一个需求,希望通过import注解按需将Student类或者User类导入容器中。首先我们看看user类代码,没有任何实现,代码示例如下:
public class User {
}
Student 类代码同理,没有任何实现仅仅做测试使用
public class Student {
}
完成测试类的创建之后,我们就以用户类为例,创建UserConfig 代码如下:
@Configuration
public class UserConfig {
@Bean
public User getUser() {
return new User();
}
}
然后编写ImportSelector 首先类,编写自己的导入逻辑,可以看到笔者简单实现了一个selectImports方法返回UserConfig的类路径。
public class CustomImportSelector implements ImportSelector {
privatestatic Logger logger = LoggerFactory.getLogger(CustomImportSelector.class);
/**
* importingClassMetadata:被修饰的类注解信息
*/
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
logger.info("获取到的注解类型:{}",importingClassMetadata.getAnnotationTypes().toArray());
// 如果被CustomImportSelector导入的组件是类,那么我们就实例化UserConfig
if (!importingClassMetadata.isInterface()) {
returnnew String[] { "com.example.UserConfig" };
}
// 此处不要返回null
returnnew String[] { "com.example.StudentConfig" };
}
}
完成这些步骤我们就要来到最关键的一步了,在Spring Boot启动类中使用@Import导入CustomImportSelector:
@SpringBootApplication
@Configuration
@Import(CustomImportSelector.class)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
为了测试我们编写这样一个controller看看bean是否会导入到容器中
@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";
}
}
结果测试我们发现user不为空,说明CustomImportSelector确实将UserConfig导入到容器中,并将User导入到容器中了。
4. 从源码角度了解ImportSelector工作原理
我们以上文笔者所给出的UserConfig导入作为示例讲解一下源码的工作流程:
- 在Spring初始化容器阶段,AbstractApplicationContext执行invokeBeanFactoryPostProcessors开始调用上下文中关于BeanFactory的处理器。
- 执行到BeanDefinitionRegistryPostProcessor的处理,在循环过程中就会得到一个ConfigurationClassPostProcessor处理器它会拿到所有带有@Import注解的类
- 得到我们的启动类由此执行到我们所实现的CustomImportSelector得到要注入的配置类。
- 将其放入beanDefinitionMap中让Spring完成后续java bean的创建和注入:
对此我们给出入口源码即AbstractApplicationContext的refresh()方法,它会调用一个invokeBeanFactoryPostProcessors(beanFactory);进行bean工厂后置操作:
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
.........
//执行bean工厂后置操作
invokeBeanFactoryPostProcessors(beanFactory);
........
}
}
步入代码,可以看到容器会不断遍历各个postProcessor 即容器后置处理器,然后执行他们的逻辑
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
.....
//执行各个postProcessor 的逻辑
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
}
重点来了,遍历过程中得到一个ConfigurationClassPostProcessor,这个类就会得到我们的CustomImportSelector,然后执行selectImports获取需要导入的类信息,最终会生成一个Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
如下图所示可以看到configClasses就包含UserConfig
sharkChili
总结一下核心流程的时序图
完成上述步骤后ConfigurationClassPostProcessor就会通过这个set集合执行loadBeanDefinitions方法将需要的bean导入到容器中,进行后续IOC操作:
//configClasses 中就包含了UserConfig类
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);
//执行 loadBeanDefinitions
this.reader.loadBeanDefinitions(configClasses);
二、Spring Boot自动装配原理(重点)
了解了import原理后,我们了解Spring Boot自动装配原理也很简单了,我们不妨看看Spring Boot的@SpringBootApplication这个注解中包含一个@EnableAutoConfiguration注解,我们不妨点入看看,可以看到它包含一个@Import(AutoConfigurationImportSelector.class)注解,从名字上我们可以知晓这是一个ImportSelector的实现类。
所以我们不妨看看它的selectImports逻辑,可以看到它会通过getAutoConfigurationEntry方法获取需要装配的类,然后通过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方法,我们可以看到它通过getCandidateConfigurations获取各个xxxxAutoConfigure,并返回结果:
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);
//返回结果
returnnew AutoConfigurationEntry(configurations, exclusions);
}
而getCandidateConfigurations实际上是会通过一个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 {
//解析这个配置文件获取所有配置类然后返回
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);
}
}
}
最终结果过滤解析,回到我们上文说的beanDefinitionMap中,最终通过IOC完成自动装配。
三、(实践)落地通用日志组件
1. 需求介绍
微服务项目中,基于日志排查问题是非常重要的手段,而日志属于非功能范畴的一个职责,所以我们希望将日志打印和功能解耦。AOP就是非常不错的手段,但是在每个服务中都编写一个切面显然是非常不可取的。 所以我们希望通过某种手段会编写一个通用日志打印工具,只需一个注解即可实现对方法的请求响应进行日志打印。 所以我们这个例子仍然是利用自动装配原理编写一个通用日志组件。
2. 实现步骤
(1) 搭建工程
cloud-component-logging-starter,并引入我们需要的依赖,如下所示,因为笔者要对spring-web应用进行拦截所以用到的starter-web和aop模块,以及为了打印响应结果,笔者也用到hutool,完整的依赖配置如下所示:
<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) 编写日志注解
如下所示,该注解的value用于记录当前方法要执行的操作,例如某方法上@SysLog("获取用户信息"),当我们的aop拦截到之后,就基于该注解的value打印该方法的功能。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SysLog {
/**
* 记录方法要执行的操作
*
* @return
*/
String value();
}
(3) 编写环绕切面逻辑
逻辑非常简单,拦截到了切面后若报错则打印报错的逻辑,反之打印正常请求响应结果:
@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();
//类名
String className = joinPoint.getTarget().getClass().getName();
//方法名
String methodName = signature.getName();
SysLog syslog = method.getAnnotation(SysLog.class);
//获取当前方法进行的操作
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("业务请求:[类名: {}][执行方法: {}][执行操作: {}][耗时: {}ms][请求参数: {}][发生异常]",
className, methodName, operator, joinPoint.getArgs(), ex);
} else {
logger.info("业务请求:[类名: {}][执行方法: {}][执行操作: {}][耗时: {}ms][请求参数: {}][响应结果: {}]",
className, methodName, operator, cost, joinPoint.getArgs(), JSONUtil.toJsonStr(returnValue));
}
}
}
}
(4) 编写配置类
最后我们给出后续自动装配会扫描到的配置类,并基于bean注解创建SysLogAspect切面:
@Configuration
public class SysLogAutoConfigure {
@Bean
public SysLogAspect getSysLogAspect() {
return new SysLogAspect();
}
}
(5) 新建spring.factories
该配置文件,告知要导入Spring容器的类,内容如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.sharkChili.config.SysLogAutoConfigure
(6) 服务测试
服务引入进行测试,以笔者为例,方法如下
@SysLog("获取用户信息")
@GetMapping("getByCode/{accountCode}")
public ResultData<AccountDTO> getByCode(@PathVariable(value = "accountCode") String accountCode) {
log.info("远程调用feign接口,请求参数:{}", accountCode);
return accountFeign.getByCode(accountCode);
}
请求之后输出结果如下:
2023-02-16 00:08:08,085 INFO SysLogAspect:58 - 业务请求:[类名: com.sharkChili.order.controller.OrderController][执行方法: getByCode][执行操作: 获取用户信息][耗时: 892ms][请求参数: [sharkChili]][响应结果: {"data":{"accountCode":"sharkChili","amount":10000,"accountName":"sharkChili","id":1},"message":"操作成功","success":true,"status":100,"timestamp":1676477287856}]