分类
spring-boot

Spring Boot 创建自定义自动配置

Create a Custom Auto-Configuration with Spring Boot

1. 概述

Spring Boot提供了一种自动配置的机制,它可以根据当前的Spring应用所加载的依赖项对项目进行自动的配置。比如当Spring Boot检测到项目仅依赖于H2数据库时,将自动启动H2数据库做为项目的默认数据库。

该自动配置机制的存在无疑使得应用开发起来更轻松、更简单。本文我们将围绕Spring Boot的自动配置展开阐述。

2. 版本信息

本文基于Spring Boot版本为2.3.3.RELEASE,采用java1.8

3. 按是否存在特定的Class配置

按Class判断是否加载配置信息指:

  1. 当前Spring项目中存在指定的class时,加载配置信息,否则不加载配置信息。使用 @ConditionalOnClass 注解。
  2. 当前Spring项目中不存在指定的class时,加载配置信息,否则不加载配置信息。使用 @ConditionalOnMissingClass 注解。

3.1 @ConditionalOnClass 适用环境

@ConditionalOnClass接收的参数为Class<?>,也就是我们需要如下使用:@ConditionalOnClass<Student.class>。这同时意味着如果该语句能过顺利的通过编译器,首先要保证Student.class是存在的。

细想下会发现以下问题:只有当Student.class存在,@ConditionalOnClass<Student.class>才能通过编译,项目才能成功启动;而当Student.class不存在时,@ConditionalOnClass<Student.class>不能通过编译,项目同时无法启动。

再总结一下:使用@ConditionalOnClass<Student.class>注解时,只有存在Student.class时编译才能通过。也就是说使用了@ConditionalOnClass<Student.class>注解的项目,能启动的前提是存在Student.class。那么在@ConditionalOnClass<Student.class>注解下的方法或是类恒为ture,当然也就失去了使用此注解的意义。

其实该注解的用武之地并不是普通的Spring项目,而是基于Spring开发的第三方包。@ConditionalOnClass(Class<?>)中的Class往往是指其它的依赖中的类。假设开发一个发送短信的第三方jar包,开发的思想为:如果依赖于该包的项目同时依赖于阿里短信,则启动短信发送功能。

@ConditionalOnClass(AliSmsService.class)
public class AliSmsAutoconfigration {
    // 配置相关的BEAN
}

则其它依赖于上述第三方包的Spring应用如果依赖了阿里短信,则会启动该配置文件,从而达到了:如果该Spring应用依赖阿里短信,则启用短信发送功能,否则不启动短信功能的目的。

3.2 使用方法

Spring Boot在启动时,会扫描所依赖包的资源文件:resources/META-INF/spring.factories,并根据该文件中的相应值加载自动配置文件:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=club.codedemo.springbootcustomautoconfiguration.MyzAutoConfiguration

MyzAutoConfiguration

@Configuration
@ConditionalOnClass(YunzhiService.class)
public class MyzAutoConfiguration {
    @Bean
    SmsService smsService() {
        return new SmsServiceMyzImpl();
    }
}

上述代码使用@Configuration表示当前为配置类,使用@ConditionalOnClass(YunzhiService.class)表示当项目中存在于YunzhiService时,该配置文件下生效。YunzhiService依赖于com.mengyunzhi.core

<dependency>
	<groupId>com.mengyunzhi</groupId>
	<artifactId>core</artifactId>
	<version>2.1.7.0</version>
</dependency>

此时其它依赖于本第三方包的应用如果同时依赖了com.mengyunzhi.core,则将自动配置MyzAutoConfiguration。接下来便可以在该项目中注入SmsService了。

3.3 ConditionalOnMissingClass

@ConditionalOnClass不同,@ConditionalOnMissingClass注解收到的参数为String而非Class<?>,表示:当某个类不存在时....。

@Configuration
@ConditionalOnMissingClass("com.mengyunzhi.core.service.YunzhiService")
public class OnMissingClassAutoConfiguration {
    @Bean
    SmsService smsService() {
        return new SmsServiceErrorImpl();
    }
}

如果当前项目的定位为第三方包,则还应该将其加入到resources/META-INF/spring.factories中,以使依赖于该包的Spring项目能够启用该自动配置文件:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=club.codedemo.springbootcustomautoconfiguration.MyzAutoConfiguration,club.codedemo.springbootcustomautoconfiguration.OnMissingClassAutoConfiguration

注意:两个文件以,相隔。

3.4 加载顺序

当存在多个自动配置文件时,还可以使用@AutoConfigureOrder(int 权重)来指定其加载的顺序,比如将MyzAutoConfiguration的加载顺序设置为最前(优先级最高):

