SpringBoot 自动配置源码原理 、自定义一个Starter

SpringBoot 自动配置源码分析 、自定义一个Starter

SpringBoot 自动配置原理是面试官最喜欢询问的一个问题、考察你是否真的了解其底层实现。本篇文章和您一起探索自动配置的原理、以及如何自定义一个Starter

SpringBoot自动配置的原理

本人总结:SpringBoot 自动配置原理就是根据某种条件加载默认支持的第三方配置类进ioc容器里。为了验证这么原理我们从以下2点进行源码的跟踪

1、 SpringBoot 自动配置默认支持的配置类在哪?有哪些默认支持的配置类?

2、是如何将这些默认支持的配置类加载进容器里的?如果让配置类生效?

SpringBoot 自动配置默认支持的配置类在哪?有哪些默认支持的配置类?

SpringBoot 应用程序想必大家都再熟悉不过了,其程序入口main 方法 上的@SpringBootApplication注解就是我们研究SpringBoot 自动配置的原理,如下程序:

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

按住ctrl 鼠标右击点击 (idea)@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 注解是一个组合注解、其中@EnableAutoConfiguration 这个注解是我们需要研究的,不难揣测出其中文名称为:“自动配置”

按住ctrl 鼠标右击点击 (idea)@EnableAutoConfiguration 注解进入,可用看到如下代码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    // 这里很多代码
    ......
}

在以上 @EnableAutoConfiguration 注解中,@Import 注解 的值,AutoConfigurationImportSelector.class 译为:自动配置导入选择器,在AutoConfigurationImportSelector 类中有一个方法为:

/**
 * Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
 * of the importing {@link Configuration @Configuration} class.
 * @param annotationMetadata the annotation metadata of the configuration class
 * @return the auto-configurations that should be imported
 */
// 大致意思为:返回被@Configuration 注解标记的类
// annotationMetadata 可用理解为 @Configuration 注解对象、包含值等
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);
}

以上代码我们重点看:getCandidateConfigurations(xx,xx)

/**
 * Return the auto-configuration class names that should be considered. By default
 * this method will load candidates using {@link SpringFactoriesLoader} with
 * {@link #getSpringFactoriesLoaderFactoryClass()}.
 * @param metadata the source metadata
 * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
 * attributes}
 * @return a list of candidate configurations
 */
// 大致意思是:默认使用SpringFactoriesLoader,加载返回自动配置的类的类名
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
   List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
         getBeanClassLoader());
   Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
         + "are using a custom packaging, make sure that file is correct.");
   return configurations;
}

再跟踪进:SpringFactoriesLoader.loadFactoryNames(xxx,xxx);

	/**
	 * Load the fully qualified class names of factory implementations of the
	 * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
	 * class loader.
	 * <p>As of Spring Framework 5.3, if a particular implementation class name
	 * is discovered more than once for the given factory type, duplicates will
	 * be ignored.
	 * @param factoryType the interface or abstract class representing the factory
	 * @param classLoader the ClassLoader to use for loading resources; can be
	 * {@code null} to use the default
	 * @throws IllegalArgumentException if an error occurs while loading factory names
	 * @see #loadFactories
	 */
//大致意思:通过给定的类加载器,从 FACTORIES_RESOURCE_LOCATION 加载出全限定类名 
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
   ClassLoader classLoaderToUse = classLoader;
   if (classLoaderToUse == null) {
      classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
   }
   String factoryTypeName = factoryType.getName();
   return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

