自動化裝配的確有很大的便利性,但是卻並不能適用在所有的應用場景,比如需要裝配的組件類不是由自己的應用程式維護,而是引用了第三方的類庫,這個時候自動裝配便無法實現,Spring對此也提供了相應的解決方案,那就是通過顯示的裝配機制——Java配置和XML配置的方式來實現bean的裝配。 Java配置類裝 ...
自動化裝配的確有很大的便利性,但是卻並不能適用在所有的應用場景,比如需要裝配的組件類不是由自己的應用程式維護,而是引用了第三方的類庫,這個時候自動裝配便無法實現,Spring對此也提供了相應的解決方案,那就是通過顯示的裝配機制——Java配置和XML配置的方式來實現bean的裝配。
Java配置類裝配bean
我們還是藉助上篇博文中的老司機開車的示例來講解。Car介面中有開車的drive方法,該介面有兩個實現——QQCar和BenzCar
package spring.impl; import spring.facade.Car; public class QQCar implements Car { @Override public void drive() { System.out.println("開QQ車"); } }
既然是通過Java代碼來裝配bean,那就是不是我們上一篇講的通過組件掃描的方式來發現應用程式中的bean的自動裝配機制了,而是需要我們自己通過配置類來聲明我們的bean。我們先通過@Configuration註解來創建一個Spring的配置類,該類中包含了bean的創建細節——
import org.springframework.context.annotation.Configuration; import spring.facade.Car; import spring.impl.QQCar; /** * @Configuration 表明該類是Spring的一個配置類,該類中會包含應用上下文創建bean的具體細節 * @Bean 告訴Spring該方法會返回一個要註冊成為應用上下文中的bean的對象 */ @Configuration public class CarConfig { @Bean public Car laoSiJi() { return new QQCar(); } }
以上類中創建的bean實例預設情況下和方法名是一樣的,我們也可以通過@Bean註解的name屬性自定義ID,例如 @Bean(name = "chenbenbuyi") ,那麼在獲取bean的時候根據你自己定義的ID獲取即可。接著我們測試——
package spring.test; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import spring.config.CarConfig; import spring.facade.Car; public class CarTest { @Test public void carTest() { ApplicationContext context = new AnnotationConfigApplicationContext(CarConfig.class); //根據ID從容器容獲取bean Car car = (Car) context.getBean("chenbenbuyi"); car.drive(); } }
以上測試能夠成功輸出,這就表明我們能夠獲取到QQCar的實例對象的,而這也是最簡單的基於Java配置類來裝配bean的示例了。但是你可能會說,明明是我們自己創建的Car的實例,怎麼就成了Spring為我們創建的呢?好吧,我們把@Bean註解拿開,測試當然是無法通過,會拋NoSuchBeanDefinitionException異常。這裡,你可能需要好好理解控制反轉的思想了:因為現在對於bean創建的控制權我們是交給了Spring容器的,如果沒有@Bean註解,方法就只是一個普通方法,方法體返回的實例對象就不會註冊到應用上下文(容器)中,也就說,Spring不會為我們管理該方法返回的實例對象,當我們在測試類中向容器伸手要對象的時候,自然就找不到。
上述示例過於簡單,現在,我們要更進一步,給簡單的對象添加依賴,來完成稍微複雜一點的業務邏輯。車是需要老司機來開的,於是我們同上篇一樣定義一個Man類,Man的工作就是開車——
package spring.impl; import spring.facade.Car; public class Man { private Car car;public Man(Car car) { this.car = car; } public void work() { car.drive(); } }
Car的對象實例是通過構造器註入,而Car的實例對象在配置類中通過方法laoSiJi()返回,所以我們在配置類中可以直接調用laoSiJi方法獲取bean註入到Man的實例對象——
package spring.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import spring.facade.Car; import spring.impl.BenzCar; import spring.impl.Man; @Configuration public class CarConfig { @Bean public Car laoSiJi() { return new BenzCar(); } @Bean public Man work() { return new Man(laoSiJi()); } }
測試類中通過上下文對象的getBean("work")方法就可以獲取到Man的實例對象,從而完成對老司機開車的測試。或許,你會覺得,work方法是通過調用laoSiJi方法才獲取的Car的實例的,實際上並非如此。因為有了@Bean註解,Spring會攔截所有對該註解方法的調用,直接返回該方法創建的bean,也即容器中的管理的bean。也就是說,laoSiJi方法返回的bean交給了Spring容器管理後,當其他地方需要實例對象的時候,是直接從容器中獲取的第一次調用方法產生的實例對象,而不會重覆的調用laoSiJi方法。我們可以如下測試——
package spring.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import spring.facade.Car; import spring.impl.BenzCar; import spring.impl.Man; @Configuration public class CarConfig { @Bean public Car laoSiJi() { System.out.println("方法調用"); return new BenzCar(); } @Bean public Man work() { return new Man(laoSiJi()); } @Bean public Man work2() { return new Man(laoSiJi()); } }
如上測試你會發現,雖然我定義了兩個方法來獲取Man實例,但是控制台只輸出了一次調用列印,即證明方法只在最初返回bean的時候被調用了一次,而後的實例獲取都是直接從容器中獲取的。這也就是預設情況下Spring返回的實例都是單例的原因:一旦容器中註冊了實例對象,應用程式需要的時候,就直接給予,不用重覆創建。當然,很多情況下我們不會如上面的方式去引入依賴的bean,而可能會通過參數註入的方式,這樣你就可以很靈活的使用不同的裝配機制來滿足對象之間的依賴關係,比如下麵這種自動裝配的方式給Man的實例註入依賴的Car對象——
package spring.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import spring.facade.Car; import spring.impl.Man; @Configuration @ComponentScan("spring.impl") public class CarConfig { @Bean public Man work(Car car) { return new Man(car); } }
當然,如果你喜歡去簡就繁,也可以通過XML配置文件配置依賴的bean。下麵再來看看XML的方式如何裝配bean。
XML配置文件裝配bean
使用XML配置文件的方式裝配bean,首要的就是要創建一個基於Spring配置規範的XML文件,該配置文件以<beans>為根元素(相當於Java配置的@Configuration註解),包含一個或多個<bean>元素(相當於配置類中@Bean註解)。針對上文的汽車示例,如果改成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"> <!--通過類的全限定名來聲明要創建的bean--> <bean class="spring.impl.BenzCar"></bean> </beans>
然後,從基於XML的配置文件中載入上下文定義,我們就能根據ID獲取到對應的bean了——
package spring.test; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import spring.facade.Car; public class CarTest { @Test public void carTest() { ApplicationContext context = new ClassPathXmlApplicationContext("resource/applicationContext.xml"); //XML的方式如果沒有明確給定ID,預設bean的ID會根據類的全限定名來命名,以#加計數序號的方式命名。 Car car = (Car)context.getBean("spring.impl.BenzCar#0"); car.drive(); } }
當然,示例中使用自動化的命名ID看起來逼格滿滿,但其實並不實用,如果需要引用bean的實例就有點操蛋了,實際應用中當然還是要藉助<bean>的id屬性來自定義命名。
構造器註入
給<bean>元素設置id屬性,在構建另外的對象實例的時候,就可以很方便的引用,譬如上面基於Java的配置中的構造器註入,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"> <bean id="car" class="spring.impl.BenzCar"></bean> <bean id="man" class="spring.impl.Man"> <!--通過Man的構造器註入Car的實例對象--> <constructor-arg ref="car"></constructor-arg> </bean> </beans>
而有時候我們並不一定都是將對象的引用裝配到依賴對象中,也可以簡單的註入字面值——
package spring.impl; import spring.facade.Car; public class Man { private Car car; private String str;
public Man(String str ,Car car) { this.car = car; this.str = str; } public void work() { System.out.println(str); car.drive(); } }
<?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="car" class="spring.impl.BenzCar"></bean> <bean id="man" class="spring.impl.Man"> <!--分別註入字面值和對象的應用--> <constructor-arg value="陳本布衣"></constructor-arg> <constructor-arg ref="car"></constructor-arg> </bean> </beans>
接著,我們繼續對已有代碼做些改動,將註入的參數改為Car的List集合——
public Man(List<Car> cars) { this.cars = cars; }
那麼配置文件就可以這樣配置——
<?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="benzCar" class="spring.impl.BenzCar"></bean> <bean id="qqCar" class="spring.impl.QQCar"></bean> <bean id="man" class="spring.impl.Man"> <!--通過<list>子元素實現List集合對象的裝配--> <constructor-arg> <list> <ref bean="benzCar"/> <ref bean="qqCar"/> </list> </constructor-arg> </bean> </beans>
如果是需要註入集合中的字面值,寫法如下——
<?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="benzCar" class="spring.impl.BenzCar"></bean> <bean id="qqCar" class="spring.impl.QQCar"></bean> <bean id="man" class="spring.impl.Man"> <!--通過<list>子元素實現List集合字面值的裝配--> <constructor-arg> <list> <value>這裡直接填寫字面值</value> <value>陳本布衣</value> </list> </constructor-arg> </bean> </beans>
我們可以採用同樣的方式裝配Set集合,只是Set集合會忽略掉重覆的值,而且順序也不保證。此處不做演示。
屬性註入
構造器註入是一種強依賴註入,而很多時候我們並不傾向於寫那種依賴性太強的代碼,而屬性的Setter方法註入作為一種可選性依賴,在實際的開發中是應用得非常多的。上面Man類如果要通過屬性註入的方式註入Car的實例,就該是這樣子——
package spring.impl; import spring.facade.Car; public class Man { private Car car; public void setCar(Car car) { this.car = car; } public void work() { car.drive(); } }
<?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="benzCar" class="spring.impl.BenzCar"></bean> <bean id="qqCar" class="spring.impl.QQCar"></bean> <bean id="man" class="spring.impl.Man"> <!--通過屬性註入的方式註入Car的實例--> <property name="car" ref="benzCar"></property> </bean> </beans>
以上示例中,XML配置文件中屬性註入的屬性名必須要和Java類中Setter方法對應的屬性名一致。而對於字面量的註入,和上面構造器的方式類似,只不過使用的元素名換成了<property>而已,下麵僅做展示——
<bean id="man" class="spring.impl.Man"> <property name="str" value="字面量的註入"></property> <property name="list"> <list> <value>集合的字面量註入1</value> <value>集合的字面量註入2</value> </list> </property> </bean>
<bean id="benzCar" class="spring.impl.BenzCar"></bean> <bean id="qqCar" class="spring.impl.QQCar"></bean> <bean id="man" class="spring.impl.Man"> <!--屬性註入的方式註入集合--> <property name="cars"> <list> <ref bean="qqCar"></ref> <ref bean="benzCar"></ref> </list> </property> </bean>
三種裝配方式的混合使用
在同一個應用程式中,Spring常見的這三種裝配方式我們可能都會用到,而對於不同的裝配方式,他們之間如何實現相互引用從而整合到一起的呢?我們先看看Java配置類的引用問題。試想如果Java配置類中的bean數量過多,我們可能會考慮拆分。在本文的示例中,Man類實例的創建必須通過構造器註入Car的實例,如果把兩個實例的產生分成兩個配置類,那麼在依賴註入的配置類中可以通過@Import註解引入被依賴的配置類——
package spring.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import spring.facade.Car; import spring.impl.Man; @Configuration @Import(CarConfig.class) //通過@Import註解引入產生Car實例的配置類 public class ManConfig { @Bean public Man work(Car car) { return new Man(car); } }
但是如果Car的實例不是通過Java類配置的,而是通過XML方式配置的方式配置,我們只需通過@ImportResource註解將配置bean的XML文件引入即可,只不過這個時候要保證XML中被依賴的bean的id要和Java配置類中的形參保持一致——
package spring.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; import spring.facade.Car; import spring.impl.Man; @Configuration @ImportResource("classpath:resource/applicationContext.xml") public class ManConfig { @Bean public Man work(Car car) { return new Man(car); } }
而如果bean是採用XML進行裝配,如果需要裝配的bean過多,我們當然還是會根據業務拆分成不同的配置文件,然後使用<improt>元素進行不同XML配置文件之間的引入,形如: <import resource="classpath:xxx.xml" /> ;而如果要在XML中引入Java配置,只需將Java配置類當成普通的bean在XML中進行聲明即可,但是在測試的時候要註意開啟組件掃描,因為載入XML配置的上下文對象只會載入XML配置文件中的bean定義,無法讓基於Java配置類產生bean的裝配機制自動生效——
<?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.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--開啟組件掃描,在測試的時候配置類才能向容器中註冊類中聲明的bean--> <context:component-scan base-package="spring"/> <!--XML中引入Java配置類:將配置類聲明為bean--> <bean class="spring.config.CarConfig"></bean> <bean id="man" class="spring.impl.Man"> <constructor-arg ref="laoSiJi"></constructor-arg> </bean> </beans>
最後說一點,不管是Java配置還是XML配置,有個通常的做法就是創建一個比所有配置都更高層次的根配置類/文件,該配置不聲明任何的bean,只用來將多個配置組合在一起,從而讓配置更易於維護和擴展。好了,以上便是兩種bean的裝配方式的簡單講解,如有紕漏,歡迎指正,不勝感激。