依赖注入,Dependency Injection,简称DI,是spring中的核心技术,此技术贯穿Spring全局,是必须要熟练掌握的知识点。
在本文中,我们将要深入研究spring中的IOC和DI,理解核心思想,并学会如何在spring boot中使用基于java和注解的方式正确使用DI来创建spring应用程序。
控制反转 IOC
要理解DI,首先需要理解spring的核心思想之一,控制反转(Inversion of Control,简称IOC),下面将通过一个简单的例子来演示什么是IOC,以及解释为什么IOC是一种优秀的思想。
首先,创建一个spring boot项目,来模拟一个简单的场景。
根据MVC的分层思想,模拟一个简单的分层(与实际开发中的分层有区别):
-
sql 层:模拟数据库映射
-
DAO 层:模拟数据库交互
-
Service 层:模拟服务层,实现主要的业务逻辑
-
定义一个接口 Sql ,使用 sql 模拟数据库。
public interface Sql {
/**
* 模拟数据库运行
*/
void run();
}
-
定义一个 sql 的实现类 MySql ,表示 MySQL 数据库。
public class MySql implements Sql{
@Override
public void run() {
System.out.println("MySQL正在运行");
}
}
-
定义一个 SqlUser 类,此类的作用是直接使用下层数据库。 SqlUser 中声明一个 MySql 对象,在构造函数中创建这个对象。注意此时创建对象的方法,是在构造函数中直接使用 new MySql() 的方式,这也是平时使用最多的方式。
public class SqlUser {
private MySql mySql;
public SqlUser() {
this.mySql = new MySql();
}
public void use() {
mySql.run();
}
}
-
定义一个 SqlService 类,此类的作用是实现业务逻辑,它可以调用 DAO 层的数据库交互对象。在此类中声明一个 SqlUser 对象,构造器中创建这个对象,在方法 service 中调用 sqlUser 的 run 方法。
-
创建一个 main 函数,执行 service 方法。
public class SqlService {
private final SqlUser sqlUser;
public SqlService() {
sqlUser = new SqlUser();
}
public void service() {
sqlUser.use();
}
public static void main(String[] args) {
SqlService sqlService = new SqlService();
sqlService.service();
}
}
-
执行主函数,查看输出结果
跟预想的一样,SqlUser正确使用了MySQL数据库。
我们现在要把这几个类都想象成一个个的对象,SqlUser是向SqlService提供服务的,而不是程序员作为上帝它们向我们提供服务。现在SqlService觉得MySql不好用,想换成SqlServer数据库,它要求SqlUser能支持SqlServer数据库。
但是现在Sql底层还不支持SqlServer怎么办?
SqlUser又去要求Sql接口能够提供SqlServer的实现。
Sql觉得这个简单,直接新增一个实现类就OK了:
public class SqlServer implements Sql{
@Override
public void run() {
System.out.println("sql server 正在运行");
}
}
SqlUser这里怎么改成对SqlServer的支持呢?
一个最笨的办法:把MySql对象全部换成SqlServer(注意思考这个过程中我们做了什么)
public class SqlUser {
private final SqlServer sqlServer;
public SqlUser() {
this.sqlServer = new SqlServer();
}
public void use() {
sqlServer.run();
}
}
SqlService不需要修改任何代码即可切换到使用SqlServer
现在看上去似乎皆大欢喜?
问题又来了,用了一段时间SqlService觉得SqlServer不好用,想切换回MySql怎么办?
照之前的办法,SqlUser中的所有SqlServer对象还得全部换成MySql,我们现在只有十行代码,觉得还可以接受,如果项目后期,SqlUser中有一千行代码呢,一百个对SqlServer的引用怎么办?
聪明一点的办法:使用多态。SqlUser中SqlServer的声明改为Sql接口,构造器中还是给sql new一个SqlServer对象,后面的逻辑代码中调用Sql对象接口,这样的话如果再需要更改sql的实现,只需要在构造器中将new SqlServer改为new MySql即可,需要修改的代码量大大减少。
public class SqlUser {
private final Sql sql;
public SqlUser() {
this.sql = new MySql();
}
public void use() {
sql.run();
}
}
虽然现在SqlUser自身代码的耦合性降低了,但是在业务逻辑上看,SqlUser与Sql层的耦合性还是很高,因为SqlService每次想要调用不同的服务,SqlUser都需要去修改代码。
这个时候SqlUser不服了,为什么每次SqlService的需求在变,却要我修改代码?
有没有一种方法,SqlService想要SqlUser实现哪个功能就调用哪个功能,SqlUser只用躺平,再也不用参与到SqlService与Sql的爱恨情仇之中了呢?(这里可能有人会问,如果这样的话,把SqlUser层存在的意义是什么呢?答案是解耦。想一下,如果SqlService直接去调用Sql,那SqlService的处境不是和现在的SqlUser一样了吗?每次有新的需求都需要去修改大量的代码。)
既然这么问了,那肯定是有的,这就是本节的主题IOC了,如果把SqlUser改成这样:
public class SqlUser {
private final Sql sql;
// 区别在这
public SqlUser(Sql sql) {
this.sql = sql;
}
public void use() {
sql.run();
}
}
仔细对比一下两种构造器的区别
// 旧
public SqlUser() {
this.sql = new MySql();
}
// 新
public SqlUser(Sql sql) {
this.sql = sql;
}
仔细思考一下哪里不同!
旧的构造器中,sql对象的控制权在SqlUser自己的手里,指定哪一个实现由SqlUser控制。
新的构造器中,sql对象的控制权在调用SqlUser的对象手里,调用SqlUser的对象想要使用哪一个实现就在构造器中传入哪一个实现。
完成了控制权的反转!这就是IOC。
但构造器只是IOC实现的一种方式,还有其他的实现方式会在后面的小节中讨论。
现在看看SqlService使用SqlUser对象的方式有哪些改变:
public class SqlService {
private final SqlUser sqlUser;
public SqlService() {
// 区别在这
sqlUser = new SqlUser(new MySql());
}
public void service() {
sqlUser.use();
}
public static void main(String[] args) {
SqlService sqlService = new SqlService();
sqlService.service();
}
}
现在SqlService构造器中需要明确指定想要使用的Sql实现是哪一种,并传入一个实现对象,这时SqlUser已经完全解脱了,自己什么都不用做,就可以轻松应对SqlService的各种需求。
那么有人又要问了,这么做SqlUser是解脱了,但是SqlService的使用不是变麻烦了吗?那不是一样的吗?
其实非也。对于SqlService来说,它的自主性更高了,以前它只能使用SqlUser提供的功能,而现在它可以自由选择。把SqlUser想象成社交软件,把SqlService想象成我们用户,按照之前的方式我们只可以使用软件提供的昵称、头像,软件给你什么你就用什么,而现在我们可以自定义,想用什么就用什么,孰优孰劣一想便知。
依赖注入 DI
搞清楚什么是IOC之后,可以来看看DI了。IOC只是一种设计模式,一种思想,IOC有很多的实现方式,DI就是IOC的一种强大的实现方式。
再来看上一小节的例子,SqlUser实现了控制反转,但是SqlService还没有,SqlUser对象的控制权还是在自己的手中。那么SqlService对SqlUser的控制权可以交给谁呢?答案是IOC容器。
继续思考,对于上面的这个SqlService的使用变麻烦的问题,我给出的解释是SqlService的自主性变高了,但不可避免的是,使用确实变得复杂了,以前只需要新建一个SqlUser对象,现在还需要再新建一个Sql的实现对象。
这就引申出另一个问题,在创建SqlUser对象之前,必须先有Sql实现对象,它们之间形成了依赖关系。
如果项目变得更庞大,依赖关系复杂起来,可能会变成这个样子
可以看出,对象之间的耦合性还是很高,如果一个被依赖的对象没有被创建,那么这个依赖的对象也无法成功运行。
对象之间的依赖关系生活中有许多形象的例子,而且生活中的做法已经给出了答案。
我要举的例子就是对象..."找对象"的那个对象,就说相亲吧。
一个单身男性想要找女朋友,他需要先有一个目标,一个单身女性。才可以去做其他的事情,比如追求、表白、求婚、结婚等等。但是他也是有要求的,身高、颜值等等,这就导致这个单身女性目标不好找,没有目标的话,追求、表白等等这些事情也就无法进行。他觉得目标实在太难找了,就去找了婚介所,里面有很多的单身女性,婚介所可以按照你的要求挑选合适的目标,同时他也作为婚介所中的单身男性之一,可以被婚介所提供给其他单身女性。
很好理解吧?现在就使用面向对象的方法解释一下这个例子。
把男性看作是一个对象man,man有一些方法,这些方法是追求、表白、求婚、结婚,但是man依赖于另一个对象woman,如果woman没有创建,man就没法执行这些方法,我们就说man和woman之间的耦合性很高。把婚介所看作一个容器container,container里有许许多多各种各样的man和woman,因为他们有不同的特征,所以代表了man和woman接口的实现或者基类的继承。
man现在想要执行结婚方法,container就会把man想要的woman给man,实现上称为把woman注入到man,而且如果有其他的woman对象依赖与man对象,container就可以把man注入给需要的woman,这样,man与woman之间就实现了解耦。
现在让我们回到Sql的恩怨情仇中,通过上面生动形象的讲解,应该可以理解这幅图了:
创建一个公共的容器,所有创建的对象都放到容器中,如果有对象对其他对象产生了依赖,容器将主动将依赖的对象给予被依赖的对象。这样也就实现了SqlService的控制反转,现在SqlUser的控制权交给了容器。这个容器在Spring中就被称为IOC容器。IOC容器中的对象就称为Bean。
使用Spring提供的容器之前,我们先来自己实现一个简单的容器。
public static void main(String[] args) {
// 容器,键值对为对象名:对象
Map<String, Object> container = new HashMap<>(20);
// 向容器中注册对象
container.put("MySql", new MySql());
container.put("SqlServer", new SqlServer());
// 从容器中取出需要的依赖
MySql mySql = (MySql) container.get("MySql");
// 使用依赖向容器中注册对象
container.put("SqlUserMySql", new SqlUser(mySql));
// 取出依赖
SqlUser mySqlUser = (SqlUser) container.get("SqlUserMySql");
// 使用依赖注册对象
container.put("SqlService", new SqlService(mySqlUser));
// 取出要使用的对象
SqlService sqlService = (SqlService) container.get("SqlService");
// 使用对象
sqlService.service();
}
在本例中,我们用一个Map来模拟容器,并向容器中提交了两个Sql对象MySql、SqlServer。由于SqlUser依赖于Sql,所以在创建SqlUser之前,先从容器中取出MySql对象,在构造函数中使用MySql对象来创建SqlUser,并注册到容器中。创建SqlService的过程就如法炮制了。
在这个过程中可以看到,对象与对象之间的依赖关系变成了对象与容器之间的依赖关系。从对象存不存在变成了容器里有没有这个对象。
在实际的开发中,我们不需要手动的注册、取出这些对象,spring将会使用IOC容器自动帮我们完成这些操作。这个过程就称为依赖注入。
使用spring的依赖注入
spring依赖注入目前最流行的方式是基于注解的自动配置
优点:配置简洁、清晰
缺点:对象间依赖关系太复杂时会头疼
除了基于注解的自动配置外,还有两种配置方式
Java配置:也是使用注解和类,与注解自动配置类似,理论上可以互相替换。
XML配置:比较老的配置方式,使用xml文件配置
没有特殊情况,都使用注解的自动配置。
自动配置主要依赖于两个注解:
-
@Component:标记这个类是一个组件,此类将自动向容器注册Bean对象。
-
@Autowired:可作用于属性、构造器或set方法,容器将自动注入对象。
@Component
此注解的定义如下
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
String value() default "";
}
此注解有三个拓展注解,用于标记MVC分层中的不同层,与@Component注解的作用相同,只是为了增加代码的可读性,@Component注解则表示这是一个普通的Bean。
-
@Controller:控制层
-
@Service:服务层
-
@Repository:DAO层
当这四种注解之一作用于一个类时,这个类就会被自动识别为Bean,启动Spring Boot程序时,容器中就会存在这个Bean了。
在IDEA中加上此注解之后,左侧会有一个小图标的提示,表示这是一个spring bean
@Autowired
此注解的定义如下
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}
此注解的官方定义如下
Marks a constructor, field, setter method, or config method as to be autowired by Spring's dependency injection facilities.
标记由Spring的DI自动注入的构造器、字段、set方法或者配置方法
其中提到了三种比较常用的自动装配方法:
-
基于构造器的自动装配
-
基于setter方法的自动装配
-
基于字段的自动装配
注意此注解有一个required参数,默认为true,如果容器中没有相应的bean,就会报错;当设置为false时,如果容器中没有相应的bean,不会报错,也不会注入。
基于构造的自动装配
将@Autowired注解作用于一个类构造器,在对象创建之前调用构造器,可以完成对类中属性的自动注入,构造器的参数就是依赖的对象。
-
一个类中 只能有一个构造器使用了@Autowired注解标记
-
如果没有构造器使用@Autowired注解且存在多个构造器,将选择依赖数量最多的构造器完成注入
-
如果没有构造器使用@Autowired注解且只存在一个构造器,将选择这个默认的构造器
-
使用此方法注入的字段可以设置为final类型
@Repository
public class SqlUser {
private final Sql sql;
/**
* 基于构造器的自动装配
*/
@Autowired
public SqlUser(MySql sql) {
this.sql = sql;
}
public void use() {
sql.run();
}
}
基于setter方法的自动装配
将@Autowired注解作用于一个set方法,可以完成对类中属性的自动注入,set方法的参数就是依赖的对象。
-
类中的任意方法都可以完成注入,对名称、参数没有要求,setter方法是一种特殊情况,也是最常用的方法
-
对象创建之后才会调用setter方法,因此字段不能设置为final类型
@Repository
public class SqlUser {
private Sql sql;
/**
* 基于构造器的自动装配
*/
@Autowired
public void setSql(MySql sql) {
this.sql = sql;
}
public void use() {
sql.run();
}
}
基于字段的自动装配
这种方法最简单,将@Autowired注解作用于类中的一个字段即可
-
装配时机是调用构造器之后、调用配置方法之前,因此也不可以将字段设置为final类型
@Repository
public class SqlUser {
@Autowired
private MySql sql;
public void use() {
sql.run();
}
}
三种方法的总结与比较
在IDEA中使用这三种方法时都会有DI的标志
可以根据这个判断程序的依赖注入是不是正确
在使用基于字段的自动配置时,IDEA会提示
Field injection is not recommended
原因是现在spring boot已经不建议使用这种注入方法
这是为什么呢?下面就来看看这种方法有哪些缺点。
基于字段自动注入的缺点:
-
不能有效指明类的依赖 。使用构造器的方式将明确指明类需要哪些依赖,使用Setter的方式表示此属性为可选依赖,而使用基于属性的方式很容易让程序员忽略需要的依赖,当bean容器中不包含此依赖时,程序将无法正常运行。
-
违反了单一职责设计原则 。使用这种方法会不自觉地给类增加过多的依赖,将违反单一职责原则。
-
增加了耦合性 。使用这种方法意味着你不能向一个接口类注入它的任意实现类,因为同时具有多个实现类时,IOC将不知道注入哪一个。
至于另外两种方法的使用,都可以达到相同的效果,下面是一些建议
-
对于必须的依赖,使用构造器注入,并将相应字段设置为final,可以指导使用别人正确地构造对象
-
对于可选的依赖,使用setter注入,@Autowired注解中设置 required=false
最后
在我们例子的各类中增加@Component、@Autowired,将这些对象都交给IOC容器管理
@Component
public class MySql implements Sql{
@Override
public void run() {
System.out.println("MySQL正在运行");
}
}
@Repository
public class SqlUser {
private final Sql sql;
@Autowired(required = false)
public SqlUser(MySql sql) {
this.sql = sql;
}
public void use() {
sql.run();
}
}
@Component
public class SqlService {
private final SqlUser sqlUser;
@Autowired
public SqlService(SqlUser sqlUser) {
this.sqlUser = sqlUser;
}
public void service() {
sqlUser.use();
}
}
在spring boot的启动类中添加如下代码
@SpringBootApplication
public class Demo1Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Demo1Application.class, args);
SqlService service = context.getBean(SqlService.class);
service.service();
}
}
关于启动类的问题,不是本文的主题,就不在此讨论了。只需要关注一个方法:
getBean(SqlService.class)
此方法其实就是利用反射主动从IOC容器中获取Bean对象。
我们运行这个启动类。
可以看到,整个程序没有使用到一个new,就完成了所有对象的管理,这就是Spring的依赖注入!