这篇文章,让我们讨论一下Spring boot中的依赖注入(Dependency Injection)和相关注解(Annotations)的使用。
演示例子源代码 演示案例
概念
Spring Boot的一个重要概念和实现--依赖注入。我列举了几个和依赖注入相关的概念,面向接口编程、IOC和依赖倒转。这些都是Spring Boot 依赖注入机制实现的理论基础。更多的了解这些概念将有助于深入理解Spring Boot 的依赖注入。
由于讲述所有这些概念是另一个更大的关于设计模式的话题,以后有机会抽时间再写一下相关文章。有兴趣的朋友也可以给我留言,大家共同讨论。
相关概念解释:
- 面对接口编程
IOP: Interface Oriented Programming
系统应该依赖接口(Interface)或抽象类进行编程,而不是具体的实现类(Class),即将抽象和具体分离,以便增强系统的可扩展性。 - 依赖倒转
IOC: Inverse of Control
面向对象的一种设计思想,它提出在设计中对象交由一个容器控制在运行时动态创建和管理,而不是在设计时由程序代码直接管理对象。 - 依赖注入:
DI: Dependency Injection
在IOC的设计思想基础上,DI提供对象管理容器并在运行时
提供相关查找、定位、初始化、装配对象的过程。也就是,你在写代码的时候只需要关心接口,不需要关心对象如何被装配的。
依赖注入
解决的问题
在一个应用系统中,通常会存在分层、分模块的情况,通常一个系统都有由很多不同模块共同合作构建而成。比如,经典的三层模式,它包括服务层、业务层、数据层,层次之间存在大量的对象引用(依赖)。这导致:
- 存在大量的对象强依赖(引用),不利于维护、变更和扩展。
- 设计时,需要关注大量对象的创建,这是一件繁杂且需要耐心的事情。
- 系统维护随着系统变大变得更加艰难。
- 如果不使用依赖注入很难做到面向接口编程,包和类的引用管理都需要人为或程序进行管理。
依赖注入就是解决上述问题的。通过依赖注入机制,程序员在编程时(设计时)只需要面向接口进行编程,而无需关心具体实现对象如何被初始化和加载的(装配过程)。
原理
“没有对比就没有伤害。”,我们来对比下面两种编程模式,以便知道为什么需要依赖注入。
第一种模式中,上层的调用方(Controller)需要明确知道下层的实际实现类(OrderService)以及明确所在的包。这个方案的最大问题就是耦合度太高,一旦程序需要更改,调用方和被调用方都需修改代码,并且存在对包的版本管理。
第二种模式中,类的实现和接口的定义被分割到不同的包中。在
设计时
,上层的调用方(Controller)仅需要知道接口(OrderService)的存在,至于需要加载哪个包,使用哪个具体的实现类是不需要关心的。具体的实现类的引用、初始化被“延迟”到程序运行时
。程序运行时可利用Factory模式或者依赖注入机制选择具体的实现类并加载(延迟确定)。由于在运行时动态确定和装配,使得程序灵活度更高,通过配置就可以更改和替换逻辑和规则,实现内聚合、外开放
。
示意图
接下来,看看依赖注入机制都做了什么。
以上图例中:
- OrderServiceImpl类实现了OrderService接口。
- OrderControler作为上层模块依赖OrderService接口。
- 1、2的信息在
设计时
通过多种可能的途径被配置,通常依赖注入机制会提供相应的配置机制。 - 依赖注入机制在程序启动
运行时
(runtime),读取相关配置信息并分析、定位和装配相关对象。 - 最后,当OrderController被调用的时候,如期得到来之OrderServiceImpl的支持。
在上述过程中,借助依赖注入机制,运行时程序也只关注于接口定义,依赖注入容器会在运行时完成自动装配过程。
- 设计时,OrderController作为上层调用者只需要知道OrderService接口的存在,并做相关“依赖配置”。
- 运行时(runtime),依赖注入机制根据配置信息,自动
装配
所需对象。
原理对比
为更加清楚的说明,我们将依赖注入机制和自行车的制造过程做一个类比。下图描述了自行车从设计到生产的大概过程。
- 自行车的设计(
设计时
)类似我们编写代码的时候定义的接口、逻辑、以及配置文件等等。 - 自行车厂家做完自行车设计后,将零件设计标准提供给零配件厂家,厂家按照设计标准进行生产。当然,在符合标准的情况厂家也可以做一些调整,比如,颜色。这个过程类似我们将符合接口的实现类分解到不同的包中,或者请“外部团队”根据接口要求编写和构建包。
- 装配车辆的过程(
运行时
)就是将零配件按照设计进行实际装配。这类似Spring Boot的依赖注入机制,它根据接口的定义、实现类(制作好的零件)以及相关的配置文件在程序运行时进行动态装配。
上面生产自行车的过程如果用代码编写出来的话,我们只需要定义好做好设计标准并生产出相应的零件就好,自行车的装配工作就交给依赖注入机制完成就好了。
演示例子源代码 演示案例
Spring boot 依赖注入
看完前面的描述,spring boot 依赖注入机制就显得很简单。它提供了一个容器( spring container)管理一堆的bean对象。spring container通过“配置信息“知道如何将一个普通对象变成一个bean对象并放入到容器中。同时,spring container也知道如何装配一个对象到另一个对象中(可能是字段、构造函数或属性)
一个bean对象,记录了被注入的对象的类型、如何初始化以及这个对象依赖其他的哪些对象(严格意义上来说应该是哪些bean对象)。
容器如何知道哪些类型需要被转变成bean呢?这就是下面我们需要讲的部分。
spring boot只在应用程序启动的时刻注入所有的bean对象以及进行装配。
注解(Annotation)
Spring container依赖三种配置方式收集哪些类型需要被转化成bean对象并注入到spring container中进行管理。
注解方式,是spring boot 依赖注入中三种静态bean配置其中的一种。下面列举依赖注入的三种配置方式。
配置bean(注入bean)的方式
- 通过注解进行配置
这章节讲述的
- 通过编码方式,使用@Configuraiton的方式进行配置,它可以取代下面基于xml的配置方式。
- 通过bean xml配置文件形式配置 (现代应用程序中很少使用这样方式)
相关的注解
使用注解方式配置依赖注入bean配置一般涉及到以下几类注解。
用于把对象配置为bean对象的标签
- @Component
- 它由Spring boot框架,是元注解,可以用于标注其他注解(元注解)
- 它是@Service, @Controller, @Respository的注解标签
- 被@Component的注解的对象以及被它注解的注解,如@Service, @Controller, @Respository都将被组件扫描(Component scanning)过程收集注册为bean对象。
- @Controller, @Service, @Respository
- 它们在经典三层模式中带有特殊含义,分别代API接口组件、服务组件和持久化组件。
- 其用途和@Component相当。
- @Primary
- 在一个接口有多个实现的情况下,通过@Primary指定默认实现此接口的实现类作为主要的bean。
- @Order
- 控制bean的加载次序,有时候一个对象需要另一个对象被加载后在能被加载。
- Conditional 【单独描述和举例】
- Spring 4.0引入的条件注解,满足某个特定条件下创建特定的Bean。而我们实现的方式就是实现Condition接口。
用于指示需要装配bean对象的标签
- @Autowired 和 @Qualifier
- 上面两各标签由Spring boot框架
- @Autowired通过类型查找bean并进行装配,如果同一个类型有多个bean,将引发错误。
- 如果@Autowired和@Qualifier一起使用将使用名字的方式查找bean并装配,如果没有找到,将报错。
- @Resource
- 属于JDK提供的标签,位于javax.annotation.Resource, 最早实现的DI是作为JDK的一部分。
- 默认用名字来查找bean并进行装配,如果查找失败,将回退到@Autowired通过类型查找并装配。
第一个例子
这个例子中将基于典型三层,使用 @Autowired、@Compoent、@Service, @Controller, @Respository的注解进行bean对象的配置和装配。
演示例子源代码 演示案例
例子讲解
-
项目结构
从图中可以看出来,这是一个典型的三层结构的 rest api项目。
截屏2020-02-1800.16.59.png 先从定义 bean开始
UserService接口
package com.juqimiao.di.demos.d01.daos;
public interface UserDao {
String select(String name);
}
定义UserDaoImpl实现类,它实现了UserDao接口,并且将这个类打上@ Repository注解。它将被作为bean注入到spring container 中。
@Repository
public class UserDaoImpl implements UserDao {
@Override
public String select(String name) {
return "User's information " + name;
}
}
接下来,我们定义UserService接口。
package com.juqimiao.di.demos.d01.services;
public interface UserService {
String getInfo(String name);
}
定义UserServiceImpl实现类,它实现了UserService接口,并且在这个类上面大上了@Service注解。它将被作为bean注入到spring container 中。
package com.juqimiao.di.demos.d01.services.Impl;
import com.juqimiao.di.demos.d01.daos.UserDao;
import com.juqimiao.di.demos.d01.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
/**
* 演示构造函数注入方式。
* @param userDao {@link UserDao}
*/
@Autowired
public UserServiceImpl(UserDao userDao){
this.userDao = userDao;
}
@Override
public String getInfo(String name) {
return userDao.select(name);
}
}
你看到了代码中,类的构造函数上被打上了@Autowired注解,构造函数中有一个参数 “UserDao userDao”。这意味着,当程序运行的时候,UserDaoImpl的一个实例会被自动装配给这个参数。
最后,我们创建UserController,这是典型的rest api的Controller。
package com.juqimiao.di.demos.d01.controllers;
import com.juqimiao.di.demos.d01.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/info")
public String getInfo(@RequestParam String name){
return userService.getInfo(name);
}
}
这里,你可以看到@Autowired注解被打在了userService 字段上面,意味着UserServiceImpl对象将被装配到这个字段上。
同时,我们也在项目中通过,CustomerController/CustomService和CustomerDao演示了@Component是如何工作的。
例子小结
- @Service、@ Repository和@Component都用于声明一个对象作为bean对象的注解。
- @Autowired告诉spring container它所需要依赖的bean并要求container自动查找并装配。
- bean可以通过 构造函数、字段、属性进行装配,但装配的bean对象并不能保证一定是不为null的。
- UserServiceImpl和UserController中相应引用的是UserDao和UserServic接口,而不是具体实现类。因为,如果直接指向具体实现类,依赖注入本身其实就失去了意义,毕竟它是为面向接口编程而生的。