springboot+redis+Interceptor+custom annotation to realize automatic idempotent interface

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

1: Establishing a unique index in the database ensures that only one data is inserted into the database at last.

2:token mechanism, which gets a token before each interface request, then adds the token to the header body of the request when the next request is made, and verifies it in the background. If the verification is deleted by token, the next request judges token again.

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, select condition must be unique index to prevent locking the whole table)

4. Query first, then judge. First, query whether there is data in the database. If there is evidence that the request has been requested, reject the request directly. If there is no evidence, it will prove that it is the first time to come in and release directly.

Principle diagram of redis realizing automatic idempotency:

 

Catalog

First: Build the service Api of redis

1: First of all, build redis server, which has been built before, let's not dwell on it. Details can be referred to: https://www.cnblogs.com/wyq178/p/10340234.html

2: Introduce the stater of redis in Spring boot, or jedis encapsulated in Spring. The main api used later is its set method and exists method. Here we use the encapsulated redis Template of Spring boot.

/**
 * redis Tool 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 Setting Ageing 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;
    }


    /**
     * Determine whether there is a corresponding value 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;

    }

}

 

2: Custom Annotation AutoIdempotent

Customize a comment. The main purpose of defining this comment is to add it to a method that needs to be idempotent. Any method that annotates it will achieve automatic idempotency. Background uses reflection to scan the annotation, which handles the method to achieve automatic idempotency. Metaannotation ElementType.METHOD is used to indicate that it can only be placed on the method, and etentionPolicy.RUNTIME is used to indicate that it is running.

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

Third: token Creation and Inspection

1:token service interface

We create a new interface to create a token service. There are two main methods, one for creating token and the other for validating token. Creating token mainly produces a string, and checking token mainly conveys the request object. Why pass the request object? The main function is to get the token in the header, then check it, and get the specific error information back to the front end by throwing the Exception.

public interface TokenService {

    /**
     * Create token
     * @return
     */
    public  String createToken();

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

}

 

2:token's service implementation class

Token refers to redis service, creates token, generates random uuid strings using random algorithm tool classes, and then puts them into redis. If successful, the token value is returned. The checkToken method is to get the token to the value from the header (if it is not available in the header, it is obtained from the paramter) and throw an exception directly if it does not exist. This exception information can be captured 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() {
        String str = RandomUtil.randomUUID();
        StrBuilder token = new StrBuilder();
        try {
            token.append(Constant.Redis.TOKEN_PREFIX).append(str);
            redisService.setEx(token.toString(), token.toString(),1000L);
            boolean notEmpty = StrUtil.isNotEmpty(token.toString());
            if (notEmpty) {
                return token.toString();
            }
        }catch (Exception ex){
            ex.printStackTrace();
        }
        return null;
    }


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

        String token = request.getHeader(Constant.TOKEN_NAME);
        if (StrUtil.isBlank(token)) {// header It does not exist. token
            token = request.getParameter(Constant.TOKEN_NAME);
            if (StrUtil.isBlank(token)) {// parameter Nor does it exist in China. 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;
    }
}

 

IV: Interceptor configuration

1:web configuration class, to implement WebMvc Configurer Adapter, the main role is to add autoIdempotent Interceptor to the configuration class, so that we can take effect to the interceptor, pay attention to the use of the @Configuration annotation, so that when the container starts, it can be added to the context.

@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @Resource
   private AutoIdempotentInterceptor autoIdempotentInterceptor;

    /**
     * Adding interceptors
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
        super.addInterceptors(registry);
    }
}

2: Interception Processor: The main function is to intercept scanning to AutoIdempotent to annotate to method, then call tokenService's checkToken() method to check whether token is correct, and if an exception is caught, render the exception information as json back to the front end.

/**
 * Interceptor
 */
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    /**
     * Pretreatment
     *
     * @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();
        //cover ApiIdempotment Scanning of markers
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if (methodAnnotation != null) {
            try {
                return tokenService.checkToken(request);// Idempotency Check, Checkout pass is released, Check failures throw exceptions, Return friendly prompts through unified exception handling
            }catch (Exception ex){
                ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
                writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
                throw ex;
            }
        }
//You must return true, or you will be intercepted for all requests
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 value returned * @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(); } } }

5: Test cases

1: Simulated 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, which annotates @AutoIdempotent, and the interceptor intercepts all requests. When we judge that the annotation is on the processing method, we call C in TokenService. The heckToken () method throws an exception out of the caller if an exception is caught. 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;
    }
}

 

 

2: Use postman requests

First, access the get/token path to get specific to token:

By getting token and putting it into the header, you can see that the first request succeeds, and then we request the second:

The second request returns to the repetitive operation, so the repetitive validation passes. The second request will only succeed for the first time and fail for the second time.

Sixth: Summary

This blog introduces how to use spring boot, interceptor and redis to realize interface idempotency elegantly, which is very important in the actual development process, because an interface may be called by countless clients, how to ensure that it does not affect the background business processing, how to ensure that it only affects data once is Very important, it can prevent dirty data or random data, but also reduce the amount of concurrency, it is a very useful thing. The traditional method is to judge data every time, which is not intelligent and automated enough, and is more troublesome. Today's automated processing can also enhance the scalability of programs.

Keywords: PHP Redis Database JSON Spring

Added by seanrock on Sat, 06 Jul 2019 22:13:47 +0300