每天学习一点点之 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);
    }

}

接下来其他的业务功能只需要定义注解后,编写拦截器继承 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 端实现代理功能。

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

你可能感兴趣的:(spring,mvc,学习,spring,前端)