SpringBoot API implemented in @Idempotent idempotent and processed by the Redis setnx custom annotations

1. Briefly

  • Objective: certain period of time, the same request (the same traffic parameter) access the same interface, the only successful once, the rest of the refuse.

2. Introduction redis support

Because the need redisis setnxto ensure that only one interface can be accessed, it is necessary to introduce redis.

2.1.pom.xml

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
  <exclusions>
    <!-- 需要排除哪些包由具体项目觉得 -->
    <exclusion>
      <artifactId>spring-boot-starter-logging</artifactId>
      <groupId>org.springframework.boot</groupId>
    </exclusion>
  </exclusions>
</dependency>

2.2.application.properties

spring.redis.host=11.22.33.44
spring.redis.port=26379
spring.redis.database=1
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=500
spring.redis.pool.min-idle=0
spring.redis.timeout=0

2.3.Redis JUnit Test Case

/**
 * @author hanchao
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTemplateTest {

    @Resource
    private RedisTemplate<String,String > redisTemplate;

    @Test
    public void simpleTest() {
        ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
        String key = "RedisTemplateTest-simpleTest-001";
        valueOperations.set(key,key+key);
        System.out.println(valueOperations.get(key));
    }
}

3. The introduction of idempotent

3.1. Idempotent abnormal

/**
 * 用于专门处理幂等相关异常。
 * @author hanchao
 */
public class IdempotentException extends RuntimeException {

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

    @Override
    public String getMessage() {
        return super.getMessage();
    }
}

3.2. Idempotent comment

/**
 * 幂等注解
 * @author wangchao
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
    /**
     * 幂等名称,作为redis缓存Key的一部分。
     */
    String value();
    
    /**
     * 幂等过期时间,即:在此时间段内,对API进行幂等处理。
     */
    long expireMillis();
}

3.3. Idempotent section

/**
 * 幂等切面
 * @author wangchao
 */
@Aspect
@Component
@ConditionalOnClass(RedisTemplate.class)
public class IdempotentAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(IdempotentAspect.class);
    /**
     * redis缓存key的模板
     */
    private static final String KEY_TEMPLATE = "idempotent_%s";

    @Resource
    private RedisTemplate<String,String> redisTemplate;

    /**
     * 根据实际路径进行调整
     */
    @Pointcut("@annotation(pers.hanchao......anno.Idempotent)")
    public void executeIdempotent() {
    }

    @Around("executeIdempotent()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
      	//获取方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
      	//获取幂等注解
        Idempotent idempotent = method.getAnnotation(Idempotent.class);
      	//根据 key前缀 + @Idempotent.value() + 方法签名 + 参数 构建缓存键值
      	//确保幂等处理的操作对象是:同样的 @Idempotent.value() + 方法签名 + 参数
        String key = String.format(KEY_TEMPLATE, idempotent.value() + "_" + KeyUtil.generate(method, joinPoint.getArgs()));
        //通过setnx确保只有一个接口能够正常访问
        //调用KeyUtil工具类生成key
      	String redisRes = redisTemplate.execute((RedisCallback<String>) conn -> ((JedisCommands) conn.getNativeConnection()).set(key, key, "NX", "PX", idempotent.expireMillis()));
      
        if (Objects.equals("OK", redisRes)) {
            return joinPoint.proceed();
        } else {
            LOGGER.debug("Idempotent hits, key=" + key);
            throw new IdempotentException("Idempotent hits, key=" + key);
        }
    }
}

3.4. Tools

/**
 * Key生成工具
 * @author hanchao
 */
public class KeyUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(KeyUtil.class);

    /**
     * 根据{方法名 + 参数列表}和md5转换生成key
     */
    public static String generate(Method method, Object... args) {
        StringBuilder sb = new StringBuilder(method.toString());
        for (Object arg : args) {
            sb.append(toString(arg));
        }
        return DigestUtils.md5Hex(sb.toString());
    }

    private static String toString(Object object) {
        if (object == null) {
            return "null";
        }
        if (object instanceof Number) {
            return object.toString();
        }
        //调用json工具类转换成String
        return JsonUtil.toJson(object);
    }
}

/**
 * Json格式化工具
 * @author hanchao
 */
public class JsonUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtil.class);
    private static final ObjectMapper MAPPER = new ObjectMapper();

    static {
        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).setSerializationInclusion(Include.NON_NULL);
    }

    /**
     * Java Object Maps To Json
     */
    public static String toJson(Object obj) {
        String result;
        if (obj == null || obj instanceof String) {
            return (String) obj;
        }
        try {
            result = MAPPER.writeValueAsString(obj);
        } catch (Exception e) {
            LOGGER.error("Java Object Maps To Json Error !");
            throw new RuntimeException("Java Object Maps To Json Error !", e);
        }
        return result;
    }
}

4. Interface annotation marker idempotent

@RestController
public class DemoController {
    @Resource
    private DemoService demoService;

    /**
     * @Idempotent的value值随意,一般保持与接口url一致接口。
     */
    @Idempotent(value = "/cock/alarm", expireMillis = 1000L)
    @PostMapping(value = "/cock/alarm")
    public String demo(@RequestBody DemoPo po) {
      //..
    }
}

Guess you like

Origin blog.csdn.net/hanchao5272/article/details/92073405