草庐IT

SpringBoot的核心注解及自动配置原理

never debug 2023-09-16 原文

文章目录


1. 前言

        SpringBoot的自动配置原理是一个令人头疼的问题,经过一下午的学习写下这篇文章供大家参考,如果有雷同纯属巧合。文章中如果出现错误,欢迎大家指出,本人会随时修改。
        SpringBoot版本:2.7.5(注意版本号,不然跟文章中调试结果可能不一致)。

2. SpringBoot的核心注解

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

        我们在创建的springboot项目时都会写一个主启动类,每次启动项目时只要运行主启动类中的main方法即可。标注在主启动类上的 @SpringBootApplication注解就是springboot的核心注解,但该注解是一个合成注解,主要由下面三个注解组成:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)

@SpringBootConfiguration
        @SpringBootConfiguration注解的作用是标注该类是一个配置类。它与@Configuration注解的功能一致,区别是@SpringBootConfiguration是springboot中的注解,而@Configuration注解是spring中的注解。由下面的代码可以看出@SpringBootConfiguration是@Configuration的一个派生注解。

@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

@ComponentScan
        @ComponentScan注解的作用是扫描指定包及其子包中所有的类,将满村过滤器条件的类作为bean注册到spring容器中。默认情况下会扫描主启动类所在的包及其子包中所有的类。
@EnableAutoConfiguration
        @EnableAutoConfiguration注解的作用是开启自动配置功能。加上该注解,程序会加载META-INF/spring.factories文件中注册的各种AutoConfiguration类,当某个AutoConfiguration类满足指定的生效条件时,实例化AutoConfiguration类中定义的bean,最后注入到Spring容器中,这样就完成了依赖框架的自动配置。接下来的自动配置的讲解都是通过该注解进行展开。

3. SpringBoot的自动配置

@EnableAutoConfiguration:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

        @EnableAutoConfiguration注解主要由@AutoConfigurationPackage、@Import这两个注解组成。

3.1 @AutoConfigurationPackage注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}

Registrar类:

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
		@Override
		public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
			register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
		}

		@Override
		public Set<Object> determineImports(AnnotationMetadata metadata) {
			return Collections.singleton(new PackageImports(metadata));
		}
	}

        @AutoConfigurationPackage注解在底层使用@Import注解将Registrar导入到容器中。而Registrar类的作用是以编程的形式注册自动配置包名称,以记录包扫描的入口。那获取的是哪个包名呢?


        我们看registerBeanDefinitions方法中的metadata参数(该参数表示注解元信息),其中包括注解的位置等信息,而注解的位置正是在启动类上面,所以获取的包名正是启动类所在的包。看上面的图,该代码计算出来的结果就是主启动类所在的包。
        很多人在这里就有疑问了,启动类上面的注解是@SpringbootApplication,为什么能够获取该包名?其实metadata元信息中的注解位置指的是@AutoConfigurationPackage位置,而@SpringbootApplication是合成注解,里面包括了@EnableAutoConfiguration注解,而@EnableAutoConfiguration注解又包括了@AutoConfigurationPackage注解,所以metadata中的包名就是主启动类的包名。

3.2 @Import注解

        我们查看@EnableAutoConfiguration注解中第二个重要的注解@Import。其作用是将AutoConfigurationImportSelector类作为一个组件导入容器中。接下来我们对AutoConfigurationImportSelector类进行分析一下。

@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

        selectImports方法返回的数组中有哪些组件,那么程序就会导入哪些组件。组件的信息获取是通过getAutoConfigurationEntry方法进行获取的。

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
		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);
		return new AutoConfigurationEntry(configurations, exclusions);
	}

        通过对getAutoConfigurationEntry方法进行分析,我们可以看出该方法还是要调用getCandidateConfigurations方法来获取数据,然后移除重复的组件等一系列操作后在返回给selectImports方法,接下来我们继续分析getCandidateConfigurations方法。

	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		List<String> configurations = new ArrayList<>(
				SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
		ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
		Assert.notEmpty(configurations,
				"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
						+ "are using a custom packaging, make sure that file is correct.");
		return configurations;
	}
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
        ClassLoader classLoaderToUse = classLoader;
        if (classLoader == null) {
            classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
        }

        String factoryTypeName = factoryType.getName();
        return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
    }
  private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        Map<String, List<String>> result = (Map)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            Map<String, List<String>> result = new HashMap();
            try {
                Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");

                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
                    UrlResource resource = new UrlResource(url);
                    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();

                    while(var6.hasNext()) {
                        Map.Entry<?, ?> entry = (Map.Entry)var6.next();
                        String factoryTypeName = ((String)entry.getKey()).trim();
                        String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        String[] var10 = factoryImplementationNames;
                        int var11 = factoryImplementationNames.length;

                        for(int var12 = 0; var12 < var11; ++var12) {
                            String factoryImplementationName = var10[var12];
                            ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
                                return new ArrayList();
                            })).add(factoryImplementationName.trim());
                        }
                    }
                }

        为了不讲废话了,直接将所有代码粘出来一起分析。整体逻辑就是getCandidateConfigurations方法调用loadFactoryNames方法,然后loadFactoryNames方法调用loadSpringFactories方法得到的,我们直接分析loadSpringFactories即可。
        我们从loadSpringFactories方法的第8行可以知道,组件的获取是扫描META-INF文件夹下的spring.factories得到的。
