• 【SpringBoot的自动配置--下篇】架构师如何自定义自己的条件注解与自动配置


    本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringBoot相关知识相关知识,打造完整的云原生学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获,也请大家多多支持。
    专栏地址:SpringBoot专栏
    本文涉及的代码都已放在gitee上:gitee地址
    如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。

    Spring Boot的核心功能就是为整合第三方框架提供自动配置,而本文则带着大家实现了自己的自动配置和Starter,一旦真正掌握了本文的内容,就会对Spring Boot产生“一览众山小”的感觉。

    文章目录

    自定义条件注解

    在SpringBoot中,所有自定义条件注解其实都是基于@Conditional而来的,使用@Conditional定义新条件注解关键就是要有一个Condition实现类,该Condition实现类就负责条件注解的处理逻辑,该实现类所实现的matches()方法决定了条件注解的要求是否得到满足。

    下面是自定义条件注解的Condition实现类的代码。

    • src/main/java/com/example/_003configtest/condition/MyCondition.java

      package com.example._003configtest.condition;

      import com.example._003configtest.annotation.ConditionalCustom;
      import org.springframework.context.annotation.Condition;
      import org.springframework.context.annotation.ConditionContext;
      import org.springframework.core.env.Environment;
      import org.springframework.core.type.AnnotatedTypeMetadata;

      import java.util.Map;

      public class MyCondition implements Condition {

      @Override
      public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
          //获取@ConditionalCustom注解的全部属性,其中ConditionalCustom是自定义的注解
          Map annotationAttributes = metadata.getAnnotationAttributes(ConditionalCustom.class.getName());
          //获取注解的value属性值
          String[] vals = (String[]) annotationAttributes.get("value");
          //env是application.properties或application.yml中配置的属性
          Environment env = context.getEnvironment();
          //遍历每个value的每个属性值
          for (String val : vals) {
              //如果某个属性值对应的配置属性不存在,则返回false
              if(env.getProperty(val.toString())== null){
                  return false;
              }
          }
          return true;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17

      }

    从上面的逻辑可以看到,自定义条件注解的处理逻辑比较简单:就是要求value属性所指定的所有配置属性必须存在,至于这些配置属性的值是什么无所谓,这些配置属性是否有值也无所谓。

    有了上面的Condition实现类之后,接下来即可基于@Conditional来定义自定义条件注解。下面是自定义条件注解的代码。

    • src/main/java/com/example/_003configtest/annotation/ConditionalCustom.java

      package com.example._003configtest.annotation;
      import com.example._003configtest.condition.MyCondition;
      import org.springframework.context.annotation.Conditional;

      import java.lang.annotation.*;

      @Target({ElementType.TYPE,ElementType.METHOD})
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      //只要通过@Conditional指定Condition实现类即可,该Condition实现类就会负责该条件注解的判断逻辑
      @Conditional(MyCondition.class)
      public @interface ConditionalCustom {

      String[] value() default {};
      
      • 1

      }

    下面的配置类示范了如何使用该自定义的条件注解:

    • src/main/java/com/example/_003configtest/config/MyConfigTest.java

      // proxyBeanMethods = true :单例模式,保证每个@Bean方法被调用多少次返回的组件都是同一个
      // proxyBeanMethods = false :原型模式,每个@Bean方法被调用多少次返回的组件都是新创建的
      @Configuration(proxyBeanMethods = true)
      public class MyConfigTest {
      @Bean
      //只有当applicaion.properties或application.yml中org.test1,org.test2两个配置属性都存在时才生效
      @ConditionalCustom({“org.test1”,“org.test2”})
      public MyBean myBean(){
      return new MyBean();
      }
      }

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

    org.test1 = 1
    org.test2 = 2
    
    • 1
    • 2

    运行测试发现成功获得了容器中对应的类:

    image-20220608230844951

    自定义自动配置

    开发自己的自动配置很简单,其实也就两步:

    1. 使用@Configuration和条件注解定义自动配置类。

    2. 在META-INF/spring.factories文件中注册自动配置类。

    为了清楚地演示Spring Boot自动配置的效果,避免引入第三方框架导致的额外复杂度,本例先自行开发一个funny框架,该框架的功能是用文件或数据库保存程序的输出信息。

    新建一个Maven项目funny(注意不是用SpringInitializr创建项目),为该项目添加mysql-connector-java和slf4j-api两个依赖。由于该项目是我们自己开发的框架,因此无须为该项目添加任何Spring Boot依赖。下面是该项目的pom.xml文件代码。

    
    
        4.0.0
    
        org.example
        funny
        1.0-SNAPSHOT
    
        
        
            8
            8
            UTF-8
        
    
        
    
            
            
                mysql
                mysql-connector-java
                8.0.27
            
    
            
            
                org.slf4j
                slf4j-api
                1.7.36
            
        
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    接下来为这个框架项目开发如下类。

    • src/main/java/io/WriterTemplate.java

      package io;

      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;

      import java.io.File;
      import java.io.FileNotFoundException;
      import java.io.IOException;
      import java.io.RandomAccessFile;
      import java.nio.charset.Charset;
      import java.sql.Connection;
      import java.sql.ResultSet;
      import java.sql.SQLException;
      import java.util.Objects;
      import javax.sql.DataSource;

      public class WriterTemplate {
      Logger log = LoggerFactory.getLogger(this.getClass());
      private final DataSource dataSource;
      private Connection conn;
      private 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)){
              //查询当前数据库的fnny_message表是否存在
              ResultSet rs = conn.getMetaData().getTables(conn.getCatalog(),null,"funny_message",null);
              //如果funy_message表不存在,需要创建表
              if(!rs.next()){
                  log.debug("~~~~~~~~~创建funny_message表~~~~~~~~~");
                  conn.createStatement().execute("create table funny_message " + "(id int primary key auto_increment,message_text text)");
              }
              log.debug("~~~~~~~~~输出到数据表~~~~~~~~~");
              //往数据库中插入数据
              conn.createStatement().executeUpdate("insert into " + "funny_message values(null,'" + message + "')");
              rs.close();
          }
          else{
              log.debug("~~~~~~~~~输出到文件~~~~~~~~~");
              raf.seek(this.dest.length());
              raf.write((message + "
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35

      ").getBytes(this.charset));
      }
      }

      //关闭资源
      public void close() throws SQLException,IOException{
          if(this.conn != null){
              this.conn.close();
          }
      
          if(this.raf != null){
              this.raf.close();
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10

      }

    该工具类根据是否传入 DataSource 来决定输出目标:如果为该工具类传入了DataSource,它就会向该数据源所连接的数据库中的funny_message表输出内容(如果该表不存在,该工具类将会自动建表);如果没有为该工具类传入DataSource,它就会向指定文件输出内容。

    接下来使用install打包到maven仓库:

    image-20220609112738611

    有了该框架之后,接下来为该框架开发自动配置。如果为整合现有的第三方框架开发自动配置,则可直接从这一步开始(因为框架已经存在了,直接为框架开发自动配置即可)。

    同样新建一个Maven项目funny-spring-boot-starter(为了方便可以用SpringInitializr创建项目),这个项目是自定义Starter项目,因此必须要有Spring Boot支持,将前面Spring Boot项目中的pom.xml文件复制过来,保留其中的spring-boot-starter依赖,并添加刚刚开发的funny框架的依赖。此外,由于该项目不是Spring Boot应用,因此不需要主类,也不需要运行,故删除其中的spring-boot-maven-plugin插件。修改后的pom.xml文件内容如下。

    
    
        4.0.0
        com.example
        funny-spring-boot-starter
        0.0.1-SNAPSHOT
        funny-spring-boot-starter
        funny-spring-boot-starter
    
        
            1.8
            UTF-8
            UTF-8
            2.3.7.RELEASE
        
    
        
            
            
                org.springframework.boot
                spring-boot-starter
            
    
            
            
                org.example
                funny
                1.0-SNAPSHOT
            
        
    
        
            
                
                    org.springframework.boot
                    spring-boot-dependencies
                    ${spring-boot.version}
                    pom
                    import
                
            
        
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    接下来定义如下自动配置类。

    • src/main/java/com/example/funnyspringbootstarter/autoconfig/FunnyAutoConfiguration.java

      package com.example.funnyspringbootstarter.autoconfig;

      import io.WriterTemplate;
      import org.springframework.boot.autoconfigure.AutoConfigureAfter;
      import org.springframework.boot.autoconfigure.AutoConfigureOrder;
      import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
      import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
      import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
      import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
      import org.springframework.boot.context.properties.EnableConfigurationProperties;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;

      import javax.sql.DataSource;
      import javax.xml.crypto.Data;
      import java.io.File;
      import java.io.FileNotFoundException;
      import java.nio.charset.Charset;
      import java.sql.SQLException;

      @Configuration
      //当WriteTemplate类存在时配置生效
      @ConditionalOnClass(WriterTemplate.class)
      //FunnyProperties是自定义的类,后面会定义,这里表示启动FunnyProperties
      @EnableConfigurationProperties(FunnyProperties.class)
      //让该自动配置类位于DataSourceAutoConfiguration自动配置类之后处理
      @AutoConfigureAfter(DataSourceAutoConfiguration.class)
      public class FunnyAutoConfiguration {
      private final FunnyProperties properties;
      //FunnyProperties类负责加载配置属性
      public FunnyAutoConfiguration(FunnyProperties properties) {
      this.properties = properties;
      }

      @Bean(destroyMethod = "close")
      //当单例的DataSource Bean存在时配置生效
      @ConditionalOnSingleCandidate(DataSource.class)
      //只有当容器中没有WriterTemplate Bean时,该配置才会生效
      @ConditionalOnMissingBean
      //通过@AutoConfigureOrder注解指定该配置方法比下一个配置WriterTemplate的方法的优先级更高
      @AutoConfigureOrder(99)
      public WriterTemplate writerTemplate(DataSource dataSource) throws SQLException{
          return new WriterTemplate(dataSource);
      }
      
      
      @Bean(destroyMethod = "close")
      //只有当前面的WriteTemplate配置没有生效时,该方法的配置才会生效
      @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);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21

      }

    在FunnyAutoConfiguration 自动配置类中定义了两个@Bean方法,这两个@Bean 方法都用于自动配置 WriterTemplate。为了指定它们的优先级,程序使用了@AutoConfigureOrder 注解修饰它们,该注解指定的数值越小,优先级越高。

    FunnyAutoConfiguration 自动配置类中的@Bean 方法同样使用了@ConditionalOnMissingBean`@ConditionalOnSingleCandidate等条件注解修饰,从而保证只有当容器中不存在WriterTemplate时,该自动配置类才会配置WriterTemplate Bean,且优先配置基于DataSource的WriterTemplate。

    上面的自动配置类还用到了FunnyProperties属性处理类,该类的代码如下:

    package com.example.funnyspringbootstarter.autoconfig;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    
    @ConfigurationProperties(prefix = FunnyProperties.FUNNY_PREFIX)
    public class FunnyProperties {
        public static final String FUNNY_PREFIX = "org.test";
        private String dest;
        private String charset;
    
        public String getDest() {
            return dest;
        }
    
        public void setDest(String dest) {
            this.dest = dest;
        }
    
        public String getCharset() {
            return charset;
        }
    
        public void setCharset(String charset) {
            this.charset = charset;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    上面的属性处理类负责处理以“org.test”开头的属性,这个“org.test”是必要的,它相当于这一组配置属性的“命名空间”,通过这个命名空间可以将这些配置属性与其他框架的配置属性区分开。

    有了上面的自动配置类之后,接下来使用如下META-INF/spring.factories文件来注册自动配置类。

    • src/main/resources/META-INF/spring.factories

      org.springframework.boot.autoconfigure.EnableAutoConfiguration =
      com.example.funnyspringbootstarter.autoconfig.FunnyAutoConfiguration

    经过上面步骤,自动配置开发完成,接下来使用install打包到maven仓库:

    image-20220609125306059

    有了自定义的Starter之后,接下来使用该Starter与使用Spring Boot官方Starter并没有任何区别。首先新建一个Maven项目myfunnytest,在pom文件中引入该starter:

    
    
        4.0.0
        com.example
        myfunnytest
        0.0.1-SNAPSHOT
        myfunnytest
        myfunnytest
    
        
            1.8
            UTF-8
            UTF-8
            2.3.7.RELEASE
        
    
        
            
                com.example
                funny-spring-boot-starter
                0.0.1-SNAPSHOT
            
    
    
    
    
    
    
            
                org.springframework.boot
                spring-boot-starter-test
                test
                
                    
                        org.junit.vintage
                        junit-vintage-engine
                    
                
            
        
    
        
            
                
                    org.springframework.boot
                    spring-boot-dependencies
                    ${spring-boot.version}
                    pom
                    import
                
            
        
    
        
            
                
                    org.apache.maven.plugins
                    maven-compiler-plugin
                    3.8.1
                    
                        1.8
                        1.8
                        UTF-8
                    
                
                
                    org.springframework.boot
                    spring-boot-maven-plugin
                    2.3.7.RELEASE
                    
                        com.example.myfunnytest.MyfunnytestApplication
                    
                    
                        
                            repackage
                            
                                repackage
                            
                        
                    
                
            
        
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86

    由于 funny-spring-boot-starter 本身需要依赖 spring-boot-starter,因此不再需要显式配置依赖spring-boot-starter。

    在添加了上面的funny-spring-boot-starter依赖之后,该Starter包含的自动配置生效,它会尝试在容器中自动配置WriterTemplate,并且还会读取application.properties因此还需要在application.properties文件中进行配置。

    • src/main/resources/application.properties

      应用名称

      spring.application.name=myfunnytest
      org.test.dest = F:/abc-12345.txt
      org.test.charset=UTF-8
      spring.datasource.url=jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDateTimeCode=false&serverTimezone=GMT%2B8
      spring.datasource.username=root
      spring.datasource.password=root

    该示例的主类很简单,它直接获取容器中的WriterTemplate Bean,并调用该Bean的write()方法执行输出。下面是该主类的代码:

    package com.example.myfunnytest;
    
    import io.WriterTemplate;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.ConfigurableApplicationContext;
    
    @SpringBootApplication
    public class MyfunnytestApplication {
    
        public static void main(String[] args) throws Exception{
            ConfigurableApplicationContext run = SpringApplication.run(MyfunnytestApplication.class, args);
            WriterTemplate writerTemplate = run.getBean(WriterTemplate.class);
            System.out.println(writerTemplate);
            writerTemplate.write("自动配置");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    运行该程序,由于当前Spring容器中没有DataSource Bean,因此FunnyAutoConfiguration将会自动配置输出到文件的WriterTemplate。因此,运行该程序,可以看到程序向“f:/abc-12345.txt”文件(由前面的org.test.dest属性配置)输出内容:

    在这里插入图片描述

    运行结果如下:

    image-20220609130748581

    如果在项目的pom.xml文件中通过如下配置来添加依赖。

    
            
                org.springframework.boot
                spring-boot-starter-jdbc
            
    
    • 1
    • 2
    • 3
    • 4
    • 5

    此时为项目添加了spring-boot-starter-jdbc依赖,该依赖将会在容器中自动配置一个DataSource Bean,这个自动配置的DataSource Bean将导致FunnyAutoConfiguration会自动配置输出到数据库的WriterTemplate。因此,运行该程序,可以看到程序向数据库名为springboot数据库的funny_message表输出内容:

    image-20220609132600694

    Spring Boot的核心功能就是为整合第三方框架提供自动配置,而本文则带着大家实现了自己的自动配置和Starter,一旦真正掌握了本文的内容,就会对Spring Boot产生“一览众山小”的感觉。

    先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

  • 相关阅读:
    kafka的auto.offset.reset详解与测试
    VirtualBox为虚拟机设置固定IP且可通过外网访问
    notification控件 通知栏
    【更新!】3dMax材质ID随机生成器插件MaterialIDsRandomGenerator v2.1.2使用教程
    shell_53.理解Linux输入和输出
    Power BI DAX 编写利器 —— DaxStudio 的简单用法
    【电路笔记】-交流波形和交流电路理论
    c++并发线程之创建线程、 join、detach、joinable、线程临时变量问题、ref使用
    react16之前diff算法的理解和总结
    如何在CSDN获得铁粉
  • 原文地址:https://blog.csdn.net/m0_67390379/article/details/126080671