简介

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 ? "<EMPTY>" : 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/*"); // 设置初始化参数
registration.addInitParameter("name", "customFilter"); registration.setName("CustomerFilter");
registration.setOrder(1);
return registration;
}

其中 CustomerFilter 实现了Filter接口,如下:

public class CustomerFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(CustomerFilter.class);
private String name; @Override
public void init(FilterConfig filterConfig) throws ServletException {
name = filterConfig.getInitParameter("name");
} @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("Filter {} handle before", name);
chain.doFilter(request, response);
logger.info("Filter {} handle after", name);
}
}

2. @WebFilter 注解

为Filter的实现类添加 @WebFilter注解,由SpringBoot 框架扫描后注入

@WebFilter的启用需要配合@ServletComponentScan才能生效

@Component
@ServletComponentScan
@WebFilter(urlPatterns = "/intercept/*", filterName = "annotateFilter")
public class AnnotateFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(AnnotateFilter.class);
private final String name = "annotateFilter"; @Override
public void init(FilterConfig filterConfig) throws ServletException {
} @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("Filter {} handle before", name);
chain.doFilter(request, response);
logger.info("Filter {} handle after", name);
}
}

使用注解是最简单的,但其缺点是仍然无法支持 order属性(用于控制Filter的排序)。

而通常的@Order注解只能用于定义Bean的加载顺序,却真正无法控制Filter排序。

这是一个已知问题,参考这里

推荐指数

3 颗星,Filter 定义属于J2EE规范,由Servlet容器调度执行。

由于独立于框架之外,无法使用 Spring 框架的便捷特性,

目前一些第三方组件集成时会使用该方式。

姿势二、HanlderInterceptor

HandlerInterceptor 用于拦截 Controller 方法的执行,其声明了几个方法:

方法 说明
preHandle Controller方法执行前调用
postHandle Controller方法后,视图渲染前调用
afterCompletion 整个方法执行后(包括异常抛出捕获)

基于 HandlerInterceptor接口 实现的样例:

public class CustomHandlerInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(CustomHandlerInterceptor.class);

    /*
* Controller方法调用前,返回true表示继续处理
*/
@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;
} /*
* Controller方法调用后,视图渲染前
*/
@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());
} /*
* 整个请求处理完,视图已渲染。如果存在异常则Exception不为空
*/
@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/**");
super.addInterceptors(registry);
}

推荐指数

4颗星,HandlerInterceptor 来自SpringMVC框架,基本可代替 Filter 接口使用;

除了可以方便的进行异常处理之外,通过接口参数能获得Controller方法实例,还可以实现更灵活的定制。

姿势三、@ExceptionHandler 注解

@ExceptionHandler 的用途是捕获方法执行时抛出的异常,

通常可用于捕获全局异常,并输出自定义的结果。

如下面的实例:

@ControllerAdvice(assignableTypes = InterceptController.class)
public class CustomInterceptAdvice { private static final Logger logger = LoggerFactory.getLogger(CustomInterceptAdvice.class); /**
* 拦截异常
*
* @param e
* @param m
* @return
*/
@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;
}
}

代码说明

完成数据流的转换,包括以下步骤:

  1. 获取请求内容字符串;
  2. 构建 MsgBody 对象,将内容字符串作为其 content 字段;
  3. 将 MsgBody 对象 Json 序列化,再次转成字节流供后续环节使用。

ResponseBodyAdvice 用法

ResponseBodyAdvice 的用途在于对返回内容做拦截处理,如下面的示例:

    @ControllerAdvice(assignableTypes = InterceptController.class)
public static class CustomResponseAdvice implements ResponseBodyAdvice<String> { 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篇" ,期待更多精彩内容-

补习系列(7)-springboot 实现拦截的五种姿势的更多相关文章

  1. 补习系列(15)-springboot 分布式会话原理

    目录 一.背景 二.SpringBoot 分布式会话 三.样例程序 四.原理进阶 A. 序列化 B. 会话代理 C. 数据老化 小结 一.背景 在 补习系列(3)-springboot 几种scope ...

  2. 补习系列(14)-springboot redis 整合-数据读写

