简介

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. 关于阿里ICON矢量图(SVG)上传问题.

    注意点: 1. 存储为svg格式(建议使用存储为svg,不要使用导出为svg)2. 图像位置:链接(注意哦,不要点嵌入和保留编辑功能)---确定3. AI里面选中图形,点对象-路径-轮廓化描边 软件编 ...

  2. git逻辑和基本命令

    提交和推送的区别 提交(commit):把您做的修改,保存到本地仓库中 推送(push):把您本地仓库的代码推送至服务器(一般是远程服务器及gitlab或github) 拉取和获取的区别 git  p ...

  3. css3_transition: 体验好的过渡效果。附 好看的按钮

    利用css的transition属性详解,上图就是利用transition效果做的一个按钮. transition属性://举例子:transition:all 1s ease;transition: ...

  4. oracle 报错无法从套接字获取更多数据

    报错信息如下: ---查看_optimizer_join_elimination_enabled参数值 切换sys用户 select a.ksppinm name, b.ksppstvl value, ...

  5. C. Neko does Maths

    time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...

  6. Centos7 编译安装 Nginx Mariadb Asp.net Core2 (实测 笔记 Centos 7.3 + Openssl 1.1.0h + Mariadb 10.3.7 + Nginx 1.14.0 + Asp.net. Core 2 )

    环境: 系统硬件:vmware vsphere (CPU:2*4核,内存2G,双网卡) 系统版本:CentOS-7-x86_64-Minimal-1611.iso 安装步骤: 1.准备 1.0 查看硬 ...

  7. thinkphp 单图上传组建成数组然后追加到一个字段

    //上传的数组字段 $note1 = input('note1'); $note2 = input('note2'); $note3 = input('note3'); $note4 = input( ...

  8. 28 ArcMap 运行特别慢怎么办

    小编电脑配置如下: , 虽然不是太好吧,但还是满足ArcMap运行的要求的,但不知道为什么,就是很慢,终于在无意中,发现了一个位置,取消勾选以后,ArcMap变的快很多,亲测有效 取消后台处理后,Ar ...

  9. Python练手例子(16)

    91.时间函数举例1. #!/usr/bin/python #coding=utf-8 import time if __name__ == '__main__': #time.time()返回当前的 ...

  10. 将本地jar包打包到本地仓库和上传到私服

    1.本地jar打包到本地仓库 mvn install:install-file -Dfile=jar包完整地址或相对地址 -DgroupId=自定义的groupID -DartifactId=自定义的 ...