拦截器 & 过滤器

先来说说者的区别:

  • filter 属于 servlet 的概念需要在 web.xml 里定义,不可以使用 Spring 上下文里的资源。

  • interceptor 属于 Spring 的概念,可以使用 Spring 上下文里的资源。

总的来说拦截器和过滤器的用途有以下的几种:

  • Authentication
  • Logging and Auditing
  • Response Data/Image compression
  • Encryption
  • Tokenizing
  • Filters that trigger resource access events
  • XSL/T filters
  • Mime-type chain Filter

拦截器

在 Spring boot 的应用场景之下,interceptor 更适合些,preHandle 将会在 controller 调用前执行,postHandle 将在 controller 之后调用。

下面介绍下在 Spring boot 里如何使用自定义的拦截器;首先新建 interceptor.AuthorizationInterceptor.java 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// interceptor.AuthorizationInterceptor.java
@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("pre " + request.getRequestURI());
        // true 表示继续执行,false 为停止执行并返回。

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        System.out.println("post " + request.getRequestURI());
    }
}

拦截器除了上面两个方法还有一个比较有用的方法可以重载,即 afterCompletion ,在请求完全结束之后被调用。之后新建 config.WebConfig.java 文件将拦截器注册到某些 path 下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// config.WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private AuthorizationInterceptor authorizationInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 这里将会以 /mybatis/** 的匹配模式进行拦截器的添加
        registry.addInterceptor(authorizationInterceptor).addPathPatterns("/mybatis/**");
    }
}

之后访问 root path 为 /mybatis 下的东西的话,拦截器将会被调用。从拦截器 AuthorizationInterceptor 的名字可以看出来这个是打算实现未登录用户的拦截,限于篇幅下面简单的提供个实现思路:

 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
@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Login annotation;
        if(handler instanceof HandlerMethod) {
            // 尽管在注册时候限定了拦截方法,但是细度还是不够可以定义一个 Login 的注解
            // 在拦截的路径下且 HandlerMethod 使用了 Login 的注解才需要验证登录
            annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
        }else{
            return true;
        }

        if(annotation == null){
            return true;
        }

        // 1. 从 request 中获取类 token 的数据
        // 2. 如果是 jwt 可直接判断 token 是否有效
        // 3. 普通 token 可以从尝试数据库或者缓存里获取用户信息进行验证
        // 4. 如果有必要可以验证通过的用户信息同 request.setAttribute 设置到 request 中以备后用

        return true;
    }
}

过滤器

虽说过滤器是 servlet 的概念,但是在 Spring boot 中可以很方便的不修改 web.xml 来完成自定义 filter 的,新建 filter.AuditingFilter.java 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// filter.AuditingFilter.java
@Component
public class AuditingFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException { }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        System.out.println("before " + request.getRequestURI());
        // 调用链
        filterChain.doFilter(servletRequest, servletResponse);
        System.out.println("after " + request.getRequestURI());
    }

    @Override
    public void destroy() { }
}

从 doFilter 的 ServletRequest 参数类型可以看出这是个比较底层的类,需要在方法里转换为 HttpServletRequest 便于使用。接下来还是需要在 config.WebConfig.java 注册该过滤器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Configuration
public class WebConfig implements WebMvcConfigurer {
    // addInterceptors ...

    @Bean
    public FilterRegistrationBean filterRegistrationBean(AuditingFilter auditingFilter ) {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(auditingFilter);

        List<String> urlPatterns = new ArrayList<>();
        // 过滤的路径
        urlPatterns.add("/*");
        filterRegistrationBean.setUrlPatterns(urlPatterns);
        return filterRegistrationBean;
    }
}

简单编写 handler ,之后访问 /mybatis/user/31/ 会发现在控制台里输出:

1
2
3
4
before /mybatis/user/31/
pre /mybatis/user/31/
post /mybatis/user/31/
after /mybatis/user/31/

由此可知,处于 servlet 侧的 filter 是优先于 interceptor 的调用的。

HandlerMethodArgumentResolver

在前面的数据转换和验证可以了解到,可以用自定义的 Formatters 和 WebDataBinder 来完成请求参数的转换和验证,更高阶一点的可以探究为什么 @RequestParam 注解之后之后就可以进行数据的绑定?其奥秘在于 ArgumentResolvers ,名为 RequestParamMethodArgumentResolver 的父类实现了 HandlerMethodArgumentResolver 接口,然后注册到 ArgumentResolvers 即可。

