背景
这里介绍一下本文的背景(废话,可跳过)。上周有个我们服务的调用方反馈某个接口调用失败率很高,排查了一下,发现是因为这个接口被我之前写的一个限流器给拦截了,随着我们的服务接入了 Sentinel,这个 限流器也可以下线了。于是今天又看了一下当初了实现,发现实现的很粗糙,核心还是基于 Spring AOP 实现的。
又突然想起前段时间由于某些原因想过下掉我们服务中使用的 Shiro,因为只是因为要使用 Shiro 的鉴权( @RequiresPermissions
)就要单独引入一个框架,有点重。感觉这种鉴权完全可以自己实现,那怎么实现呢,脑子第一印象又是 Spring AOP。
这里就陷入了一种误区,啥事都用 Spring AOP。Spring AOP 的实现会依赖动态代理,无论是使用 JDK 动态代理还是 CGLIB 动态代理,都会有一定的性能开销。但其实在 Web 端很多功能,都是可以避免使用 Spring AOP 减少无意义的性能损耗,比如上面提到的限流和鉴权。
抽象实现
其实原理很简单,就是基于 HandlerInterceptor
来做。但由于类似的功能会很多,比如限流、鉴权、日志打印等,可以将相关功能进行抽象,便于后续类似功能快速实现。
核心抽象类:
package blog.dongguabai.spring.web.mvc.handlerinterceptor.core;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.util.Objects;
/**
* @author dongguabai
* @date 2023-11-19 23:43
*/
public abstract class CustomizedHandlerMethodInterceptor<A extends Annotation> implements HandlerInterceptor {
private final Class<A> annotationType;
protected CustomizedHandlerMethodInterceptor() {
ParameterizedType superclass = (ParameterizedType) getClass().getGenericSuperclass();
this.annotationType = (Class<A>) superclass.getActualTypeArguments()[0];
}
protected abstract boolean preHandle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, A annotation) throws Exception;
protected abstract void afterCompletion(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, A annotation, Exception ex) throws Exception;
protected abstract void postHandle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, ModelAndView modelAndView, A annotation) throws Exception;
@Override
public final boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
A annotation = getAnnotation((HandlerMethod) handler);
if (match(annotation)) {
return preHandle(request, response, (HandlerMethod) handler, annotation);
}
}
return true;
}
@Override
public final void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if (handler instanceof HandlerMethod) {
A annotation = getAnnotation((HandlerMethod) handler);
if (match(annotation)) {
postHandle(request, response, (HandlerMethod) handler, modelAndView, annotation);
}
}
}
@Override
public final void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if (handler instanceof HandlerMethod) {
A annotation = getAnnotation((HandlerMethod) handler);
if (match(annotation)) {
afterCompletion(request, response, (HandlerMethod) handler, annotation, ex);
}
}
}
protected A getAnnotation(HandlerMethod handlerMethod) {
return handlerMethod.getMethodAnnotation(annotationType);
}
protected boolean match(A annotation) {
return Objects.nonNull(annotation);
}
}
接下来其他的业务功能只需要定义注解后,编写拦截器继承 CustomizedHandlerMethodInterceptor
即可。
业务快速实现:鉴权
定义注解:
package blog.dongguabai.spring.web.mvc.handlerinterceptor.require;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Dongguabai
* @description
* @date 2023-11-19 23:31
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RequiresPermissions {
// Permissions
String[] value();
}
拦截器实现:
package blog.dongguabai.spring.web.mvc.handlerinterceptor.require;
import blog.dongguabai.spring.web.mvc.handlerinterceptor.RequestContext;
import blog.dongguabai.spring.web.mvc.handlerinterceptor.core.CustomizedHandlerMethodInterceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
/**
* @author dongguabai
* @date 2023-11-19 23:34
*/
@Component
public class RequiresPermissionsHandlerMethodInterceptor extends CustomizedHandlerMethodInterceptor<RequiresPermissions> {
@Override
protected boolean preHandle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, RequiresPermissions annotation) throws Exception {
List<String> permissons = Arrays.asList(annotation.value());
if (RequestContext.getCurrentUser().getPermissions().stream().anyMatch(permissons::contains)){
return true;
}
System.out.println("无权限.....");
return false;
}
@Override
protected void afterCompletion(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, RequiresPermissions annotation, Exception ex) throws Exception {
}
@Override
protected void postHandle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, ModelAndView modelAndView, RequiresPermissions annotation) throws Exception {
}
}
也就是说标注了 RequiresPermissions
注解的接口都会进行鉴权。
验证一下:
package blog.dongguabai.spring.web.mvc.handlerinterceptor;
import blog.dongguabai.spring.web.mvc.handlerinterceptor.require.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author dongguabai
* @date 2023-11-20 00:17
*/
@RestController
public class TestController {
//只有拥有 BOSS 权限的用户才能调用
@GetMapping("/get-reports")
@RequiresPermissions("BOSS")
public String getReports() {
return "ALL...";
}
}
模拟当前登陆用户(无 BOSS
权限):
package blog.dongguabai.spring.web.mvc.handlerinterceptor;
import java.util.Arrays;
/**
* @author dongguabai
* @date 2023-11-20 01:21
*/
public final class RequestContext {
public static User getCurrentUser() {
User user = new User();
user.setUsername("tom");
user.setPermissions(Arrays.asList("ADMIN", "STUDENT"));
return user;
}
}
调用:
➜ github curl http://localhost:8080/get-reports
{"message":"无权限..."}%
即拦截成功。
业务快速实现:限流
定义注解:
package blog.dongguabai.spring.web.mvc.handlerinterceptor.canyon;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* @author dongguabai
* @date 2023-11-20 01:56
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface Canyon {
double value();
long timeout() default 0;
TimeUnit timeunit() default TimeUnit.SECONDS;
String message() default "系统繁忙,请稍后再试.";
}
实现拦截器:
package blog.dongguabai.spring.web.mvc.handlerinterceptor.canyon;
import blog.dongguabai.spring.web.mvc.handlerinterceptor.RequestContext;
import blog.dongguabai.spring.web.mvc.handlerinterceptor.core.CustomizedHandlerMethodInterceptor;
import blog.dongguabai.spring.web.mvc.handlerinterceptor.require.RequiresPermissions;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* @author dongguabai
* @date 2023-11-19 23:34
*/
@Component
public class CanyonHandlerMethodInterceptor extends CustomizedHandlerMethodInterceptor<Canyon> {
@Override
protected boolean preHandle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, Canyon annotation) throws Exception {
if (tryAcquire()) {
return true;
}
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(String.format("{\"message\":\"%s\"}", annotation.message()));
return false;
}
@Override
protected void afterCompletion(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, Canyon annotation, Exception ex) throws Exception {
}
@Override
protected void postHandle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, ModelAndView modelAndView, Canyon annotation) throws Exception {
}
/**
* todo:流量控制逻辑
*/
private boolean tryAcquire() {
return false;
}
}
验证一下:
package blog.dongguabai.spring.web.mvc.handlerinterceptor;
import blog.dongguabai.spring.web.mvc.handlerinterceptor.canyon.Canyon;
import blog.dongguabai.spring.web.mvc.handlerinterceptor.require.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author dongguabai
* @date 2023-11-20 00:17
*/
@RestController
public class TestController {
@GetMapping("/get-reports")
@RequiresPermissions("BOSS")
public String getReports() {
return "ALL...";
}
@GetMapping("/search")
@RequiresPermissions("ADMIN")
@Canyon(1)
public String search() {
return "search...";
}
}
调用:
➜ github curl http://localhost:8080/search
{"message":"系统繁忙,请稍后再试."}%
即限流成功。
总结
本文首先阐述了虽然 Spring AOP 可以实现限流、鉴权等需要代理的功能,但由于依赖动态代理,会带来一定的性能损耗。然后通过对 HandlerInterceptor
的抽象,我们实现了一套在 Spring Web MVC 层面的静态代理机制,从而方便快速地在 Web 端实现代理功能。
欢迎关注公众号: