上一次的练习中,我们主要实现了后置处理器,对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可以通过实现这两个方法来完成自己的初始化和销毁。
package com.akitsuki.springframework.beans.factory;
/**
* bean初始化接口
* @author [email protected]
* @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 [email protected]
* @date 2022/11/14 14:36
*/
public interface DisposableBean {
/**
* 销毁bean
* @throws Exception
*/
void destroy() throws Exception;
}
接口也都很好理解,就不多解释了。知道它们是用来给bean进行实现的就可以了。
许久不动的bean定义,此刻再次迎来扩充。这次我们要加入的内容,自然是bean的初始/销毁方法名称。可能看到这里会有疑惑,我们上面明明刚加过标记,这里还要什么方法名称干什么。这样做的目的,是为了我们可以有多种方法来实现初始化和销毁。有些时候我们不想通过实现接口的方式,而是通过配置文件的方式来指定方法名称,那么这个时候,就需要有个地方来储存这些方法名称了。
package com.akitsuki.springframework.beans.factory.config;
import lombok.Getter;
import lombok.Setter;
/**
* Bean的定义,包含bean的一些基本信息
*
* @author [email protected]
* @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 [email protected]
* @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 [email protected]
* @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 [email protected]
* @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 [email protected]
* @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 [email protected]
* @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
这个方法。所以我们捋一下,这是个什么操作呢?
总之,就是这么一套看起来有些奇怪的流程。我自己也暂时没有搞明白,为什么要以这种方式来实现。在这里也只能介绍一下来龙去脉。
我们上面也说到,关于初始化和销毁有多种实现方式。目前我们实现的一种是实现接口,一种则是配置文件。但我们好像并没有加入相关内容的解析>_<!可以预想到的是,我们如果就像这样去测试,加到配置文件里面的内容,肯定毫无作用。所以,我们要在这里,加入对初始化方法名和销毁方法名的解析处理。以及,不要忘了我们的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 [email protected]
* @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 [email protected]
* @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则采用实现接口的方式。下面我们来看看配置文件的变化。
<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 [email protected]
* @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