幂等性注解,以及适配成启动类注解

一. 前言:

在后端提供给前端接口之后,在某性场景下,如支付等。如果一个请求多次消费同一个接口,显然是错误的。这里显然就需要实现,多次消费和一次消费,是一样的结果了,这就和幂等性有关了。

二. 前后端未分离编写:

这由于架构的特殊性,小熙采用的是后端跳转页面并签发幂等性token,访问时再次校验。当然也有唯一索引、加锁、状态机等方法,看个人习惯吧。

  1. 创建幂等性注解

    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 {
    }
    
  2. 添加对应处理拦截器(这里是实现的主要地方,采用拦截器在进入方法之前做校验)

    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 {
        }
    
    }
    
    
  3. 编写签发和校验幂等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());
            }
        }
    
    }
    
  4. 提供相应辅助工具类
    (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;
    
    }
    
    
  5. 将幂等处理拦截器注册到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"
            };
        }
    }
    
    
    1. 编写幂等配置文件类
    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("/**");
        }
    
    
    }
    
    
  6. 签发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)在上面的幂等配置文件类中,注册该处理拦截器
    注册签发幂等拦截器

三. 测试前后端未分离的幂等注解

  1. 编写测试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);
        }
    
  2. 通过方法获取签发的幂等token(这里可以自定义封装,小熙只是展示下返回结果)
    签发幂等token

  3. 通过签发幂等token注解获取(在cookies中查看)
    注解签发幂等token

  4. 多次请求统一接口(测试幂等性注解是否生效,截图结果为第二次请求以及以后的或token过期的预期结果)
    幂等token过多消费

  5. 没有携带幂等token
    未携带幂等token的非法访问

四. 前后端分离编写:

由于架构特性,小熙这里采用访问时生成唯一幂等token,存储到redis中,然后返回前端,下次访问如果已存在则判定为重复提交。当然这是在不是恶意访问的时候,成立的。
理解了上述编写,那实现就较为简单了,可自行实现。(不想实现的,可以看下小熙简单编写的)

  1. 处理拦截器添加处理

    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 {
        }
    
    
    }
    
    
  2. 业务逻辑中添加处理(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();
        }
    
  3. 展示结果
    展示结果

五. 后语:

以上就是小熙对于幂等接口,提供的一些想法了,如果有好的想法可以提出来讨论下。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值