公众号merlinsea
- 下单流程设计
- 首先需要生成token,这个token作为唯一标识这个用户下的这笔订单
- 其次携带token和订单信息创建订单并完成支付功能。
- 如何设计才能更加解耦呢?
- 第5步的过程可以通过注解切面来实现判断是否存在token的步骤和下单支付的流程解耦,通过拦截器将用户携带token和订单信息的请求拦截下来,判断是否携带了token,如果是且验证成功则放行,否则不放行。
- 代码实现
- 编写自定义注解
- 这里的自定义注解的核心作用是【标识】哪个方法需要注入切面类,其验证的类型分为方法参数类型和token类型,即有该自定义注解的方法就需要验证request-token。
/** * 自定义防重提交注解,用于标识是这次request的类型是方法参数还是token类型 * @author lianglin */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RepeatSubmit { /** * 防重提交类型: * 1、方法参数类型 : key=ip+method+param * 2、token类型:key=accountNo+token */ enum Type { PARAM, TOKEN } /** * 默认防重提交是方法参数 * @return */ Type limitType() default Type.PARAM; /** * 加锁过期时间是5秒 * @return */ long LockTime() default 5; }
- 编写切面类【核心重点】
- 切面类的核心作用就是配合自定义注解@RepeatSubmit,在有自定义注解的地方注入这个切面方法并执行
- @PointCut代表切入点
- @Around代表核心切入逻辑
/** * 定义切面类 */ @Aspect @Component @Slf4j public class RepeatSubmitAspect { @Autowired private StringRedisTemplate stringRedisTemplate; /** * 定义 @Pointcut注解表达式, * 方式一:@annotation:当执行的方法上拥有指定的注解时生效(我们采用这) * 方式二:execution:一般用于指定方法的执行 * * @param repeatSubmit */ @Pointcut("@annotation(repeatSubmit)") public void pointcutNoRepeatSubmit(RepeatSubmit repeatSubmit) { } /** * 环绕通知, 围绕着方法执行 * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。 * * 方式一:单用 @Around("execution(* net.xdclass.controller.*.*(..))")可以 * 方式二:用@Pointcut和@Around联合注解也可以(我们采用这个) * * * 两种方式 * 方式一:加锁 固定时间内不能重复提交 * <p> * 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交 * * @param joinPoint * @param noRepeatSubmit * @return * @throws Throwable * 所有@RepeatSubmit注解的方法都添加该环绕通知 */ @Around("pointcutNoRepeatSubmit(noRepeatSubmit)") public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit noRepeatSubmit) throws Throwable { //获取request请求 HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); boolean res=false; String type = noRepeatSubmit.limitType().name(); if (type.equals(RepeatSubmit.Type.PARAM.name())) { //方式一方法参数 TODO } else { //方式二,令牌形式 String requestToken = request.getHeader("request-token"); if (StringUtils.isBlank(requestToken)) { throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL); } LoginUser loginUser = LoginInterceptor.threadLocal.get(); //"order:submit:%s:%s" String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, loginUser.getAccountNo(),requestToken); /** * 提交表单的token key * 方式一:不用lua脚本获取再判断,之前是因为 key组成是 order:submit:accountNo, value是对应的token,所以需要先获取值,再判断 * 方式二:可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成 */ res = stringRedisTemplate.delete(key); } if (!res) { //删除失败,说明redis中没有这个token throw new BizException(BizCodeEnum.ORDER_CONFIRM_REPEAT); } System.out.println("目标方法执行前"); //被自定义注解标识的方法继续执行 Object object = joinPoint.proceed(); System.out.println("目标方法执行后"); return object; } }
- 注解方式解决防重提交问题的测试
- 业务controller层测试接口
- 这个接口用@RepeatSubmit()标识,spring就会在执行这个方法前先执行切面类中的around逻辑代码,根据around逻辑代码的执行结果来判断是否需要继续执行testRequestToken代码。
/** * 测试下单token的获取 * @return */ @GetMapping("test") @RepeatSubmit(limitType = RepeatSubmit.Type.TOKEN) public JsonData testRequestToken(){ return JsonData.buildSuccess("测试成功"); }
- 测试结果
- 不携带request-token的情况
- 结论:下单之前必须先获取下单token,否则校验失败
- 第一次携带request-token的情况
- 结论:携带了下单token且第一次使用这个token才起作用
- 第二次携带同一个token的情况
- 结论:多次使用同一个下单token会导致校验失败的,因此避免了重复下单的问题
- 总结一下自定义注解的实现逻辑
- 自定义注解,需要指定这个注解作用在哪些地方 即@Target的功能
- 定义切面类
- 通过@Aspect告诉spring这个是一个切面类并交给spring ioc容器管理。
- 配合自定义注解指明该切面逻辑怎么执行
- @Around是核心执行逻辑。