平淡幽远 發表於 2025-7-21 11:15:00

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

<p>在实际开发中,仅靠SpringBoot的自动配置是远远不够的,比如要访问多个数据源,自动配置就完全无能为力了。</p>
<h1 id="自动配置的本质">自动配置的本质</h1>
<p>本质就是在容器中预配置要整合的框架所需的基础Bean。</p>
<p>以MyBatis为例,spring整合MyBatis无非就是完成以下事情:</p>
<ol>
<li>配置SqlSessionFactory Bean,当然,该Bean需要注入一个DataSource</li>
<li>配置SqlSessionTemplate Bean,将上面的SqlSessionFactory 注入该Bean</li>
<li>注册Mapper组件的自动扫描,相当于添加&lt;mybatis:scan.../&gt;元素</li>
</ol>
<p>自动配置非常简单,无非就是有框架提供一个@Configuration修饰的配置类(相当于传统的xml配置文件),在该配置类中用@Bean预先配置默认的SqlSessionFactory、SqlSessionTemplate,并注册Mapper组件的自动扫描即可。</p>
<p>比如MybatisAutoConfiguration源代码:</p>
<pre><code class="language-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&lt;ConfigurationCustomizer&gt; configurationCustomizers;
    private final List&lt;SqlSessionFactoryBeanCustomizer&gt; sqlSessionFactoryBeanCustomizers;

    public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider&lt;Interceptor[]&gt; interceptorsProvider, ObjectProvider&lt;TypeHandler[]&gt; typeHandlersProvider, ObjectProvider&lt;LanguageDriver[]&gt; languageDriversProvider, ResourceLoader resourceLoader, ObjectProvider&lt;DatabaseIdProvider&gt; databaseIdProvider, ObjectProvider&lt;List&lt;ConfigurationCustomizer&gt;&gt; configurationCustomizersProvider, ObjectProvider&lt;List&lt;SqlSessionFactoryBeanCustomizer&gt;&gt; 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() &amp;&amp; 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&lt;String&gt; factoryPropertyNames = (Set) Stream.of((new BeanWrapperImpl(SqlSessionFactoryBean.class)).getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collectors.toSet());
      Class&lt;? extends LanguageDriver&gt; defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
      if (factoryPropertyNames.contains("scriptingLanguageDrivers") &amp;&amp; !ObjectUtils.isEmpty(this.languageDrivers)) {
            factory.setScriptingLanguageDrivers(this.languageDrivers);
            if (defaultLanguageDriver == null &amp;&amp; this.languageDrivers.length == 1) {
                defaultLanguageDriver = this.languageDrivers.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 &amp;&amp; !StringUtils.hasText(this.properties.getConfigLocation())) {
            configuration = new org.apache.ibatis.session.Configuration();
      }

      if (configuration != null &amp;&amp; !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&lt;String&gt; packages = AutoConfigurationPackages.get(this.beanFactory);
                if (org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.logger.isDebugEnabled()) {
                  packages.forEach((pkg) -&gt; {
                        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&lt;String&gt; 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 &amp;&amp; this.beanFactory instanceof ListableBeanFactory) {
                  ListableBeanFactory listableBeanFactory = (ListableBeanFactory)this.beanFactory;
                  Optional&lt;String&gt; sqlSessionTemplateBeanName = Optional.ofNullable(this.getBeanNameForType(SqlSessionTemplate.class, listableBeanFactory));
                  Optional&lt;String&gt; sqlSessionFactoryBeanName = Optional.ofNullable(this.getBeanNameForType(SqlSessionFactory.class, listableBeanFactory));
                  if (!sqlSessionTemplateBeanName.isPresent() &amp;&amp; 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&lt;?&gt; type, ListableBeanFactory factory) {
            String[] beanNames = factory.getBeanNamesForType(type);
            return beanNames.length &gt; 0 ? beanNames : null;
      }
    }
}
</code></pre>
<p>开开发完自动配置类后,还需要使用META-INF/spring.factories文件来定义自动配置类,比如:</p>
<pre><code class="language-java"># Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
</code></pre>
<p>自动配置类只能通过META-INF/spring.factories来加载,并确保它们处于一个特殊的包空间内,尤其不能让他们变成普通@ComponentScan的目标。此外,自动配置类不应该使用@ComponentScan来扫描其他组件,如果需要加载其他配置文件,应使用@Import来加载。</p>
<p>如果要为自动配置类指定加载顺序,可使用以下注解:</p>
<ol>
<li>@AutoConfigureAfter:指定被修饰的类必须在一个或多个自动配置类之后加载</li>
<li>@AutoConfigureBefore:指定被修饰的类必须在一个或多个自动配置类之前加载</li>
</ol>
<p>如果自动配置包中包含多个自动配置类,且以特定的顺序来加载,可使用@AutoConfigureOrder来修饰它们,@AutoConfigureOrder类似于@Order注解,只不过专门修饰自动配置类。</p>
<h1 id="条件注解">条件注解</h1>
<p>条件注解用于修饰@Configuration类 或@Bean方法等,表示只有条件有效时,被修饰的 配置类或配置方法才生效。SpringBoot的条件注解可支持如下几种条件:</p>
<ol>
<li>类条件注解:@ConditionalOnClass(表示某些类存在时,可通过Value或 name指定所要求存在的类,value属性是 被检查类的   Class对象;name属性是被检查类的全限定类名的字符串形式)、@ConditionalOnMissingClass(某些类不存在时,只能通过value属性指定不存在的类,value属性值只能是被检查类的全限定类名的字符串形式)</li>
<li>Bean条件注解 :@ConditionalOnMissingBean、@ConditionalOnSingleCandidate、@ConditionalOnBean、@ConditionalOnMissingFilterBean</li>
<li>属性条件注解:@ConditionalOnProperity</li>
<li>资源条件注解:@ConditionalOnResource</li>
<li>Web应用条件注解:@ConditionalOnWebApplication、@ConditionalOnNotWebApplication、@ConditionalOnWarDeployment</li>
<li>SpEL表达式条件注解:@ConditionalOnExpression</li>
<li>特殊条件注解:@ConditionalOnCloudPlatform、@ConditionalOnJava、@ConditionalOnJndi、@ConditionalOnRepositoryType</li>
</ol>
<p>代码示例:</p>
<pre><code class="language-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();
   }
}
</code></pre>
<p>@ConditionalOnMissingBean、@ConditionalOnSingleCandidate、@ConditionalOnBean可指定 如下属性:</p>
<ol>
<li>Class&lt;? extends Annotation&gt;[] annotattion:指定要检查的 Bean必须用该属性指定的注解修饰</li>
<li>Class&lt;?&gt;[] ignored:指定要忽略哪些类型 的Bean。该属性及ignoredType 仅对@ConditionalOnMissingBean注解有效</li>
<li>String[] ignoredType :与ignored属性的作用相同,只不过该属性用字符串形式 的全限定类名</li>
<li>String[] name:指定要检查的Bean的ID</li>
<li>search:指定搜索目标Bean的 搜索策略、支持CURRENT(仅在容器中搜索)、ACESTORS(仅在祖先容器中搜索)、ALL(在所有容器中搜索)三个枚举值</li>
<li>Class&lt;?&gt; [] value:指定要检查的Bean的类型</li>
<li>String[] type:与value属性作用相同,只不过该属性用字符串形式 的全限定类名</li>
</ol>
<p>@ConditionalOnSingleCandidate注解相当于@ConditionalOnMissingBean的增强版,不仅要求被检查的Bean必须存在,而且只能有一个“候选者”--能满足byType依赖注入条件。</p>
<p>如果@ConditionalOnMissingBean、@ConditionalOnBean注解不指定任何属性,默认根据目标Bean的类型进行检查,默认检查被修饰的方法返回的Bean类型,代码示例:</p>
<pre><code class="language-java">// 仅当容器中不存在名为myService的Bean时,才创建该Bean   
@ConditionalOnMissingBean
@Bean
public MyService myService()
{
   ...
}

// 当容器中不存在名为jdbcTemplate的Bean时,才创建该Bean
@ConditionalOnMissingBean(name="jdbcTemplate")
@Bean
public JdbcTemplate JdbcTemplate()
{
   ...
}
</code></pre>
<p>@ConditionalOnMissingFilterBean相当于@ConditionalOnMissingBean的特殊版本,专门检查容器中是否有指定类型的javax.servlet.Filter,因此只能通过value指定要检查的Filter的类型。</p>
<p>@ConditionalOnProperity注解 用于检查特定属性是否具有指定的属性值。该注解支持如下属性:</p>
<ol>
<li>String[] value:指定要检查的属性</li>
<li>String[] name:指定value属性的别名</li>
<li>String havingValue:被检查属性必须具有的属性值</li>
<li>String prefix:自动为各属性名添加该属性指定的前缀</li>
<li>boolean matchMissing:指定当属性未设置属性值时,是否通过检查</li>
</ol>
<p>代码示例:</p>
<pre><code class="language-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();
   }
}
</code></pre>
<p>启动类代码:</p>
<pre><code class="language-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"));
   }
}
</code></pre>
<p>此时直接运行程序会有异常。</p>
<p><img src="https://raw.githubusercontent.com/growingbambi/typora/master/ali2_7_1.png"></p>
<p>在application.properties文件添加如下配置:</p>
<pre><code class="language-java">org.fkjava.test=foo
</code></pre>
<p>运行结果如下</p>
<p><img src="https://raw.githubusercontent.com/growingbambi/typora/master/ali2_7_2.png"></p>
<p>@ConditionalOnResource的作用很简单,它要求指定的资源必须存在,修饰的配置类才会生效。使用该注解只需指定resource属性,该属性指定必须存在的资源。</p>
<p>@ConditionalOnWebApplication要求当前应用必须是Web应用时,修饰 的配置类才会生效。可通过type属性指定Web应用类型。该属性支持如下三个枚举值:</p>
<ol>
<li>ANY:任何Web应用</li>
<li>REACTIVE:当应用时反应式Web应用时</li>
<li>SERVLET:基于servlet的Web应用</li>
</ol>
<p>代码示例:</p>
<pre><code class="language-java">@Configuration(proxyBeanMethods = false)
public class FkConfig
{
   @Bean
   // 只有当前应用是反应式Web应用时,该配置才会生效
   @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
   public DateFormat dateFormat()
   {
      return DateFormat.getDateInstance();
   }
}
</code></pre>
<p>启动类:</p>
<pre><code class="language-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"));
   }
}
</code></pre>
<p>@ConditionalOnNotWebApplication要求当前应用不是Web应用时,修饰的配置类或方法才生效</p>
<p>@ConditionalOnWarDeployment要求当前应用以War包部署 到Web服务器或应用服务器中时(不以独立的java程序的方式运行),才生效。</p>
<p>@ConditionalOnNotWebApplication、@ConditionalOnWarDeployment这2个注解使用简单,不需要指定任何属性</p>
<p>@ConditionalOnExpression要求指定SpEL表达式的值为true,所修饰的配置类或方法才会生效。代码示例:</p>
<pre><code class="language-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();
   }
}
</code></pre>
<p>@ConditionalOnCloudPlatform要求应用被部署在特定云平台,修饰的配置类或方法才生效。可通过value属性指定要求的云平台,支持如下枚举值:</p>
<ol>
<li>CLOUD_FOUNDRY</li>
<li>HEROKU</li>
<li>KUBERNETES</li>
<li>SAP</li>
</ol>
<p>@ConditionalOnJava对目标平台的java版本进行检测,既可以要求java版本是某个具体的版本,也可以要求高于或低于某个版本。可指定如下两个属性:</p>
<ol>
<li>JavaVersion value:指定要求的java版本</li>
<li>ConditionalOnJava.Range range:该属性支持EQUAL_OR_NEWER(大于或等于某版本)和OLDER_THAN(小于某版本)两个枚举值。如果不指定该属性,则要求java版本必须是value属性所指定的版本。</li>
</ol>
<p>代码示例:</p>
<pre><code class="language-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();
   }
}
</code></pre>
<p>@ConditionalOnJndi要求指定JNDI必须存在,通过value属性指定要检查的JNDI。</p>
<p>@ConditionalOnRepositoryType要求特定的Spring Data Repository被启用时,修饰的配置类或方法才会生效。</p>
<h1 id="自定义条件注解">自定义条件注解</h1>
<p>自定义条件注解的关键就是要有一个Condition实现类,该类负责条件注解的处理逻辑--它所实现的matches()方法决定了条件注解的要求是否得到满足。</p>
<p>代码示例:Condition实现类</p>
<pre><code class="language-java">public class MyCondition implements Condition
{
   @Override
   public boolean matches(ConditionContext context,
         AnnotatedTypeMetadata metadata)
   {
      // 获取@ConditionalCustom注解的全部属性
      Map&lt;String, Object&gt; 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;
   }
}
</code></pre>
<p>此处逻辑是要求value属性所指定的所有配置属性必须存在,至于属性值是什么无所谓,这些属性是否有值也无所谓。</p>
<p>自定义条件注解的代码:</p>
<pre><code class="language-java">@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 指定Conditional的实现类
@Conditional(MyCondition.class)
public @interface ConditionalCustom
{
   String[] value() default {};
}
</code></pre>
<p>使用自定义条件注解:</p>
<pre><code class="language-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();
   }
}
</code></pre>
<h1 id="自定义自动配置">自定义自动配置</h1>
<p>开发自定义的自动配置很简单,分为两步:</p>
<ol>
<li>使用@Configuration和条件注解自定义配置类</li>
<li>在META-INF/spring.factories文件中注册自动配置类</li>
</ol>
<p>为了演示,先自行开发一个funny框架,功能是用文件或数据库保存程序输出信息。</p>
<ol>
<li>先新建一个maven项目,pom.xml如下</li>
</ol>
<pre><code class="language-xml">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;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"&gt;
   &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;

   &lt;groupId&gt;org.crazyit&lt;/groupId&gt;
   &lt;artifactId&gt;funny&lt;/artifactId&gt;
   &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;
   &lt;name&gt;funny&lt;/name&gt;

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

   &lt;dependencies&gt;
      &lt;!-- MySQL驱动依赖 --&gt;
      &lt;dependency&gt;
         &lt;groupId&gt;mysql&lt;/groupId&gt;
         &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;
         &lt;version&gt;8.0.22&lt;/version&gt;
      &lt;/dependency&gt;
      &lt;dependency&gt;
         &lt;groupId&gt;org.slf4j&lt;/groupId&gt;
         &lt;artifactId&gt;slf4j-api&lt;/artifactId&gt;
         &lt;version&gt;1.7.30&lt;/version&gt;
         &lt;optional&gt;true&lt;/optional&gt;
      &lt;/dependency&gt;
   &lt;/dependencies&gt;
&lt;/project&gt;
</code></pre>
<p>开发WriterTemplate类</p>
<pre><code class="language-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();
      }
   }
}
</code></pre>
<p>然后使用 mvn install 命令打成jar包并安装到本地资源库。</p>
<p>在Starter的项目中引入上面的jar包。pom.xml如下:</p>
<pre><code class="language-xml">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;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"&gt;
   &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;

   &lt;!-- 指定继承spring-boot-starter-parent POM文件 --&gt;
   &lt;parent&gt;
      &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
      &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;
      &lt;version&gt;2.4.2&lt;/version&gt;
      &lt;relativePath/&gt;
   &lt;/parent&gt;

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

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

   &lt;dependencies&gt;
      &lt;!-- Spring Boot Starter依赖 --&gt;
      &lt;dependency&gt;
         &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
         &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt;
      &lt;/dependency&gt;
      &lt;!-- 依赖自定义的funny框架 --&gt;
      &lt;dependency&gt;
         &lt;groupId&gt;org.crazyit&lt;/groupId&gt;
         &lt;artifactId&gt;funny&lt;/artifactId&gt;
         &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;
      &lt;/dependency&gt;
      &lt;dependency&gt;
         &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
         &lt;artifactId&gt;spring-boot-configuration-processor&lt;/artifactId&gt;
         &lt;optional&gt;true&lt;/optional&gt;
      &lt;/dependency&gt;
   &lt;/dependencies&gt;
