手写Spring-第七章-钩直饵咸?虚拟机钩子与bean的初始化和销毁

前言

上一次的练习中,我们主要实现了后置处理器,对bean定义以及bean进行了扩展。那个时候留下了一个坑,也就是bean的【初始化】,我们是没有内容的。那么这一次,我们就来填上这个坑。不仅如此,有开始,就有结束。所以,我们还要加上bean的销毁逻辑。对于没接触过的人来说,这块可能没有什么【实感】。你说初始化和销毁,我又没接触过,它到底可以完成哪些操作?其实不管是初始化还是销毁,它都是一个方法。方法的内容是我们自己来决定的,Spring只是留了这么个扩展点给我们。我们日常使用,自然可以不用去理会这些。但当我们想要做一些数据的加载执行、链接注册中心暴露RPC接口,又或者在web程序关闭的时候,执行链接断开、内存销毁等操作,这个初始化和销毁的扩展,就变得有用起来了。虽说即使没有这个扩展点,我们依然可以通过构造方法、静态代码块、甚至手动调用等方式,来完成这部分的内容。但是这些方式,终究不如交给Spring容器来处理更为合适,让Spring来管理bean的整个生命周期。

在标题中我们提到了【钩子】这个概念,看起来有些神秘。其实我们前面已经多次使用过了。还记得我们的抽象方法吗?只有一个方法的声明,但是没有方法的实现。但是依然可以去调用它。那么这里的抽象方法,其实就相当于一个钩子。它会被调用,但具体的内容是什么,是由其他人来决定的。就像是预先留下了一个钩子,可以往上面挂任何自己想要的东西一样。那么虚拟机钩子,其实也就是虚拟机留给我们的一个钩子,我们可以通过注册的方式,将自己想要执行的内容挂到钩子上。

项目结构

├─src
│  ├─main
│  │  ├─java
│  │  │  └─com
│  │  │      └─akitsuki
│  │  │          └─springframework
│  │  │              ├─beans
│  │  │              │  ├─exception
│  │  │              │  │      BeanException.java
│  │  │              │  │  
│  │  │              │  └─factory
│  │  │              │      │  BeanFactory.java
│  │  │              │      │  ConfigurableListableBeanFactory.java
│  │  │              │      │  DisposableBean.java
│  │  │              │      │  HierarchicalBeanFactory.java
│  │  │              │      │  InitializingBean.java
│  │  │              │      │  ListableBeanFactory.java
│  │  │              │      │  
│  │  │              │      ├─config
│  │  │              │      │      AutowireCapableBeanFactory.java
│  │  │              │      │      BeanDefinition.java
│  │  │              │      │      BeanFactoryPostProcessor.java
│  │  │              │      │      BeanPostProcessor.java
│  │  │              │      │      BeanReference.java
│  │  │              │      │      ConfigurableBeanFactory.java
│  │  │              │      │      DefaultSingletonBeanRegistry.java
│  │  │              │      │      PropertyValue.java
│  │  │              │      │      PropertyValues.java
│  │  │              │      │      SingletonBeanRegistry.java
│  │  │              │      │  
│  │  │              │      ├─support
│  │  │              │      │      AbstractAutowireCapableBeanFactory.java
│  │  │              │      │      AbstractBeanDefinitionReader.java
│  │  │              │      │      AbstractBeanFactory.java
│  │  │              │      │      BeanDefinitionReader.java
│  │  │              │      │      BeanDefinitionRegistry.java
│  │  │              │      │      CglibSubclassingInstantiationStrategy.java
│  │  │              │      │      DefaultListableBeanFactory.java
│  │  │              │      │      DisposableBeanAdapter.java
│  │  │              │      │      InstantiationStrategy.java
│  │  │              │      │      SimpleInstantiationStrategy.java
│  │  │              │      │  
│  │  │              │      └─xml
│  │  │              │              XmlBeanDefinitionReader.java
│  │  │              │  
│  │  │              ├─context
│  │  │              │  │  ApplicationContext.java
│  │  │              │  │  ConfigurableApplicationContext.java
│  │  │              │  │  
│  │  │              │  └─support
│  │  │              │          AbstractApplicationContext.java
│  │  │              │          AbstractRefreshableApplicationContext.java
│  │  │              │          AbstractXmlApplicationContext.java
│  │  │              │          ClasspathXmlApplicationContext.java
│  │  │              │  
│  │  │              ├─core
│  │  │              │  └─io
│  │  │              │          ClasspathResource.java
│  │  │              │          DefaultResourceLoader.java
│  │  │              │          FileSystemResource.java
│  │  │              │          Resource.java
│  │  │              │          ResourceLoader.java
│  │  │              │          UrlResource.java
│  │  │              │  
│  │  │              └─util
│  │  │                      ClassUtils.java
│  │  │          
│  │  └─resources
│  └─test
│      ├─java
│      │  └─com
│      │      └─akitsuki
│      │          └─springframework
│      │              └─test
│      │                  │  ApiTest.java
│      │                  │  
│      │                  └─bean
│      │                          UserDao.java
│      │                          UserService.java
│      │              
│      └─resources
│              spring.xml

这一次,目录没有什么变化,大多都是在之前的基础上进行修改的,可喜可贺可喜可贺。

我标记了一个敌人!为bean加上标记

所谓加标记,实际上也就是实现一个接口而已。对于需要实现初始化或者销毁方法的bean,我们可以通过实现两个接口的方式来进行。接口中有我们的初始化方法和销毁方法,bean可以通过实现这两个方法来完成自己的初始化和销毁。

package com.akitsuki.springframework.beans.factory;

/**
 * bean初始化接口
 * @author ziling.wang@hand-china.com
 * @date 2022/11/14 14:34
 */
public interface InitializingBean {

    /**
     * Bean处理完属性后调用
     * @throws Exception e
     */
    void afterPropertiesSet() throws Exception;
}

package com.akitsuki.springframework.beans.factory;

/**
 * 销毁bean接口
 * @author ziling.wang@hand-china.com
 * @date 2022/11/14 14:36
 */
public interface DisposableBean {

    /**
     * 销毁bean
     * @throws Exception
     */
    void destroy() throws Exception;
}

接口也都很好理解,就不多解释了。知道它们是用来给bean进行实现的就可以了。

再次扩充!bean定义

许久不动的bean定义,此刻再次迎来扩充。这次我们要加入的内容,自然是bean的初始/销毁方法名称。可能看到这里会有疑惑,我们上面明明刚加过标记,这里还要什么方法名称干什么。这样做的目的,是为了我们可以有多种方法来实现初始化和销毁。有些时候我们不想通过实现接口的方式,而是通过配置文件的方式来指定方法名称,那么这个时候,就需要有个地方来储存这些方法名称了。

package com.akitsuki.springframework.beans.factory.config;

import lombok.Getter;
import lombok.Setter;

/**
 * Bean的定义,包含bean的一些基本信息
 *
 * @author ziling.wang@hand-china.com
 * @date 2022/11/7 9:54
 */
@Getter
@Setter
public class BeanDefinition {

    /**
     * bean的class
     */
    private final Class<?> beanClass;

    /**
     * bean的属性和值
     */
    private PropertyValues propertyValues = new PropertyValues();

    /**
     * 初始化方法名称
     */
    private String initMethodName;

    /**
     * 销毁方法名称
     */
    private String destroyMethodName;

    public BeanDefinition(Class<?> beanClass) {
        this.beanClass = beanClass;
    }

    public BeanDefinition(Class<?> beanClass, PropertyValues propertyValues) {
        this.beanClass = beanClass;
        this.propertyValues = propertyValues;
    }
}

增加了两个String属性用来储存方法名,也很好理解。

