解析SpringBoot @EnableAutoConfiguration的使用

刚做后端开发的时候,最早接触的是基础的spring,为了引用二方包提供bean,还需要在xml中增加对应的包<context:component-scan base-package="xxx" /> 或者增加注解@ComponentScan({ "xxx"})。当时觉得挺urgly的,但也没有去研究有没有更好的方式。

直到接触Spring Boot 后,发现其可以自动引入二方包的bean。不过一直没有看这块的实现原理。直到最近面试的时候被问到。所以就看了下实现逻辑。

使用姿势

讲原理前先说下使用姿势。

在project A中定义一个bean。

package com.wangzhi;

import org.springframework.stereotype.Service;

@Service
public class Dog {
}

并在该project的resources/META-INF/下创建一个叫spring.factories的文件,该文件内容如下

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.wangzhi.Dog

然后在project B中引用project A的jar包。

projectA代码如下:

package com.wangzhi.springbootdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@EnableAutoConfiguration
public class SpringBootDemoApplication {

  public static void main(String[] args) {
    ConfigurableApplicationContext context = SpringApplication.run(SpringBootDemoApplication.class, args);
    System.out.println(context.getBean(com.wangzhi.Dog.class));
  }

}

打印结果:

com.wangzhi.Dog@3148f668

原理解析

总体分为两个部分:一是收集所有spring.factories中EnableAutoConfiguration相关bean的类,二是将得到的类注册到spring容器中。

收集bean定义类

在spring容器启动时,会调用到AutoConfigurationImportSelector#getAutoConfigurationEntry

protected AutoConfigurationEntry getAutoConfigurationEntry(
    AutoConfigurationMetadata autoConfigurationMetadata,
    AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
  }
  // EnableAutoConfiguration注解的属性:exclude,excludeName等
  AnnotationAttributes attributes = getAttributes(annotationMetadata);
  // 得到所有的Configurations
  List<String> configurations = getCandidateConfigurations(annotationMetadata,
      attributes);
  // 去重
  configurations = removeDuplicates(configurations);
  // 删除掉exclude中指定的类
  Set<String> exclusions = getExclusions(annotationMetadata, attributes);
  checkExcludedClasses(configurations, exclusions);
  configurations.removeAll(exclusions);
  configurations = filter(configurations, autoConfigurationMetadata);
  fireAutoConfigurationImportEvents(configurations, exclusions);
  return new AutoConfigurationEntry(configurations, exclusions);
}

getCandidateConfigurations会调用到方法loadFactoryNames:

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
    // factoryClassName为org.springframework.boot.autoconfigure.EnableAutoConfiguration
 String factoryClassName = factoryClass.getName();
    // 该方法返回的是所有spring.factories文件中key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的类路径
 return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
 }

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
 MultiValueMap<String, String> result = cache.get(classLoader);
 if (result != null) {
  return result;
 }

 try {
      // 找到所有的"META-INF/spring.factories"
  Enumeration<URL> urls = (classLoader != null ?
   classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
   ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
  result = new LinkedMultiValueMap<>();
  while (urls.hasMoreElements()) {
  URL url = urls.nextElement();
  UrlResource resource = new UrlResource(url);
        // 读取文件内容,properties类似于HashMap,包含了属性的key和value
  Properties properties = PropertiesLoaderUtils.loadProperties(resource);
  for (Map.Entry<?, ?> entry : properties.entrySet()) {
   String factoryClassName = ((String) entry.getKey()).trim();
          // 属性文件中可以用','分割多个value
   for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
   result.add(factoryClassName, factoryName.trim());
   }
  }
  }
  cache.put(classLoader, result);
  return result;
 }
 catch (IOException ex) {
  throw new IllegalArgumentException("Unable to load factories from location [" +
   FACTORIES_RESOURCE_LOCATION + "]", ex);
 }
 }

注册到容器

在上面的流程中得到了所有在spring.factories中指定的bean的类路径,在processGroupImports方法中会以处理@import注解一样的逻辑将其导入进容器。

public void processGroupImports() {
  for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
    // getImports即上面得到的所有类路径的封装
    grouping.getImports().forEach(entry -> {
      ConfigurationClass configurationClass = this.configurationClasses.get(
          entry.getMetadata());
      try {
        // 和处理@Import注解一样
        processImports(configurationClass, asSourceClass(configurationClass),
            asSourceClasses(entry.getImportClassName()), false);
      }
      catch (BeanDefinitionStoreException ex) {
        throw ex;
      }
      catch (Throwable ex) {
        throw new BeanDefinitionStoreException(
            "Failed to process import candidates for configuration class [" +
                configurationClass.getMetadata().getClassName() + "]", ex);
      }
    });
  }
}

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
  Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
 ...
  // 遍历收集到的类路径
  for (SourceClass candidate : importCandidates) {
    ...
    //如果candidate是ImportSelector或ImportBeanDefinitionRegistrar类型其处理逻辑会不一样,这里不关注
   // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
   // process it as an @Configuration class
   this.importStack.registerImport(
    currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
 // 当作 @Configuration 处理
    processConfigurationClass(candidate.asConfigClass(configClass));
  ...
}

  ...
}

可以看到,在第一步收集的bean类定义,最终会被以Configuration一样的处理方式注册到容器中。

End

@EnableAutoConfiguration注解简化了导入了二方包bean的成本。提供一个二方包给其他应用使用,只需要在二方包里将对外暴露的bean定义在spring.factories中就好了。对于不需要的bean,可以在使用方用@EnableAutoConfiguration的exclude属性进行排除。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • SpringBoot 中常用注解及各种注解作用

    本篇文章将介绍几种SpringBoot 中常用注解 其中,各注解的作用为: @PathVaribale 获取url中的数据 @RequestParam 获取请求参数的值 @GetMapping 组合注解,是@RequestMapping(method = RequestMethod.GET)的缩写 @RestController是@ResponseBody和@Controller的组合注解. @PathVaribale 获取url中的数据 看一个例子,如果我们需要获取Url=localhost:

  • SpringBoot使用自定义注解实现权限拦截的示例

    本文介绍了SpringBoot使用自定义注解实现权限拦截的示例,分享给大家,具体如下: HandlerInterceptor(处理器拦截器) 常见使用场景 日志记录: 记录请求信息的日志, 以便进行信息监控, 信息统计, 计算PV(page View)等 性能监控: 权限检查: 通用行为: 使用自定义注解实现权限拦截 首先HandlerInterceptor了解 在HandlerInterceptor中有三个方法: public interface HandlerInterceptor { //

  • 详解Spring Boot集成MyBatis(注解方式)

    MyBatis是支持定制化SQL.存储过程以及高级映射的优秀的持久层框架,避免了几乎所有的JDBC代码和手动设置参数以及获取结果集.spring Boot是能支持快速创建Spring应用的Java框架.本文通过一个例子来学习Spring Boot如何集成MyBatis,而且过程中不需要XML配置. 创建数据库 本文的例子使用MySQL数据库,首先创建一个用户表,执行sql语句如下: CREATE TABLE IF NOT EXISTS user ( `id` INT(10) NOT NULL A

  • spring boot 的常用注解使用小结

    @RestController和@RequestMapping注解 4.0重要的一个新的改进是@RestController注解,它继承自@Controller注解.4.0之前的版本,Spring MVC的组件都使用@Controller来标识当前类是一个控制器servlet.使用这个特性,我们可以开发REST服务的时候不需要使用@Controller而专门的@RestController. 当你实现一个RESTful web services的时候,response将一直通过response

  • 浅谈SpringBoot处理url中的参数的注解

    1.介绍几种如何处理url中的参数的注解 @PathVaribale 获取url中的数据 @RequestParam 获取请求参数的值 @GetMapping 组合注解,是 @RequestMapping(method = RequestMethod.GET) 的缩写 (1)PathVaribale 获取url中的数据 看一个例子,如果我们需要获取Url=localhost:8080/hello/id中的id值,实现代码如下: @RestController public class Hello

  • Spring Boot 整合 Mybatis Annotation 注解的完整 Web 案例

    前言 距离第一篇 Spring Boot 系列的博文 3 个月了.虽然 XML 形式是我比较推荐的,但是注解形式也是方便的.尤其一些小系统,快速的 CRUD 轻量级的系统. 这里感谢晓春 http://xchunzhao.tk/ 的 Pull Request,提供了 springboot-mybatis-annotation 的实现. 一.运行 springboot-mybatis-annotation 工程 然后Application 应用启动类的 main 函数,然后在浏览器访问: http

  • 浅谈springBoot注解大全

    一.注解(annotations)列表 @SpringBootApplication:包含了@ComponentScan.@Configuration和@EnableAutoConfiguration注解.其中@ComponentScan让spring Boot扫描到Configuration类并把它加入到程序上下文. @Configuration 等同于spring的XML配置文件:使用Java代码可以检查类型安全. @EnableAutoConfiguration 自动配置. @Compon

  • 详解spring boot mybatis全注解化

    本文重点给大家介绍spring boot mybatis 注解化的实例代码,具体内容大家参考下本文: pom.xml <!-- 引入mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version

  • SpringBoot 注解事务声明式事务的方式

    springboot 对新人来说可能上手比springmvc要快,但是对于各位从springmvc转战到springboot的话,有些地方还需要适应下,尤其是xml配置.我个人是比较喜欢注解➕xml是因为看着方便,查找方便,清晰明了.但是xml完全可以使用注解代替,今天就扒一扒springboot中事务使用注解的玩法. springboot的事务也主要分为两大类,一是xml声明式事务,二是注解事务,注解事务也可以实现类似声明式事务的方法,关于注解声明式事务,目前网上搜索不到合适的资料,所以在这里

  • Spring Boot 基于注解的 Redis 缓存使用详解

    看文本之前,请先确定你看过上一篇文章<Spring Boot Redis 集成配置>并保证 Redis 集成后正常可用,因为本文是基于上文继续增加的代码. 一.创建 Caching 配置类 RedisKeys.Java package com.shanhy.example.redis; import java.util.HashMap; import java.util.Map; import javax.annotation.PostConstruct; import org.springf

  • Spring Boot使用Value注解给静态变量赋值的方法

    昨天在使用@Value注解给静态变量赋值的时候,发现静态变量的值始终是null.后来搜索一下得知其中原因,Spring Boot 不允许/不支持把值注入到静态变量中.但是我们可以变通一下解决这个问题.因为Spring Boot 支持set方法注入,我们可以利用非静态set方法注入静态变量.废话不多说,贴上我昨天写的代码: @Component public class CoverImageUtil { private static String endpoint; private static

随机推荐