如想要实现从请求头里的 token 里直接获取(从缓存或者session)到用户实体类,然后注入到 controller 里,这样需要用到用户实体类的 controller 就不用千篇一律的在实现里从请求里获取 token 然后根据 token 获取用户实体,然后赋值等一系列的操作。如下先定义自己的注解 annotation.LoginUser.java

1
2
3
4
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}

然后实现一个 resolver.LoginUserHandlerMethodArgumentResolver.java 在里面完成支持参数的筛选和解析方法的定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Autowired
    private UserService userService;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // 注解的参数需要是 UserEntity 类型,且被 LoginUser 注解
        return parameter.getParameterType().isAssignableFrom(UserEntity.class) && parameter.hasParameterAnnotation(LoginUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
                                  NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
        // 从前面的拦截器里获取验证过的 userid,也可自行获取
        Object object = request.getAttribute(AuthorizationInterceptor.USER_KEY, RequestAttributes.SCOPE_REQUEST);
        if(object == null){
            return null;
        }
        // 获取用户实体(数据库或者缓存)
        UserEntity user = userService.selectById((Long)object);
        return user;
    }
}

之后也少不了注册这一步骤:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Configuration
public class WebConfig implements WebMvcConfigurer {
    // addInterceptors ...
    // filterRegistrationBean ...

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
    }
}

之后就可以在 controller 里对参数这样使用:

1
2
3
4
5
6
7
public class UserController {
    @Login
    @GetMapping("userInfo")
    public ResponseEntity<UserEntity> userInfo(@LoginUser UserEntity user){
        return new ResponseEntity<User>(new User());;
    }
}

下面是些常用的 HandlerMethodArgumentResolver,可以自行参考学习其源码。

1
2
3
4
5
6
7
8
9
AbstractCookieValueMethodArgumentResolver
ExpressionValueMethodArgumentResolver
MatrixVariableMethodArgumentResolver
PathVariableMethodArgumentResolver
RequestAttributeMethodArgumentResolver
RequestHeaderMethodArgumentResolver
RequestParamMethodArgumentResolver
ServletCookieValueMethodArgumentResolver
SessionAttributeMethodArgumentResolver

用户验证管理

ACl(Access Control List),访问控制列表。Spring Security 实现了根据用户(可自己扩展)登录状态和角色来实现对请求路径的安全访问,作为一个单独的模块,需要在 maven 中引入 starter:

1
2
3
4
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

用户鉴权还可以参考下面的:

异常捕获

@Controller@ControllerAdvice @RestControllerAdvice 类里面的方法可以使用 @ExceptionHandler 注解去处理抛出的异常。@Controller 是在定义业务 handler 的时候也可以定义几个 exception 的handler ,而 @xxxAdvice 里面的 exception 的 handler 的是通用定义处理,如不定义范围则是全局的异常处理类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/user")
public class MybaitsController {
    @Autowired
    UserService userService;

    // 这里定义接受的 exception 可以是数组的形式
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
    }

    @GetMapping("/{id}")
    public User getUser(@PathVariable int id) {
        if (id < 0) throw new IllegalArgumentException("id must be positive");
        return userService.getUser(id);
    }
}

@ExceptionHandler 注解的方法,可以被自动注入多种数据,如 HandlerMethod, WebRequest, NativeWebRequest, HttpMethod, ServletRequest, ServletResponse 等等,具体参考 exceptionhandler-args,其返回值可以是视图,可以是 ResponseEntity 同一个 controller 是一样的,其返回值将最作为 response。

同样可以做通用的 root exception 处理,新建 execption.RootExceptionHandler

1
2
3
4
5
6
7
@RestControllerAdvice
public class RootExceptionHandler {
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("root:" + e.getMessage());
    }
}

很好理解这里的 RestControllerAdvice 优先级是小于局部 controller 的 @ExceptionHandler,可以自行验证。Advice 的方式可以限定其捕获的范围可有下面的几种方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

从上面的例子可以看到 @ControllerAdvice @RestControllerAdvice 的区别就是其捕获的 controller 类型不同。

在 exception 里除了返回错误信息之外,通常还会做异常的记录,即异常日志。