填坑!完成未完的使命

这次开篇我们就说到了,要来填上一次bean初始化的坑。那么话不多说,先上代码。由于类的代码太长,这里就不放完整内容了,但要记得这里是 AbstractAutowireCapableBeanFactory

private void invokeInitMethod(String beanName, Object bean, BeanDefinition beanDefinition) throws Exception {
        //实现了接口InitializingBean的bean,调用接口方法
        if (bean instanceof InitializingBean) {
            ((InitializingBean) bean).afterPropertiesSet();
        }

        //配置了initMethod的bean,调用初始化方法
        String initMethodName = beanDefinition.getInitMethodName();
        if (StrUtil.isNotBlank(initMethodName)) {
            Method initMethod = beanDefinition.getBeanClass().getMethod(initMethodName);
            initMethod.invoke(bean);
        }
    }

让我看看!可以看到,这里分2步走,首先判断bean是否实现了InitializingBean接口,如果实现了,就调用afterPropertiesSet方法,执行初始化。接下来是看配置文件中是否配置了初始化方法名称,如果配置了,则利用反射来调用相应的方法。

有始有终!完成销毁方法

有开始,就有结束。有初始化,自然就会有销毁。那么这里,我们就来完善bean的销毁流程。首先,我们先来个适配器…等一下,为什么刚才初始化那么轻松写意,到了这里就要搞什么幺蛾子适配器了!因为和初始化不同,销毁方法我们是注册虚拟机钩子,在jvm停止的时候执行的。而我们实现销毁的方法可以有很多种,比如实现接口或者通过配置文件配置销毁方法名称等等。而我们在销毁的时候,希望有一个统一的接口来进行销毁(毕竟是让jvm来干活,需要遵守jvm的规定),于是就有了这么个适配器。

package com.akitsuki.springframework.beans.factory.support;

import cn.hutool.core.util.StrUtil;
import com.akitsuki.springframework.beans.factory.DisposableBean;
import com.akitsuki.springframework.beans.factory.config.BeanDefinition;
import lombok.AllArgsConstructor;

import java.lang.reflect.Method;

/**
 * bean销毁方法适配器
 * @author ziling.wang@hand-china.com
 * @date 2022/11/14 14:57
 */
@AllArgsConstructor
public class DisposableBeanAdapter implements DisposableBean {

    private final Object bean;
    private final String beanName;
    private final BeanDefinition beanDefinition;

    @Override
    public void destroy() throws Exception {
        //实现了DisposableBean接口
        if (bean instanceof DisposableBean) {
            ((DisposableBean) bean).destroy();
        }

        //配置了destroy-method
        if (StrUtil.isNotBlank(beanDefinition.getDestroyMethodName()) &&
                !(bean instanceof DisposableBean && "destroy".equals(beanDefinition.getDestroyMethodName()))) {
            Method destroyMethod = beanDefinition.getBeanClass().getMethod(beanDefinition.getDestroyMethodName());
            destroyMethod.invoke(bean);
        }
    }
}

可以看到,这里实际的实现和初始化是类似的,只不过在判断的时候复杂了一些。这样做是为了避免执行多次销毁。

有了适配器,我们要在哪里用呢?有句话这样说,在哪里开始,就在哪里结束。所以,我们自然是在 AbstractAutowireCapableBeanFactory中来进行。

    @Override
    protected Object createBean(String beanName, BeanDefinition beanDefinition, Object[] args) throws BeanException {
        Object bean;
        try {
            bean = createBeanInstance(beanDefinition, beanName, args);
            //设置bean属性
            applyPropertyValues(beanName, beanDefinition, bean);
            //初始化bean,执行beanPostProcessor的前置和后置方法
            initializeBean(beanName, bean, beanDefinition);
            //注册实现了DisposableBean接口的对象
            registerDisposableBeanIfNecessary(beanName, bean, beanDefinition);
            //创建好的单例bean,放入缓存
            addSingleton(beanName, bean);
        } catch (Exception e) {
            throw new BeanException("创建bean失败", e);
        }
        return bean;
    }



    /**
     * 在需要的情况下,注册销毁方法
     * @param beanName
     * @param bean
     * @param beanDefinition
     */
    protected void registerDisposableBeanIfNecessary(String beanName, Object bean, BeanDefinition beanDefinition) {
        if (bean instanceof DisposableBean || StrUtil.isNotBlank(beanDefinition.getDestroyMethodName())) {
            registerDisposableBean(beanName, new DisposableBeanAdapter(bean, beanName, beanDefinition));
        }
    }

