重学SpringBoot系列之生命周期内的拦截过滤与监听

2021-12-07 18:23:46 浏览数 (1)

重学SpringBoot系列之生命周期内的拦截过滤与监听

  • Servlet域对象与属性变化监听
    • 监听器定义与实现
    • 使用场景
    • 监听器的实现
    • 全局Servlet组件扫描注解
    • 监听器测试
      • session创建时机
  • Servlet过滤器的实现
    • 过滤器
      • 过滤器的实现
  • servlet
  • spring拦截器及请求链路说明
    • 拦截器Interceptor
    • 拦截器与过滤器的核心区别
    • 拦截器的实现
    • 请求链路说明
  • 自定义事件的发布与监听
    • 事件监听介绍
      • 事件监听的角色
      • 事件监听的使用场景
    • 代码具体实现
      • 自定义事件
      • 自定义事件监听器
        • 方式1
        • 方式2(推荐)
        • 方式3
        • 方式4(推荐).
    • 测试监听事件的发布
  • 应用启动的监听
    • 简介
    • 常用场景介绍
    • 代码小实验
      • 通过@Component定义方式实现
      • 通过@Bean定义方式实现
    • 执行测试
    • 总结
    • 问题总结

Servlet域对象与属性变化监听

监听器定义与实现

定义:

Servlet 监听器是 Servlet 规范中定义的一种特殊类,用于监听 ServletContextHttpSessionServletRequest 等作用域对象的创建与销毁事件,以及监听这些作用域对象中属性发生修改的事件。监听器使用了设计模式中的观察者模式,它关注特定事物的创建、销毁以及变化并做出回调动作,因此监听器具有异步的特性。

Servlet Listener 监听三大域对象的创建和销毁事件,三大对象分别是:

  • ServletContext Listener:application级别,整个应用只存在一个,所有用户使用一个ServletContext
  • HttpSession Listener:session 级别,同一个用户的浏览器开启与关闭生命周期内使用的是同一个session
  • ServletRequest Listener:request 级别,每一个HTTP请求为一个request

除了监听域对象的创建和销毁,还可以监听域对象中属性发生修改的事件。

  • HttpSessionAttributeListener
  • ServletContextAttributeListener
  • ServletRequestAttributeListener

使用场景

Servlet 规范设计监听器的作用是在事件发生前、发生后进行一些处理,一般可以用来统计在线人数和在线用户、统计网站访问量、系统启动时初始化信息等


监听器的实现

代码语言:javascript复制
@Slf4j
@WebListener//标注当前类为Servlet的一个监听器组件
public class CustomListener implements ServletContextListener,
        ServletRequestListener,
        HttpSessionListener,
        ServletRequestAttributeListener{

    @Override
    public void contextInitialized(ServletContextEvent se) {
        log.info("==============context创建");
    }

    @Override
    public void contextDestroyed(ServletContextEvent se) {
        log.info("==============context销毁");
    }


    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        log.info("                   request监听器:销毁");
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        log.info("                   request监听器:创建");
    }


    @Override
    public void sessionCreated(HttpSessionEvent se) {
        log.info("----------------session创建");
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        log.info("----------------session销毁");
    }

    public void attributeAdded(ServletRequestAttributeEvent srae) {

        log.info("----------------attributeAdded");
    }

    public void attributeRemoved(ServletRequestAttributeEvent srae) {
        log.info("----------------attributeRemoved");
    }

    public void attributeReplaced(ServletRequestAttributeEvent srae) {
        log.info("----------------attributeReplaced");
    }
}
  • 实现ServletRequestListener接口,并重写requestDestroyed销毁和requestInitialized方法。一次ServletRequest的requestInitialized方法和requestDestroyed销毁方法的执行代表1次请求的接收与处理完毕。所以比较适合网站资源被访问次数的统计。
  • 实现HttpSessionListener接口,并重写sessionInitialized初始化和sessionDestroyed销毁方法,可以监听session会话的开启与销毁(用户的上线与下线)。比如:可以用来实现在线用户数量的统计
  • 实现ServletContextListener接口,并重写contextInitialized初始化和contextDestroyed销毁方法,可以监听全局应用的初始化和销毁。比如:在系统启动的时候,初始化一些数据到内存中供后续使用。
  • 实现ServletRequestAttributeListener接口(或HttpSessionAttributeListener或ServletContextAttributeListener)。可以监听到对应的作用域内数据属性的attributeAdded新增、attributeRemoved删除、attributeReplaced替换等动作。

全局Servlet组件扫描注解