重载默认异常处理

对于默认的一些异常的处理的重载,更好的方式是继承 ResponseEntityExceptionHandler 然后使用重载的方式,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@RestControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
                                                                  HttpHeaders headers, HttpStatus status,
                                                                  WebRequest request) {
        List<String> details = new ArrayList<>();
        for (FieldError error : ex.getBindingResult().getFieldErrors()) {
            details.add(error.getField() + ": " + error.getDefaultMessage());
        }
        for (ObjectError error : ex.getBindingResult().getGlobalErrors()) {
            details.add(error.getObjectName() + ": " + error.getDefaultMessage());
        }
        BaseResponse error = new BaseResponse("Argument Not Valid");
        error.setDetails(details);
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }
}

其中的 BaseResponse 参考代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class BaseResponse {
    private String msg;
    private int errCode = 0;
    private List<String> details = null;

    public BaseResponse(){}
    public BaseResponse(String msg, int errCode) {
        this.msg = msg;
        this.errCode = errCode;
    }
    public BaseResponse(String msg) {
        this.msg = msg;
    }
    // getter and setter
}

异常映射

通常的,抛出的异常会映射为 response 的 status 里,这是写在 Spring 中的,如果想自定义一个映射,则可以通过 @ResponseStatus 注解将自定义的 exception 或者原有的映射到某个 ResponseStatus 上,当发生异常时即在 response 里设置相应的 status:

1
2
@ResponseStatus(HttpStatus.FORBIDDEN, reason = "FORBIDDEN")
public class ForbiddenException extends RuntimeException {}

@ResponseStatus 还可以修饰 handler 固定其返回的 status:

1
2
3
4
5
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
    // ...
}

日志记录

Spring Boot 在所有内部日志中使用 Commons Logging,但也提供了对常用日志的支持,如:Java Util Logging,Log4J, Log4J2 和 Logback 等。

一般的日志的输出内容格式如下:

1
2
时间日期               日志级别 进程ID 分隔符      线程名       Logger名(源代码的类名)                    日志内容
2016-04-13 08:23:50.120  INFO 37397    --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.11.Final}

debug 模式可通过运行时通过 --debug 传入,或者配置文件 debug=true,日志的输出级别可以由 logging.level.*=LEVEL 配置指定,后面的通配符 * 也可以替换为包名,生成多个配置,级别从低到高为 TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF。

默认的日志会输出到控制台中,但是通过配置 logging.file=my.loglogging.path=/var/log 两项可以达到输出到文件中的目的,日志文件在 10Mb 大小的时候被截断,产生新的日志文件。输出的日志格式也可以通过配置 logging.pattern.consolelogging.pattern.file 来定义。

配置之后的 logger 在程序中可以这样的单独使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@RestControllerAdvice
public class RootExceptionHandler {
    // 以类名作为 logger 名
    Logger logger = Logger.getLogger(getClass().getName());

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) {
        logger.info(e.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
    }
}

通常还会建议使用 SLF4J 来作为日志的隔离层,比 java.util 的 logger 功能更健全些:

1
2
3
4
5
6
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// ...
Logger logger = LoggerFactory.getLogger(getClass());
// SLF4J 比起拼接占位符效率更高
logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);

方法监测

可以利用切面方便的为 controller 等加上日志切面,新建一个 annotation.SysLog.java 文件,存放用于表示的注解:

1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
    String value() 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
27
28
29
30
31
@Aspect
@Component
public class SysLogAspect {

    Logger logger = Logger.getLogger(getClass().getName());

    @Pointcut("@annotation(xxx.annotation.SysLog)")
    public void logPointCut() { }

    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        long beginTime = System.currentTimeMillis();

        //执行方法
        Object result = point.proceed();

        // 保存日志
        long time = System.currentTimeMillis() - beginTime;
        saveSysLog(point, time);
        return result;
    }

    private void saveSysLog(ProceedingJoinPoint joinPoint, long time) {
        // ...
        SysLog syslog = method.getAnnotation(SysLog.class);
        if(syslog != null){
            // logger ...
        }
        // ...
    }
}

利用如 @SysLog("测试") 的方式注解就可。

微服务

可以使用下面的工具进行 Http 接口微服务的调用:

  • RestTemplate
  • WebClient
  • Feign

参考