首先我们可以看到,我们修改了创建bean的流程,在其中加入了销毁bean的注册操作。而具体的注册内容,则是在 registerDisposableBeanIfNecessary方法中实现。可以看到方法中调用了 registerDisposableBean方法来完成注册。而这个没见过的方法,是新增在许久没有动过的单例bean注册接口 SingletonBeanRegistry中的。还有一点需要注意,这里传输过去的是我们上面所创建的适配器,也就意味着我们可以对销毁进行统一的处理了。

package com.akitsuki.springframework.beans.factory.config;

import com.akitsuki.springframework.beans.factory.DisposableBean;

/**
 * 单例Bean注册接口
 *
 * @author ziling.wang@hand-china.com
 * @date 2022/11/7 9:56
 */
public interface SingletonBeanRegistry {

    /**
     * 获取单例Bean
     *
     * @param beanName bean的名称
     * @return 单例bean
     */
    Object getSingleton(String beanName);

    /**
     * 添加一个单例Bean
     *
     * @param beanName bean的名称
     * @param bean     单例bean
     */
    void addSingleton(String beanName, Object bean);

    /**
     *注册可销毁的bean
     * @param beanName
     * @param bean
     */
    void registerDisposableBean(String beanName, DisposableBean bean);
}

既然这里有了需求,自然就要有相应的实现。我们再看看这个注册接口的默认实现类 DefaultSingletonBeanRegistry是如何处理的。

package com.akitsuki.springframework.beans.factory.config;

import com.akitsuki.springframework.beans.exception.BeanException;
import com.akitsuki.springframework.beans.factory.DisposableBean;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 默认的单例Bean注册获取实现
 *
 * @author ziling.wang@hand-china.com
 * @date 2022/11/7 9:58
 */
public class DefaultSingletonBeanRegistry implements SingletonBeanRegistry {

    private final Map<String, Object> singletonBeans = new HashMap<>();

    private final Map<String, DisposableBean> disposableBeans = new HashMap<>();

    @Override
    public Object getSingleton(String beanName) {
        return singletonBeans.get(beanName);
    }

    @Override
    public void addSingleton(String beanName, Object singletonBean) {
        singletonBeans.put(beanName, singletonBean);
    }

    @Override
    public void registerDisposableBean(String beanName, DisposableBean bean) {
        disposableBeans.put(beanName, bean);
    }

    /**
     * 销毁单例对象
     */
    public void destroySingletons() {
        String[] keys = disposableBeans.keySet().toArray(new String[0]);
        for (String key: keys) {
            DisposableBean bean = disposableBeans.remove(key);
            try {
                bean.destroy();
            } catch (Exception e) {
                throw new BeanException("exception on destroy bean " + key, e);
            }
        }
    }
}

唔,一下子好像多出来不少东西呢。不过针对销毁bean的注册则是很简单,首先我们多出来一个map用来维护,方法的内容则只是将需要注册的bean放入map中。

而最下面则多出来一个方法:销毁单例bean。这个方法本不在我们的介绍范围内,但它出现在这里,所以我们一并介绍了。很简单,这里的方法作用是销毁一个单例的bean。怎么操作呢?其实就是迭代上面的map,先 remove掉,再调用 destroy方法。还记得我们之前传过来的是什么吗?对,是适配器,所以这里我们就可以不用管那些千奇百怪的实现方式,统统调用 destroy方法即可。

但这个方法果然很奇怪。事到如今写了这么多的demo了,很少有一个单独的public方法,就这么放在这里。甚至有些不习惯了,总觉得它应该实现某个接口或者抽象类的方法才是。事实上的确如此,但它有些复杂,我们下面再详细的去分析。

虚拟机,你的钩子在哪里!

我们上面说了那么多,销毁方法终究还是要用jvm钩子交给虚拟机来执行的。那么这个动作我们定义在哪里呢?ConfigurableApplicationContext

package com.akitsuki.springframework.context;

import com.akitsuki.springframework.beans.exception.BeanException;

/**
 * @author ziling.wang@hand-china.com
 * @date 2022/11/10 14:15
 */
public interface ConfigurableApplicationContext extends ApplicationContext {

    /**
     * 刷新容器
     *
     * @throws BeanException e
     */
    void refresh() throws BeanException;

    /**
     * 注册关闭的虚拟机钩子
     */
    void registerShutdownHook();

    /**
     * 手动执行关闭
     */
    void close();
}

我们来看这两个新增的销毁方法是如何实现的。这里同样因为太长的缘故,所以只展示部分代码。所属的类是 AbstractApplicationContext

@Override
    public void registerShutdownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(this::close));
    }

    @Override
    public void close() {
        getBeanFactory().destroySingletons();
    }

这里可以看到,我们注册虚拟机钩子的方式很简单,Runtime.getRuntime().addShutdownHook()。但其实涉及到的知识点却不少。这里要求传入一个线程,所以我们新建了一个线程。但是传参:this::close却有些令人费解。我们去查看Thread的源码可以知道,这里需要传入一个Runnable对象。但这里直接传this::close是什么意思呢?这其实是一种语法糖,它等效于下面这种写法:

    @Override
    public void registerShutdownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                close();
            }
        }));
    }

但其实又不太一样。这里的方法选择并不是随意的。它必须和 Runnable接口的 run方法一样,是无参的方法。至于有没有返回值,倒是无所谓,因为run方法的返回值是void,不管我们的方法有没有返回值,都无所谓,就算有,也会被忽略。所以其实上面的这种等效写法并不完全。我认为这个语法糖的本意应该是:将我们传入的方法引用,作为这里的run方法来使用。而不是说,让run方法再来调用我们传入的方法。当然,这里只是我个人的观点,可以作为参考。

还有这里的 close方法,其实也值得玩味。可以看到,它调用的方法是beanFactory中的 destroySingletons方法。这个方法我们在上面其实也讨论过,它没有实现接口方法或者抽象方法,就是孤零零的一个公共方法。这在我们前面的练习中,是极为少见的情况。更重要的是,它也没有在beanFactory中,而是属于单例bean注册类的。那么这里的方法来源是什么呢?其实它声明在了 ConfugurableBeanFactory接口中。

package com.akitsuki.springframework.beans.factory.config;

import com.akitsuki.springframework.beans.factory.HierarchicalBeanFactory;

/**
 * 可获取 BeanPostProcessor、BeanClassLoader等的一个配置化接口
 *
 * @author ziling.wang@hand-china.com
 * @date 2022/11/10 14:54
 */
public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {

    String SCOPE_SINGLETON = "singleton";

    String SCOPE_PROTOTYPE = "prototype";

    /**
     * 添加一个processor
     *
     * @param processor processor
     */
    void addBeanPostProcessor(BeanPostProcessor processor);

    /**
     * 销毁单例对象
     */
    void destroySingletons();
}

