secKill项目 --- @AccessLimit用法的问题

本文探讨了在Spring MVC项目中使用@AccessLimit注解进行限流时遇到的问题。当Controller参数包含MiaoshaUser对象时,原始数据可能会丢失。解决方案包括自定义VO对象以避免限流注解的影响,或者创建新的注解以区分限流处理。通过调整,可以将用户逻辑移到参数解析器,保持拦截器仅处理限流功能,简化代码并提高可读性。文章最后作者分享了对VO对象意义的理解。

先附上原本的代码:

项目中,用了拦截器,用于简化限流判断

@Service
public class AccessInterceptor extends HandlerInterceptorAdapter{
	@Autowired
	MiaoshaUserService miaoshaUserService;
	@Autowired
	RedisService redisService;
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		if(handler instanceof HandlerMethod) {
			//先去取得用户做判断
			MiaoshaUser user=getUser(request,response);		
			//将user保存下来
			UserContext.setUser(user);
			HandlerMethod hm=(HandlerMethod)handler;
			AccessLimit aclimit=hm.getMethodAnnotation(AccessLimit.class);
			//无该注解的时候,那么就不进行拦截操作
			if(aclimit==null) {
				return true;
			}
			//获取参数
			int seconds=aclimit.seconds();
			int maxCount=aclimit.maxCount();
			boolean needLogin=aclimit.needLogin();
			String key=request.getRequestURI();
			System.out.println("------------:"+key);
			if(needLogin) {
				if(user==null) {
					//需要给客户端一个提示
					render(response,CodeMsg.SESSION_ERROR);
					return false;
				}
				//需要的登录
				key+="_"+user.getId();
			}else {//不需要登录
				//不需要操作
			}
            ...  //限制访问次数

		}
		return super.preHandle(request, response, handler);
	}
    
    // 从request中获取 token,根据token去Redis取User
    private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) {

		String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
		String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
		if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
			return null;
		}
		String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
		return userService.getByToken(response, token);
	}
}

经过拦截器处理,通过ThreadLocal传参到 UserArgumentResolver

@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    ... // 省略代码
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        MiaoshaUser user = UserContext.getUser();
        return user;
    }

}


但其实这里是有问题的。

当Controller的传参有MiaoshaUser时,看看下面这两种情况会怎么样:

  1. 用了@accessLimit , 但 needLogin=false
  2. 没用 @accessLimit

代码逻辑:

  1. 首先,needLogin=false ,那无论是redis还是request中,都是没有token的,那自然getUser()返回的MiaoshaUser 必然为null。那参数处理器(UserArgumentResolver)中,获得的MiaoshaUser 那当然也是null了。

    那么问题来了:原本传参的MiaoshaUser 数据被覆盖,丢失了。

    因为needLogin = false ,可是传了 MiaoshaUser ,这传的数据自然不可能是null。

  2. 而没用注解的情况也是类似的:

    • token,那就返回缓存里面的MiaoshaUser 对象
    • 没有token,那就返回null

    但无论是那种,传参的MiaoshaUser原本的数据,都丢失了

解决办法:

  1. 真要传相关数据的,自定义一个xxxVO,这样就不会受限制。MiaoshaUser就照常,根据token获取。而这也是项目的使用方法,但这种情况下,@AccessLimit中的needLogin属性就没有必要存在了,不是说 default true 的问题。而是这种用法中,就不存在needLogin = false的情况,因此这属性是多余的。

  2. 不自定义额外的xxxVO,则需再自定义一个注解@realData ,作用到参数上,

    即: controllerMethod(@realData MiaoshaUser miaoshaUser) ,然后在参数解析器中,

     public boolean supportsParameter(MethodParameter parameter) {
            Class<?> clazz = parameter.getParameterType();
            return clazz == MiaoshaUser.class
                //加上参数判断,没有@realData注解的,才进行解析
                && !parameter.hasParameterAnnotation(realData.class);
        }
    

    (ps:其实更简单的,可以根据传的MiaoshaUser 是否为null,来判断是否处理。
    理论上是可以根据request获取相关的JSON对象判断的,但笔者找了相关资料,实验过,都没有达到目标。
    若有人知道如何处理,麻烦留言告知一下,谢谢。)


笔者是比较推荐用第一个种自定义xxxVO的方法的。因为更容易理解,注解的话,很容易忘记使用。

附上这种方式实现后的代码,主要是把User的逻辑转移到参数解析器UserArgumentResolver中。因为此时拦截器要实现的就只是限流功能,与User无关。(此时ThreadLocal也就没用了)

拦截器:

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        if (handler instanceof HandlerMethod) {
            // 去掉(转移)了 getUser等逻辑、UserContext等内容
            HandlerMethod hm = (HandlerMethod) handler;
            AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
            if (accessLimit == null) {
                return true;
            }
            int seconds = accessLimit.seconds();
            int maxCount = accessLimit.maxCount();


            String key = request.getRequestURI();
            // 根据 ip 等限流,原本是根据userId
            // key += "_" + user.getId();
            key += "_" + request.getRemoteAddr();

            // 后续代码无异
            AccessKey ak = AccessKey.withExpire(seconds);
            Integer count = redisService.get(ak, key, Integer.class);
            if (count == null) {
                redisService.set(ak, key, 1);
            } else if (count < maxCount) {
                redisService.incr(ak, key);
            } else {
                // 其实这里可以直接抛出异常,效果一样
                // throw new GlobalException(CodeMsg.ACCESS_LIMIT_REACHED);
                render(response, CodeMsg.ACCESS_LIMIT_REACHED);
                return false;
            }
        }
        return super.preHandle(request, response, handler);
    }

参数解析器:

public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        HttpServletRequest request=webRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response=webRequest.getNativeResponse(HttpServletResponse.class);
        MiaoshaUser user = getUser(request,response);
    	// 统一判空
    	if (user == null) {
            throw new GlobalException(CodeMsg.SESSION_ERROR);
        }
    
        return user;
    }

 	// 下面的就是转移了代码,无异
    private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) {
        String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
        String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
        if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
            return null;
        }
        String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
        return userService.getByToken(response, token);
    }

    private String getCookieValue(HttpServletRequest request, String cookiName) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null || cookies.length <= 0) {
            return null;
        }
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals(cookiName)) {
                return cookie.getValue();
            }
        }
        return null;
    }

这样处理后,Controller方法里面大量的(↓),就可以去掉了

if (user == null) {
      return Result.error(CodeMsg.SESSION_ERROR);
}

至于真的用于传输数据的,就如上所述,替换成xxxVO,简化判空的逻辑就跟其他的一样,用JSR-303简化即可。


感触:

其实之前一直不懂为什么不用现成的对象,非要自定义VO对象。

如果是传参数据涉及多个对象还可以理解,但数据都在一个对象内的时候,还要定义VO对象,说是减少传输数据、逻辑清晰。但笔者还是不太理解的。而经过这次的处理,也算是理解了VO的意义所在。


本文完,有误欢迎指出

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值