拦截器 & 过滤器
先来说说者的区别:
总的来说拦截器和过滤器的用途有以下的几种:
- 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.log
和 logging.path=/var/log
两项可以达到输出到文件中的目的,日志文件在 10Mb 大小的时候被截断,产生新的日志文件。输出的日志格式也可以通过配置 logging.pattern.console
和 logging.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
参考