⽣命周期内的拦截过滤与监听


⽣命周期内的拦截过滤与监听

1.Servlet 域对象与属性变化监听

⼀、监听器定义与实现

1.1 定义

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

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

  1. ServletContext Listener:application 级别,整个应⽤只存在⼀个,所有⽤户使⽤⼀个 ServletContext
  2. HttpSession Listener:session 级别,同⼀个⽤户的浏览器开启与关闭⽣命周期内使⽤的是同⼀个 session
  3. ServletRequest Listener:request 级别,每⼀个 HTTP 请求为⼀个 request

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

  • HttpSessionAttributeListener
  • ServletContextAttributeListener
  • ServletRequestAttributeListener

1.2 使⽤场景

Servlet 规范设计监听器的作⽤是在事件发⽣前、发⽣后进⾏⼀些处理,⼀般可以⽤来统计在线⼈数和在线⽤户、统计⽹站访问量、系统启动时初始化信息等。

1.3 监听器的实现

@Slf4j
@WebListener
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 替换等动作。

1.4 全局 Servlet 组件扫描注解

在启动类中加⼊**@ServletComponentScan**进⾏⾃动注册即可。

⼆、监听器测试

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

@RestController
public class TestController {
    @GetMapping("/hello")
    public String hello(HttpServletRequest request, HttpSession session)
		{
        //操作 request 的 attribute
        request.setAttribute("a", "a");
        request.setAttribute("a", "b");
        request.getAttribute("a");
        request.removeAttribute("a");
        //操作 session 的 attribute
        session.setAttribute("a", "a");
        session.getAttribute("a");
        session.invalidate();
        return "hello world---";
   }
}
  • 当应⽤启动的时候。“==============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

在以上的监听测试中,会有⼀些多余的监听⽇志被打印,是 SpringBoot 系统默认帮我们做⼀些属性的添加与删除设置,从⽽触发监听,可以忽略掉。

2.Servlet 过滤器的实现

⼀、过滤器

1.1 定义

Servlet 过滤器是可⽤于 Servlet 编程的 Java 类,⽬的:

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

1.2 使⽤场景

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

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

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

1.3 过滤器的实现

注册⽅式⼀:利⽤ WebFilter 注解配置

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

编写 Filter 类:

//注册器名称为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进⾏⼀些预处理
        //链路 直接传给下⼀个过滤器
        chain.doFilter(request, response);
        log.info("customFilter 请求处理之后----doFilter⽅法之后处理响应");
   }
    @Override
    public void destroy() {
        log.info("filter 销毁");
   }
}

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

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

–⼩技巧–

通过对过滤器名称的指定,进⾏顺序的约定,⽐如 LogFilter 和 AuthFilter,此时 AuthFilter 就会⽐ LogFilter 先执⾏,因为⾸字⺟ A ⽐ L 排序靠前。

注册⽅式⼆:FilterRegistrationBean ⽅式

FilterRegistrationBean是 SpringBoot 提供的,此类提供 setOrder ⽅法,可以为 filter 设置排序值,让 Spring 在注册 WebFilter 之前排序后再依次注册。

@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 即可。启动后效果和第⼀种是⼀样的。可以访问 应⽤内的任意资源进⾏过滤器测试。

⼆、servlet

2.1 定义

Java 程序员⼗⼏年前做 Web 开发的时候,所有的请求都是由 Servlet 来接受并响应的。每来⼀个请求,就要写⼀个 Servlet。 这种⽅式很麻烦,⼤家就在想能不能根据请求的路径以及参数不同,映射到不同的⽅法上去执⾏,这样 就可以在⼀个 Servlet 类⾥⾯处理多个请求,每个请求就是⼀个⽅法。这个思想后来就逐渐发展为 struts、SpringMVC 等框架。

2.2 使⽤场景

⽬前来看,Servlet 使⽤的场景已经被 SpringMVC 封装架构全⾯覆盖,⼏乎没有什么需要使⽤原始 Servlet 进⾏开发的场景。但是不排除,⽼项⽬向 SpringBoot 项⽬迁移融合,需要⽀持 Servlet 的情况, 作为基础也是有必要的。

2.3 实现

我们来看⼀下在 SpringBoot ⾥⾯如何实现 Servlet 的编写和使⽤。

@WebServlet(name = "firstServlet", urlPatterns = "/firstServlet")
@Slf4j
public class FirstServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 		{
        log.info("firstServlet");
        resp.getWriter().append("firstServlet");
    }
}

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

3. 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。

三、拦截器的实现

编写⾃定义拦截器类,此处⽤⼀个简单的例⼦让⼤家了解拦截器的⽣命周期。

package top.syhan.boot.interceptor;
import top.syhan.boot.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @description: ⾃定义拦截器类,了解拦截器的⽣命周期
* @author: syhan
* @date: 2022-04-04
**/
@Slf4j
@Component
public class CustomHandlerInterceptor implements HandlerInterceptor {
    @Resource
    private TestService testService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("preHandle:请求前调⽤");
        log.info(testService.test());
        //返回 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:请求调⽤完成后回调⽅法,即在视图渲染完成后回调");
    }
}

