乐趣区

关于spring-boot-编程思想: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<String, Object> 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;
    }
}

从下面的逻辑能够看到,自定义条件注解的解决逻辑比较简单:就是要求 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 {};

}

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

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

运行测试发现胜利取得了容器中对应的类:

自定义主动配置

开发本人的主动配置很简略,其实也就两步:

应用 @Configuration 和条件注解定义主动配置类。

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

为了分明地演示 Spring Boot 主动配置的成果,防止引入第三方框架导致的额定复杂度,本例先自行开发一个 funny 框架,该框架的性能是用文件或数据库保留程序的输入信息。

新建一个 Maven 我的项目 funny(留神不是用 SpringInitializr 创立我的项目),为该我的项目增加 mysql-connector-java 和 slf4j-api 两个依赖。因为该我的项目是咱们本人开发的框架,因而毋庸为该我的项目增加任何 Spring Boot 依赖。上面是该项目标 pom.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.example</groupId>
    <artifactId>funny</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 定义所应用的 Java 版本和源代码应用的字符集 -->
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</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.27</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.36</version>
        </dependency>
    </dependencies>


</project>

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

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 + "\n").getBytes(this.charset));
        }
    }

    // 敞开资源
    public void close() throws SQLException,IOException{if(this.conn != null){this.conn.close();
        }

        if(this.raf != null){this.raf.close();
        }
    }
}

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

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

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

同样新建一个 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 文件内容如下。

<?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>
    <groupId>com.example</groupId>
    <artifactId>funny-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>funny-spring-boot-starter</name>
    <description>funny-spring-boot-starter</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Starter 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- 依赖自定义的 funny 框架,如果正在为其余第三方框架开发主动配置,则此处应该填写被整合的第三方框架的坐标。-->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>funny</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

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

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);
    }
}

在 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;}
}

下面的属性解决类负责解决以“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 仓库:

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

<?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>
    <groupId>com.example</groupId>
    <artifactId>myfunnytest</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>myfunnytest</name>
    <description>myfunnytest</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>funny-spring-boot-starter</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

<!--        <dependency>-->
<!--            <groupId>org.springframework.boot</groupId>-->
<!--            <artifactId>spring-boot-starter</artifactId>-->
<!--        </dependency>-->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.7.RELEASE</version>
                <configuration>
                    <mainClass>com.example.myfunnytest.MyfunnytestApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

因为 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("主动配置");
    }

}

运行该程序,因为以后 Spring 容器中没有 DataSource Bean,因而 FunnyAutoConfiguration 将会主动配置输入到文件的 WriterTemplate。因而,运行该程序,能够看到程序向“f:/abc-12345.txt”文件(由后面的 org.test.dest 属性配置)输入内容:

运行后果如下:

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

<!--        Spring Boot JDBC Starter 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

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

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

以上就是全部内容,心愿对大家有所帮忙。学习,

退出移动版