常用自定义注解

Myth丶恋晨 2022-09-02 00:43 305阅读 0赞

导航

  • 一、方法计时器
  • 二、valid 参数校验的通用返回
  • 三、接口访问频次拦截(幂等)

一、方法计时器

注解类:MethodTimer

  1. @Target({ ElementType.METHOD})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface MethodTimer {
  4. }

处理器(需要AOP和spring的支持):MethodTimerProcessor

  1. @Slf4j
  2. @Component
  3. @Aspect
  4. public class MethodTimerProcessor {
  5. /** * 处理 @MethodTimer 注解 */
  6. @Around("@annotation(methodTimer)")
  7. public Object timerAround(ProceedingJoinPoint point, MethodTimer methodTimer) throws Throwable {
  8. long beginMills = System.currentTimeMillis();
  9. // process the method
  10. Object result = point.proceed();
  11. log.info("{} 耗时 : {} ms", point.getSignature(), System.currentTimeMillis() - beginMills);
  12. return result;
  13. }
  14. }

使用方法:直接标记在 Controller 的方法上。

二、valid 参数校验的通用返回

注解类:ValidCommonResp

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface ValidCommonResp {
  4. }

处理器(aop+spring):ValidCommonRespProcessor

  1. @Slf4j
  2. @Aspect
  3. @Component
  4. public class ValidCommonRespProcessor {
  5. /** * 处理 @ValidCommonResp 注解. * 注意,BindingResult是Spring validation的校验结果, * 当参数传入 BindingResult后,Spring MVC就不再控制校验 * 结果的返回,如果不希望使用 @ValidCommonResp的校验结果 * 封装,请在方法中实现校验结果的处理,二者任选其一。 * * @author mouhaotian */
  6. @Around("@annotation(validCommonResp)")
  7. public Object aroundAdvice(ProceedingJoinPoint point, ValidCommonResp validCommonResp) throws Throwable {
  8. Object[] args = point.getArgs();
  9. for (Object arg : args) {
  10. if (arg instanceof BindingResult) {
  11. BindingResult bindingResult = (BindingResult) arg;
  12. if (bindingResult.hasErrors()) {
  13. FieldError fieldError = bindingResult.getFieldError();
  14. CommonResp commonResp = new CommonResp(
  15. CommonCode.FAIL,
  16. fieldError.getField() + fieldError.getDefaultMessage());
  17. return R.data(commonResp);
  18. }
  19. break;
  20. }
  21. }
  22. Object result = point.proceed(args);
  23. return result;
  24. }
  25. }

使用方法:搭配 validation 注解、BindingResult 一起使用:

  1. @PostMapping("/xxxx")
  2. @ValidCommonResp
  3. public R submit(@Valid @RequestBody DoorzoneInfo doorzoneInfo, BindingResult result) {
  4. log.info("请求{}", doorzoneInfo);
  5. R commonResp = doorzoneInfoService.insertOrUpdDoorzoneInfo(doorzoneInfo);
  6. log.info("响应{}", commonResp);
  7. return commonResp;
  8. }

好处:可以替代代码块中处理 BindingResult 的逻辑。

三、接口访问频次拦截(幂等)

实现一个注解,当controller中的方法收到请求后,在一定时间之内(如10s内)拒绝接收相同参数的请求。即对后台接口的访问增加了频次限制,可以理解为一种不是特别标准的幂等。

注解 @Ide

  1. /** * 幂等校验注解类 */
  2. @Target(ElementType.METHOD)
  3. @Retention(RetentionPolicy.RUNTIME)
  4. @Documented
  5. public @interface Ide {
  6. /** * 关键key * key是本次请求中参数的键, * 重复请求的key取自header中的rid * 用来标识这个请求的唯一性 * 拦截器中会使用key从请求参数中获取value * * @return String */
  7. String key() default "";
  8. /** * 自定义key的前缀用来区分业务 */
  9. String perFix();
  10. /** * 自定义key的超时时间(基于接口) */
  11. String expireTime();
  12. /** * 禁止重复提交的模式 * 默认是全部使用 */
  13. IdeTypeEnum ideTypeEnum() default IdeTypeEnum.ALL;
  14. }