实现 WebMvcConfigurer 接⼝完成拦截器的注册。

package top.syhan.boot.interceptor;
import org.springframework.context.annotation.Configuration;
import
org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import
org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* @description: 注册拦截器,废弃:public class MyWebMvcConfigurer extends
WebMvcConfigurerAdapter
* @author: syhans
* @date: 2022-04-04
**/
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
    private final String[] excludePath = {"/static"};
    @Resource
    private CustomHandlerInterceptor customHandlerInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册拦截器 拦截规则
        //多个拦截器时 以此添加 执⾏顺序按添加顺序
      registry.addInterceptor(customHandlerInterceptor).addPathPatterns("/**").excludePathPatterns(excludePath);
   }
}

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

四、请求链路说明

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

Text
CustomFilter : customFilter 请求处理之前----doFilter⽅法之前过滤请求 CustomHandlerInterceptor : preHandle:请求前调⽤ CustomHandlerInterceptor : postHandle:请求后调⽤ CustomHandlerInterceptor : afterCompletion:请求调⽤完成后回调⽅法,即在视图渲染完成后回调 CustomFilter : customFilter 请求处理之后----doFilter⽅法之后处理响应

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

4.⾃定义事件的发布与监听

⼀、事件监听介绍:

1.1 事件监听的⻆⾊

⾸先我们要理解事件监听中需要的⼏个⻆⾊

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

1.2 事件监听的使⽤场景

举⼀个简单的例⼦:⽐如居委会发布停⽔通知。居委会就是事件源、停⽔就是事件本身、该居委会的辖区居⺠就是事件监听者。这个例⼦,有这样⼏个特点:

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

当你在⼀个系统的业务需求中,满⾜上⾯的⼏个特点中的 2 点,就应该考虑使⽤事件监听机制实现业务需求。

实现事件监听机制有很多⽅法,⽐如:

  • 使⽤消息队列中间件的发布订阅模式
  • JDK ⾃带的 java.util.EventListener
  • Spring 环境下的实现事件发布监听的⽅法

⼆、代码具体实现

2.1 ⾃定义事件

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

/**
* @description: ⾃定义事件:继承ApplicationEvent抽象类,并定义⾃⼰的构造器
* @author: syhan
* @date: 2022-04-04
**/
public class MyEvent extends ApplicationEvent {
    public MyEvent(Object source) {
        super(source);
   }
}

2.2 ⾃定义事件监听器

springboot 进⾏事件监听有四种⽅式

  1. 写代码向 ApplicationContext 中添加监听器

  2. 使⽤ Component 注解将监听器装载⼊ spring 容器

  3. 在 application.properties 中配置监听器

  4. 通过@EventListener 注解实现事件监听

⽅式 1

⾸先创建 MyListener1 类

/**
* @description: ⾃定义事件监听器⽅式1:实现ApplicationListener接⼝
* @author: syhan
* @date: 2022-04-04
**/
@Slf4j
public class MyListener1 implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent event) {
        log.info(String.format("%s 监听到事件源:%s.", MyListener1.class.getName(), event.getSource()));
   }
}

然后在 SpringBoot 应⽤启动类中获取 ConfigurableApplicationContext 上下⽂,装载监听

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        //获取ConfigurableApplicationContext上下⽂
        ConfigurableApplicationContext context =
SpringApplication.run(Application.class, args);
        //装载监听
        context.addApplicationListener(new MyListener1());
   }
}
⽅式 2(推荐)

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

/**
* @description: ⾃定义事件监听器⽅式2:使⽤@Component注解将该类装载⼊spring容器
中
* @author: mqxu
* @date: 2022-04-04
**/
@Component
@Slf4j
public class MyListener2 implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent event) {
        log.info(String.format("%s 监听到事件源:%s.", MyListener2.class.getName(), event.getSource()));
   }
}
⽅式 3

⾸先创建 MyListener3 类

/**
* @description: ⾃定义事件监听器⽅式3:在application.properties中配置监听
* @author: mqxu
* @date: 2022-04-04
**/
@Slf4j
public class MyListener3 implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent event) {
        log.info(String.format("%s 监听到事件源:%s.", MyListener3.class.getName(), event.getSource()));
   }
}

然后在 application.yml 中配置监听

context:
listener:
   classes: top.syhan.boot.listener.MyListener3
⽅式 4(推荐)

创建 MyListener4 类,该类⽆需实现 ApplicationListener 接⼝,使⽤@EventListener 装饰具体⽅法

/**
 * @description: ⾃定义事件监听器⽅式4:使⽤@EventListener装饰具体⽅法
 * @author: syhan
 * @date: 2022-04-04
 **/
