赞
踩
在后端提供给前端接口之后,在某性场景下,如支付等。如果一个请求多次消费同一个接口,显然是错误的。这里显然就需要实现,多次消费和一次消费,是一样的结果了,这就和幂等性有关了。
这由于架构的特殊性,小熙采用的是后端跳转页面并签发幂等性token,访问时再次校验。当然也有唯一索引、加锁、状态机等方法,看个人习惯吧。
创建幂等性注解
package com.chengxi.datalom.idempotent;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author chengxi
* @date 2020/8/7 10:00
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
添加对应处理拦截器(这里是实现的主要地方,采用拦截器在进入方法之前做校验)
package com.chengxi.datalom.idempotent; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; 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.lang.reflect.Method; /** * @author chengxi * @date 2020/8/7 10:02 */ @AllArgsConstructor @NoArgsConstructor public class ApiIdempotentInterceptor implements HandlerInterceptor { @Autowired private TokenService tokenService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class); if (methodAnnotation != null) { // 幂等性校验, 校验通过则放行, 这里的异常采用自定义服务层收集异常捕获,并友好返回 check(request); } return true; } private void check(HttpServletRequest request) { tokenService.checkToken(request); } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
编写签发和校验幂等token的逻辑
(1) 接口
package com.chengxi.datalom.idempotent; import javax.servlet.http.HttpServletRequest; /** * @author chengxi * @date 2020/8/7 10:21 */ public interface TokenService { /** * 签发token * @return */ String createToken(); /** * 校验幂等性的token * @param request */ void checkToken(HttpServletRequest request); }
(2)实现类
package com.chengxi.datalom.idempotent; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletRequest; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * @author chengxi * @date 2020-08-07 */ @Service public class TokenServiceImpl implements TokenService { @Autowired private RedisTemplate redisTemplate; @Override public String createToken() { String str = UUID.randomUUID().toString().replaceAll("-",""); StringBuffer token = new StringBuffer(); token.append(Constant.IDEMPOTENCE_TOKEN_PREFIX).append("_").append(str); // 签发的IDEMPOTENCE_TOKEN默认有效期为1分钟 redisTemplate.opsForValue().set(token.toString(), token.toString(), 1, TimeUnit.MINUTES); return token.toString(); } @Override public void checkToken(HttpServletRequest request) { String token = request.getHeader(Constant.IDEMPOTENT_TOKEN.getName()); // header中不存在token if (StringUtils.isBlank(token)) { token = request.getParameter(Constant.IDEMPOTENT_TOKEN.getName()); // parameter中也不存在token if (StringUtils.isBlank(token)) { throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg()); } } if (StringUtils.isBlank((String)redisTemplate.opsForValue().get(token))) { throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg()); } Boolean deleteTokenBoolean = redisTemplate.delete(token); if (!deleteTokenBoolean) { throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg()); } } }
提供相应辅助工具类
(1) 服务层收集异常类
package com.chengxi.datalom.idempotent; /** * @author chengxi * @date 2020/8/7 11:00 */ public class BaseException extends RuntimeException { private static final long serialVersionUID = 8415686531516484839L; public BaseException(String message) { super(message); } public BaseException(Exception e){ this(e.getMessage()); } }
package com.chengxi.datalom.idempotent; /** * @author chengxi * @date 2020/8/7 11:02 */ public class ServiceException extends BaseException { public ServiceException(String message) { super(message); } public ServiceException(Exception e) { super(e); } }
(2)常量枚举
package com.chengxi.datalom.idempotent; /** * @author chengxi * @date 2020/8/7 10:27 */ public enum Constant { IDEMPOTENCE_TOKEN_PREFIX("IDEMPOTENCE_TOKEN_PREFIX"), IDEMPOTENT_TOKEN("IdempotentToken"); private String name; Constant(String name) { this.name = name; } public String getName() { return name; } }
(3)响应枚举
package com.chengxi.datalom.idempotent; /** * @author chengxi * @date 2020/8/7 11:20 */ public enum ResponseCode { ILLEGAL_ARGUMENT(4001, "非法请求,缺少IdempotentToken"), REPETITIVE_OPERATION(4002, "请勿重复请求"); private Integer code; private String msg; private ResponseCode(Integer code, String msg) { this.code = code; this.msg = msg; } public Integer getCode() { return code; } public String getMsg() { return msg; } }
(4)简易的响应封装类(这是小熙临时写的,大家可以替换成自己的)
package com.chengxi.demo02; import lombok.Data; import lombok.experimental.Accessors; /** * @author chengxi * @date 2020/8/10 10:16 */ @Data @Accessors(chain = true) public class ResponseDTO { private Integer resultCode; private Object resultInfo; }
将幂等处理拦截器注册到bean中(这里有两种方式,小熙倾向于自定义启动类注解的装配)
(1) 直接在模块的启动类中添加注册(虽然简单方便,但是拓展性等不太友好)
package com.chengxi.datalom; import com.chengxi.datalom.idempotent.ApiIdempotentInterceptor; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @SpringBootApplication //@MapperScan(value = "com.chengxi.datalom.mapper") public class DataLomApplication extends WebMvcConfigurerAdapter { public static void main(String[] args) { SpringApplication.run(DataLomApplication.class, args); } @Override public void addInterceptors(InterceptorRegistry registry) { // 注册幂等性拦截器 registry.addInterceptor(apiIdempotentInterceptor()); super.addInterceptors(registry); } @Bean public ApiIdempotentInterceptor apiIdempotentInterceptor() { return new ApiIdempotentInterceptor(); } }
(2) 添加自定义幂等启动类注解(小熙比较倾向于这类实现)
1. 在启动类中添加注解
package com.chengxi.datalom; import com.chengxi.datalom.idempotent.enable.EnableApiIdempotent; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @EnableApiIdempotent @SpringBootApplication //@MapperScan(value = "com.chengxi.datalom.mapper") public class DataLomApplication { public static void main(String[] args) { SpringApplication.run(DataLomApplication.class, args); } }
2.编写自定义幂等启动类
package com.chengxi.datalom.idempotent.enable; import org.springframework.context.annotation.Import; import java.lang.annotation.*; /** * @author chengxi * @date 2020/8/7 14:43 */ @Documented @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Import(ApiIdempotentImportSelector.class) public @interface EnableApiIdempotent { }
3.编写对应的选择器(这里有疑问的可以参考springBootApplication注解的实现)
package com.chengxi.datalom.idempotent.enable; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; /** * @author chengxi * @date 2020/8/7 14:45 */ public class ApiIdempotentImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata annotationMetadata) { return new String[]{ "com.chengxi.datalom.idempotent.enable.ApiIdempotentConfig" }; } }
package com.chengxi.datalom.idempotent.enable; import com.chengxi.datalom.idempotent.ApiIdempotentInterceptor; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author chengxi * @date 2020/8/7 14:47 */ @Configuration @ConditionalOnClass(WebMvcConfigurer.class) public class ApiIdempotentConfig implements WebMvcConfigurer { /** * 拦截器中加载比bean的注入优先,所以在其中加载不到,所以在这里加载从拦截器的构造中传递过去 */ @Autowired private TokenService tokenService; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new ApiIdempotentInterceptor(tokenService)).addPathPatterns("/**"); } }
签发token注解(有想法的可忽略,自定义)
这里的签发token可以自己根据自己的喜好封装响应给前端,小熙这里提供一种思路,生成签发token注解,可以加在跳转方法上
(1) 签发幂等token的注解
package com.chengxi.datalom.idempotent.token; import java.lang.annotation.*; /** * @author chengxi * @date 2020/8/7 14:43 */ @Inherited @Documented @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface IdempotentTokenIssuer { }
(2) 编写幂等签发token拦截器
package com.chengxi.datalom.idempotent.token; import com.chengxi.datalom.idempotent.Constant; import com.chengxi.datalom.idempotent.TokenService; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; /** * @author chengxi * @date 2020/8/7 16:36 */ @NoArgsConstructor @AllArgsConstructor public class IdempotentTokenIssuerInterceptor implements HandlerInterceptor { @Autowired private TokenService tokenService; /** * handler :处理器(控制器) = controller对象 * preHandle :处理请求的(再调用controller对象方法之前执行) * :对请求进行放行(继续执行进入到方法) * :对请求过滤 * 返回值:true = 放行 * false = 过滤 * */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return false; } /** * postHandle 调用控制器方法之后执行的方法 * :处理响应的 */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } /** * afterCompletion : * 整个请求处理完毕,在视图渲染完毕时回调,一般用于资源的清理或性能的统计 * 在多个拦截器调用的过程中, * afterCompletion是否取决于preHandler是否=true * */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); IdempotentTokenIssuer methodAnnotation = method.getAnnotation(IdempotentTokenIssuer.class); if (methodAnnotation != null) { createToken(response); } } } public void createToken(HttpServletResponse response){ // 签发幂等token Cookie cookie = new Cookie(Constant.IDEMPOTENT_TOKEN.getName(), tokenService.createToken()); cookie.setHttpOnly(true); response.addCookie(cookie); } }
(3)在上面的幂等配置文件类中,注册该处理拦截器
编写测试controller
/** * @author chengxi * @date 2020/5/25 11:54 */ @Controller @RequestMapping(value = "/userController") public class UserController { @Autowired private TokenService tokenService; @RequestMapping(value = "/testRequest") // @PreAuthorize("hasAuthority('admin:list')") @ApiIdempotent @ResponseBody public void testRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { System.out.println("开始进入"); String requestURI = request.getRequestURI(); System.out.println("requestURI: "+requestURI); ServletOutputStream outputStream = response.getOutputStream(); outputStream.write("响应数据:访问成功".getBytes()); outputStream.flush(); outputStream.close(); System.out.println("流程结束"); } @RequestMapping(value = "/createToken") @ResponseBody public ResponseEntity<Map<String, Object>> createToken(){ return new ResponseEntity<>(MapBuilder.<String,Object>create(Maps.newHashMap()) .put(Constant.IDEMPOTENT_TOKEN.getName(),tokenService.createToken()) .build(), HttpStatus.OK); } } /** * 测试签发幂等token注解 */ @RequestMapping(value = "/createToken2") @IdempotentTokenIssuer() public void createToken2(){ System.out.println(123); }
通过方法获取签发的幂等token(这里可以自定义封装,小熙只是展示下返回结果)
通过签发幂等token注解获取(在cookies中查看)
多次请求统一接口(测试幂等性注解是否生效,截图结果为第二次请求以及以后的或token过期的预期结果)
没有携带幂等token
由于架构特性,小熙这里采用访问时生成唯一幂等token,存储到redis中,然后返回前端,下次访问如果已存在则判定为重复提交。当然这是在不是恶意访问的时候,成立的。
理解了上述编写,那实现就较为简单了,可自行实现。(不想实现的,可以看下小熙简单编写的)
处理拦截器添加处理
package com.chengxi.demo02.idempotent; import cn.hutool.json.JSONUtil; import com.alibaba.druid.support.json.JSONUtils; import com.chengxi.demo02.ResponseDTO; import com.chengxi.demo02.service.TokenService; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Method; /** * @author chengxi * @date 2020/8/7 10:02 */ @Slf4j @AllArgsConstructor @NoArgsConstructor public class ApiIdempotentInterceptor implements HandlerInterceptor { @Autowired private TokenService tokenService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class); if (methodAnnotation != null) { try { judgeIdempotentToken(request, response); } catch (Exception e) { e.printStackTrace(); log.error(e.toString()); // ResponseDTO 是小熙临时写的简易响应也可替换成自己的,或者jsonObject等都可以 String errorInfoJson = JSONUtil.toJsonStr(new ResponseDTO().setResultCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) .setResultInfo(e.getMessage())); returnErrorInfo(response, errorInfoJson); return false; } } return true; } /** * 前后端分离时的校验 * @param request * @param response */ private void judgeIdempotentToken(HttpServletRequest request, HttpServletResponse response) { String token = tokenService.judgeIdempotentToken(request); if (StringUtils.isNotBlank(token)) { // 签发幂等token Cookie cookie = new Cookie(Constant.IDEMPOTENT_TOKEN.getName(), token); cookie.setHttpOnly(true); response.addCookie(cookie); } } private void returnErrorInfo(HttpServletResponse response, String errorInfo) throws IOException { response.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=utf-8"); PrintWriter writer = response.getWriter(); writer.println(errorInfo); writer.flush(); writer.close(); } /** * 前后未分离时的校验 * @param request */ private void check(HttpServletRequest request) { tokenService.checkToken(request); } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
业务逻辑中添加处理(tokeService中添加)
@Override public String judgeIdempotentToken(HttpServletRequest request) { String token = request.getHeader(Constant.IDEMPOTENT_TOKEN.getName()); // header中不存在token if (token == null) { token = request.getParameter(Constant.IDEMPOTENT_TOKEN.getName()); // parameter中也不存在token if (token == null) { throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg()); } } // 校验redis中是否存有已访问的token if (redisTemplate.hasKey(token)) { throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg()); } String requestURI = request.getRequestURI(); String str = UUID.randomUUID().toString().replaceAll("-",""); StringBuffer tokenSt = new StringBuffer(); tokenSt.append(Constant.IDEMPOTENCE_TOKEN_PREFIX).append("_").append(requestURI).append("_").append(str); // 签发的IDEMPOTENCE_TOKEN默认有效期为1天 redisTemplate.opsForValue().set(tokenSt.toString(), tokenSt.toString(), 1, TimeUnit.DAYS); return tokenSt.toString(); }
展示结果
以上就是小熙对于幂等接口,提供的一些想法了,如果有好的想法可以提出来讨论下。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。