AOP 横切处理逻辑

  1. /** * 注解执行器 处理重复请求 和串行指定条件的请求 * <p> * 两种模式的拦截 * 1.rid 是针对每一次请求的 * 2.key+val 是针对相同参数请求 * </p> * <p> * 另根据谢新的建议对所有参数进行加密检验,提供思路,可以自行扩展 * DigestUtils.md5Hex(userId + "-" + request.getRequestURL().toString()+"-"+ JSON.toJSONString(request.getParameterMap())); * 或 DigestUtils.md5Hex(ip + "-" + request.getRequestURL().toString()+"-"+ JSON.toJSONString(request.getParameterMap())); * </p> */
  2. @Slf4j
  3. @Aspect
  4. @Component
  5. @RequiredArgsConstructor
  6. @ConditionalOnClass(RedisService.class)
  7. public class IdeAspect extends BaseAspect {
  8. private final ThreadLocal<String> PER_FIX_KEY = new ThreadLocal<String>();
  9. /** * 配置注解后 默认开启 */
  10. private final boolean enable = true;
  11. /** * request请求头中的key */
  12. private final static String HEADER_RID_KEY = "RID";
  13. /** * redis中锁的key前缀 */
  14. private static final String REDIS_KEY_PREFIX = "RID:";
  15. /** * 锁等待时长 */
  16. private static int LOCK_WAIT_TIME = 10;
  17. private final RedisService redisService;
  18. @Autowired
  19. IdeAspectConfig ideAspectConfig;
  20. @Pointcut("@annotation(cn.com.bmac.wolf.core.ide.annotation.Ide)")
  21. public void watchIde() {
  22. }
  23. @Before("watchIde()")
  24. public void doBefore(JoinPoint joinPoint) {
  25. Ide ide = getAnnotation(joinPoint, Ide.class);
  26. if (enable && null != ide) {
  27. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  28. if (null == attributes) {
  29. throw new IdeException("请求数据为空");
  30. }
  31. HttpServletRequest request = attributes.getRequest();
  32. //根据配置文件中的超时时间赋值
  33. if (Func.isNotBlank(ideAspectConfig.getExpireTime())) {
  34. if(Func.isNumeric(ideAspectConfig.getExpireTime())){
  35. LOCK_WAIT_TIME = Integer.parseInt(ideAspectConfig.getExpireTime());
  36. }
  37. }
  38. //根据注解传参赋值
  39. if(Func.isNotBlank(ide.expireTime())){
  40. LOCK_WAIT_TIME = Integer.parseInt(ide.expireTime());
  41. }
  42. //1.判断模式
  43. if (ide.ideTypeEnum() == IdeTypeEnum.ALL || ide.ideTypeEnum() == IdeTypeEnum.RID) {
  44. //2.1.通过rid模式判断是否属于重复提交
  45. String rid = request.getHeader(HEADER_RID_KEY);
  46. if (Func.isNotBlank(rid)) {
  47. Boolean result = redisService.tryLock(REDIS_KEY_PREFIX + rid, LOCK_WAIT_TIME);
  48. if (!result) {
  49. throw new IdeException("命中RID重复请求");
  50. }
  51. log.debug("msg1=当前请求已成功记录,且标记为0未处理,,{}={}", HEADER_RID_KEY, rid);
  52. } else {
  53. log.warn("msg1=header没有rid,防重复提交功能失效,,remoteHost={}" + request.getRemoteHost());
  54. }
  55. }
  56. boolean isApiExpireTime = false;
  57. if (ide.ideTypeEnum() == IdeTypeEnum.ALL
  58. || ide.ideTypeEnum() == IdeTypeEnum.KEY) {
  59. //2.2.通过自定义key模式判断是否属于重复提交
  60. String key = ide.key();
  61. if (Func.isNotBlank(key)) {
  62. String val = "";
  63. Object[] paramValues = joinPoint.getArgs();
  64. String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
  65. //获取自定义key的value
  66. String[] keys = key.split("\\|");
  67. for(int i = 0; i < keys.length; i++){
  68. for (int j = 0; j < paramNames.length; j++) {
  69. //BindingResult 不能转json,会导致线程报错终止
  70. if (paramValues[j] instanceof BindingResult) {
  71. continue;
  72. }
  73. String params = JSON.toJSONString(paramValues[j]);
  74. if (params.startsWith("{")) {
  75. //如果是对象
  76. //通过key获取value
  77. JSONObject jsonObject = JSON.parseObject(params);
  78. val = val + jsonObject.getString(keys[i]);
  79. } else if (keys[i].equals(paramNames[j])) {
  80. //如果是单个k=v
  81. val = val + params;
  82. } else {
  83. //如果自定义的key,在请求参数中没有此参数,说明非法请求
  84. log.warn("自定义的key,在请求参数中没有此参数,防重复提交功能失效");
  85. }
  86. }
  87. }
  88. //判断重复提交的条件
  89. String perFix = "";
  90. if (Func.isNotBlank(val)) {
  91. String[] perFixs = ide.perFix().split("\\|");
  92. int perFixsLength = perFixs.length;
  93. for(int i = 0; i < perFixs.length; i++){
  94. if(Func.isNotBlank(perFix)){
  95. perFix = perFix + ":" + perFixs[i];
  96. }else{
  97. perFix = perFixs[i];
  98. }
  99. }
  100. perFix = perFix + ":" + val;
  101. Boolean result = true;
  102. try {
  103. result = redisService.tryLock(perFix, LOCK_WAIT_TIME);
  104. } catch (Exception e) {
  105. log.error("获取redis锁发生异常", e);
  106. throw e;
  107. }
  108. if (!result) {
  109. String targetName = joinPoint.getTarget().getClass().getName();
  110. String methodName = joinPoint.getSignature().getName();
  111. log.error("msg1=不允许重复执行,,key={},,targetName={},,methodName={}", perFix, targetName, methodName);
  112. throw new IdeException("不允许重复提交");
  113. }
  114. //存储在当前线程
  115. PER_FIX_KEY.set(perFix);
  116. log.info("msg1=当前请求已成功锁定:{}", perFix);
  117. } else {
  118. log.warn("自定义的key,在请求参数中value为空,防重复提交功能失效");
  119. }
  120. }
  121. }
  122. }
  123. }
  124. @After("watchIde()")
  125. public void doAfter(JoinPoint joinPoint) throws Throwable {
  126. try {
  127. Ide ide = getAnnotation(joinPoint, Ide.class);
  128. if (enable && null != ide) {
  129. if (ide.ideTypeEnum() == IdeTypeEnum.ALL
  130. || ide.ideTypeEnum() == IdeTypeEnum.RID) {
  131. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  132. HttpServletRequest request = attributes.getRequest();
  133. String rid = request.getHeader(HEADER_RID_KEY);
  134. if (Func.isNotBlank(rid)) {
  135. try {
  136. log.info("msg1=当前请求已成功处理,,rid={}", rid);
  137. } catch (Exception e) {
  138. log.error("释放redis锁异常", e);
  139. }
  140. }
  141. PER_FIX_KEY.remove();
  142. }
  143. if (ide.ideTypeEnum() == IdeTypeEnum.ALL
  144. || ide.ideTypeEnum() == IdeTypeEnum.KEY) {
  145. // 自定义key
  146. String key = ide.key();
  147. if (Func.isNotBlank(key) && Func.isNotBlank(PER_FIX_KEY.get())) {
  148. try {
  149. log.info("msg1=当前请求已成功释放,,key={}", PER_FIX_KEY.get());
  150. PER_FIX_KEY.set(null);
  151. PER_FIX_KEY.remove();
  152. } catch (Exception e) {
  153. log.error("释放redis锁异常", e);
  154. }
  155. }
  156. }
  157. }
  158. } catch (Exception e) {
  159. log.error(e.getMessage(), e);
  160. }
  161. }
  162. }

其他相关类

  1. @Data
  2. @Component
  3. @ConfigurationProperties(prefix = "ide")
  4. public class IdeAspectConfig {
  5. /** * 过期时间 秒 */
  6. private String expireTime;
  7. }
  8. @Getter
  9. @AllArgsConstructor
  10. public enum IdeTypeEnum {
  11. /** * 0+1 */
  12. ALL(0, "ALL"),
  13. /** * ruid 是针对每一次请求的 */
  14. RID(1, "RID"),
  15. /** * key+val 是针对相同参数请求 */
  16. KEY(2, "KEY");
  17. private final Integer index;
  18. private final String title;
  19. }

发表评论

表情:
评论列表 (有 0 条评论,305人围观)

还没有评论,来说两句吧...

相关阅读