Spring Boot接口幂等性封装
封装思路
接口幂等性后端的处理方式,就是通过redis来验证表单提交时申请的token有效性。因此,我们可以利用Spring Boot的自动装配特性,针对此功能封装一个可用的starter。本文仅提供实现的思路和核心代码供大家参考。
配置文件
我们针对前段能够配置一些属性进行参数封装,如:可以配置Token有效时间、可以配置请求头中的key,以及是否启用等信息。
在本文中,我针对模块的启用信息、请求头Token的key以及Token过期时间封装成IdempotentProperties类
@Data
@ConfigurationProperties(prefix = "boot.idempotent")
public class IdempotentProperties {
private boolean enable;
private String storeTokenKey = "IDEMPOTENT-TOKEN";
private int expireTime = 5;
}
核心实现思路
针对幂等性的思路,我们可以自定义一个注解,利用Spring的AOP特性,对标注注解的方法进行增强,然后判断改请求头中的Token是否有效,来确保接口的幂等性。同时,自动装配置只需要做两件事情,
1、把AOP的切点配置进去。
2、暴露外部申请接口请求Token的接口
自定义注解
@Idempotent
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
boolean enable() default true;
}
幂等性核心service
IdempotentService
@AllArgsConstructor
public class IdempotentService {
private final IdempotentProperties properties;
private final RedisTemplate redisTemplate;
public String createToken() {
String token = UUID.fastUUID().toString();
redisTemplate.opsForValue().set(token, token, properties.getExpireTime(), TimeUnit.MINUTES);
return token;
}
public void checkToken(HttpServletRequest request) {
String token = request.getHeader(properties.getStoreTokenKey());
if (StrUtil.isBlank(token)) {
token = request.getParameter(properties.getStoreTokenKey());
if (StrUtil.isBlank(token)) {
throw new IdempotentException("非法提交");
}
}
Object tokenVal = redisTemplate.opsForValue().get(token);
if (Objects.isNull(tokenVal)) {
throw new IdempotentException("禁止重复提交!");
}
Boolean del = redisTemplate.delete(token);
if (!del) {
throw new IdempotentException("禁止重复提交!");
}
}
}
幂等性方法拦截器
IdempotentMethodInterceptor
public class IdempotentMethodInterceptor implements MethodInterceptor {
private IdempotentService idempotentService;
public IdempotentMethodInterceptor(IdempotentService idempotentService) {
this.idempotentService = idempotentService;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//获取幂等性注解对象
Idempotent idempotent = invocation.getMethod().getAnnotation(Idempotent.class);
//幂等性未启用
if (!idempotent.enable()) {
return invocation.proceed();
}
ServletRequestAttributes attributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
// 非Web环境
if (Objects.isNull(attributes)) {
return invocation.proceed();
}
HttpServletRequest request = attributes.getRequest();
idempotentService.checkToken(request);
return invocation.proceed();
}
}
自定义幂等性异常
IdempotentException
public class IdempotentException extends RuntimeException {
public IdempotentException() {
}
public IdempotentException(String message) {
super(message);
}
public IdempotentException(Throwable cause) {
super(cause);
}
public IdempotentException(String message, Throwable cause) {
super(message, cause);
}
public IdempotentException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
自动装配类
自动装配类IdempotentAutoConfiguration如下:
@Configuration
@EnableConfigurationProperties(IdempotentProperties.class)
@ConditionalOnProperty(prefix = "boot.idempotent", name = "enable", havingValue = "true", matchIfMissing = true)
@RestController
public class IdempotentAutoConfiguration {
private static final String REPEAT_SUBMIT_POINT_CUT = "@annotation(com.xx.common.idempotent.annotation.Idempotent)";
@Autowired
private IdempotentProperties idempotentProperties;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private IdempotentService idempotentService;
@Bean
public IdempotentService idempotentService() {
return new IdempotentService(idempotentProperties, redisTemplate);
}
@Bean
public DefaultPointcutAdvisor repeatSubmitPointCutAdvice() {
//声明一个AspectJ切点
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
//设置切点表达式
pointcut.setExpression(REPEAT_SUBMIT_POINT_CUT);
// 配置增强类advisor, 切面=切点+增强
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
//设置切点
advisor.setPointcut(pointcut);
//设置增强(Advice)
advisor.setAdvice(new IdempotentMethodInterceptor(idempotentService));
//设置增强拦截器执行顺序
// FIXME 所有的AOP顺序需要统一管理,否则会顺序错乱会导致功能异常
advisor.setOrder(600);
return advisor;
}
@GetMapping("api/idempotent/token")
public Result generationToken() {
return Res.ok().data(idempotentService.createToken());
}
}