&lt;/project&gt;
</code></pre>
<p>然后在开发中编写自定义配置类:</p>
<pre><code class="language-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);
   }
}
</code></pre>
<p>上面代码中的FunnyProperties类</p>
<pre><code class="language-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
}
</code></pre>
<p>接下来在META-INF/spring.factories文件中注册自动配置类</p>
<pre><code class="language-properties">org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.crazyit.funny.autoconfigure.FunnyAutoConfiguration
</code></pre>
<p>然后使用 mvn install 命令打成jar包,并安装到maven本地资源库中,就会在自己的本地资源库中找到该jar包,这样就完成了自定义配置的实现。</p>
<h1 id="创建自定义的starter">创建自定义的Starter</h1>
<p>一个完整的SpringBoot Starter包含一下两个组件:</p>
<ol>
<li>自动配置模块(auto-configure):包含自动配置类和spring.factories文件</li>
<li>Starter模块:负责管理自动配置模块和第三方依赖。简而言之,添加本Starter就能使用该自动配置。</li>
</ol>
<p>由此看出,Starter不包含任何Class文件,只管理依赖。如果查看官方提供的jar就会发现,它所有自动配置类的Class都由spring-boot-autoconfigure.jar提供,而各个xxx-starter.jar并未提供任何Class文件,只是在这些jar下的相同路径下提供了一个xxx-starter.pom文件,该文件指定Starter管理的自动依赖模块和第三方依赖。</p>
<p>SpringBoot为自动配置包和Starter包提供推荐命名</p>
<ol>
<li>自动配置包的推荐名:xxx-spring-boot</li>
<li>Starter包的推荐名:xxx-spring-boot-starter</li>
</ol>
<p>对于第三方Starter不要使用spring-boot-starter-xxx这种方式,这是官方使用的。</p>
<p>有了自定义的Starter后,使用起来和官方的没有区别,比如</p>
<p>添加依赖:</p>
<pre><code class="language-xml">&lt;!-- 自定义的funny-spring-boot-starter依赖 --&gt;
&lt;dependency&gt;
   &lt;groupId&gt;org.crazyit&lt;/groupId&gt;
   &lt;artifactId&gt;funny-spring-boot-starter&lt;/artifactId&gt;
   &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>在application.properties文件添加配置</p>
<pre><code class="language-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
</code></pre>
<p>主类的代码:</p>
<pre><code class="language-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("自动配置其实很简单");
   }
}
</code></pre>


</div>
<div id="MySignature" role="contentinfo">
    <p>本文来自博客园,作者:NE_STOP,转载请注明原文链接:https://www.cnblogs.com/alineverstop/p/18995435</p><br><br>
来源:https://www.cnblogs.com/alineverstop/p/18995435
頁: [1]
查看完整版本: SpringBoot--如何创建自己的自动配置