有接触过 starter 组件吗?
相信大家在接触 Spring Boot 的项目时,都遇见过像 spring-boot-starter-web、spring-boot-starter-amqp、mybatis-spring-boot-starter 等诸如此类的 starter 组件了吧。用过 Spring Boot 的会发现它最大的特点就是自动装配,凭借这一特点可以简化依赖,快速搭建项目。那么除了使用之外有没有想了解过如何自定义一个这样的 starter 组件呢?
为什么会想自定义 starter 组件呢?
在实际开发中经常会发现现在开发的功能好像之前项目也开发过呢,那么这些功能能不能封装在一起,让每个项目都可以使用呢?
基于这样的需求,为了不重复写同样的功能,可以将需要封装的功能做成一个 starter 组件,这样在每次需要使用时只需要使用 Maven 依赖进来即可。
文章说明
下面文章将会介绍的自定义 starter 组件是一个很简单很简单的 starter 组件实现。
这篇文章的目的在于让一些像我一样还没有接触过自定义 starter 组件的读者们,体验一下自定义 starter 组件的编写过程。对于后续内容如果不感兴趣可以直接跳过文章,最后希望这篇文章能对读者起到一点帮助。
目标
当前自定义 stater 组件的应用场景
如上图所示,这个自定义 starter 组件的目标是通过注解方式对指定的接口参数进行指定方式的校验。
文章后续介绍的内容
如何自定义注解以及元注解的相关知识
如何结合注解与 Spring AOP 的切面方式完成校验
如何让 starter 组件自动装配,如何让 starter 组件可配置
实现
放一张项目总体结构图
项目中的完整代码后续文章都会有,仔细看项目代码-XXX 加粗字体下方就是完整代码。
1. 创建工程
命名规范:对于 SpringBoot 自带的 starter 通常命名为 spring-boot-starter-xxx,对于第三方提供(自定义)的 starter 通常命名为 xxx-spring-boot-starter。所以工程名取为 check-spring-boot-starter。
项目代码-项目依赖 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>cn.lucas.check</groupId>
<artifactId>check-spring-boot-starter</artifactId>
<version>1.0</version>
<name>check-spring-boot-starter</name>
<description>接口参数校验组件</description>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 生成配置元数据 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!-- 自动配置 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- Spring AOP 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
</project>
复制代码
2. 自定义校验注解
自定义校验注解相关代码设计
项目代码-校验注解代码
/**
* 接口参数校验注解
* @author 单程车票
*/
@Target(ElementType.METHOD) // 目标作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
public @interface DoCheck {
// 校验方式 (枚举)
Check value() default Check.Email;
// 校验参数
String arg() default "";
// 错误信息
String msg() default "";
}
复制代码
其中的 @Target 和 @Retention 是元注解,通过元注解限定自定义注解的修饰目标以及保留时间。(有关元注解的使用后续会讲解)
这里自定义的校验注解有三个属性,分别是枚举类型的校验方式、String 类型的需要校验的参数名、String 类型的校验失败后返回的错误信息。
项目代码-校验方式枚举类代码
/**
* 校验枚举类
* @author 单程车票
*/
public enum Check {
// 邮箱校验方式
Email("参数应为Email地址", CheckUtil::isEmail);
public String msg;
// 函数式接口 Object为传入参数类型,Boolean为返回类型
public Function<Object, Boolean> function;
Check(String msg, Function<Object, Boolean> function) {
this.msg = msg;
this.function = function;
}
}
复制代码
这里为了简化代码,只实现了测试接口需要使用的校验方式(邮箱校验),有兴趣的可以拓展添加其他需要校验的枚举方式。
当前枚举类有两个属性,分别为 校验错误时返回的固定错误信息(也就是说如果注解中没有指定错误信息 msg 时,会使用枚举方式中自带的固定错误信息) 和 函数式接口(指定该接口入参为 Object 类型,返回参数为 Boolean 类型)
这样设计枚举类的原因是,通过实现函数式接口的方式定义校验逻辑(把 Object 类型的校验参数作为入参,校验完成后返回 Boolean 类型的返回结果)。这里实现方式封装在了校验工具类中。
项目代码-校验工具类代码
/**
* 校验工具类
* @author 单程车票
*/
public class CheckUtil {
/**
* 使用正则表达式判断是否是邮箱格式
*/
public static Boolean isEmail(Object value) {
if(value == null) {
return Boolean.FALSE;
}
if(value instanceof String) {
String regEx = "^([a-z0-9A-Z]+[-|\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\.)+[a-zA-Z]{2,}$";
Pattern p = Pattern.compile(regEx);
Matcher m = p.matcher((String) value);
if (m.matches()) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
}
复制代码
看完这个工具类,可以发现上述代码正是通过 isEmail()方法实现的函数式接口,isEmail()方法内容就是邮箱校验逻辑。
总结
当需要使用校验逻辑时,会先通过注解信息(关于怎么获取注解信息后续会讲解)获取到校验方式枚举类,然后通过校验方式枚举类获取到函数式接口,进而使用函数式接口的 apply()方法完成校验逻辑。
// 注解信息(annotation)获取校验方式枚举类(value())获取函数式接口(function)调用apply()
// 实际上就相当于调用了CheckUtil的isEmial()方法。
Boolean res = annotation.value().function.apply(argValue);
复制代码
通过这样的设计方式实现动态地获取注解中枚举方式从而获取需要的校验逻辑。
补充知识:元注解
元注解可以看作是修饰注解的注解类,列举如下:
补充说明一下元注解 @Repeatable:
Java8 以后的新元注解(重复注解),如果想要在一个方法进行两次相同的注解会报错,可以通过该注解实现。接下来展示使用和不使用该注解的对比。
在 Java8 之前解决在一个方法上使用两次注解如下:
public @interface DoCheck {
Check value() default Check.Email;
String arg() default "";
}
public @interface DoChecks {
DoCheck[] value();
}
// 使用@DoChecks包含@DoCheck的方式进行两次注解
@DoChecks({
@DoCheck(value = Check.Email, argN = "email"),
@DoCheck(value = Check.NotEmpty, arg = "email"),
})
@GetMapping("/test")
public R<String> sendEmail(@RequestParam("email") String email) {
return R.success("发送成功");
}
复制代码
在 Java8 后解决在一个方法上使用两次注解如下:
@Repeatable(DoChecks.class)
public @interface DoCheck {
Check value() default Check.Email;
String arg() default "";
}
public @interface DoChecks {
DoCheck[] value();
}
// 只需要通过@Repeatable修饰@DoCheck即可直接在方法上进行多次注解
@DoCheck(value = Check.Email, argN = "email")
@DoCheck(value = Check.NotEmpty, arg = "email")
@GetMapping("/test")
public R<String> sendEmail(@RequestParam("email") String email) {
return R.success("发送成功");
}
复制代码
通过 @Repeatable 可以提高代码可读性,仅此而已,仍然需要声明 @DoChecks 存储 @DoCheck 注解。
考虑到项目的主要目的还是在于手写一个 starter 组件的过程,所以当前自定义的校验注解并未实现重复注解功能,有兴趣的可以自行拓展开发。
3. 结合 Spring AOP 切面方式实现注解校验
项目代码-切面代码
/**
* aop切面方法(执行校验工作)
* @author 单程车票
*/
@Aspect
public class DoCheckPoint {
// 记录日志
private final Logger log = LoggerFactory.getLogger(DoCheckPoint.class);
/**
* 自定义切入点
* 切入点说明:这样指定的切入点是任何一个执行的方法有一个 @DoCheck 注解的连接点(这里连接点可以看是方法)
*/
@Pointcut("@annotation(cn.lucas.check.annotation.DoCheck)")
private void doCheckPoint() {}
/**
* 定义环绕通知
*/
@Around("doCheckPoint()")
public Object doCheck(ProceedingJoinPoint jp) throws Throwable {
// 获取被修饰的方法信息
Method method = getMethod(jp);
// 获取方法的所有参数值
Object[] args = jp.getArgs();
// 获取方法的所有参数名
String[] paramNames = getParamName(jp);
// 获取注解信息
DoCheck annotation = method.getAnnotation(DoCheck.class);
// 获取需要校验的参数名
String argName = annotation.arg();
// 获取需要的校验方式枚举类
Check value = annotation.value();
// 获取需要返回的报错信息
String msg = annotation.msg();
// 判断是否未配置msg,未配置则直接使用枚举类的固定提示
if ("".equals(msg)) msg = value.msg;
// 获取需要校验的参数值
Object argValue = getArgValue(argName, args, paramNames);
// 记录日志
log.info("校验方法:{} 校验值:{}", method.getName(), argValue);
// 如果找不到需要校验的参数直接放行
if (argValue == null) return jp.proceed();
// 通过函数式接口传入需要校验的值, 内部会调用工具类的isEmail方法进行校验
Boolean res = value.function.apply(argValue);
if (res) {
return jp.proceed(); // 校验成功则放行
}else {
// 校验失败抛出异常(带上错误信息msg)并交给调用方捕获(调用方:使用该注解的项目可以定义全局异常捕获,遇到IllegalArgumentException异常则返回对应报错信息)
throw new IllegalArgumentException(msg);
}
}
/**
* 获取方法信息
*/
public Method getMethod(ProceedingJoinPoint jp) throws NoSuchMethodException {
MethodSignature methodSignature = (MethodSignature) jp.getSignature();
return jp.getTarget().getClass() // 获取切入点的目标(被修饰方法)的Class对象
.getMethod(methodSignature.getName(), methodSignature.getParameterTypes()); // 通过方法名和方法参数类型使用反射获取到方法对象
}
/**
* 获取方法的所有参数名字
*/
private String[] getParamName(ProceedingJoinPoint jp) {
MethodSignature methodSignature = (MethodSignature) jp.getSignature();
return methodSignature.getParameterNames();
}
/**
* 获取需要检验的参数值
* @param target 需要校验的参数名
* @param args 被修饰的方法中所有的参数
* @param paramNames 被修饰的方法中所有的参数名
*/
private Object getArgValue(String target, Object[] args, String[] paramNames){
// 标记当前遍历的索引(因为args和paramNames是一一对应的)
int idx = 0;
// 遍历参数名
for (String name : paramNames) {
// 匹配对应的参数名则直接返回对应的参数值
if (name.equals(target)) {
return args[idx];
}
idx++;
}
return null;
}
}
复制代码
一眼看到上面这么臭的代码估计读者们也会感到厌烦导致一时半会无法理解校验内容,接下来会通过一点点的深入慢慢理解。
需要了解的知识一:AOP 几个重要的编程术语
需要了解的知识二:AOP 的注解
通过上面两个 AOP 的知识结合代码,就可以理解代码中的 @Aspect 修饰该类表明该类是一个切面,@Pointcut 注解用于自定义切入点表达式,方便后续使用,@Around("doCheckPoint()")用于定义环绕通知,也就是切面的主要执行逻辑。
自定义切入点代码理解
/**
* 自定义切入点表达式
* 切入点说明:这样指定的切入点是任何一个执行的方法有一个 @DoCheck 注解的连接点(这里连接点可以看是方法)
*/
@Pointcut("@annotation(cn.lucas.check.annotation.DoCheck)")
private void doCheckPoint() {}
复制代码
这里使用 @Pointcut 自定义切入点表达式,在后续需要使用切入点表达式时只需要使用 doCheckPoint()代替即可。@Pointcut 中放入的是切入点表达式,这样自定义切入点就像是做一个全局切入点表达式,可以使得后续不用重复写切入点表达式。
这里自定义的切入点表达式指定的切入位置是每一个被 @DoCheck 注解修饰的方法都是切入位置,都会执行切面业务。
有关切入点表达式这里只能简单介绍,需要详细了解的可以查阅相关资料
// execution(访问权限 方法返回值 方法声明(参数) 异常类型)
execution(modifiers-pattern ret-type-pattern declaring-type-pattern name-pattern(param-pattern) throws-pattern)
/*
对应的参数说明:
modifiers-pattern 访问权限类型(可选(即可以不写))
ret-type-pattern 返回值类型
declaring-type-pattern 包名类名(可选)
name-pattern(param-pattern) 方法名(参数类型和参数个数)
throws-pattern 抛出异常类型(可选)
*/
// 下面放一些表达式例子
// 指定切入点为:任意公共方法。
execution(public * *(..))
// 指定切入点为:任何一个以“set”开始的方法。
execution(* set*(..))
// 指定切入点为:定义在 service 包里的任意类的任意方法。
execution(* cn.lucas.service.*.*(..))
// 指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点
execution(* *..service.*.*(..))
// 目标对象中有一个 @DoCheck 注解的任意连接点
@target(cn.lucas.check.annotation.DoCheck)
// 任何一个执行的方法有一个 @DoCheck 注解的连接点
@annotation(cn.lucas.check.annotation.DoCheck)
// 任何一个只接受一个参数,并且运行时所传入的参数类型具有@DoCheck 注解的连接点
@args(cn.lucas.check.annotation.DoCheck)
复制代码
环绕通知代码理解
/**
* 定义环绕通知
*/
@Around("doCheckPoint()")
public Object doCheck(ProceedingJoinPoint jp) throws Throwable {
复制代码
首先可以看到环绕通知 @Around()的 value 值是"doCheckPoint()",即使用自定义的切入点表示式声明切面代码的切入位置,使用环绕通知声明切面代码的切入时机。
方法中携带一个参数 ProceedingJoinPoint,可以理解成连接点(JoinPoint)的定义,在这里通过它可以拿到注解修饰的方法 Method 信息,也可以拿到方法的参数名信息,参数值信息等。
获取注解修饰的方法信息,返回的是 Method 对象。
/**
* 获取方法信息
*/
public Method getMethod(ProceedingJoinPoint jp) throws NoSuchMethodException {
MethodSignature methodSignature = (MethodSignature) jp.getSignature();
return jp.getTarget().getClass() // 获取切入点的目标(被修饰方法)的Class对象
.getMethod(methodSignature.getName(), methodSignature.getParameterTypes()); // 通过方法名和方法参数类型使用反射获取到方法对象
}
复制代码
获取注解修饰的方法的所有参数名,返回的是 String 数组。
/**
* 获取方法的所有参数名字
*/
private String[] getParamName(ProceedingJoinPoint jp) {
MethodSignature methodSignature = (MethodSignature) jp.getSignature();
return methodSignature.getParameterNames();
}
复制代码
梳理校验逻辑的过程:
通过参数 ProceedingJoinPoint 参数可以拿到注解修饰的方法 Method 信息、方法的所有参数名、方法的所有参数值。
// 获取被修饰的方法信息
Method method = getMethod(jp);
// 获取方法的所有参数值
Object[] args = jp.getArgs();
// 获取方法的所有参数名
String[] paramNames = getParamName(jp);
复制代码
通过 Method 对象可以拿到注解信息 Annotation 对象(这是因为 Method 接口源码中实现了反射包下的 AnnotatedElement 接口的 getAnnotation()方法,此方法可以获取到修饰 Method 方法的注解 Annotation 信息)。拿到注解信息即可动态的获取注解中的属性信息:校验方式枚举类、校验参数名、校验错误返回信息。
// 获取注解信息
DoCheck annotation = method.getAnnotation(DoCheck.class);
// 获取需要校验的参数名
String argName = annotation.arg();
// 获取需要的校验方式
Check value = annotation.value();
// 获取需要返回的报错信息
String msg = annotation.msg();
复制代码
通过拿到的方法所有参数名、所有参数值、以及需要校验的参数名即可获取到需要校验的参数值。这里通过循环对比参数名与需要校验的参数名一致时返回对应的参数值。逻辑很简单,后续读者想要拓展可以考虑当传入参数为对象,而需要校验参数名为对象的属性时,如何获取对应的参数值。
/**
* 获取需要检验的参数值
* @param target 需要校验的参数名
* @param args 被修饰的方法中所有的参数值
* @param paramNames 被修饰的方法中所有的参数名
*/
private Object getArgValue(String target, Object[] args, String[] paramNames){
// 标记当前遍历的索引(因为args和paramNames是一一对应的)
int idx = 0;
// 遍历参数名
for (String name : paramNames) {
// 匹配对应的参数名则直接返回对应的参数值
if (name.equals(target)) {
return args[idx];
}
idx++;
}
return null;
}
复制代码
通过获取到需要校验的参数值,调用注解信息获取到的校验方式枚举类拿到对应的 function 函数式接口调用 apply()即可进行参数值校验。最后通过是否校验成功进行判断是放行还是抛出异常。
// 通过函数式接口传入需要校验的值, 内部会调用工具类的isEmail方法进行校验
Boolean res = value.function.apply(argValue);
if (res) {
return jp.proceed(); // 校验成功则放行
}else {
// 校验失败抛出异常由调用方捕获
throw new IllegalArgumentException(msg);
}
复制代码
以上就是所有校验的逻辑以及如何结合 Spring AOP 完成对注解的切面(校验)业务。
4. 实现 starter 组件自动装配以及可配置
Spring Boot 自动装配原理
前面说过 Spring Boot 的特点就是自动装配,相信使用过 Spring 的读者们都体会过 XML 配置的复杂,而使用 Spring Boot 会发现除了导入依赖之外,无需过多的配置即可使用,就算需要配置时也只需要通过配置类或配置文件进行简单配置即可,这样的便利归功于 Spring Boot 的自动装配。接下来就了解一下 Spring Boot 究竟如何实现了自动装配。
学过 Java 的应该都听过 SPI 机制(JDK 内置的一种服务发现机制),而 Spring boot 正是基于 SPI 机制的方式对外提供了一套接口规范(当 Spring Boot 项目启动时,会扫描外部引入的 Jar 中的 META-INF/spring.factories 文件,将文件中配置的类信息装配到 Spring 容器中),让引入的 Jar 实现这套规范即可自动装配进 Spring Boot 中。
所以想要自定义的 starter 组件实现自动装配,只需要在项目的 resources 中创建 META-INF 目录,并在此目录下创建一个 spring.factories 文件,将 starter 组件的自动配置类的类路径写在文件上即可。
项目代码-META-INF/spring.factories
// 添加项目的自动配置类的类路径
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.lucas.check.config.CheckAutoConfigure
复制代码
虽然已经解决了自定义 starter 实现自动装配,但是有兴趣的还是接着了解一下底层自动装配的过程。那么 Spring Boot 是如何找到 META-INF/spring.factories 的文件并进行自动配置的呢?
深入源码解析自动装配过程
Spring Boot 项目在启动类上都会有一个 @SpringBootApplication 注解,这个注解可以看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 的集合,而自动装配原理就在其中的 @EnableAutoConfiguration 中,这个注解作用是开启自动配置机制。
@EnableAutoConfiguration 中实现自动装配核心功能的是 @Import,通过加载自动装配类 AutoConfigurationImportSelector 实现自动装配功能。
深入 AutoConfigurationImportSelector 代码发现该类实现了 ImportSelector 接口同时实现了该接口的 selectImports 方法,这个方法用来获取所有符合条件的类的全限定名(为什么是符合条件,只在后续按需装配中说明),并且将这些类加载进 Spring 容器中。
再深入 selectImports 方法中会发现这个 getAutoConfigurationEntry()方法才是最终获取到 META-INF/spring.factories 文件的方法。
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
// 第一步:先判断是否开启了自动装配机制
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
// 第二步:获取@SpringBootApplication中需要排除的类(exclude和excludeName属性),通过这两个属性排除指定的不需要自动装配的类
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
// 第三步:获取需要自动装配的所有配置类
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
复制代码
到这里便是 SpringBoot 的自动装配原理整个过程(可能描述的不完整,有兴趣的可以去找找资料看看)。
抛出问题:刚刚提到的 Spring Boot 只会加载符合条件的类是怎么回事?
Spring Boot 其实并不会加载 META-INF/spring.factories 文件下的所有类,而是按需加载,怎么个按需加载呢?
Spring Boot 可以通过 @ConditionalOnXXX 满足条件时加载(即按需加载),下面列举一些常用的注解:
ConditionalOnBean:当容器里存在指定 Bean 时,实例化(加载)当前 Bean。
@ConditionalOnMissingBean:当容器里不存在指定 Bean 时,实例化(加载)当前 Bean。
@ConditionalOnClass:当类路径下存在指定类时,实例化(加载)当前 Bean。
@ConditionalOnMissingClass:当类路径下不存在指定类时,实例化(加载)当前 Bean。
@ConditionalOnProperty:配置文件中指定的 value 属性是指定的 havingValue 属性值时,实例化(加载)当前 Bean。
项目代码-自动配置类
/**
* 自动配置类
* @author 单程车票
*/
@Configuration
// 注意@ConditionalOnProperty注解要放在后面两个注解的前面,这样才会优先通过配置文件判断是否要开启自动装配。
@ConditionalOnProperty(value = "check.enabled", havingValue = "true")
@ConditionalOnClass(CheckProperties.class)
@EnableConfigurationProperties(CheckProperties.class)
public class CheckAutoConfigure {
/**
* 使用配置Bean的方式使用DoCheckPoint切面
*/
@Bean
@ConditionalOnMissingBean
public DoCheckPoint point() {
return new DoCheckPoint();
}
}
复制代码
解释代码:
@Configuration 注解:标注这个注解的类可以看成是配置类(也可以看为是 Bean 对象的工厂),主要作用就是将 Bean 对象注入容器中。
@ConditionalOnProperty(value = "check.enabled", havingValue = "true")注解:当引入当前 starter 的项目的配置文件中出现 check.enabled=true(不出现也行,因为默认就是 true)时,自动装配当前配置类。当配置文件中出现的是 check.enabled=false 时,则不装配该类,则无法使用切面,则 starter 组件失效。(坑点:该注解一定要放前面,优先通过配置文件判断是否需要开启自动装配,而后再判断其他条件)
@ConditionalOnClass(CheckProperties.class)注解:上面解释过,就是当 CheckProperties 出现在类路径下时自动装配当前配置类。
@EnableConfigurationProperties(CheckProperties.class)注解:让使用了 @ConfigurationProperties 注解的类生效。
这里使用 @Bean 的方式将切面注入 Spring 容器中,使得切面生效得以使用。(还有另外一种将切面注入容器的方式,即在切面类上加 @Component 注解,并且在自动配置类上添加 @ComponentScan(basePackages = "cn.lucas.check.*")用于扫描切面类的 @Component 并将切面 Bean 添加进容器中)具体使用哪种方式注入切面类 Bean 都行。
项目代码-配置文件读取类
/**
* 读取配置文件的信息类
* @author 单程车票
*/
@ConfigurationProperties("check") // 用于读取配置文件前缀为check的属性
public class CheckProperties {
// 默认为true,表示开启校验
private boolean enabled = true;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
复制代码
到此完成了对自定义 Spring Boot starter 的自动装配以及可通过配置文件的 check.enabled 控制是否开启 starter 组件(是否加载该 starter 配置类)。
测试
完成了对自定义 starter 的编写后,还需要对 starter 进行测试,确保功能能正常使用,所以接下来新建一个项目可以命名为 check-spring-boot-starter-test。
需要先将编写好的自定义的 starter 的 jar 包安装到本地 maven 仓库中。
测试项目编写步骤
1.引入依赖 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>cn.lucas.check</groupId>
<artifactId>check-spring-boot-starter-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>check-spring-boot-starter-test</name>
<description>校验测试服务</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 引入自定义的starter -->
<dependency>
<groupId>cn.lucas.check</groupId>
<artifactId>check-spring-boot-starter</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
复制代码
2.创建统一封装结果类
/**
* 统一封装结果类
* @author 单程车票
*/
@Data
public class R<T> implements Serializable {
private Integer code;
private String msg;
private T data;
public static <T> R<T> success(int code, String msg, T data) {
R<T> result = new R<>();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
public static <T> R<T> fail(int code, String msg, T data) {
R<T> result = new R<>();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
public static <T> R<T> success(T data) {
return success(200, "操作成功", data);
}
public static <T> R<T> fail(String msg) {
return fail(500, msg, null);
}
}
复制代码
3.创建全局异常处理类(用来处理检验失败捕获参数异常)
/**
* 全局异常处理
* @author 单程车票
*/
@Slf4j
@ResponseBody
@ControllerAdvice(annotations = {RestController.class, Controller.class})
public class GlobalExceptionHandler {
/**
* 捕获参数异常信息
* @param ex 异常信息
* @return R
*/
@ExceptionHandler(IllegalArgumentException.class)
public R<String> illegalArgumentException(IllegalArgumentException ex){
log.info(ex.getMessage());
return R.fail(ex.getMessage());
}
}
复制代码
4.创建 TestController 用于接口测试
/**
* 测试
* @author 单程车票
*/
@Slf4j
@RestController
public class TestController {
@DoCheck(value = Check.Email, arg = "email", msg = "邮箱格式不正确!")
@GetMapping("/test")
public R<String> sendEmail(@RequestParam("email") String email) {
log.info("校验参数:{}", email);
return R.success("发送成功");
}
}
复制代码
5.配置文件 application.yml
# 端口号
server:
port: 8888
# 控制校验开关
check:
enabled: true
复制代码
测试一:配置文件中开启校验,传入接口参数为正确邮箱格式
通过 postman 工具测试接口,校验成功能够成功发送,预期结果一致。
postman 结果图
控制台信息图
控制台打印 starter 的日志以及接口的日志,说明校验成功并且执行了接口方法。
测试二:配置文件中开启校验,传入接口参数为错误邮箱格式
通过 postman 工具测试接口,校验失败不能够成功发送,预期结果一致。
postman 结果图
控制台信息图
控制台打印报错信息,说明校验失败。
测试三:配置文件中关闭校验,传入接口参数为错误邮箱格式
# 端口号
server:
port: 8888
# 控制校验开关
check:
enabled: false
复制代码
通过 postman 工具测试接口,无法校验,直接发送成功,预期结果一致。
postman 结果图
控制台信息图
可以看到只打印了接口的日志,并没有打印 starter 中的日志,说明 starter 未启用。
作者:单程车票
链接:https://juejin.cn/post/7201104797473357880
来源:稀土掘金
评论