「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」。
1 依赖注入
Spring的依赖注入,是一个老生常谈的知识点,正常开发时候基本没卵用,毕竟2021年了,都习惯了注解方式来注入其它依赖,但面试时候或许会问到这个知识。
这个知识点网上有很多,百度看了几篇,发现很多文章关于这块讲解都是重复且错误的,给人一种很混乱的感觉。说一下我的理解吧,有不对的地方还请直接指出来,一起提高技术水平。
控制反转(IOC)是程序设计的一种思想,它解决了对象之间的耦合问题,使代码的扩展性更高,Spring也是通过这一种思想来管理Bean之间的依赖关系的。 而要实现IOC这一思想,依赖注入(DI)和依赖查找就是其中两种有效的方式。依赖查找在早期Spring版本使用过,后期因为对用户太不友好,基本停止了这种方式。所以DI依赖注入成了实现IOC的最佳实现,这也就是IOC和DI之间的关系。
2 Spring应用层面上有几种依赖注入的方式
这个问题本质其实就是说如何让程序知道两个bean之间的依赖关系,如何确定这种关系? 从时间轴来说可能会更好理解点,早期的Spring是基于xml来让程序知道对象间的依赖关系的,那时候还没有注解。 所以仅通过xml如何让程序知道对象间的依赖关系呢? 不考虑Spring,我们先自己想想Java层面上要让两个对象产生依赖关系,从类的组成(属性/构造器/方法)上无非也就这么三种入口。
构造器的参数 属性(通过setter方法的参数) 方法参数(通过普通常规方法的参数,和属性类似,都是通过方法参数产生依赖) Spring其实也是从这三个方向处理bean之间的依赖,先看看官网(Version 5.2.7.RELEASE)的介绍。
其中标记的地方也就是Spring去实现依赖注入的方式。不过这三种方式其实是早期的Spring通过XML这种配置文件来定义bean的。
- 构造器参数
- 工厂方法参数
- 构造器或者工厂方法创建对象后,通过类似setter方法注入依赖 最后的标记表示依赖注入相对于用户来讲,最主要的方式就是构造器和setter方式。
分别举一个简单的示例吧。
3、Constructor-based Dependency Injection
The Spring team generally advocates constructor injection as it enables one to implement application components as immutable objects and to ensure that required dependencies are not null. Furthermore constructor-injected components are always returned to client (calling) code in a fully initialized state.
简单的翻译一下:这个构造器注入的方式啊,能够保证注入的组件不可变,并且确保需要的依赖不为空。此外,构造器注入的依赖总是能够在返回客户端(组件)代码的时候保证完全初始化的状态
普通bean,对象StudentDao通过构造器传入TeacherDao,使两个类产生依赖的关系。
public class StudentDao {
private TeacherDao teacherDao;
public StudentDao(TeacherDao teacherDao) {
this.teacherDao = teacherDao;
}
public void getStudentInfo() {
teacherDao.getTeacher();
}
}
public class TeacherDao {
public void getTeacher() {
System.out.println("TeacherDao#getTeacher");
}
}
3.1 配置类导入xml配置文件
@Configuration
@Component
@ComponentScan("com.binghuazheng.springioc.beaninit")
@ImportResource("/SpringConfig.xml")
public class AppConfig {
}
3.2 xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:abc="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="studentDao" class="com.binghuazheng.springioc.beaninit.bean.StudentDao">
<constructor-arg ref="teacherDao"/>
</bean>
<bean id="teacherDao" class="com.binghuazheng.springioc.beaninit.bean.TeacherDao"/>
</beans>
3.3 测试
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(AppConfig.class);
context.refresh();
StudentDao studentDao = context.getBean("studentDao", StudentDao.class);
studentDao.getStudentInfo();
}
}
4 Setter-based Dependency Injection
一定要注意这种setter的方式注入依赖,被注入的bean一定要有setter方法,并且setXXX方法的XXX名字(首字母小写)一定要和xml中配置的property元素的属性name相匹配。 也就是说此示例中的xml中的studentDao依赖属性teacherDao名称要和StudentDao类中的setTeacherDao方法相匹配。他和成员变量private TeacherDao teacher;中的属性名没有关系
public class StudentDao {
private TeacherDao teacher;
public TeacherDao getTeacherDao() {
return teacher;
}
public void setTeacherDao(TeacherDao teacher) {
this.teacher = teacher;
}
public void getStudentInfo() {
teacher.getTeacher();
}
}
public class TeacherDao {
public void getTeacher() {
System.out.println("TeacherDao#getTeacher");
}
}
4.1 配置类
@Configuration
@Component
@ComponentScan("com.binghuazheng.springioc.beaninit")
@ImportResource("/SpringConfig.xml")
public class AppConfig {
}
4.2 xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:abc="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="studentDao" class="com.binghuazheng.springioc.beaninit.bean.StudentDao">
<property name="teacherDao" ref="teacherDao"/>
</bean>
<bean id="teacherDao" class="com.binghuazheng.springioc.beaninit.bean.TeacherDao"/>
</beans>
4.3 测试
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(AppConfig.class);
context.refresh();
StudentDao studentDao = context.getBean("studentDao", StudentDao.class);
studentDao.getStudentInfo();
}
}
5 Factory Method Dependency Injection
工厂方法创建对象,下面的示例用的是静态工厂方法(factoroy-method),需要指定静态方法。 还有一个factory-bean方式,暂时不做示例了,这种可以指定一个其它对象的非静态方法产生bean.
StudentDao类需要指定一个静态方法作为产生bean的工厂方法。
public class StudentDao {
private TeacherDao teacherDao;
public static StudentDao createInstance(TeacherDao teacherDao) {
StudentDao studentDao = new StudentDao();
studentDao.setTeacherDao(teacherDao);
return studentDao;
}
public void setTeacherDao(TeacherDao teacherDao) {
this.teacherDao = teacherDao;
}
public void getStudentInfo(){
teacherDao.getTeacher();
}
}
public class TeacherDao {
public void getTeacher() {
System.out.println("TeacherDao#getTeacher");
}
}
5.1 配置类
@Configuration
@Component
@ComponentScan("com.binghuazheng.springioc.beaninit")
@ImportResource("/SpringConfig.xml")
public class AppConfig {
}
5.2 xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:abc="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="studentDao" class="com.binghuazheng.springioc.beaninit.bean.StudentDao" factory-method="createInstance">
<constructor-arg ref="teacherDao"/>
</bean>
<bean id="teacherDao" class="com.binghuazheng.springioc.beaninit.bean.TeacherDao"/>
</beans>
5.3 测试
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(AppConfig.class);
context.refresh();
StudentDao studentDao = context.getBean("studentDao", StudentDao.class);
studentDao.getStudentInfo();
}
}
6 答疑
好了,相信已经园友们知道了构造器注入的好处,那么回到了在前面提到的问题:
Q1:跟3.x里说的一样,我要是有大量的依赖要注入,构造方法不会显得很臃肿吗?
对于这个问题,说明你的类当中有太多的责任,那么你要好好想一想是不是自己违反了类的单一性职责原则,从而导致有这么多的依赖要注入。
Q2:是不是其他的注入方式都不适合用了呢?
当然不是,存在即是合理!setter的方式既然一开始被Spring推荐肯定是有它的道理,像之前提到的setter的方式能用让类在之后重新配置或者重新注入,就是其优点之一。除此之外,如果一个依赖有多种实现方式,我们可以使用@Qualifier ,在构造方法里选择对应的名字注入,也可以使用field或者setter的方式来手动配置要注入的实现。
7 Spring构造注入不需要加@Autowired?
@Autowired并不是必须的,不加也能注入成功,这是为什么? 在 Spring4.x 中增加了新的特性:如果类只提供了一个带参数的构造方法,则不需要对对其内部的属性写 @Autowired 注解,Spring 会自动为你注入属性。
@RestController
@RequestMapping("/user")
public class UserController {
private UserService userService;
public UserController(UserService userService;) {
this.userService= userService;
}
//简单的使用例子,下同
public List<User> userList() {
return userService.list();
}
}
8 总结
官方推荐理由
单一职责: 当使用构造函数注入的时候,你会很容易发现参数是否过多,这个时候需要考虑你这个类的职责是否过大,考虑拆分的问题;而当使用@Autowired注入field的时候,不容易发现问题依赖不可变: 只有使用构造函数注入才能注入final依赖隐藏:使用依赖注入容器意味着类不再对依赖对象负责,获取依赖对象的职责就从类抽离出来,IOC容器会帮你自动装备。这意味着它应该使用更明确清晰的公用接口方法或者构造器,这种方式就能很清晰的知道类需要什么和到底是使用setter还是构造器降低容器耦合度: 依赖注入框架的核心思想之一是托管类不应依赖于所使用的DI容器。换句话说,它应该只是一个普通的POJO,只要您将其传递给所有必需的依赖项,就可以独立地实例化。这样,您可以在单元测试中实例化它,而无需启动IOC容器并单独进行测试(使用一个可以进行集成测试的容器)。如果没有容器耦合,则可以将该类用作托管或非托管类,甚至可以切换到新的DI框架。
简单总结使用构造器注入的好处:
- 保证依赖不可变(final关键字)
- 保证依赖不为空(省去了我们对其检查)
- 保证返回客户端(调用)的代码的时候是完全初始化的状态
- 避免了循环依赖
- 提升了代码的可复用性