您现在的位置是:首页 >其他 >Spring之路——深入理解与实现IOC依赖查找与依赖注入网站首页其他
Spring之路——深入理解与实现IOC依赖查找与依赖注入
本文从
xml
开始讲解,注解篇后续给出
文章目录
1. 一个最基本的 IOC 依赖查找实例
首先,我们需要明白什么是IOC
(控制反转)和依赖查找。在Spring Framework
中,控制反转是一种设计模式,可以帮助我们解耦模块间的关系,这样我们就可以把注意力更多地集中在核心的业务逻辑上,而不是在对象的创建和管理上。
依赖查找(Dependency Lookup
)是一种技术手段,使得我们的对象可以从一个管理它们的容器(例如Spring
的ApplicationContext
)中获得它所需要的资源或者依赖。这种查找过程通常是通过类型、名称或者其他的标识进行的。
我们来看一个简单的例子,通过这个例子,你可以理解在Spring
中如何实现IOC
依赖查找。这个例子的目标是创建一个简单的"Hello World
"应用,通过依赖查找,我们将从Spring
的容器中获取一个打印"Hello, World!
"的Bean
。
第一步,我们需要创建一个简单的Java
类,我们将其命名为"HelloWorld
":
public class HelloWorld {
public void sayHello() {
System.out.println("Hello, World!");
}
}
接下来,我们需要在Spring
的配置文件中定义这个Bean
。这个配置文件通常命名为applicationContext.xml
,并放在项目的src/main/resources
目录下:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 定义一个名为helloWorld的Bean -->
<bean id="helloWorld" class="com.example.HelloWorld" />
</beans>
然后,我们就可以在我们的主程序中通过Spring
的ApplicationContext
来获取并使用这个Bean
:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
// 创建一个ApplicationContext,加载spring配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 通过id从ApplicationContext中获取Bean
HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
// 调用方法
obj.sayHello();
}
}
可以在控制台上看到"Hello, World!
"的输出。
2. IOC 的两种实现方式
首先,让我们开始了解什么是控制反转(IoC
)。在传统的程序设计中,我们常常会在需要的地方创建对象,然后使用这些对象来完成一些工作。这种方式存在一个问题,那就是对象之间的耦合度太高。如果我们要改变对象的创建方式或者使用不同的对象来完成同样的工作,我们可能需要修改大量的代码。
为了解决这个问题,IoC
的概念被引入。在IoC
的设计模式中,对象的创建和维护不再由使用它们的代码来控制,而是由一个容器来控制。这个容器会负责对象的创建、配置和生命周期管理,使得我们的代码可以专注于核心的业务逻辑,而不需要关心对象是如何创建和管理的。这样,当我们需要改变对象的创建方式或者替换对象时,我们只需要修改容器的配置,而不需要修改使用对象的代码。
接下来,让我们来看看IoC
的两种实现方式:依赖查找和依赖注入。
2.1 依赖查找(Dependency Lookup)
在这种方式中,当一个对象需要一个依赖(比如另一个对象)时,它会主动从容器中查找这个依赖。通常,这个查找的过程是通过类型、名称或者其他的标识来进行的。
- 根据名称查找
在这种方式中,你需要知道你要查找的bean
的ID
,然后使用ApplicationContext.getBean(String name)
方法查找bean
。
例如,假设我们有一个Printer
类,它需要一个Ink
对象来打印信息。在没有使用Spring
框架的情况下,Printer
可能会直接创建一个Ink
对象:
public class Printer {
private Ink ink = new Ink();
//...
}
但是在Spring
框架中,我们可以将Ink
对象的创建交给Spring
的容器,然后在Printer
需要Ink
对象时,从容器中查找获取:
public class Printer {
private Ink ink;
public void setInk() {
this.ink = (Ink) ApplicationContext.getBean("ink");
}
//...
}
<bean id="ink" class="com.example.Ink" />
在这个配置中,<bean>
元素定义了一个bean
,id
属性给这个bean
定义了一个唯一的名字,class
属性则指定了这个bean
的完全限定类名。所以当你调用 context.getBean("ink")
时,Spring
容器会找到这个名为 “ink
” 的bean
,然后创建一个 Ink
类的实例(如果它还没有被创建)返回。
- 根据类型查找
在这种方式中,你不需要知道bean
的ID
,只需要知道它的类型。然后,你可以使用ApplicationContext.getBean(Class<T> requiredType)
方法查找bean
。
首先,你需要在XML
配置文件中定义你的bean
。以下面的方式定义Ink
和Printer
类:
<bean id="ink1" class="com.example.Ink">
<property name="color" value="Black"/>
</bean>
<bean id="ink2" class="com.example.Ink">
<property name="color" value="Blue"/>
</bean>
<bean id="printer" class="com.example.Printer" />
这里,我们定义了两个类型为Ink
的bean
,ink1
和ink2
,他们的颜色属性分别为“Black
”和“Blue
”。另外,我们还定义了一个类型为Printer
的bean
,没有注入任何依赖。
然后,在你的Java
代码中,你可以按照类型获取Ink
类型的bean
:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 按类型获取bean
Ink ink = context.getBean(Ink.class); // 注意,如果存在多个Ink类型的bean,这行代码会抛出异常
// 如果存在多个Ink类型的bean,你可以这样获取所有的Ink bean
Map<String, Ink> beans = context.getBeansOfType(Ink.class);
for (String id : beans.keySet()) {
System.out.println("Found ink with id: " + id);
Ink inkBean = beans.get(id);
// do something with inkBean...
}
在这个例子中,context.getBean(Ink.class)
会按照类型查找Ink
的bean
。但是,如果有多个Ink
类型的bean
(如本例所示),这种方式会抛出异常。对于这种情况,你可以使用context.getBeansOfType(Ink.class)
方法获取所有类型为Ink
的bean
,然后自行决定如何使用这些bean
。
2.2 依赖注入(Dependency Injection)
- 通过类型进行依赖注入
假设我们有如下的Printer
和Ink
类:
public class Ink {
public void useInk() {
System.out.println("Using ink...");
}
}
public class Printer {
private Ink ink;
public void setInk(Ink ink) {
this.ink = ink;
}
public void print() {
ink.useInk();
System.out.println("Printing...");
}
}
在这个例子中,Printer
类有一个Ink
类型的属性ink
,并且有一个setter
方法setInk
用于设置这个属性的值。这就是我们通常说的依赖注入的setter
方法。
这里我们没有使用 Spring
的 @Autowired
注解,而是使用了 Java
的 setter
方法。你可以通过在 Spring
配置文件中使用 <bean>
和 <property>
标签来配置这种依赖关系
在Spring
的XML
配置文件中,我们可以如下配置:
<bean class="com.example.Ink" />
<bean id="printer" class="com.example.Printer">
<property name="ink" />
</bean>
注意,我们没有为ink bean
指定id
,这样Spring
就会自动根据类型来进行依赖注入。
在这个配置中,我们首先定义了一个Ink
对象的bean
,然后在Printer
对象的bean
定义中,定义了另一个名为 “printer
” 的 bean
,该 bean
的类类型是 com.example.Printer
。不同之处在于,这个 <bean>
元素内嵌了一个 <property>
元素。<property>
元素是用来设置 bean
的属性值的。
当我们需要使用Printer
对象时,我们可以从Spring
容器中获取:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Printer printer = (Printer) context.getBean("printer");
printer.print();
当我们调用printer.print()
时,Printer
对象会使用被Spring
容器注入的Ink
对象来打印信息。
Using ink...
Printing...
通过这种方式,我们不再需要在Printer
类中直接创建Ink
对象,而是让Spring
容器来管理Ink
对象的创建和注入,从而实现了Printer
和Ink
之间的解耦。
- 通过名称进行依赖注入
首先,我们修改Ink
类,使其可以有多种颜色:
public class Ink {
private String color;
public Ink(String color) {
this.color = color;
}
public void useInk() {
System.out.println("Using " + color + " ink...");
}
}
然后,在Spring的XML
配置文件中,我们定义两种颜色的墨水,以及一个打印机:
<bean id="blackInk" class="com.example.Ink">
<constructor-arg value="black" />
</bean>
<bean id="blueInk" class="com.example.Ink">
<constructor-arg value="blue" />
</bean>
<bean id="printer" class="com.example.Printer">
<property name="ink" ref="blackInk" />
</bean>
在这个配置中,我们有两种颜色的墨水:黑色和蓝色。在printer bean
的定义中,我们通过ref
属性指定我们想要注入的墨水的id
为"blackInk
"。
在 <property>
元素中,name
和 ref
属性有不同的作用:
name
属性指的是要注入的目标 bean
(这里是 “printer
”)的属性名。这个属性名应该在目标 bean
的类(在这个例子中是 com.example.Printer
类)中存在,且应该有相应的 setter
方法。比如,如果 name="ink"
,那么 com.example.Printer
类中需要有一个名为 ink
的属性,且有一个名为 setInk(...)
的方法。
ref
属性指的是要注入的源 bean
的 ID
。这个 ID
应该在同一份 Spring
配置文件中定义过。比如,如果 ref="blackInk"
,那么应该在配置文件中有一个 <bean id="blackInk" ... />
的定义。
因此,当你看到 <property name="ink" ref="blackInk" />
这样的配置时,你可以理解为:Spring
容器会找到 ID
为 “blackInk
” 的 bean
(在这个例子中是 com.example.Ink
类的一个实例),然后调用 com.example.Printer
类的 setInk(...)
方法,将这个 Ink
实例注入到 Printer
实例的 ink
属性中。
最后,我们可以在一个Java
类中获取printer bean
并调用其print
方法:
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Printer printer = context.getBean("printer", Printer.class);
printer.print();
这将输出以下结果:
Using black ink...
Printing...
这里体现的通过名称进行依赖注入的含义是:我们通过指定bean
的id
(在这里是"blackInk
"),来明确地告诉Spring
我们想要注入哪一个bean
。这是与通过类型进行依赖注入的一个主要区别:通过类型进行依赖注入时,Spring
会自动选择一个与目标属性类型匹配的bean
进行注入,而不需要我们明确指定bean
的id
。
3. 在三层架构中的 service 层与 dao 层体会依赖查找与依赖注入的使用
在三层架构中,我们通常会有以下三层:
- 表示层(
Presentation Layer
):与用户进行交互的层次,如前端页面、命令行等,比如Controller
类里的逻辑。 - 业务逻辑层(
Business Logic Layer
),也叫作Service
层:实现业务逻辑的地方。 - 数据访问层(
Data Access Layer
),也叫作DAO
层:直接操作数据库或者调用API来获取数据。
在本例中,我们将只关注 Service
层与 DAO
层,并且会使用到 Spring
框架。
首先,让我们定义一个业务对象,比如一个用户(User
):
public class User {
private String id;
private String name;
private String email;
// getters and setters omitted for brevity
}
接着,我们定义 DAO
层。在 DAO
层中,我们通常会有一些方法来操作数据库,如创建用户,获取用户,更新用户等。在本例中,我们简化为一个接口:
public interface UserDao {
User getUser(String id);
}
在实际项目中,我们可能有多种 UserDao
的实现,比如 MySQLUserDao
、MongoDBUserDao
等。在这里,我们简化为一个模拟实现:
public class UserDaoImpl implements UserDao {
public User getUser(String id) {
// 为了简化,我们在这里直接返回一个静态的 User 对象
User user = new User();
user.setId(id);
user.setName("Test User");
user.setEmail("test@example.com");
return user;
}
}
然后,我们定义 Service
层。在 Service
层中,我们会调用 DAO
层的方法来完成业务逻辑:
public class UserService {
private UserDao userDao;
public User getUser(String id) {
return userDao.getUser(id);
}
}
这里的问题是,UserService
需要一个 UserDao
的实例,但是 UserService
并不知道如何获取 UserDao
的实例。这就是我们要解决的问题,我们可以使用依赖查找或者依赖注入来解决。
1.依赖查找:
我们可以修改 UserService
类,让它从 Spring
容器中获取 UserDao
的实例:
public class UserService {
private UserDao userDao;
public UserService() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
this.userDao = (UserDao) context.getBean("userDao");
}
public User getUser(String id) {
return userDao.getUser(id);
}
}
这样,当我们创建 UserService
的实例时,它会自动从 Spring
容器中获取 UserDao
的实例。
2.依赖注入:
我们可以利用 Spring
框架的依赖注入特性,让 Spring
容器自动将 UserDao
的实例注入到 UserService
中。为此,我们需要在 UserService
中添加一个 setter
方法,并在 Spring
的配置文件中配置这个依赖关系:
UserService
类:
public class UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public User getUser(String id) {
return userDao.getUser(id);
}
}
Spring
配置文件(applicationContext.xml
):
<bean id="userDao" class="com.example.UserDaoImpl" />
<bean id="userService" class="com.example.UserService">
<property name="userDao" ref="userDao" />
</bean>
这样,当我们从 Spring
容器中获取 UserService
的实例时,Spring
容器会自动将 UserDao
的实例注入到 UserService
中:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) context.getBean("userService");
User user = userService.getUser("1");
如果你想在代码中获取这个bean
实例,你需要做两件事情:
-
创建一个
Spring
应用上下文(ApplicationContext
)对象。这个对象会读取你的Spring
配置文件,然后根据配置文件的内容创建和管理bean
实例。在你的代码中,new ClassPathXmlApplicationContext("applicationContext.xml")
就是在创建一个Spring
应用上下文对象。“applicationContext.xml
” 是你的Spring
配置文件的路径。 -
调用
context.getBean("userDao")
来获取 “userDao
” 这个bean
的实例。Spring
会根据你的配置创建一个UserDaoImpl
类的实例,并返回给你。
这个例子中,我们看到了如何使用依赖查找和依赖注入来获取依赖对象。在实践中,依赖注入通常是更好的选择,因为它可以更好地实现解耦,并且使得代码更加简洁和易于理解。
4. 使用注解时,依赖查找在哪里查找?依赖注入在哪里注入?
当我们全部用注解,不用xml
来实现的时候,会写成如下形式:
Ink.java
@Component
public class Ink {
public void useInk() {
System.out.println("Using ink...");
}
}
Printer.java
@Component
public class Printer {
@Resource
private Ink ink;
public void print() {
ink.useInk();
System.out.println("Printing...");
}
}
@Resource
注解告诉Spring
,请查找一个名为"ink
"的Bean
并注入到这个字段中。注意,@Resource
的名称是大小写敏感的,因此"ink
"和"Ink
"是两个不同的名字。
@Resource
注解的 name
属性应与 @Component
注解中指定的名称相匹配,如果@Component
没有指定name
属性,那么bean
的名称默认是类名的小写形式。
如果省略了@Resource
注解的name
属性,则默认的注入规则是根据字段的名称进行推断,并尝试注入同名的资源,如果Ink
类上注解为@Component("inkComponent")
,那这个bean
的名称就是inkComponent
,只能写成如下形式
@Resource(name = "inkComponent")
private Ink ink;
或者
@Resource
private Ink inkComponent;
为了使这些注解起作用,我们需要开启注解扫描@ComponentScan("com.example")
:
@Configuration
@ComponentScan("com.example")
public class AppConfig {
// 可以在这里定义其他的配置或进行其他的注解配置
}
然后,你可以在你的Java
代码中通过AnnotationConfigApplicationContext
类获取Printer
的Bean
并调用它的print
方法:
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
Printer printer = context.getBean(Printer.class);
printer.print();
输出以下结果:
Using ink...
Printing...
这个例子中,我们使用了完全基于Java
的配置,没有使用任何XML
。通过@Configuration
和@ComponentScan
注解,我们告诉了Spring
在哪里找到我们的Bean
,这个例子是按照类型进行了依赖查找,按照名称进行了依赖注入。
依赖注入中的按名称和按类型两种方式,主要体现在注入时如何选择合适的bean
进行注入。
-
按名称进行依赖注入: 是指在进行依赖注入时,根据名称来查找合适的
bean
。比如在Java
代码中使用@Resource(name = "beanName")
,或者在XML
配置文件中使用<property name="xxx" ref="beanName"/>
。这种方式的优点是明确指定了注入的bean
,但是缺点是代码与配置文件中的名称耦合,如果bean
的名称发生改变,就需要修改代码或者配置文件。 -
按类型进行依赖注入: 是指在进行依赖注入时,根据类型来查找合适的
bean
。比如在Java
代码中使用@Autowired
,或者在XML
配置文件中使用<property name="xxx"><ref bean="beanId"/></property>
,其中beanId
对应的bean
类型必须是property name
所指定类型或其子类型。这种方式的优点是代码与配置文件中的名称解耦,但是缺点是当有多个相同类型的bean
存在时,可能会导致选择错误的bean
。
至于context.getBean()
方法,这是依赖查找的方式,而不是依赖注入。它也分为按名称和按类型两种方式,与依赖注入的按名称和按类型是类似的。
-
使用
context.getBean("beanName")
,是按名称进行依赖查找。 -
使用
context.getBean(ClassType)
或者context.getBean("beanName", ClassType)
,是按类型进行依赖查找。如果有多个同类型的bean
,则会抛出异常。
5. 【面试题】依赖查找与依赖注入的对比
依赖查找(Dependency Lookup
)和依赖注入(Dependency Injection
)都是在控制反转(Inversion of Control
,IoC
)的背景下,解决对象间依赖关系的方式,但它们在实现方式上有所不同。
- 依赖查找(
Dependency Lookup
)
依赖查找是一种更为直接的方式,依赖对象在需要的时候,会直接查找(即去获取或创建)所需要的资源。这意味着依赖的类需要明确知道如何在工作环境中查找其依赖项。在Spring
框架中,这通常是通过ApplicationContext.getBean()
方法实现的。
依赖查找的主要问题是,它可能导致代码与工作环境(在这种情况下是Spring IoC容器)紧密耦合,从而降低代码的可测试性和可重用性。
- 依赖注入(
Dependency Injection
)
依赖注入则是另一种方式,依赖对象的创建和绑定由IoC
容器完成,当创建一个对象的时候,IoC
容器会自动地将该对象所需要的依赖对象通过构造器、方法或字段注入进来。这样,对象不需要知道其依赖项是如何创建和查找的,只需要声明它需要哪些依赖项即可。
与依赖查找相比,依赖注入的优点是减少了对象间的耦合度,提高了代码的可测试性和可重用性。因为对象不再需要知道如何获取其依赖项,而只需要知道它需要哪些依赖项,这样就更容易在不同的环境中重用这个对象(例如在测试环境和生产环境中)。此外,因为依赖注入是由IoC
容器管理的,因此也更容易实现如生命周期管理、AOP
(面向切面编程)等高级功能。
总结来说,尽管依赖查找和依赖注入都是解决对象依赖问题的手段,但在大多数情况下,依赖注入是更好的选择,因为它可以提高代码的解耦程度,从而使代码更加灵活、可测试和可重用。
欢迎一键三连~
有问题请留言,大家一起探讨学习
----------------------Talk is cheap, show me the code-----------------------