那么既然声明在了这里,应该由谁来实现呢?自然是直接实现这个接口的类:AbstractBeanFactory。因为销毁单例bean是一个偏通用的操作,所以应该由比较通用的类来实现。但我们如果去类里面查看,会发现其实并没有关于这个方法的实现。绕来绕去是不是绕晕了?我们还要注意的是,AbstractBeanFactory是继承了 DefaultSingletonBeanRegistry的,这也就意味着,它也会继承到 destroySingletons这个方法。所以我们捋一下,这是个什么操作呢?

  1. ConfigurableBeanFactory接口声明了destroySingletons方法;
  2. AbstractBeanFactory实现了这个接口,需要实现这个方法;
  3. DefaultSingletonBeanRegistry中实现了destroySingletons方法;
  4. AbstractBeanFactory通过继承,获得了这个方法,正好可以让自己实现这个接口方法。

总之,就是这么一套看起来有些奇怪的流程。我自己也暂时没有搞明白,为什么要以这种方式来实现。在这里也只能介绍一下来龙去脉。

配置文件新变化!

我们上面也说到,关于初始化和销毁有多种实现方式。目前我们实现的一种是实现接口,一种则是配置文件。但我们好像并没有加入相关内容的解析>_<!可以预想到的是,我们如果就像这样去测试,加到配置文件里面的内容,肯定毫无作用。所以,我们要在这里,加入对初始化方法名和销毁方法名的解析处理。以及,不要忘了我们的xml处理方法在哪个类里:XmlBeanDefinitionReader


    /**
     * 真正通过xml读取bean定义的方法实现
     *
     * @param inputStream xml配置文件输入流
     * @throws BeanException          e
     * @throws ClassNotFoundException e
     */
    private void doLoadBeanDefinitions(InputStream inputStream) throws BeanException, ClassNotFoundException {
        Document doc = XmlUtil.readXML(inputStream);
        Element root = doc.getDocumentElement();
        NodeList childNodes = root.getChildNodes();

        for (int i = 0; i < childNodes.getLength(); i++) {
            //如果不是bean,则跳过
            if (!isBean(childNodes.item(i))) {
                continue;
            }
            // 解析标签
            Element bean = (Element) childNodes.item(i);
            String id = bean.getAttribute("id");
            String name = bean.getAttribute("name");
            String className = bean.getAttribute("class");
            String initMethodName = bean.getAttribute("init-method");
            String destroyMethodName = bean.getAttribute("destroy-method");
            // 获取 Class,方便获取类中的名称
            Class<?> clazz = Class.forName(className);
            // 优先级 id > name
            String beanName = StrUtil.isNotEmpty(id) ? id : name;
            if (StrUtil.isEmpty(beanName)) {
                beanName = StrUtil.lowerFirst(clazz.getSimpleName());
            }
            // 定义Bean
            BeanDefinition beanDefinition = new BeanDefinition(clazz);
            beanDefinition.setInitMethodName(initMethodName);
            beanDefinition.setDestroyMethodName(destroyMethodName);
            // 读取属性并填充
            buildProperty(bean, beanDefinition);
            if (getRegistry().containsBeanDefinition(beanName)) {
                throw new BeanException("Duplicate beanName[" + beanName + "] is not allowed");
            }
            // 注册 BeanDefinition
            getRegistry().registerBeanDefinition(beanName, beanDefinition);
        }
    }

可以看到,处理也比较简单,在原来bean标签下的id、name、classs三个属性中,又增加了init-method和destroy-method两个属性,很好理解。最后将其填入bean定义中的属性即可。

钩直饵咸,愿者上钩。开始测试!

这一期的花样还真是不少,有很多难以理解的部分(至少对于我来说是这样)。所以如果觉得有些地方理解的不够透彻,可以试着停下来想一想,像我一样慢慢思考,写一些自己的总结,我觉得是有助于理解和掌握知识点的。

既然这一次的主题是初始化和销毁,那么我们的bean,自然也要加上这两块。

package com.akitsuki.springframework.test.bean;

import java.util.HashMap;
import java.util.Map;

