SpringBoot--如何创建自己的自动配置

在实际开发中,仅靠SpringBoot的自动配置是远远不够的,比如要访问多个数据源,自动配置就完全无能为力了。

自动配置的本质

本质就是在容器中预配置要整合的框架所需的基础Bean。

以MyBatis为例,spring整合MyBatis无非就是完成以下事情:

  1. 配置SqlSessionFactory Bean,当然,该Bean需要注入一个DataSource
  2. 配置SqlSessionTemplate Bean,将上面的SqlSessionFactory 注入该Bean
  3. 注册Mapper组件的自动扫描,相当于添加<mybatis:scan.../>元素

自动配置非常简单,无非就是有框架提供一个@Configuration修饰的配置类(相当于传统的xml配置文件),在该配置类中用@Bean预先配置默认的SqlSessionFactory、SqlSessionTemplate,并注册Mapper组件的自动扫描即可。

比如MybatisAutoConfiguration源代码:

java 复制代码
@Configuration // 被修饰的类变成配置类
// 当SqlSessionFactory、SqlSessionFactoryBean类存在时,才会生效。
// 条件注解之一
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class}) 
// 当DataSource Bean存在时,才会生效
// 条件注解之一
@ConditionalOnSingleCandidate(DataSource.class)
// 启用Mybatis的属性处理类
// 启动属性处理类
@EnableConfigurationProperties({MybatisProperties.class})
//指定该配置类在DataSourceAutoConfiguration和MybatisLanguageDriverAutoConfiguration之后加载
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
// 实现 InitializingBean 接口,该接口中的 afterPropertiesSet 方法会在该Bean初始化完成后被自动调用
public class MybatisAutoConfiguration implements InitializingBean {
    private static final Logger logger = LoggerFactory.getLogger(org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.class);
    // Mybatis的配置属性
    private final MybatisProperties properties;
    // Mybatis的拦截器、类型处理器、语言驱动等
    private final Interceptor[] interceptors;
    private final TypeHandler[] typeHandlers;
    private final LanguageDriver[] languageDrivers;
    private final ResourceLoader resourceLoader;
    private final DatabaseIdProvider databaseIdProvider;
    private final List<ConfigurationCustomizer> configurationCustomizers;
    private final List<SqlSessionFactoryBeanCustomizer> sqlSessionFactoryBeanCustomizers;

