1. 前言
上一节我们讲了环境变量的解析,执行的是字段注入的流程,但这并不是通常意义上的依赖注入,关键在于缺少了筛选的过程。换句话说,环境变量的解析只是字段注入的特殊情况,注入的值来自环境变量而不是 Spring 容器所管理的单例。
为了便于理解,我们可以把依赖解析的过程看作是一场招聘会。Spring 容器作为组织者,发布了大量工作岗位,容器中的所有单例都尝试投递简历。每个工作岗位的基本要求是符合指定的类型,只有一部分单例可以通过初轮筛选,进入面试环节。面试的要求会更加具体,最终只有一个优胜者的依赖项。为了更好地描述不同阶段的 Bean,我们引入以下两个概念:
- 候选项(candidate):满足最基本的筛选条件,可能有多个对象入围
- 合格项(qualifier):经过综合考量,选出最符合条件的一个对象
2. 依赖分类
2.1 概述
Spring 将依赖项分为两类,首先是预先设置的依赖项。比如 BeanFactory
、ApplicationContext
、ResourceLoader
、ApplicationEventPublisher
等,它们实际上是 Spring 容器本身,只是以不同的形式呈现罢了。在 BeanFactory
的实例创建之后,手动注册这些依赖项,当执行依赖解析的操作时,优先查找预设的依赖项,如果找不到,则进一步寻找容器托管的单例。
其次是由 Spring 容器管理的单例,又可以分为两种。一是由外界创建的对象,比较典型的是表示环境变量的 Environment
对象。二是容器创建的对象,通过 BeanDefinition
的方式实现,这是最常见的依赖项的来源。
2.2 预设依赖
ConfigurableBeanFactory
接口定义了 registerResolvableDependency
方法,作用是将某个对象作为指定类型的依赖项,也就是预设依赖项。
public interface ConfigurableBeanFactory {
void registerResolvableDependency(Class<?> dependencyType, Object autowiredValue);
}
DefaultListableBeanFactory
持有 resolvableDependencies
字段来存储指定的依赖项,这些依赖项都是唯一的,不存在多个候选项,因此使用 Map
存储,一个萝卜一个坑。预设依赖项的数量不会太多,初始容量是 16。
public class DefaultListableBeanFactory {
private final Map<Class<?>, Object> resolvableDependencies = new ConcurrentHashMap<>(16);
@Override
public void registerResolvableDependency(Class<?> dependencyType, Object autowiredValue) {
if (!dependencyType.isInstance(autowiredValue)) {
throw new IllegalArgumentException(autowiredValue + "不是依赖类型[" + dependencyType.getName() + "]的实例");
}
this.resolvableDependencies.put(dependencyType, autowiredValue);
}
}
3. 单一类型依赖
3.1 主流程
回到 DefaultListableBeanFactory
的 doResolveDependency
方法,第二步是集合类型依赖,这个回头再讲,先跳过。第三步是依赖解析流程的关键所在,单一类型依赖的解析过程可以分为三步,如下所示:
- 获取符合条件的候选项列表(筛选简历环节)
- 如果存在多个候选项,需要找出最适合的那个(面试环节)
- 得到最终的依赖项后,需要确保对应的实例存在,可能会触发新的获取 Bean 流程
//所属类[cn.stimd.spring.beans.factory.support.DefaultListableBeanFactory]
//依赖解析
private Object doResolveDependency(DependencyDescriptor descriptor, String beanName) {
//1. 环境变量解析(略)
//2. 集合类型依赖(TODO)
//3. 单一对象依赖
//3.1 寻找依赖的候选项列表
Map<String, Object> matchingBeans = findAutowireCandidates(descriptor.getDependencyType(), descriptor);
//3.2 获得唯一的候选项
String autowiredBeanName;
Object instanceCandidate;
if (matchingBeans.size() > 1) {
//存在多个候选项,进一步判断找出唯一的候选项
autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
if (autowiredBeanName == null && this.autowireCandidateResolver.isRequired(descriptor)) {
throw new RuntimeException("依赖类型为" + descriptor.getDependencyType().getName() + "的候选项不是唯一的");
}
instanceCandidate = matchingBeans.get(autowiredBeanName);
}else{
Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
autowiredBeanName = entry.getKey();
instanceCandidate = entry.getValue();
}
//3.3 获取依赖项实例
Object result;
if(instanceCandidate instanceof Class){
//如果依赖项是Class类型,则创建Bean实例
result = getBean(autowiredBeanName);
}else {
result = instanceCandidate;
}
return result;
}
3.2 寻找候选项列表
findAutowireCandidates
方法的作用是返回一组符合条件的候选项,可以视为筛选简历的环节。findAutowireCandidates
方法可以分为两步,首先从预设的依赖项中查找类型匹配的实例,其次获取指定类型的所有 Bean 的名称,并进行筛选。一个 Bean 是否有资格成为候选者,至少要符合以下几个条件:
- 比如声明了
@Qualifier
注解的情况。isAutowireCandidate
方法的逻辑较为繁琐,此处不做过多讨论,可简单认为返回 true 即可。 - Spring 还规定了其他的限制,比如不能是自引用,候选项不能是集合类型等(仅了解)
//所属类[cn.stimd.spring.beans.factory.support.DefaultListableBeanFactory]
//寻找符合条件的候选项集合
private Map<String,Object> findAutowireCandidates(Class<?> requiredType, DependencyDescriptor descriptor) {
Map<String, Object> result = new LinkedHashMap<>();
//1. 查找预设的依赖项
for(Class<?> autowiringType: this.resolvableDependencies.keySet()){
if(autowiringType.isAssignableFrom(requiredType)){
Object autowiringValue = this.resolvableDependencies.get(autowiringType);
if(requiredType.isInstance(autowiringValue)){
result.put(ObjectUtils.identityToString(autowiringValue), autowiringValue);
break;
}
}
}
//2. 查找指定类型的所有Bean的名称(包括父容器)
List<String> candidateNames = getBeanNamesForType(requiredType);
if(getParentBeanFactory() != null){
candidateNames.addAll(getParentBeanFactory().getBeanNamesForType(requiredType));
}
for (String candidate : candidateNames) {
//进一步检查是否满足相关要求,比如声明了@Qualifier的情况
if(isAutowireCandidate(candidate, descriptor)){
//候选项是已注册的单例,直接将Bean加入列表
if(descriptor instanceof MultiElementDescriptor || containsSingleton(candidate)){
result.put(candidate, getBean(candidate));
}
//否则添加Class对象
else{
result.put(candidate, getType(candidate));
}
}
}
return result;
}
这里有一个很重要的问题,为什么候选项不是已注册的单例,就只获取 Class
对象,难道不能用 getBean
方法创建实例吗?直接创建对象当然是可行的,但 Spring 的考虑是避免提前实例化对象而造成副作用,这是原则问题,所有的流程都必须无条件做出让步。那么什么是提前实例化,造成的副作用(side effect)又是什么?
在一个软件系统中,对象之间的依赖关系可能非常复杂,如果不加以控制,就会造成连锁反应,我们称之为依赖裂变。举个例子,本来只想实例化 A 对象和依赖的 B 对象,结果实例化了十几个对象。假如 F 对象的依赖项 G 此时尚不存在,导致解析失败,抛出异常。换句话说,F 对象不应该在创建 A 的时候实例化,这就是提现实例化。所谓的副作用就是不可预知的后果,不可预知是说造成的影响可大可小,最严重的情况会导致 Spring 容器创建失败。考虑到代码的健壮性,必须做最坏的打算。
我们现在寻找的是候选项,还不是板上钉钉的最终合格项,在面试的过程中有被淘汰的可能。因此应当尽量避免提现实例化,如果候选项对应的单例还不存在,那就把它的 Class
对象添加到列表中。等到最终确定了唯一的合格项,再创建实例不迟。
3.3 获取唯一的候选项
findAutowireCandidates
方法返回的 matchingBeans
变量是 Map
类型,说明可能存在多个候选项。如果只有一个元素,那么直接视为合格项即可。如果大于一个元素,还需要进行筛选,接下来进入面试环节。determineAutowireCandidate
方法按照三个条件进行考察,它们之间有优先级,只要满足任何一个条件则立即返回。
- 候选项是否被显式标记为[首要的],也就是检查
BeanDefinition
的primary
属性 - 优先级检查,候选项可能实现了
Ordered
接口或者声明了@Order
注解,需要找出优先级最高的(仅了解) - 作为兜底的措施,优先从预设的依赖项中查找,其次检查候选项的
beanName
与字段名或方法参数名是否相同
//所属类[cn.stimd.spring.beans.factory.support.DefaultListableBeanFactory]
//从众多候选项找到一个最符合条件的合格项
protected String determineAutowireCandidate(Map<String, Object> candidates, DependencyDescriptor descriptor) {
//查找primary标记为true的候选项
String primaryCandidate = determinePrimaryCandidate(candidates);
if(primaryCandidate != null){
return primaryCandidate;
}
//查找优先级最高的候选项(略)
//查找预设的依赖项,或者beanName与字段/参数名相同的候选项
for (Map.Entry<String, Object> entry : candidates.entrySet()) {
String candidateName = entry.getKey();
Object beanInstance = entry.getValue();
if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) ||
(candidateName != null && candidateName.equals(descriptor.getDependencyName()))) {
return candidateName;
}
}
return null;
}
3.4 获取依赖项实例
经过了简历筛选和面试两轮淘汰之后,拿到了唯一的合格项,也就是最终依赖项。此时变量 instanceCandidate
可能是一个 Bean,也可能是 Class
对象。接下来判断变量 instanceCandidate
是不是 Class
类型,如果是则需要调用 getBean
方法创建依赖项的实例。第三步的逻辑比较简单,但告诉我们一个非常重要的信息,依赖解析的过程可能会导致连锁反应。因此,Spring 通过避免对候选项进行提前实例化,将连锁反应的影响降至最低。
4. 集合类型依赖
4.1 概述
依赖注入的目标不仅可以是单一类型,还可以是 List
、Map
、数组等集合类型。相对于单一类型的依赖来说,集合类型依赖的处理反而更简单,只需查找指定类型的所有候选项,它们都被看作是符合条件的。也就是说只要符合简历要求的统统都要,省去了面试的环节。
4.2 代码实现
doResolveDependency
方法的第二步就是解析集合类型的依赖,具体工作由 resolveMultipleBeans
方法完成。
//所属类[cn.stimd.spring.beans.factory.support.DefaultListableBeanFactory]
//依赖解析
private Object doResolveDependency(DependencyDescriptor descriptor, String beanName){
//1. 处理@Value注解(略)
//2. 依赖是集合类型
Object multipleBeans = resolveMultipleBeans(descriptor);
if (multipleBeans != null) {
return multipleBeans;
}
//3. 单一对象依赖(略)
}
resolveMultipleBeans
方法实现了 Collection
和 Map
两种集合类型的依赖解析,由于两种类型的处理方式差不多,这里仅以 Collection
为例进行分析。为了简化代码,我们不考虑多层嵌套,仅处理类似 List<Foo>
、Map<String, Foo>
这种只嵌套了一层的情况。
- 获取集合内部元素的类型,如果是
List<Foo>
类型,则拿到Foo
的类型信息。 - 调用
findAutowireCandidates
方法查找所有符合条件的候选项。(只有筛选简历的环节) - 候选项列表的类型是一个
Map
,我们需要的是Collection
类型,因此必须进行类型转换操作。同样地,如果是List
接口类型,会生成对应的ArrayList
对象。
//所属类[cn.stimd.spring.beans.factory.support.DefaultListableBeanFactory]
//处理集合类型的依赖项
private Object resolveMultipleBeans(DependencyDescriptor descriptor) {
Class<?> type = descriptor.getDependencyType();
//处理Collection类型
if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
//1) 获取集合元素的类型(不考虑多层嵌套的情况)
Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric();
//2) 寻找符合条件的候选项列表
Map<String, Object> matchingBeans = findAutowireCandidates(elementType, new MultiElementDescriptor(descriptor));
//3) 类型转换,比如List类型会转成ArrayList
return getTypeConverter().convertIfNecessary(matchingBeans.values(), type);
}
//处理Map类型(略)
return null;
}
需要注意的是,在 findAutowireCandidates
方法中,由于集合类型使用的属性描述符实现类是 MultiElementDescriptor
,直接调用 getBean
方法创建实例。也就是说,这里并不担心提前实例化的副作用,这是因为集合依赖的所有候选项都不会被淘汰,可以放心地进行实例化。
5. 延迟依赖解析
5.1 概述
在实际应用中,有时会出现依赖项尚未注册到 Spring 容器中,此时会导致依赖解析失败。Spring 的解决方案是在解析依赖项时返回一个包装对象,然后手动地触发真正的依赖解析操作。Spring 为延迟依赖解析提供了一组 API,如下所示:
ObjectProvider
:标记接口,表明实现类提供了延迟初始化的功能DependencyObjectProvider
:重写了getObject
方法,实际调用依赖解析的方法Jsr330DependencyProvider
:作用是兼容 JDK 的Provider
接口
5.2 相关 API
ObjectProvider
是一个标记接口,实现类重写的是父接口 ObjectFactory
的 getObject
方法。看起来有点乏善可陈,但是为什么一定要定义这个标记接口?这是因为 ObjectFactory
接口的实例由 Spring 容器在创建实例时自动调用,而 ObjectProvider
接口的实例只能由用户手动调用。
public interface ObjectProvider <T> extends ObjectFactory<T> {}
DependencyObjectProvider
作为 ObjectProvider
接口的实现类,getObject
方法调用了 doResolveDependency
方法。显而易见,只有当调用 getObject
方法的时候才会执行依赖解析的操作。
private class DependencyObjectProvider implements ObjectProvider<Object> {
private final DependencyDescriptor descriptor;
private final String beanName;
@Override
public Object getObject() throws BeansException {
return doResolveDependency(this.descriptor);
}
}
Jsr330DependencyProvider
扩展了 DependencyObjectProvider
,主要目的是为了兼容 JDK 的 Provider
接口。
private class Jsr330DependencyProvider extends DependencyObjectProvider implements Provider<Object> {
@Override
public Object get() throws BeansException {
return getObject();
}
}
5.3 代码实现
上节提到,resolveDependency
方法起到辅助作用,doResolveDependency
方法才是真正进行依赖解析的操作。实际上,延迟依赖解析正是在 resolveDependency
方法中执行的。代码处理了两种情况,如下所示:
- 如果字段或方法参数的类型是
OjbectFactory
或ObjectProvider
,则返回一个DependencyObjectProvider
包装对象。 - 如果字段或方法参数的类型是
Provider
,返回一个Jsr330DependencyProvider
包装对象。
//所属类[cn.stimd.spring.beans.factory.support.DefaultListableBeanFactory]
//依赖解析的入口方法,负责外围工作
@Override
public Object resolveDependency(DependencyDescriptor descriptor, String requestingBeanName) throws BeansException {
//此时返回的是包装对象,将实际的依赖解析延迟到真正使用的时候
if (ObjectFactory.class == descriptor.getDependencyType() || ObjectProvider.class == descriptor.getDependencyType()) {
return new DependencyObjectProvider(descriptor, requestingBeanName);
}
if(descriptor.getDependencyType() == javaxInjectProviderClass){
return new Jsr330DependencyProvider(descriptor, requestingBeanName);
}
return doResolveDependency(descriptor, requestingBeanName);
}
6. 测试
6.1 单一类型依赖
先来看测试类,UserController
的 userService
字段声明了 @Autowired
,表示需要将一个 UserService
的实例注入给该字段。此外还定义了 getUser
方法,实际上委托给 UserService
来创建一个 User
对象。这种结构是 web 应用中的典型用例,控制器类负责映射请求,业务类完成具体功能。
//测试类
public class UserController {
@Autowired
private UserService userService;
public User getUser(){
return this.userService.getUser();
}
}
//测试类
public class UserService {
public User getUser(){
return new User("Tom", 20);
}
}
测试方法分为三步,第一步是准备工作,第二步把两个测试类对应的 BeanDefinition
注册到容器中,第三步通过容器获取 UserController
的实例。Spring 容器在创建实例之后,执行自动装配的操作。检测到 userService
字段声明了 @Autowired
注解,寻找对应的依赖项也就是 UserService
对象,然后通过反射的方式赋给字段。
//测试方法
@Test
public void testAutowiredSingle(){
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor();
processor.setBeanFactory(factory);
factory.addBeanPostProcessor(processor);
factory.registerBeanDefinition("userController", new RootBeanDefinition(UserController.class));
factory.registerBeanDefinition("userService", new RootBeanDefinition(UserService.class));
UserController controller = factory.getBean("userController", UserController.class);
User user = controller.getUser();
System.out.println("@Autowired单一依赖测试: " + user);
}
从测试结果可以看到,调用 UserController
的 getUser
方法,实际上会调用 UserService
的 getUser
方法返回一个 User
对象。
单一依赖测试: User{name='Tom', age=20}
6.2 集合类型依赖
在测试类 AutowireConfig
中,定义了一个集合类型的字段 userList
,setter 方法声明了 @Autowired
注解,本次测试的目标是集合依赖以及 setter 方法注入。
//测试类
public class AutowireConfig {
private Set<User> userList;
@Autowired
public void setUserList(Set<User> userList) {
this.userList = userList;
}
public Set<User> getUserList() {
return userList;
}
}
在测试方法中,首先是准备工作,然后向容器中注册了两个 User
对象,最后调用 getBean
方法获取 AutowireConfig
的实例。当实例创建之后,在执行自动装配的流程中,检测到 setUserList
方法上声明了 @Autowired
注解,且方法参数是一个集合类型。这时容器会寻找所有的 User
实例,全部添加到集合中,然后赋给 userList
字段。
注:集合类型也可以是
Map
类型,测试流程与List
类似,不赘述,详见testAutowiredMap
测试方法。
//测试方法
@Test
public void testAutowiredList(){
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor();
processor.setBeanFactory(factory);
factory.addBeanPostProcessor(processor);
factory.registerBeanDefinition("autowireConfig", new RootBeanDefinition(AutowireConfig.class));
factory.registerSingleton("tom", new User("Tom", 18));
factory.registerSingleton("jerry", new User("Jerry", 22));
AutowireConfig autowireConfig = factory.getBean("autowireConfig", AutowireConfig.class);
System.out.println("List集合依赖测试: " + autowireConfig.getUserList());
}
从测试结果可以看到,Spring 容器中所有 User
类型的单例都打印了出来。
List集合依赖测试: [User{name='Tom', age=18}, User{name='Jerry', age=22}]
6.3 延迟依赖解析
先定义一个测试类 LazyBean
,在构造方法中打印日志,作用是观察对象是何时创建的。然后是 AutowireConfig
,声明一个 ObjectProvider
包装类型的字段。
//测试类:延迟加载
public class LazyBean {
public LazyBean() {
System.out.println("创建LazyBean");
}
}
//测试类:持有一个延迟加载的对象
public class AutowireConfig {
private ObjectProvider<LazyBean> lazyBean;
@Autowired
public void setLazyBean(ObjectProvider<LazyBean> lazyBean) {
this.lazyBean = lazyBean;
}
}
测试方法分为三步,首先是准备工作,然后注册 AutowireConfig
,并调用 getBean
方法触发实例化操作。需要注意的是,此时还没有注册 LazyBean
实例,依赖解析并没有报错。第三步,注册 LazyBean
,然后调用 getLazyBean
方法,此时获得的是包装类型 ObjectProvider
,因此需要进一步调用 getObject
方法获取目标对象。
//测试方法
@Test
public void testLazyDependency() {
//1. 准备工作
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor();
processor.setBeanFactory(factory);
factory.addBeanPostProcessor(processor);
//2. 触发AutowireConfig的创建工作,依赖解析没有报错
factory.registerBeanDefinition("autowireConfig", new RootBeanDefinition(AutowireConfig.class));
AutowireConfig autowireConfig = factory.getBean("autowireConfig", AutowireConfig.class);
System.out.println("获取AutowireConfig实例");
//3. 第一次获取LazyBean,触发依赖解析的操作
factory.registerBeanDefinition("lazyBean", new RootBeanDefinition(LazyBean.class));
ObjectProvider<LazyBean> lazyBean = autowireConfig.getLazyBean();
lazyBean.getObject();
}
从测试结果可以看到,获取 AutowireConfig
实例的日志先打印,直到第一次获取 LazyBean
实例时,才触发真正的依赖解析的操作。
获取AutowireConfig实例
创建LazyBean
6.4 其他依赖
在测试类中定义 BeanFactory
和 Environment
类型的字段,并声明 @Autowired
注解,并将 required
属性指定为 false
,此举是为了避免在其他测试中报错。
//测试类
public class AutowireConfig {
@Autowired(required = false)
private BeanFactory beanFactory;
@Autowired(required = false)
private Environment environment;
}
测试方法注意两点,一是将 BeanFactory
注册为预设依赖项。二是创建 StandardEnvironment
实例,指定运行环境为 dev
,然后手动注册环境变量的实例。
//测试方法
@Test
public void testOtherDependency() {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor();
processor.setBeanFactory(factory);
factory.addBeanPostProcessor(processor);
factory.registerBeanDefinition("autowireConfig", new RootBeanDefinition(AutowireConfig.class));
//将BeanFactory注册为预设依赖
factory.registerResolvableDependency(BeanFactory.class, factory);
//手动注册Environment
StandardEnvironment environment = new StandardEnvironment();
environment.addActiveProfile("dev");
factory.registerSingleton("environment", environment);
AutowireConfig autowireConfig = factory.getBean("autowireConfig", AutowireConfig.class);
System.out.println("手动注册依赖测试:运行环境为" + Arrays.toString(autowireConfig.getEnvironment().getActiveProfiles()));
System.out.println("预设依赖测试:" + autowireConfig.getBeanFactory());
}
从测试结果可以看到,BeanFactory
和 Environment
的实例都被注入到了测试类中。
手动注册依赖测试:运行环境为[dev]
预设依赖测试:cn.stimd.spring.beans.factory.support.DefaultListableBeanFactory@4590c9c3
7. 总结
本节介绍了依赖注入的另一种情况,即候选项的类型是一个对象。对象解析分为两种情况,一是单一类型,二是集合类型。由于集合类型的解析是以单一类型的解析为基础,因此重点在于单一类型的解析。本节内容由以下四个部分组成:
-
依赖分类:Spring 提供了两种依赖项,一是预设的依赖项,二是容器托管的单例。
-
单一类型依赖:首先是筛选简历,需要对 Spring 容器中的所有单例进行筛选,找出一组初步符合条件(指定类型)的候选项。然后是面试环节,从几个方面对若干候选项进行考察,符合任一条件的优先被挑选,最后得到唯一的合格项。在这一过程中,需要注意依赖裂变的问题,尽量避免提前实例化所引发的副作用。
-
集合类型依赖:只有筛选简历环节,指定类型的单例均认为是符合条件的。
-
延迟依赖解析:在依赖解析时返回一个包装对象,直到第一次访问该依赖项时才执行真正的依赖解析操作。
此外,Spring 还提供了对 JDK 的 @Resource
注解的支持,CommonAnnotationBeanPostProcessor
组件实现了依赖注入的功能。实际上 @Resource
注解的功能更为强大,出于精简代码的考虑,我们不对该组件进行介绍,感兴趣的读者可以尝试实现。
8. 项目信息
新增修改一览,新增(4),修改(6)。
beans
├─ src
│ ├─ main
│ │ └─ java
│ │ └─ cn.stimd.spring.beans
│ │ └─ factory
│ │ ├─ config
│ │ │ └─ ConfigurableBeanFactory.java (*)
│ │ ├─ support
│ │ │ ├─ AbstractBeanFactory.java (*)
│ │ │ └─ DefaultListableBeanFactory.java (*)
│ │ └─ ObjectProvider.java (+)
│ └─ test
│ └─ java
│ └─ beans
│ └─ autowire
│ ├─ AutowireConfig.java (*)
│ ├─ AutowireTest.java (*)
│ ├─ LazyBean.java (+)
│ ├─ UserController.java (+)
│ └─ UserService.java (+)
└─ pom.xml (*)
注:+号表示新增、*表示修改
-
项目地址:https://gitee.com/stimd/spring-wheel
-
本节分支:https://gitee.com/stimd/spring-wheel/tree/chapter1-7
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
关注公众号【Java编程探微】,加群一起讨论。