springboot 拦截方式

实际项目中,我们经常需要输出请求参数,响应结果,方法耗时,统一的权限校验等。

本文首先为大家介绍 HTTP 请求中三种常见的拦截实现,并且比较一下其中的差异。

(1)基于 Aspect 的拦截方式

(2)基于 HandlerInterceptor 的拦截方式

(3)基于 ResponseBodyAdvice 的拦截方式

推荐阅读:

统一日志框架: https://github.com/houbb/auto-log

springboot 入门案例

为了便于大家学习,我们首先从最基本的 springboot 例子讲起。

maven 引入

引入必须的 jar 包。

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent> <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.10</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.10</version>
</dependency>
</dependencies>
<!-- Package as an executable jar -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

启动类

实现最简单的启动类。

@SpringBootApplication
public class Application { public static void main(String[] args) {
SpringApplication.run(Application.class, args);
} }

定义 Controller

为了演示方便,我们首先实现一个简单的 controller。

@RestController
public class IndexController { @RequestMapping("/index")
public AsyncResp index() {
AsyncResp asyncResp = new AsyncResp();
asyncResp.setResult("ok");
asyncResp.setRespCode("00");
asyncResp.setRespDesc("成功"); System.out.println("IndexController#index:" + asyncResp);
return asyncResp;
} }

其中 AsyncResp 的定义如下:

public class AsyncResp {

    private String respCode;

    private String respDesc;

    private String result;

    // getter & setter & toString()
}

拦截方式定义

基于 Aspect

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component; import java.util.Arrays; /**
*
* @author binbin.hou
* @since 1.0.0
*/
@Aspect
@Component
@EnableAspectJAutoProxy
public class AspectLogInterceptor { /**
* 日志实例
* @since 1.0.0
*/
private static final Logger LOG = LoggerFactory.getLogger(AspectLogInterceptor.class); /**
* 拦截 controller 下所有的 public方法
*/
@Pointcut("execution(public * com.github.houbb.springboot.learn.aspect.controller..*(..))")
public void pointCut() {
//
} /**
* 拦截处理
*
* @param point point 信息
* @return result
* @throws Throwable if any
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
try {
//1. 设置 MDC // 获取当前拦截的方法签名
String signatureShortStr = point.getSignature().toShortString();
//2. 打印入参信息
Object[] args = point.getArgs();
LOG.info("{} 参数: {}", signatureShortStr, Arrays.toString(args)); //3. 打印结果
Object result = point.proceed();
LOG.info("{} 结果: {}", signatureShortStr, result);
return result;
} finally {
// 移除 mdc
}
} }

这种实现的优点是比较通用,可以结合注解实现更加灵活强大的功能。

是个人非常喜欢的一种方式。

主要用途:

(1)日志的出参/入参

(2)统一设置 TraceId

(3)方法的调用耗时统计

基于 HandlerInterceptor

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView; import javax.servlet.DispatcherType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; /**
* @author binbin.hou
* @since 1.0.0
*/
@Component
public class LogHandlerInterceptor implements HandlerInterceptor { private Logger logger = LoggerFactory.getLogger(LogHandlerInterceptor.class); @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 统一的权限校验、路由等
logger.info("LogHandlerInterceptor#preHandle 请求地址:{}", request.getRequestURI()); if (request.getDispatcherType().equals(DispatcherType.ASYNC)) {
return true;
} return true;
} @Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.info("LogHandlerInterceptor#postHandle 调用");
} @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }

然后需要指定对应的 url 和拦截方式之间的关系才会生效:

import com.github.houbb.springboot.learn.aspect.aspect.LogHandlerInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; /**
* spring mvc 配置
* @since 1.0.0
*/
@Configuration
public class SpringMvcConfig extends WebMvcConfigurerAdapter { @Autowired
private LogHandlerInterceptor logHandlerInterceptor; @Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logHandlerInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/version");
super.addInterceptors(registry);
} }

这种方式的优点就是可以根据 url 灵活指定不同的拦截方式。

缺点是主要用于 Controller 层。