这个方法中再跟进 loadSpringFactories(xxx,xxx),整个方法实现如下:

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
   Map<String, List<String>> result = cache.get(classLoader);
   if (result != null) {
      return result;
   }
   result = new HashMap<>();
   try {
      Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
      while (urls.hasMoreElements()) {
         URL url = urls.nextElement();
         UrlResource resource = new UrlResource(url);
         Properties properties = PropertiesLoaderUtils.loadProperties(resource);
         for (Map.Entry<?, ?> entry : properties.entrySet()) {
            String factoryTypeName = ((String) entry.getKey()).trim();
            String[] factoryImplementationNames =
                  StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
            for (String factoryImplementationName : factoryImplementationNames) {
               result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
                     .add(factoryImplementationName.trim());
            }
         }
      }
      // Replace all lists with unmodifiable lists containing unique elements
      result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
            .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
      cache.put(classLoader, result);
   }
   catch (IOException ex) {
      throw new IllegalArgumentException("Unable to load factories from location [" +
            FACTORIES_RESOURCE_LOCATION + "]", ex);
   }
   return result;
}

以上代码注意点:

Enumeration urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);

静态变量 FACTORIES_RESOURCE_LOCATION的值是:

public final class SpringFactoriesLoader {
   /**
    * The location to look for factories.
    * <p>Can be present in multiple JAR files.
    */
    // factorires 的查询路径
   public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
   	....
   }

我们把断点标记在如下位置后启动项目:会看到下图的结果 引入了redisson-spring-boot-stater 这个包,程序启动时回去加载 redisson-spring-boot-stater包 “META-INF/spring.factories” 中定义的自动配置类

redisson-spring-boot-stater包 ,META-INF/spring.factories” 文件内容如下图:

在这里插入图片描述

加载所有的自动配置类之后返回的全限定类名如下:(项目不同扫描出来的都不会相同)
在这里插入图片描述

以上的截图是再pom.xml文件中添加的 redission依赖,而springBoot 也默认了很多自动配置类,该文件在 spring-boot-autoconfigure.jar 中 如图:
在这里插入图片描述

从以上源码跟踪,可用得出 结论1:

SpringBoot 程序在启动时候会扫描所有jar包 META-INF/spring.factories 文件里定义的自动配置类

自此我们已经知道了springboot 去哪里找到这些配置类的了,接下来是第二个问题 怎么样才能让这些配置类生效?

如何将这些默认支持的配置类加载进容器里的?如果让配置类生效?

这个项目里面已经引入了redis 的starter , 但是沒有引入 Mongo的 starter

我们分别看看 这两个 配置如下:

在这里插入图片描述

MongoDataAutoConfiguration 如下图:可以看到这个MongoClient 和 MongoTemplate 这两个类是不存在的(因为没有引入jar包依赖),所以显示红色

在这里插入图片描述

RedisAutoConfiguratiion: 如下图:可以看到RedisAutoConfiguratiion 这个类是正确可用的一个类,当程序启动时候扫描到@Configuration 这注解,将配置类加载,就完成了redis 的自动配置

在这里插入图片描述

思考:上图为什么Mongo的配置类,方法都找不到了,程序还能启动起来?(常规我们自己写的代码方法找不到项目启动不起来) 仅仅是因为添加了 坐标依赖 就可以完成自动配置了吗?

条件注解:springBoot 中定义了很多条件注解,条件注解作用就是当满足条件注解的要求这个类才会被实例化,如以下这段代码的意思可以理解为 当存在MongoClient.class 和 MongoTemplate.class 这两个class 文件时,@Configuration 才会生效。

@ConditionalOnClass({ MongoClient.class, MongoTemplate.class })
ConditionalOnBeanApplicationContext存在某些bean时条件才成立OnBeanCondition
ConditionalOnClassclassPath中存在某些Class时条件才成立OnClassCondition
ConditionalOnCloudPlatform在某些云平台(Kubemetes、Heroku、Cloud Foundry)下条件才成立OnCloudPlatformCondition
ConditionalOnExpressionSPEL表达式成立时条件才成立OnExpressionCondition
ConditionalOnJavaJDK某些版本条件才成立OnJavaCondition
ConditionalOnJndiJNDI路径下存在时条件才成立OnJndiCondition
ConditionalOnMissingBeanApplicationContext不存在某些bean时条件才成立OnBeanCondition
ConditionalOnMissingClassclasspath中不存在某些class是条件才成立OnClassCondition
ConditionalOnNotWebApplication在非Web环境下条件才成立OnWebApplicationCondition
ConditionalOnPropertyEnvironment中存在某些配置项时条件才成立OnPropertyCondition
ConditionalOnResource存在某些资源时条件才成立OnResourceCondition
ConditionalOnSingleCandidateApplicationContext中存在且只有一个bean时条件成立OnBeanCondition
ConditionalOnWebApplication在Web环境下条件才成立OnWebApplicationCondition

我们常用的有@ConditionalOnClass、@ConditionalOnBean 、@ConditionalOnMissingBean、@ConditionalOnMissingClass、@ConditionalOnProperty

然而上图中mongoDB 中都找不到类了程序运行起来不会报错?

我们知道读取该注解的值JVM肯定是先要内存中加载该值的Class对象的。其实原理就是Spring Boot并不是通过反射去获取该值,而是直接去读取标注该注解的类的二进制文件,去获取其中的值

/**
 * {@link Conditional @Conditional} that only matches when the specified classes are on
 * the classpath.
 * <p>
 * A {@link #value()} can be safely specified on {@code @Configuration} classes as the
 * annotation metadata is parsed by using ASM before the class is loaded. Extra care is
 * required when placed on {@code @Bean} methods, consider isolating the condition in a
 * separate {@code Configuration} class, in particular if the return type of the method
 * matches the {@link #value target of the condition}.
 *
 * @author Phillip Webb
 * @since 1.0.0
 */
//大概意思是:这里意思是说,使用value属性(类型为Class<?>)进行赋值,那么当@ConditionalOnClass()注解作用在@Configuration类上是安全的,因为这时Spring Boot会通过ASM(一个操作字节码的框架)去获取对应的Class值。而当@ConditionalOnClass()和@Bean搭配使用时,则不能使用value属性赋值了,应该使用name属性(类型为String),大概是因为这个时候Spring Boot不会使用ASM去获取值了。当然除了使用name属性,也可以按注释中给出的代码,继续使用value属性,但是需要多创建一个@Configuration类去隔绝@Conditional条件。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
...
}

总结:首先这些@Configuration类没有被程序中的类引用到、其次即使引用到这个类,不一定引用到类中的具体某个方法。 虽然这些地方import失败了, 但是不影响.class类加载,也就是说编译这些@Configuration类时依赖的jar是必须存在的,但是运行时这些jar可以不提供

到此上文的两个问题我们都大致初略的明白了 以下两个问题:

1、 SpringBoot 自动配置默认支持的配置类在哪?有哪些默认支持的配置类?

2、是如何将这些默认支持的配置类加载进容器里的?如果让配置类生效?

自定义一个starter

案例背景:我们在开发中每个项目都会使用到文件上传模块、这个案例中就把文件上传的功能封装成一个starter

快速创建一个SpringBoot项目 pom.xml如下:

以下三个依赖必须的:

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
	</dependencies>

提供一个上传文件接口IUploadService

public interface IUploadService {
    /**
     * 上传文件接口
     * @param file
     * @return
     */
    Result upload(MultipartFile file);
    ...
}

properties 对应类:

/**
 * 对应的是application文件的配置
 */
@Component
@ConfigurationProperties(prefix = "spring.upload")
public class UploadProperties {
    // 文件的上传地址
    private String dir;
    // 文件前缀
    private String prefix;
    
    //getter and setter
}

默认实现方法:

