一、Bean的循環依賴問題
1.什么是Bean的循環依賴
A對象中有B屬性。B對象中有A屬性。這就是循環依賴。我依賴你,你也依賴我。
比如:丈夫類Husband,妻子類Wife。Husband中有Wife的引用。Wife中有Husband的引用。
public class Husband {private String name;private Wife wife;
}
public class Wife {private String name;private Husband husband;
}
2.singleton下的set注入產生的循環依賴
我們來編寫程序,測試一下在singleton+setter的模式下產生的循環依賴,Spring是否能夠解決?
public class Husband {private String name;private Wife wife;public void setName(String name) {this.name = name;}public String getName() {return name;}public void setWife(Wife wife) {this.wife = wife;}// toString()方法重寫時需要注意:不能直接輸出wife,輸出wife.getName()。要不然會出現遞歸導致的棧內存溢出錯誤。@Overridepublic String toString() {return "Husband{" +"name='" + name + '\'' +", wife=" + wife.getName() +'}';}
}
public class Wife {private String name;private Husband husband;public void setName(String name) {this.name = name;}public String getName() {return name;}public void setHusband(Husband husband) {this.husband = husband;}// toString()方法重寫時需要注意:不能直接輸出husband,輸出husband.getName()。要不然會出現遞歸導致的棧內存溢出錯誤。@Overridepublic String toString() {return "Wife{" +"name='" + name + '\'' +", husband=" + husband.getName() +'}';}
}
<?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"><!--singleton + setter模式下的循環依賴是沒有任何問題的。--><!--singleton表示在整個Spring容器當中是單例的,獨一無二的對象。--><!--在singleton + setter模式下,為什么循環依賴不會出現問題,Spring是如何應對的?主要的原因是,在這種模式下Spring對Bean的管理主要分為清晰的兩個階段:第一個階段:在Spring容器加載的時候,實例化Bean,只要其中任意一個Bean實例化之后,馬上進行 “曝光”【不等屬性賦值就曝光】第二個階段:Bean“曝光”之后,再進行屬性的賦值(調用set方法。)。核心解決方案是:實例化對象和對象的屬性賦值分為兩個階段來完成的。注意:只有在scope是singleton的情況下,Bean才會采取提前“曝光”的措施。--><bean id="husbandBean" class="com.spring6.bean.Husband" scope="singleton"><property name="name" value="張三"/><property name="wife" ref="wifeBean"/></bean><bean id="wifeBean" class="com.spring6.bean.Wife" scope="singleton"><property name="name" value="小花"/><property name="husband" ref="husbandBean"/></bean></beans>
Test
@Testpublic void testCD(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);System.out.println(husbandBean);Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);System.out.println(wifeBean);}
通過測試得知:在singleton + set注入的情況下,循環依賴是沒有問題的。Spring可以解決這個問題。
3.prototype下的set注入產生的循環依賴
我們再來測試一下:prototype+set注入的方式下,循環依賴會不會出現問題?
<?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"><!--在prototype + setter模式下的循環依賴,存在問題,會出現異常!--><!--BeanCurrentlyInCreationException 當前的Bean正在處于創建中異常。。。--><!-- 注意:當兩個bean的scope都是prototype的時候,才會出現異常。如果其中任意一個是singleton的,就不會出現異常。--><bean id="husbandBean" class="com.spring6.bean.Husband" scope="prototype"><property name="name" value="張三"/><property name="wife" ref="wifeBean"/></bean><bean id="wifeBean" class="com.spring6.bean.Wife" scope="prototype"><property name="name" value="小花"/><property name="husband" ref="husbandBean"/></bean>
</beans>
執行測試程序:發生了異常,異常信息如下: Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'husbandBean': Requested bean is currently in creation: Is there an unresolvable circular reference? at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:265) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325) ... 44 more
翻譯為:創建名為“husbandBean”的bean時出錯:請求的bean當前正在創建中:是否存在無法解析的循環引用?
通過測試得知,當循環依賴的所有Bean的scope="prototype"的時候,產生的循環依賴,Spring是無法解決的,會出現BeanCurrentlyInCreationException異常。
大家可以測試一下,以上兩個Bean,如果其中一個是singleton,另一個是prototype,是沒有問題的。
為什么兩個Bean都是prototype時會出錯呢?
在Spring框架中,當兩個prototype
作用域的Bean通過setter注入產生循環依賴時,會拋出BeanCurrentlyInCreationException
異常。這與prototype
作用域的特性及Spring的依賴解決機制有關,以下是具體原因:
⑴.?prototype
作用域的核心特點
-
無緩存:
prototype
作用域的Bean每次請求時都會創建一個全新的實例,Spring不會緩存這些實例。 -
生命周期不托管:與單例Bean不同,
prototype
?Bean的初始化和銷毀方法由調用者管理,Spring容器不負責其完整生命周期。
⑵.循環依賴的解決機制對比
-
單例Bean的解決方案:
-
Spring通過三級緩存(
singletonFactories
、earlySingletonObjects
、singletonObjects
)提前暴露未完全初始化的Bean引用。 -
例如,Bean A和Bean B都是單例,Spring會先實例化A,在填充屬性時發現需要B,然后實例化B并注入A的早期引用(未完成初始化的對象),最終完成兩者的初始化。
-
-
prototype
?Bean的困境:-
無緩存:由于
prototype
?Bean每次都需要新實例,Spring無法通過緩存提前暴露一個固定的早期引用。 -
無限遞歸:當創建
prototype
的Bean A時,它需要注入Bean B的新實例;而創建Bean B時,又需要注入Bean A的新實例。這個過程會無限循環,直到拋出異常。
-
⑶.Spring的異常觸發邏輯
-
循環檢測:Spring在創建Bean時,會記錄當前正在創建的Bean名稱。如果發現某個Bean的創建過程中再次請求自身(或形成環形依賴),則觸發異常。
-
示例流程:
-
請求
prototype
?Bean A → 開始創建A。 -
A需要注入
prototype
?Bean B → 請求創建B。 -
B需要注入
prototype
?Bean A → 再次請求創建A。 -
Spring檢測到A已經在創建中(但無法通過緩存復用),拋出
BeanCurrentlyInCreationException
。
-
⑷.為何單例Bean可以解決循環依賴,而prototype
不行?
-
單例Bean:整個容器中只有唯一實例,通過緩存可提前暴露引用,依賴注入的是同一個對象。
-
prototype
?Bean:每次注入都是新實例,無法復用已存在的引用,導致無限循環。
5.?代碼示例與現象
假設以下兩個prototype
?Bean互相依賴:
@Component
@Scope("prototype")
public class A {private B b;@Autowiredpublic void setB(B b) { this.b = b; }
}@Component
@Scope("prototype")
public class B {private A a;@Autowiredpublic void setA(A a) { this.a = a; }
}
異常信息:
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
6.?解決方案
-
避免循環依賴:重新設計Bean之間的關系,移除不必要的雙向依賴。
-
使用
@Lazy
延遲注入:對其中一個Bean的依賴添加@Lazy
注解,延遲實際代理對象的創建。@Component @Scope("prototype") public class A {@Autowired@Lazy // 延遲注入B的代理對象private B b; }
-
改用單例作用域:如果業務允許,將其中一個Bean改為單例,利用Spring的緩存機制解決循環依賴。
⑺.總結
prototype
作用域的Bean在循環依賴時失敗,本質是因為Spring無法通過緩存復用未初始化的對象,導致無限遞歸創建新實例。而單例Bean的緩存機制和三級緩存設計使得循環依賴得以解決。在設計prototype
?Bean時,需特別注意依賴關系的合理性。
4.singleton下的構造注入產生的循環依賴
我們再來測試一下singleton + 構造注入的方式下,spring是否能夠解決這種循環依賴。
public class Husband {private String name;private Wife wife;public Husband(String name, Wife wife) {this.name = name;this.wife = wife;}// -----------------------分割線--------------------------------public String getName() {return name;}@Overridepublic String toString() {return "Husband{" +"name='" + name + '\'' +", wife=" + wife +'}';}
}
public class Wife {private String name;private Husband husband;public Wife(String name, Husband husband) {this.name = name;this.husband = husband;}// -------------------------分割線--------------------------------public String getName() {return name;}@Overridepublic String toString() {return "Wife{" +"name='" + name + '\'' +", husband=" + husband +'}';}
}
<?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"><!--構造注入,這種循環依賴有沒有問題?--><!--注意:基于構造注入的方式下產生的循環依賴也是無法解決的,所以編寫代碼時一定要注意。--><bean id="h" scope="singleton" class="com.spring6.bean2.Husband"><constructor-arg index="0" value="張三"></constructor-arg><constructor-arg index="1" ref="w"></constructor-arg></bean><bean id="w" scope="singleton" class="com.spring6.bean2.Wife"><constructor-arg index="0" value="小花"></constructor-arg><constructor-arg index="1" ref="h"></constructor-arg></bean></beans>
Test
@Testpublic void testCD2(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");Husband husbandBean = applicationContext.getBean("h", Husband.class);System.out.println(husbandBean);Wife wifeBean = applicationContext.getBean("w", Wife.class);System.out.println(wifeBean);}
和上一個測試結果相同,都是提示產生了循環依賴,并且Spring是無法解決這種循環依賴的。
為什么呢?
主要原因是因為通過構造方法注入導致的:因為構造方法注入會導致實例化對象的過程和對象屬性賦值的過程沒有分離開,必須在一起完成導致的。
5.Spring解決循環依賴的機理
Spring為什么可以解決set + singleton模式下循環依賴?
根本的原因在于:這種方式可以做到將“實例化Bean”和“給Bean屬性賦值”這兩個動作分開去完成。
實例化Bean的時候:調用無參數構造方法來完成。此時可以先不給屬性賦值,可以提前將該Bean對象“曝光”給外界。
給Bean屬性賦值的時候:調用setter方法來完成。
兩個步驟是完全可以分離開去完成的,并且這兩步不要求在同一個時間點上完成。
也就是說,Bean都是單例的,我們可以先把所有的單例Bean實例化出來,放到一個集合當中(我們可以稱之為緩存),所有的單例Bean全部實例化完成之后,以后我們再慢慢的調用setter方法給屬性賦值。這樣就解決了循環依賴的問題。
那么在Spring框架底層源碼級別上是如何實現的呢?請看:
在以上類中包含三個重要的屬性:
Cache of singleton objects: bean name to bean instance. 單例對象的緩存:key存儲bean名稱,value存儲Bean對象【一級緩存】
Cache of early singleton objects: bean name to bean instance. 早期單例對象的緩存:key存儲bean名稱,value存儲早期的Bean對象【二級緩存】
Cache of singleton factories: bean name to ObjectFactory. 單例工廠緩存:key存儲bean名稱,value存儲該Bean對應的ObjectFactory對象【三級緩存】
這三個緩存其實本質上是三個Map集合。
我們再來看,在該類中有這樣一個方法addSingletonFactory(),這個方法的作用是:將創建Bean對象的ObjectFactory對象提前曝光。
再分析下面的源碼:
源碼分析: DefaultSingletonBeanRegistry類中有三個比較重要的緩存:private final Map<String, Object> singletonObjects 一級緩存private final Map<String, Object> earlySingletonObjects 二級緩存private final Map<String, ObjectFactory<?>> singletonFactories 三級緩存這三個緩存都是Map集合。Map集合的key存儲的都是bean的name(bean id)。一級緩存存儲的是:單例Bean對象。完整的單例Bean對象,也就是說這個緩存中的Bean對象的屬性都已經賦值了。是一個完整的Bean對象。二級緩存存儲的是:早期的單例Bean對象。這個緩存中的單例Bean對象的屬性沒有賦值。只是一個早期的實例對象。三級緩存存儲的是:單例工廠對象。這個里面存儲了大量的“工廠對象”,每一個單例Bean對象都會對應一個單例工廠對象。這個集合中存儲的是,創建該單例對象時對應的那個單例工廠對象。
從源碼中可以看到,spring會先從一級緩存中獲取Bean,如果獲取不到,則從二級緩存中獲取Bean,如果二級緩存還是獲取不到,則從三級緩存中獲取之前曝光的ObjectFactory對象,通過ObjectFactory對象獲取Bean實例,這樣就解決了循環依賴的問題。
總結:
Spring只能解決setter方法注入的單例bean之間的循環依賴。ClassA依賴ClassB,ClassB又依賴ClassA,形成依賴閉環。
Spring在創建ClassA對象后,不需要等給屬性賦值,直接將其曝光到bean緩存當中。在解析ClassA的屬性時,又發現依賴于ClassB,再次去獲取ClassB,當解析ClassB的屬性時,又發現需要ClassA的屬性,但此時的ClassA已經被提前曝光加入了正在創建的bean的緩存中,則無需創建新的的ClassA的實例,直接從緩存中獲取即可。從而解決循環依賴問題。
二、回顧反射機制
1.分析方法四要素
我們先來看一下,不使用反射機制調用一個方法需要幾個要素的參與。 有一個這樣的類:
SomeService類
public class SomeService {public void doSome(){System.out.println("public void doSome()執行。");}public String doSome(String s){System.out.println("public String doSome(String s)執行。");return s;}public String doSome(String s, int i){System.out.println("public String doSome(String s, int i)執行。");return s + i;}}
編寫程序調用方法:
public class Test {public static void main(String[] args) {// 不使用反射機制調用這些方法SomeService someService = new SomeService();someService.doSome();/*** 分析:調用一個方法,當中含有幾個要素?四要素。* 第一要素:調用哪個對象* 第二要素:調用哪個方法* 第三要素:調用方法的時候傳什么參數* 第四要素:方法執行結束之后的返回結果** 調用哪個對象的哪個方法,傳什么參數,返回什么值。** 即使是使用反射機制調用方法,也同樣需要具備這四個要素。*/String s1 = someService.doSome("張三");System.out.println(s1);String s2 = someService.doSome("李四", 250);System.out.println(s2);}
}
通過以上代碼可以看出,調用一個方法,一般涉及到4個要素:
-
調用哪個對象的
-
哪個方法
-
傳什么參數
-
返回什么值
2.獲取Method
要使用反射機制調用一個方法,首先你要獲取到這個方法。 在反射機制中Method實例代表的是一個方法。那么怎么獲取Method實例呢? 有這樣一個類:
public class SomeService {public void doSome(){System.out.println("public void doSome()執行。");}public String doSome(String s){System.out.println("public String doSome(String s)執行。");return s;}public String doSome(String s, int i){System.out.println("public String doSome(String s, int i)執行。");return s + i;}}
我們如何獲取到 doSome( )、doSome(String s)、doSome(String s, int i) 這三個方法呢? 要獲取方法Method,首先你需要獲取這個類Class。
Class<?> clazz = Class.forName("com.reflect.SomeService");
當拿到Class之后,調用getDeclaredMethod()方法可以獲取到方法。 假如你要獲取這個方法:
doSome(String s, int i)
Method doSomeMethod = clazz.getDeclaredMethod("doSome", String.class, int.class);
假如你要獲取到這個方法:doSome(String s)
Method doSomeMethod = clazz.getDeclaredMethod("doSome", String.class);
獲取一個方法,需要告訴Java程序,你要獲取的方法的名字是什么,這個方法上每個形參的類型是什么。這樣Java程序才能給你拿到對應的方法。
這樣的設計也非常合理,因為在同一個類當中,方法是支持重載的,也就是說方法名可以一樣,但參數列表一定是不一樣的,所以獲取一個方法需要提供方法名以及每個形參的類型。 假設有這樣一個方法:
public void setAge(int age){this.age = age;
}
你要獲取這個方法的話,代碼應該這樣寫:
Method setAgeMethod = clazz.getDeclaredMethod("setAge", int.class);
其中setAge是方法名,int.class是形參的類型。
如果要獲取上面的doSome( )方法,代碼應該這樣寫:
Method doSomeMethod = clazz.getDeclaredMethod("doSome");
因為這個方法形式參數的個數是0個。所以只需要提供方法名就行了。你學會了嗎?
3.調用Method
要讓一個方法調用的話,就關聯到四要素了:
-
調用哪個對象的
-
哪個方法
-
傳什么參數
-
返回什么值
public class SomeService {public void doSome(){System.out.println("public void doSome()執行。");}public String doSome(String s){System.out.println("public String doSome(String s)執行。");return s;}public String doSome(String s, int i){System.out.println("public String doSome(String s, int i)執行。");return s + i;}}
public class Test2 {public static void main(String[] args) throws Exception{// 使用反射機制怎么調用方法。// 獲取類Class<?> clazz = Class.forName("com.reflect.SomeService");// 獲取方法Method doSomeMethod = clazz.getDeclaredMethod("doSome", String.class, int.class);// 調用方法// 四要素:調用哪個對象、哪個方法、傳什么參數、返回什么值。// obj 要素:哪個對象// doSomeMethod 要素:哪個方法// "李四", 250 要素:傳什么參數// retValue 要素:返回什么值。Object obj = clazz.newInstance();Object retValue = doSomeMethod.invoke(obj, "李四", 250);System.out.println(retValue);}
}
4.假設你知道屬性名
假設有這樣一個類:
public class User {private String name;private int age;public User() {}public User(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +'}';}
}
需求:假設你現在已知以下信息:1. 有這樣一個類,類名叫做:com.reflect.User2. 這個類符合javabean規范。屬性私有化,對外提供公開的setter和getter方法。3. 你還知道這個類當中有一個屬性,屬性的名字叫做 age4. 并且你還知道age屬性的類型是int類型。請使用反射機制調用set方法,給User對象的age屬性賦值。
public class Test4 {public static void main(String[] args) throws Exception {/*需求:假設你現在已知以下信息:1. 有這樣一個類,類名叫做:com.powernode.reflect.User2. 這個類符合javabean規范。屬性私有化,對外提供公開的setter和getter方法。3. 你還知道這個類當中有一個屬性,屬性的名字叫做 age4. 并且你還知道age屬性的類型是int類型。請使用反射機制調用set方法,給User對象的age屬性賦值。*/String className = "com.reflect.User";String propertyName = "age";// 通過反射機制調用setAge(int)方法// 獲取類Class<?> clazz = Class.forName(className);// 獲取方法名String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);// 根據屬性名獲取屬性類型Field field = clazz.getDeclaredField(propertyName);// 獲取方法Method setMethod = clazz.getDeclaredMethod(setMethodName, field.getType());// 準備對象Object obj = clazz.newInstance();// 調用方法setMethod.invoke(obj, 30);System.out.println(obj);}
}
三、手寫Spring框架
Spring IoC容器的實現原理:工廠模式 + 解析XML + 反射機制。 我們給自己的框架起名為:myspring(我的春天)
第一步:創建模塊myspring
采用Maven方式新建Module:myspring
打包方式采用jar,并且引入dom4j和jaxen的依賴,因為要使用它解析XML文件,還有junit依賴。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.myspringframework</groupId><artifactId>myspring</artifactId><version>1.0.0</version><packaging>jar</packaging><dependencies><dependency><groupId>org.dom4j</groupId><artifactId>dom4j</artifactId><version>2.1.3</version></dependency><dependency><groupId>jaxen</groupId><artifactId>jaxen</artifactId><version>1.2.0</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope></dependency></dependencies><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target></properties></project>
第二步:準備好我們要管理的Bean
準備好我們要管理的Bean(這些Bean在將來開發完框架之后是要刪除的) 注意包名,不要用org.myspringframework包,因為這些Bean不是框架內置的。是將來使用我們框架的程序員提供的。
public class User {private String name;private int age;public void setName(String name) {this.name = name;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +'}';}
}
public class UserDao {public void insert(){System.out.println("Mysql數據庫正在保存用戶信息");}
}
public class UserService {private UserDao userDao;public void setUserDao(UserDao userDao) {this.userDao = userDao;}public void save(){userDao.insert();}
}
第三步:準備myspring.xml配置文件
將來在框架開發完畢之后,這個文件也是要刪除的。因為這個配置文件的提供者應該是使用這個框架的程序員。 文件名隨意,我們這里叫做:myspring.xml 文件放在類路徑當中即可,我們這里把文件放到類的根路徑下。
<?xml version="1.0" encoding="UTF-8"?>
<beans><bean id="user" class="org.example1.bean.User"><property name="name" value="張三"></property><property name="age" value="30"></property><!--<property name="height" value="1.83"></property>--></bean><bean id="userDaoBean" class="org.example1.bean.UserDao"></bean><bean id="userService" class="org.example1.bean.UserService"><property name="userDao" ref="userDaoBean"/></bean></beans>
使用value給簡單屬性賦值。使用ref給非簡單屬性賦值。
第四步:編寫ApplicationContext接口
ApplicationContext接口中提供一個getBean()方法,通過該方法可以獲取Bean對象。 注意包名:這個接口就是myspring框架中的一員了。
public interface ApplicationContext {Object getBean(String beanName);}
第五步:編寫ClassPathXmlApplicationContext
ClassPathXmlApplicationContext是ApplicationContext接口的實現類。該類從類路徑當中加載myspring.xml配置文件。
public class ClassPathXmlApplicationContext implements ApplicationContext{public ClassPathXmlApplicationContext(String configLocation){}@Overridepublic Object getBean(String beanName) {return null;}
}
第六步:確定采用Map集合存儲Bean
確定采用Map集合存儲Bean實例。Map集合的key存儲beanId,value存儲Bean實例。Map<String,Object> 在ClassPathXmlApplicationContext類中添加Map<String,Object>屬性。
并且在ClassPathXmlApplicationContext類中添加構造方法,該構造方法的參數接收myspring.xml文件。 同時實現getBean方法。
public class ClassPathXmlApplicationContext implements ApplicationContext{private Map<String, Object> singletonObjects = new HashMap<>();/*** 解析myspring的配置文件,然后初始化所有的Bean對象。* @param configLocation spring配置文件的路徑。注意:使用ClassPathXmlApplicationContext,配置文件應當放到類路徑下。*/public ClassPathXmlApplicationContext(String configLocation) {// 解析myspring.xml文件,然后實例化Bean,將Bean存放到singletonObjects集合當中。}@Overridepublic Object getBean(String beanName) {return null;}
}
第七步:解析配置文件實例化所有Bean
在ClassPathXmlApplicationContext的構造方法中解析配置文件,獲取所有bean的類名,通過反射機制調用無參數構造方法創建Bean。并且將Bean對象存放到Map集合中。
package org.example1.core;import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class ClassPathXmlApplicationContext implements ApplicationContext {private static final Logger logger = LoggerFactory.getLogger(ClassPathXmlApplicationContext.class);private Map<String, Object> singletonObjects = new HashMap<>();/*** 解析myspring的配置文件,然后初始化所有的Bean對象。* @param configLocation spring配置文件的路徑。注意:使用ClassPathXmlApplicationContext,配置文件應當放到類路徑下。*/public ClassPathXmlApplicationContext(String configLocation) {try {// 解析myspring.xml文件,然后實例化Bean,將Bean存放到singletonObjects集合當中。// 這是dom4j解析XML文件的核心對象。SAXReader reader = new SAXReader();// 獲取一個輸入流,指向配置文件InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream(configLocation);// 讀文件Document document = reader.read(in);// 獲取所有的bean標簽List<Node> nodes = document.selectNodes("//bean");// 遍歷bean標簽nodes.forEach(node -> {try {// 向下轉型的目的是為了使用Element接口里更加豐富的方法。Element beanElt = (Element) node;// 獲取id屬性String id = beanElt.attributeValue("id");// 獲取class屬性String className = beanElt.attributeValue("class");logger.info("beanName=" + id);logger.info("beanClassName=" + className);// 通過反射機制創建對象,將其放到Map集合中,提前曝光。// 獲取ClassClass<?> aClass = Class.forName(className);// 獲取無參數構造方法Constructor<?> defaultCon = aClass.getDeclaredConstructor();// 調用無參數構造方法實例化BeanObject bean = defaultCon.newInstance();// 將Bean曝光,加入Map集合singletonObjects.put(id, bean);// 記錄日志logger.info(singletonObjects.toString());} catch (Exception e) {e.printStackTrace();}});} catch (Exception e) {e.printStackTrace();} // 這里補上了缺失的右花括號}@Overridepublic Object getBean(String beanName) {return null;}
}
第八步:測試能否獲取到Bean
編寫測試程序。
@Testpublic void testMySpring(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("myspring.xml");}
通過測試Bean已經實例化成功了,屬性的值是null,這是我們能夠想到的,畢竟我們調用的是無參數構造方法,所以屬性都是默認值。 下一步就是我們應該如何給Bean的屬性賦值呢?
第九步:給Bean的屬性賦值
通過反射機制調用set方法,給Bean的屬性賦值。 繼續在ClassPathXmlApplicationContext構造方法中編寫代碼。
package org.example1.core;import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class ClassPathXmlApplicationContext implements ApplicationContext{private static final Logger logger = LoggerFactory.getLogger(ClassPathXmlApplicationContext.class);private Map<String, Object> singletonObjects = new HashMap<>();/*** 解析myspring的配置文件,然后初始化所有的Bean對象。* @param configLocation spring配置文件的路徑。注意:使用ClassPathXmlApplicationContext,配置文件應當放到類路徑下。*/public ClassPathXmlApplicationContext(String configLocation) {try {// 解析myspring.xml文件,然后實例化Bean,將Bean存放到singletonObjects集合當中。// 這是dom4j解析XML文件的核心對象。SAXReader reader = new SAXReader();// 獲取一個輸入流,指向配置文件InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream(configLocation);// 讀文件Document document = reader.read(in);// 獲取所有的bean標簽List<Node> nodes = document.selectNodes("//bean");// 遍歷bean標簽nodes.forEach(node -> {try {// 向下轉型的目的是為了使用Element接口里更加豐富的方法。Element beanElt = (Element) node;// 獲取id屬性String id = beanElt.attributeValue("id");// 獲取class屬性String className = beanElt.attributeValue("class");logger.info("beanName=" + id);logger.info("beanClassName="+className);// 通過反射機制創建對象,將其放到Map集合中,提前曝光。// 獲取ClassClass<?> aClass = Class.forName(className);// 獲取無參數構造方法Constructor<?> defaultCon = aClass.getDeclaredConstructor();// 調用無參數構造方法實例化BeanObject bean = defaultCon.newInstance();// 將Bean曝光,加入Map集合singletonObjects.put(id, bean);// 記錄日志logger.info(singletonObjects.toString());} catch (Exception e) {e.printStackTrace();}});// 再次重新把所有的bean標簽遍歷一次,這一次主要是給對象的屬性賦值。nodes.forEach(node -> {try {Element beanElt = (Element) node;// 獲取idString id = beanElt.attributeValue("id");// 獲取classNameString className = beanElt.attributeValue("class");// 獲取ClassClass<?> aClass = Class.forName(className);// 獲取該bean標簽下所有的屬性property標簽List<Element> propertys = beanElt.elements("property");// 遍歷所有的屬性標簽propertys.forEach(property -> {try {// 獲取屬性名String propertyName = property.attributeValue("name");// 獲取屬性類型Field field = aClass.getDeclaredField(propertyName);logger.info("屬性名:" + propertyName);// 獲取set方法名String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);// 獲取set方法Method setMethod = aClass.getDeclaredMethod(setMethodName, field.getType());// 獲取具體的值String value = property.attributeValue("value"); // "30"Object actualValue = null; // 真值String ref = property.attributeValue("ref");if (value != null) {// 說明這個值是簡單類型// 調用set方法(set方法沒有返回值)// 我們myspring框架聲明一下:我們只支持這些類型為簡單類型// byte short int long float double boolean char// Byte Short Integer Long Float Double Boolean Character// String// 獲取屬性類型名String propertyTypeSimpleName = field.getType().getSimpleName();switch (propertyTypeSimpleName) {case "byte":actualValue = Byte.parseByte(value);break;case "short":actualValue = Short.parseShort(value);break;case "int":actualValue = Integer.parseInt(value);break;case "long":actualValue = Long.parseLong(value);break;case "float":actualValue = Float.parseFloat(value);break;case "double":actualValue = Double.parseDouble(value);break;case "boolean":actualValue = Boolean.parseBoolean(value);break;case "char":actualValue = value.charAt(0);break;case "Byte":actualValue = Byte.valueOf(value);break;case "Short":actualValue = Short.valueOf(value);break;case "Integer":actualValue = Integer.valueOf(value);break;case "Long":actualValue = Long.valueOf(value);break;case "Float":actualValue = Float.valueOf(value);break;case "Double":actualValue = Double.valueOf(value);break;case "Boolean":actualValue = Boolean.valueOf(value);break;case "Character":actualValue = Character.valueOf(value.charAt(0));break;case "String":actualValue = value;}setMethod.invoke(singletonObjects.get(id), actualValue);}if (ref != null) {// 說明這個值是非簡單類型// 調用set方法(set方法沒有返回值)setMethod.invoke(singletonObjects.get(id), singletonObjects.get(ref));}} catch (Exception e) {e.printStackTrace();}});} catch (Exception e) {e.printStackTrace();}});} catch (Exception e) {e.printStackTrace();}}@Overridepublic Object getBean(String beanName) {return singletonObjects.get(beanName);}
}
重點處理:當property標簽中是value怎么辦?是ref怎么辦?
執行測試程序:
第十步:打包發布
將多余的類以及配置文件刪除,使用maven打包發布。
第十一步:站在程序員角度使用myspring框架
新建模塊:myspring-test
引入myspring框架的依賴:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.powernode</groupId><artifactId>myspring-test</artifactId><version>1.0-SNAPSHOT</version><dependencies><!--用myspring框架,需要引入依賴--><dependency><groupId>org.example1</groupId><artifactId>myspring</artifactId><version>1.0.0</version></dependency><!--單元測試--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope></dependency></dependencies><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target></properties></project>
編寫Bean
public class Vip {private String name;private int age;private double height;@Overridepublic String toString() {return "Vip{" +"name='" + name + '\'' +", age=" + age +", height=" + height +'}';}public void setName(String name) {this.name = name;}public void setAge(int age) {this.age = age;}public void setHeight(double height) {this.height = height;}
}
public class OrderService {private OrderDao orderDao;public void setOrderDao(OrderDao orderDao) {this.orderDao = orderDao;}public void generate(){orderDao.insert();}}
public class OrderDao {public void insert(){System.out.println("正在保存訂單信息....");}
}
編寫myspring.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans><bean id="vip" class="com.myspring.bean.Vip"><property name="name" value="jackson"></property><property name="age" value="30"></property><property name="height" value="1.83"></property></bean><bean id="orderDaoBean" class="com.myspring.bean.OrderDao"></bean><bean id="orderService" class="com.myspring.bean.OrderService"><property name="orderDao" ref="orderDaoBean"/></bean></beans>
編寫測試程序
@Testpublic void testMySpring(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("myspring.xml");Object vip = applicationContext.getBean("vip");System.out.println(vip);OrderService orderService = (OrderService) applicationContext.getBean("orderService");orderService.generate();}
四、Spring IoC注解式開發
1.回顧注解
注解的存在主要是為了簡化XML的配置。Spring6倡導全注解開發。 我們來回顧一下:
-
第一:注解怎么定義,注解中的屬性怎么定義?
-
第二:注解怎么使用?
-
第三:通過反射機制怎么讀取注解?
注解怎么定義,注解中的屬性怎么定義?
// 標注注解的注解,叫做元注解。@Target注解用來修飾@Component可以出現的位置。
// 以下表示@Component注解可以出現在類上、屬性上。
//@Target(value = {ElementType.TYPE, ElementType.FIELD})
// 以下表示@Component注解可以出現在類上
//@Target(value = {ElementType.TYPE})
// 使用某個注解的時候,如果注解的屬性名是value的話,value可以省略。
//@Target({ElementType.TYPE})
// 使用某個注解的時候,如果注解的屬性值是數組,并且數組中只有一個元素,大括號可以省略。
@Target(ElementType.TYPE)
// @Retention 也是一個元注解。用來標注@Component注解最終保留在class文件當中,并且可以被反射機制讀取。
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {// 定義注解的屬性// String是屬性類型// value是屬性名String value();// 其他的屬性// 屬性類型String// 屬性名是name//String name();// 數組屬性// 屬性類型是:String[]// 屬性名:names//String[] names();//int[] ages();//int age();
}
以上是自定義了一個注解:Component
該注解上面修飾的注解包括:Target注解和Retention注解,這兩個注解被稱為元注解。
Target注解用來設置Component注解可以出現的位置,以上代表表示Component注解只能用在類和接口上。
Retention注解用來設置Component注解的保持性策略,以上代表Component注解可以被反射機制讀取。
String value(); 是Component注解中的一個屬性。該屬性類型String,屬性名是value。
注解怎么使用?
//@Component(屬性名 = 屬性值, 屬性名 = 屬性值, 屬性名 = 屬性值....)
//@Component(value = "userBean")
// 如果屬性名是value,value可以省略。
@Component("userBean")
public class User {// 編譯器報錯,不能出現在這里。//@Component(value = "test")//private String name;
}
用法簡單,語法格式:@注解類型名(屬性名=屬性值, 屬性名=屬性值, 屬性名=屬性值......)
userBean為什么使用雙引號括起來,因為value屬性是String類型,字符串。
另外如果屬性名是value,則在使用的時候可以省略屬性名
通過反射機制怎么讀取注解?
//@Component(屬性名 = 屬性值, 屬性名 = 屬性值, 屬性名 = 屬性值....)
//@Component(value = "userBean")
// 如果屬性名是value,value可以省略。
@Component("userBean")
public class User {// 編譯器報錯,不能出現在這里。//@Component(value = "test")//private String name;
}
public class ReflectAnnotationTest1 {public static void main(String[] args) throws Exception{// 通過反射機制怎么讀取注解// 獲取類Class<?> aClass = Class.forName("com.bean.User");// 判斷類上面有沒有這個注解if (aClass.isAnnotationPresent(Component.class)) {// 獲取類上的注解Component annotation = aClass.getAnnotation(Component.class);// 訪問注解屬性System.out.println(annotation.value());}}
}
接下來,我們來寫一段程序,當Bean類上有Component注解時,則實例化Bean對象,如果沒有,則不實例化對象。
我們準備兩個Bean,一個上面有注解,一個上面沒有注解。
@Component("vipBean")
public class Vip {
}
public class Order {
}
假設我們現在只知道包名:com.bean。至于這個包下有多少個Bean我們不知道。哪些Bean上有注解,哪些Bean上沒有注解,這些我們都不知道,如何通過程序全自動化判斷。
package com.client;import com.annotation.Component;import java.io.File;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;/*** @className ComponentScan* @since 1.0**/
public class ComponentScan {public static void main(String[] args){Map<String,Object> beanMap = new HashMap<>();// 目前只知道一個包的名字,掃描這個包下所有的類,當這個類上有@Component注解的時候,實例化該對象,然后放到Map集合中。String packageName = "com.bean";// 開始寫掃描程序。// . 這個正則表達式代表任意字符。這里的"."必須是一個普通的"."字符。不能是正則表達式中的"."// 在正則表達式當中怎么表示一個普通的"."字符呢?使用 \. 正則表達式代表一個普通的 . 字符。String packagePath = packageName.replaceAll("\\.", "/");System.out.println(packagePath);// com是在類的根路徑下的一個目錄。URL url = ClassLoader.getSystemClassLoader().getResource(packagePath);String path = url.getPath();System.out.println(path);System.out.println("==========================");// 獲取一個絕對路徑下的所有文件File file = new File(path);File[] files = file.listFiles();Arrays.stream(files).forEach(f -> {try {System.out.println(f.getName());System.out.println(f.getName().split("\\.")[0]);String className = packageName + "." + f.getName().split("\\.")[0];System.out.println(className);// 通過反射機制解析注解Class<?> aClass = Class.forName(className);// 判斷類上是否有這個注解if (aClass.isAnnotationPresent(Component.class)) {// 獲取注解Component annotation = aClass.getAnnotation(Component.class);String id = annotation.value();// 有這個注解的都要創建對象Object obj = aClass.newInstance();beanMap.put(id, obj);}} catch (Exception e) {e.printStackTrace();}});System.out.println(beanMap);}
}
2.聲明Bean的注解
負責聲明Bean的注解,常見的包括四個:
-
@Component
-
@Controller
-
@Service
-
@Repository
源碼如下:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(value = {ElementType.TYPE})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Component {String value();
}
package org.springframework.stereotype;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {@AliasFor(annotation = Component.class)String value() default "";
}
package org.springframework.stereotype;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {@AliasFor(annotation = Component.class)String value() default "";
}
package org.springframework.stereotype;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {@AliasFor(annotation = Component.class)String value() default "";
}
通過源碼可以看到,@Controller、@Service、@Repository這三個注解都是@Component注解的別名。 也就是說:這四個注解的功能都一樣。用哪個都可以。 只是為了增強程序的可讀性,建議:
-
控制器類上使用:Controller
-
service類上使用:Service
-
dao類上使用:Repository
他們都是只有一個value屬性。value屬性用來指定bean的id,也就是bean的名字。
3.Spring注解的使用
如何使用以上的注解呢?
-
第一步:加入aop的依賴
-
第二步:在配置文件中添加context命名空間
-
第三步:在配置文件中指定掃描的包
-
第四步:在Bean類上使用注解
第一步:加入aop的依賴 我們可以看到當加入spring-context依賴之后,會關聯加入aop的依賴。所以這一步不用做。
第二步:在配置文件中添加context命名空間
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.spring6.bean"/>
</beans>
第四步:在Bean類上使用注解
@Component(value = "userBean")
public class User {
}
@Controller("vipBean") // 如果屬性名是value的話,這個屬性名可以省略。
public class Vip {
}
@Service(value="orderBean") // 如果你把整個value屬性全部省略了,bean有沒有默認的名稱?有:類名首字母變小寫就是bean的名字。
public class Order {
}
@Repository(value = "studentBean")
public class Student {
}
編寫測試程序:
@Testpublic void testBeanComponent(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");User userBean = applicationContext.getBean("userBean", User.class);System.out.println(userBean);Vip vipBean = applicationContext.getBean("vipBean", Vip.class);System.out.println(vipBean);Order orderBean = applicationContext.getBean("orderBean", Order.class);System.out.println(orderBean);Student studentBean = applicationContext.getBean("studentBean", Student.class);System.out.println(studentBean);}
如果注解的屬性名是value,那么value是可以省略的。
@Repository( "studentBean")
public class Student {
}
@Controller("vipBean") // 如果屬性名是value的話,這個屬性名可以省略。
public class Vip {
}
test
@Testpublic void testBeanComponent(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");User userBean = applicationContext.getBean("userBean", User.class);System.out.println(userBean);Vip vipBean = applicationContext.getBean("vipBean", Vip.class);System.out.println(vipBean);Order orderBean = applicationContext.getBean("orderBean", Order.class);System.out.println(orderBean);Student studentBean = applicationContext.getBean("studentBean", Student.class);System.out.println(studentBean);}
照常運行:
如果把value屬性徹底去掉,spring會被Bean自動取名嗎?會的。并且默認名字的規律是:Bean類名首字母小寫即可。
@Repository
public class Student {
}
@Service // 如果你把整個value屬性全部省略了,bean有沒有默認的名稱?有:類名首字母變小寫就是bean的名字。
public class Order {
}
Test
@Testpublic void testBeanComponent(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");//Order orderBean = applicationContext.getBean("orderBean", Order.class);Order orderBean = applicationContext.getBean("order", Order.class);System.out.println(orderBean);//Student studentBean = applicationContext.getBean("studentBean", Student.class);Student studentBean = applicationContext.getBean("student", Student.class);System.out.println(studentBean);}
如果是多個包怎么辦?有兩種解決方案:
-
第一種:在配置文件中指定多個包,用逗號隔開。
-
第二種:指定多個包的共同父包。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><!--給Spring框架指定要掃描哪些包中的類--><!--<context:component-scan base-package="com.powernode.spring6.bean"/>--><!--多個包,使用逗號隔開。--><!--<context:component-scan base-package="com.powernode.spring6.bean,com.powernode.spring6.dao"/>--><!--多個包,也可以指定這多個包共同的父包,但是這肯定要犧牲一部分效率。--><context:component-scan base-package="com.powernode.spring6"/></beans>
4.選擇性實例化Bean
假設在某個包下有很多Bean,有的Bean上標注了Component,有的標注了Controller,有的標注了Service,有的標注了Repository,現在由于某種特殊業務的需要,只允許其中所有的Controller參與Bean管理,其他的都不實例化。這應該怎么辦呢?
⑴.第一種解決方案
@Component
public class A {public A() {System.out.println("A的無參數構造方法執行");}
}@Controller
class B {public B() {System.out.println("B的無參數構造方法執行");}
}@Service
class C {public C() {System.out.println("C的無參數構造方法執行");}
}@Repository
class D {public D() {System.out.println("D的無參數構造方法執行");}
}@Controller
class E {public E() {System.out.println("E的無參數構造方法執行");}
}
我只想實例化bean2包下的Controller。配置文件這樣寫:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><!--第一種解決方案:use-default-filters="false"如果這個屬性是false,表示com.spring6.bean2包下所有的帶有聲明Bean的注解全部失效。@Component @Controller @Service @Repository全部失效。--><context:component-scan base-package="com.spring6.bean2" use-default-filters="false"><!--只有@Repository @Service 被包含進來,生效。--><context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan></beans>
Test
@Testpublic void testChoose(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-choose.xml");}
use-default-filters="true" 表示:使用spring默認的規則,只要有Component、Controller、Service、Repository中的任意一個注解標注,則進行實例化。
use-default-filters="false" 表示:不再spring默認實例化規則,即使有Component、Controller、Service、Repository這些注解標注,也不再實例化。
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> 表示只有Controller進行實例化。
⑵.第二種解決方案
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><!--第二種解決方案:use-default-filters="true"如果這個屬性的值是true,表示com.powernode.spring6.bean2下的所有的帶有聲明Bean的注解全部生效。use-default-filters="true" 默認值就是true,不用寫。--><context:component-scan base-package="com.spring6.bean2"><!--@Controller注解失效--><context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan></beans>
也可以將use-default-filters設置為true(不寫就是true),并且采用exclude-filter方式排出哪些注解標注的Bean不參與實例化:
5.負責注入的注解
@Component @Controller @Service @Repository 這四個注解是用來聲明Bean的,聲明后這些Bean將被實例化。接下來我們看一下,如何給Bean的屬性賦值。給Bean屬性賦值需要用到這些注解:
-
@Value
-
@Autowired
-
@Qualifier
-
@Resource
⑴.@Value
當屬性的類型是簡單類型時,可以使用@Value注解進行注入。
MyDataSource類
package com.spring6.bean3;import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;/*** @className MyDataSource* @since 1.0**/
@Component
public class MyDataSource implements DataSource {@Value(value = "com.mysql.cj.jdbc.Driver")private String driver;@Value("jdbc:mysql://localhost:3306/spring6")private String url;@Value("root")private String username;@Value("123456")private String password; // 使用@Value注解注入的話,可以用在屬性上,并且可以不提供setter方法。public void setDriver(String driver) {this.driver = driver;}public void setUrl(String url) {this.url = url;}public void setUsername(String username) {this.username = username;}public void setPassword(String password) {this.password = password;}@Overridepublic String toString() {return "MyDataSource{" +"driver='" + driver + '\'' +", url='" + url + '\'' +", username='" + username + '\'' +", password='" + password + '\'' +'}';}@Overridepublic Connection getConnection() throws SQLException {return null;}@Overridepublic Connection getConnection(String username, String password) throws SQLException {return null;}@Overridepublic PrintWriter getLogWriter() throws SQLException {return null;}@Overridepublic void setLogWriter(PrintWriter out) throws SQLException {}@Overridepublic void setLoginTimeout(int seconds) throws SQLException {}@Overridepublic int getLoginTimeout() throws SQLException {return 0;}@Overridepublic Logger getParentLogger() throws SQLFeatureNotSupportedException {return null;}@Overridepublic <T> T unwrap(Class<T> iface) throws SQLException {return null;}@Overridepublic boolean isWrapperFor(Class<?> iface) throws SQLException {return false;}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.spring6.bean3"/></beans>
Test
@Testpublic void testDIByAnnotation(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-di-annotation.xml");MyDataSource myDataSource = applicationContext.getBean("myDataSource", MyDataSource.class);System.out.println(myDataSource);}
把set方法注釋掉,依舊可以運行
使用@Value注解注入的話,可以用在屬性上,并且可以不提供setter方法。
通過以上代碼可以發現,我們并沒有給屬性提供setter方法,但仍然可以完成屬性賦值。 如果提供setter方法,并且在setter方法上添加@Value注解,可以完成注入嗎?嘗試一下:
@Component
public class Product {private String name;private int age;// @Value注解也可以使用在方法上。@Value("隔壁老王2")public void setName(String name) {this.name = name;}@Value("33")public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +'}';}
}
Test
@Testpublic void testDIByAnnotation(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-di-annotation.xml");MyDataSource myDataSource = applicationContext.getBean("myDataSource", MyDataSource.class);System.out.println(myDataSource);Product product = applicationContext.getBean("product", Product.class);System.out.println(product);}
通過測試可以得知,@Value注解可以直接使用在屬性上,也可以使用在setter方法上。都是可以的。都可以完成屬性的賦值。 為了簡化代碼,以后我們一般不提供setter方法,直接在屬性上使用@Value注解完成屬性賦值。
出于好奇,我們再來測試一下,是否能夠通過構造方法完成注入:
@Component
public class Product {private String name;private int age;public Product(@Value("隔壁老王3") String name, @Value("42") int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +'}';}
}
Test
@Testpublic void testDIByAnnotation(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-di-annotation.xml");MyDataSource myDataSource = applicationContext.getBean("myDataSource", MyDataSource.class);System.out.println(myDataSource);Product product = applicationContext.getBean("product", Product.class);System.out.println(product);}
通過測試得知:@Value注解可以出現在屬性上、setter方法上、以及構造方法的形參上。可見Spring給我們提供了多樣化的注入。太靈活了。
⑵@Autowired與@Qualifier
@Autowired注解可以用來注入非簡單類型。被翻譯為:自動連線的,或者自動裝配。
單獨使用@Autowired注解,默認根據類型裝配。【默認是byType】 看一下它的源碼:
package org.springframework.beans.factory.annotation;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {boolean required() default true;
}
源碼中有兩處需要注意:
-
第一處:該注解可以標注在哪里?
-
構造方法上
-
方法上
-
形參上
-
屬性上
-
注解上
-
-
第二處:該注解有一個required屬性,默認值是true,表示在注入的時候要求被注入的Bean必須是存在的,如果不存在則報錯。如果required屬性設置為false,表示注入的Bean存在或者不存在都沒關系,存在的話就注入,不存在的話,也不報錯。
我們先在屬性上使用@Autowired注解:
OrderDao接口
public interface OrderDao {void insert();}
OrderDaoImplForMySQL實現類
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="org.dao,org.service"/></beans>
@Repository("orderDaoImplForMySQL")
public class OrderDaoImplForMySQL implements OrderDao {@Overridepublic void insert() {System.out.println("MySQL數據庫正在保存訂單信息...");}
}
OrderService類
@Service("orderService")
public class OrderService {// @Autowired注解使用的時候,不需要指定任何屬性,直接使用這個注解即可。// 這個注解的作用是根據類型byType進行自動裝配。@Autowiredprivate OrderDao orderDao;public void generate(){orderDao.insert();}
}
Test
@Testpublic void testAutowired(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-autowired.xml");org.service.OrderService orderService = applicationContext.getBean("orderService", org.service.OrderService.class);orderService.generate();}
當存在多個相同類型的Bean時,無法自動裝配
添加OrderDaoImplOracle類
/*** @className OrderDaoImplForOracle* @since 1.0**/
@Repository("orderDaoImplForOracle")
public class OrderDaoImplForOracle implements OrderDao {@Overridepublic void insert() {System.out.println("Oracle數據庫正在保存訂單信息...");}
}
解決方法:根據名字進行裝配,Qualifier
@Service("orderService")
public class OrderService {// @Autowired注解使用的時候,不需要指定任何屬性,直接使用這個注解即可。// 這個注解的作用是根據類型byType進行自動裝配。//@Autowired//private OrderDao orderDao;// 如果想解決以上問題,只能根據名字進行裝配。// @Autowired和@Qualifier聯合使用,可以根據名字進行裝配。@Autowired@Qualifier("orderDaoImplForOracle")//@Qualifier("orderDaoImplForMySQL")private OrderDao orderDao;public void generate(){orderDao.insert();}
}
Test
@Testpublic void testAutowired(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-autowired.xml");org.service.OrderService orderService = applicationContext.getBean("orderService", org.service.OrderService.class);orderService.generate();}
以上構造方法和setter方法都沒有提供,經過測試,仍然可以注入成功。
接下來,再來測試一下@Autowired注解出現在setter方法上:
@Service("orderService")
public class OrderService {private OrderDao orderDao;@Autowiredpublic void setOrderDao(OrderDao orderDao) {this.orderDao = orderDao;}public void generate(){orderDao.insert();}
}
我們再來看看能不能出現在構造方法上:
@Service("orderService")
public class OrderService {private OrderDao orderDao;@Autowiredpublic OrderService(OrderDao orderDao) {this.orderDao = orderDao;}public void generate(){orderDao.insert();}
}
再來看看,這個注解能不能只標注在構造方法的形參上:
@Service("orderService")
public class OrderService {private OrderDao orderDao;public OrderService(@Autowired OrderDao orderDao) {this.orderDao = orderDao;}public void generate(){orderDao.insert();}
}
還有更勁爆的,當有參數的構造方法只有一個時,@Autowired注解可以省略。
@Service("orderService")
public class OrderService {private OrderDao orderDao;public OrderService(OrderDao orderDao) {this.orderDao = orderDao;}public void generate(){orderDao.insert();}
}
當然,如果有多個構造方法,@Autowired肯定是不能省略的。
總結:
@Autowired注解可以出現在:屬性上、構造方法上、構造方法的參數上、setter方法上。
當帶參數的構造方法只有一個,@Autowired注解可以省略。
@Autowired注解默認根據類型注入。如果要根據名稱注入的話,需要配合@Qualifier注解一起使用。
⑶@Resource
@Resource注解也可以完成非簡單類型注入。那它和@Autowired注解有什么區別?
-
@Resource注解是JDK擴展包中的,也就是說屬于JDK的一部分。所以該注解是標準注解,更加具有通用性。(JSR-250標準中制定的注解類型。JSR是Java規范提案。)
-
@Autowired注解是Spring框架自己的。
-
@Resource注解默認根據名稱裝配byName,未指定name時,使用屬性名作為name。通過name找不到的話會自動啟動通過類型byType裝配。
-
@Autowired注解默認根據類型裝配byType,如果想根據名稱裝配,需要配合@Qualifier注解一起用。
-
@Resource注解用在屬性上、setter方法上。
-
@Autowired注解用在屬性上、setter方法上、構造方法上、構造方法參數上。
@Resource注解屬于JDK擴展包,所以不在JDK當中,需要額外引入以下依賴:【如果是JDK8的話不需要額外引入依賴。高于JDK11或低于JDK8需要引入以下依賴。】
<dependency><groupId>jakarta.annotation</groupId><artifactId>jakarta.annotation-api</artifactId><version>2.1.1</version>
</dependency>
一定要注意:如果你用Spring6,要知道Spring6不再支持JavaEE,它支持的是JakartaEE9。(Oracle把JavaEE貢獻給Apache了,Apache把JavaEE的名字改成JakartaEE了,大家之前所接觸的所有的 javax.* 包名統一修改為 jakarta.*包名了。)
<dependency><groupId>javax.annotation</groupId><artifactId>javax.annotation-api</artifactId><version>1.3.2</version>
</dependency>
@Resource注解的源碼如下:
測試一下:
StudentDao接口
public interface StudentDao {void deleteById();}
/*** @className StudentDaoImplForMySQL* @since 1.0**/
@Repository("studentDaoImplForMySQL")
public class StudentDaoImplForMySQL implements StudentDao {@Overridepublic void deleteById() {System.out.println("mysql數據庫正在刪除學生信息");}
}
/*** @className StudentService* @since 1.0**/
@Service("studentService")
public class StudentService {@Resource(name = "studentDaoImplForMySQL")private StudentDao studentDao;public void deleteStudent(){studentDao.deleteById();}}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="cn.dao,cn.service"/></beans>
Test
@Testpublic void testResource(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-resource.xml");StudentService studentService = applicationContext.getBean("studentService", StudentService.class);studentService.deleteStudent();}
測試Resource注解能不能出現在set方法上,做以下修改
/*** @className StudentService* @since 1.0**/
@Service("studentService")
public class StudentService {private StudentDao studentDao;@Resource(name = "studentDaoImplForMySQL")public void setStudentDao(StudentDao studentDao) {this.studentDao = studentDao;}public void deleteStudent(){studentDao.deleteById();}}
不能出現在構造方法上,直接報錯
當@Resource注解使用時沒有指定name的時候,還是根據name進行查找,這個name是屬性名。
@Service("studentService")
public class StudentService {@Resourceprivate StudentDao studentDao;public void deleteStudent(){studentDao.deleteById();}}
@Repository("studentDao")
public class StudentDaoImplForMySQL implements StudentDao {@Overridepublic void deleteById() {System.out.println("mysql數據庫正在刪除學生信息");}
}
但是根據類型注入在有多個Bean時就不行了
增加一個類
@Repository
public class StudentDaoImplForOracle implements StudentDao {@Overridepublic void deleteById() {System.out.println("oracle正在刪除student。。。。");}
}
6.全注解式開發
所謂的全注解開發就是不再使用spring配置文件了。寫一個配置類來代替配置文件。
package cn;import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;@Configuration
@ComponentScan({"cn.dao", "cn.service"})
public class Spring6Config {
}
編寫測試程序:不再new ClassPathXmlApplicationContext()對象了。
@Testpublic void testNoXML(){AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spring6Config.class);StudentService studentService = context.getBean("studentService", StudentService.class);studentService.deleteStudent();}