在启动类中加入@ServletComponentScan进行自动注册即可。

源码剖析:

由于@ServletComponentScan所在类被加载BeanDefinition时,会加载其Registar。所以会导入ServletComponentScanRegistrar,由此向beanfactory添加了一个ServletComponentRegisteringPostProcessor。当最后调用各种nonOrderedPostProcessors时,会调用ServletComponentRegisteringPostProcessor的postProcessBeanFactory,最终扫描路径获取由@WebServlet,@WebFilter,@WebListener注册的servlet,filter,listener。

这里被扫描到的组件会被注入到IOC容器中,但由于原生listener,filter,servlet粒度比spring的大,因此这三项会先于spring初始化所有bean时候就已经存在了,所以无法在这里类里面进行bean的注入

包路径不指定,默认是当前包及其子包

详细源码剖析过程看这里


监听器测试

定义如下的Controller进行访问测试:

  • 当应用启动的时候。“==============context创建”被打印出来,说明触发contextInitialized监听函数
  • 访问“http://localhost:8888/hello”, 断点断住,“ request监听器:创建”被打印出来,说明requestInitialized回调函数被触发
  • 紧接着“----------------session创建”被打印出来,说明sessionCreated监听函数被触发
  • 继续执行request.setAttribute(“a”, “a”);,“----------------attributeAdded”被打印出来,说明attributeAdded监听函数被触发
  • 继续执行request.setAttribute(“a”, “b”);“----------------attributeReplaced”被打印出来,说明attributeReplaced监听函数被触发
  • 继续执行完成request.removeAttribute(“a”);“----------------attributeRemoved”被打印出来,说明attributeRemoved监听函数被触发
  • 继续执行session.invalidate();,“----------------session销毁”被打印出来,说明sessionDestroyed监听函数被触发
  • 将controller方法执行完成," request监听器:销毁"被打印出来,说明requestDestroyed监听函数被触发。
  • 当停掉应用的时候,”==============context销毁”被打印出来,说明contextDestroyed监听函数被触发

从上面的打印结果看:作用域范围是context 大于 request 大于sesion,实际上并不是。因为我们手动的调用了session.invalidate();,session才会被销毁。正常情况下session的销毁是由servlet容器根据session超时时间等因素来控制的。

所以正常的作用域生命周期 ServletContext > HttpSession > request

在以上的断点监听测试中,会有一些多余的监听日志被打印,是Spring Boot系统默认的帮我们做一些属性的添加与删除设置,从而触发监听,请忽略掉它们。就看我们断点执行之后的那一条,和断点停住的前一条日志


session创建时机

注意: session并不是一访问controller层就会创建,默认是不创建的,除非调用类似下面的代码,才会创建:

代码语言:javascript复制
request.getSession()
或者request.getSession(true);服务器才会产生session。
如果调用request.getSession(false);将不会产生session

如果我们在controller层中的方法参数上填request或者session对象,spring会自动帮我们进行注入
例如如果写了HttpSession 那么才会帮我们创建一个session对象,否则不会有session对象生成

如果是jsp文件,服务器会在生成的servlet文件中为你自动创建
jsp中默认的是
HttpSession session=request.getSession(ture);

会自动创建session的写法

代码语言:javascript复制
  @GetMapping("aaa")
    public Object test(HttpServletRequest req,HttpSession session)
    {
        String requestURI = req.getRequestURI();
        return requestURI;
    }
代码语言:javascript复制
  @GetMapping("aaa")
    public Object test(HttpServletRequest req)
    {
        String requestURI = req.getRequestURI();
            HttpSession session = req.getSession();
        return requestURI;
    }

不会自动创建session的写法

代码语言:javascript复制
  @GetMapping("aaa")
    public Object test(HttpServletRequest req)
    {
        String requestURI = req.getRequestURI();
        return requestURI;
    }
代码语言:javascript复制
  @GetMapping("aaa")
    public Object test(HttpServletRequest req)
    {
        String requestURI = req.getRequestURI();
            HttpSession session = req.getSession(false);
        return requestURI;
    }

Servlet过滤器的实现

过滤器

定义

Servlet 过滤器是可用于 Servlet 编程的 Java 类,有以下目的:

  • 在客户端的请求访问后端资源之前,拦截这些请求。
  • 在服务器的响应发送回客户端之前,处理这些响应。

使用场景

