Spring官網閱讀(二)(依賴注入及方法注入)

上篇文章我們學習了官網中的1.2,1.3兩小節,主要是涉及了容器,以及Spring實例化對象的一些知識。這篇文章我們繼續學習Spring官網,主要是針對1.4小節,主要涉及到Spring的依賴注入。雖然只有一節,但是涉及的東西確不少。話不多說,開始正文。

依賴注入:

根據官網介紹,依賴注入主要分爲兩種方式

  1. 構造函數注入

  2. Setter方法注入

    ​ 官網:

在這裏插入圖片描述

我們分別對以上兩種方式進行測試,官網上用的是XML的方式,我這邊就採用註解的方式了:

測試代碼如下,我們通過在Service中注入LuBanService這個過程來

public class Main02 {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext ac = new 
            // config類主要完成對類的掃描
            AnnotationConfigApplicationContext(Config.class);
		Service service = (Service) ac.getBean("service");
		service.test();
	}
}

@Component
public class LuBanService {
	LuBanService(){
		System.out.println("luBan create ");
	}
}
測試setter方法注入
@Component
public class Service {

	private LuBanService luBanService;

	public Service() {
		System.out.println("service create");
	}

	public void test(){
		System.out.println(luBanService);
	}
	// 通過autowired指定使用set方法完成注入
	@Autowired
	public void setLuBanService(LuBanService luBanService) {
		System.out.println("注入luBanService by setter");
		this.luBanService = luBanService;
	}
}

輸出如下:


luBan create 
service create
注入luBanService by setter  // 驗證了確實是通過setter注入的
com.dmz.official.service.LuBanService@5a01ccaa

測試構造函數注入
@Component
public class Service {

	private LuBanService luBanService;
    
    public Service() {
		System.out.println("service create by no args constructor");
	}
	
    // 通過Autowired指定使用這個構造函數,否則默認會使用無參
	@Autowired
	public Service(LuBanService luBanService) {
		System.out.println("注入luBanService by constructor with arg");
		this.luBanService = luBanService;
		System.out.println("service create by constructor with arg");
	}

	public void test(){
		System.out.println(luBanService);
	}
}

輸出如下:

luBan create 
注入luBanService by constructor // 驗證了確實是通過constructor注入的
service create by constructor
com.dmz.official.service.LuBanService@1b40d5f0

疑問:

在上面的驗證中,大家可能會有以下幾個疑問:

  1. @Autowired直接加到字段上跟加到set方法上有什麼區別?爲什麼我們驗證的時候需要將其添加到setter方法上?

    • 首先我們明確一點,直接添加@Autowired註解到字段上,不需要提供setter方法也能完成注入。以上面的例子來說,Spring會通過反射獲取到Service中luBanService這個字段,然後通過反射包的方法,Filed.set(Service,luBanService)這種方式來完成注入
    • 我們將@Autowired添加到setter方法時,我們可以通過斷點看一下方法的調用棧,如下:

在這裏插入圖片描述

