即使有一天,我放弃了自己的身体,也请你,不要放弃我,我亲爱的灵魂.
上一章简单介绍了Spring_Session解决Session共享的问题(二十三),如果没有看过,请观看上一章
一. 自定义Starter 基础知识
一.一 自定义Starter
以前我们在学习 Spring 时,依赖的依赖,都是某一个具体的依赖信息。
在Spring Boot 阶段,我们导入的依赖 的 artifactId 都拥有 starter
如: mybatis-spring-boot-starter pagehelper-spring-boot-starter 等.
这些其实,都是比较官方的自定义 Starter。
在日常开发里面,对于独立于业务模块的一些功能,常常会放置在一个单独的包里,
如 切面实现日志功能, 在项目A里面使用到了,在项目B 里面也使用到了,
以前的做法,是什么样子的呢? 通常将这个日志功能实现的代码复制A项目里面,
同时复制到B项目里面,这样 A项目便拥有了日志的功能,B项目也拥有了日志的功能。
如果实现日志功能的代码发生改变,那么项目A,项目B都需要进行修改。
如果能够将 这个切面实现日志功能的代码 提取出来,变成一个 依赖的jar包,
项目A,项目B直接导入依赖,便可以使用,那就好了。
我们可以通过自定义Starter 实现这些功能。
SpringBoot提供的starter以spring-boot-starter-xxx
的方式命名的。
官方建议自定义的starter使用xxx-spring-boot-starter
命名规则。以区分SpringBoot生态提供的starter。
这一章节,先简单实现一个基础的自定义Starter 的功能。
下一章节,老蝴蝶带着大家通过自定义 Starter 实现切面日志记录的功能。
一.二 自定义Starter 需要什么
我们自定义 Starter 需要什么呢? 想一想,以前的配置, 如 redis的配置,mysql数据库的配置信息.
- 我们需要传入一些参数,可以更改配置 (在application.yml 中配置参数) ,同时有默认的配置参数.
- 有一个功能实现,可以获取到用户传入的配置参数,使用这些配置参数,可以实现我们想要的功能.
- 动态的进行配置,热插拔效果。达到 我们拥有这个自定义starter,就拥有这些东西,没有自定义starter,就没有这些东西的效果。
- 自动化的配置,可以被 SpringBoot 官方识别并承认 (需要固定的格式).
我们需要拥有这些东西,才可以进行自定义 Starter
二. 实现用户信息打印的 自定义 Starter
先创建一个默认的 Maven 项目, 非 SpringBoot 项目。
主要依靠的是 SpringBoot 的自动配置
我们做一个简单的,配置信息打印的 Starter
1,2,3,4 其实对应的就是 一.二 部分的 1,2,3,4
二.一 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>top.yueshushu</groupId>
<artifactId>starter</artifactId>
<version>1.0-SNAPSHOT</version>
<name>ButterflyStarter</name>
<dependencies>
<!--添加自动配置的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<!--添加配置文件引用的依赖信息-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.2.2.RELEASE</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>
除些之外,不需要引入其他的.
二.二 属性类接收用户自定义参数
在 application.yml 中配置参数,通常有很多个,且格式固定, 我们常常通过一个配置属性类进行接收用户的自定义参数。
我们传入 名称,年龄,描述 三个基础的参数 信息.
UserProperties.java
package top.yueshushu.starter.mode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import java.io.Serializable;
/**
* @ClassName:User
* @Description 自定义的一个实体
* @Author zk_yjl
* @Date 2021/10/22 16:57
* @Version 1.0
* @Since 1.0
**/
/**
不建议引用 @Data lombok 第三方插件
* @author yjl
*/
@SuppressWarnings("serial")
@ConfigurationProperties(prefix = "butterfly")
public class UserProperties implements Serializable {
/**
定义 final 常量,用于默认值
*/
private final String DEFAULT_NAME="两个蝴蝶飞";
private final Integer DEFAULT_AGE=26;
private final String DEFAULT_DESCRIPTION="一个快乐的程序员";
/**
定义属性,使用默认值.
*/
private String name=DEFAULT_NAME;
private Integer age=DEFAULT_AGE;
private String description=DEFAULT_DESCRIPTION;
/**
提示默认的构造方法
*/
public UserProperties() {
}
public UserProperties(String name, Integer age, String description) {
this.name = name;
this.age = age;
this.description = description;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", description='" + description + '\'' +
'}';
}
}
二.三 做什么, 要实现的功能
我们拿到用户的自定义参数 (没有的话,走默认的参数)后,要做实现事情,即要实现的功能.
UserPropertiesService.java
package top.yueshushu.starter.service;
import java.util.logging.Logger;
/**
* @ClassName:UserService
* @Description 用户提示的服务,表示有这一个类。
* @Author zk_yjl
* @Date 2021/10/22 17:02
* @Version 1.0
* @Since 1.0
**/
public class UserPropertiesService {
//定义日志
Logger logger=Logger.getLogger(UserPropertiesService.class.getSimpleName());
//参数信息,自定义starter 提供的全部的参数信息。
private String name;
private Integer age;
private String description;
public UserPropertiesService() {
}
// 接收参数
public UserPropertiesService(String name,Integer age,String description) {
this.name=name;
this.age=age;
this.description=description;
}
/**
* 要实现的功能点, 这儿只做一个简单的打印。
* @date 2021/10/29 14:30
* @author zk_yjl
* @param
* @return void
*/
public String println(){
String message = String.format("大家好,我叫: %s, 今年 %s岁, 个人描述: %s",
name, age,description);
logger.info(">>>用户的信息:"+message);
return message;
}
}
二.四 动态热插拔配置
主要是将 这个 XxxService 管控起来。
UserPropertiesServiceConfiguration.java
package top.yueshushu.starter.configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.yueshushu.starter.mode.UserProperties;
import top.yueshushu.starter.service.UserPropertiesService;
/**
* @ClassName:UserServiceConfiguration
* @Description 用户Service类的配置信息
* @Author zk_yjl
* @Date 2021/10/22 17:12
* @Version 1.0
* @Since 1.0
**/
//表示是一个配置类
@Configuration
//用户输入的参数信息,可以放置到 UserProperties参数里面。一一对应起来
@EnableConfigurationProperties(UserProperties.class)
//存在某个条件类时触发, 即寻找到 UserPropertiesService.class时,才生效
@ConditionalOnClass(UserPropertiesService.class)
public class UserPropertiesServiceConfiguration {
@Autowired
private UserProperties userProperties;
@Bean
public UserPropertiesService userPropertiesService(){
//注意,当条件触发时,才会创建 Bean
UserPropertiesService userService=new UserPropertiesService(
userProperties.getName(),
userProperties.getAge(),
userProperties.getDescription()
);
return userService;
}
}
二.五 配置,能被 SpringBoot 官方承认
必须要进行自动装配,否则这些东西,不被 SpringBoot 官方承认,那么在SpringBoot 项目里面进行引用,就找不到,造成功能不生效.
在 resources 目录下 创建 META-INF 目录,下面放置 spring.factories 文件 (必须叫这个名字)
# 添加配置信息,后面跟的是 自定义的那个configuration的全限定类名
org.springframework.boot.autoconfigure.EnableAutoConfiguration=top.yueshushu.starter.configuration.UserPropertiesServiceConfiguration
现在,这个自定义配置的 ButterflyStarter 就算了成功了。
将其进行打包,安装到本地仓库 (老蝴蝶这儿是通过 mvn clean install 命令实现的)
也可以通过 Lifecycle 生命周期按照进行实现
二.六 测试自定义 Starter
新创建一个 SpringBoot 的项目, StarterApply, 可以正常的启动和访问即可。
二.六.一 pom.xml 中添加依赖
除了 SpringBoot 该有的依赖外,导入刚才 install 生成的自定义Starter 依赖
<!--添加我们自定义的依赖-->
<dependency>
<groupId>top.yueshushu</groupId>
<artifactId>starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
二.六.二 测试自定义Starter
在 StarterApply 项目里面,添加 StarterTest 测试类,进行测试
package top.yueshushu.learn;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.test.context.junit4.SpringRunner;
import top.yueshushu.condition.Animal;
import top.yueshushu.condition.JavaAnimalConfig;
import top.yueshushu.starter.service.UserPropertiesService;
/**
* @ClassName:StarterTest
* @Description TODO
* @Author zk_yjl
* @Date 2021/10/22 17:26
* @Version 1.0
* @Since 1.0
**/
@RunWith(SpringRunner.class)
@SpringBootTest
public class StarterTest {
@Autowired
private UserPropertiesService userPropertiesService;
@Test
public void starterPrintlnTest(){
String message = userPropertiesService.println();
System.out.println("Apply项目输出信息:"+message);
}
}
二.六.二.一 进行测试
此时, application.yml 配置文件里面,没有关于自定义 Starter 的配置信息
进行测试,发现可以打印, 但是出现了乱码。 是 starter 本身出现的乱码。
二.六.二.二 解决 Starter 乱码问题
在 自定义的Starter pom.xml 添加编译插件,设置编码为 UTF-8
<!-- spring boot 添加编辑方式-->
<build>
<plugins>
<!-- 打包成可执行jar,防止中文乱码,必须要下面这一个插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
重新 mvn clean install ,然后刷新依赖即可。
解决了中文乱码的问题,
自定义 starter 里面的 日志信息会输出,同时项目里面的输出打印语言也生效。 输出的信息,是默认的信息
二.六.二.三 用户自定义参数测试
在 application.yml 中, 输入 butt 发现,有相应的属性提示
输入自定义的参数信息
butterfly:
name: 岳泽霖
age: 26
description: 一个孤独的小孩子
进行测试
可以打印出用户自定义输入的参数信息。
说明 自定义 Starter 是生效的。
三. 自定义Starter 深入理解
自定义的Starter 能够起作用,条件注解起了很大的作用.
演示一下条件注解的使用.
仍然使用 刚才的自定义 Starter
三.一 pom.xml 中添加依赖
<dependencies>
<!--添加自动配置的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<!--添加配置文件引用的依赖信息-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.2.2.RELEASE</version>
<optional>true</optional>
</dependency>
<!--添加 spring-context依赖, 对应的 2.2.2 版本-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
</dependencies>
三.二 定义一个接口和两个实现类
定义 Animal 接口
package top.yueshushu.condition;
/**
* @ClassName:Animal
* @Description 定义一个动物的接口
* @Author zk_yjl
* @Date 2021/10/25 20:48
* @Version 1.0
* @Since 1.0
**/
public interface Animal {
/**
* 定义一个声音的接口
* @date 2021/10/25 20:49
* @author zk_yjl
* @param
* @return void
*/
public void voice();
}
有两个实现类, 一个是 Cat 一个是 Dog
Cat.java
package top.yueshushu.condition;
/**
* @ClassName:Cat
* @Description TODO
* @Author zk_yjl
* @Date 2021/10/25 20:50
* @Version 1.0
* @Since 1.0
**/
public class Cat implements Animal {
@Override
public void voice() {
System.out.println(">>>喵喵喵");
}
}
Dog.java
package top.yueshushu.condition;
/**
* @ClassName:Dog
* @Description 狗的实现类
* @Author zk_yjl
* @Date 2021/10/25 20:49
* @Version 1.0
* @Since 1.0
**/
public class Dog implements Animal {
@Override
public void voice() {
System.out.println(">>>>汪汪汪");
}
}
三.三 定义实现类起作用的条件
Animal 接口有两个实现类 Cat, Dog 在使用中,要使用哪一个呢?
总不能
@Resource("cat")
private Animal animal
@Resource("dog")
private Animal animal
这样肯定是不行的。
可以定义条件, 当Cat 的条件满足时,就用 Cat 的实现类, 当 Dog 的条件满足时,就用 Dog 的实现类
三.三.一 Cat 实现条件 CatCondition
package top.yueshushu.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* @ClassName:CatCondition
* @Description TODO
* @Author zk_yjl
* @Date 2021/10/25 20:53
* @Version 1.0
* @Since 1.0
**/
// 实现 Condition 接口
public class CatCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String animal = context.getEnvironment().getProperty("animal");
//为空属性,不创建
if(StringUtils.isEmpty(animal)){
return false;
}
return "Cat".equalsIgnoreCase(animal);
}
}
三.三.二 Dog 实现条件 DogCondition
package top.yueshushu.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.StringUtils;
/**
* @ClassName:DogCondition
* @Description TODO
* @Author zk_yjl
* @Date 2021/10/25 20:51
* @Version 1.0
* @Since 1.0
**/
public class DogCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//没有获取到这个属性时,走 Dog
String animal = context.getEnvironment().getProperty("animal");
if(StringUtils.isEmpty(animal)){
return true;
}
return "Dog".equalsIgnoreCase(animal);
}
}
三.四 让条件被管控
现在有 Cat 的条件,也有 Dog 的条件。
但这两个条件,只是实现了 Condition 接口, 并没有添加形如 @Component @Configuration 等类似的注解
说明,这两个条件,还没有办法被SpringBoot 管控到.
JavaAnimalConfig 管控条件
package top.yueshushu.condition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
/**
* @ClassName:JavaAnimalConfig
* @Description TODO
* @Author zk_yjl
* @Date 2021/10/25 20:54
* @Version 1.0
* @Since 1.0
**/
@Configuration
public class JavaAnimalConfig {
/**
条件符合时,创建
*/
@Bean("animal")
@Conditional(DogCondition.class)
Animal dog(){
return new Dog();
}
/**
条件符合时,创建
*/
@Bean("animal")
@Conditional(CatCondition.class)
Animal cat(){
return new Cat();
}
}
两者创建的,bean 都是 animal
三.五 测试
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
//ctx.getEnvironment().getSystemProperties().put("animal", "Cat");
// ctx.getEnvironment().getSystemProperties().put("animal", "Dog");
// ctx.getEnvironment().getSystemProperties().put("animal", "YJL");
ctx.register(JavaAnimalConfig.class);
ctx.refresh();
Animal animal = (Animal) ctx.getBean("animal");
animal.voice();
}
}
设置环境变量值的信息, 都去掉, 会生成默认的 Dog
当使用 Cat 时, 会打印 Cat 的实现类
当使用 Dog 时,会打印 Dog 的实现类
当使用 YJL 时, 因为都不符合,所以创建不了相应的 Bean
也可以将这个自定义的Starter clean install 安装后, StarterApply 引用
@RunWith(SpringRunner.class)
@SpringBootTest
public class StarterTest {
@Autowired
private UserPropertiesService userPropertiesService;
@Test
public void starterPrintlnTest(){
String message = userPropertiesService.println();
System.out.println("Apply项目输出信息:"+message);
}
// 运行测试一下
@Test
public void animalJavaTes(){
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().getSystemProperties().put("animal", "Cat");
ctx.register(JavaAnimalConfig.class);
ctx.refresh();
Animal animal = (Animal) ctx.getBean("animal");
animal.voice();
}
}
发现,也是相同的测试结果.
三.六 Profile 多环境配置
在多环境配置时,我们使用到了 Profile
在 JavaAnimalConfig 里面,
public class JavaAnimalConfig {
/**
条件符合时,创建
*/
@Bean("animal")
@Conditional(DogCondition.class)
Animal dog(){
return new Dog();
}
/**
条件符合时,创建
*/
@Bean("animal")
@Conditional(CatCondition.class)
Animal cat(){
return new Cat();
}
}
我们定义了 DogCondition, 能不能将这个换成 Profile 呢? 这样就不用根据环境变量的值来确定实例化哪一个实现了,而是根据环境配置来实现化具体的实现.
@Configuration
public class JavaAnimalConfig {
/**
条件符合时,创建
*/
@Bean("animal")
// @Conditional(DogCondition.class) 不使用
@Profile("Dog") //触发条件
Animal dog(){
return new Dog();
}
/**
条件符合时,创建
*/
@Bean("animal")
//@Conditional(CatCondition.class)
@Profile("Cat") //触发条件
Animal cat(){
return new Cat();
}
}
进行测试
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
//ctx.getEnvironment().getSystemProperties().put("animal", "Cat");
// ctx.getEnvironment().getSystemProperties().put("animal", "Dog");
// ctx.getEnvironment().getSystemProperties().put("animal", "YJL");
// ctx.getEnvironment().setActiveProfiles("Dog");
//ctx.getEnvironment().setActiveProfiles("Cat");
// ctx.getEnvironment().setActiveProfiles("YJL");
ctx.register(JavaAnimalConfig.class);
ctx.refresh();
Animal animal = (Animal) ctx.getBean("animal");
animal.voice();
}
}
当启用 Dog 时, 实现的是 Dog
当启用的是 Cat 时,实现的是 Cat 实现
当启用的是 YJL 时,会报异常信息。与上面的测试结果一致.
也同样可以在 StarterTest 类中进行测试,
@Test
public void animalJavaTes(){
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
//ctx.getEnvironment().getSystemProperties().put("animal", "Cat");
// ctx.getEnvironment().getSystemProperties().put("animal", "Dog");
// ctx.getEnvironment().getSystemProperties().put("animal", "YJL");
ctx.getEnvironment().setActiveProfiles("Dog");
//ctx.getEnvironment().setActiveProfiles("Cat");
// ctx.getEnvironment().setActiveProfiles("YJL");
ctx.register(JavaAnimalConfig.class);
ctx.refresh();
Animal animal = (Animal) ctx.getBean("animal");
animal.voice();
}
测试结果,也是一样的。
但如果将 setActiveProfiles 都去掉,在 application.yml 中进行设置
是不可以的。 还是会报错误
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named ‘animal’ available
本章节的代码放置在 github 上:
https://github.com/yuejianli/springboot/tree/develop/ButterflyStarter
https://github.com/yuejianli/springboot/tree/develop/StarterApply
谢谢您的观看,如果喜欢,请关注我,再次感谢 !!!