2023-02-18  阅读(1)
原文作者: chen_hao 原文地址:https://www.cnblogs.com/java-chen-hao/p/11850862.html

在我们用 springboot 搭建项目的时候,有时候会碰到在项目启动时初始化一些操作的需求 ,针对这种需求 spring boot为我们提供了以下几种方案供我们选择:

  • ApplicationRunnerCommandLineRunner接口
  • Spring容器初始化时InitializingBean接口和@PostConstruct
  • Spring的事件机制

ApplicationRunner与CommandLineRunner

我们可以实现 ApplicationRunnerCommandLineRunner接口, 这两个接口工作方式相同,都只提供单一的run方法,该方法在SpringApplication.run(…)完成之前调用,不知道大家还对我上一篇文章结尾有没有印象,我们先来看看这两个接口

    public interface ApplicationRunner {
        void  run (ApplicationArguments var1) throws Exception;
    }
    
    public interface CommandLineRunner {
        void  run (String... var1) throws Exception;
    }

都只提供单一的run方法,接下来我们来看看具体的使用

ApplicationRunner

构造一个类实现ApplicationRunner接口

     //需要加入到Spring容器中
    @Component
     public class ApplicationRunnerTest implements ApplicationRunner {
    
        @Override
        public void  run (ApplicationArguments args) throws Exception {
            System.out.println("ApplicationRunner");
        }
    }

很简单,首先要使用 @Component 将实现类加入到Spring容器中,为什么要这样做我们待会再看,然后实现其run方法实现自己的初始化数据逻辑就可以了

CommandLineRunner

对于这两个接口而言,我们可以通过Order注解或者使用Ordered接口来指定调用顺序, @Order()中的值越小,优先级越高

     //需要加入到Spring容器中
      @Component 
    @Order(1)
    public class CommandLineRunnerTest implements CommandLineRunner {
    
        @Override
        public void run(String... args) throws Exception {
            System.out.println("CommandLineRunner...");
        }
    }

同样需要加入到Spring容器中,CommandLineRunner的参数是最原始的参数,没有进行任何处理,ApplicationRunner的参数是ApplicationArguments,是对原始参数的进一步封装

源码分析

大家回顾一下我上一篇文章,也就是SpringApplication.run方法的最后一步第八步:执行Runners,这里我直接把代码复制过来

    private void callRunners(ApplicationContext context, ApplicationArguments args) {
        List<Object> runners = new ArrayList<Object>();
         //获取容器中所有的ApplicationRunner的Bean实例
        runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
        //获取容器中所有的CommandLineRunner的Bean实例
        runners.addAll(context.getBeansOfType(CommandLineRunner.class  ).values()); 
        AnnotationAwareOrderComparator.sort(runners);
        for (Object runner : new LinkedHashSet<Object>(runners)) {
            if (runner instanceof ApplicationRunner) {
                 //执行ApplicationRunner的run方法
                  callRunner((ApplicationRunner) runner, args); 
            }
            if (runner instanceof CommandLineRunner) {
                 //执行CommandLineRunner的run方法
                  callRunner((CommandLineRunner) runner, args); 
            }
        }
    }

很明显,是直接从Spring容器中获取 ApplicationRunner和 CommandLineRunner的实例, 并调用其run方法,这也就是为什么我要使用 @Component将 ApplicationRunner和 CommandLineRunner接口的实现类加入到Spring容器中了。

InitializingBean

在spring初始化bean的时候,如果bean实现了 InitializingBean接口,在对象的所有属性被初始化后之后才会调用afterPropertiesSet()方法

     @Component
     public class InitialingzingBeanTest  implements   InitializingBean  {
    
        @Override
        public void  afterPropertiesSet()  throws Exception {
            System.out.println("InitializingBean..");
        }
    }

我们可以看出spring初始化bean肯定会在 ApplicationRunner和CommandLineRunner接口调用之前。

@PostConstruct

     @Component
     public class PostConstructTest {
    
         @PostConstruct
         public void postConstruct() {
            System.out.println("init...");
        }
    }

我们可以看到,只用在方法上添加 @PostConstruct注解, 并将类注入到Spring容器中就可以了。我们来看看 @PostConstruct注解的方法是何时执行的

