一、什么是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 {@Beanpublic 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 實例@Testvoid 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;@Autowiredpublic 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 {@Beanpublic User user1(){User user=new User();user.setName("圖圖");return user; //對象1}@Beanpublic 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@Beanpublic User user1(){User user=new User();user.setName("圖圖");return user;}@Beanpublic User user2(){User user=new User();user.setName("小美");return user;}
}
@Controller
public class UserController {@Autowiredprivate User user; //注入成功沒有報錯public void desc(){user.desc();}
}
注意:@Qualifier注解不能單獨使?,必須配合@Autowired使?
4.2 解決方法之?@Qualifier
@Controller
public class UserController {@Qualifier("user1") //添加注入指定Bean@Autowiredprivate 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裝配順序
