一、什么是DI?
DI(Dependency Injection,依赖注入)是 IoC(控制反转) 思想的最典型实现方式,核心目标只有一个:
让对象不再自己“找”依赖,而是由外部容器“送”依赖进来,从而彻底解耦。
一句话看懂DI:以创建狗这个实例举例
过去:自己 new
//当我们创建狗这个实例时,需要通过 new 关键实现对象的实例化 public class testDiBlog { public static void main(String[] args) { Dog dog=new Dog(); } }
现在:依赖注入(这里是构造注入)
public class testDiBlog { private Dog dog; //只声明,不创建,容器将实例送进来 public testDiBlog(Dog dog) { //解耦 this.dog = dog; } }
spring 依赖注入的方式有三种,上述是其中一种(构造方法注入)其他两种分别是:属性注入、Setter方法注入,接下来让我们仔细的来看看这三种注入方式该如何进行书写。
二、依赖注入
首先创建 Dog 类,创建 run 方法
@Getter @Setter
public class Dog {
private String name;
public void run(){
System.out.println("running....");
}
}
将 Dog 类交给Spring 进行管理(PS:@Bean(方法注解) 需要搭配 五大类注解(@Controller、@Service、@Component、@Repository、@Configuration)使用)
@Configuration
public class DogConfig {
@Bean
public Dog dog(){
Dog dog=new Dog();
dog.setName("旺财");
return dog;
}
}
对上面代码的解释:
Spring 管理的对象 =
dog()
方法返回的那只 Dog 实例类型 =
Dog
(返回值类型)Bean 名称 =
dog
(默认等于方法名,等价于@Bean("dog")
)
2.1 属性注入
属性注入是使用 @Autowired 注解
@SpringBootTest
class SpringPrincipleApplicationTests {
@Autowired //属性注入,使用注解
Dog dog; //拿到 dog 实例
@Test
void DogTest(){
dog.run(); //调用实例方法
}
}
当 @Autowired 被注释掉时,此时的执行结果显示空指针异常
2.2 构造方法注入
使用构造方法注入Bean
//注入Bean
@Controller
public class TestDog2 {
private Dog dog;
public TestDog2(Dog dog) {
this.dog = dog;
}
public void run(){
dog.run();
}
}
//启动spring(此段代码与上面的代码在idea中不是位于同一个类中,这里是为了方便观看写在一起)
@SpringBootApplication
public class SpringPrincipleApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringPrincipleApplication.class, args);
TestDog2 bean = context.getBean(TestDog2.class);
bean.run();
}
}
执行结果如下:
此时 TestDog2 方法中只有一个构造方法,我们知道如果当类中没有构造方法时,编译器会默认调用一个无参的构造方法,既然现在是构造方法注入,如果现在我们将这个默认的无参构造函数加上,程序执行的结果还会是这样吗??
@Controller
public class TestDog2 {
private Dog dog;
public TestDog2() {
System.out.println("无参构造方法...");//这里打印一下日志为了方便观察
}
public TestDog2(Dog dog) {
System.out.println("有参构造方法...");
this.dog = dog;
}
public void run(){
dog.run();
}
}
结果执行如下:
可以看到这里执行了这个无参的构造方法,且报出了空指针异常,报空指针异常是因为没有执行下面的有参构造方法,dog 没有进行赋值,后面调用了 dog 的 run 方法,此时 dog 为null。此时的解决方法是在有参构造方法上添加 @Autowried 注解,当添加注解后会告诉 Spring 默认帮我执行带注解的构造方法。修改如下:
@Controller
public class TestDog2 {
private Dog dog;
public TestDog2() {
System.out.println("无参构造方法...");
}
@Autowired //添加注解,指定默认的构造方法
public TestDog2(Dog dog) {
System.out.println("有参构造方法...");
this.dog = dog;
}
public void run(){
dog.run();
}
}
执行结果如下:
不知道有没有小伙伴注意到下图这里的 dog 参数,在代码中我们没有给它传递参数,这个 dog 是从哪儿来的呢?构造函数注入时,Spring 必须能把所有参数都解析成容器里的 Bean,去进行查找,如果找到了就进行相应的赋值,只要有一个参数匹配不到Bean(类型+名称)启动就会失败并抛出:
No qualifying bean of type 'xxx.xxx' available: expected at least 1 bean which qualifies as autowire candidate.
2.3 Setter方法注入
Setter 注⼊和属性的 Setter ⽅法实现类似,只不过在设置 set ⽅法的时候需要加上 @Autowired 注解
@Controller
public class TestDog3 {
private Dog dog;
@Autowired
public void setDog(Dog dog) {
this.dog = dog;
}
public void run(){
System.out.println("这是TestDog3....");
dog.run();
}
}
执行结果如下:
去掉 @Autowired 后,执行结果如下:
三、优缺点分析
再进行优缺点分析之前,主播先提出一个问题,不知道小伙伴们是否还记得 final 关键字修饰得变量有什么特点?我们知道被 final 修饰得变量初始化要么再最初定义变量得时候就初始化,要么就是再构造器中被初始化。当我们回想起这一点后我们再来看这三种注入方式,不难发现,只有构造方法注入可以注入 final 修饰得变量,Setter 和属性注入不可以注入 final 修饰得变量。
3.1 原因分析
首先我们来看看 spring 创建对象的流程:① 分配空白内存 → ② 默认值(0/null) → ③ 构造代码块/构造器(final 唯一合法写入点) → ④ 对象头设置 → ⑤ 返回引用。这里一旦构造器返回,final 修饰的字段就进入了“只读”模式,后续任何赋值都会编译失败。
public class User {
private final String name;
public User(String name){ this.name = name; } // 合法
public void setName(String name){ this.name = name; } // ❌ 编译错误
}
属性注入发生的时间段在返回引用之后,也就是流程⑤之后,spring 属性注入时机(源码级)
//java源码
AbstractAutowireCapableBeanFactory#populateBean
Field field = UserController.class.getDeclaredField("userService");
field.set(controllerInstance, userServiceImpl); // 反射 putfield
JVM 校验:发现对 final 字段 执行
putfield
→ 直接抛java.lang.IllegalAccessError: Update to final field
3.2 spring三种注入的时间轴
注入方式 | 触发时刻 | 对象状态 | 能否再写final |
属性注入 | 第⑤步:对象引用返回 | 对象创建完成 | ❌ 拒绝 |
Setter注入 | 第⑤步:对象引用返回 | 对象创建完成 | ❌ 拒绝 |
构造器注入 | 第③步:构造器里 | 对象正在创建 | ✅ 允许 |
3.3 时间轴再次对照
步骤 | 时刻 | final可否写入 | spring属性注入是否在此 |
类加载 | 类装载模板 | ❌ | ❌ 不参与 |
new | 构造器类 | ✅ 唯一机会 | ❌ 尚未开始 |
构造器返回 | 对象已创建 | ❌ 锁死 | ❌ 尚未开始 |
populateBean | 反射字段赋值 | ❌ 抛错 | ✅ 在这里发生 →失败 |
3.4 优缺点总结
通过上面的分析我们可以总结出三种注入方式的优缺点
方式 | 优点 | 缺点 |
属性注入 |
简洁,使⽤⽅便
|
▪只能⽤于 IoC 容器,如果是⾮ IoC 容器不可⽤,并且只有在使⽤的时候才会出现 NPE(空指
针异常)
▪ 不能注⼊一个Final修饰的属性
|
构造方法注入 |
▪可以注⼊final修饰的属性
▪注⼊的对象不会被修改
▪ 依赖对象在使⽤前⼀定会被完全初始化,因为依赖是在类的构造⽅法中执⾏的,⽽构造⽅法 是在类加载阶段就会执⾏的⽅法.
▪ 通⽤性好, 构造⽅法是JDK⽀持的, 所以更换任何框架,他都是适⽤的
|
▪ 注⼊多个对象时, 代码会⽐较繁琐
|
Setter注入 |
⽅便在类实例之后, 重新对该对象进⾏配置或者注⼊
|
▪不能注⼊⼀个Final修饰的属性
▪ 注⼊对象可能会被改变, 因为setter⽅法可能会被多次调⽤,就有被修改的风险
|
四、@Autowired存在的问题
当同一个类型的对象有多个的时候,此时又会发生什么状况呢??
@Component
public class TestUser {
@Bean
public User user1(){
User user=new User();
user.setName("图图");
return user; //对象1
}
@Bean
public User user2(){
User user=new User();
user.setName("小美");
return user; //对象2
}
}
错误提示:这里不只有一个User Bean对象,当同⼀类型存在多个bean时, 使⽤@Autowired会存在问题。
4.1 解决方法之 @Primary
使⽤@Primary注解:当存在多个相同类型的Bean注⼊时,加上@Primary注解,来确定默认的实现.
@Component
public class TestUser {
@Primary //指定该 Bean为默认的 Bean
@Bean
public User user1(){
User user=new User();
user.setName("图图");
return user;
}
@Bean
public User user2(){
User user=new User();
user.setName("小美");
return user;
}
}
@Controller
public class UserController {
@Autowired
private User user; //注入成功没有报错
public void desc(){
user.desc();
}
}
注意:@Qualifier注解不能单独使⽤,必须配合@Autowired使⽤
4.2 解决方法之 @Qualifier
@Controller
public class UserController {
@Qualifier("user1") //添加注入指定Bean
@Autowired
private User user;
public void desc(){
user.desc();
}
}
4.3 解决方法之 @Resource
使⽤@Resource注解:是按照bean的名称进⾏注⼊。通过name属性指定要注⼊的bean的名称。
@Controller
public class UserController {
@Resource(name= "user1")
private User user;
public void desc(){
user.desc();
}
}
五、@Resource 和 @Autowired 的区别
1. @Autowired 是spring框架提供的注解,⽽@Resource是JDK提供的注解2. @Autowired 默认是按照类型注⼊,当同类型有多个实例时,也会根据名称去进行匹配,⽽@Resource是按照名称注⼊,按名称肯定也会需要类型是一致的.3. 相⽐于 @Autowired 来说,@Resource ⽀持更多的参数设置,例如 name 设置,根据名称获取 Bean
六、@Autowired装配顺序
