目录
一、分析
1、业务需求
前端,或者第三方的接口请求要实现幂等操作,考虑到节省开发时间,做了一个可配置的幂等注解
2、原理
1.通过自定义注解配置幂等参数:唯一主键,过期时间;
2.通过HandleInterceptor拦截请求方法和注解参数,进行校验
3.根据唯一主键,主键的值,过期时间,请求url来生成唯一token存redis。后续重复请求判断redis是否已经存在key。如果redis存在则说明已经执行过了(不管执行失败还是成功都算)
3、优点
只要在方法上添加注解配置
@AutoIdempotent(id = "business",expire = 90)
4、缺点
需借助redis,只是过期时间内的幂等,过期时间外的就不支持
二、代码实现
1、创建元注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 接口幂等注解
* 原理:
* 1.通过自定义注解配置幂等参数:唯一主键,过期时间;
* 2.AutoIdempotentInterceptor类中通过实现HandlerInterceptor接口拦截请求方法和注解参数,进行校验
* 3.根据唯一主键,主键的值,过期时间,请求url来生成唯一token存redis。后续重复请求判断redis是否已经存在key。如果redis存在则说明已经执行过了(不管执行失败还是成功都算)
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
//幂等控制主键
String id();
//键过期时间单位:s
long expire() default 60;
}
2、自定义幂等拦截器
import cn.hutool.json.JSONUtil;
import com.tlxy.lhn.annotation.AutoIdempotent;
import com.tlxy.lhn.service.TokenService;
import com.tlxy.lhn.enums.ResultCode;
import com.tlxy.lhn.model.response.vo.ResultVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
/**
* 幂等拦截器
* WebConfiguration类中实现WebMvcConfigurer接口,将该类添加进去
*/
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
/**
* 预处理
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//被ApiIdempotment标记的扫描
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if (methodAnnotation != null) {
try {
// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
String id = methodAnnotation.id();
long expire = methodAnnotation.expire();
return tokenService.checkToken(request, id, expire);
} catch (Exception ex) {
ResultVO failedResult = ResultVO.fail(ResultCode.FAILED, ex.getMessage());
writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
throw ex;
}
}
//必须返回true,否则会被拦截一切请求
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
/**
* 返回的json值
*
* @param response
* @param json
* @throws Exception
*/
private void writeReturnJson(HttpServletResponse response, String json) throws Exception {
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);
} catch (IOException e) {
} finally {
if (writer != null)
writer.close();
}
}
}
3、添加自定义拦截器到Spring MVC的配置
import com.tlxy.lhn.component.AutoIdempotentInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import javax.annotation.Resource;
/**
* 过时方式:继承WebMvcConfigurerAdapter
* Spring 5.0 以后WebMvcConfigurer接口替换掉 WebMvcConfigurerAdapter
* 实现WebMvcConfigurer接口
*/
@Configuration
//public class WebConfiguration extends WebMvcConfigurerAdapter {
public class WebConfiguration implements WebMvcConfigurer {
@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//AutoIdempotentInterceptor类为实现HandlerInterceptor接口的拦截器实例
registry.addInterceptor(autoIdempotentInterceptor);
// //用于设置拦截器的过滤路径规则
// .addPathPatterns("/**/*.json")
// //用于设置不需要拦截的过滤规则
// .excludePathPatterns("/adm/commons/**");
// super.addInterceptors(registry);
}
}
4、Token操作类
import javax.servlet.http.HttpServletRequest;
public interface TokenService {
/**
* 创建token
*
* @return
*/
public String createToken();
/**
* 检验token
*
* @param request
* @return
*/
public boolean checkToken(HttpServletRequest request, String id, long expire) throws Exception;
}
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.tlxy.lhn.common.Constant;
import com.tlxy.lhn.component.RedisService;
import com.tlxy.lhn.service.TokenService;
import com.tlxy.lhn.util.RequestHandlerUtil;
import org.apache.commons.lang.text.StrBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* token操作实现类
*/
@Service
public class TokenServiceImpl implements TokenService {
/**
* 幂等检测key过期时间默认120s
*/
private final static long expire = 120;
private final static String AUTO_IDEMPOTENT_KEY = "md";
@Autowired
RedisService redisService;
@Override
public String createToken() {
String str = RandomUtil.randomString(32);
StrBuilder token = new StrBuilder();
try {
token.append(Constant.Redis.TOKEN_PREFIX).append(str);
redisService.setEx(token.toString(), token.toString(), 10000L);
boolean notEmpty = StrUtil.isNotEmpty(token.toString());
if (notEmpty) {
return token.toString();
}
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
/**
* 检验token
*
* @param request
* @return
*/
@Override
public boolean checkToken(HttpServletRequest request, String id, long expire) throws Exception {
String token = request.getHeader(Constant.TOKEN_NAME);
Map<String, String> reqParam = RequestHandlerUtil.getReqParam(request, id);
String requestURI = request.getRequestURI();
requestURI = requestURI.substring(requestURI.length() - 20);
StringBuffer key = new StringBuffer();
key.append(AUTO_IDEMPOTENT_KEY)
.append(requestURI).append(":")
.append(id == null ? "id" : id).append(":")
.append(reqParam == null ? "value" : reqParam).append(":")
.append(token == null ? "0" : token);
if (StrUtil.isBlank(token)) {// header中不存在token
token = request.getParameter(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// parameter中也不存在token
throw new Exception("parameter为空");
}
}
if (!redisService.exists(token)) {
throw new Exception("parameter不存在");
}
boolean remove = redisService.remove(token);
if (!remove) {
throw new Exception("token不存在");
}
return true;
}
}