基于 ResponseBodyAdvice

此接口有beforeBodyWrite方法,参数body是响应对象response中的响应体,那么我们就可以用此方法来对响应体做一些统一的操作。

比如加密,签名等。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import javax.servlet.http.HttpServletRequest; /**
* @author binbin.hou
* @since 1.0.0
*/
@ControllerAdvice
public class MyResponseBodyAdvice implements ResponseBodyAdvice<Object> { /**
* 日志实例
* @since 1.0.0
*/
private static final Logger LOG = LoggerFactory.getLogger(MyResponseBodyAdvice.class); @Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
//这个地方如果返回false, 不会执行 beforeBodyWrite 方法
return true;
} @Override
public Object beforeBodyWrite(Object resp, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
String uri = serverHttpRequest.getURI().getPath();
LOG.info("MyResponseBodyAdvice#beforeBodyWrite 请求地址:{}", uri); ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) serverHttpRequest;
HttpServletRequest servletRequest = servletServerHttpRequest.getServletRequest(); // 可以做统一的拦截方式处理 // 可以对结果做动态修改等
LOG.info("MyResponseBodyAdvice#beforeBodyWrite 响应结果:{}", resp);
return resp;
} }

测试

我们启动应用,页面访问:

http://localhost:18080/index

页面响应:

{"respCode":"00","respDesc":"成功","result":"ok"}

后端日志:

c.g.h.s.l.a.a.LogHandlerInterceptor      : LogHandlerInterceptor#preHandle 请求地址:/index
c.g.h.s.l.a.aspect.AspectLogInterceptor : IndexController.index() 参数: []
IndexController#index:AsyncResp{respCode='00', respDesc='成功', result='ok'}
c.g.h.s.l.a.aspect.AspectLogInterceptor : IndexController.index() 结果: AsyncResp{respCode='00', respDesc='成功', result='ok'}
c.g.h.s.l.a.aspect.MyResponseBodyAdvice : MyResponseBodyAdvice#beforeBodyWrite 请求地址:/index
c.g.h.s.l.a.aspect.MyResponseBodyAdvice : MyResponseBodyAdvice#beforeBodyWrite 响应结果:AsyncResp{respCode='00', respDesc='成功', result='ok'}
c.g.h.s.l.a.a.LogHandlerInterceptor : LogHandlerInterceptor#postHandle 调用

这里执行的先后顺序也比较明确,此处不再赘述。

异步执行

当然,如果只是上面这些内容,并不是本篇文章的重点。

接下来,我们一起来看下,如果引入了异步执行会怎么样。

定义异步线程池

springboot 中定义异步线程池,非常简单。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; /**
* 请求异步处理配置
*
* @author binbin.hou
*/
@Configuration
@EnableAsync
public class SpringAsyncConfig { @Bean(name = "asyncPoolTaskExecutor")
public AsyncTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(10);
executor.setQueueCapacity(10);
executor.setCorePoolSize(10);
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
} }

异步执行的 Controller

@RestController
public class MyAsyncController extends BaseAsyncController<String> { @Override
protected String process(HttpServletRequest request) {
return "ok";
} @RequestMapping("/async")
public AsyncResp hello(HttpServletRequest request) {
AsyncResp resp = super.execute(request); System.out.println("Controller#async 结果:" + resp);
return resp;
} }

其中 BaseAsyncController 的实现如下:

@RestController
public abstract class BaseAsyncController<T> { protected abstract T process(HttpServletRequest request); @Autowired
private AsyncTaskExecutor taskExecutor; protected AsyncResp execute(HttpServletRequest request) {
// 异步响应结果
AsyncResp resp = new AsyncResp();
try {
taskExecutor.execute(new Runnable() {
@Override
public void run() {
try {
T result = process(request); resp.setRespCode("00");
resp.setRespDesc("成功");
resp.setResult(result.toString()); } catch (Exception exception) {
resp.setRespCode("98");
resp.setRespDesc("任务异常");
}
}
});
} catch (TaskRejectedException e) {
resp.setRespCode("99");
resp.setRespDesc("任务拒绝");
} return resp;
} }