    目录 一.简介 二.SpringBoot Redis 读写 A. 引入 spring-data-redis B. 序列化 C. 读写样例 三.方法级缓存 四.连接池 小结 一.简介 在 补习系列(A3 ...

  3. 顽石系列:CSS实现垂直居中的五种方法

    顽石系列:CSS实现垂直居中的五种方法 在开发过程中,我们可能沿用或者试探性地去使用某种方法实现元素居中,但是对各种居中方法的以及使用场景很不清晰.参考的内容链接大概如下: 行内元素:https:// ...

  4. 补习系列(4)-springboot 参数校验详解

    目录 目标 一.PathVariable 校验 二.方法参数校验 三.表单对象校验 四.RequestBody 校验 五.自定义校验规则 六.异常拦截器 参考文档 目标 对于几种常见的入参方式,了解如 ...

  5. 补习系列(1)-springboot项目基础搭建课

    目录 前言 一.基础结构 二.添加代码 三.应用配置 四.日志配置 五.打包部署 小结 前言 springboot 最近火的不行,目前几乎已经是 spring 家族最耀眼的项目了.抛开微服务.技术社区 ...

  6. 补习系列(8)-springboot 单元测试之道

    目录 目标 一.About 单元测试 二.About Junit 三.SpringBoot-单元测试 项目依赖 测试样例 四.Mock测试 五.最后 目标 了解 单元测试的背景 了解如何 利用 spr ...

  7. 补习系列(6)- springboot 整合 shiro 一指禅

    目标 了解ApacheShiro是什么,能做什么: 通过QuickStart 代码领会 Shiro的关键概念: 能基于SpringBoot 整合Shiro 实现URL安全访问: 掌握基于注解的方法,以 ...

  8. 补习系列(3)-springboot中的几种scope

    目标 了解HTTP 请求/响应头及常见的属性: 了解如何使用SpringBoot处理头信息 : 了解如何使用SpringBoot处理Cookie : 学会如何对 Session 进行读写: 了解如何在 ...

  9. 补习系列(2)-springboot mime类型处理

    目标 了解http常见的mime类型定义: 如何使用springboot 处理json请求及响应: 如何使用springboot 处理 xml请求及响应: http参数的获取及文件上传下载: 如何获得 ...

随机推荐

  1. jQuery(三)

    jquery链式调用 jquery对象的方法会在执行完后返回这个jquery对象,所有jquery对象的方法可以连起来写: $('#div1') // id为div1的元素 .children('ul ...

  2. 洛谷p3801:红色的幻想乡

    初见完全没有思路.....感觉像是线段树 但二维感觉完全不可做嘛 于是只能去看了看题解 然而还是疯狂爆零+WA.. 和yycc神犇调了两三个小时才调出来... ——————以下个人理解 考虑到每次的修 ...

  3. VB读写进程的内存

    在窗体部分简单测试了ReadProcessMemory和WriteProcessMemory对另一个程序进程的读写. 由于临时项目变动,又不需要了,所以直接封类,删工程.以下代码没有一个函数经过测试, ...

  4. Recycle移动端界面设计成果图

    经过功能分析,我最终设计出来了该App界面图: (1)主页面图 (2)消息界面图 (3)我的界面图 (4)垃圾页面图 由于时间原因,此次设计仅为初稿.以后会继续抽出时间,与团队成员一起完善该项目App ...

  5. 自然语言处理(四)统计机器翻译SMT

    1.统计机器翻译三要素 1.翻译模型 2.语言模型 3.排序模型 2.翻译流程 1.双语数据预处理 2.词对齐 3.构造短语翻译表 4.对短语翻译表进行概率估计 5.解码,beam search 6. ...

  6. Bypass_Disable_functions_Shell

    Bypass_Disable_functions_Shell https://github.com/l3m0n/Bypass_Disable_functions_Shell 一个各种方式突破Disab ...

  7. 大数相加 Big Num

    代码: #include<stdio.h>#include<algorithm>#include<iostream>#include<string.h> ...

  8. windows 10 64位机器上 安装部署

    mi这个博客写的不错 https://www.cnblogs.com/dingguofeng/p/8709476.html 安装redis 可视化工具后 ,新建连接 名称随意,注意端口号是否有误默认6 ...

  9. 操作系统中 heap 和 stack 的区别

    操作系统中 heap 和 stack 的区别heap 和 stack是什么堆栈是两种数据结构.堆栈都是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除.==在单片机 ...

  10. [Swift]LeetCode261.图验证树 $ Graph Valid Tree

    Given n nodes labeled from 0 to n - 1 and a list of undirected edges (each edge is a pair of nodes), ...