简介
AOP(面向切面编程)常用于解决系统中的一些耦合问题,是一种编程的模式
通过将一些通用逻辑抽取为公共模块,由容器来进行调用,以达到模块间隔离的效果。
其还有一个别名,叫面向关注点编程,把系统中的核心业务逻辑称为核心关注点,而一些通用的非核心逻辑划分为横切关注点
AOP常用于…
日志记录
你需要为你的Web应用程序实现访问日志记录,却又不想在所有接口中一个个进行打点。
安全控制
为URL 实现访问权限控制,自动拦截一些非法访问。
事务
某些业务流程需要在一个事务中串行
异常处理
系统发生处理异常,根据不同的异常返回定制的消息体。
在笔者刚开始接触编程之时,AOP还是个新事物,当时曾认为AOP会大行其道。
果不其然,目前流行的Spring 框架中,AOP已经成为其关键的核心能力。
接下来,我们要看看在SpringBoot 框架中,怎么实现常用的一些拦截操作。
先看看下面的一个Controller方法:
示例
@RestController
@RequestMapping("/intercept")
public class InterceptController {
@PostMapping(value = "/body", consumes = { MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_UTF8_VALUE })
public String body(@RequestBody MsgBody msg) {
return msg == null ? "" : msg.getContent();
}
public static class MsgBody {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
在上述代码的 body 方法中,会接受一个MsgBody请求消息体,最终简单的输出content字段。
下面,我们将介绍如何为这个方法实现拦截动作。算起来,共有五种姿势。
姿势一、使用 Filter 接口
Filter 接口由 J2EE 定义,在Servlet执行之前由容器进行调用。
而SpringBoot中声明 Filter 又有两种方式:
1. 注册 FilterRegistrationBean
声明一个FilterRegistrationBean 实例,对Filter 做一系列定义,如下:
@Bean
public FilterRegistrationBean customerFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
// 设置过滤器
registration.setFilter(new CustomerFilter());
// 拦截路由规则
registration.addUrlPatterns("/intercept
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
HandlerMethod method = (HandlerMethod) handler;
logger.info("CustomerHandlerInterceptor preHandle, {}", method.getMethod().getName());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
HandlerMethod method = (HandlerMethod) handler;
logger.info("CustomerHandlerInterceptor postHandle, {}", method.getMethod().getName());
response.getOutputStream().write("append content".getBytes());
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
HandlerMethod method = (HandlerMethod) handler;
logger.info("CustomerHandlerInterceptor afterCompletion, {}", method.getMethod().getName());
}
}
除了上面的代码实现,还不要忘了将 Interceptor 实现进行注册:
@Configuration
public class InterceptConfig extends WebMvcConfigurerAdapter {
// 注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CustomHandlerInterceptor()).addPathPatterns("/intercept
@ExceptionHandler(value = { Exception.class })
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public String handle(Exception e, HandlerMethod m) {
logger.info("CustomInterceptAdvice handle exception {}, method: {}", e.getMessage(), m.getMethod().getName());
return e.getMessage();
}
}
需要注意的是,@ExceptionHandler 需要与 @ControllerAdvice配合使用
其中 @ControllerAdvice的 assignableTypes 属性指定了所拦截类的名称。
除此之外,该注解还支持指定包扫描范围、注解范围等等。
推荐指数
5颗星,@ExceptionHandler 使用非常方便,在异常处理的机制上是首选;
目前也是SpringBoot 框架最为推荐使用的方法。
姿势四、RequestBodyAdvice/ResponseBodyAdvice
RequestBodyAdvice、ResponseBodyAdvice 相对于读者可能比较陌生,
而这俩接口也是 Spring 4.x 才开始出现的。
RequestBodyAdvice 的用法
我们都知道,SpringBoot 中可以利用**@RequestBody这样的注解完成请求内容体与对象的转换。
而RequestBodyAdvice 则可用于在请求内容对象转换的前后时刻**进行拦截处理,其定义了几个方法:
| 方法 | 说明 |
|---|---|
| supports | 判断是否支持 |
| handleEmptyBody | 当请求体为空时调用 |
| beforeBodyRead | 在请求体未读取(转换)时调用 |
| afterBodyRead | 在请求体完成读取后调用 |
实现代码如下:
@ControllerAdvice(assignableTypes = InterceptController.class)
public class CustomRequestAdvice extends RequestBodyAdviceAdapter {
private static final Logger logger = LoggerFactory.getLogger(CustomRequestAdvice.class);
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class extends HttpMessageConverter>> converterType) {
// 返回true,表示启动拦截
return MsgBody.class.getTypeName().equals(targetType.getTypeName());
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class extends HttpMessageConverter>> converterType) {
logger.info("CustomRequestAdvice handleEmptyBody");
// 对于空请求体,返回对象
return body;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class extends HttpMessageConverter>> converterType) throws IOException {
logger.info("CustomRequestAdvice beforeBodyRead");
// 可定制消息序列化
return new BodyInputMessage(inputMessage);
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class extends HttpMessageConverter>> converterType) {
logger.info("CustomRequestAdvice afterBodyRead");
// 可针对读取后的对象做转换,此处不做处理
return body;
}
上述代码实现中,针对前面提到的 MsgBody对象类型进行了拦截处理。
在beforeBodyRead 中,返回一个BodyInputMessage对象,而这个对象便负责源数据流解析转换
public static class BodyInputMessage implements HttpInputMessage {
private HttpHeaders headers;
private InputStream body;
public BodyInputMessage(HttpInputMessage inputMessage) throws IOException {
this.headers = inputMessage.getHeaders();
// 读取原字符串
String content = IOUtils.toString(inputMessage.getBody(), "UTF-8");
MsgBody msg = new MsgBody();
msg.setContent(content);
this.body = new ByteArrayInputStream(JsonUtil.toJson(msg).getBytes());
}
@Override
public InputStream getBody() throws IOException {
return body;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}
代码说明
完成数据流的转换,包括以下步骤:
- 获取请求内容字符串;
- 构建 MsgBody 对象,将内容字符串作为其 content 字段;
- 将 MsgBody 对象 Json 序列化,再次转成字节流供后续环节使用。
ResponseBodyAdvice 用法
ResponseBodyAdvice 的用途在于对返回内容做拦截处理,如下面的示例:
@ControllerAdvice(assignableTypes = InterceptController.class)
public static class CustomResponseAdvice implements ResponseBodyAdvice {
private static final Logger logger = LoggerFactory.getLogger(CustomRequestAdvice.class);
@Override
public boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> converterType) {
// 返回true,表示启动拦截
return true;
}
@Override
public String beforeBodyWrite(String body, MethodParameter returnType, MediaType selectedContentType,
Class extends HttpMessageConverter>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
logger.info("CustomResponseAdvice beforeBodyWrite");
// 添加前缀
String raw = String.valueOf(body);
return "PREFIX:" + raw;
}
}
看,还是容易理解的,我们在返回的字符串中添加了一个前缀!
推荐指数
2 颗星,这是两个非常冷门的接口,目前的使用场景也相对有限;
一般在需要对输入输出流进行特殊处理(比如加解密)的场景下使用。
姿势五、@Aspect 注解
这是目前最灵活的做法,直接利用注解可实现任意对象、方法的拦截。
在某个Bean的类上面** @Aspect** 注解便可以将一个Bean 声明为具有AOP能力的对象。
@Aspect
@Component
public class InterceptControllerAspect {
private static final Logger logger = LoggerFactory.getLogger(InterceptControllerAspect.class);
@Pointcut("target(org.zales.dmo.boot.controllers.InterceptController)")
public void interceptController() {
}
@Around("interceptController()")
public Object handle(ProceedingJoinPoint joinPoint) throws Throwable {
logger.info("aspect before.");
try {
return joinPoint.proceed();
} finally {
logger.info("aspect after.");
}
}
}
简单说明
@Pointcut 用于定义切面点,而使用target关键字可以定位到具体的类。
@Around 定义了一个切面处理方法,通过注入ProceedingJoinPoint对象达到控制的目的。
一些常用的切面注解:
| 注解 | 说明 |
|---|---|
| @Before | 方法执行之前 |
| @After | 方法执行之后 |
| @Around | 方法执行前后 |
| @AfterThrowing | 抛出异常后 |
| @AfterReturing | 正常返回后 |
深入一点
aop的能力来自于spring-boot-starter-aop,进一步依赖于aspectjweaver组件。
有兴趣可以进一步了解。
推荐指数
5颗星,**aspectj **与 SpringBoot 可以无缝集成,这是一个经典的AOP框架,
可以实现任何你想要的功能,笔者之前曾在多个项目中使用,效果是十分不错的。
注解的支持及自动包扫描大大简化了开发,然而,你仍然需要先对 Pointcut 的定义有充分的了解。
思考
到这里,读者可能想知道,这些实现拦截器的接口之间有什么关系呢?
答案是,没有什么关系! 每一种接口都会在不同的时机被调用,我们基于上面的代码示例做了日志输出:
- Filter customFilter handle before - Filter annotateFilter handle before - CustomerHandlerInterceptor preHandle, body - CustomRequestAdvice beforeBodyRead - CustomRequestAdvice afterBodyRead - aspect before. - aspect after. - CustomResponseAdvice beforeBodyWrite - CustomerHandlerInterceptor postHandle, body - CustomerHandlerInterceptor afterCompletion, body - Filter annotateFilter handle after - Filter customFilter handle after
可以看到,各种拦截器接口的执行顺序如下图:
小结
AOP 是实现拦截器的基本思路,本文介绍了SpringBoot 项目中实现拦截功能的五种常用姿势。
对于每一种方法都给出了真实的代码样例,读者可以根据需要选择自己适用的方案。
最后,欢迎继续关注"美码师的补习系列-springboot篇" ,期待更多精彩内容-