execute 的实现也比较简单:

(1)主线程创建一个 AsyncResp,用于返回。

(2)线程池异步执行具体的子类方法,并且设置对应的值。

思考

接下来,问大家一个问题。

如果我们请求 http://localhost:18080/async,那么:

(1)页面得到的返回值是什么?

(2)Aspect 日志输出的返回值是?

(3)ResponseBodyAdvice 日志输出的返回值是什么?

你可以在这里稍微停一下,记录下你的答案。

测试

我们页面请求 http://localhost:18080/async

页面响应如下:

{"respCode":"00","respDesc":"成功","result":"ok"}

后端的日志:

c.g.h.s.l.a.a.LogHandlerInterceptor      : LogHandlerInterceptor#preHandle 请求地址:/async
c.g.h.s.l.a.aspect.AspectLogInterceptor : MyAsyncController.hello(..) 参数: [org.apache.catalina.connector.RequestFacade@7e931750]
Controller#async 结果:AsyncResp{respCode='null', respDesc='null', result='null'}
c.g.h.s.l.a.aspect.AspectLogInterceptor : MyAsyncController.hello(..) 结果: AsyncResp{respCode='null', respDesc='null', result='null'}
c.g.h.s.l.a.aspect.MyResponseBodyAdvice : MyResponseBodyAdvice#beforeBodyWrite 请求地址:/async
c.g.h.s.l.a.aspect.MyResponseBodyAdvice : MyResponseBodyAdvice#beforeBodyWrite 响应结果:AsyncResp{respCode='00', respDesc='成功', result='ok'}
c.g.h.s.l.a.a.LogHandlerInterceptor : LogHandlerInterceptor#postHandle 调用

对比一下,可以发现我们上面问题的答案:

(1)页面得到的返回值是什么?

{"respCode":"00","respDesc":"成功","result":"ok"}

可以获取到异步执行完成的结果。

(2)Aspect 日志输出的返回值是?

AsyncResp{respCode='null', respDesc='null', result='null'}

无法获取异步结果。

(3)ResponseBodyAdvice 日志输出的返回值是什么?

AsyncResp{respCode='00', respDesc='成功', result='ok'}

可以获取到异步执行完成的结果。

这个看起来有些奇怪,本质上原因是什么呢?又怎么验证呢?

异步执行

原因

本质上,异步执行和 spring 本身的机制关系不大。

只不过是异步执行的方法本身需要时间,拦截方式越靠后,如果异步执行完了,刚好就可以获取到对应的信息而已。

验证方式

如何验证这个猜想呢?

我们在 process 中添加一个 sleep 即可。

代码调整

  • BaseAsyncController.java

execute 中添加一些执行的日志信息,便于查看时间。

taskExecutor.execute(new Runnable() {
@Override
public void run() {
try {
logger.info("AsyncResp#execute 异步开始执行。");
T result = process(request);
resp.setRespCode("00");
resp.setRespDesc("成功");
resp.setResult(result.toString());
logger.info("AsyncResp#execute 异步完成执行。");
} catch (Exception exception) {
resp.setRespCode("98");
resp.setRespDesc("任务异常");
}
}
});
  • MyAsyncController.java

执行时添加沉睡时间。

@Override
protected String process(HttpServletRequest request) {
try {
TimeUnit.SECONDS.sleep(5);
return "ok";
} catch (InterruptedException e) {
return "error";
}
}

测试

页面访问 http://localhost:18080/async

页面返回如下:

{"respCode":null,"respDesc":null,"result":null}

对应的日志如下:

