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());
}
}