结果:
spring.factories文件:

configuration数量:

        哈哈哈,是不是发现一个新的问题。。。没错,spring.factories文件中远远没有144个。经过我的努力,终于找到了问题所在。在springboot2.7.5的版本中,需要加载的自动配类已经放到META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中,而不是写在spring.factories文件中。
官网原文:


        在这个文件中刚好有144条需要加载的配置类。

4. 按需开启自动配置

        上面讲解了springboot如何加载所有的自动配置类,但在实际上,并不会加载所有的自动配置类,而是按需加载。如何按需加载呢,我们举两个例子。

4.1 以AopAutoConfiguration为例

@AutoConfiguration
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(Advice.class)
	static class AspectJAutoProxyingConfiguration {
		@Configuration(proxyBeanMethods = false)
		@EnableAspectJAutoProxy(proxyTargetClass = false)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
		static class JdkDynamicAutoProxyConfiguration {
		}
		@Configuration(proxyBeanMethods = false)
		@EnableAspectJAutoProxy(proxyTargetClass = true)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
				matchIfMissing = true)
		static class CglibAutoProxyConfiguration {
		}
	}
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnMissingClass("org.aspectj.weaver.Advice")
	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
			matchIfMissing = true)
	static class ClassProxyingConfiguration {

		@Bean
		static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() {
			return (beanFactory) -> {
				if (beanFactory instanceof BeanDefinitionRegistry) {
					BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
					AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
					AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
				}
			};
		}

	}
}

        在AopAutoConfiguration类上面加上了@ConditionalOnProperty注解,表明配置文件中如果存在spring.aop.auto=true,那么则加载该配置类,matchIfMissing = true表示就算配置文件没有配置,该配置类也生效。
        上面的第5行代码(@ConditionalOnClass(Advice.class))表示如果Advice类存在,那么AspectJAutoProxyingConfiguration配置类则生效;上面第20行代码(@ConditionalOnMissingClass(“org.aspectj.weaver.Advice”))表示Advice类不存在则ClassProxyingConfiguration配置类生效。
        由此可知,当没有导入aspectj相关的jar包时,springboot中AOP默认使用JDK动态代理来实现。

4.2 以BatchAutoConfiguration为例

@AutoConfiguration(after = HibernateJpaAutoConfiguration.class)
@ConditionalOnClass({ JobLauncher.class, DataSource.class })
@ConditionalOnBean(JobLauncher.class)
@EnableConfigurationProperties(BatchProperties.class)
@Import({ BatchConfigurerConfiguration.class, DatabaseInitializationDependencyConfigurer.class })
public class BatchAutoConfiguration {
}

        如上面的代码(第二第三行代码),当JobLauncher类存在且JobLauncher对应的bean存在于容器时,该配置类才会生效,否则不会开启该配置。
        总结:在springboot中默认会加载所有的配置类且开启按需自动配置类,自动配置功能是通过@ConditionalOnClass、@ConditionalOnProperty和@ConditionalOnMissingClass等一系列注解实现的。

5. 总结

  • SpringBoot中的核心注解是@SpringBootApplication,不过该注解是一个合成注解,主要是由@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan等三个注解组成。
  • SpringBoot的自动配置是通过扫描spring-boot-configuration中META-INF文件夹下的spring.factories文件,然后加载对应的配置类来实现的。
  • SpringBoot的自动配置不会加载所有的配置类,而是按需配置,主要是通过@ConditionalOnClass、@ConditionalOnProperty和@ConditionalOnMissingClass等一系列注解实现的。

注意:在以前的版本中,SpringBoot会扫描META-INF文件夹下的spring.factories文件,在SpringBoot2.7.5(从哪个版本开始不知道)中改成了spring文件夹下的org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。

参考:【尚硅谷】SpringBoot2零基础入门教程