2021-07-10 09:16:08.661  INFO 11008 --- [io-18080-exec-1] c.g.h.s.l.a.a.LogHandlerInterceptor      : LogHandlerInterceptor#preHandle 请求地址:/async
2021-07-10 09:16:08.685 INFO 11008 --- [io-18080-exec-1] c.g.h.s.l.a.aspect.AspectLogInterceptor : MyAsyncController.hello(..) 参数: [org.apache.catalina.connector.RequestFacade@1d491e0]
Controller#async 结果:AsyncResp{respCode='null', respDesc='null', result='null'}
2021-07-10 09:16:08.722 INFO 11008 --- [io-18080-exec-1] c.g.h.s.l.a.aspect.AspectLogInterceptor : MyAsyncController.hello(..) 结果: AsyncResp{respCode='null', respDesc='null', result='null'}
2021-07-10 09:16:08.722 INFO 11008 --- [lTaskExecutor-1] c.g.h.s.l.a.c.BaseAsyncController : AsyncResp#execute 异步开始执行。
2021-07-10 09:16:08.777 INFO 11008 --- [io-18080-exec-1] c.g.h.s.l.a.aspect.MyResponseBodyAdvice : MyResponseBodyAdvice#beforeBodyWrite 请求地址:/async
2021-07-10 09:16:08.777 INFO 11008 --- [io-18080-exec-1] c.g.h.s.l.a.aspect.MyResponseBodyAdvice : MyResponseBodyAdvice#beforeBodyWrite 响应结果:AsyncResp{respCode='null', respDesc='null', result='null'}
2021-07-10 09:16:08.797 INFO 11008 --- [io-18080-exec-1] c.g.h.s.l.a.a.LogHandlerInterceptor : LogHandlerInterceptor#postHandle 调用
2021-07-10 09:16:13.729 INFO 11008 --- [lTaskExecutor-1] c.g.h.s.l.a.c.BaseAsyncController : AsyncResp#execute 异步完成执行。

可以发现 spring 本身依然按照正常的流程执行,因为 process 的执行时间过长,导致三种拦截方式都是无法获取异步内容。

反思

写到这里,自己的收获还是不少。

(1)拦截器的叫法问题

平时会习惯的叫日志拦截器之类的,所以一开始标题使用的是 3 种拦截器,诚然,严谨的说并不能将这些混为一谈。

否则,就如评论区所言,filter 也可以称为拦截器了。

所以将拦截器统一修正为拦截方式。

(2)对知识的理解问题

第一次实现的时候,因为 process 时间太短,让人产生误以为 spring 会有特殊的处理机制。

学习本身还是要严谨一些,所以本文重新做了修正。

小结

希望本文对你有帮助,如果有其他想法的话,也可以评论区和大家分享哦。

各位极客的点赞收藏转发,是老马持续写作的最大动力!

我是老马,期待与你的下次重逢。

