提起Spring
,大家肯定不陌生,它是每一个Java开发者绕不过去的坎。Spring
框架为基于 java
的企业应用程序提供了一整套解决方案,方便开发人员在框架基础快速进行业务开发。最近开始玩公众号了,喜欢的小伙伴可以关注我
在官网中,我们发现它的核心技术之一:Dependency Injection
,简称:DI
,翻译过来就是依赖注入。今天我们就来盘一盘它。
在本文中,我们将深入研究 Spring
框架 DI
背后的故事,包括 Spring Inversion of Control
(控制反转)、 DI
和 ApplicationContext
接口。 基于这些基本概念,我们将研究如何使用基于 java
和基于 XML
的配置来 创建Spring
应用程序。 最后,我们将探讨在创建 Spring
应用程序时遇到的一些常见问题,包括 bean冲突和循环依赖性。
在学习DI
之前,我们先学习一下 IoC
(控制反转),接下来的一段可能读起来会让你感觉比较啰嗦,但是要细细体会每一次改变的意图,和我们的解决方案,对于理解控制反转非常重要。
首先来了解下我们通常实例化一个对象的方式。 在 平时,我们使用 new
关键字实例化一个对象。 例如,如果有一个 Car
类,我们可以使用以下方法实例化一个对象 Car
Car car = new Car();
因为汽车有很多零部件组成,我们定义Engine
接口来模拟汽车引擎,然后将engine
对象作为成员变量放在Car
类
public interface Engine {
void turnOn();
}
public class Car {
private Engine engine;
public Car() {}
public void start() {
engine.turnOn();
}
}
现在,我们可以调用start()方法吗?显然是不行的,一眼可以看出会报NullPointerException (NPE)
,因为我们没有在Car
的构造函数中初始化engine
。通常我们采用的方案就是在Car的构造函数中觉得使用Engine
接口的哪个实现,并直接将该实现分配给engine
字段;
现在,我们来首先创建Engine
接口的实现类
public class ElectricEngine implements Engine {
@Override
public void turnOn() {
System.out.println("电动引擎启动");
}
}
public class CombustionEngine implements Engine {
@Override
public void turnOn() {
System.out.println("燃油引擎启动");
}
}
我们修改Car的构造函数,使用ElectricEngine
实现,将我们的engine
字段分配给一个实例化的ElectricEngine
对象
public class Car {
private Engine engine;
public Car() {
this.engine = new ElectricEngine();
}
public void start() {
engine.turnOn();
}
public static void main(String[] args) {
Car car = new Car();
car.start();
}
}
现在我们执行start()
方法,我们会看到如下输出:
大功告成,我们成功解决了 NPE
(空指针)问题,但是我们胜利了吗?哈哈哈,显然没有!
在解决问题的同时,我们又引入了另一个问题。尽管我们通过抽象Engine
接口,然后通过不同的Engine
实现类来负责不同类型引擎的业务逻辑,的确是很好的设计策略。但是细心的伙伴可能已经发现了,我们Car
类的构造函数中将engine
声明为CombustionEngine
,这将导致所有车都有一个燃油引擎。假如我们现在要创建不同的汽车对象,它有一个电动引擎,我们将不得不改变我们的设计。比较常见的方法是创建两个独立里的类,各司其职,在他们的构造函数中将engine
分配给Engine
接口的不同实现;
例如:
public class CombustionCar {
private Engine engine;
public CombustionCar() {
this.engine = new CombustionEngine();
}
public void start() {
engine.turnOn();
}
}
public class ElectricCar {
private Engine engine;
public ElectricCar() {
this.engine = new ElectricEngine();
}
public void start() {
engine.turnOn();
}
}
通过上面的一顿骚操作,我们成功的解决了我们引擎的问题。如果是一个日常需求,我们已经可以成功交工了。但是这显然不是我写这篇文章的目的。
从设计的角度来说,目前的代码是糟糕的,有以下两点原因:
start()
方法;Engine
实现类创建一个新的类;尤其后一个问题更加难以解决,因为我们不控制Engine
的实现,随着开发人员不断的创建自己的实现类,这个问题会更加恶化;
带着上面的问题,我们继续思考…
我们可以创建一个父类Car
,将公共代码抽取到父类中,可以轻松解决第一个问题。由于Engine
字段是私有的,我们在父类Car
的构造函数中接收Engine
对象,并且进行赋值。
public class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.turnOn();
}
}
public class CombustionCar extends Car{
public CombustionCar() {
super(new CombustionEngine());
}
}
public class ElectricCar extends Car {
public ElectricCar() {
super(new ElectricEngine());
}
}
通过这种方法,我们成功的解决了代码重复的问题,我们来测试一下:
public class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.turnOn();
}
public static void main(String[] args) {
CombustionCar combustionCar1 = new CombustionCar();
combustionCar1.start();
ElectricCar electricCar1 = new ElectricCar();
electricCar1.start();
}
}
那么我们该如何解决我们提出的第二个问题那?
其实这个问题我们可以换个角度看:为什么我们要去关注CombustionCar
和ElectricCar
,我们现在将关注点回到我们的Car
,我们现在已经允许客户端实例化Car
对象时候将Engine
对象作为构造函数的参数传入,其实已经消除了为每个Engine
对象创建新Car
的问题。因为现在Car
类依赖于Engine
接口,并不知道任何Engine
的实现;
通过带有Engine
参数的构造函数,我们已将要使用哪个Engine
实现的决定从Car
类本身(最初由CombustionEngine
决定)更改为实例化Car
类的客户端。 决策过程的这种逆转称为IoC原则
。 现在,由客户端控制使用哪种实现,而不是由Car
类本身控制使用哪种Engine
实现。
有点绕,大家结合下面的示例代码,细细琢磨
public class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.turnOn();
}
public static void main(String[] args) {
/**
* 老法子
* 为每一类型发送机的车创建类,然后实现父类car,然后在构造函数传入自己的引擎,然后调用start()
*/
CombustionCar combustionCar1 = new CombustionCar();
combustionCar1.start();
ElectricCar electricCar1 = new ElectricCar();
electricCar1.start();
/**
* 控制反转思想
* 把自己看作实例化car的客户端,需要什么引擎,直接传入相关对象
*/
CombustionEngine combustionEngine = new CombustionEngine();
Car combustionCar = new Car(combustionEngine);
combustionCar.start();
ElectricEngine electricEngine = new ElectricEngine();
Car electricCar = new Car(electricEngine);
electricCar.start();
}
}
执行上面的代码,我们发现都可以获得我们想要的结果:
从上面的例子我们可以看到,实例化Car
类的客户端可以控制所使用的Engine
实现,并且取决于将哪个Engine
实现传递给Car
构造函数,Car
对象的行为发生巨大变化。为什么这么说,接着看下面
在上面控制反转的知识点,我们已经解决了由谁决定使用哪种Engine
实现的问题,但是不可避免,我们也更改了实例化一个Car
对象的步骤;
最开始,我们实例化Car
不需要参数,因为在它的构造函数里面已经为我们new
了Engine
对象。使用IoC
方法之后,我们要求在实例化一个Car
之前,我们需要先创建一个Engine
对象,并作为参数传递给Car
构造对象。换句话说,最初,我们首先实例化Car
对象,然后实例化Engine
对象。但是,使用IoC
之后,我们首先实例化Engine
对象,然后实例化Car
对象;
因此,我们在上面的过程中创建了一个依赖关系。不过这种依赖关系不是指编译时候Car
类对Engine
接口的依赖关系,相反,我们引入了一个运行时依赖关系。在运行时,实例化Car
对象之前,必须首先实例化Engine
对象。
某一个具体的依赖对象大家可以理解为Spring中的bean,对于两个有依赖关系的bean,其中被依赖的那个bean,我们把它称为依赖对象
我们用图形化的方式来看看它们之间的依赖关系,其中图形的节点代表对象,箭头代表依赖关系(箭头指向依赖对象)。对于我们我的Car
类,依赖关系树非常简单:
如果依赖关系树的终端结点还有自己的附加依赖关系,那么这个依赖关系树将变得更加复杂。现在再看我们上面的例子,如果CombustionEngine
还有其他依赖对象,我们首先需要创建CombustionEngine
的依赖对象,然后才能实例化一个CombustionEngine
对象。这样在创建Car
对象时候,才能将CombustionEngine
传递给Car
的构造函数;
//凸轮轴
public class Camshaft {}
//机轴
public class Crankshaft {}
public class CombustionEngine implements Engine {
//凸轮轴
private Camshaft camshaft;
//机轴
private Crankshaft crankshaft;
public CombustionEngine(Camshaft camshaft, Crankshaft crankshaft) {
this.camshaft = camshaft;
this.crankshaft = crankshaft;
}
@Override
public void turnOn() {
System.out.println("燃油引擎启动");
}
}
经过我们改造,我们现在的依赖关系树变为下面的样子
随着我们不断引入更多的依赖关系,这种复杂性将继续增长。为了解决这个复杂问题,我们需要基于依赖关系树抽取对象的创建过程。这就是依赖注入框架。
一般来说,我们可以把这个过程分为三个部分:
通过反射,我们可以查看 Car
类的构造函数,并且知道它需要一个 Engine
参数。因此为了创建Car对象,我们必须创建至少一个Engine
接口的实现类用作依赖项来使用。在这里,我们创建一个CombustionEngine
对象(为了方便,暂时当做只有一个实现类,bean冲突问题待会再说)来声明它作为依赖项来使用,就满足Car
对象创建时的需求.
其实,这个过程是递归的,因为CombustionEngine
依赖于其他对象,我们需要不断重复第一个过程,直到把所有依赖对象声明完毕,然后注册创建这些依赖对象所需要的类。
第三点其实就是将前面两点思想付诸实施,从而形成一种创建对象的机制
举个例子:比如我们需要一个Car
对象,我们必须遍历依赖关系树并检查是否存在至少一个符合条件的类来满足所有依赖关系。 例如,声明CombustionEngine
类可满足Engine
节点要求。 如果存在这种依赖关系,我们将实例化该依赖关系,然后移至下一个节点。
如果有一个以上的类满足所需的依赖关系,那么我们必须显式声明应该选择哪一种依赖关系。 稍后我们将讨论 Spring 是如何做到这一点的。
一旦我们确定所有的依赖关系都准备好了,我们就可以从终端节点开始创建依赖对象。 对于 Car
对象,我们首先实例化 Camshaft
和Crankshaft
ーー因为这些对象没有依赖关系ーー然后将这些对象传递给 CombustionEngine
构造函数,以实例化 CombunstionEngine
对象。 最后,我们将 CombunstionEngine
对象传递给 Car
构造函数,以实例化所需的 Car
对象。
了解了 DI
的基本原理之后,我们现在可以继续讨论 Spring
如何执行 DI
。
Spring
的核心是一个DI
框架,它可以将DI
配置转换为Java
应用程序。
在这里我们要阐述一个问题:那就是库和框架的区别。库只是类定义的集合。背后的原因仅仅是代码重用,即获取其他开发人员已经编写的代码。这些类和方法通常在域特定区域中定义特定操作。例如,有一些数学库可让开发人员仅调用函数而无需重做算法工作原理的实现。
框架通常被认为是一个骨架,我们在其中插入代码以创建应用程序。 许多框架保留了特定于应用程序的部分,并要求我们开发人员提供适合框架的代码。 在实践中,这意味着编写接口的实现,然后在框架中注册实现。
在 Spring
中,框架围绕 ApplicationContext
接口实现上一节中概述的三个 DI
职责。通常这个接口代表了一个上下文。 因此,我们通过基于 java
或基于 xml
的配置向 ApplicationContext
注册合适的类,并从 ApplicationContext
请求创建 bean
对象。 然后 ApplicationContext
构建一个依赖关系树并遍历它以创建所需的 bean
对象
Applicationcontext
中包含的逻辑通常被称为 Spring
容器。 通常,一个 Spring
应用程序可以有多个 ApplicationContext
,每个 ApplicationContext
可以有单独的配置。 例如,一个 ApplicationContext
可能被配置为使用 CombustionEngine
作为其引擎实现,而另一个容器可能被配置为使用 ElectricEngine
作为其实现。
在本文中,我们将重点讨论每个应用程序的单个 ApplicationContext
,但是下面描述的概念即使在一个应用程序有多个 ApplicationContext
实例时也适用。
Spring为我们提供了两种基于 java
的配置方式
基于java
的基本配置的核心,其实是下面两个注解:
@Configuration
: 定义配置类@Bean
: 创建一个bean
例如,给出我们之前定义的 Car
, CombustionEngine
, Camshaft
, 和Crankshaft
类,我们可以创建一个下面 的配置类:
/**
* @author milogenius
* @date 2020/5/17 20:52
*/
@Configuration
public class AnnotationConfig {
@Bean
public Car car(Engine engine) {
return new Car(engine);
}
@Bean
public Engine engine(Camshaft camshaft, Crankshaft crankshaft) {
return new CombustionEngine(camshaft, crankshaft);
}
@Bean
public Camshaft camshaft() {
return new Camshaft();
}
@Bean
public Crankshaft crankshaft() {
return new Crankshaft();
}
}
接下来,我们创建一个 ApplicationContext
对象,从 ApplicationContext
对象获取一个 Car
对象,然后在创建的 Car
对象上调用 start
方法:
ApplicationContext context =
new AnnotationConfigApplicationContext(AnnotationConfig.class);
Car car = context.getBean(Car.class);
car.start();
执行结果如下:
Started combustion engine
虽然@Configuration
和@Bean
注解的组合为 Spring
提供了足够的信息来执行依赖注入,但我们仍然需要手动手动定义每个将被注入的 bean
,并显式地声明它们的依赖关系。 为了减少配置 DI
框架所需的开销,Spring 提供了基于java
的自动配置。
为了支持基于 java
的自动配置,Spring
提供了额外的注解。 虽然我们平时可能加过很多这种类型的注解,但是有三个最基本的注解:
@Component
: 注册为由 Spring 管理的类@Autowired
: 指示 Spring 注入一个依赖对象@ComponentScan
: 指示Spring在何处查找带有@Component
注解的类@Autowired
注解用来指导 Spring
,我们打算在使用注解的位置注入一个依赖对象。 例如,在 Car
构造函数中,我们期望注入一个 Engine
对象,因此,我们给 Car
构造函数添加@Autowired
注解。 通过使用@Component
和@Autowired
注解改造我们Car
类,如下所示:
@Component
public class Car {
private Engine engine;
@Autowired
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.turnOn();
}
}
我们可以在其他类中重复这个过程:
@Component
public class Camshaft {}
@Component
public class Crankshaft {}
@Component
public class CombustionEngine implements Engine {
private Camshaft camshaft;
private Crankshaft crankshaft;
@Autowired
public CombustionEngine(Camshaft camshaft, Crankshaft crankshaft) {
this.camshaft = camshaft;
this.crankshaft = crankshaft;
}
@Override
public void turnOn() {
System.out.println("Started combustion engine");
}
}
改造完成相关类之后,我们需要创建一个@Configuration
类来指导 Spring
如何自动配置我们的应用程序。 对于基于 java
的基本配置,我们明确指示 Spring
如何使用@Bean
注解创建每个 bean,但在自动配置中,我们已经通过@Component
和@Autowired
注解提供了足够的信息,说明如何创建所需的所有 bean。 唯一缺少的信息是 Spring
应该在哪里寻找我们的带有@Component
注解的 类,并把它注册为对应的bean。
@ Componentscan
注释包含一个参数 basePackages
,它允许我们将包名称指定为一个 String
,Spring
将通过递归搜索来查找@Component
类。 在我们的示例中,包是 com.milo.domain
,因此,我们得到的配置类是:
@Configuration
@ComponentScan(basePackages = "com.milo.domain")
public class AutomatedAnnotationConfig {}
ApplicationContext context =
new AnnotationConfigApplicationContext(AutomatedAnnotationConfig.class);
Car car = context.getBean(Car.class);
car.start();
执行结果:
Started combustion engine
通过和基于java
的基础配置比较,我们发现基于 java
的自动配置方法有两个主要优点:
所以无特殊情况,自动配置是首选
除了构造函数注入,我们还可以通过字段直接注入。 我们可以将@Autowired
注解应用到所需的字段来实现这一点:
@Component
public class Car {
@Autowired
private Engine engine;
public void start() {
engine.turnOn();
}
}
这种方法极大地减少了我们的编码压力,但是它也有一个缺点,就是在使用字段之前,我们将无法检查自动注入的对象是否为空。
构造函数注入的最后一种替代方法是 setter 注入,其中@Autowired
注解应用于与字段关联的 setter。 例如,我们可以改变 Car
类,通过 setter 注入获得 Engine
对象,方法是用@Autowired
注解 setEngine
方法:
@Component
public class Car {
private Engine engine;
public void start() {
engine.turnOn();
}
public Engine getEngine() {
return engine;
}
@Autowired
public void setEngine(Engine engine) {
this.engine = engine;
}
}
Setter 注入类似于字段注入,但它允许我们与 注入对象交互。 在有些情况下,setter 注入可能特别有用,例如具有循环依赖关系,但 setter 注入可能是三种注入技术中最不常见的,尽可能优先使用构造函数注入。
另一种配置方法是基于 xml
的配置。 我们在 XML
配置文件中定义 bean
以及它们之间的关系,然后指示 Spring
在哪里找到我们的配置文件。
第一步是定义 bean
。 我们基本遵循与基于 java
的基本配置相同的步骤,但使用 xmlbean
元素代替。 在 XML
的情况下,我们还必须显式地声明我们打算使用 constructor-arg
元素注入到其他构造函数中的 bean
。 结合 bean
和 constructor-arg
元素,我们得到以下 XML
配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<bean id="car" class="com.milo.domain.Car">
<constructor-arg ref="engine" />
bean>
<bean id="engine" class="com.milo.CombustionEngine">
<constructor-arg ref="camshaft" />
<constructor-arg ref="crankshaft" />
bean>
<bean id="camshaft" class="com.milo.Camshaft" />
<bean id="crankshaft" class="com.milo.Crankshaft" />
beans>
在 bean 元素中,我们必须指定两个属性:
id
: bean 的唯一 ID (相当于带有@Bean
注解方法名)class
: 类的全路径(包括包名)对于 constructor-arg
元素,我们只需要指定 ref
属性,它是对现有 bean ID
的引用。 例如,元素构造函数
规定,具有 ID engine
(直接定义在 car
bean 之下)的 bean 应该被用作注入 car
bean 构造函数的 bean。
构造函数参数的顺序由 constructor-arg
元素的顺序决定。 例如,在定义 engine
bean 时,传递给 CombustionEngine
构造函数的第一个构造函数参数是 camshaft
bean,而第二个参数是 crankshaft
bean。
获取ApplicationContext
对象,我们只需修改 ApplicationContext
实现类型。 因为我们将 XML
配置文件放在类路径上,所以我们使用 ClassPathXmlApplicationContext
:
ApplicationContext context =
new ClassPathXmlApplicationContext("basic-config.xml");
Car car = context.getBean(Car.class);
car.start();
执行结果:
Started combustion engine
现在,我们已经摸清了Spring框架如何进行DI
,并正确地将所有依赖关系注入到我们的应用程序中,但是我们必须处理两个棘手的问题:
在基于 java
和基于 xml
的方法中,我们已经指示 Spring
只使用 CombustionEngine
作为我们的Engine
实现。 如果我们将ElectricEngine
注册为符合 di
标准的部件会发生什么? 为了测试结果,我们将修改基于 java
的自动配置示例,并用@Component
注解 ElectricEngine
类:
@Component
public class ElectricEngine implements Engine {
@Override
public void turnOn() {
System.out.println("Started electric engine");
}
}
如果我们重新运行基于 java 的自动配置应用程序,我们会看到以下错误:
No qualifying bean of type 'com.dzone.albanoj2.spring.di.domain.Engine' available: expected single matching bean but found 2: combustionEngine,electricEngine
由于我们已经注释了用@Component
实现 Engine
接口的两个类ーー即 CombustionEngine
和ElectricEngine
ーー spring
现在无法确定在实例化 Car
对象时应该使用这两个类中的哪一个来满足 Engine
依赖性。 为了解决这个问题,我们必须明确地指示 Spring
使用这两个 bean
中的哪一个。
一种方法是给我们的依赖对象命名,并在应用@Autowired
注解的地方使用@Qualifier
注解来确定注入哪一个依赖对象。 所以,@Qualifier
注解限定了自动注入的 bean,从而将满足需求的 bean 数量减少到一个。 例如,我们可以命名我们的CombustionEngine
依赖对象:
@Component("defaultEngine")
public class CombustionEngine implements Engine {
// ...代码省略,未改变
}
然后我们可以添加@Qualifier
注解,其名称和我们想要注入的依赖对象的名称保持一致,这样,我们Engine
对象在 Car
构造函数中被自动注入
@Component
public class Car {
@Autowired
public Car(@Qualifier("defaultEngine") Engine engine) {
this.engine = engine;
}
// ...existing implementation unchanged...
}
如果我们重新运行我们的应用程序,我们不再报以前的错误:
Started combustion engine
注意,如果没有显式申明bean名称的类都有一个默认名称,该默认名称就是类名首字母小写。 例如,我们的 Combusttionengine
类的默认名称是 combusttionengine
如果我们知道默认情况下我们更喜欢一个实现,那么我们可以放弃@Qualifier
注释,直接将@Primary
注释添加到类中。 例如,我们可以将我们的 Combusttionengine
、 ElectricEngine
和 Car
类更改为:
@Component
@Primary
public class CombustionEngine implements Engine {
// ...existing implementation unchanged...
}
@Component
public class ElectricEngine implements Engine {
// ...existing implementation unchanged...
}
@Component
public class Car {
@Autowired
public Car(Engine engine) {
this.engine = engine;
}
// ...existing implementation unchanged...
}
我们重新运行我们的应用程序,我们会得到以下输出:
Started combustion engine
这证明,虽然有两种可能性满足 Engine
依赖性,即 CombustionEngine
和 Electricengine
,但 Spring
能够根据@Primary
注释决定两种实现中哪一种应该优先使用。
虽然我们已经深入讨论了 Spring DI
的基础知识,但是还有一个主要问题没有解决: 如果依赖关系树有一个循环引用会发生什么? 例如,假设我们创建了一个 Foo
类,它的构造函数需要一个 Bar
对象,但是 Bar
构造函数需要一个 Foo
对象。
我们可以使用代码实现上面问题:
@Component
public class Foo {
private Bar bar;
@Autowired
public Foo(Bar bar) {
this.bar = bar;
}
}
@Component
public class Bar {
private Foo foo;
@Autowired
public Bar(Foo foo) {
this.foo = foo;
}
}
然后我们可以定义以下配置:
@Configuration
@ComponentScan(basePackageClasses = Foo.class)
public class Config {}
最后,我们可以创建我们的 ApplicationContext
:
ApplicationContext context =
new AnnotationConfigApplicationContext(Config.class);
Foo foo = context.getBean(Foo.class);
当我们执行这个代码片段时,我们看到以下错误:
Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'bar': Requested bean is currently in creation: Is there an unresolvable circular reference?
首先,Spring
尝试创建 Foo
对象。 在这个过程中,Spring
认识到需要一个 Bar
对象。 为了构造 Bar
对象,需要一个 Foo
对象。 由于 Foo
对象目前正在构建中(这也是创建 Bar 对象的原因) ,spring
认识到可能发生了循环引用。
这个问题最简单的解决方案之一是在一个类和注入点上使用@Lazy
注解。 这指示 Spring 推迟带注解的 bean 和带注释的@Autowired
位置的初始化。 这允许成功地初始化其中一个 bean,从而打破循环依赖链。 理解了这一点,我们可以改变 Foo
和 Bar
类:
@Component
public class Foo {
private Bar bar;
@Autowired
public Foo(@Lazy Bar bar) {
this.bar = bar;
}
}
@Component
@Lazy
public class Bar {
@Autowired
public Bar(Foo foo) {}
}
如果使用@Lazy
注解后重新运行应用程序,没有发现报告任何错误。
在本文中,我们探讨了 Spring
的基础知识,包括 IoC
、 DI
和 Spring ApplicationContext
。 然后,我们介绍了使用基于 java
的配置和基于 xml
的配置创建 Spring
应用程序的基本知识,同时研究了使用 Spring DI
时可能遇到的一些常见问题。 虽然这些概念一开始可能晦涩难懂,与 Spring
代码脱节,但是我们可以从基底层认识Spirng
,希望对大家有所帮助,谢谢大家。