• 每天学习一点点之 Spring Web MVC 之抽象 HandlerInterceptor 实现常用功能(限流、权限等)


    背景

    这里介绍一下本文的背景(废话,可跳过)。上周有个我们服务的调用方反馈某个接口调用失败率很高,排查了一下,发现是因为这个接口被我之前写的一个限流器给拦截了,随着我们的服务接入了 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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    接下来其他的业务功能只需要定义注解后,编写拦截器继承 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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    拦截器实现:

    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 {
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    也就是说标注了 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...";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    模拟当前登陆用户(无 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    调用:

    ➜  github curl http://localhost:8080/get-reports
    {"message":"无权限..."}%  
    
    • 1
    • 2

    即拦截成功。

    业务快速实现:限流

    定义注解:

    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 "系统繁忙,请稍后再试.";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    实现拦截器:

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    验证一下:

    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...";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    调用:

    ➜  github curl http://localhost:8080/search     
    {"message":"系统繁忙,请稍后再试."}%  
    
    • 1
    • 2

    即限流成功。

    总结

    本文首先阐述了虽然 Spring AOP 可以实现限流、鉴权等需要代理的功能,但由于依赖动态代理,会带来一定的性能损耗。然后通过对 HandlerInterceptor 的抽象,我们实现了一套在 Spring Web MVC 层面的静态代理机制,从而方便快速地在 Web 端实现代理功能。

    欢迎关注公众号:
    在这里插入图片描述

  • 相关阅读:
    Qt QSplitter拆分器
    从不同视角理解架构
    使用 ZipArchiveInputStream 读取压缩包内文件总数
    spark运行报错
    Qt开发学习笔记02
    基于SpringBoot的个人博客系统设计与实现
    深度学习-卷积神经网络
    使用 v-for 指令和数组来实现在 Uni-app 中动态增减表单项并渲染多个数据
    Visual Studio Code 自动编译 TypeScript
    Linux20 -- 线程安全、保证线程安全的示例代码
  • 原文地址:https://blog.csdn.net/Dongguabai/article/details/134520661