1.自定义注解
@Documented
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLimit {
// 在 second 秒内,最大只能请求 maxCount 次
int second() default 1;
int maxCount() default 5;
}
- @Target({ElementType.METHOD,ElementType.TYPE}) 指定这个注解可以用在方法或者类上(或者接口)
- @Retention(RetentionPolicy.RUNTIME) 指定注解的生命周期是运行时。
首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。
2.使用注解
直接在controller上加注解即可,当然也可以在单独接口方法加,这里表示1秒最多三次请求,如果不指定,则按照默认1秒五次(定义注解时指定的默认值)
3.拦截器
想要注解生效需要定义接口访问的拦截器,访问接口前先通过拦截器获取到我们的自定义注解及配置内容,实现限流
@Component
public class RequestLimitIntercept implements HandlerInterceptor {
@Autowired
RedisTemplate<String,Object> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* isAssignableFrom() 判定此 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口
* isAssignableFrom()方法是判断是否为某个类的父类
* instanceof关键字是判断是否某个类的子类
*/
if(handler.getClass().isAssignableFrom(HandlerMethod.class)){
//HandlerMethod 封装方法定义相关的信息,如类,方法,参数等
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 获取方法中是否包含注解
RequestLimit methodAnnotation = method.getAnnotation(RequestLimit.class);
//获取 类中是否包含注解,也就是controller 是否有注解
RequestLimit classAnnotation = method.getDeclaringClass().getAnnotation(RequestLimit.class);
// 如果 方法上有注解就优先选择方法上的参数,否则类上的参数
RequestLimit requestLimit = methodAnnotation != null?methodAnnotation:classAnnotation;
if(requestLimit != null){
if(isLimit(request,requestLimit)){
responseOut(response, R.error(403,"请求过快"));
//throw new RuntimeException("请求过快");
return false;
}
}
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
//判断请求是否受限
public boolean isLimit(HttpServletRequest request,RequestLimit requestLimit){
// 受限的redis 缓存key ,因为这里用浏览器做测试,我就用sessionid 来做唯一key,如果是app ,可以使用 用户ID 之类的唯一标识。
String limitKey = request.getServletPath()+request.getSession().getId();
// 从缓存中获取,当前这个请求访问了几次
Integer redisCount = (Integer) redisTemplate.opsForValue().get(limitKey);
if(redisCount == null){
//初始 次数
redisTemplate.opsForValue().set(limitKey,1,requestLimit.second(), TimeUnit.SECONDS);
System.out.println("写入redis --");
}else{
System.out.println("intValue-->"+redisCount.intValue());
if(redisCount.intValue() >= requestLimit.maxCount()){
return true;
}
// 次数自增
redisTemplate.opsForValue().increment(limitKey);
}
return false;
}
/**
* 回写给客户端
* @param response
* @param result
* @throws IOException
*/
private void responseOut(HttpServletResponse response, R result) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = null ;
String json = JSONObject.toJSON(result).toString();
out = response.getWriter();
out.append(json);
}
}
流程:请求进来先进拦截器,然后获取到接口方法,如果是处理请求的方法就获取到当前方法和当前方法所在的类是否有RequestLimit 注解(方法上的注解优先),如果有的话判断是否达到限制,达到的话进行拦截,返回限流信息。
判断是否达到限流的流程:获取sessionid作为redis的key,拿到请求次数,如果次数为空,就保存到redis,value是1,过期时间是注解指定的判断时间(这里是1s),如果在1s内请求再次进来,redis中的value就进行+1操作,如果redis中value的值高于注解设定的值,则表示请求次数达到限制,返回true,否则返回false。
这样一个简单的接口限流就实现了。