前言
我们都知道可以使用SpringBoot快速的开发基于Spring框架的项目。由于围绕SpringBoot存在很多开箱即用的Starter依赖,使得我们在开发业务代码时能够非常方便的、不需要过多关注框架的配置,而只需要关注业务即可。
例如我想要在SpringBoot项目中集成web,那么我只需如下两步:
第一步 要加入spring-boot-starter-web的依赖并简单配置一下信息
<!-- spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
第二步 在启动类或配置类上增加一个注解即可完成
@GetMapping("/user/user_id")
这为我们省去了之前很多的配置操作,有些功能的开启只需关心业务逻辑
是不是特别方便?
也许,你可以对面试官说你熟(略)练(懂)使(皮)用(毛)SpringBoot了。
但是你有没有想过自己开发一个炫酷功能的starter被别人拿来引用呢?比如 spring-boot-starter-cache
@Cache(key = "user_id", action = "用户查询", type = "redis")
只要加上这个注解,系统便会自动利用redis对数据库进行缓存查询优化,而不是在业务中进行代码优化。
我相信只要大家花三分钟看完这篇文章一定可以开发出来的。
原理浅谈
从spring boot启动类说起,众所周知启动类会有@SpringBootApplication注解
@SpringBootApplication
public class EdgeApplication {
public static void main(String[] args) {
SpringApplication.run(EdgeApplication.class, args);
}
}
ctrl+alt+鼠标左键查看注解源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
我们看下@EnableAutoConfiguration这个注解:
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
通过@AutoConfiguration启用Spring应用程序上下文的自动配置,这个注解会导入一个AutoConfigurationImportSelector的类,而这个类会去读取一个spring.factories下key为EnableAutoConfiguration对应的全限定名的值。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
这个spring.factories里面配置的那些类,主要作用是告诉Spring Boot这个stareter所需要加载的那些xxxAutoConfiguration类,也就是你真正的要自动注册的那些bean或功能。然后,我们实现一个spring.factories指定的类,标上@Configuration注解,一个starter就定义完了。
概括 SpringBoot 在启动时会去依赖的 starter 包中寻找 /META-INF/spring.factories 文件,然后根据文件中配置的路径去扫描项目所依赖的 Jar 包,这类似于 Java 的 SPI 机制。
实战说明
需求
之前系统前后端传参使用VO,如果微服务场景下各个服务频繁发版,其他服务调用方改动会很大,因此后端改用了Map<String, String> paramMap数据类型来传递参数,请求进入到controller层后要在每一个方法内进行参数校验,如果我们可以开发一个starter,只需加上注解即可实现参数校验,就会减少很多冗余代码。
效果
通过数组注解实现参数校验使用示例
@GetMapping("/product")
@ParamCheck(name = "paramMap", params = {
@ParamCheck.Param(name = "requireProductCategory", type = Integer.class),
@ParamCheck.Param(name = "requireProductDetail", type = Integer.class, required = false),
@ParamCheck.Param(name = "productName", type = String.class, required = true)})
public ResultModel query(@RequestParam Map<String, String> paramMap) {
// 业务逻辑
ResultModel resultModel = new ResultModel();
resultModel.setMsg("success");
return resultModel;
}
验证效果
请求
http://localhost:8080/product
响应
{
code: 10001,
msg: "参数校验失败"
}
请求
http://localhost:8080/product?productName=%E8%8B%B9%E6%9E%9C
响应
{
code: 0,
msg: "success"
}
实战开发
为了方便,大家可以先把demo代码克隆下来https://github.com/aaa081215/parametercheck-spring-boot-starter.git
命名规则
官方命名:spring-boot-starter-xxxx
非官方命名:xxxx-spring-boot-starter
这里我们使用非官方命名 parametercheck-spring-boot-starter (避免将来spring-boot官方使用你的starter而重名)
开发步骤
第一步 新建 maven 工程
相信各位小伙都非常熟练,不多说了,省略
第二步 添加 pom 依赖,完整 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>
<artifactId>parametercheck-spring-boot-starter</artifactId>
<groupId>9421.top</groupId>
<version>1.0.1-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>7</source>
<target>7</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
</project>
仅需引入spring-boot-autoconfigure即可,由于此项目用到了aop等,所以引用了多个依赖。
第三步 编写上文曾提到的spring.factories文件
- resource 目录下,创建 META-INF 目录
- META-INF 目录下创建 spring.factories 文件
完整spring.factories文件如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
hiweek.autoconfig.VerifyAnnotationAutoConfiguration
第四步 新建自动配置类,VerifyAnnotationAutoConfiguration 类如下
类名为spring.factories声明的类
@Aspect
@Configuration
public class VerifyAnnotationAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ParamAspect paramAspect() {
return new ParamAspect();
}
/**
* aop切入点
*/
@Around("@annotation(hiweek.verify.annotation.ParamCheck)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
return paramAspect().around(joinPoint);
}
}
解释:
@Aspect声明切面
@Configuration 相当于把该类作为spring的xml配置文件中的,配置spring容器(应用上下文)
@ConditionalOnMissingBean 判断是否执行初始化代码,即如果用户已经创建了bean,则相关的初始化代码不再执行。
@Around("@annotation(hiweek.verify.annotation.ParamCheck)")
切入点为hiweek.verify.annotation.ParamCheck注解
也就是说当执行到有ParamCheck注解的方法时,会执行环绕通知paramAspect().around(joinPoint);
第五步 自定义元注解
现在starter配置基本完成了,接下来开发上面代码中的自定义注解hiweek.verify.annotation.ParamCheck
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamCheck {
/**
* 存放参数的变量名
*/
String name();
/**
* 要校验的参数
*/
Param[] params();
/**
* 自定义注解,用来描述要校验的参数信息
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface Param {
/**
* 参数名
*/
String name();
/**
* 是否必须
*/
boolean required() default false;
/**
* 参数类型
*/
Class type();
/**
* 参数格式
*/
String format() default "";
}
}
解释:
@Target({ElementType.METHOD})
作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
取值(ElementType)有:
- 1.CONSTRUCTOR:用于描述构造器
- 2.FIELD:用于描述域
- 3.LOCAL_VARIABLE:用于描述局部变量
- 4.METHOD:用于描述方法
- 5.PACKAGE:用于描述包
- 6.PARAMETER:用于描述参数
- 7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
@Retention(RetentionPolicy.RUNTIME)
- 1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃
- 2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期
- 3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
第六步 ParamAspect 业务逻辑开发(配置环绕通知,校验参数是否合法)
ParamAspect
完整代码
public class ParamAspect {
/**
* 配置环绕通知。
* 校验参数是否合法
*
* @param joinPoint 切入点
* @return 执行结果
*/
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
ResultModel resultModel = new ResultModel();
// 1 对包含ParamCheck注解的方法进行操作
if (method.isAnnotationPresent(ParamCheck.class)) {
// 2 获取注解paramCheckName要校验的参数名
ParamCheck paramCheck = method.getAnnotation(ParamCheck.class);
String paramCheckName = paramCheck.name();
// 3 从joinPoint中获得获取方法的所有参数名
String[] parameterNames = methodSignature.getParameterNames();
// 4 根据注解中要校验的参数名与joinPoint中所有的参数名,获取到要校验的数据的下标
int paramMapIndex = ArrayUtils.indexOf(parameterNames, paramCheckName);
if (paramMapIndex == -1) {
resultModel.setFailed(CommonStatusCode.PARAM_INVALID, "要校验的方法参数名应与注解字段相同");
return resultModel;
}
// 5 根据joinPoint获取方法的所有元素与下标,拿到真实值
Object[] args = joinPoint.getArgs();
Map<String, String> realParamMap = (Map<String, String>) args[paramMapIndex];
int index = 0;
ParamCheck.Param[] params = paramCheck.params();
String[] paramArray = new String[params.length];
boolean[] requiredArray = new boolean[params.length];
Class[] classArray = new Class[params.length];
String[] format = new String[params.length];
// 6 获得注解中定义的校验规则
for (ParamCheck.Param paramItem : params) {
paramArray[index] = paramItem.name();
requiredArray[index] = paramItem.required();
classArray[index] = paramItem.type();
format[index] = paramItem.format();
index++;
}
try {
StringUtils.checkParam(realParamMap, paramArray, requiredArray, classArray, format);
} catch (ParamException e) {
resultModel.setFailed(CommonStatusCode.PARAM_INVALID, e.getMessage());
return resultModel;
}
}
return joinPoint.proceed();
}
}
至此开发以及完成。