在学习 @Import 这个注解时,小编在想一个问题,这个注解的作用是导入一个配置 Configuration 类,那到底什么地方会用到它呢?想到我们工程中也不会使用这个注解去导入配置呀,我们都是新建一个类 xxxxxxConfiguration.java ,然后直接在类里边把所有的 Bean 组件啥的都给声明了,下面的代码我们感觉似曾相识,哈哈。
/**
* xx配置类,里边会有n个bean
* @Author jiawei huang
* @Since 2019年8月26日
* @Version 1.0
*/
@Configuration
public class CustomConfig {
@Bean
public Marker zuulProxyMarkerBean() {
return new Marker();
}
......
}
复制代码
但你有没有想过一个问题,当配置类 CustomConfig 不在 @SpringBootApplication 所在包及其子包下时,它还能被装配进去吗?答案是不能。因为,它不在 springboot 默认扫描范围内。详情可查看 SpringBoot封装我们自己的Starter
我讲的到底有没有道理呢?让我们来做个实验。 UserConfig 用于配置 User 对象,它位于 com.example 包下, DemoApplication.java 位于 com.example.demo 包下,此时 SpringBoot 是没法扫描到 UserConfig 并注入 User 对象的。
UserConfig.java
/**
* @Author jiawei huang
* @Since 2019年8月26日
* @Version 1.0
*/
@Configuration
public class UserConfig {
@Bean
public User getUser() {
return new User();
}
}
复制代码
使用下面代码注入会报错:
@Autowired private User user; 复制代码
The injection point has the following annotations: - @org.springframework.beans.factory.annotation.Autowired(required=true) 复制代码
怎么办呢?解决办法有二种:
@ComponentScan("com.**")
@Import
方法一简单粗暴,看似没啥毛病,但这是建立在你知道 bean 对象的大概包路径的基础上的,第三方的jar包中的 bean 可并不是都是以 com 开头命名的,这就尴尬了。 在上面的路径结构基础上,我们在 DemoApplication.java 中加入 @Import(UserConfig.class) 这个注解即可解决问题。
另外, @Import 相当于Spring xml配置文件中的 <import /> 标签。
ImportSelector @Import 注释是让我们导入一组指定的配置类-- @Configuration 修饰的类,类名一旦指定,将全部被解析。相反, ImportSelector 将允许我们根据条件动态选择想导入的配置类,换句话说,它具有动态性。 ImportSelector 使用时,我们要创建一个类实现 ImportSelector 接口,并重写其中的 String[] selectImports(AnnotationMetadata importingClassMetadata); 方法。
假设我们想实现这样一个功能,我们创建一个 CustomImportSelector 类,当使用 CustomImportSelector 的元素是类时,我们返回 UserConfig 配置类,当使用 CustomImportSelector 的元素是类时,我们返回 StudentConfig 配置类。
UserConfig 和
StudentConfig 在
DemoApplication
的外层,否则,这两个配置类就会被spring默认解析到了。
/**
*
* @Author jiawei huang
* @Since 2019年8月26日
* @Version 1.0
*/
@Configuration
public class UserConfig {
@Bean
public User getUser() {
return new User();
}
}
/**
*
* @Author jiawei huang
* @Since 2019年8月26日
* @Version 1.0
*/
@Configuration
public class StudentConfig {
@Bean
public Student getStudent() {
return new Student();
}
}
@SpringBootApplication
// 1、很明显,这里CustomImportSelector修饰的是一个类,我们将会返回UserConfig
@Import(CustomImportSelector.class)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
/**
*
* @Author jiawei huang
* @Since 2019年8月19日
* @Version 1.0
*/
@RestController
public class MyController {
@Autowired(required = false)
private Student student;
@Autowired(required = false)
private User user;
@RequestMapping("/getStudent")
private String getStudent() {
return "student=[" + student + "],user=[" + user + "]";
}
}
/**
*
* @Author jiawei huang
* @Since 2019年8月26日
* @Version 1.0
*/
public class CustomImportSelector implements ImportSelector {
/**
* importingClassMetadata:被修饰的类注解信息
*/
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 注意,自定义注解这里是拿不到的
System.out.println(importingClassMetadata.getAnnotationTypes());
// 如果被CustomImportSelector导入的组件是类,那么我们就实例化UserConfig
if (!importingClassMetadata.isInterface()) {
return new String[] { "com.example.UserConfig" };
}
// 此处不要返回null
return new String[] { "com.example.StudentConfig" };
}
}
复制代码
打开浏览器,调用接口,得到如下返回,证明 Student 没有被注入成为bean,而 User 成功被注入
注解在Spring启动过程中在哪里被解析? Spring源码版本: 5.1.6.RELEASE
小编粗略debug了下源码,这2个注解的解析过程统一在 ConfigurationClassParser$DeferredImportSelectorGroupingHandler 类中的 processImports() 方法实现的,该方法大致源码如下:
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
if (importCandidates.isEmpty()) {
return;
}
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
// 1、如果该配置类被ImportSelector修饰,则当成ImportSelector进行处理
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
ParserStrategyUtils.invokeAwareMethods(
selector, this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(
configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
// 2、如果该配置类被ImportBeanDefinitionRegistrar修饰,则当成ImportBeanDefinitionRegistrar进行处理
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
ParserStrategyUtils.invokeAwareMethods(
registrar, this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
// 3、如果该配置类被Import修饰,则当成Import进行处理
else {
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
this.importStack.pop();
}
}
}
复制代码
从Spring启动开始,到执行注解解析,大致调用链路如下:
SpringApplication-refreshContext() -> AbstractApplicationContext-refresh()-postProcessBeanFactory() -> PostProcessorRegistrationDelegate-invokeBeanDefinitionRegistryPostProcessors() -> ConfigurationClassPostProcessor-processConfigBeanDefinitions() -> ConfigurationClassParser-parse() -> ConfigurationClassParser-processImports()
ConfigurationClassParser 是 Spring 提供的用于解析 @Configuration 的配置类,通过它将会得到一个 ConfigurationClass 对象列表。
其实一般在项目上,我们实在是用不到上面的注解。有时候知识我们学会了,但是我们总想不出一种应用场景来将技术给用上,好烦。其实并不是这样的,了解技术的来龙去脉,久而久之会给我们带来很多能力,比如编写更加优秀的代码,更容易看懂框架源码,框架上手快,bug解决速度快,牛逼吹起来会更有逼格。
但是,脱离需求,技术可能意义不是很大,接到一个需求,我们可以动动脑,看下这个需求能不能用上,就好比下面这张购物车实现图:
像这些商品数量的操作,我们完全可以使用redis的相关操作来实现,你却非要给我建一张表来存储,当然不是不可以,只是缓存更简单,更高效罢了。以用户id为key,商品id作为field,使用redis哈希这种数据结构即可解决。
小编觉得先不急着实现需求,可以先多动动脑筋,看看有什么技术点可以用到,再动手写代码。