@Slf4j
@Component
public class MyListener4 {
   @EventListener
   public void listener(MyEvent event) {
       log.info(String.format("%s 监听到事件源:%s.", MyListener4.class.getName(), event.getSource()));
    }
}

三、测试监听事件的发布

有了 applicationContext,想在哪发布事件就在哪发布事件,我们在启动主类发布事件

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        //获取ConfigurableApplicationContext上下⽂
        ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
        //装载监听
        context.addApplicationListener(new MyListener1());
        // 发布事件
        context.publishEvent(new MyEvent("测试事件"));
   }
}

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

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

5.应⽤启动的监听

⼀、简介

SpringBoot 提供了两个接⼝:CommandLineRunner、ApplicationRunner,⽤于启动应⽤时做特殊处理,这些代码会在 SpringApplication 的 run()⽅法运⾏完成之前被执⾏。 相对于之前介绍的 Spring 的 ApplicationListener 接⼝⾃定义监听器、Servlet 的 ServletContextListener 监听器。

使⽤⼆者的好处在于,可以⽅便地使⽤应⽤启动参数,根据参数不同做不同的初始化操作。

⼆、常⽤场景介绍

实现 CommandLineRunner、ApplicationRunner 接⼝,通常⽤于应⽤启动前的特殊代码执⾏,⽐如:

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

三、⼩实验

通过@Component 定义⽅式实现

CommandLineRunner:参数是字符串数组

@Component
@Slf4j
public class CommandLineStartupRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        log.info("CommandLineStartupRunner传⼊参数:{}", Arrays.toString(args));
   }
}

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

@Component
@Slf4j
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 。

package top.syhan.boot.runner;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import java.util.Arrays;
/**
* @description:
* @author: syhan
* @date: 2022-04-04
**/
@Configuration
@Slf4j
public class BeanRunner {
    @Bean
    @Order(1)
    public CommandLineRunner runner1() {
        return new CommandLineRunner() {
            @Override
            public void run(String... args) {
                log.info("BeanCommandLineRunner run1()" + Arrays.toString(args));
           }
       };
   }
    @Bean
    @Order(2)
    public CommandLineRunner runner2() {
        return new CommandLineRunner() {
            @Override
            public void run(String... args) {
                log.info("BeanCommandLineRunner run2()" + Arrays.toString(args));
           }
       };
   }
    @Bean
    @Order(3)
    public ApplicationRunner runner3() {
        return new ApplicationRunner() {
            @Override
            public void run(ApplicationArguments args) {
                log.info("BeanApplicationRunner run3()" + Arrays.toString(args.getSourceArgs()));
           }
       };
   }
}

四、执⾏测试

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

测试输出结果:

从测试结果上看

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

五、总结

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

6.类初始化监听

有些初始化动作,并不⼀定在应⽤初始化的时候进⾏,因为这个时候初始化,经常有些 Bean 还未形成对象,有些 properties 属性值还没完成注⼊,导致我们的初始化动作需要的⼀些必要条件没有准备好,所 谓的初始化也就⽆法正确进⾏。

我们经常使⽤的⼀些初始化动作,可以在 bean 进⾏初始化的时候进⾏,如下代码:

package top.syhan.boot.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @description: 类初始化监听
* @author: syhan
* @date: 2022-04-04
**/
@Component
@Slf4j
public class BeanInitListener implements InitializingBean {
    static {
        log.info("类初始化静态代码块");
   }
    public BeanInitListener() {
        log.info("类初始化构造⽅法");
   }
    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("类初始化 afterPropertiesSet ⽅法");
   }
    @PostConstruct
    void method() {
        log.info("类初始化 postConstruct 注解⽅法!");
   }
}

上⾯代码经常被我们使⽤到的是:

  • postConstruct 注解⽅法,该注解所注释的⽅法会在 Bean 对象构建完成之后去执⾏。
  • 实现 InitializingBean 接⼝的 afterPropertiesSet ⽅法,通过这个⽅法名也可以知道该⽅法是在属性被设置之后执⾏。

将上⾯的代码类放⼊⼀个 SpringBoot 应⽤,并启动应⽤,执⾏顺序(⽇志输出顺序)如下:

结论:

  • 静态代码块会在类加载器加载这个类时执⾏⼀次。
  • ⾮静态代码块会在这个类的构造⽅法被执⾏的时候执⾏,构造⽅法每被执⾏⼀次, ⾮静态代码块都会被执⾏⼀次(可以理解为⾮静态代码块的内容会被 copy 到构造⽅法内容的最前⾯)。
  • afterPropertiesSet ⽅法和被@PostConstruct ⽅法会在 BeanInitTester 实例被创建并且 BeanInitTester 类中的所有实例属性都被初始化之后执⾏. ⽽ afterPropertiesSet ⽅法会在被 @PostConstruct ⽅法标注的⽅法之后执⾏。
  • 顺序: 静态代码块->⾮静态代码块->构造⽅法->@PostConstruct ⽅法->afterPropertiesSet ⽅法。

文章作者: Syhan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Syhan !
评论
  目录