如何保证接口幂等?(注解版本)

背景

在开发过程中, 经常会遇到一些重复数据的问题, 这就是幂等性问题.

常见的解决方案

  • 前端可以通过按钮置灰, 来防止用户重复点击;
  • 乐观锁, 前提是接口中要有更新的逻辑, 需要有事务, 更新失败需要报错;
  • 唯一索引或去重表;
  • 悲观锁, 包括本地锁和分布式锁, 适用于写多读少的场景;
  • 使用token来解决, 比如防止表单重复提交.

使用token来解决(注解版本)

// 生成token的自定义注解
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 GenerateToken {
}

//生成token的工具类
import com.google.common.collect.Sets;
import java.util.Set;
import java.util.UUID;

public class TokenUtils {

    private static Set<String> tokenSet = Sets.newConcurrentHashSet();

    public static String getToken(){
        String token = UUID.randomUUID().toString();
        tokenSet.add(token);
        return token;
    }

    public static boolean existToken(String token){
        if(tokenSet.contains(token)){
            tokenSet.remove(token);
            return true;
        }
        return false;
    }

}

// 通过前置通知来拦截生成token的请求, 统一处理
import com.example.nginx.annotation.GenerateToken;
import com.example.nginx.util.TokenUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Aspect
@Component
public class GenerateTokenAOP {

    @Pointcut("execution(public * com.example.nginx.controller.*.*(..))")
    public void pointCut() {
    }

    @Before("pointCut()")
    public void before(JoinPoint point) {
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        GenerateToken generateToken = methodSignature.getMethod().getDeclaredAnnotation(GenerateToken.class);
        if (generateToken != null) {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletResponse httpServletResponse = attributes.getResponse();
            httpServletResponse.setCharacterEncoding("UTF-8");
            httpServletResponse.setContentType("application/json; charset=utf-8");
            try (ServletOutputStream servletOutputStream = httpServletResponse.getOutputStream();) {
                servletOutputStream.print(TokenUtils.getToken());
                servletOutputStream.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

//token存放位置的枚举类
public enum TokenPositionType {
    header("header"),
    url("url"),
    ;

    private final String value;

    TokenPositionType(String value){
        this.value = value;
    }
}

// 保证接口幂等性的注解
import com.example.nginx.enums.TokenPositionType;
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 Idempotent {

    /**
     * token存放的位置
     *
     * @return
     */
    TokenPositionType value() default TokenPositionType.header;
}

// 实现接口幂等性的切面
import com.example.nginx.annotation.Idempotent;
import com.example.nginx.enums.TokenPositionType;
import com.example.nginx.util.TokenUtils;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@Aspect
@Component
public class IdempotentAOP {

    @Pointcut("execution(public * com.example.nginx.controller.*.*(..))")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
        Idempotent idempotent = methodSignature.getMethod().getDeclaredAnnotation(Idempotent.class);
        if (idempotent == null) {
            return proceedingJoinPoint.proceed();
        }
        TokenPositionType tokenPositionType = idempotent.value();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest httpServletRequest = attributes.getRequest();
        String token;
        if (tokenPositionType == TokenPositionType.header) {
            token = httpServletRequest.getHeader("token");
        } else {
            token = httpServletRequest.getParameter("token");
        }
        if (StringUtils.isEmpty(token)) {
            this.returnMsg("Token must not be null", attributes);
            return false;
        }
        if (!TokenUtils.existToken(token)) {
            this.returnMsg("Do not submit again", attributes);
            return false;
        }

        return proceedingJoinPoint.proceed();
    }

    private void returnMsg(String msg, ServletRequestAttributes attributes) {
        HttpServletResponse httpServletResponse = attributes.getResponse();
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        try (ServletOutputStream servletOutputStream = httpServletResponse.getOutputStream();) {
            servletOutputStream.print(msg);
            servletOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// 测试类
import com.example.nginx.annotation.CusRateLimiter;
import com.example.nginx.annotation.GenerateToken;
import com.example.nginx.annotation.Idempotent;
import com.example.nginx.dto.UserDTO;
import com.example.nginx.entity.Users;
import com.example.nginx.mapper.UsersMapper;
import com.example.nginx.service.UsersService;
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;

@Controller
public class TestController {

    @Autowired
    private UsersService usersService;
    
    @GetMapping(value = "/getToken")
    @GenerateToken
    public void getToken() {
    }

    @PostMapping(value = "/addUser")
    @Idempotent
    @ResponseBody
    public boolean addUser(@RequestBody UserDTO userDTO) throws Exception {
        Users users = usersService.getByName(userDTO.getName());
        if (users != null) {
            throw new Exception("用户已存在");
        }
        System.out.println(userDTO.getName());
        System.out.println(userDTO.getAge());
        return usersService.save(new Users(null, userDTO.getName(), userDTO.getAge()));
    }
}

测试效果

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

遇到的问题

  • getWriter() has already been called for this response. 参考链接: https://www.cnblogs.com/onone/articles/8759679.html. stream输出的是二进制流, 没有对字符进行编码, stream 只适用于ISO 8859-1编码的字符;
    writer输出的是文本的信息, 是进行过系统编码后的。
  • Null return value from advice does not match primitive return type for. 通知的返回值与接口的返回值需要保持一致.
  • 集成mp会报如下的一些错, 通过换mp的版本和spring boot的版本可以解决.
 - spring boot版本: 2.x
java.lang.NoClassDefFoundError: org/springframework/boot/bind/RelaxedDataBinder
 - mp版本: 2.3.3, 刚开始报错, 之后又可以了.
Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值