@ConditionalOnClass(YunzhiService.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class MyzAutoConfiguration {

其中权重的范围为:-21474836482147483647,值越小则权重越高,加载的顺序越靠前。

注意:必须将@AutoConfigureOrder注解的类同步添加到resources/META-INF/spring.factories中时,该注解才会生效。

4. 按是否存在特定的Bean配置

可以使用@ConditionalOnBean@ConditionalOnMissingBean来根据某个Bean是否存在来选择加载配置项。

@Configuration
@AutoConfigureOrder(1)
public class BeanConditionalAutoConfiguration {

    /**
     * 当前容器中存在SmsService时生效
     * @return 邮件服务
     */
    @Bean
    @ConditionalOnBean(SmsService.class)
    public EmailService emailService() {
        return new EmailServiceImpl();
    }

    /**
     * 当前容器中 不 存在SmsService时生效
     * @return 邮件服务
     */
    @Bean
    @ConditionalOnMissingBean(SmsService.class)
    public EmailService customEmailService() {
        return (address, title, description) -> {
            throw new RuntimeException("未找到默认的emailService实现");
        };
    }
}

上述代码的作用是:按SmsService Bean是否存在为项目装配不同的EmailService实现。

5. 按配置项自动配置

在Spring中可以使用@ConditionalOnProperty注解来关联相应的配置文件,比如在classPath中存在ding.properties配置文件,则可以使用@ConditionalOnProperty注解来完成配置文件与类之间的关联。

@PropertySource("classpath:ding.properties")
@Configuration
public class ConditionalOnPropertyAutoConfiguration {

@PropertySource关联ding.properties,@Configuration以表明此类为配置类。

则可以根据ding.properties中的属性值来决定自动装配的情况:

@PropertySource("classpath:ding.properties")
@Configuration
public class ConditionalOnPropertyAutoConfiguration {

    /**
     * 当url 值为alibaba 时,装配此bean
     * bean名起为dingService
     * @return
     */
    @Bean(name = "dingService")
    @ConditionalOnProperty(
            name = "url",
            havingValue = "alibaba")
    DingService dingService() {
        return message -> {
            // 处理钉钉消息
        };
    }

    /**
     * 当URL值为codedemo时,装配此bean
     * bean名称为ding1Service
     * @return
     */
    @Bean(name = "ding1Service")
    @ConditionalOnProperty(
            name = "url",
            havingValue = "codedemo")
    DingService dingService1() {
        return message -> {
            // 处理钉钉消息
        };
    }
}

6. 按配置文件存在与否自动配置

除了可以根据配置文件中的属性来进行自动配置外,还可以根据是否存在某个配置文件来配置。

比如当存在codedemo.properties时,装配ding3Service:

@Configuration
@ConditionalOnResource(resources = "classpath:codedemo.properties")
public class ConditionalOnResourceAutoConfiguration {
    @Bean("ding2Service")
    DingService dingService() {
        return (message) -> {

        };
    }
}

当存在alibaba.properties时,装配ding2Service:

@Configuration
@ConditionalOnResource(resources = "classpath:alibaba.properties")
public class ConditionalOnResourceAutoConfiguration1 {
    @Bean("ding3Service")
    DingService dingService() {
        return (message) -> {

        };
    }
}

7. 自定义自动配置条件

当Spring提供的自动配置条件注解不能满足我们的要求时,还可以自定义自动配置条件。

自定自动配置条件仅需要继承SpringBootCondition抽象类并重写其getMatchOutcome() 方法:

public class CustomerConditionTrue extends SpringBootCondition {

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 第一个参数返回true,表示该注解下的配置生效。生产条件应该根据当前情景动态计算出true或false
        return new ConditionOutcome(true, "message");
    }
}

在方法上使用该注解:

    @Bean("ding4Service")
    @Conditional(CustomerConditionTrue.class)
    DingService ding4Service() {
        return message -> {

        };
    }

8. 根据是否为WEB应用进行配置

还可以通过 @ConditionalOnWebApplication 以及 @ConditionalOnNotWebApplication注解以达到:当前应用为web应用时自动配置某些bean,以及当前应用非web应用时,自动配置某些bean的目的。

9. 禁用自动配置类

有些时候我们并不希望某些自动配置类在本项目中生效,则可以将该类加入到@EnableAutoConfiguration注解的exclude属性中:

//@SpringBootApplication
@SpringBootConfiguration
@EnableAutoConfiguration(exclude = DisableAutoConfiguration.class)
@ComponentScan(excludeFilters = { @ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@ComponentScan.Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public class SpringBootCustomAutoConfigurationApplication {

注意:被排除的类必须存在于resources/META-INF/spring.factories文件的org.springframework.boot.autoconfigure.EnableAutoConfiguration属性中。

由于某些原因@SpringBootApplication不能够与@EnableAutoConfiguration同时使用,导致上述代码看起来比较臃肿。所以在条件允许的情况下,推荐使用配置项目的spring.autoconfigure.exclude来替换上述使用注解的方案:

spring.autoconfigure.exclude=club.codedemo.springbootcustomautoconfiguration.DisableAutoConfiguration

10. 总结

本文阐述了Spring提供的几种自动配置方法。合理的规划自动配置可以提升项目的健壮性与适用性,而Spring自动配置则是一把利器,使用Spring自动配置往往能起到事半功倍的效果。