/**
 * @author ziling.wang@hand-china.com
 * @date 2022/11/8 14:42
 */
public class UserDao {

    private static final Map<Long, String> userMap = new HashMap<>();

    public void initMethod() {
        System.out.println("执行UserDao的initMethod");
        userMap.put(1L, "akitsuki");
        userMap.put(2L, "toyosaki");
        userMap.put(3L, "kugimiya");
        userMap.put(4L, "hanazawa");
        userMap.put(5L, "momonogi");
    }

    public void destroyMethod() {
        System.out.println("执行UserDao的destroyMethod");
        userMap.clear();
    }

    public String queryUserName(Long id) {
        return userMap.get(id);
    }
}

package com.akitsuki.springframework.test.bean;

import com.akitsuki.springframework.beans.factory.DisposableBean;
import com.akitsuki.springframework.beans.factory.InitializingBean;
import lombok.Setter;

/**
 * @author ziling.wang@hand-china.com
 * @date 2022/11/8 14:42
 */
@Setter
public class UserService implements InitializingBean, DisposableBean {

    private String dummyString;

    private int dummyInt;

    private UserDao userDao;

    public void queryUserInfo(Long id) {
        System.out.println("dummyString:" + dummyString);
        System.out.println("dummyInt:" + dummyInt);
        String userName = userDao.queryUserName(id);
        if (null == userName) {
            System.out.println("用户未找到>_<");
        } else {
            System.out.println("用户名:" + userDao.queryUserName(id));
        }
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("userService的destroy执行了");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("userService的afterPropertiesSet执行了");
    }
}

可以看到我们的两个bean,分别用不同的方式来实现。UserDao采用配置文件的方式,UserService则采用实现接口的方式。下面我们来看看配置文件的变化。

<?xml version="1.0" encoding="utf-8" ?>
<beans>
    <bean id="userDao" class="com.akitsuki.springframework.test.bean.UserDao" init-method="initMethod" destroy-method="destroyMethod"/>

    <bean id="userService" class="com.akitsuki.springframework.test.bean.UserService">
        <property name="dummyString" value="dummy"/>
        <property name="dummyInt" value="114514"/>
        <property name="userDao" ref="userDao"/>
    </bean>
</beans>

可以看到,userDao的bean配置中,加入了初始化方法和销毁方法,而userService的配置则没有变化。

下面是主要测试类

package com.akitsuki.springframework.test;

import com.akitsuki.springframework.context.ApplicationContext;
import com.akitsuki.springframework.context.support.ClasspathXmlApplicationContext;
import com.akitsuki.springframework.test.bean.UserService;
import org.junit.Test;

/**
 * @author ziling.wang@hand-china.com
 * @date 2022/11/15 13:58
 */
public class ApiTest {

    @Test
    public void test() {
        //初始化BeanFactory
        ClasspathXmlApplicationContext context = new ClasspathXmlApplicationContext("classpath:spring.xml");
        context.registerShutdownHook();

        //获取bean,测试
        UserService userService = context.getBean("userService", UserService.class);
        userService.queryUserInfo(1L);
        userService.queryUserInfo(4L);
        userService.queryUserInfo(114L);
    }
}

测试结果

执行UserDao的initMethod
userService的afterPropertiesSet执行了
dummyString:dummy
dummyInt:114514
用户名:akitsuki
dummyString:dummy
dummyInt:114514
用户名:hanazawa
dummyString:dummy
dummyInt:114514
用户未找到>_<
执行UserDao的destroyMethod
userService的destroy执行了

Process finished with exit code 0

可以看到和上次基本一致,这次主要多了一个注册虚拟机关闭钩子的操作。可以看到我们的初始化方法和销毁方法都好好的被执行了,证明我们这一次的练习也顺利完成!敬请期待下一期。

相关源码可以参考我的gitee:https://gitee.com/akitsuki-kouzou/mini-spring,这里对应的代码是mini-spring-07

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值