public class DefaultUploadService implements IUploadService {
    @Autowired
    private UploadProperties uploadProperties;
    public DefaultUploadService (UploadProperties uploadProperties){
        this.uploadProperties=uploadProperties;
    }
    /**
     * 文件上传方法
     */
    @Override
    public Result upload(MultipartFile file){
        if(uploadProperties.getDir()==null || uploadProperties.getPrefix()==null){
            return Result.error("请配置文件上传路径");
        }
        try {
        if(file.isEmpty()){
            return Result.error("文件为空");
        }
        String originalFilename=file.getOriginalFilename();
        boolean blob = originalFilename.equals("blob");
        String fileExt = blob?".jpg": originalFilename.substring(originalFilename.lastIndexOf(".") ).toLowerCase();
        if(StringUtils.isBlank(fileExt)||fileExt.equals(".blob")){
            fileExt=".jpg";
        }
        String month= DateUtil.formatDate(new Date(),"yyyyMMdd");
        String uploadPathPrefix=uploadProperties.getPrefix()+month+"/";
        SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
        String newFileName = UUID.randomUUID() + fileExt;
        String absuDir=uploadProperties.getDir()+uploadPathPrefix;
            //是否有目录,没有就创建
            File absuDirFile=new File(absuDir);
            if(!absuDirFile.exists()){
                absuDirFile.mkdirs();
            }
            //目录下写入文件
            File uploadFile=new File(absuDir,newFileName);
            Files.copy(file.getInputStream(), uploadFile.toPath());
            NameUrlBean bean=new NameUrlBean();
            bean.setName(originalFilename);
            bean.setUrl(uploadPathPrefix+newFileName);
            return Result.success(bean);
     } catch (IOException e) {
            e.printStackTrace();
            return Result.error("上传文件失败"+e.getMessage());
        }
    }
    ...
}

UploadConfig 关键配置类:

/**
 * @ConditionalOnProperty(name = "dir",prefix = "spring.upload")
 * 当引入此jar的项目需要在配置文件中配置 spring.upload.dir
 */
@Configuration
@EnableConfigurationProperties(UploadProperties.class)
@ConditionalOnProperty(name = "dir",prefix = "spring.upload")
public class UploadConfig {
    @Autowired
    private UploadProperties uploadProperties;

    @Bean
    public DefaultUploadService defaultUploadService(){
        return new DefaultUploadService(uploadProperties);
    }
}

以上代码中注意 @ConditionalOnProperty(name = “dir”,prefix = “spring.upload”),这里的意思是:当引入此上传文件jar 的项目,在application.yml中 配置 了上传文件地址 spring.upload.dir 时才把这个类注册到容器中。

在resource 目录下新建 /META-INF/spring.factories 文件添加配置如下:

#-------starter自动装配---------
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.it520.upload.config.UploadConfig

项目pom.xml 添加如下:

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<skip>true</skip>
				</configuration>
			</plugin>
		</plugins>
	</build>

默认的springBoot 项目是没有 这个节点的配置的,这里需要新增该配置不然 打包之后生成的jar 文件路径找不到。

完整的stater项目如下:

在这里插入图片描述

在这个springBoot 项目我们不需要启动类 和 原来的 application 文件、test 目录 的,在此删掉也无妨。

idea 点击 install,把项目打包并存到本地maven仓库中

使用测试:

随便找一个springBoot 项目引入我们自定义的stater 如下:

在这里插入图片描述

新增一个接口使用默认的DefalutUploadService,进行文件上传:如下

@RestController
public class UploadTestController {
    @Autowired
    private DefaultUploadService uploadService;

    
    @PostMapping("/myUpload")
    public Result upload(@RequestParam("file") MultipartFile file){
        return uploadService.upload(file);
    }
}

此时我们不在application 文件中配置上传文件的地址时启动项目:

在这里插入图片描述

启动项目时 @Autowired 在容器中找不到 DefaultUploadService 这个bean,所以启动失败了

我们在application.yml 中配置上传文件的地址:

#other setting

spring.upload.dir=D:/upload
spring.upload.prefix=/test/

#other setting

配置后重启项目,就不会报错了,测试访问刚刚定义的 上传文件接口,结果如下图显示:

在这里插入图片描述

至此我们自定义一个stater 完成了,可以上传到maven私服仓库,之后的项目中需要上传文件的功能时,引入这个jar 调用默认的实现方法即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值