先附上原本的代码:
项目中,用了拦截器,用于简化限流判断
@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时,看看下面这两种情况会怎么样:
- 用了
@accessLimit, 但needLogin=false - 没用
@accessLimit
代码逻辑:
-
首先,
needLogin=false,那无论是redis还是request中,都是没有token的,那自然getUser()返回的MiaoshaUser必然为null。那参数处理器(UserArgumentResolver)中,获得的MiaoshaUser那当然也是null了。那么问题来了:原本传参的
MiaoshaUser数据被覆盖,丢失了。因为
needLogin = false,可是传了MiaoshaUser,这传的数据自然不可能是null。 -
而没用注解的情况也是类似的:
- 有
token,那就返回缓存里面的MiaoshaUser对象 - 没有
token,那就返回null
但无论是那种,传参的
MiaoshaUser原本的数据,都丢失了 - 有
解决办法:
-
真要传相关数据的,自定义一个xxxVO,这样就不会受限制。
MiaoshaUser就照常,根据token获取。而这也是项目的使用方法,但这种情况下,@AccessLimit中的needLogin属性就没有必要存在了,不是说default true的问题。而是这种用法中,就不存在needLogin = false的情况,因此这属性是多余的。 -
不自定义额外的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的意义所在。
本文完,有误欢迎指出

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

2993

被折叠的 条评论
为什么被折叠?



