Spring-@Configuration注解简析
前言
Spring中的@Configuration
注解修饰的类被称为配置类,通过配置类可以向容器注册bean以及导入其它配置类,本篇文章将结合例子和源码对@Configuration
注解原理进行学习,并引出对Spring框架在处理配置类过程中起重要作用的ConfigurationClassPostProcessor
的讨论。
Springboot版本:2.4.1
正文
一. @Configuration注解简析
基于@Configuration
注解可以实现基于JavaConfig的方式来声明Spring中的bean,与之作为对比的是基于XML的方式来声明bean。由@Configuration
注解标注的类中所有由@Bean
注解修饰的方法返回的对象均会被注册为Spring容器中的bean,使用举例如下。
@Configuration
public class TestBeanConfig {@Bean
public TestBean testBean() {
return new TestBean();
}}
如上所示,Spring容器会将
TestBean
注册为Spring容器中的bean。由@Configuration
注解修饰的类称为Spring中的配置类,Spring中的配置类在Spring启动阶段会被先加载并解析为ConfigurationClass
,然后会基于每个配置类对应的ConfigurationClass
对象为容器注册BeanDefinition
,以及基于每个配置类中由@Bean
注解修饰的方法为容器注册BeanDefinition
,后续Spring也会基于这些BeanDefinition
向容器注册bean。关于BeanDefinition
的概念,可以参见Spring-BeanDefinition简析。在详细分析由
@Configuration
注解修饰的配置类是如何被解析为ConfigurationClass
以及最终如何被注册为BeanDefinition
前,得先探究一下Springboot的启动类,因为后续的分析会以Springboot的启动为基础,所以有必要先了解一下Springboot中的启动类。Springboot的启动类由
@SpringBootApplication
注解修饰,@SpringBootApplication
注解签名如下所示。@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
......
}
@SpringBootApplication
注解的功能主要由@SpringBootConfiguration
,@EnableAutoConfiguration
和@ComponentScan
实现,后两者与Springboot中的自动装配有关,关于Springboot实现自动装配,会在后续文章中学习,在这里主要关心@SpringBootConfiguration
注解。实际上,@SpringBootConfiguration
注解其实就是@Configuration
注解,@SpringBootConfiguration
注解的签名如下所示。@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
......
}
既然
@SpringBootConfiguration
注解等同于@Configuration
注解,那么相应的Springboot的启动类就是一个配置类,Springboot的启动类对应的BeanDefinition
会在准备Springboot容器阶段就注册到容器中,将断点打到SpringApplication#run()
方法中调用refreshContext()
方法这一行代码,而已知refreshContext()
这一行代码用于初始化容器,执行到refreshContext()
方法时容器已经完成了准备,此时看一下容器的数据,如下所示。文章图片
此时Springboot容器持有的
DefaultListableBeanFactory
中的beanDefinitionMap中已经存在了Springboot启动类对应的BeanDefinition
,在初始化Springboot容器阶段,Springboot启动类对应的BeanDefinition
会首先被处理,通过处理Springboot启动类对应的BeanDefinition
才会引入对其它配置类的处理。关于Springboot启动类,暂时了解到这里,下面再给出一张处理配置类的调用链,以供后续阅读参考。文章图片
本篇文章后续将从
ConfigurationClassPostProcessor
的postProcessBeanDefinitionRegistry()
方法开始,对由@Configuration
注解修饰的配置类的处理进行说明。二. ConfigurationClassPostProcessor处理配置类
通过第一节中的调用链可知,在Springboot启动时,初始化容器阶段会调用到
ConfigurationClassPostProcessor
来处理配置类,即由@Configuration
注解修饰的类。ConfigurationClassPostProcessor
是由Spring框架提供的bean工厂后置处理器,类图如下所示。文章图片
可知
ConfigurationClassPostProcessor
实现了BeanDefinitionRegistryPostProcessor
接口,同时BeanDefinitionRegistryPostProcessor
接口又继承于BeanFactoryPostProcessor
,所以ConfigurationClassPostProcessor
本质上就是一个bean工厂后置处理器。ConfigurationClassPostProcessor
实现了BeanDefinitionRegistryPostProcessor
接口定义的postProcessBeanDefinitionRegistry()
方法,在ConfigurationClassPostProcessor
中对该方法的注释如下。Derive further bean definitions from the configuration classes in the registry.直译过来就是:从注册表中的配置类派生进一步的bean定义。那么这里的注册表指的就是容器持有的
DefaultListableBeanFactory
,而Springboot框架在容器准备阶段就将Springboot的启动类对应的BeanDefinition
注册到了DefaultListableBeanFactory
的beanDefinitionMap中,所以注册表中的配置类指的就是Springboot的启动类(前文已知Springboot的启动类就是一个配置类),而派生进一步的bean定义,就是将Springboot启动类上@EnableAutoConfiguration
和@ComponentScan
等注解加载的配置类解析为BeanDefinition
并注册到DefaultListableBeanFactory
的beanDefinitionMap中。暂时不清楚在Springboot启动流程中,ConfigurationClassPostProcessor
的postProcessBeanDefinitionRegistry()
方法注释中提到的配置类是否会有除了Springboot启动类之外的配置类,欢迎留言讨论。即现在知道,
ConfigurationClassPostProcessor
的postProcessBeanDefinitionRegistry()
方法主要处理目标就是Springboot的启动类,通过处理Springboot启动类引出对其它配置类的处理,下面跟随源码,进行学习。postProcessBeanDefinitionRegistry()
方法如下所示。@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
//记录已经处理过的注册表id
this.registriesPostProcessed.add(registryId);
processConfigBeanDefinitions(registry);
}
postProcessBeanDefinitionRegistry()
方法会记录已经处理过的注册表id,防止同一注册表被重复处理。实际的处理逻辑在processConfigBeanDefinitions()
中,由于processConfigBeanDefinitions()
方法比较长,所以这里先把processConfigBeanDefinitions()
方法的处理流程进行一个梳理,如下所示。- 先把Springboot启动类的
BeanDefinition
从注册表(这里指DefaultListableBeanFactory
,后续如果无特殊说明,注册表默认指DefaultListableBeanFactory
)的beanDefinitionMap中获取出来; - 创建
ConfigurationClassParser
,解析Springboot启动类的BeanDefinition
,即解析@PropertySource
,@ComponentScan
,@Import
,@ImportResource
,@Bean
等注解并生成ConfigurationClass
,最后缓存在ConfigurationClassParser
的configurationClasses中; - 创建
ConfigurationClassBeanDefinitionReader
,解析所有ConfigurationClass
,基于ConfigurationClass
创建BeanDefinition
并缓存到注册表的beanDefinitionMap中。
processConfigBeanDefinitions()
方法源码如下。public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();
//从注册表中把Springboot启动类对应的BeanDefinition获取出来
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}//如果未获取到Springboot启动类对应的BeanDefinition,则直接返回
if (configCandidates.isEmpty()) {
return;
}configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}if (this.environment == null) {
this.environment = new StandardEnvironment();
}//创建ConfigurationClassParser以解析Springboot启动类及其引出的其它配置类
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
Set candidates = new LinkedHashSet<>(configCandidates);
Set alreadyParsed = new HashSet<>(configCandidates.size());
do {
StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
//ConfigurationClassParser开始执行解析
parser.parse(candidates);
parser.validate();
//将ConfigurationClassParser解析得到的ConfigurationClass拿出来
Set configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);
//创建ConfigurationClassBeanDefinitionReader,以基于ConfigurationClass创建BeanDefinition
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
//开始创建BeanDefinition并注册到注册表中
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);
processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end();
candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}
在
processConfigBeanDefinitions()
方法中,ConfigurationClassPostProcessor
将解析Sprngboot启动类以得到ConfigurationClass
的任务委托给了ConfigurationClassParser
,将基于ConfigurationClass
创建BeanDefinition
并注册到注册表的任务委托给了ConfigurationClassBeanDefinitionReader
,所以下面会对这两个步骤进行分析。首先是ConfigurationClassParser
解析Springboot启动类,其parse()
方法如下所示。public void parse(Set configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
//Springboot启动类对应的BeanDefinition为AnnotatedGenericBeanDefinition
//AnnotatedGenericBeanDefinition实现了AnnotatedBeanDefinition接口
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}//延迟处理DeferredImportSelector
this.deferredImportSelectorHandler.process();
}
由于Springboot启动类对应
的BeanDefinition
为AnnotatedGenericBeanDefinition
,而AnnotatedGenericBeanDefinition
实现了AnnotatedBeanDefinition
接口,所以继续看parse(AnnotationMetadata metadata, String beanName)
方法,如下所示。protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER);
}
继续看
processConfigurationClass()
方法,如下所示。protected void processConfigurationClass(ConfigurationClass configClass, Predicate filter) throws IOException {
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
}
return;
}
else {
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}SourceClass sourceClass = asSourceClass(configClass, filter);
do {
//实际开始处理配置类
sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);
this.configurationClasses.put(configClass, configClass);
}
在
processConfigurationClass()
方法中会调用doProcessConfigurationClass()
方法来实际的处理配置类的@ComponentScan
,@Import
,@Bean
等注解。在本节的论述中,其实一直是将Springboot启动类与其它配置类分开的,因为笔者认为Springboot启动类是一个特殊的配置类,其它配置类的扫描和加载均依赖Springboot启动类上的一系列注解(@ComponentScan
,@Import
等)。上述processConfigurationClass()
方法是一个会被递归调用的方法,第一次该方法被调用时,处理的配置类是Springboot的启动类,处理Springboot启动类时就会加载进来许多其它的配置类,那么这些配置类也会调用processConfigurationClass()
方法来处理,因为其它配置类上可能也会有一些@Import
,@Bean
等注解。这里只讨论第一次调用,即处理Springboot启动类的情况。doProcessConfigurationClass()
方法源码如下所示。@Nullable
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass, Predicate filter)
throws IOException {if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
processMemberClasses(configClass, sourceClass, filter);
}//处理@PropertySource注解
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}//处理@ComponentScan注解
Set componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
Set scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}//处理@Import注解
processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
//处理@ImportResource注解
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}//处理由@Bean注解修饰的方法
Set beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}processInterfaces(configClass, sourceClass);
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
return sourceClass.getSuperClass();
}
}return null;
}
doProcessConfigurationClass()
方法中对于每种注解的处理会在后续文章中介绍,本文暂时不讨论。在processConfigurationClass()
方法中处理完Springboot启动类之后,实际上此时只会将自定义bean(由@Component
,@Controller
,@Service
等注解修饰的类)对应的ConfigurationClass
,自定义配置类(由@Configuration
注解修饰的类)对应的ConfigurationClass
添加到ConfigurationClassParser
的configurationClasses
中,那么最为关键的各种starter中的配置类对应的ConfigurationClass
是在哪里添加的呢,回到ConfigurationClassParser
的parse()
方法,下面再给出其源码,如下所示。public void parse(Set configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
//这里处理完,ConfigurationClassParser的configurationClasses中只会有自定义bean和自定义配置类对应的ConfigurationClass
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}//这里处理完,starter中的配置类对应的ConfigurationClass才会添加到ConfigurationClassParser的configurationClasses中
this.deferredImportSelectorHandler.process();
}
因为Springboot扫描starter并处理其配置类是依赖启动类上的
@EnableAutoConfiguration
注解,@EnableAutoConfiguration
注解的功能由@Import(AutoConfigurationImportSelector.class)
实现,其中AutoConfigurationImportSelector
实现了DeferredImportSelector
接口,而DeferredImportSelector
表明需要被延迟处理,所以Springboot需要延迟处理AutoConfigurationImportSelector
,延迟处理的地方就在上述parse()
方法的最后一行代码,关于@Import
注解,后续文章中会对其进行分析,这里暂时不讨论。现在定义一个TestBeanConfig
配置类,在其中向容器注册TestBean
,同时再定义一个由@Component
注解修饰的TestComponent
,代码如下所示。@Configuration
public class TestBeanConfig {@Bean
public TestBean testBean() {
return new TestBean();
}}
public class TestBean {public TestBean() {
System.out.println("Initialize TestBean.");
}}
@Component
public class TestComponent {public TestComponent() {
System.out.println("Initialize TestComponent.");
}}
现在在
ConfigurationClassParser
的parse()
方法的this.deferredImportSelectorHandler.process();
这一行代码打断点,程序运行到这里时,ConfigurationClassParser
的configurationClasses如下所示。文章图片
可见此时configurationClasses中没有starter中的配置类对应的
ConfigurationClass
,往下执行一行,此时ConfigurationClassParser
的configurationClasses如下所示。文章图片
可见此时starter中的配置类对应的
ConfigurationClass
已经被加载,至此ConfigurationClassParser
解析Springboot启动类分析完毕。【Spring-@Configuration注解简析】现在分析
ConfigurationClassBeanDefinitionReader
解析所有ConfigurationClass
,并基于ConfigurationClass
创建BeanDefinition
并缓存到注册表的beanDefinitionMap中。首先是ConfigurationClassBeanDefinitionReader
的loadBeanDefinitions()
方法,如下所示。public void loadBeanDefinitions(Set configurationModel) {
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
for (ConfigurationClass configClass : configurationModel) {
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
}
}
在
loadBeanDefinitions()
方法中遍历每一个ConfigurationClass
并调用了loadBeanDefinitionsForConfigurationClass()
方法,继续看loadBeanDefinitionsForConfigurationClass()
方法,如下所示。private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}if (configClass.isImported()) {
//基于ConfigurationClass自身创建BeanDefinition并缓存到注册表中
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
//基于ConfigurationClass中由@Bean注解修饰的方法创建BeanDefinition并缓存到注册表中
loadBeanDefinitionsForBeanMethod(beanMethod);
}loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
上述
loadBeanDefinitionsForConfigurationClass()
方法中,除了将自身创建为BeanDefinition
外,还会将所有由@Bean
注解修饰的方法(如果有的话)创建为BeanDefinition
,所有创建的BeanDefinition
最后都会注册到注册表中,即缓存到DefaultListableBeanFactory
的beanDefinitionMap中。至此,ConfigurationClassBeanDefinitionReader
解析所有ConfigurationClass
的大致流程也分析完毕。总结 由
@Configuration
注解修饰的配置类结合@Bean
注解可以实现向容器注册bean的功能,同时也可以借助@ComponentScan
,@Import
等注解将其它配置类扫描到容器中。Springboot的启动类就是一个配置类,通过ConfigurationClassPostProcessor
处理Springboot启动类,可以实现将自定义的bean,自定义的配置类和各种starter中的配置类扫描到容器中,以达到自动装配的效果。推荐阅读
- Lombok基本注解之@SneakyThrows的作用
- 历时三个月,史上最详细的Spring注解驱动开发系列教程终于出炉了,给你全新震撼
- Spring|Spring @Cacheable注解类内部调用失效的解决方案
- Mybatis|Mybatis plus逻辑删除注解@TableLogic的使用
- Mybatis-Plus实体类注解方法与mapper层和service层的CRUD方法
- python|idea在创建实体类的时候自动加上lombok注解和时间作者等注释
- Java|Java Spring的使用注解开发详解
- Spring注解配置IOC|Spring注解配置IOC,DI的方法详解
- 关于@Autowired注解和静态方法及new的关系
- 事务注解失效的问题