大体思路:
前端请求头存放幂等性ID,自定义注解ApiIdempotent,在需要幂等性controller访问前拦截获取幂等性ID作为Redis的Key,值为N(正在执行),设置一定的过期时间。
controller执行后拦截设置Redis中key幂等性ID的值为Y(已执行完)
代码:
1. 自定义幂等性注解,在需要幂等性校验的controller方法上添加该注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
2. 访问前拦截检验幂等性Id
import com.sf.cg.common.Code.ResponseCode;
import com.sf.cg.common.constant.CommonConstant;
import com.sf.cg.common.exception.ServiceException;
import com.sf.cg.model.common.baseRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
public class baseRequestValidationAdvice extends RequestBodyAdviceAdapter {
@Resource
private RedisService redisService;
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class extends HttpMessageConverter>> converterType) {
return true;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class extends HttpMessageConverter>> converterType) {
baseRequest request = (baseRequest) body;
// ...参数检验、token检验等等
// 幂等性校验
checkIdempotent(inputMessage, parameter);
return body;
}
private void checkIdempotent(HttpInputMessage inputMessage, MethodParameter parameter){
// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
Method method = parameter.getMethod();
assert method != null;
ApiIdempotent apiIdempotent = method.getAnnotation(ApiIdempotent.class);
if (apiIdempotent != null) {
// 请求头中获得tractionId幂等性Id, 前端保证唯一
if(inputMessage.getHeaders().containsKey(CommonConstant.TRACTION_ID)){
// 检验幂等性
String tractionId = inputMessage.getHeaders().get(CommonConstant.TRACTION_ID).get(0);
checkTractionId(tractionId);
}else{
throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg());
}
}
}
private void checkTractionId(String tractionId){
if (StringUtils.isBlank(tractionId)) {
throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg());
}
// 判断Redis中是否存在,加锁看情况
String key = "traction:" + tractionId;
String value = redisService.getString(key);
if (StringUtils.isNotBlank(value)) {
if(CommonConstant.STATUS_Y.equals(value)){
// 已经执行
throw new ServiceException(CommonConstant.SUCCESS, "success");
} else{
// 请求太频繁, 请稍后再试
throw new ServiceException(ResponseCode.ACCESS_LIMIT.getMsg());
}
} else{
// 存到redis,N表示正在执行
redisService.setString(key, CommonConstant.STATUS_N, 10L);
}
}
}
3. 访问后修改幂等性Id
import com.sf.boot.base.vo.Result;
import com.sf.cg.common.constant.CommonConstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.annotation.Resource;
import java.lang.reflect.Method;
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
public class WebControllerAdvice implements ResponseBodyAdvice {
private Logger logger = LoggerFactory.getLogger(getClass());
@Resource
private RedisService redisService;
@ResponseBody
@ExceptionHandler(Exception.class)
public Result exceptionHandler(Exception e) {
return Result.DefaultFailure(e.getMessage());
}
@Override
public boolean supports(MethodParameter methodParameter, Class extends HttpMessageConverter>> aClass) {
Method method = methodParameter.getMethod();
assert method != null;
return method.isAnnotationPresent(ApiIdempotent.class);
}
@Override
public Result beforeBodyWrite(Result restResult, MethodParameter methodParameter, MediaType mediaType, Class extends HttpMessageConverter>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
String tractionId = "";
if(serverHttpRequest.getHeaders().containsKey(CommonConstant.TRACTION_ID)) {
tractionId = serverHttpRequest.getHeaders().get(CommonConstant.TRACTION_ID).get(0);
}
String key = "traction:" + tractionId;
if(CommonConstant.SUCCESS.equals(restResult.getCode())){
// 更新key的值为Y,表示已经执行完
int result = redisService.setString(key, CommonConstant.STATUS_Y);
logger.info("request success update token, result:{}", result);
} else {
// 接口执行错误,删除key
int result = redisService.del(key);
logger.info("request error delete token, result:{}", result);
}
return restResult;
}
}
4. 全局返回异常Code和错误消息
public enum ResponseCode {
ILLEGAL_ARGUMENT(10000, "参数不合法, 缺少tractionId"),
ACCESS_LIMIT(10002, "请求太频繁, 请稍后再试"),;
ResponseCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
private Integer code;
private String msg;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}