    public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider, ObjectProvider<List<SqlSessionFactoryBeanCustomizer>> sqlSessionFactoryBeanCustomizers) {
        this.properties = properties;
        this.interceptors = (Interceptor[])interceptorsProvider.getIfAvailable();
        this.typeHandlers = (TypeHandler[])typeHandlersProvider.getIfAvailable();
        this.languageDrivers = (LanguageDriver[])languageDriversProvider.getIfAvailable();
        this.resourceLoader = resourceLoader;
        this.databaseIdProvider = (DatabaseIdProvider)databaseIdProvider.getIfAvailable();
        this.configurationCustomizers = (List)configurationCustomizersProvider.getIfAvailable();
        this.sqlSessionFactoryBeanCustomizers = (List)sqlSessionFactoryBeanCustomizers.getIfAvailable();
    }

    // 在Bean初始化完成后调用该方法
    public void afterPropertiesSet() {
        this.checkConfigFileExists();
    }

    // 检查Mybatis配置文件是否存在
    private void checkConfigFileExists() {
        if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
            // 获取配置文件的资源
            Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
            // 如果resource.exists()方法返回false,则抛出异常
            Assert.state(resource.exists(), "Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)");
        }

    }

    // 创建SqlSessionFactory Bean
    @Bean
    // 当没有SqlSessionFactory Bean时才会创建
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        // 创建SqlSessionFactoryBean实例
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        // 注入数据源
        factory.setDataSource(dataSource);
        factory.setVfs(SpringBootVFS.class);
        // 如果配置文件路径不为空,则设置配置文件位置
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }

        this.applyConfiguration(factory);
        // 如果配置属性不为空,则设置配置属性
        if (this.properties.getConfigurationProperties() != null) {
            factory.setConfigurationProperties(this.properties.getConfigurationProperties());
        }
        // 应用所有的拦截器
        if (!ObjectUtils.isEmpty(this.interceptors)) {
            factory.setPlugins(this.interceptors);
        }
        // 应用所有databaseIdProvider
        if (this.databaseIdProvider != null) {
            factory.setDatabaseIdProvider(this.databaseIdProvider);
        }
        // 根据包名应用TypeAliases
        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }
        // 根据父类型应用TypeAliases
        if (this.properties.getTypeAliasesSuperType() != null) {
            factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
        }
        // 根据包名应用TypeHandler
        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }
        // 应用所有TypeHandler
        if (!ObjectUtils.isEmpty(this.typeHandlers)) {
            factory.setTypeHandlers(this.typeHandlers);
        }
        // 设置mapper的加载位置
        if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
            factory.setMapperLocations(this.properties.resolveMapperLocations());
        }

        Set<String> factoryPropertyNames = (Set) Stream.of((new BeanWrapperImpl(SqlSessionFactoryBean.class)).getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collectors.toSet());
        Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
        if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
            factory.setScriptingLanguageDrivers(this.languageDrivers);
            if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
                defaultLanguageDriver = this.languageDrivers[0].getClass();
            }
        }

        if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
            factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
        }

        this.applySqlSessionFactoryBeanCustomizers(factory);
        // 返回SqlSessionFactory对象
        return factory.getObject();
    }

    private void applyConfiguration(SqlSessionFactoryBean factory) {
        org.apache.ibatis.session.Configuration configuration = this.properties.getConfiguration();
        if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
            configuration = new org.apache.ibatis.session.Configuration();
        }

        if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
            Iterator var3 = this.configurationCustomizers.iterator();

            while(var3.hasNext()) {
                ConfigurationCustomizer customizer = (ConfigurationCustomizer)var3.next();
                customizer.customize(configuration);
            }
        }

        factory.setConfiguration(configuration);
    }

    private void applySqlSessionFactoryBeanCustomizers(SqlSessionFactoryBean factory) {
        if (!CollectionUtils.isEmpty(this.sqlSessionFactoryBeanCustomizers)) {
            Iterator var2 = this.sqlSessionFactoryBeanCustomizers.iterator();

            while(var2.hasNext()) {
                SqlSessionFactoryBeanCustomizer customizer = (SqlSessionFactoryBeanCustomizer)var2.next();
                customizer.customize(factory);
            }
        }

    }

    // 创建SqlSessionTemplate Bean
    @Bean
    // 当没有SqlSessionTemplate Bean时才会创建
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        ExecutorType executorType = this.properties.getExecutorType();
        // 如果executorType不为null,则创建SqlSessionTemplate时使用该executorType
        return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
    }

    @Configuration
    // 导入MapperScannerRegistrarNotFoundConfiguration注册类
    @Import({org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
    // 当MapperFactoryBean和MapperScannerConfigurer都不存在时,才会生效
    @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
    // 实现 InitializingBean 接口,该接口中的 afterPropertiesSet 方法会在该Bean初始化完成后被自动调用
    public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
        public MapperScannerRegistrarNotFoundConfiguration() {
        }

        // 重写afterPropertiesSet方法
        public void afterPropertiesSet() {
            org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.logger.debug("Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
        }
    }

    // 注册Mapper扫描器的自动配置类
    // 实现 BeanFactoryAware接口可访问spring容器、
    // 实现ImportBeanDefinitionRegistrar 接口可配置额外的bean
    public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, EnvironmentAware, ImportBeanDefinitionRegistrar {
        // BeanFactory对象,用于保存Spring容器
        private BeanFactory beanFactory;
        private Environment environment;

        public AutoConfiguredMapperScannerRegistrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            if (!AutoConfigurationPackages.has(this.beanFactory)) {
                org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
            } else {
                org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.logger.debug("Searching for mappers annotated with @Mapper");
               // 获取自动配置要处理的包
                List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
                if (org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.logger.isDebugEnabled()) {
                    packages.forEach((pkg) -> {
                        org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.logger.debug("Using auto-configuration base package '{}'", pkg);
                    });
                }
                // 创建BeanDefinitionBuilder对象
                // 它帮助开发者以反射的方式创建任意类的实例
                // 此处就是帮助创建MapperScannerConfigurer类的实例
                BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
               // 为要创建的对象设置属性
                builder.addPropertyValue("processPropertyPlaceHolders", true);
                builder.addPropertyValue("annotationClass", Mapper.class);
                builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
                BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
                Set<String> propertyNames = (Set)Stream.of(beanWrapper.getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collectors.toSet());
                if (propertyNames.contains("lazyInitialization")) {
                    builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");
                }

                if (propertyNames.contains("defaultScope")) {
                    builder.addPropertyValue("defaultScope", "${mybatis.mapper-default-scope:}");
                }

                boolean injectSqlSession = (Boolean)this.environment.getProperty("mybatis.inject-sql-session-on-mapper-scan", Boolean.class, Boolean.TRUE);
                if (injectSqlSession && this.beanFactory instanceof ListableBeanFactory) {
                    ListableBeanFactory listableBeanFactory = (ListableBeanFactory)this.beanFactory;
                    Optional<String> sqlSessionTemplateBeanName = Optional.ofNullable(this.getBeanNameForType(SqlSessionTemplate.class, listableBeanFactory));
                    Optional<String> sqlSessionFactoryBeanName = Optional.ofNullable(this.getBeanNameForType(SqlSessionFactory.class, listableBeanFactory));
                    if (!sqlSessionTemplateBeanName.isPresent() && sqlSessionFactoryBeanName.isPresent()) {
                        builder.addPropertyValue("sqlSessionFactoryBeanName", sqlSessionFactoryBeanName.get());
                    } else {
                        builder.addPropertyValue("sqlSessionTemplateBeanName", sqlSessionTemplateBeanName.orElse("sqlSessionTemplate"));
                    }
                }

                builder.setRole(2);
                // 在容器中注册BeanDefinitionBuilder创建的MapperScannerConfigurer对象
                registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
            }
        }

        // 获取spring容器和环境对象
        public void setBeanFactory(BeanFactory beanFactory) {
            this.beanFactory = beanFactory;
        }

        public void setEnvironment(Environment environment) {
            this.environment = environment;
        }

        private String getBeanNameForType(Class<?> type, ListableBeanFactory factory) {
            String[] beanNames = factory.getBeanNamesForType(type);
            return beanNames.length > 0 ? beanNames[0] : null;
        }
    }
}

开开发完自动配置类后,还需要使用META-INF/spring.factories文件来定义自动配置类,比如:

java 复制代码
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

自动配置类只能通过META-INF/spring.factories来加载,并确保它们处于一个特殊的包空间内,尤其不能让他们变成普通@ComponentScan的目标。此外,自动配置类不应该使用@ComponentScan来扫描其他组件,如果需要加载其他配置文件,应使用@Import来加载。

如果要为自动配置类指定加载顺序,可使用以下注解:

  1. @AutoConfigureAfter:指定被修饰的类必须在一个或多个自动配置类之后加载
  2. @AutoConfigureBefore:指定被修饰的类必须在一个或多个自动配置类之前加载

如果自动配置包中包含多个自动配置类,且以特定的顺序来加载,可使用@AutoConfigureOrder来修饰它们,@AutoConfigureOrder类似于@Order注解,只不过专门修饰自动配置类。

条件注解

条件注解用于修饰@Configuration类 或@Bean方法等,表示只有条件有效时,被修饰的 配置类或配置方法才生效。SpringBoot的条件 注解可支持如下几种条件:

  1. 类条件注解:@ConditionalOnClass(表示某些类存在时,可通过Value或 name指定所要求存在的类,value属性是 被检查类的 Class对象;name属性是被检查类的全限定类名的字符串形式)、@ConditionalOnMissingClass(某些类不存在时,只能通过value属性指定不存在的类,value属性值只能是被检查类的全限定类名的字符串形式)
  2. Bean条件注解 :@ConditionalOnMissingBean、@ConditionalOnSingleCandidate、@ConditionalOnBean、@ConditionalOnMissingFilterBean
  3. 属性条件注解:@ConditionalOnProperity
  4. 资源条件注解:@ConditionalOnResource
  5. Web应用条件注解:@ConditionalOnWebApplication、@ConditionalOnNotWebApplication、@ConditionalOnWarDeployment
  6. SpEL表达式条件注解:@ConditionalOnExpression
  7. 特殊条件注解:@ConditionalOnCloudPlatform、@ConditionalOnJava、@ConditionalOnJndi、@ConditionalOnRepositoryType

代码示例:

java 复制代码
@Configuration(proxyBeanMethods = false)
// 仅当com.mysql.cj.jdbc.Driver类存在时该配置类生效
@ConditionalOnClass(name = "com.mysql.cj.jdbc.Driver")
public class FkConfig
{
   @Bean
   public MyBean myBean()
   {
      return new MyBean();
   }
}

@ConditionalOnMissingBean、@ConditionalOnSingleCandidate、@ConditionalOnBean可指定 如下属性:

  1. Class<? extends Annotation>[] annotattion:指定 要检查的 Bean必须用该属性指定的注解修饰
  2. Class<?>[] ignored:指定要忽略哪些类型 的Bean。该属性及ignoredType 仅对@ConditionalOnMissingBean注解有效
  3. String[] ignoredType :与ignored属性的作用相同,只不过该属性用字符串形式 的全限定类名
  4. String[] name:指定要检查的Bean的ID
  5. search:指定搜索目标Bean的 搜索策略、支持CURRENT(仅在容器中搜索)、ACESTORS(仅在祖先容器中搜索)、ALL(在所有容器中搜索)三个枚举值
  6. Class<?> [] value:指定要检查的Bean的类型
  7. String[] type:与value属性作用相同,只不过该属性用字符串形式 的全限定类名

@ConditionalOnSingleCandidate注解相当于@ConditionalOnMissingBean的增强版,不仅要求被检查的Bean必须存在,而且只能有一个"候选者"--能满足byType依赖注入条件。

如果@ConditionalOnMissingBean、@ConditionalOnBean注解不指定任何属性,默认根据目标Bean的类型进行检查,默认检查被修饰的方法返回的Bean类型,代码示例:

java 复制代码
// 仅当容器中不存在名为myService的Bean时,才创建该Bean   
@ConditionalOnMissingBean
@Bean
public MyService myService()
{
   ...
}

// 当容器中不存在名为jdbcTemplate的Bean时,才创建该Bean
@ConditionalOnMissingBean(name="jdbcTemplate")
@Bean
public JdbcTemplate JdbcTemplate()
{
   ...
}

@ConditionalOnMissingFilterBean相当于@ConditionalOnMissingBean的特殊版本,专门检查容器中是否有指定类型的javax.servlet.Filter,因此只能通过value指定要检查的Filter的类型。

@ConditionalOnProperity注解 用于检查特定属性是否具有指定的属性值。该注解支持如下属性:

  1. String[] value:指定要检查的属性
  2. String[] name:指定value属性的别名
  3. String havingValue:被检查属性必须具有的属性值
  4. String prefix:自动为各属性名添加该属性指定的前缀
  5. boolean matchMissing:指定当属性未设置属性值时,是否通过检查

代码示例:

java 复制代码
@Configuration(proxyBeanMethods = false)
public class FkConfig
{
   @Bean
   // 只有当org.fkjava.test属性具有foo属性值时,下面配置方法才会生效
   @ConditionalOnProperty(name = "test", havingValue = "foo",
         prefix = "org.fkjava")
   public DateFormat dateFormat()
   {
      return DateFormat.getDateInstance();
   }
}

