一、java核心知识-自定义注解
- Annotation(注解)
- 从JDK 1.5开始, Java增加了对元数据(MetaData)的支持,也就是 Annotation(注解)。
- 注解其实就是代码里的特殊标记,它用于替代配置文件
- 常见的很多 @Override、@Deprecated等
- 什么是元注解
- 注解的注解,比如当我们需要自定义注解时
- 会需要一些元注解(meta-annotation),如@Target和@Retention
- java内置4种元注解
- @Target 表示该注解用于什么地方
- ElementType.CONSTRUCTOR 用在构造器
- ElementType.FIELD 用于描述域-属性上
- ElementType.METHOD 用在方法上
- ElementType.TYPE 用在类或接口上
- ElementType.PACKAGE 用于描述包
- @Retention 表示在什么级别保存该注解信息
- RetentionPolicy.SOURCE 保留到源码上
- RetentionPolicy.CLASS 保留到字节码上
- RetentionPolicy.RUNTIME 保留到虚拟机运行时(最多,可通过反射获取)
- @Documented 将此注解包含在 javadoc 中
- @Inherited 是否允许子类继承父类中的注解
- @Target 表示该注解用于什么地方
- @interface
- 用来声明一个注解,可以通过default来声明参数的默认值
- 自定义注解时,自动继承了java.lang.annotation.Annotation接口
- 通过反射可以获取自定义注解
二、AOP+自定义注解接口防重提交多场景设计
- 防重提交方式
- token令牌方式
- ip+类+方法方式
- 利用AOP
- Aspect Oriented Program 面向切面编程, 在不改变原有逻辑上增加额外的功能
- AOP思想把功能分两个部分,分离系统中的各种关注点
- 好处
- 减少代码侵入,解耦
- 可以统一处理横切逻辑
- 方便添加和删除横切逻辑
- 业务流程
三、完整代码
RepeatSubmit
package net.dbc.annotation; import java.lang.annotation.*; /** * 自定义防重提交 * @author DBC * @version 1.0.0 * @date 2023年01月19日 14:20:13 * @website dbc655.top */ @Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RepeatSubmit { /** * 防重提交,支持两种,一个是方法参数,一个是令牌 */ enum Type { PARAM, TOKEN } /** * 默认防重提交,是方法参数 * @return */ Type limitType() default Type.PARAM; /** * 加锁过期时间,默认是5秒 * @return */ long lockTime() default 5; }
RepeatSubmitAspect
package net.dbc.aspect; import lombok.extern.slf4j.Slf4j; import net.dbc.annotation.RepeatSubmit; import net.dbc.constant.RedisKey; import net.dbc.enums.BizCodeEnum; import net.dbc.exception.BizException; import net.dbc.interceptor.LoginInterceptor; import net.dbc.utils.CommonUtil; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; import java.util.concurrent.TimeUnit; /** * 定义一个切面类 * @author DBC * @version 1.0.0 * @date 2023年01月19日 14:34:11 * @website dbc655.top */ @Aspect @Component @Slf4j public class RepeatSubmitAspect { @Autowired private StringRedisTemplate redisTemplate; @Autowired private RedissonClient redissonClient; /** * 定义 @Pointcut注解表达式, * 方式一:@annotation:当执行的方法上拥有指定的注解时生效(我们采用这) * 方式二:execution:一般用于指定方法的执行 */ @Pointcut("@annotation(repeatSubmit)") public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) { } /** * 环绕通知, 围绕着方法执行 * * @param joinPoint * @param repeatSubmit * @return * @throws Throwable * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。 * <p> * 方式一:单用 @Around("execution(* net.dbc.controller.*.*(..))")可以 * 方式二:用@Pointcut和@Around联合注解也可以(我们采用这个) * <p> * <p> * 两种方式 * 方式一:加锁 固定时间内不能重复提交 * <p> * 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交 */ @Around("pointCutNoRepeatSubmit(repeatSubmit)") public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); long accountNo = LoginInterceptor.threadLocal.get().getAccountNo(); //用于记录成功或者失败 boolean res = false; //防重提交类型 String type = repeatSubmit.limitType().name(); if(type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())){ //方式一,参数形式防重提交 long lockTime = repeatSubmit.lockTime(); String ipAddr = CommonUtil.getIpAddr(request); MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature(); Method method = methodSignature.getMethod(); String className = method.getDeclaringClass().getName(); String key = "order-server:repeat_submit:"+CommonUtil.MD5(String.format("%s-%s-%s-%s",ipAddr,className,method,accountNo)); //加锁 //res = redisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS); RLock lock = redissonClient.getLock(key); // 尝试加锁,最多等待0秒,上锁以后5秒自动解锁 [lockTime默认为5s, 可以自定义] res = lock.tryLock(0,lockTime,TimeUnit.SECONDS); }else { //方式二,令牌形式防重提交 String requestToken = request.getHeader("request-token"); if(StringUtils.isBlank(requestToken)){ throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL); } String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY,accountNo,requestToken); /** * 提交表单的token key * 方式一:不用lua脚本获取再判断,之前是因为 key组成是 order:submit:accountNo, value是对应的token,所以需要先获取值,再判断 * 方式二:可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成 */ res = redisTemplate.delete(key); } if(!res){ throw new BizException(BizCodeEnum.ORDER_CONFIRM_REPEAT); } log.info("环绕通知执行前"); Object obj = joinPoint.proceed(); log.info("环绕通知执行后"); return obj; } }
本文作者为DBC,转载请注明。