有关SpringBoot的核心注解及自动配置原理的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby-on-rails - 独立 ruby​​ 脚本的配置文件 - 2

    我有一个在Linux服务器上运行的ruby​​脚本。它不使用rails或任何东西。它基本上是一个命令行ruby​​脚本,可以像这样传递参数:./ruby_script.rbarg1arg2如何将参数抽象到配置文件(例如yaml文件或其他文件)中?您能否举例说明如何做到这一点?提前谢谢你。 最佳答案 首先,您可以运行一个写入YAML配置文件的独立脚本:require"yaml"File.write("path_to_yaml_file",[arg1,arg2].to_yaml)然后,在您的应用中阅读它:require"yaml"arg

  3. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  4. Ruby Sinatra 配置用于生产和开发 - 2

    我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm

  5. Vscode+Cmake配置并运行opencv环境(Windows和Ubuntu大同小异) - 2

    之前在培训新生的时候,windows环境下配置opencv环境一直教的都是网上主流的vsstudio配置属性表,但是这个似乎对新生来说难度略高(虽然个人觉得完全是他们自己的问题),加之暑假之后对cmake实在是爱不释手,且这样配置确实十分简单(其实都不需要配置),故斗胆妄言vscode下配置CV之法。其实极为简单,图比较多所以很长。如果你看此文还配不好,你应该思考一下是不是自己的问题。闲话少说,直接开始。0.CMkae简介有的人到大二了都不知道cmake是什么,我不说是谁。CMake是一个开源免费并且跨平台的构建工具,可以用简单的语句来描述所有平台的编译过程。它能够根据当前所在平台输出对应的m

  6. ruby-on-rails - 从应用程序中自定义文件夹内的命名空间自动加载 - 2

    我们目前正在为ROR3.2开发自定义cms引擎。在这个过程中,我们希望成为我们的rails应用程序中的一等公民的几个类类型起源,这意味着它们应该驻留在应用程序的app文件夹下,它是插件。目前我们有以下类型:数据源数据类型查看我在app文件夹下创建了多个目录来保存这些:应用/数据源应用/数据类型应用/View更多类型将随之而来,我有点担心应用程序文件夹被这么多目录污染。因此,我想将它们移动到一个子目录/模块中,该子目录/模块包含cms定义的所有类型。所有类都应位于MyCms命名空间内,目录布局应如下所示:应用程序/my_cms/data_source应用程序/my_cms/data_ty

  7. 神州数码无线产品(AC+AP)配置 - 2

    注意:本文主要掌握DCN自研无线产品的基本配置方法和注意事项,能够进行一般的项目实施、调试与运维AP基本配置命令AP登录用户名和密码均为:adminAP默认IP地址为:192.168.1.10AP默认情况下DHCP开启AP静态地址配置:setmanagementstatic-ip192.168.10.1AP开启/关闭DHCP功能:setmanagementdhcp-statusup/downAP设置默认网关:setstatic-ip-routegeteway192.168.10.254查看AP基本信息:getsystemgetmanagementgetmanaged-apgetrouteAP配

  8. hadoop安装之保姆级教程(二)之YARN的配置 - 2

    1.1.1 YARN的介绍 为克服Hadoop1.0中HDFS和MapReduce存在的各种问题⽽提出的,针对Hadoop1.0中的MapReduce在扩展性和多框架⽀持⽅⾯的不⾜,提出了全新的资源管理框架YARN. ApacheYARN(YetanotherResourceNegotiator的缩写)是Hadoop集群的资源管理系统,负责为计算程序提供服务器计算资源,相当于⼀个分布式的操作系统平台,⽽MapReduce等计算程序则相当于运⾏于操作系统之上的应⽤程序。 YARN被引⼊Hadoop2,最初是为了改善MapReduce的实现,但是因为具有⾜够的通⽤性,同样可以⽀持其他的分布式计算模

  9. Ruby 默认将 IRB 配置为 Pretty_Inspect - 2

    我是ruby​​的新手,正在配置IRB。我喜欢pretty-print(需要'pp'),但总是输入pp来漂亮地打印它似乎很麻烦。我想做的是默认情况下让它漂亮地打印出来,所以如果我有一个var,比如说,'myvar',然后键入myvar,它会自动调用pretty_inspect而不是常规检查。我从哪里开始?理想情况下,我将能够向我的.irbrc文件添加一个自动调用的方法。有什么想法吗?谢谢! 最佳答案 irb中默认pretty-print对象正是hirb被迫去做。Theseposts解释hirb如何将几乎所有内容转换为ascii表。虽

  10. ruby-on-rails - 有没有一种工具可以在编码时自动保存对文件的增量更改? - 2

    我最喜欢的Google文档功能之一是它会在我工作时不断自动保存我的文档版本。这意味着即使我在进行关键更改之前忘记在某个点进行保存,也很有可能会自动创建一个保存点。至少,我可以将文档恢复到错误更改之前的状态,并从该点继续工作。对于在MacOS(或UNIX)上运行的Ruby编码器,是否有具有等效功能的工具?例如,一个工具会每隔几分钟自动将Gitcheckin我的本地存储库以获取我正在处理的文件。也许我有点偏执,但这点小保险可以让我在日常工作中安心。 最佳答案 虚拟机有些人可能讨厌我对此的回应,但我在编码时经常使用VIM,它具有自动保存功

随机推荐