springboot 实现拦截的 3 种方式介绍及异步执行的思考的更多相关文章

  1. springboot成神之——mybatis在spring-boot中使用的几种方式

    本文介绍mybatis在spring-boot中使用的几种方式 项目结构 依赖 WebConfig DemoApplication 方式一--@Select User DemoApplication ...

  2. Spark部署三种方式介绍:YARN模式、Standalone模式、HA模式

    参考自:Spark部署三种方式介绍:YARN模式.Standalone模式.HA模式http://www.aboutyun.com/forum.php?mod=viewthread&tid=7 ...

  3. Mysql查看版本号的五种方式介绍

    Mysql查看版本号的五种方式介绍 作者: 字体:[增加 减小] 类型:转载 时间:2013-05-03   一.使用命令行模式进入mysql会看到最开始的提示符;二.命令行中使用status可以看到 ...

  4. SpringBoot整合Servlet的两种方式

    SpringBoot整合Servlet有两种方式: 1.通过注解扫描完成Servlet组件的注册: 2.通过方法完成Servlet组件的注册: 现在简单记录一下两种方式的实现 1.通过注解扫描完成Se ...

  5. android 定位的几种方式介绍

    [地理位置] android 定位的几种方式介绍 开发中对于地图及地理位置的定位是我们经常要用地,地图功能的使用使得我们应用功能更加完善,下面 www.androidkaifa.com 总结了一下网络 ...

  6. MySQL查看版本号的五种方式介绍1111111

    MySQL查看版本号的五种方式介绍 1 命令行模式登录MySQL [root@localhost ~]# mysql -uroot -p Enter password: Welcome to the ...

  7. SpringBoot从入门到精通二(SpringBoot整合myBatis的两种方式)

    前言 通过上一章的学习,我们已经对SpringBoot有简单的入门,接下来我们深入学习一下SpringBoot,我们知道任何一个网站的数据大多数都是动态的,也就是说数据是从数据库提取出来的,而非静态数 ...

  8. 【SpringBoot】05.SpringBoot整合Listener的两种方式

    SpringBoot整合Listener的两种方式: 1.通过注解扫描完成Listener组件的注册 创建一个类实现ServletContextListener (具体实现哪个Listener根据情况 ...

  9. 【SpringBoot】03.SpringBoot整合Servlet的两种方式

    SpringBoot整合Servlet的两种方式: 1. 通过注解扫描完成Servlet组件注册 新建Servlet类继承HttpServlet 重写超类doGet方法 在该类使用注解@WebServ ...

  10. Springboot下载Excel的3种方式

    Springboot下载Excel的3种方式 汇总一下浏览器下载和代码本地下载实现的3种方式. (其实一般都是在代码生成excel,然后上传到oss,然后传链接给前台,但是我好像没有实现过直接点击就能 ...

随机推荐

  1. 搞了个Blazor工具站,域名一次性买了10年!

    大家好,我是沙漠尽头的狼. 在 Dotnet9 上线在线小工具和小游戏后,服务器的压力感觉挺大的,打开25个页面,内存占用170MB左右,CPU保持在60~70%,看来Server真不适合搞这类交互较 ...

  2. [转帖]jmeter SSL证书相关配置

    在实际工作中,我们大多数接口都是用的HTTPS来保证安全,使用jmeter测试HTTPS请求是如何配置证书呢? 1.最简单的方法,在选项里选择SSL管理器,然后选择相应的证书即可 在弹出的选择框选择证 ...

  3. [转帖]关于gdb相关的几个工具的说明

    https://phpor.net/blog/post/846 使用rpm命名查看gdb的rpm包,主要由下面几个程序:/usr/bin/gcore/usr/bin/gdb/usr/bin/gdbse ...

  4. [转帖]CPU的制造和概念

    https://plantegg.github.io/2021/06/01/CPU%E7%9A%84%E5%88%B6%E9%80%A0%E5%92%8C%E6%A6%82%E5%BF%B5/ 为了让 ...

  5. [转帖]MySQL 8.0新特性和性能数据

    https://plantegg.github.io/2022/07/03/MySQL8.0%E7%9A%84%E4%B8%80%E4%BA%9B%E6%95%B0%E6%8D%AE/ MySQL 8 ...

  6. fio test 简单查看一些系统的io性能结果

    简单测试的脚本: echo "本次测试测试128k 16k 8k 1k 的 顺序读写 随机读写性能,每个脚本耗时约30s, 总计耗时大约8min左右完成: " fio -name= ...

  7. 京东云开发者|提高IT运维效率,深度解读京东云AIOps落地实践

    基于深度学习对运维时序指标进行异常检测,快速发现线上业务问题 时间序列的异常检测是实际应用中的一个关键问题,尤其是在 IT 行业.我们没有采用传统的基于阈值的方法来实现异常检测,而是通过深度学习提出了 ...

  8. csv用Excel打开出现乱码

    CSV用Excel打开出现乱码 今天出现一个问题 使用wps打开不会出现乱码.但使用 excel 打开的时候会出现乱码. 其实在我们把文件流转成文件的时候需要在bolb 对象前加上unicode标识, ...

  9. 使用Ant Design Vue的select搜索框出现的问题

    Select 选择器进行搜索 <template> <div> <a-form-item label="分类:"> <a-select p ...

  10. file文件转为base64

    场景描述 在工作中,我们经常需要进行文件上传. 比如在进行图片上传的时候, 我们需要将上传的图片展示出来. 这个时候我们就需要将file文件转化为base64. 将file文件转化为base64 // ...