通过Kaptcha初步学习Spring boot starter的原理

卢深
2023-12-01

```

//@Configuration
//public class KaptchaConfig {
//    @Value("${youkol.web.kaptcha.config.kaptcha.image.width}")
//    private String width;
//    @Value("${youkol.web.kaptcha.config.kaptcha.image.height}")
//    private String height;
//    @Value("${youkol.web.kaptcha.config.kaptcha.textproducer.font.size}")
//    private String size;
//    @Value("${youkol.web.kaptcha.config.kaptcha.textproducer.font.color}")
//    private String color;
//
//    @Value("${youkol.web.kaptcha.config.kaptcha.textproducer.char.string}")
//    private String string;
//    @Value("${youkol.web.kaptcha.config.kaptcha.textproducer.char.length}")
//    private String length;
//    @Value("${youkol.web.kaptcha.config.kaptcha.noise.impl}")
//    private String impl;
//    @Bean
//    public Producer kaptchaProducer(){
//
//        Properties properties = new Properties();
//       properties.setProperty("kaptcha.image.width",width);
//       properties.setProperty("kaptcha.image.height",height);
//       properties.setProperty("kaptcha.textproducer.font.size",size);
//       properties.setProperty("kaptcha.textproducer.font.color",color);
//       properties.setProperty("kaptcha.textproducer.char.string",string);
//       properties.setProperty("kaptcha.textproducer.char.length",length);
//        properties.setProperty("kaptcha.noise.impl",impl);
//
//
//      DefaultKaptcha kaptcha = new DefaultKaptcha(); // 步骤1 生成默认producer对象
//
//        Config config = new Config(properties);//步骤 2 创建config对象
//
       kaptcha.setConfig(config);//步骤3 给producer设置config对象
//        return kaptcha;
```

使用starter之前:
            1.定义配置信息(可以在xml文件里,或者在Spring的application.yml中)
            2.定义一个properties用来装载这些配置项
            3.用一个Config对象来解析properties中的参数信息
            4.将这个Config对象传给Producer对象,至此KaptchaProducer对象构建完毕,将该生成函数定义成一个Bean对象,用于SpringBoot启动时自动装配
            
            
引入依赖:<dependency>
            <groupId>com.youkol.support.kaptcha</groupId>
            <artifactId>kaptcha-spring-boot-starter</artifactId>
            <version>2.3.2</version>
        </dependency>
该starter会同时下载相应版本的com.youkol.support.kaptcha,这里要注意如果之前有了不同版本的com.youkol.support.kaptcha,需要将其移除。

![image-20220120233649194](C:\Users\zms\AppData\Roaming\Typora\typora-user-images\image-20220120233649194.png)

**让我们看看starter是如何简化开发的呢?**

打开starter的源码目录,可以看到只有两个文件,一个KaptchaAutoConfiguration,用来配置Producer,另一个是KaptchaProperties用来完成解析配置好的参数并返回Config 对象供KaptchaAutoConfiguration来生成Producer对象。

那么一切就清晰起来了,查看KaptchaAutoConfiguration代码:

![](https://gitee.com/ll533/blog-images/raw/master/img//20220120235419.png)

首先,定义了一个@ConfigurationProperties注解,对于该注解:

# 注解@ConfigurationProperties使用方法

------

## 前言[#](https://www.cnblogs.com/tian874540961/p/12146467.html#1389066136)

最近在思考使用java config的方式进行配置,java config是指基于java配置的spring。传统的Spring一般都是基本xml配置的,后来spring3.0新增了许多java config的注解,特别是spring boot,基本都是清一色的java config。

### Spring配置方式[#](https://www.cnblogs.com/tian874540961/p/12146467.html#1300870826)

第一阶段:xml配置
    在spring 1.x时代,使用spring开发满眼都是xml配置的bean,随着项目的扩大,
我们需要把xml配置文件分放到不同的配置文件中,那时候需要频繁地在开发的类和配置文件间切换。

### 第二阶段:注解配置[#](https://www.cnblogs.com/tian874540961/p/12146467.html#3108624230)

在spring 2.x时代,随着JDK1.5带来的注解支持,spring提供了声明bean的注解,
大大减少了配置量。这时spring圈子存在一种争论:注解配置和xml配置究竟哪个更好?我们最终的选择是应用
的基本配置用xml,业务配置用户注解。

### 第三阶段:Java配置(java config)[#](https://www.cnblogs.com/tian874540961/p/12146467.html#528311852)

从spring 3.x到现在,spring提供了Java配置的能力,使用Java配置更好的理解
配置的bean。spring 4.x和spring boot都推荐使用Java配置。

Spring IOC有一个非常核心的概念——Bean。由Spring容器来负责对Bean的实例化,装配和管理。XML是用来描述Bean最为流行的配置方式。但随着Spring的日益发展,越来越多的人对Spring提出了批评。“Spring项目大量的烂用XML”就是最为严励的一个批评。由于Spring会把几乎所有的业务类都以Bean的形式配置在XML文件中,造成了大量的XML文件。使用XML来配置Bean失去了编译时的类型安全检查。大量的XML配置使得整个项目变得更加复杂。

随着JAVA EE 5.0的发布,其中引入了一个非常重要的特性——Annotations(注释)。注释是源代码的标签,这些标签可以在源代码层进行处理或通过编译器把它熔入到class文件中。在JAVA EE5以后的版本中,注释成为了一个主要的配置选项。Spring使用注释来描述Bean的配置与采用XML相比,因类注释是在一个类源代码中,可以获得类型安全检查的好处。可以良好的支持重构。

JavaConfig就是使用注释来描述Bean配置的组件。JavaConfig 是Spring的一个子项目, 比起Spring,它还是一个非常年青的项目。目前的版本是1.0 M2。使用XML来配置Bean所能实现的功能,通过JavaConfig同样可以很好的实现。

**下面具体讲一讲@ConfigurationProperties使用方法**

## @ConfigurationProperties[#](https://www.cnblogs.com/tian874540961/p/12146467.html#3191007665)

Spring源码中大量使用了ConfigurationProperties注解,比如`server.port`就是由该注解获取到的,通过与其他注解配合使用,能够实现Bean的按需配置。

该注解有一个prefix属性,通过指定的前缀,绑定配置文件中的配置,该注解可以放在类上,也可以放在方法上。

![img](https://qboshi.oss-cn-hangzhou.aliyuncs.com/pic/c903b621-ff2b-41b4-b9c8-af7ce6718ab6.png)

可以从注解说明中看到,当将该注解作用于方法上时,如果想要有效的绑定配置,那么该方法需要有@Bean注解且所属Class需要有@Configuration注解。

**简单一句话概括就是:Spring的有效运行是通过上下文(Bean容器)中Bean的配合完成的,Bean可以简单理解成对象,<font color=red>有些对象需要指定字段内容,那么这些内容我们可以通过配置文件进行绑定,然后将此Bean归还给容器</font>**

## 作用于方法[#](https://www.cnblogs.com/tian874540961/p/12146467.html#3789809195)

比较常见的就是配置读写分离的场景。

### 配置文件内容[#](https://www.cnblogs.com/tian874540961/p/12146467.html#706814808)

```
Copy#数据源
spring.datasource.druid.write.url=jdbc:mysql://localhost:3306/jpa
spring.datasource.druid.write.username=root
spring.datasource.druid.write.password=1
spring.datasource.druid.write.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.druid.read.url=jdbc:mysql://localhost:3306/jpa
spring.datasource.druid.read.username=root
spring.datasource.druid.read.password=1
spring.datasource.druid.read.driver-class-name=com.mysql.jdbc.Driver
```

### java代码[#](https://www.cnblogs.com/tian874540961/p/12146467.html#356316745)

```java
Copy@Configuration
public class DruidDataSourceConfig {
    /**
     * DataSource 配置
     * @return
     */
    @ConfigurationProperties(prefix = "spring.datasource.druid.read")
    @Bean(name = "readDruidDataSource")
    public DataSource readDruidDataSource() {
        return new DruidDataSource();
    }


    /**
     * DataSource 配置
     * @return
     */
    @ConfigurationProperties(prefix = "spring.datasource.druid.write")
    @Bean(name = "writeDruidDataSource")
    @Primary
    public DataSource writeDruidDataSource() {
        return new DruidDataSource();
    }
}
```

也许有的人看到这里会比较疑惑,prefix并没有指定配置的全限定名,那它是怎么进行配置绑定的呢?

相信大家肯定了解@Value注解,它可以通过全限定名进行配置的绑定,这里的ConfigurationProperties其实就类似于使用多个@Value同时绑定,绑定的对象就是DataSource类型的对象,而且是 **隐式绑定** 的,意味着在配置文件编写的时候需要与对应类的字段名称 **相同**,比如上述`spring.datasource.druid.write.url=jdbc:mysql://localhost:3306/jpa` ,当然了,你也可以随便写个配置,比如 `spring.datasource.druid.write.uuu=www.baidu.com`,此时你只需要在注解中加上以下参数即可

![img](https://qboshi.oss-cn-hangzhou.aliyuncs.com/pic/c5a81a7e-8d26-42be-af66-5af0c713fa5e.png)

以上就完成了多个数据源的配置,为读写分离做了铺垫

## 作用于Class类及其用法[#](https://www.cnblogs.com/tian874540961/p/12146467.html#4091460327)

### 配置文件内容[#](https://www.cnblogs.com/tian874540961/p/12146467.html#2718935402)

```
spring.datasource.url=jdbc:mysql://127.0.0.1:8888/test?useUnicode=false&autoReconnect=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
```

### java代码[#](https://www.cnblogs.com/tian874540961/p/12146467.html#2331010475)

```java
@ConfigurationProperties(prefix = "spring.datasource")
@Component
public class DatasourcePro {

    private String url;

    private String username;

    private String password;

    // 配置文件中是driver-class-name, 转驼峰命名便可以绑定成
    private String driverClassName;

    private String type;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getDriverClassName() {
        return driverClassName;
    }

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}
```

### 用法[#](https://www.cnblogs.com/tian874540961/p/12146467.html#138780491)

```
@Controller
@RequestMapping(value = "/config")
public class ConfigurationPropertiesController {

    @Autowired
    private DatasourcePro datasourcePro;

    @RequestMapping("/test")
    @ResponseBody
    public Map<String, Object> test(){

        Map<String, Object> map = new HashMap<>();
        map.put("url", datasourcePro.getUrl());
        map.put("userName", datasourcePro.getUsername());
        map.put("password", datasourcePro.getPassword());
        map.put("className", datasourcePro.getDriverClassName());
        map.put("type", datasourcePro.getType());

        return map;
    }
}
```

## 总结[#](https://www.cnblogs.com/tian874540961/p/12146467.html#2682833216)

1. @ConfigurationProperties 和 @value 有着相同的功能,但是 @ConfigurationProperties的写法更为方便
2. @ConfigurationProperties 的 POJO属性的命名比较严格,因为它必须和prefix的后缀名要一致, 不然值会绑定不上, 特殊的后缀名是“driver-class-name”这种带横杠的情况,在POJO里面的命名规则是 **下划线转驼峰** 就可以绑定成功,所以就是 “driverClassName”

有了这些知识,我们就可以知道这里的@ConfigurationProperties是为了将我们自己写的application.yml配置文件中的针对kaptcha的配置信息注入到当前自动配置类中来,那么上面说到了,注入的属性需要与类中的属性名一一对应,我们发现类中并没有定义属性而是有一个@NestedConfigurationProperties的注解,该注解作用于private Map<String, String> config = new HashMap(); ,猜测,该注解的作用是将所有的属性以及值注入到了这个config 的 map对象中,于是查阅资料,发现果然是这样的:

```
/**
 * Indicates that a field in a {@link ConfigurationProperties @ConfigurationProperties}
 * object should be treated as if it were a nested type. This annotation has no bearing on
 * the actual binding processes, but it is used by the
 * {@code spring-boot-configuration-processor} as a hint that a field is not bound as a
 * single value. When this is specified, a nested group is created for the field and its
 * type is harvested.
 */
源码中的注释说明这个注解声明该注入的值是一个嵌套类型,使用它了之后就不是一个简单单值了,一般是比较复杂的类型。所以实现了将配置文件中的键值对注入到这个config map中了。

```

ok,.明白了这两个注解的作用之后,继续向下看,发现createKaptchaConfig函数,

![](https://gitee.com/ll533/blog-images/raw/master/img//20220121001714.png)

显然,该函数的作用就是解析注入到config中的配置参数,然后创建Config对象供给createKaptchaConfig使用。

回到KaptchaAutoConfiguration中,我们看到定义了了两个条件Bean,一个是Config,另一个是Producer。

在Config生成的函数里,调用了上面讲到的KaptchaProperties中的createKaptchaConfig方法,得到Config对象,对应步骤2 创建config对象。

在Producer的生成函数中刚好对应了未使用starter的步骤1 生成默认producer对象,以及步骤3 给producer设置config对象。

![](https://gitee.com/ll533/blog-images/raw/master/img//20220120235506.png)

到此就完成了Producer对象的自动创建,我们在使用的时候,只需要使用自动装配

```
@Autowired
private Producer producer;

```

就可以正常使用来创建对象了。

总结:这里的自动装配帮我们将三个创建producer的步骤进行了封装了,使得创建步骤由spring  自行完成,我们只需要自动装配就可以使用了。实质上大多数的starter都是这种模式,这篇分析将有助于理解其他starter的工作原理。

 类似资料: