Idempotent encapsulation of Spring Boot interface

Packaging ideas

The processing method of the interface idempotence backend is to verify the validity of the token applied when the form is submitted through redis. Therefore, we can use Spring Boot's auto assembly feature to encapsulate an available starter for this function. This paper only provides the implementation ideas and core code for your reference.

configuration file

We can configure some attributes for parameter encapsulation, such as the Token valid time, the key in the request header, and whether to enable or not.

In this article, I encapsulate the enabling information of the module, the key of the request header Token and the Token expiration time into the IdempotentProperties class

@Data
@ConfigurationProperties(prefix = "boot.idempotent")
public class IdempotentProperties {

    private boolean enable;

    private String storeTokenKey = "IDEMPOTENT-TOKEN";
    /**
     * Expiration time default 5 MIN
     */
    private int expireTime = 5;

}

Core implementation ideas

According to the idea of idempotence, we can customize an annotation, use Spring's AOP feature to enhance the annotation method, and then judge whether the Token in the request header is valid to ensure the idempotence of the interface. At the same time, there are only two things that need to be done for Autoinstall configuration,

1. Configure AOP's pointcut.

2. Expose the interface of external application interface request Token

Custom annotation

@Idempotent

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

    /**
     * Enable idempotent function
     */
    boolean enable() default true;
}

Idempotent core service

IdempotentService

@AllArgsConstructor
public class IdempotentService {

    private final IdempotentProperties properties;
    private final RedisTemplate redisTemplate;

    public String createToken() {
        String token = UUID.fastUUID().toString();
        redisTemplate.opsForValue().set(token, token, properties.getExpireTime(), TimeUnit.MINUTES);
        return token;
    }

    public void checkToken(HttpServletRequest request) {
        String token = request.getHeader(properties.getStoreTokenKey());
        if (StrUtil.isBlank(token)) {
            token = request.getParameter(properties.getStoreTokenKey());
            if (StrUtil.isBlank(token)) {
                throw new IdempotentException("Illegal submission");
            }
        }
        Object tokenVal = redisTemplate.opsForValue().get(token);
        if (Objects.isNull(tokenVal)) {
            throw new IdempotentException("Do not submit again!");
        }
        Boolean del = redisTemplate.delete(token);
        if (!del) {
            throw new IdempotentException("Do not submit again!");
        }
    }
}

Idempotent method interceptor

IdempotentMethodInterceptor

public class IdempotentMethodInterceptor implements MethodInterceptor {

    private IdempotentService idempotentService;

    public IdempotentMethodInterceptor(IdempotentService idempotentService) {
        this.idempotentService = idempotentService;
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //Get idempotent annotation object
        Idempotent idempotent = invocation.getMethod().getAnnotation(Idempotent.class);
        //Idempotent not enabled
        if (!idempotent.enable()) {
            return invocation.proceed();
        }
        ServletRequestAttributes attributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
        // Non Web Environment
        if (Objects.isNull(attributes)) {
            return invocation.proceed();
        }
        HttpServletRequest request = attributes.getRequest();
        idempotentService.checkToken(request);
        return invocation.proceed();
    }
}

Custom idempotent exception

IdempotentException

public class IdempotentException extends RuntimeException {

    public IdempotentException() {
    }

    public IdempotentException(String message) {
        super(message);
    }

    public IdempotentException(Throwable cause) {
        super(cause);
    }

    public IdempotentException(String message, Throwable cause) {
        super(message, cause);
    }

    public IdempotentException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

Automatic assembly class

The automatic assembly class IdempotentAutoConfiguration is as follows:

@Configuration
@EnableConfigurationProperties(IdempotentProperties.class)
@ConditionalOnProperty(prefix = "boot.idempotent", name = "enable", havingValue = "true", matchIfMissing = true)
@RestController
public class IdempotentAutoConfiguration {

    /**
     * The pointcut needs to be replaced with the custom annotation full path according to the actual situation 
     */
    private static final String REPEAT_SUBMIT_POINT_CUT = "@annotation(com.xx.common.idempotent.annotation.Idempotent)";

    @Autowired
    private IdempotentProperties idempotentProperties;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private IdempotentService idempotentService;

    @Bean
    public IdempotentService idempotentService() {
        return new IdempotentService(idempotentProperties, redisTemplate);
    }

    /**
     * Controller AOP intercept processing
     */
    @Bean
    public DefaultPointcutAdvisor repeatSubmitPointCutAdvice() {
        //Declare an AspectJ tangent
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        //Set tangent expression
        pointcut.setExpression(REPEAT_SUBMIT_POINT_CUT);
        // Configure the enhanced advisor, tangent = tangent + enhanced
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        //Set tangent point
        advisor.setPointcut(pointcut);
        //Set up enhancement (Advice)
        advisor.setAdvice(new IdempotentMethodInterceptor(idempotentService));
        //Set the execution order of the enhanced interceptor
        // All AOP sequences of FIXME need to be managed in a unified way. Otherwise, the sequence will be disordered and the function will be abnormal
        advisor.setOrder(600);
        return advisor;
    }

    /**
     * The token token is automatically generated and stored in the cache. The expiration time is 30s
     *
     * @return token
     */
    @GetMapping("api/idempotent/token")
    public Result<Object> generationToken() {
        return Res.ok().data(idempotentService.createToken());
    }
}

Tags: Programming Spring Redis

Posted on Sat, 30 May 2020 09:53:31 -0400 by kelly001