對於這種方式來說,最終是通過Method.invoke(object,args)的方式來完成注入的,這裏的method對象就是我們的setter方法

  1. @Autowired爲什麼加到構造函數上可以指定使用這個構造函數?

    • 我們先可以測試下,如果我們不加這個註解會怎麼樣呢?我把前文中的@Autowired註解註釋,然後運行發現

    luBan create 
    service create by no args constructor  // 可以看到執行的是空參構造
    null
    

    先不急得出結論,我們再進行一次測試,就是兩個函數上都添加@Autowired註解呢?

    Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'service': Invalid autowire-marked constructor: public com.dmz.official.service.Service(com.dmz.official.service.LuBanService). Found constructor with 'required' Autowired annotation already: public com.dmz.official.service.Service()
    

    發現直接報錯了,報錯的大概意思是已經找到了一個被@Autowired註解標記的構造函數,同時這個註解中的required屬性爲true。後來我測試了將其中一個註解中的required屬性改爲false,發現還是報同樣的錯,最終將兩個註解中的屬性都改爲false測試才通過,並且測試結果跟上面的一樣,都是執行的無參構造。

    要說清楚這一點,涉及到兩個知識

    • Spring中的注入模型,下篇文章專門講這個
    • Spring對構造函數的推斷。這個到源碼階段我打算專門寫一篇文章,現在我們暫且記得:

    在***默認的注入模型***下,Spring如果同時找到了兩個***符合要求的構造函數***,那麼Spring會採用默認的無參構造進行實例化,如果這個時候沒有無參構造,那麼此時會報錯java.lang.NoSuchMethodException。什麼叫符合要求的構造函數呢?就是構造函數中的參數Spring能找到,參數被Spring所管理。

    這裏需要着重記得:一,默認注入模型;二,符合要求的構造函數

  2. 如果我們同時採用構造注入加屬性注入會怎麼樣呢?

    在沒有進行測試前,我們可以大膽猜測下,Spring雖然能在構造函數裏完成屬性注入,但是這屬於實例化對象階段做的事情,那麼在後面真正進行屬性注入的時候,肯定會將其覆蓋掉。現在我們來驗證我們的結論

    @Component
    public class Service {
    	private LuBanService luBanService;	
    	public Service(LuBanService luBanService) {
    		System.out.println("注入luBanService by constructor with arg");
    		this.luBanService = luBanService;
    		System.out.println("service create by constructor with arg");
    	}
    	public void test(){
    		System.out.println(luBanService);
    	}
    	@Autowired
    	public void setLuBanService(LuBanService luBanService) {
    		System.out.println("注入luBanService by setter");
    		this.luBanService = null;
    	}
    }
    

    運行結果:


    注入luBanService by constructor with arg  // 實例化時進行了一次注入
    service create by constructor with arg   // 完成了實例化
    注入luBanService by setter    // 屬性注入時將實例化時注入的屬性進行了覆蓋
    null
    

區別:

在這裏插入圖片描述

根據上圖中官網所說,我們可以得出如下結論:

  1. 構造函數注入跟setter方法注入可以混用
    1. 對於一些強制的依賴,我們最好使用構造函數注入,對於一些可選依賴我們可以採用setter方法注入
  2. Spring團隊推薦使用構造函數的方式完成注入。但是對於一些參數過長的構造函數,Spring是不推薦的
方法注入:

我們不完全按照官網順序進行學習,先看這一小節,對應官網上的位置如下圖:

在這裏插入圖片描述

爲什麼需要方法注入

首先我們思考一個問題,在有了依賴注入的情況下,爲什麼還需要方法注入這種方式呢?換而言之,方法注入解決了什麼問題?

我們來看下面這種場景:

@Component
public class MyService {

	@Autowired
	private LuBanService luBanService;

	public void test(int a){
		luBanService.addAndPrint(a);
	}

}

@Component
// 原型對象
@Scope("prototype")
public class LuBanService {
	int i;

	LuBanService() {
		System.out.println("luBan create ");
	}
	// 每次將當前對象的屬性i+a然後打印
	public void addAndPrint(int a) {
		i+=a;
		System.out.println(i);
	}
}

public class Main02 {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
		MyService service = (MyService) ac.getBean("myService");
		service.test(1);
		service.test(2);
		service.test(3);
	}
}

在上面的代碼中,我們有兩個Bean,MyService爲單例的Bean,LuBanService爲原型的Bean。我們的本意可能是希望每次都能獲取到不同的LuBanService,預期的結果應該打印出:


1,2,3


實際輸出:


1
3
6


這個結果說明我們每次調用到的LuBanService是同一個對象。當然,這也很好理解,因爲在依賴注入階段我們就完成了LuBanService的注入,之後我們在調用測試方法時,不會再去進行注入,所以我們一直使用的是同一個對象。

我們可以這麼說,原型對象在這種情況下,失去了原型的意義,因爲每次都使用的是同一個對象。那麼如何解決這個問題呢?只要我每次在使用這個Bean的時候都去重新獲取就可以了,那麼這個時候我們可以通過方法注入來解決。

