在Spring基礎 - Spring簡單例子引入Spring的核心中向你展示了IoC的基礎含義,同時以此發散了一些IoC相關知識點; 本節將在此基礎上進一步解讀IOC的含義以及IOC的使用方式。@pdai
我們在Spring基礎 - Spring簡單例子引入Spring的核心中向你展示了IoC的基礎含義,同時以此發散了一些IoC相關知識點。
本節將在此基礎上進一步解讀IOC的含義以及IOC的使用方式;後續的文章還將深入IOC的實現原理:
如果你有精力看英文,首推 Martin Fowler大師的 Inversion of Control Containers and the Dependency Injection pattern;其次IoC作為一種設計思想,不要過度解讀,而是應該簡化理解,所以我這裡也整合了 張開濤早前的部落格IoC基礎並加入了自己的理解。
IoC Container管理的是Spring Bean, 那麼Spring Bean是什麼呢?
Spring裡面的bean就類似是定義的一個元件,而這個元件的作用就是實現某個功能的,這裡所定義的bean就相當於給了你一個更為簡便的方法來呼叫這個元件去實現你要完成的功能。
Ioc—Inversion of Control,即「控制反轉」,不是什麼技術,而是一種設計思想。在Java開發中,Ioc意味著將你設計好的物件交給容器控制,而不是傳統的在你的物件內部直接控制。
我們來深入分析一下:
傳統Java SE程式設計,我們直接在物件內部通過new進行建立物件,是程式主動去建立依賴物件;而IoC是有專門一個容器來建立這些物件,即由Ioc容器來控制對 象的建立;誰控制誰?當然是IoC 容器控制了物件;控制什麼?那就是主要控制了外部資源獲取(不只是物件包括比如檔案等)。
有反轉就有正轉,傳統應用程式是由我們自己在物件中主動控制去直接獲取依賴物件,也就是正轉;而反轉則是由容器來幫忙建立及注入依賴物件;為何是反轉?因為由容器幫我們查詢及注入依賴物件,物件只是被動的接受依賴物件,所以是反轉;哪些方面反轉了?依賴物件的獲取被反轉了。
傳統程式設計下,都是主動去建立相關物件然後再組合起來:
當有了IoC/DI的容器後,在使用者端類中不再主動去建立這些物件了,如圖
IoC 不是一種技術,只是一種思想,一個重要的物件導向程式設計的法則,它能指導我們如何設計出鬆耦合、更優良的程式。
傳統應用程式都是由我們在類內部主動建立依賴物件,從而導致類與類之間高耦合,難於測試;有了IoC容器後,把建立和查詢依賴物件的控制權交給了容器,由容器進行注入組合物件,所以物件與物件之間是 鬆散耦合,這樣也方便測試,利於功能複用,更重要的是使得程式的整個體系結構變得非常靈活。
其實IoC對程式設計帶來的最大改變不是從程式碼上,而是從思想上,發生了「主從換位」的變化。應用程式原本是老大,要獲取什麼資源都是主動出擊,但是在IoC/DI思想中,應用程式就變成被動的了,被動的等待IoC容器來建立並注入它所需要的資源了。
IoC很好的體現了物件導向設計法則之一—— 好萊塢法則:「別找我們,我們找你」;即由IoC容器幫物件找相應的依賴物件並注入,而不是由物件主動去找。
控制反轉是通過依賴注入實現的,其實它們是同一個概念的不同角度描述。通俗來說就是IoC是設計思想,DI是實現方式。
DI—Dependency Injection,即依賴注入:元件之間依賴關係由容器在執行期決定,形象的說,即由容器動態的將某個依賴關係注入到元件之中。依賴注入的目的並非為軟體系統帶來更多功能,而是為了提升元件重用的頻率,併為系統搭建一個靈活、可延伸的平臺。通過依賴注入機制,我們只需要通過簡單的設定,而無需任何程式碼就可指定目標需要的資源,完成自身的業務邏輯,而不需要關心具體的資源來自何處,由誰實現。
我們來深入分析一下:
當然是應用程式依賴於IoC容器;
應用程式需要IoC容器來提供物件需要的外部資源;
很明顯是IoC容器注入應用程式某個物件,應用程式依賴的物件;
就是注入某個物件所需要的外部資源(包括物件、資源、常數資料)。
其實它們是同一個概念的不同角度描述,由於控制反轉概念比較含糊(可能只是理解為容器控制物件這一個層面,很難讓人想到誰來維護物件關係),所以2004年大師級人物Martin Fowler又給出了一個新的名字:「依賴注入」,相對IoC 而言,「依賴注入」明確描述了「被注入物件依賴IoC容器設定依賴物件」。通俗來說就是IoC是設計思想,DI是實現方式。
在Spring基礎 - Spring簡單例子引入Spring的核心已經給出了三種設定方式,這裡再總結下;總體上目前的主流方式是 註解 + Java 設定。
顧名思義,就是將bean的資訊設定.xml檔案裡,通過Spring載入檔案為我們建立bean。這種方式出現很多早前的SSM專案中,將第三方類庫或者一些設定工具類都以這種方式進行設定,主要原因是由於第三方類不支援Spring註解。
優點: 可以使用於任何場景,結構清晰,通俗易懂
缺點: 設定繁瑣,不易維護,枯燥無味,擴充套件性差
舉例:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- services -->
<bean id="userService" class="tech.pdai.springframework.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions for services go here -->
</beans>
將類的建立交給我們設定的JavcConfig類來完成,Spring只負責維護和管理,採用純Java建立方式。其本質上就是把在XML上的設定宣告轉移到Java設定類中
優點:適用於任何場景,設定方便,因為是純Java程式碼,擴充套件性高,十分靈活
缺點:由於是採用Java類的方式,宣告不明顯,如果大量設定,可讀性比較差
舉例:
/**
* @author pdai
*/
@Configuration
public class BeansConfig {
/**
* @return user dao
*/
@Bean("userDao")
public UserDaoImpl userDao() {
return new UserDaoImpl();
}
/**
* @return user service
*/
@Bean("userService")
public UserServiceImpl userService() {
UserServiceImpl userService = new UserServiceImpl();
userService.setUserDao(userDao());
return userService;
}
}
通過在類上加註解的方式,來宣告一個類交給Spring管理,Spring會自動掃描帶有@Component,@Controller,@Service,@Repository這四個註解的類,然後幫我們建立並管理,前提是需要先設定Spring的註解掃描器。
優點:開發便捷,通俗易懂,方便維護。
缺點:具有侷限性,對於一些第三方資源,無法新增註解。只能採用XML或JavaConfig的方式設定
舉例:
<context:component-scan base-package='tech.pdai.springframework'>
, 或者@ComponentScan("tech.pdai.springframework")
註解,或者 new AnnotationConfigApplicationContext("tech.pdai.springframework")
指定掃描的basePackage./**
* @author pdai
*/
@Service
public class UserServiceImpl {
/**
* user dao impl.
*/
@Autowired
private UserDaoImpl userDao;
/**
* find user list.
*
* @return user list
*/
public List<User> findUserList() {
return userDao.findUserList();
}
}
常用的注入方式主要有三種:構造方法注入(Construct注入),setter注入,基於註解的注入(介面注入)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- services -->
<bean id="userService" class="tech.pdai.springframework.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions for services go here -->
</beans>
本質上包含兩步:
所以對應的service類是這樣的:
/**
* @author pdai
*/
public class UserServiceImpl {
/**
* user dao impl.
*/
private UserDaoImpl userDao;
/**
* init.
*/
public UserServiceImpl() {
}
/**
* find user list.
*
* @return user list
*/
public List<User> findUserList() {
return this.userDao.findUserList();
}
/**
* set dao.
*
* @param userDao user dao
*/
public void setUserDao(UserDaoImpl userDao) {
this.userDao = userDao;
}
}
/**
* @author pdai
*/
public class UserServiceImpl {
/**
* user dao impl.
*/
private UserDaoImpl userDao;
/**
* find user list.
*
* @return user list
*/
public List<User> findUserList() {
return this.userDao.findUserList();
}
/**
* set dao.
*
* @param userDao user dao
*/
@Autowired
public void setUserDao(UserDaoImpl userDao) {
this.userDao = userDao;
}
}
在Spring3.x剛推出的時候,推薦使用注入的就是這種, 但是這種方式比較麻煩,所以在Spring4.x版本中推薦建構函式注入。
<constructor-arg>
是通過建構函式引數注入,比如下面的xml:<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- services -->
<bean id="userService" class="tech.pdai.springframework.service.UserServiceImpl">
<constructor-arg name="userDao" ref="userDao"/>
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions for services go here -->
</beans>
本質上是new UserServiceImpl(userDao)建立物件, 所以對應的service類是這樣的:
/**
* @author pdai
*/
public class UserServiceImpl {
/**
* user dao impl.
*/
private final UserDaoImpl userDao;
/**
* init.
* @param userDaoImpl user dao impl
*/
public UserServiceImpl(UserDaoImpl userDaoImpl) {
this.userDao = userDaoImpl;
}
/**
* find user list.
*
* @return user list
*/
public List<User> findUserList() {
return this.userDao.findUserList();
}
}
/**
* @author pdai
*/
@Service
public class UserServiceImpl {
/**
* user dao impl.
*/
private final UserDaoImpl userDao;
/**
* init.
* @param userDaoImpl user dao impl
*/
@Autowired // 這裡@Autowired也可以省略
public UserServiceImpl(final UserDaoImpl userDaoImpl) {
this.userDao = userDaoImpl;
}
/**
* find user list.
*
* @return user list
*/
public List<User> findUserList() {
return this.userDao.findUserList();
}
}
在Spring4.x版本中推薦的注入方式就是這種, 具體原因看後續章節。
以@Autowired(自動注入)註解注入為例,修飾符有三個屬性:Constructor,byType,byName。預設按照byType注入。
比如:
/**
* @author pdai
*/
@Service
public class UserServiceImpl {
/**
* user dao impl.
*/
@Autowired
private UserDaoImpl userDao;
/**
* find user list.
*
* @return user list
*/
public List<User> findUserList() {
return userDao.findUserList();
}
}
這裡總結下實際開發中會遇到的一些問題:
先來看看Spring在檔案裡怎麼說:
The Spring team generally advocates constructor injection as it enables one to implement application components as immutable objects and to ensure that required dependencies are not null. Furthermore constructor-injected components are always returned to client (calling) code in a fully initialized state.
簡單的翻譯一下:這個構造器注入的方式能夠保證注入的元件不可變,並且確保需要的依賴不為空。此外,構造器注入的依賴總是能夠在返回使用者端(元件)程式碼的時候保證完全初始化的狀態。
下面來簡單的解釋一下:
所以通常是這樣的
/**
* @author pdai
*/
@Service
public class UserServiceImpl {
/**
* user dao impl.
*/
private final UserDaoImpl userDao;
/**
* init.
* @param userDaoImpl user dao impl
*/
public UserServiceImpl(final UserDaoImpl userDaoImpl) {
this.userDao = userDaoImpl;
}
}
如果使用setter注入,缺點顯而易見,對於IOC容器以外的環境,除了使用反射來提供它需要的依賴之外,無法複用該實現類。而且將一直是個潛在的隱患,因為你不呼叫將一直無法發現NPE的存在。
// 這裡只是模擬一下,正常來說我們只會暴露介面給使用者端,不會暴露實現。
UserServiceImpl userService = new UserServiceImpl();
userService.findUserList(); // -> NullPointerException, 潛在的隱患
迴圈依賴的問題:使用field注入可能會導致迴圈依賴,即A裡面注入B,B裡面又注入A:
public class A {
@Autowired
private B b;
}
public class B {
@Autowired
private A a;
}
如果使用構造器注入,在spring專案啟動的時候,就會丟擲:BeanCurrentlyInCreationException:Requested bean is currently in creation: Is there an unresolvable circular reference?從而提醒你避免迴圈依賴,如果是field注入的話,啟動的時候不會報錯,在使用那個bean的時候才會報錯。
比如當你一個Controller中注入了太多的Service類,Sonar會給你提示相關告警
對於這個問題,說明你的類當中有太多的責任,那麼你要好好想一想是不是自己違反了類的單一性職責原則,從而導致有這麼多的依賴要注入。
(pdai: 想起來一句話:所有困難問題的解決方式,都在另外一個層次)
@Autowired和@Resource以及@Inject等註解注入有何區別? 這時平時在開發中,或者常見的面試題。
在Spring 2.5 引入了 @Autowired 註解
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}
從Autowired註解原始碼上看,可以使用在下面這些地方:
@Target(ElementType.CONSTRUCTOR) #建構函式
@Target(ElementType.METHOD) #方法
@Target(ElementType.PARAMETER) #方法引數
@Target(ElementType.FIELD) #欄位、列舉的常數
@Target(ElementType.ANNOTATION_TYPE) #註解
還有一個value屬性,預設是true。
1、@Autowired是Spring自帶的註解,通過AutowiredAnnotationBeanPostProcessor 類實現的依賴注入
2、@Autowired可以作用在CONSTRUCTOR、METHOD、PARAMETER、FIELD、ANNOTATION_TYPE
3、@Autowired預設是根據型別(byType )進行自動裝配的
4、如果有多個型別一樣的Bean候選者,需要指定按照名稱(byName )進行裝配,則需要配合@Qualifier。
指定名稱後,如果Spring IOC容器中沒有對應的元件bean丟擲NoSuchBeanDefinitionException。也可以將@Autowired中required設定為false,如果設定為false之後,當沒有找到相應bean的時候,系統不會拋異常
在欄位屬性上。
@Autowired
private HelloDao helloDao;
或者
private HelloDao helloDao;
public HelloDao getHelloDao() {
return helloDao;
}
@Autowired
public void setHelloDao(HelloDao helloDao) {
this.helloDao = helloDao;
}
或者
private HelloDao helloDao;
//@Autowired
public HelloServiceImpl(@Autowired HelloDao helloDao) {
this.helloDao = helloDao;
}
// 構造器注入也可不寫@Autowired,也可以注入成功。
將@Autowired寫在被注入的成員變數上,setter或者構造器上,就不用再xml檔案中設定了。
如果有多個型別一樣的Bean候選者,則預設根據設定的屬性名稱進行獲取。如 HelloDao 在Spring中有 helloWorldDao 和 helloDao 兩個Bean候選者。
@Autowired
private HelloDao helloDao;
首先根據型別獲取,發現多個HelloDao,然後根據helloDao進行獲取,如果要獲取限定的其中一個候選者,結合@Qualifier進行注入。
@Autowired
@Qualifier("helloWorldDao")
private HelloDao helloDao;
注入名稱為helloWorldDao 的Bean元件。@Qualifier("XXX") 中的 XX是 Bean 的名稱,所以 @Autowired 和 @Qualifier 結合使用時,自動注入的策略就從 byType 轉變成 byName 了。
多個型別一樣的Bean候選者,也可以@Primary進行使用,設定首選的元件,也就是預設優先使用哪一個。
注意:使用@Qualifier 時候,如何設定的指定名稱的Bean不存在,則會丟擲異常,如果防止丟擲異常,可以使用:
@Qualifier("xxxxyyyy")
@Autowired(required = false)
private HelloDao helloDao;
在SpringBoot中也可以使用@Bean+@Autowired進行元件注入,將@Autowired加到引數上,其實也可以省略。
@Bean
public Person getPerson(@Autowired Car car){
return new Person();
}
// @Autowired 其實也可以省略
@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
public @interface Resource {
String name() default "";
// 其他省略
}
從Resource註解原始碼上看,可以使用在下面這些地方:
@Target(ElementType.TYPE) #介面、類、列舉、註解
@Target(ElementType.FIELD) #欄位、列舉的常數
@Target(ElementType.METHOD) #方法
name 指定注入指定名稱的元件。
1、@Resource是JSR250規範的實現,在javax.annotation包下
2、@Resource可以作用TYPE、FIELD、METHOD上
3、@Resource是預設根據屬性名稱進行自動裝配的,如果有多個型別一樣的Bean候選者,則可以通過name進行指定進行注入
@Component
public class SuperMan {
@Resource
private Car car;
}
按照屬性名稱 car 注入容器中的元件。如果容器中BMW還有BYD兩種型別元件。指定加入BMW。如下程式碼:
@Component
public class SuperMan {
@Resource(name = "BMW")
private Car car;
}
name 的作用類似 @Qualifier
@Target({ METHOD, CONSTRUCTOR, FIELD })
@Retention(RUNTIME)
@Documented
public @interface Inject {}
從Inject註解原始碼上看,可以使用在下面這些地方:
@Target(ElementType.CONSTRUCTOR) #建構函式
@Target(ElementType.METHOD) #方法
@Target(ElementType.FIELD) #欄位、列舉的常數
1、@Inject是JSR330 (Dependency Injection for Java)中的規範,需要匯入javax.inject.Inject jar包 ,才能實現注入
2、@Inject可以作用CONSTRUCTOR、METHOD、FIELD上
3、@Inject是根據型別進行自動裝配的,如果需要按名稱進行裝配,則需要配合@Named;
@Inject
private Car car;
指定加入BMW元件。
@Inject
@Named("BMW")
private Car car;
@Named 的作用類似 @Qualifier!
1、@Autowired是Spring自帶的,@Resource是JSR250規範實現的,@Inject是JSR330規範實現的
2、@Autowired、@Inject用法基本一樣,不同的是@Inject沒有required屬性
3、@Autowired、@Inject是預設按照型別匹配的,@Resource是按照名稱匹配的
4、@Autowired如果需要按照名稱匹配需要和@Qualifier一起使用,@Inject和@Named一起使用,@Resource則通過name進行指定
如果你還期望原始碼層理解,我給你找了一篇文章Spring原始碼分析@Autowired、@Resource註解的區別
Inversion of Control Containers and the Dependency Injection pattern
https://www.iteye.com/blog/jinnianshilongnian-1413846
https://blog.csdn.net/qq_35634181/article/details/104276056
https://www.cnblogs.com/diandianquanquan/p/11518365.html
首先, 從Spring框架的整體架構和組成對整體框架有個認知。
其次,通過案例引出Spring的核心(IoC和AOP),同時對IoC和AOP進行案例使用分析。
基於Spring框架和IOC,AOP的基礎,為構建上層web應用,需要進一步學習SpringMVC。
Spring進階 - IoC,AOP以及SpringMVC的原始碼分析
ConcurrentHashMap<String, Object>
;並且BeanDefinition介面中包含了這個類的Class資訊以及是否是單例等。那麼如何從BeanDefinition中範例化Bean物件呢,這是本文主要研究的內容?