启动类代码:

java 复制代码
@SpringBootApplication
public class App
{
   public static void main(String[] args)
   {
      // 创建Spring容器、运行Spring Boot应用
      var ctx = SpringApplication.run(App.class, args);
      System.out.println(ctx.getBean("dateFormat"));
   }
}

此时直接运行程序会有异常。

在application.properties文件添加如下配置:

java 复制代码
org.fkjava.test=foo

运行结果如下

@ConditionalOnResource的作用很简单,它要求指定的资源必须存在,修饰的配置类才会生效。使用该注解只需指定resource属性,该属性指定必须存在的资源。

@ConditionalOnWebApplication要求当前应用必须是Web应用时,修饰 的配置类才会生效。可通过type属性指定Web应用类型。该属性支持如下三个枚举值:

  1. ANY:任何Web应用
  2. REACTIVE:当应用时反应式Web应用时
  3. SERVLET:基于servlet的Web应用

代码示例:

java 复制代码
@Configuration(proxyBeanMethods = false)
public class FkConfig
{
   @Bean
   // 只有当前应用是反应式Web应用时,该配置才会生效
   @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
   public DateFormat dateFormat()
   {
      return DateFormat.getDateInstance();
   }
}

启动类:

java 复制代码
@SpringBootApplication
public class App
{
   public static void main(String[] args)
   {
      var app = new SpringApplication(App.class);
      // 设置Web应用的类型,如果不设置则使用默认的类型:
      // 如果有Sping Web依赖,自动是基于Servlet的Web应用
      // 如果有Sping WebFlux依赖,自动是反应式Web应用
      app.setWebApplicationType(WebApplicationType.REACTIVE);  // ①
      // 创建Spring容器、运行Spring Boot应用
      var ctx = app.run(args);
      System.out.println(ctx.getBean("dateFormat"));
   }
}

@ConditionalOnNotWebApplication要求当前应用不是Web应用时,修饰的配置类或方法才生效

@ConditionalOnWarDeployment要求当前应用以War包部署 到Web服务器或应用服务器中时(不以独立的java程序的方式运行),才生效。

@ConditionalOnNotWebApplication、@ConditionalOnWarDeployment这2个注解使用简单,不需要指定任何属性

@ConditionalOnExpression要求指定SpEL表达式的值为true,所修饰的配置类或方法才会生效。代码示例:

java 复制代码
@Configuration(proxyBeanMethods = false)
public class FkConfig
{
   @Bean
   public User user()
   {
      return new User("fkjava", true);
   }
   @Bean
   // 只有当user.active表达式为true时,该方法才生效。也就是容器中User Bean的active属性为true时,该方法才生效
   @ConditionalOnExpression("user.active")
   public DateFormat dateFormat()
   {
      return DateFormat.getDateInstance();
   }
}

@ConditionalOnCloudPlatform要求应用被部署在特定云平台,修饰的配置类或方法才生效。可通过value属性指定要求的云平台,支持如下枚举值:

  1. CLOUD_FOUNDRY
  2. HEROKU
  3. KUBERNETES
  4. SAP

@ConditionalOnJava对目标平台的java版本进行检测,既可以要求java版本是某个具体的版本,也可以要求高于或低于某个版本。可指定如下两个属性:

  1. JavaVersion value:指定要求的java版本
  2. ConditionalOnJava.Range range:该属性支持EQUAL_OR_NEWER(大于或等于某版本)和OLDER_THAN(小于某版本)两个枚举值。如果不指定该属性,则要求java版本必须是value属性所指定的版本。

代码示例:

java 复制代码
@Configuration(proxyBeanMethods = false)
public class FkConfig
{
   @Bean
   // 只有当目标平台的Java版本是11或更新的平台时,该方法才生效
   @ConditionalOnJava(value = JavaVersion.ELEVEN,range = ConditionalOnJava.Range.EQUAL_OR_NEWER)
   public DateFormat dateFormat()
   {
      return DateFormat.getDateInstance();
   }
}

@ConditionalOnJndi要求指定JNDI必须存在,通过value属性指定要检查的JNDI。

@ConditionalOnRepositoryType要求特定的Spring Data Repository被启用时,修饰的配置类或方法才会生效。

自定义条件注解

自定义条件注解的关键就是要有一个Condition实现类,该类负责条件注解的处理逻辑--它所实现的matches()方法决定了条件注解的要求是否得到满足。

代码示例:Condition实现类

java 复制代码
public class MyCondition implements Condition
{
   @Override
   public boolean matches(ConditionContext context,
         AnnotatedTypeMetadata metadata)
   {
      // 获取@ConditionalCustom注解的全部属性
      Map<String, Object> map = metadata.getAnnotationAttributes(
            ConditionalCustom.class.getName());
      // 获取注解的value属性值(String[]数组)
      String[] vals = (String[]) map.get("value");
      Environment env = context.getEnvironment();
      // 遍历每个属性值
      for (Object val : vals)
      {
         // 如果某个属性值对应的配置属性不存在,返回false
         if (env.getProperty(val.toString()) == null)
         {
            return false;
         }
      }
      return true;
   }
}

此处逻辑是要求value属性所指定的所有配置属性必须存在,至于属性值是什么无所谓,这些属性是否有值也无所谓。

自定义条件注解的代码:

java 复制代码
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 指定Conditional的实现类
@Conditional(MyCondition.class)
public @interface ConditionalCustom
{
   String[] value() default {};
}

使用自定义条件注解:

java 复制代码
@Configuration(proxyBeanMethods = false)
public class FkConfig
{
   @Bean
   // 只有当org.fkjava.test和org.crazyit.abc两个配置属性存在时该方法才生效
   @ConditionalCustom({"org.fkjava.test", "org.crazyit.abc"})
   public DateFormat dateFormat()
   {
      return DateFormat.getDateInstance();
   }
}

自定义自动配置

开发自定义的自动配置很简单,分为两步:

  1. 使用@Configuration和条件注解自定义配置类
  2. 在META-INF/spring.factories文件中注册自动配置类

为了演示,先自行开发一个funny框架,功能是用文件或数据库保存程序输出信息。

  1. 先新建一个maven项目,pom.xml如下
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>org.crazyit</groupId>
   <artifactId>funny</artifactId>
   <version>1.0-SNAPSHOT</version>
   <name>funny</name>

   <properties>
      <!-- 定义所使用的Java版本和源代码所用的字符集 -->
      <maven.compiler.source>11</maven.compiler.source>
      <maven.compiler.target>11</maven.compiler.target>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   </properties>

   <dependencies>
      <!-- MySQL驱动依赖 -->
      <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>
         <version>8.0.22</version>
      </dependency>
      <dependency>
         <groupId>org.slf4j</groupId>
         <artifactId>slf4j-api</artifactId>
         <version>1.7.30</version>
         <optional>true</optional>
      </dependency>
   </dependencies>
</project>

开发WriterTemplate类

java 复制代码
public class WriterTemplate
{
   Logger log = LoggerFactory.getLogger(this.getClass());
   private final DataSource dataSource;
   private Connection conn;
   private final File dest;
   private final Charset charset;
   private RandomAccessFile raf;

   public WriterTemplate(DataSource dataSource) throws SQLException
   {
      this.dataSource = dataSource;
      this.dest = null;
      this.charset = null;
      if (Objects.nonNull(this.dataSource))
      {
         log.debug("==========获取数据库连接==========");
         this.conn = dataSource.getConnection();
      }
   }

   public WriterTemplate(File dest, Charset charset) throws FileNotFoundException
   {
      this.dest = dest;
      this.charset = charset;
      this.dataSource = null;
      this.raf = new RandomAccessFile(this.dest, "rw");
   }

   public void write(String message) throws IOException, SQLException
   {
      if (Objects.nonNull(this.conn))
      {
         // 查询当前数据库的funny_message表是否存在
         ResultSet rs = conn.getMetaData().getTables(conn.getCatalog(), null,
               "funny_message", null);
         //  如果funny_message表不存在
         if (!rs.next())
         {
            log.debug("~~~~~~创建funny_message表~~~~~~");
            conn.createStatement().execute("create table funny_message " +
                  "(id int primary key auto_increment, message_text text)");
            rs.close();
         }
         log.debug("~~~~~~输出到数据表~~~~~~");
         // 插入要输出的字符串
         conn.createStatement().executeUpdate("insert into " +
               "funny_message values (null, '" + message + "')");
      }
      else
      {
         log.debug("~~~~~~输出到文件~~~~~~");
         // 输出到文件
         raf.seek(this.dest.length());
         raf.write((message + "\n").getBytes(this.charset));
      }
   }
   // 关闭资源
   public void close() throws SQLException, IOException
   {
      if (this.conn != null)
      {
         this.conn.close();
      }
      if (this.raf != null)
      {
         this.raf.close();
      }
   }
}

然后使用 mvn install 命令打成jar包并安装到本地资源库。

在Starter的项目中引入上面的jar包。pom.xml如下:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <!-- 指定继承spring-boot-starter-parent POM文件 -->
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.4.2</version>
      <relativePath/>
   </parent>

   <!-- 定义基本的项目信息 -->
   <groupId>org.crazyit</groupId>
   <artifactId>funny-spring-boot-starter</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>funny-spring-boot-starter</name>

   <properties>
      <!-- 定义所使用的Java版本和源代码所用的字符集 -->
      <java.version>11</java.version>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   </properties>

   <dependencies>
      <!-- Spring Boot Starter依赖 -->
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter</artifactId>
      </dependency>
      <!-- 依赖自定义的funny框架 -->
      <dependency>
         <groupId>org.crazyit</groupId>
         <artifactId>funny</artifactId>
         <version>1.0-SNAPSHOT</version>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-configuration-processor</artifactId>
         <optional>true</optional>
      </dependency>
   </dependencies>
</project>

然后在开发中编写自定义配置类:

java 复制代码
@Configuration
// 当WriterTemplate类存在时配置生效
// WriterTemplate类是自己编写的工具项目中的类
@ConditionalOnClass(WriterTemplate.class)
// 启用FunnyProperties属性处理类
@EnableConfigurationProperties(FunnyProperties.class)
// 让该自动配置位于DataSourceAutoConfiguration自动配置之后处理
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class FunnyAutoConfiguration
{
   // FunnyProperties类负责加载配置属性
   private final FunnyProperties properties;

   public FunnyAutoConfiguration(FunnyProperties properties)
   {
      this.properties = properties;
   }

   @Bean(destroyMethod = "close")
   // 当单例的DataSource Bean存在时配置生效
   @ConditionalOnSingleCandidate(DataSource.class)
   // 只有当容器中没有WriterTemplate Bean时,该配置才会生效
   @ConditionalOnMissingBean
   // 通过@AutoConfigureOrder注解指定该配置方法
   // 比下一个配置WriterTemplate的方法的优先级更高
    // @AutoConfigureOrder 数值越小,优先级越高
   @AutoConfigureOrder(99)
   public WriterTemplate writerTemplate(DataSource dataSource) throws SQLException
   {
      return new WriterTemplate(dataSource);
   }

   @Bean(destroyMethod = "close")
   // 只有当前面的WriterTemplate配置没有生效时,该方法的配置才会生效
   @ConditionalOnMissingBean
   @AutoConfigureOrder(199)
   public WriterTemplate writerTemplate2() throws FileNotFoundException
   {
      File f = new File(this.properties.getDest());
      Charset charset = Charset.forName(this.properties.getCharset());
      return new WriterTemplate(f, charset);
   }
}

上面代码中的FunnyProperties类

