常用自定义注解

该博客介绍了如何利用Spring AOP实现三个方面:1)方法执行计时,通过`MethodTimer`注解记录方法执行时间;2)参数校验的通用返回,借助`ValidCommonResp`注解简化错误处理;3)接口访问频次拦截,通过`Ide`注解实现幂等性,防止短时间内重复请求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、方法计时器

注解类:MethodTimer

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

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

@Slf4j
@Component
@Aspect
public class MethodTimerProcessor {

    /**
     * 处理 @MethodTimer 注解
     */
    @Around("@annotation(methodTimer)")
    public Object timerAround(ProceedingJoinPoint point, MethodTimer methodTimer) throws Throwable {
        long beginMills = System.currentTimeMillis();
        // process the method
        Object result = point.proceed();
        log.info("{} 耗时 : {} ms", point.getSignature(), System.currentTimeMillis() - beginMills);
        return result;
    }
}

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

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

注解类:ValidCommonResp

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

处理器(aop+spring):ValidCommonRespProcessor

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

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

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

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

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

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

注解 @Ide

/**
 * 幂等校验注解类
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Ide {

	/**
	 * 关键key
	 * key是本次请求中参数的键,
	 * 重复请求的key取自header中的rid
	 * 用来标识这个请求的唯一性
	 * 拦截器中会使用key从请求参数中获取value
	 *
	 * @return String
	 */
	String key() default "";

	/**
	 * 自定义key的前缀用来区分业务
	 */
	String perFix();

	/**
	 * 自定义key的超时时间(基于接口)
	 */
	String expireTime();

	/**
	 * 禁止重复提交的模式
	 * 默认是全部使用
	 */
	IdeTypeEnum ideTypeEnum() default IdeTypeEnum.ALL;
}

AOP 横切处理逻辑

/**
 * 注解执行器 处理重复请求 和串行指定条件的请求
 * <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>
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
@ConditionalOnClass(RedisService.class)
public class IdeAspect extends BaseAspect {

	private final ThreadLocal<String> PER_FIX_KEY = new ThreadLocal<String>();

	/**
	 * 配置注解后 默认开启
	 */
	private final boolean enable = true;

	/**
	 * request请求头中的key
	 */
	private final static String HEADER_RID_KEY = "RID";

	/**
	 * redis中锁的key前缀
	 */
	private static final String REDIS_KEY_PREFIX = "RID:";

	/**
	 * 锁等待时长
	 */
	private static int LOCK_WAIT_TIME = 10;

	private final RedisService redisService;

	@Autowired
	IdeAspectConfig ideAspectConfig;

	@Pointcut("@annotation(cn.com.bmac.wolf.core.ide.annotation.Ide)")
	public void watchIde() {

	}

	@Before("watchIde()")
	public void doBefore(JoinPoint joinPoint) {
		Ide ide = getAnnotation(joinPoint, Ide.class);

		if (enable && null != ide) {
			ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
			if (null == attributes) {
				throw new IdeException("请求数据为空");
			}
			HttpServletRequest request = attributes.getRequest();
			//根据配置文件中的超时时间赋值
			if (Func.isNotBlank(ideAspectConfig.getExpireTime())) {
				if(Func.isNumeric(ideAspectConfig.getExpireTime())){
					LOCK_WAIT_TIME = Integer.parseInt(ideAspectConfig.getExpireTime());
				}
			}
			//根据注解传参赋值
			if(Func.isNotBlank(ide.expireTime())){
				LOCK_WAIT_TIME = Integer.parseInt(ide.expireTime());
			}
			//1.判断模式
			if (ide.ideTypeEnum() == IdeTypeEnum.ALL || ide.ideTypeEnum() == IdeTypeEnum.RID) {
				//2.1.通过rid模式判断是否属于重复提交
				String rid = request.getHeader(HEADER_RID_KEY);

				if (Func.isNotBlank(rid)) {
					Boolean result = redisService.tryLock(REDIS_KEY_PREFIX + rid, LOCK_WAIT_TIME);
					if (!result) {
						throw new IdeException("命中RID重复请求");
					}
					log.debug("msg1=当前请求已成功记录,且标记为0未处理,,{}={}", HEADER_RID_KEY, rid);
				} else {
					log.warn("msg1=header没有rid,防重复提交功能失效,,remoteHost={}" + request.getRemoteHost());
				}
			}
			boolean isApiExpireTime = false;
			if (ide.ideTypeEnum() == IdeTypeEnum.ALL
					|| ide.ideTypeEnum() == IdeTypeEnum.KEY) {
				//2.2.通过自定义key模式判断是否属于重复提交
				String key = ide.key();
				if (Func.isNotBlank(key)) {
					String val = "";
					Object[] paramValues = joinPoint.getArgs();
					String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
					//获取自定义key的value
					String[] keys = key.split("\\|");

					for(int i = 0; i < keys.length; i++){
						for (int j = 0; j < paramNames.length; j++) {
							//BindingResult 不能转json,会导致线程报错终止
							if (paramValues[j] instanceof BindingResult) {
								continue;
							}
							String params = JSON.toJSONString(paramValues[j]);
							if (params.startsWith("{")) {
								//如果是对象
								//通过key获取value
								JSONObject jsonObject = JSON.parseObject(params);
								val = val + jsonObject.getString(keys[i]);
							} else if (keys[i].equals(paramNames[j])) {
								//如果是单个k=v
								val = val + params;
							} else {
								//如果自定义的key,在请求参数中没有此参数,说明非法请求
								log.warn("自定义的key,在请求参数中没有此参数,防重复提交功能失效");
							}
						}
					}


					//判断重复提交的条件
					String perFix = "";
					if (Func.isNotBlank(val)) {
						String[] perFixs = ide.perFix().split("\\|");
						int perFixsLength = perFixs.length;
						for(int i = 0; i < perFixs.length; i++){
							if(Func.isNotBlank(perFix)){
								perFix = perFix + ":" + perFixs[i];
							}else{
								perFix = perFixs[i];
							}
						}
						perFix = perFix + ":" + val;
						Boolean result = true;
						try {
							result = redisService.tryLock(perFix, LOCK_WAIT_TIME);
						} catch (Exception e) {
							log.error("获取redis锁发生异常", e);
							throw e;
						}
						if (!result) {
							String targetName = joinPoint.getTarget().getClass().getName();
							String methodName = joinPoint.getSignature().getName();
							log.error("msg1=不允许重复执行,,key={},,targetName={},,methodName={}", perFix, targetName, methodName);
							throw new IdeException("不允许重复提交");
						}
						//存储在当前线程
						PER_FIX_KEY.set(perFix);
						log.info("msg1=当前请求已成功锁定:{}", perFix);
					} else {
						log.warn("自定义的key,在请求参数中value为空,防重复提交功能失效");
					}
				}
			}
		}
	}

	@After("watchIde()")
	public void doAfter(JoinPoint joinPoint) throws Throwable {
		try {
			Ide ide = getAnnotation(joinPoint, Ide.class);
			if (enable && null != ide) {

				if (ide.ideTypeEnum() == IdeTypeEnum.ALL
						|| ide.ideTypeEnum() == IdeTypeEnum.RID) {
					ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
					HttpServletRequest request = attributes.getRequest();
					String rid = request.getHeader(HEADER_RID_KEY);
					if (Func.isNotBlank(rid)) {
						try {
							log.info("msg1=当前请求已成功处理,,rid={}", rid);
						} catch (Exception e) {
							log.error("释放redis锁异常", e);
						}
					}
					PER_FIX_KEY.remove();
				}

				if (ide.ideTypeEnum() == IdeTypeEnum.ALL
						|| ide.ideTypeEnum() == IdeTypeEnum.KEY) {
					// 自定义key
					String key = ide.key();
					if (Func.isNotBlank(key) && Func.isNotBlank(PER_FIX_KEY.get())) {
						try {
							log.info("msg1=当前请求已成功释放,,key={}", PER_FIX_KEY.get());
							PER_FIX_KEY.set(null);
							PER_FIX_KEY.remove();
						} catch (Exception e) {
							log.error("释放redis锁异常", e);
						}
					}
				}
			}
		} catch (Exception e) {
			log.error(e.getMessage(), e);
		}
	}
}

