补习系列(7)-springboot 实现拦截的五种姿势
目录
- 简介
- 姿势一、使用 Filter 接口
- 1. 注册 FilterRegistrationBean
- 2. @WebFilter 注解
- 姿势二、HanlderInterceptor
- 姿势三、@ExceptionHandler 注解
- 姿势四、RequestBodyAdvice/ResponseBodyAdvice
- RequestBodyAdvice 的用法
- ResponseBodyAdvice 用法
- 姿势五、@Aspect 注解
- 思考
- 小结
简介
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 做一系列定义,如下:
@Beanpublic 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;@Overridepublic void init(FilterConfig filterConfig) throws ServletException {name = filterConfig.getInitParameter("name");}@Overridepublic 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";@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic 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表示继续处理*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {HandlerMethod method = (HandlerMethod) handler;logger.info("CustomerHandlerInterceptor preHandle, {}", method.getMethod().getName());return true;}/** Controller方法调用后,视图渲染前*/@Overridepublic 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不为空*/@Overridepublic 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 {// 注册拦截器@Overridepublic 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)@ResponseBodypublic 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);@Overridepublic boolean supports(MethodParameter methodParameter, Type targetType,Class<? extends HttpMessageConverter<?>> converterType) {// 返回true,表示启动拦截return MsgBody.class.getTypeName().equals(targetType.getTypeName());}@Overridepublic Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {logger.info("CustomRequestAdvice handleEmptyBody");// 对于空请求体,返回对象return body;}@Overridepublic HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,Class<? extends HttpMessageConverter<?>> converterType) throws IOException {logger.info("CustomRequestAdvice beforeBodyRead");// 可定制消息序列化return new BodyInputMessage(inputMessage);}@Overridepublic 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());}@Overridepublic InputStream getBody() throws IOException {return body;}@Overridepublic HttpHeaders getHeaders() {return headers;}}
代码说明
完成数据流的转换,包括以下步骤:
- 获取请求内容字符串;
- 构建 MsgBody 对象,将内容字符串作为其 content 字段;
- 将 MsgBody 对象 Json 序列化,再次转成字节流供后续环节使用。
ResponseBodyAdvice 用法
ResponseBodyAdvice 的用途在于对返回内容做拦截处理,如下面的示例:
@ControllerAdvice(assignableTypes = InterceptController.class)public static class CustomResponseAdvice implements ResponseBodyAdvice<String> {private static final Logger logger = LoggerFactory.getLogger(CustomRequestAdvice.class);@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {// 返回true,表示启动拦截return true;}@Overridepublic 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 实现拦截的五种姿势相关推荐
- 补习系列(15)-springboot 分布式会话原理
目录 一.背景 二.SpringBoot 分布式会话 三.样例程序 四.原理进阶 A. 序列化 B. 会话代理 C. 数据老化 小结 一.背景 在 补习系列(3)-springboot 几种scope ...
- 补习系列(14)-springboot redis 整合-数据读写
目录 一.简介 二.SpringBoot Redis 读写 A. 引入 spring-data-redis B. 序列化 C. 读写样例 三.方法级缓存 四.连接池 小结 一.简介 在 补习系列(A3 ...
- 补习系列(4)-springboot 参数校验详解
目录 目标 一.PathVariable 校验 二.方法参数校验 三.表单对象校验 四.RequestBody 校验 五.自定义校验规则 六.异常拦截器 参考文档 目标 对于几种常见的入参方式,了解如 ...
- 补习系列(8)-springboot 单元测试之道
目录 目标 一.About 单元测试 二.About Junit 三.SpringBoot-单元测试 项目依赖 测试样例 四.Mock测试 五.最后 目标 了解 单元测试的背景 了解如何 利用 spr ...
- 补习系列(6)- springboot 整合 shiro 一指禅
欢迎添加华为云小助手微信(微信号:HWCloud002 或 HWCloud003),输入关键字"加群",加入华为云线上技术讨论群:输入关键字"最新活动",获取华 ...
- 补习系列(3)-springboot中的几种scope
目标 了解HTTP 请求/响应头及常见的属性: 了解如何使用SpringBoot处理头信息 : 了解如何使用SpringBoot处理Cookie : 学会如何对 Session 进行读写: 了解如何在 ...
- 补习系列(2)-springboot mime类型处理
目标 了解http常见的mime类型定义: 如何使用springboot 处理json请求及响应: 如何使用springboot 处理 xml请求及响应: http参数的获取及文件上传下载: 如何获得 ...
- 补习系列(1)-springboot项目基础搭建课
目录 前言 一.基础结构 二.添加代码 三.应用配置 四.日志配置 五.打包部署 小结 前言 springboot 最近火的不行,目前几乎已经是 spring 家族最耀眼的项目了.抛开微服务.技术社区 ...
- 补习系列(3)-springboot 中的几种scope
目标 了解HTTP 请求/响应头及常见的属性: 了解如何使用SpringBoot处理头信息 : 了解如何使用SpringBoot处理Cookie : 学会如何对 Session 进行读写: 了解如何在 ...
最新文章
- Nutch插件开发及发布流程
- ubuntu下解决oracle sqlplus不能查看历史命令问题
- python中os.path.join()的循环用法_Python中.join()和os.path.join()两个函数的用法详解
- 物联网核心协议—消息推送技术演进
- IB component change - CL_IBCOMPONENT_IL~CHANGE_COMPONENT
- python set_Python Set联合
- 1,机器学习应用概述
- C#学习网站资源一览
- Android Studio3.0,在原有项目中进行ndk配置
- Android开根号运算
- 数据分析师的工作职责是什么?
- 牛牛游戏牛型判断算法实现
- matlab ext2int,PF_MATLAB_new 一个非常不错的粒子滤波工具箱,基于面向对象的思 实 实现非线性 ,包 238万源代码下载- www.pudn.com...
- 人民币对美元汇率中间价报6.7542元 上调18个基点
- 华为天才少年火了!刚毕业就拿201万年薪,全球仅4人!又是这个学校的
- 海湾crt调试_海湾设备调试步骤
- DNA甲基化测序方法介绍
- 万维网,互联网,因特网之间的区别
- Google Earth Engine APP——UI地图加载一个高程显示标签并显示高程案例
- Hikaril过一段时间就出错,Possibly consider using a shorter maxLifetime value问题
热门文章
- ajax绑定事件页面重复提交,Ajax局部更新导致JS事件重复触发问题的解决方法
- 位运算符取反_Java常见的运算符——位运算
- mysql innodb和myisam区别_MySQL?存储引擎简介
- android activity从新打开,【Android开发-8】生命周期,Activity中打开另一个Activity
- html字体代码大全_HTML基础笔记(一)
- deepnude | 福利
- NeHe OpenGL教程 第四课:旋转
- 文件下载及web文件的contentType类型大全
- LOADRUNNER连接ORACLE数据库的方法
- Linux Socket学习--为套接口绑定地址