在实际的应用开发中,我们经常使用过滤器做以下的一些事情

  • 基于一定的授权逻辑,对HTTP请求进行过滤,从而保证数据访问的安全。比如:判断请求的来源IP是否在系统黑名单中
  • 对于一些经过加密的HTTP请求数据,进行统一解密,方便后端资源进行业务处理
  • 或者我们社交应用经常需要的敏感词过滤,也可以使用过滤器,将触发敏感词的非法请求过滤掉

过滤器主要的特点在于:一是可以过滤所有请求,二是它能够改变请求的数据内容。


过滤器的实现

实现及注册方式一:利用WebFilter注解配置

@WebFilter时Servlet3.0新增的注解,原先实现过滤器,需要在web.xml中进行配置,而现在通过此注解,启动启动时会自动扫描自动注册。

编写Filter类:

代码语言:javascript复制
//注册器名称为customFilter,拦截的url为所有
@WebFilter(filterName="customFilter",urlPatterns={"/*"})
@Slf4j
public class CustomFilter implements Filter{

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("filter 初始化");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        log.info("customFilter 请求处理之前----doFilter方法之前过滤请求");
        //对request、response进行一些预处理
        // 比如设置请求编码
        // request.setCharacterEncoding("UTF-8");
        // response.setCharacterEncoding("UTF-8");

        //链路 直接传给下一个过滤器
        chain.doFilter(request, response);

        log.info("customFilter 请求处理之后----doFilter方法之后处理响应");
    }

    @Override
    public void destroy() {
        log.info("filter 销毁");
    }
}

然后在启动类加入@ServletComponentScan注解即可。

使用这种方法当注册多个过滤器时,无法指定过滤器的先后执行顺序。

原本使用web.xml配置过滤器时,是可指定执行顺序的,但使用@WebFilter时,没有这个配置属性的(需要配合@Order进行),所以接下来介绍下通过FilterRegistrationBean进行过滤器的注册。

默认这种注解方式配置的过滤器排序是按照首字母

通过过滤器的java类名称,进行顺序的约定,比如LogFilter和AuthFilter,此时AuthFilter就会比LogFilter先执行,因为首字母A比L前面

SpringBoot下,利用@WebFilter配置使用与注意Filter


注册方式二:FilterRegistrationBean方式

FilterRegistrationBean是springboot提供的,此类提供setOrder方法,可以为filter设置排序值,让spring在注册web filter之前排序后再依次注册。

首先要改写filter, 其实就删掉@webFilter注解即可,其他的都没有变化。然后的代码是Filter的注册代码

代码语言:javascript复制
@Configuration
public class FilterRegistration {
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        //Filter可以new,也可以使用依赖注入Bean
        registration.setFilter(new CustomFilter());
        //过滤器名称
        registration.setName("customFilter");
        //拦截路径
        registration.addUrlPatterns("/*");
        //设置顺序
        registration.setOrder(10);
        return registration;
    }
}

注册多个时,就注册多个FilterRegistrationBean即可,启动后,效果和第一种是一样的。可以访问应用内的任意资源进行过滤器测试,因为过滤器是针对所有的请求和响应。可以输入Filter中的log信息。


servlet

定义

在java程序员10年以前做web开发的时候,所有的请求都是由servlet来接受并响应的。每一个请求,就要写一个servlet。这种方式很麻烦,大家就想能不能根据请求的路径以及参数不同,映射到不同的方法上去执行,这样就可以在一个servlet类里面处理多个请求,每个请求就是一个方法。这个思想后来就逐渐发展为structs、SpringMVC等框架

使用场景

目前来看,servlet使用的场景已经被springMVC封装架构全面覆盖,几乎没有什么需要使用原始servlet进行开发的场景。但是不排除,老项目向spring boot项目迁移融合,需要支持servlet的情况

实现

下面我们就看一下,在spring boot里面如何实现servlet。

代码语言:javascript复制
@WebServlet(name = "firstServlet", urlPatterns = "/firstServlet") //标记为servlet,以便启动器扫描。
public class FirstServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().append("firstServlet");
    }

}

然后在启动类加入@ServletComponentScan注解即可。


spring拦截器及请求链路说明

拦截器Interceptor

在 Servlet 规范中并没有拦截器的概念,它是在Spring框架内衍生出来。

Spring中拦截器有三个方法:

  • preHandle 表示被拦截的URL对应的控制层方法,执行前的自定义处理逻辑
  • postHandle 表示被拦截的URL对应的控制层方法,执行后的自定义处理逻辑,此时还未将modelAndView进行页面渲染。
  • afterCompletion 表示此时modelAndView已做页面渲染,执行拦截器的自定义处理