java 复制代码
// 定义属性处理类
@ConfigurationProperties(prefix = FunnyProperties.FUNNY_PREFIX)
public class FunnyProperties
{
   public static final String FUNNY_PREFIX = "org.crazyit.funny";
   private String dest;
   private String charset;
// 省略getter、setter
}

接下来在META-INF/spring.factories文件中注册自动配置类

properties 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.crazyit.funny.autoconfigure.FunnyAutoConfiguration

然后使用 mvn install 命令打成jar包,并安装到maven本地资源库中,就会在自己的本地资源库中找到该jar包,这样就完成了自定义配置的实现。

创建自定义的Starter

一个完整的SpringBoot Starter包含一下两个组件:

  1. 自动配置模块(auto-configure):包含自动配置类和spring.factories文件
  2. Starter模块:负责管理自动配置模块和第三方依赖。简而言之,添加本Starter就能使用该自动配置。

由此看出,Starter不包含任何Class文件,只管理愿意来。如果查看官方提供的jar就会发现,它所有自动配置类的Class都由spring-boot-autoconfigure.jar提供,而各个xxx-starter.jar并未提供任何Class文件,只是在这些jar下的相同路径下提供了一个xxx-starter.pom文件,该文件指定Starter管理的自动依赖模块和第三方依赖。

SpringBoot为自动配置包和Starter包提供推荐命名

  1. 自动配置包的推荐名:xxx-spring-boot
  2. Starter包的推荐名:xxx-spring-boot-starter

对于第三方Starter不要使用spring-boot-starter-xxx这种方式,这是官方使用的。

有了自定义的Starter后,使用起来和官方的没有区别,比如

添加依赖:

xml 复制代码
<!-- 自定义的funny-spring-boot-starter依赖 -->
<dependency>
   <groupId>org.crazyit</groupId>
   <artifactId>funny-spring-boot-starter</artifactId>
   <version>0.0.1-SNAPSHOT</version>
</dependency>

在application.properties文件添加配置

properties 复制代码
org.crazyit.funny.dest=f:/abc-98765.txt
org.crazyit.funny.charset=UTF-8
# 指定连接数据库的信息
spring.datasource.url=jdbc:mysql://localhost:3306/funny?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=32147
# 配置funny框架的日志级别为debug
logging.level.org.crazyit.funny = debug

主类的代码:

java 复制代码
@SpringBootApplication
public class App
{
   public static void main(String[] args) throws IOException, SQLException
   {
      // 创建Spring容器、运行Spring Boot应用
      var ctx = SpringApplication.run(App.class, args);
      // 获取自动配置的WriterTemplate
      WriterTemplate writerTemplate = ctx.getBean(WriterTemplate.class);
      writerTemplate.write("自动配置其实很简单");
   }
}
相关推荐
lxsy5 小时前
spring-ai-alibaba-deepresearch 学习(十三)——ResearcherNode
java·源码分析·deepresearch·ai-alibaba
ShineWinsu5 小时前
对于单链表相关经典算法题:206. 反转链表及876. 链表的中间结点的解析
java·c语言·数据结构·学习·算法·链表·力扣
迦蓝叶5 小时前
JAiRouter 配置文件重构纪实 ——基于单一职责原则的模块化拆分与内聚性提升
java·网关·ai·重构·openai·prometheus·单一职责原则
ST.J5 小时前
系统架构思考20241204
java·笔记·系统架构
TDengine (老段)6 小时前
TDengine 时间函数 TIMETRUNCATE 用户手册
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
给我个面子中不7 小时前
JUC、JVM八股补充
java·开发语言·jvm
mask哥7 小时前
详解flink性能优化
java·大数据·微服务·性能优化·flink·kafka·stream
hqxstudying7 小时前
Kafka 深入研究:从架构革新到性能优化的全面解析
java·开发语言·微服务·kafka·springcloud
失散139 小时前
并发编程——17 CPU缓存架构详解&高性能内存队列Disruptor实战
java·缓存·架构·并发编程
only-qi13 小时前
146. LRU 缓存
java·算法·缓存