在Spring初始化bean时,对bean的实例赋值时,populateBean方法下面有一个initializeBean(beanName, exposedObject, mbd)方法,这个就是用来执行用户设定的初始化操作。我们看下方法体:

    protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
        if (System.getSecurityManager() != null) {
            AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
                // 激活 Aware 方法
                invokeAwareMethods(beanName, bean);
                return null;
            }, getAccessControlContext());
        }
        else {
            // 对特殊的 bean 处理:Aware、BeanClassLoaderAware、BeanFactoryAware
            invokeAwareMethods(beanName, bean);
        }
    
        Object wrappedBean = bean;
        if (mbd == null || !mbd.isSynthetic()) {
             // 后处理器
            wrappedBean =   applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); 
        }
    
        try {
             // 激活用户自定义的 init 方法
              invokeInitMethods(beanName, wrappedBean, mbd); 
        }
        catch (Throwable ex) {
            throw new BeanCreationException(
                    (mbd != null ? mbd.getResourceDescription() : null),
                    beanName, "Invocation of init method failed", ex);
        }
        if (mbd == null || !mbd.isSynthetic()) {
             // 后处理器
            wrappedBean =   applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); 
        }
        return wrappedBean;
    }

我们看到会先执行 后处理器然后执行 invokeInitMethods方法 ,我们来看下applyBeanPostProcessorsBeforeInitialization

    public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)  
            throws BeansException {  
    
        Object result = existingBean;  
         for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {  
            result =   beanProcessor.postProcessBeforeInitialization(result, beanName);   
            if (result == null) {  
                return result;  
            }  
        }  
        return result;  
    }
    
    public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)  
            throws BeansException {  
    
        Object result = existingBean;  
        for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {  
            result = beanProcessor.postProcessAfterInitialization(result, beanName);  
            if (result == null) {  
                return result;  
            }  
        }  
        return result;  
    }

获取容器中所有的后置处理器,循环调用后置处理器的 postProcessBeforeInitialization方法,这里我们来看一个BeanPostProcessor

    public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBeanPostProcessor implements InstantiationAwareBeanPostProcessor, BeanFactoryAware, Serializable {
        public CommonAnnotationBeanPostProcessor() {
            this.setOrder(2147483644);
             //设置初始化参数为PostConstruct.class
            this.setInitAnnotationType(PostConstruct.class);
             this.setDestroyAnnotationType(PreDestroy.class);
            this.ignoreResourceType("javax.xml.ws.WebServiceContext");
        }
        //略...
    }

在构造器中设置了一个属性为 PostConstruct.class, 再次观察CommonAnnotationBeanPostProcessor这个类,它继承自InitDestroyAnnotationBeanPostProcessor。InitDestroyAnnotationBeanPostProcessor顾名思义,就是在Bean初始化和销毁的时候所作的一个前置/后置处理器。查看InitDestroyAnnotationBeanPostProcessor类下的postProcessBeforeInitialization方法:

    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {  
        LifecycleMetadata metadata   =   findLifecycleMetadata(bean.getClass());   
       try {  
            metadata.invokeInitMethods(bean, beanName);   
       }  
       catch (InvocationTargetException ex) {  
           throw new BeanCreationException(beanName, "Invocation of init method failed", ex.getTargetException());  
       }  
       catch (Throwable ex) {  
           throw new BeanCreationException(beanName, "Couldn't invoke init method", ex);  
       }  
        return bean;  
    }  
    
    private LifecycleMetadata buildLifecycleMetadata(final Class clazz) {  
           final LifecycleMetadata newMetadata = new LifecycleMetadata();  
           final boolean debug = logger.isDebugEnabled();  
           ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() {  
               public void doWith(Method method) {  
                  if (initAnnotationType != null) {  
                        //判断clazz中的methon是否有initAnnotationType注解,也就是PostConstruct.class注解
                      if (method.getAnnotation(initAnnotationType) != null) {  
                         //如果有就将方法添加进LifecycleMetadata中
                           newMetadata.addInitMethod(method);   
                         if (debug) {  
                             logger.debug("Found init method on class [" + clazz.getName() + "]: " + method);  
                         }  
                      }  
                  }  
                  if (destroyAnnotationType != null) {  
                        //判断clazz中的methon是否有destroyAnnotationType注解
                      if (method.getAnnotation(destroyAnnotationType) != null) {  
                         newMetadata.addDestroyMethod(method);  
                         if (debug) {  
                             logger.debug("Found destroy method on class [" + clazz.getName() + "]: " + method);  
                         }  
                      }  
                  }  
               }  
           });  
           return newMetadata;  
    }

在这里会去判断某方法是否有 PostConstruct.class注解 ,如果有,则添加到init/destroy队列中,后续一一执行。 @PostConstruct注解的方法会在此时执行,我们接着来看invokeInitMethods

    protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
            throws Throwable {
    
         // 是否实现 InitializingBean
        // 如果实现了 InitializingBean 接口,则只掉调用bean的 afterPropertiesSet()
        boolean isInitializingBean = (bean instanceof InitializingBean);
         if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
            if (logger.isDebugEnabled()) {
                logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
            }
            if (System.getSecurityManager() != null) {
                try {
                    AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
                        ((InitializingBean) bean).afterPropertiesSet();
                        return null;
                    }, getAccessControlContext());
                }
                catch (PrivilegedActionException pae) {
                    throw pae.getException();
                }
            }
            else {
                 // 直接调用 afterPropertiesSet()
                  ((InitializingBean) bean).afterPropertiesSet(); 
            }
        }
    
        if (mbd != null && bean.getClass() != NullBean.class) {
            // 判断是否指定了 init-method(),
            // 如果指定了 init-method(),则再调用制定的init-method
            String initMethodName = mbd.getInitMethodName();
            if (StringUtils.hasLength(initMethodName) &&
                    !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
                    !mbd.isExternallyManagedInitMethod(initMethodName)) {
                // 利用反射机制执行
                invokeCustomInitMethod(beanName, bean, mbd);
            }
        }
    }

首先检测当前 bean 是否实现了 InitializingBean 接口,如果实现了则调用其 afterPropertiesSet(),然后再检查是否也指定了 init-method(),如果指定了则通过反射机制调用指定的 init-method()

我们也可以发现 @PostConstruct会在实现 InitializingBean 接口的afterPropertiesSet()方法之前执行

Spring的事件机制

基础概念

Spring的事件驱动模型由三部分组成

  • 事件: ApplicationEvent,继承自JDK的 EventObject,所有事件都要继承它,也就是被观察者
  • 事件发布者: ApplicationEventPublisherApplicationEventMulticaster接口,使用这个接口,就可以发布事件了
  • 事件监听者: ApplicationListener,继承JDK的 EventListener,所有监听者都继承它,也就是我们所说的观察者,当然我们也可以使用注解 @EventListener,效果是一样的

事件

在Spring框架中,默认对ApplicationEvent事件提供了如下支持:

  • ContextStartedEvent:ApplicationContext启动后触发的事件
  • ContextStoppedEvent:ApplicationContext停止后触发的事件
  • ContextRefreshedEvent: ApplicationContext初始化或刷新完成后触发的事件 ;(容器初始化完成后调用,所以我们可以利用这个事件做一些初始化操作)
  • ContextClosedEvent:ApplicationContext关闭后触发的事件;(如 web 容器关闭时自动会触发spring容器的关闭,如果是普通 java 应用,需要调用ctx.registerShutdownHook();注册虚拟机关闭时的钩子才行)

202302182259234761.png

构造一个类继承ApplicationEvent

    public class TestEvent extends ApplicationEvent {
    
        private String message;
        
        public TestEvent(Object source) {
            super(source);
        }
    
        public void getMessage() {
            System.out.println(message);
        }
    
        public void setMessage(String message) {
            this.message = message;
        }
    
    }

创建事件监听者

有两种方法可以创建监听者,一种是直接实现ApplicationListener的接口,一种是使用注解 @EventListener注解是添加在监听方法上的 ,下面的例子是直接实现的接口

    @Component
    public class ApplicationListenerTest  implements ApplicationListener <TestEvent> {
        @Override
        public void onApplicationEvent(TestEvent testEvent) {
            testEvent.getMessage();
        }
    }

事件发布

对于事件发布,代表者是 ApplicationEventPublisherApplicationEventMulticaster,ApplicationContext接口继承了ApplicationEventPublisher,并在AbstractApplicationContext实现了具体代码,实际执行是委托给ApplicationEventMulticaster(可以认为是多播)

下面是一个事件发布者的测试实例:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class EventTest {
        @Autowired
        private ApplicationContext applicationContext;
    
        @Test
        public void publishTest() {
            TestEvent testEvent = new TestEvent("");
            testEvent.setMessage("hello world");
             applicationContext.publishEvent(testEvent); 
        }
    }

利用ContextRefreshedEvent事件进行初始化操作

利用 ContextRefreshedEvent事件进行初始化,该事件是 ApplicationContext初始化完成后调用的事件,所以我们可以利用这个事件,对应实现一个 监听器 ,在其 onApplicationEvent()方法里初始化操作

     @Component
     public class ApplicationListenerTest implements  ApplicationListener<ContextRefreshedEvent>  {
    
        @Override
        public void onApplicationEvent(ContextRefreshedEvent event) {
            System.out.println("容器刷新完成后,我被调用了..");
        }
    }

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文