文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

springboot 实现拦截器的 3 种方式介绍及异步执行的思考

2024-12-03 01:48

关注

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

(1)基于 Aspect 的拦截器

(2)基于 HandlerInterceptor 的拦截器

(3)基于 ResponseBodyAdvice 的拦截器


springboot 入门案例

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

maven 引入

引入必须的 jar 包。

  1.  
  2.     org.springframework.boot 
  3.     spring-boot-starter-parent 
  4.     1.5.9.RELEASE 
  5.  
  6.  
  7.  
  8.      
  9.         org.springframework.boot 
  10.         spring-boot-starter-web 
  11.      
  12.      
  13.         org.aspectj 
  14.         aspectjrt 
  15.         1.8.10 
  16.      
  17.      
  18.         org.aspectj 
  19.         aspectjweaver 
  20.         1.8.10 
  21.      
  22.  
  23. -- Package as an executable jar --> 
  24.  
  25.      
  26.          
  27.             org.springframework.boot 
  28.             spring-boot-maven-plugin 
  29.          
  30.      
  31.  

启动类

实现最简单的启动类。

  1. @SpringBootApplication 
  2. public class Application { 
  3.  
  4.     public static void main(String[] args) { 
  5.         SpringApplication.run(Application.class, args); 
  6.     } 
  7.  

定义 Controller

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

  1. @RestController 
  2. public class IndexController { 
  3.  
  4.     @RequestMapping("/index"
  5.     public AsyncResp index() { 
  6.         AsyncResp asyncResp = new AsyncResp(); 
  7.         asyncResp.setResult("ok"); 
  8.         asyncResp.setRespCode("00"); 
  9.         asyncResp.setRespDesc("成功"); 
  10.  
  11.         System.out.println("IndexController#index:" + asyncResp); 
  12.         return asyncResp; 
  13.     } 
  14.  

其中 AsyncResp 的定义如下:

  1. public class AsyncResp { 
  2.  
  3.     private String respCode; 
  4.  
  5.     private String respDesc; 
  6.  
  7.     private String result; 
  8.  
  9.  
  10.     // getter & setter & toString() 

拦截器定义

基于 Aspect

  1. import org.aspectj.lang.ProceedingJoinPoint; 
  2. import org.aspectj.lang.annotation.Around; 
  3. import org.aspectj.lang.annotation.Aspect; 
  4. import org.aspectj.lang.annotation.Pointcut; 
  5. import org.slf4j.Logger; 
  6. import org.slf4j.LoggerFactory; 
  7. import org.springframework.context.annotation.EnableAspectJAutoProxy; 
  8. import org.springframework.stereotype.Component; 
  9.  
  10. import java.util.Arrays; 
  11.  
  12.  
  13. @Aspect 
  14. @Component 
  15. @EnableAspectJAutoProxy 
  16. public class AspectLogInterceptor { 
  17.  
  18.      
  19.     private static final Logger LOG = LoggerFactory.getLogger(AspectLogInterceptor.class); 
  20.  
  21.      
  22.     @Pointcut("execution(public * com.github.houbb.springboot.learn.aspect.controller..*(..))"
  23.     public void pointCut() { 
  24.         // 
  25.     } 
  26.  
  27.      
  28.     @Around("pointCut()"
  29.     public Object around(ProceedingJoinPoint point) throws Throwable { 
  30.         try { 
  31.             //1. 设置 MDC 
  32.  
  33.             // 获取当前拦截的方法签名 
  34.             String signatureShortStr = point.getSignature().toShortString(); 
  35.             //2. 打印入参信息 
  36.             Object[] args = point.getArgs(); 
  37.             LOG.info("{} 参数: {}", signatureShortStr, Arrays.toString(args)); 
  38.  
  39.             //3. 打印结果 
  40.             Object result = point.proceed(); 
  41.             LOG.info("{} 结果: {}", signatureShortStr, result); 
  42.             return result; 
  43.         } finally { 
  44.             // 移除 mdc 
  45.         } 
  46.     } 
  47.  

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

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

主要用途:

(1)日志的出参/入参

(2)统一设置 TraceId

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

基于 HandlerInterceptor

  1. import org.slf4j.Logger; 
  2. import org.slf4j.LoggerFactory; 
  3. import org.springframework.stereotype.Component; 
  4. import org.springframework.web.servlet.HandlerInterceptor; 
  5. import org.springframework.web.servlet.ModelAndView; 
  6.  
  7. import javax.servlet.DispatcherType; 
  8. import javax.servlet.http.HttpServletRequest; 
  9. import javax.servlet.http.HttpServletResponse; 
  10.  
  11.  
  12. @Component 
  13. public class LogHandlerInterceptor implements HandlerInterceptor { 
  14.  
  15.     private Logger logger = LoggerFactory.getLogger(LogHandlerInterceptor.class); 
  16.  
  17.     @Override 
  18.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 
  19.         // 统一的权限校验、路由等 
  20.         logger.info("LogHandlerInterceptor#preHandle 请求地址:{}", request.getRequestURI()); 
  21.  
  22.         if (request.getDispatcherType().equals(DispatcherType.ASYNC)) { 
  23.             return true
  24.         } 
  25.  
  26.         return true
  27.     } 
  28.  
  29.     @Override 
  30.     public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 
  31.         logger.info("LogHandlerInterceptor#postHandle 调用"); 
  32.     } 
  33.  
  34.     @Override 
  35.     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 
  36.  
  37.     } 
  38.  

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

  1. import com.github.houbb.springboot.learn.aspect.aspect.LogHandlerInterceptor; 
  2. import org.springframework.beans.factory.annotation.Autowired; 
  3. import org.springframework.context.annotation.Configuration; 
  4. import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 
  5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 
  6.  
  7.  
  8. @Configuration 
  9. public class SpringMvcConfig extends WebMvcConfigurerAdapter { 
  10.  
  11.     @Autowired 
  12.     private LogHandlerInterceptor logHandlerInterceptor; 
  13.  
  14.     @Override 
  15.     public void addInterceptors(InterceptorRegistry registry) { 
  16.         registry.addInterceptor(logHandlerInterceptor) 
  17.                 .addPathPatterns(
  18. @ControllerAdvice 
  19. public class MyResponseBodyAdvice implements ResponseBodyAdvice { 
  20.  
  21.      
  22.     private static final Logger LOG = LoggerFactory.getLogger(MyResponseBodyAdvice.class); 
  23.  
  24.     @Override 
  25.     public boolean supports(MethodParameter methodParameter, Class aClass) { 
  26.         //这个地方如果返回false, 不会执行 beforeBodyWrite 方法 
  27.         return true
  28.     } 
  29.  
  30.     @Override 
  31.     public Object beforeBodyWrite(Object resp, MethodParameter methodParameter, MediaType mediaType, Class> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { 
  32.         String uri = serverHttpRequest.getURI().getPath(); 
  33.         LOG.info("MyResponseBodyAdvice#beforeBodyWrite 请求地址:{}", uri); 
  34.  
  35.         ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) serverHttpRequest; 
  36.         HttpServletRequest servletRequest = servletServerHttpRequest.getServletRequest(); 
  37.  
  38.         // 可以做统一的拦截器处理 
  39.  
  40.         // 可以对结果做动态修改等 
  41.         LOG.info("MyResponseBodyAdvice#beforeBodyWrite 响应结果:{}", resp); 
  42.         return resp; 
  43.     } 
  44.  
  45. 测试

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

    http://localhost:18080/index

    页面响应:

    1. {"respCode":"00","respDesc":"成功","result":"ok"

    后端日志:

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

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

    异步执行

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

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

    定义异步线程池

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

    1. import org.springframework.context.annotation.Bean; 
    2. import org.springframework.context.annotation.Configuration; 
    3. import org.springframework.core.task.AsyncTaskExecutor; 
    4. import org.springframework.scheduling.annotation.EnableAsync; 
    5. import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 
    6.  
    7.  
    8. @Configuration 
    9. @EnableAsync 
    10. public class SpringAsyncConfig { 
    11.  
    12.     @Bean(name = "asyncPoolTaskExecutor"
    13.     public AsyncTaskExecutor taskExecutor() { 
    14.         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 
    15.         executor.setMaxPoolSize(10); 
    16.         executor.setQueueCapacity(10); 
    17.         executor.setCorePoolSize(10); 
    18.         executor.setWaitForTasksToCompleteOnShutdown(true); 
    19.         return executor; 
    20.     } 
    21.  

    异步执行的 Controller

    1. @RestController 
    2. public class MyAsyncController extends BaseAsyncController { 
    3.  
    4.     @Override 
    5.     protected String process(HttpServletRequest request) { 
    6.         return "ok"
    7.     } 
    8.  
    9.     @RequestMapping("/async"
    10.     public AsyncResp hello(HttpServletRequest request) { 
    11.         AsyncResp resp = super.execute(request); 
    12.  
    13.         System.out.println("Controller#async 结果:" + resp); 
    14.         return resp; 
    15.     } 
    16.  

     其中 BaseAsyncController 的实现如下:

    1. @RestController 
    2. public abstract class BaseAsyncController { 
    3.  
    4.     protected abstract T process(HttpServletRequest request); 
    5.  
    6.     @Autowired 
    7.     private AsyncTaskExecutor taskExecutor; 
    8.  
    9.     protected AsyncResp execute(HttpServletRequest request) { 
    10.         // 异步响应结果 
    11.         AsyncResp resp = new AsyncResp(); 
    12.         try { 
    13.             taskExecutor.execute(new Runnable() { 
    14.                 @Override 
    15.                 public void run() { 
    16.                     try { 
    17.                         T result = process(request); 
    18.  
    19.                         resp.setRespCode("00"); 
    20.                         resp.setRespDesc("成功"); 
    21.                         resp.setResult(result.toString()); 
    22.  
    23.                     } catch (Exception exception) { 
    24.                         resp.setRespCode("98"); 
    25.                         resp.setRespDesc("任务异常"); 
    26.                     } 
    27.                 } 
    28.             }); 
    29.         } catch (TaskRejectedException e) { 
    30.             resp.setRespCode("99"); 
    31.             resp.setRespDesc("任务拒绝"); 
    32.         } 
    33.  
    34.         return resp; 
    35.     } 
    36.  

     execute 的实现也比较简单:

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

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

    思考

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

    如果我们请求

    http://localhost:18080/async,那么:

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

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

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

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

    测试

    我们页面请求

    http://localhost:18080/async。

    页面响应如下:

    1. {"respCode":"00","respDesc":"成功","result":"ok"

    后端的日志:

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

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

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

    1. {"respCode":"00","respDesc":"成功","result":"ok"

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

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

    1. AsyncResp{respCode='null', respDesc='null', result='null'

    无法获取异步结果。

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

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

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

    反思

    可以发现,spring 对于页面的响应也许和我们想的有些不一样,并不是直接获取同步结果。

    写到这里,发现自己对于 mvc 的理解一直只是停留在表面,没有真正理解整个流程。

    Aspect 的形式在很多框架中都会使用,不过这里会发现无法获取异步的执行结果,存在一定问题。

     

    免责声明:

    ① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

    ② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

    软考中级精品资料免费领

    • 历年真题答案解析
    • 备考技巧名师总结
    • 高频考点精准押题
    • 资料下载
    • 历年真题
    • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

      难度     813人已做
      查看
    • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

      难度     354人已做
      查看
    • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

      难度     318人已做
      查看
    • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

      难度     435人已做
      查看
    • 2024年上半年系统架构设计师考试综合知识真题

      难度     224人已做
      查看

    相关文章

    发现更多好内容
    咦!没有更多了?去看看其它编程学习网 内容吧