Spring Boot custom Annotation implements automatic interface exponentiation

In actual development projects, an exposed interface often faces many requests. Let's explain the concept of idempotence: the impact of any number of executions is the same as the impact of one execution . According to this meaning, the ultimate meaning is that the impact on the database can only be one-time and cannot be repeated. How to ensure its idempotence usually has the following means:

  1. The database establishes a unique index to ensure that only one piece of data is finally inserted into the database

  2. Token mechanism, first obtain a token before each interface request, and then add this token to the header body of the request in the next request, and verify it in the background. If the verification passes the deletion of the token, the token will be judged again in the next request

  3. Pessimistic lock or optimistic lock, pessimistic lock can ensure that other SQL cannot update data every time for update (when the database engine is innodb, the condition of select must be a unique index to prevent the entire table from being locked)

  4. Check first and then judge. First, check whether there is data in the database. If the existence certificate has been requested, the request is directly rejected. If it does not exist, it proves that it is the first time to come in and let it go directly.

Redis realizes the schematic diagram of automatic idempotence:

 

Build redis service Api

  • The first is to build a redis server.

  • It is also possible to introduce the redis statuser from springboot, or the jedis packaged by Spring. The main api used later is its set method and exists method. Here we use springboot packaged redisTemplate

/**
 * redis工具类
 */
@Component
public class RedisService {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 写入缓存
     * @param key
     * @param value
     * @return
     */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }


    /**
     * 写入缓存设置时效时间
     * @param key
     * @param value
     * @return
     */
    public boolean setEx(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }


    /**
     * 判断缓存中是否有对应的value
     * @param key
     * @return
     */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 读取缓存
     * @param key
     * @return
     */
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }

    /**
     * 删除对应的value
     * @param key
     */
    public boolean remove(final String key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        return false;

    }

}

Custom annotation AutoIdempotent

Customize an annotation. The main purpose of defining this annotation is to add it to a method that needs to be idempotent. If a method is annotated, it will be automatically idempotent. If this annotation is scanned in the background using reflection, it will process this method to achieve automatic idempotence. Use the meta annotation ElementType.METHOD to indicate that it can only be placed on the method, and attentionPolicy.RUNTIME indicates that it is running

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
  
}

token creation and verification

  • Token service interface: We create a new interface to create a token service. There are mainly two methods in it, one is used to create the token, and the other is used to verify the token. The creation of a token is mainly a string, and the verification of the token is mainly to convey the request object. Why do you want to pass the request object? The main function is to get the token in the header, and then check, get the specific error message through the thrown Exception and return it to the front end

public interface TokenService {

    /**
     * 创建token
     * @return
     */
    public  String createToken();

    /**
     * 检验token
     * @param request
     * @return
     */
    public boolean checkToken(HttpServletRequest request) throws Exception;

}
  • Token's service implementation class: The token refers to the redis service, and the creation of the token uses a random algorithm tool class to generate a random uuid string, and then puts it into redis (in order to prevent redundant retention of data, the expiration time is set to 10000 seconds, which can be specific Depending on the business), if the placement is successful, the token value will be returned at the end. The checkToken method is to get the token value from the header (if it is not available in the header, get it from the paramter), if it does not exist, throw an exception directly. This exception information can be captured by the interceptor and then returned to the front end.

@Service
public class TokenServiceImpl implements TokenService {

    @Autowired
    private RedisService redisService;


    /**
     * 创建token
     *
     * @return
     */
    @Override
    public String createToken() {
        String str = RandomUtil.randomUUID();
        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) throws Exception {

        String token = request.getHeader(Constant.TOKEN_NAME);
        if (StrUtil.isBlank(token)) {// header中不存在token
            token = request.getParameter(Constant.TOKEN_NAME);
            if (StrUtil.isBlank(token)) {// parameter中也不存在token
                throw new ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
            }
        }

        if (!redisService.exists(token)) {
            throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
        }

        boolean remove = redisService.remove(token);
        if (!remove) {
            throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
        }
        return true;
    }
}

Interceptor configuration

  • The web configuration class, which implements WebMvcConfigurerAdapter, is mainly used to add autoIdempotentInterceptor to the configuration class, so that the interceptor can take effect. Pay attention to the @Configuration annotation, so that it can be added to the context when the container is started

@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @Resource
   private AutoIdempotentInterceptor autoIdempotentInterceptor;

    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
        super.addInterceptors(registry);
    }
}
  • Intercept processor: The main function is to intercept and scan AutoIdempotent to the annotation method, and then call the checkToken() method of tokenService to verify whether the token is correct. If an exception is caught, the exception information will be rendered into json and returned to the front end

/**
 * 拦截器
 */
@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 {
                return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
            }catch (Exception ex){
                ResultVo failedResult = ResultVo.getFailedResult(101, 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();
        }
    }

}

Test case

  • To simulate the business request class, first we need to get the specific token through the getToken() method through the /get/token path, and then we call the testIdempotence method. This method is annotated with @AutoIdempotent, and the interceptor will intercept all requests. When there is the annotation on the processing method, the checkToken() method in TokenService will be called. If an exception is caught, the exception will be thrown to the caller. Let’s simulate the request below:

@RestController
public class BusinessController {


    @Resource
    private TokenService tokenService;

    @Resource
    private TestService testService;


    @PostMapping("/get/token")
    public String  getToken(){
        String token = tokenService.createToken();
        if (StrUtil.isNotEmpty(token)) {
            ResultVo resultVo = new ResultVo();
            resultVo.setCode(Constant.code_success);
            resultVo.setMessage(Constant.SUCCESS);
            resultVo.setData(token);
            return JSONUtil.toJsonStr(resultVo);
        }
        return StrUtil.EMPTY;
    }


    @AutoIdempotent
    @PostMapping("/test/Idempotence")
    public String testIdempotence() {
        String businessResult = testService.testIdempotence();
        if (StrUtil.isNotEmpty(businessResult)) {
            ResultVo successResult = ResultVo.getSuccessResult(businessResult);
            return JSONUtil.toJsonStr(successResult);
        }
        return StrUtil.EMPTY;
    }
}
  • To use postman request, first visit the get/token path to get specific token:

  • Use to get the token, and then put it in the specific request header, you can see that the first request is successful, and then we request the second time:

  • The second request, the return is a repetitive operation, it can be seen that the repetitive verification is passed, and we will only let it succeed at the first time and fail at the second time:

to sum up

This blog introduces the use of springboot, interceptors, and redis to elegantly implement interface idempotence. It is very important for idempotence in the actual development process, because an interface may be called by countless clients. How to ensure that it is not It is very important to affect the business processing of the background, how to ensure that it only affects the data once. It can prevent the generation of dirty data or messy data, and can also reduce the amount of concurrency. It is a very beneficial thing. The traditional method is to judge the data every time. This method is not intelligent and automatic enough, which is more troublesome. And today's automated processing can also improve the scalability of the program.

Guess you like

Origin blog.csdn.net/baidu_39322753/article/details/107615157