其他相关类

@Data
@Component
@ConfigurationProperties(prefix = "ide")
public class IdeAspectConfig {
	/**
	 * 过期时间 秒
	 */
	private String expireTime;

}
@Getter
@AllArgsConstructor
public enum IdeTypeEnum {

	/**
	 * 0+1
	 */
	ALL(0, "ALL"),
	/**
	 * ruid 是针对每一次请求的
	 */
	RID(1, "RID"),
	/**
	 * key+val 是针对相同参数请求
	 */
	KEY(2, "KEY");

	private final Integer index;
	private final String title;
}
常用自定义注解有以下几种: 1. @Autowired:用于自动装配依赖对象,可以在Spring容器中自动查找匹配的Bean,并将其注入到目标对象中。 2. @RequestMapping:用于映射HTTP请求的URL路径到具体的处理方法上,可以指定请求的方法、路径、参数等。 3. @Component:用于将一个类标识为Spring容器中的组件,可以通过@ComponentScan注解扫描并注册到容器中。 4. @Transactional:用于标识一个方法或类需要进行事务管理,可以控制事务的提交、回滚等行为。 5. @Validated:用于对方法参数进行校验,可以指定参数的验证规则,如非空、长度范围等。 6. @Aspect:用于定义切面,可以在方法执行前、后或异常时执行一些额外的逻辑,如日志记录、性能监控等。 这些是常用自定义注解,可以根据具体的需求自定义更多的注解来实现特定的功能。 #### 引用[.reference_title] - *1* [Spring Boot中的自定义注解](https://blog.csdn.net/qq_44717657/article/details/130869793)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [springboot项目中自定义注解的使用总结、java自定义注解实战(常用注解DEMO)](https://blog.csdn.net/qq_21187515/article/details/109643130)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [自定义注解](https://blog.csdn.net/u014365523/article/details/126730735)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值