拦截器与过滤器的核心区别

从请求处理的生命周期上看,拦截器Interceptor和过滤器filter的作用是类似的。过滤器能做的事情,拦截器几乎也都能做。

但是二者使用场景还是有一些区别的:

  • 规范不同:Filter是在Servlet规范中定义的组件,在servlet容器内生效。而拦截器是Spring框架支持的,在Spring上下文中生效。
  • 拦截器可以获取并使用Spring IOC容器中的bean,但过滤器就不行。因为过滤器是Servlet的组件,而IOC容器的bean是Spring框架内使用,拦截器恰恰是Spring框架内衍生出来的。
  • 拦截器可以访问Spring上下文值对象,如ModelAndView,过滤器不行。基于与上一点同样的原因。
  • 过滤器在进入servlet容器之前处理请求,拦截器在servlet容器之内处理请求。过滤器比拦截器的粒度更大,比较适合系统级别的所有API的处理动作。比如:权限认证,Spring Security就大量的使用了过滤器。
  • 拦截器相比于过滤器粒度更小,更适合分模块、分范围的统一业务逻辑处理。比如:分模块的、分业务的记录审计日志。(后面在日志的管理的那一章,我们会为介绍使用拦截器实现统一访问日志的记录)

比如说:我们在Filter中使用注解,注入一个测试service,结果为null。因为过滤器无法使用Spring IOC容器bean。


拦截器的实现

编写自定义拦截器类,此处我们用一个简单的例子让大家了解拦截器的生命周期。后面在日志的管理的那一章,我们会为介绍使用拦截器实现统一访问日志的记录的实战

代码语言:javascript复制
@Slf4j
@Component
public class CustomHandlerInterceptor implements HandlerInterceptor {

 @Override
 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
   throws Exception {
  log.info("preHandle:请求前调用");
  //返回 false 则请求中断
  return true;
 }

 @Override
 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
   ModelAndView modelAndView) throws Exception {
  log.info("postHandle:请求后调用");

 }

 @Override
 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
   throws Exception {
  log.info("afterCompletion:请求调用完成后回调方法,即在视图渲染完成后回调");

 }

}

通过继承WebMvcConfigurerAdapter注册拦截器。笔者在写作完成后,发现WebMvcConfigurerAdapter类已经被废弃,请实现WebMvcConfigurer接口完成拦截器的注册

代码语言:javascript复制
@Configuration
//废弃:public class MyWebMvcConfigurer extends WebMvcConfigurerAdapter{
public class MyWebMvcConfigurer implements WebMvcConfigurer {
  

  @Resource
  CustomHandlerInterceptor customHandlerInterceptor;

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
     //注册拦截器 拦截规则
    //多个拦截器时 以此添加 执行顺序按添加顺序
    registry.addInterceptor(customHandlerInterceptor).addPathPatterns("/*");
  }
   

}

如果我们在CustomHandlerInterceptor ,注入一个测试service,结果是可以正确依赖注入并使用该Service的。


请求链路说明

随便请求一个系统内的API(因为我们配置的过滤器拦截器拦截所有请求),通过输出结果分析一下拦截器、过滤器中各接口函数的执行顺序。

代码语言:javascript复制
CustomFilter  : customFilter 请求处理之前----doFilter方法之前过滤请求
CustomHandlerInterceptor  : preHandle:请求前调用
CustomHandlerInterceptor  : postHandle:请求后调用
CustomHandlerInterceptor  : afterCompletion:请求调用完成后回调方法,即在视图渲染完成后回调
CustomFilter  : customFilter 请求处理之后----doFilter方法之后处理响应

请求链路调用顺序图如下所示:


自定义事件的发布与监听

事件监听介绍

事件监听的角色

首先我们要理解事件监听中需要的几个角色

  • 事件发布者 (即事件源)
  • 事件监听者
  • 事件本身

事件监听的使用场景

为了将技术问题简单化,为大家举一个简单的例子。比如居委会发布停水通知。居委会就是事件源、停水就是事件本身、该居委会的辖区居民就是事件监听者。大家看这个例子,有这样几个特点:

  • 异步处理:居委会工作人员发布通知之后,就可以去忙别的工作了,不会原地等待所有居民的反馈。
  • 解耦:居委会和居民之间是解耦的,互相不干扰对方的工作状态与生活状态。
  • 不规律性:对于停水的事件发生频率是不规律的,触发规则相对随机。

