依赖注入(Dependency Injection)
依赖注入是这样一个过程:对象仅通过在构造器参数、工厂方法参数或对象实例(构造器或者工厂方法返回的)中设置属性来定义它们的依赖项,然后容器在创建
Bean
时注入这些依赖项。这个过程从根本上说是Bean
本身的逆过程,所以叫做控制反转
。
- 首先我们定义一个配置类
@Configuration
public class MyConfigOfDI {
}
- 并以这个配置类的配置创建一个
IoC 容器
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfigOfDI.class);
- 接着定义几个
Bean
@Controller
public class BookController {
}
@Service
public class BookService {
}
@Repository
public class BookDao {
}
@Component
public class Application {
}
- 使用
@ComponenScan
将组件扫描进IoC 容器
也可以使用其他方式,诸如:
@Import
等,可以查看 Spring - 组件(Beans)注册(到 IoC 容器)的几种方式 这篇文章
@Configuration
@ComponentScan(basePackages = "com.keke.web")
public class MyConfigOfDI {}
基于注解的依赖注入
@Autowired
注入原理在
AutowiredAnnotationBeanPostProcessor
中,关于BeanPostProcessor
的原理可以查看Spring - IoC 容器之拓展点 BeanPostProcessor
最简单的自动注入:
@Service
public class BookService {
@Autowired
private BookDao bookDao;
}
在 BookService
中定义输出 BookDao
的方法:
@Service
public class BookService {
@Autowired
private BookDao bookDao;
public void print() {
System.out.println(bookDao);
}
}
输出 BookService
中的 BookDao
,同时输出容器中的 BookDao
:
@Test
void testDI() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfigOfDI.class);
BookService bookService = applicationContext.getBean(BookService.class);
// 输出 BookService 中的 BookDao
bookService.print();
BookDao bookDao = applicationContext.getBean(BookDao.class);
// 输出容器中的 BookDao
System.out.println(bookDao);
}
// 输出结果
com.keke.web.dao.BookDao@b7c4869
com.keke.web.dao.BookDao@b7c4869
从输出结果可以看出注入到 BookService
中的 BookDao
,就是容器中的 BookDao
@Qualifier
修改 BookDao
的定义,增加一个 label
属性,默认为 1
,以区分不同的 Bean
:
@Repository
public class BookDao {
private Integer label = 1;
public BookDao() {
}
public Integer getLabel() {
return label;
}
public void setLabel(Integer label) {
this.label = label;
}
@Override
public String toString() {
return "BookDao{" +
"label=" + label +
'}';
}
}
在容器中定义另一个类型为 BookDao
的 Bean
名称为 bookDao2
,同时设置 label
属性为 2
,以区分容器中默认的名称为 bookDao
的 Bean
:
@Configuration
@ComponentScan(basePackages = "com.keke.web")
public class MyConfigOfDI {
@Bean("bookDao2")
public BookDao bookDao() {
BookDao bookDao = new BookDao();
bookDao.setLabel(2);
return bookDao;
}
}
此时,输出 BookService
中的 BookDao
,应该是 label
为 1
的 Bean
:
因为
@Autowired
默认是先去容器中查找类型为BookService
的Bean
,如果存在多个,再根据属性名称取匹配Bean
的名称。这里注入的Bean
名称是bookDao
,所以输出label
为1
的Bean
BookDao{label=1}
如果我们想在 BookService
中注入名称为 bookDao2
的 Bean
,可以使用 @Qulifier
注解指定名称
@Service
public class BookService {
@Qualifier("bookDao2")
@Autowired
private BookDao bookDao;
public void print() {
System.out.println(bookDao);
}
}
// print() 方法输出
BookDao{label=2}
@Primary
当容器中有多个同一类型的 Bean
,而我们又没有指定注入的 Bean
的名称时,这时就会报错:
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.keke.web.dao.BookDao' available: expected single matching bean but found 2: bookDao,bookDao2
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1200)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:420)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:350)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:343)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1127)
可以使用 @Primary
指定如果存在同一类型的多个 Bean
时,首选哪个:
@Configuration
@ComponentScan(basePackages = "com.keke.web")
public class MyConfigOfDI {
@Primary
@Bean("bookDao2")
public BookDao bookDao() {
BookDao bookDao = new BookDao();
bookDao.setLabel(2);
return bookDao;
}
}
在 bookDao2
的 Bean
上面加了 @Primary
注解,这样在 BookService
使用最简单的 @Autowired
获取
@Service
public class BookService {
@Autowired
private BookDao bookDao;
public void print() {
System.out.println(bookDao);
}
}
// 输出
BookDao{label=2}
因为在 bookDao2
上面注释了 @Primary
注解,所以这里依赖注入时发现多个同一类型的 Bean
其中有 @Primary
的就会首选进行注入
不允许在同一类型的
Bean
上同时存在多个@Primary
注解
如果仍然想要在 BookService
中注入名称为 bookDao
的 Bean
,可以使用 @qualifier
指定 Bean
名称
@Service
public class BookService {
@Qualifier("bookDao")
@Autowired
private BookDao bookDao;
public void print() {
System.out.println(bookDao);
}
}
// 使用 print 方法输出
BookDao{label=1}
@Resource(jsr-250)
@Resource
注解虽然是 JavaEE 提供的,Spring 同样支持用它来管理Bean
。默认是按照名称来查询IoC 容器
中的组件来完成依赖注入的,同时提供了一个name
属性来自定义要注入的组件名称。
@Service
public class BookService {
// 首先寻找名称为 bookDao 的组件来完成注入
@Resource
// 或者指定要注入的组件的名称
// @Resource(name = "bookDao2")
private BookDao bookDao;
public void print() {
System.out.println(bookDao);
}
}
注意:
@Resource
注解不支持@Primary
注解,同时也没有@Autowired
的require
属性来支持非必须注入
@Inject(jsr-330)
在 Spring 官方文档 中指出,从 Spring 3.0 开始,Spring 支持使用 JSR-330 标准注解来实现依赖注入。
要使用 @Inject
注解,首先需要导入依赖:
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
用法同 @Autowired
和 Resource
:
@Service
public class BookService {
@Inject
private BookDao bookDao;
public void print() {
System.out.println(bookDao);
}
}
这里我们在 bookDao2
这个 Bean
上加上 @Primary
注解:
@Configuration
@ComponentScan(basePackages = "com.keke.web")
public class MyConfigOfDI {
@Primary
@Bean("bookDao2")
public BookDao bookDao() {
BookDao bookDao = new BookDao();
bookDao.setLabel(2);
return bookDao;
}
}
输出 BookService
中的 bookDao
:
BookDao{label=2}
可以看到
@Inject
支持Spring
的@Primary
特性,但是没有@Autowired
的require
属性
从
@Autowired
的源码中可以看到,@Autowired
可以标注在构造器
、方法
、参数
、属性
、注解类型
上
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}
在 IoC 容器
中定义两个 Bean
:Car
、Boss
,并将 Car
注入到 Boss
中:
@Component
public class Car {}
@Component
public class Boss {
private Car car;
public Car getCar() {
return car;
}
public void setCar(Car car) {
this.car = car;
}
@Override
public String toString() {
return "Boss{" +
"car=" + car +
'}';
}
}
基于构造器的依赖注入(Spring 源码中用的比较多)
@Component
public class Boss {
private Car car;
// 标注在有参构造器上,会自动从容器中获取
@Autowired
public Boss(Car car) {
this.car = car;
}
}
也可以标注在构造器参数上
@Component
public class Boss {
private Car car;
public Boss(@Autowired Car car) {
this.car = car;
}
}
也可以省略
@Component
public class Boss {
private Car car;
public Boss(Car car) {
this.car = car;
}
}
当使用构造器注入且
只有一个
有参构造器的时候,参数会从容器中获取
基于 Setter 方法的依赖注入
@Autowired
标注在方法上面,Spring 容器创建当前对象时,会调用方法,并自动从容器中获取方法参数类型的值
public class Boss {
private Car car;
public Car getCar() {
return car;
}
// 标注在 set 方法上面
@Autowired
public void setCar(Car car) {
this.car = car;
}
@Override
public String toString() {
return "Boss{" +
"car=" + car +
'}';
}
}
@Autowired
不一定要标注在set
方法上面,标注在任何方法上都可以,比如下面代码中标注在initCar
上面。
@Bean 方法中的依赖注入(Spring 源码中用的比较多)
// 参数从容器中获取
@Bean
public Boss boss(Car car) {
return new Boss();
}
或者:
@Configuration
@Import({Car.class})
public class MyConfigOfDI {
private Car car;
@Autowired(required = false)
void setCar(Car car) {
this.car = car;
}
@Bean
public Boss boss() {
Boss boss = new Boss();
boss.setCar(car);
return boss;
}
}
基于配置类中普通方法的依赖注入(Spring 源码中用的比较多)
注意: 如果
@Autowired
中的required
属性为false
,同时方法中的参数在容器中确实没有,那么这个方法不会执行(在查看 Sping Security 源码时有这个疑惑)。
- 创建一个普通类
Car
:
public class Car {}
- 在配置类中注入这个
Car
:
@Configuration
public class MyConfigOfDI {
private Car car;
@Autowired(required = false)
void initCar(Car car) {
// 程序不会走到这里,在方法参数那里,如果 car 为空的话,就直接结束掉方法了
this.car = car;
}
}