通過注入上下文(applicationContext對象)

又分爲以下兩種方式:

  • 實現org.springframework.context.ApplicationContextAware接口
@Component
public class MyService implements ApplicationContextAware {

	private ApplicationContext applicationContext;

	public void test(int a) {
		LuBanService luBanService = ((LuBanService) applicationContext.getBean("luBanService"));
		luBanService.addAndPrint(a);
	}

	@Override
	public void setApplicationContext(@Nullable ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}
}
  • 直接注入上下文
@Component
public class MyService{
	@Autowired
	private ApplicationContext applicationContext;

	public void test(int a) {
		LuBanService luBanService = ((LuBanService) applicationContext.getBean("luBanService"));
		luBanService.addAndPrint(a);
	}
}
通過@LookUp的方式(也分爲註解跟XML兩種方式,這裏只演示註解的)
@Component
public class MyService{
	public void test(int a) {
		LuBanService luBanService = lookUp();
		luBanService.addAndPrint(a);
	}
	// 
	@Lookup
	public LuBanService lookUp(){
		return null;
	}
}
方法注入 之 replace-method

方法注入還有一種方式,即通過replace-method這種形式,沒有找到對應的註解,所以這裏我們也就用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="myService" class="com.dmz.official.service.MyService">
		<replaced-method replacer="replacer" name="test"/>
	</bean>

	<bean id="replacer" class="com.dmz.official.service.MyReplacer"/>
</beans>
public class MyReplacer implements MethodReplacer {
    @Override
   public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
        System.out.println("替代"+obj+"中的方法,方法名稱:"+method.getName());
        System.out.println("執行新方法中的邏輯");
        return null;
    }
}

public class MyService{
    public void test(int a) {
        System.out.println(a);
    }
}

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext cc =
            new ClassPathXmlApplicationContext("application.xml");
        MyService myService = ((MyService) cc.getBean("myService"));
        myService.test(1);
    }
}

執行結果:

替代com.dmz.official.service.MyService$$EnhancerBySpringCGLIB$$61c14242@63e31ee中的方法,方法名稱:test
執行新方法中的邏輯

這裏需要注意一點:

我在測試replace-method這種方法注入的方式時,受動態代理的影響,一直想將執行我們被替代的方法。用代碼體現如下:

public class MyReplacer implements MethodReplacer {

	@Override
	public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
//		System.out.println("替代"+obj+"中的方法,方法名稱:"+method.getName());
//		System.out.println("執行新方法中的邏輯");
		method.invoke(obj,args);
		return null;
	}
}

但是,這段代碼是無法執行的,會報棧內存溢出。因爲obj是我們的代理對象,method.invoke(obj,args)執行時會進入方法調用的死循環。最終我也沒有找到一種合適的方式來執行被替代的方法。目前看來這可能也是Spring的設計,所以我們使用replace-method的場景應該是想完全替代某種方法的執行邏輯,而不是像AOP那樣更多的用於在方法的執行前後等時機完成某些邏輯。

依賴注入跟方法注入的總結:
  • 我們首先要明確一點,什麼是依賴(Dependencies)?來看官網中的一段話:

在這裏插入圖片描述

可以說,一個對象的依賴就是它自身的屬性,Spring中的依賴注入就是屬性注入

  • 我們知道一個對象由兩部分組成:屬性+行爲(方法),可以說Spring通過屬性注入+方法注入的方式掌控的整個bean。
  • 屬性注入跟方法注入都是Spring提供給我們用來處理Bean之間協作關係的手段
  • 屬性注入有兩種方式:構造函數,Setter方法。
  • 方法注入(LookUp Method跟Replace Method)需要依賴動態代理完成
  • 方法注入對屬性注入進行了一定程度上的補充,因爲屬性注入的情況下,原型對象可能會失去原型的意義,見:爲什麼需要方法注入

畫圖如下:

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章