笔者当你在一个系统的业务需求中,满足上面的几个特点中的2点,就应该考虑使用事件监听机制实现业务需求。当然实现事件监听机制有很的方法,比如:

  • 使用消息队列中间件的发布订阅模式
  • JDK自带的java.util.EventListener
  • 本节为大家介绍的是:Spring环境下的实现事件发布监听的方法

代码具体实现

自定义事件

继承自ApplicationEvent抽象类,然后定义自己的构造器。

代码语言:javascript复制
@SuppressWarnings("serial")
public class MyEvent extends ApplicationEvent
{
 public MyEvent(Object source)
 {
  super(source);
 }
}

自定义事件监听器

springboot进行事件监听有四种方式

  • 1.写代码向ApplicationContext中添加监听器
  • 2.使用Component注解将监听器装载入spring容器
  • 3.在application.properties中配置监听器
  • 4.通过@EventListener注解实现事件监听

方式1

首先创建MyListener1类

代码语言:javascript复制
@Slf4j
public class MyListener1 implements ApplicationListener<MyEvent> {
    public void onApplicationEvent(MyEvent event) {
        log.info(String.format("%s监听到事件源:%s.", MyListener1.class.getName(), event.getSource()));
    }
}

然后在springboot应用启动类中获取ConfigurableApplicationContext上下文,装载监听

代码语言:javascript复制
@SpringBootApplication
public class BootLaunchApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(BootLaunchApplication.class, args);
        //装载监听
        context.addApplicationListener(new MyListener1());
    }
}

方式2(推荐)

创建MyListener2类,并使用@Component注解将该类装载入spring容器中

代码语言:javascript复制
@Component
@Slf4j
public class MyListener2 implements ApplicationListener<MyEvent> {

    public void onApplicationEvent(MyEvent event) {
        log.info(String.format("%s监听到事件源:%s.", MyListener2.class.getName(), event.getSource()));
    }

}

方式3

首先创建MyListener3类

代码语言:javascript复制
@Slf4j
public class MyListener3 implements ApplicationListener<MyEvent> {
    public void onApplicationEvent(MyEvent event) {
        log.info(String.format("%s监听到事件源:%s.", MyListener3.class.getName(), event.getSource()));
    }
}

然后在application.properties中配置监听

代码语言:javascript复制
context:
  listener:
    classes: com.zimug.bootlaunch.customlistener.MyListener3

方式4(推荐).

创建MyListener4类,该类无需实现ApplicationListener接口,使用@EventListener装饰具体方法

代码语言:javascript复制
@Slf4j
@Component
public class MyListener4 {
    @EventListener
    public void listener(MyEvent event) {
        log.info(String.format("%s监听到事件源:%s.", MyListener4.class.getName(), event.getSource()));
    }
}

测试监听事件的发布

有了applicationContext,想在哪发布事件就在哪发布事件

代码语言:javascript复制
@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomListenerTest {

    @Resource private
    ApplicationContext applicationContext;

    @Test
    public void testEvent(){
        applicationContext.publishEvent(new MyEvent("测试事件."));
    }
}

启动后,日志打印如下。(下面截图是在启动类发布事件后的截图,在单元测试里面监听器1监听不到,执行顺序问题):

由日志打印可以看出,SpringBoot四种事件的实现方式监听是有序的。无论执行多少次都是这个顺序。

由日志打印可以看出,SpringBoot四种事件的实现方式监听是有序的。无论执行多少次都是这个顺序。

深入理解 Spring 事件发布与监听


应用启动的监听

简介

Spring Boot提供了两个接口:CommandLineRunnerApplicationRunner,用于启动应用时做特殊处理,这些代码会在SpringApplicationrun()方法运行完成之前被执行。

相对于之前章节为大家介绍的Spring的ApplicationListener接口自定义监听器、Servlet的ServletContextListener监听器。

使用二者的好处在于,可以方便的使用应用启动参数,根据参数不同做不同的初始化操作。

常用场景介绍

实现CommandLineRunner、ApplicationRunner接口。通常用于应用启动前的特殊代码执行,比如:

  • 将系统常用的数据加载到内存
  • 应用上一次运行的垃圾数据清理
  • 系统启动成功后的通知的发送

如下图是我实现了CommandLineRunner接口,在应用启动时将系统内常用的配置数据。从数据库加载到内存,以后使用该数据的时候只需要调用getSysConfigList方法,不需要每次使用该数据都去数据库加载。节省系统资源、缩减数据加载时间。


代码小实验

通过@Component定义方式实现

CommandLineRunner:参数是字符串数组

