Public number: Java show small cafe, website: www.javaxks.com. Source: jianshu.com/p/c806003a8530

preface

In a real development project, where an exposed interface often faces many requests, let’s explain the concept of idempotence: Any number of executions has the same impact as one execution. By this definition, the ultimate implication is that the impact on the database must be one-time and not repeated. How to ensure its idempotent, usually have the following means:

The database establishes a unique index to ensure that only one data token can be inserted into the database. Each interface obtains a token before making a request, and then adds the token to the header body of the request in the next request for background verification. If the verification succeeds, the token can be deleted. Mysql > select * from innoDB; mysql > select * from innoDB; mysql > select * from innoDB First of all, the database is queried to see if there is any data. If there is any data, the request is rejected directly. If there is no data, it is proved that it is the first time to enter the database. Redis implements automatic idempotent:

Build redis service Api

The first is to set up the Redis server.

Redis stater from Springboot or Jedis from Spring can also be introduced. The main API used later is its set method and exists method. Here we use the redisTemplate encapsulated by SpringBoot

/** * Redis utility class */
@Component
public class RedisService {

  @Autowired
  private RedisTemplate redisTemplate;

  /** * write cache *@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;
  }


  /** * Write cache set aging time *@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;
  }


  /** * Check whether the corresponding value * exists in the cache@param key
   * @return* /
  public boolean exists(final String key) {
      return redisTemplate.hasKey(key);
  }

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

  /** * delete the corresponding value *@param key
   */
  public boolean remove(final String key) {
      if (exists(key)) {
          Boolean delete = redisTemplate.delete(key);
          return delete;
      }
      return false; }}Copy the code

Custom annotation AutoIdempotent

Define a custom annotation. The main purpose of defining this annotation is to add it to methods that need to implement idempotent. Any method that annotates it will implement automatic idempotent. If the annotation is scanned by reflection in the background, the METHOD is automatically idempotent, using the meta annotation elementType. METHOD to indicate that it can only be placed on the METHOD, and etentionPolicy.runtime to indicate that it is at RUNTIME

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

Token creation and verification

Token service interface: We create a new interface to create the token service. There are mainly two methods, one for creating the token and the other for verifying the token. Creating a token is a string. Checking a token is a request object. Why pass a Request object? Public interface TokenService {TokenService: TokenService: TokenService: TokenService: TokenService: TokenService: TokenService: TokenService: TokenService: TokenService: TokenService: TokenService: TokenService: TokenService: TokenService: TokenService

/** * createToken * @return */ public String createToken(); /** * checkToken * @param request * @return */ public Boolean checkToken(HttpServletRequest request) throws Exception;Copy the code

} Token service implementation class: Token refers to the Redis service. The random algorithm tool class is used to generate a random UUID string to create the token, and then the token is put into redis (to prevent redundant data retention, the expiration time is set to 10000 seconds, depending on the service). If the token is successfully put into REDis, the token value is returned. The checkToken method simply fetches the token from the header to the value (if not from the header, then from the paramter) and throws an exception if it does not exist. This exception message can be caught by the interceptor and returned to the front end.

@Service
public class TokenServiceImpl implements TokenService {

    @Autowired
    private RedisService redisService;


    /** * create token **@return* /
    @Override
    public String createToken(a) {
        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) {
                returntoken.toString(); }}catch (Exception ex){
            ex.printStackTrace();
        }
        return null;
    }


    /** * check token **@param request
     * @return* /
    @Override
    public boolean checkToken(HttpServletRequest request) throws Exception {

        String token = request.getHeader(Constant.TOKEN_NAME);
        if (StrUtil.isBlank(token)) {// No token exists in header
            token = request.getParameter(Constant.TOKEN_NAME);
            if (StrUtil.isBlank(token)) {// parameter does not contain tokens
                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; }}Copy the code

Configuration of interceptors

Web Configuration class, realize WebMvcConfigurerAdapter, main effect is to add autoIdempotentInterceptor to the Configuration class, so the interceptor can we effect, pay attention to using the @ Configuration annotations, So you can add it to the context when the container starts

@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @Resource
   private AutoIdempotentInterceptor autoIdempotentInterceptor;

    /** * Add interceptor *@param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
        super.addInterceptors(registry); }} Interception handler: the main function is to intercept the scanned AutoIdempotent annotations to the method, then call the checkToken() method of tokenService to verify whether the token is correct, if the exception is captured, render the exception information into JSON and return to the front end/** * interceptor */
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    /** * preprocessing **@param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if(! (handlerinstanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        // Scan marked by ApiIdempotment
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if(methodAnnotation ! =null) {
            try {
                return tokenService.checkToken(request);// Idempotent check, pass the check, fail to throw an exception, and return a friendly message through unified exception processing
            }catch (Exception ex){
                ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
                writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
                throwex; }}// Must return true, otherwise all requests will be blocked
        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 {}/** * The returned JSON value *@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(); }}}Copy the code

The test case

To simulate the business request class, first we need to get the token through the /get/ Token path through the getToken() method to obtain the specific token, then we call the testIdempotence method annotated @Autoidempotent, the interceptor will intercept all the request. The checkToken() method in TokenService is called when the annotation is detected. If an exception is caught, it will throw the exception to the caller.

@RestController
public class BusinessController {


    @Resource
    private TokenService tokenService;

    @Resource
    private TestService testService;


    @PostMapping("/get/token")
    public String  getToken(a){
        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(a) {
        String businessResult = testService.testIdempotence();
        if (StrUtil.isNotEmpty(businessResult)) {
            ResultVo successResult = ResultVo.getSuccessResult(businessResult);
            return JSONUtil.toJsonStr(successResult);
        }
        returnStrUtil.EMPTY; }}Copy the code

With postman request, first access the get/token path to obtain the token specific:

The first request is successful, and then the second request is made:

The second request is returned as a repeat operation, so we can make the first request succeed. The second request will fail:

conclusion

This blog using springboot and interceptors are introduced, and the implementation of the interface idempotent redis to grace, for power, such as in the actual development process is very important, because an interface may be countless client calls, how to ensure that it does not affect the background of business process, how to guarantee its impact data only once is very important, It prevents the generation of dirty or messy data, and also reduces concurrency, which is a very beneficial thing. While the traditional approach is to judge the data every time, this approach is not intelligent and automation, more trouble. Today’s automation can also improve application scalability.