常用自定义注解
导航
- 一、方法计时器
- 二、valid 参数校验的通用返回
- 三、接口访问频次拦截(幂等)
一、方法计时器
注解类: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;
}
还没有评论,来说两句吧...