代码语言:javascript复制
@Slf4j
@Component
public class CommandLineStartupRunner implements CommandLineRunner {
    @Override
    public void run(String... args){
        log.info("CommandLineRunner传入参数:{}", Arrays.toString(args));
    }
}

ApplicationRunner:参数被放入ApplicationArguments,通过getOptionNames()、getOptionValues()、getSourceArgs()获取参数

代码语言:javascript复制
@Slf4j
@Component
public class AppStartupRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args)  {
        log.info("ApplicationRunner参数名称: {}", args.getOptionNames());
        log.info("ApplicationRunner参数值: {}", args.getOptionValues("age"));
        log.info("ApplicationRunner参数: {}", Arrays.toString(args.getSourceArgs()));
    }
}

通过@Bean定义方式实现

这种方式可以指定执行顺序,注意前两个Bean是CommandLineRunner,最后一个Bean是ApplicationRunner 。

代码语言:javascript复制
@Configuration
public class BeanRunner {
    @Bean
    @Order(1)
    public CommandLineRunner runner1(){
        return new CommandLineRunner() {
            @Override
            public void run(String... args){
                System.out.println("BeanCommandLineRunner run1()"   Arrays.toString(args));
            }
        };
    }

    @Bean
    @Order(2)
    public CommandLineRunner runner2(){
        return new CommandLineRunner() {
            @Override
            public void run(String... args){
                System.out.println("BeanCommandLineRunner run2()"   Arrays.toString(args));
            }
        };
    }

    @Bean
    @Order(3)
    public ApplicationRunner runner3(){
        return new ApplicationRunner() {
            @Override
            public void run(ApplicationArguments args){
                System.out.println("BeanApplicationRunner run3()"   Arrays.toString(args.getSourceArgs()));
            }
        };
    }
}

可以通过@Order设置执行顺序


执行测试

在启动配置中加入如下参数,保存后启动应用

测试输出结果:

代码语言:javascript复制
c.z.boot.launch.config.AppStartupRunner  : ApplicationRunner参数名称: [name, age]
c.z.boot.launch.config.AppStartupRunner  : ApplicationRunner参数值: [18]
c.z.boot.launch.config.AppStartupRunner  : ApplicationRunner参数: [--name=zimug, --age=18]

BeanApplicationRunner run3()[--name=zimug, --age=18]

c.z.b.l.config.CommandLineStartupRunner  : CommandLineRunner传入参数:[--name=zimug, --age=18]
BeanCommandLineRunner run1()[--name=zimug, --age=18]
BeanCommandLineRunner run2()[--name=zimug, --age=18]

从测试结果上看(笔者目前不敢确定这个优先级顺序是不是常态,但从我的多次测试效果,顺序一直是这样的):

  • ApplicationRunner执行优先级高于CommandLineRunner
  • 以Bean的形式运行的Runner优先级要低于Component注解加implements Runner接口的方式
  • Order注解只能保证同类的CommandLineRunner或ApplicationRunner的执行顺序,不能跨类保证顺序

总结

CommandLineRunner、ApplicationRunner的核心用法是一致的,就是用于应用启动前的特殊代码执行。ApplicationRunner的执行顺序先于CommandLineRunner;ApplicationRunner将参数封装成了对象,提供了获取参数名、参数值等方法,操作上会方便一些。


问题总结

这是笔者在实践中真实遇到的问题,就是我定义了多个CommandLineRunner的实现。出现奇怪的问题是:当你定义多个CommandLineRunner的实现的时候,其中一个或者几个将不会执行。

分析一下:下面的代码是SpringBootApplication启动项目之后会执行的代码,大家看代码中通过一个遍历来启动CommandLineRunner或者ApplicationRunner。也就是说,只有上一个CommandLineRunner执行完成之后,才会执行下一个CommandLineRunner,是同步执行的。

代码语言:javascript复制
private void callRunners(ApplicationContext context, ApplicationArguments args) {
		List<Object> runners = new ArrayList<>();
		runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
		runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
		AnnotationAwareOrderComparator.sort(runners);
		for (Object runner : new LinkedHashSet<>(runners)) {
			if (runner instanceof ApplicationRunner) {
				callRunner((ApplicationRunner) runner, args);
			}
			if (runner instanceof CommandLineRunner) {
				callRunner((CommandLineRunner) runner, args);
			}
		}
	}

所以,如果在CommandLineRunner某个实现run 方法体中调用了同步阻塞的API或者是一个 while(true) 循环,在遍历中处于该CommandLineRunner之后的其他实现将不会被执行。

0 人点赞