spring拾遺(一)——IOC的應用

前言

spring這個玩意,其實要單純談使用的話,其實內容也不少,工作中有些東西用的挺多,但是很不繫統。去年也嘗試過看spring的源碼,但是通常陷入到"我是誰,我在哪兒的,我幹嘛要看這玩意"的思考(因爲太暈了),想想這玩意還是得學啊,不能因爲不懂,不能因爲它讓我懷疑人生就放棄,還是得從頭擼。後來思考發現自己太過依賴百度,忽略了官網這玩意,這次就從官網出發。總結一些更多的操作,讓spring的學習更加系統。這篇博客並不是一個官網的翻譯,整理的內容會非常零散,畢竟只是補充我們平常使用spring框架忽略的東西。

什麼是IOC,IOC和DI有啥區別

這又是一個靈魂拷問,在面試中遇到過很多次,不同的人回答方式不同。個人覺得這個東西一句話就能解決,DI其實就是IOC的一種實現,IOC只是一種設計理念。除了DI的方式來實現IOC,DL的方式也是IOC的一種實現。(之前有一種說法,DI是IOC的另一種表達方式,感覺不太全面)

控制反轉(Inversion of Control,縮寫爲IoC),是面向對象編程中的一種設計原則,可以用來減低計算機代碼之間的耦合度。其中最常見的方式叫做依賴注入(Dependency Injection,簡稱DI),還有一種方式叫“依賴查找”(Dependency Lookup

爲什麼要有IOC

自己new它不香嗎?不好意思,自己new,真的香不起來。我們在剛接觸spring的時候,可能很多人會告訴你spring就是避免了你new一個引用對象的過程,比如類A中維護了一個B的引用,我們在A中使用B的時候就不需要自己去new一個B對象了,IOC直接給我們注入了。但是我們自己new也能達到目標,爲啥new就香不起來呢?

這樣想想,A依賴B,正常使用沒有問題,但是如果某一天,需求說B中需要加入事務控制,這時候我們需要對B做一個代理的處理,生成一個新的代理對象,這就尷尬了,所有引用了B的對象都得重新new,這就不那麼香了。

所以我們需要有個東西能幫我們管理各個對象之間的依賴,並幫我們完成這種依賴注入的工作,這就是IOC爲啥重要的原因。因此從一定層度上來講IOC就是完成我們注入的工作(這也就是爲啥有些人理解爲IOC就是DI)

給出spring文檔的地址吧:https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html

順帶提一句官網中其實提供了三種實例,基於xml的(schema-based),基於註解的(annotation-based)和基於java config的。

Dependency Injection(DI)

關於依賴注入,官網中有這樣一句話

DI exists in two major variants: Constructor-based dependency injection and Setter-based dependency injection.

DI 主要有兩種方式:一種是構造器注入,另一種就是getter和setter注入。

構造器注入

有一個這個類,這個類的構造方法有兩個參數,這兩個參數

package x.y;

public class ThingOne {

    public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
        // ...
    }
}

將bean的管理告知spring容器

<beans>
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg ref="beanTwo"/>
        <constructor-arg ref="beanThree"/>
    </bean>

    <bean id="beanTwo" class="x.y.ThingTwo"/>

    <bean id="beanThree" class="x.y.ThingThree"/>
</beans>

通過標籤完成構造參數的注入

使用xml配置的時候,可以直接注入屬性的value值,但是這種用法並不常見,官網的1.4.2節中有相關介紹,其中還提到了c-namespace和p-namespace來完成註冊,這個並不常用,這裏只是順帶提一下而已。

基於setter方法的注入

官網中的例子是這樣的

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

    public void setBeanOne(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }

    public void setBeanTwo(YetAnotherBean beanTwo) {
        this.beanTwo = beanTwo;
    }

    public void setIntegerProperty(int i) {
        this.i = i;
    }
}

基於這個實例的配置是這樣的

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- setter injection using the nested ref element -->
    <property name="beanOne">
        <ref bean="anotherExampleBean"/>
    </property>

    <!-- setter injection using the neater ref attribute -->
    <property name="beanTwo" ref="yetAnotherBean"/>
    <property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

從配置上看似乎就是標籤完成了注入。

這裏拋出一個問題:例如上述的ExampleBean類,已經有了AnotherBean和YetAnotherBean的引用,按照道理說我們只要告知spring這三個bean就可以了不需要再設置property了,爲啥還需要在xml配置文件中動態配置property呢?這不是有點多餘麼?能不能讓spring自動給我們完成設置property的操作?

這就要提到自動注入了。

自動裝配

在spring的文檔中的1.4.5節中介紹了自動裝配的相關內容,關於自動裝配的優缺點文檔中有以下表述:

優點

1.Autowiring can significantly reduce the need to specify properties or constructor arguments. 
	自動裝配可以顯著減少屬性的指定和構造方法中的參數。
2.Autowiring can update a configuration as your objects evolve. For example, if you need to add a dependency to a class, that dependency can be satisfied automatically without you needing to modify the configuration. 
	如果對象的配置更改了,自動裝配可以自動更新這個配置。例如:如果你在一個類中增加了一個引用,如果這個引用的對象發生了改變,你不用在配置中重新指定了,自動裝配在開發中非常常用。

缺點:

Explicit dependencies in property and constructor-arg settings always override autowiring. You cannot autowire simple properties such as primitives, Strings, and Classes (and arrays of such simple properties). This limitation is by-design.
簡單翻譯一下:自動裝配機制從設計層面就不支持簡單類型的注入。

Autowiring is less exact than explicit wiring. Although, as noted in the earlier table, Spring is careful to avoid guessing in case of ambiguity that might have unexpected results. The relationships between your Spring-managed objects are no longer documented explicitly.
自動裝配並不如顯示指定精確,儘管如前面章節介紹的,spring在模棱兩可的狀態下儘量避免猜測的操作,但是使用了自動裝配之後,spring管理的對象之間的關係就不如通過文檔來管理清晰了。

Wiring information may not be available to tools that may generate documentation from a Spring container.
不需要XML等文檔來維護類與類之間的依賴了,注入信息就無法從文檔中獲取了。

Multiple bean definitions within the container may match the type specified by the setter method or constructor argument to be autowired. For arrays, collections, or Map instances, this is not necessarily a problem. However, for dependencies that expect a single value, this ambiguity is not arbitrarily resolved. If no unique bean definition is available, an exception is thrown.
容器中定義的多個bean,可能需要與setter和構造方法中的參數匹配,這對數組,集合和map實例並不是什麼大問題。但是如果一個類需要一個明確的bean,而這個bean類型對應的實現類並不唯一則會拋出異常。

總體來說如果編碼中採用相關的規範,則這些缺點完全是可以規避的,同時針對簡單類型的注入在實際中用的並不多。

自動裝配的模式

官網中關於自動裝配的方式有如下四種,這裏簡單說一下,no就是不開啓自動裝配,byName就是根據名稱進行自動裝配,byType就是根據類型完成自動裝配,constructor就是根據構造函數完成自動裝配
在這裏插入圖片描述

引入自動裝配

基於xml

官網並沒有給出詳細的例子,但是我們在使用spring的時候就會知道,在xml配置文件中beans標籤中,開啓default-autowire屬性,就可以設置一個默認的注入方式。但是在每一個bean標籤,我們也可指定autowire,麼一個bean標籤指定的autowired屬性會覆蓋全局的default-autowire屬性。

<?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"
        default-autowire="byName">

    <bean id="indexDao" class="com.learn.ioc.dao.IndexDao"></bean>

    <bean id="indexService" class="com.learn.ioc.service.IndexService"></bean>

</beans>

在已經定義了兩個bean的情況下分別設置一個全局的autowire屬性。

本身bean的定義如下

@Repository
public class IndexDao {
    public void beanTest(){
        System.out.println("bean test dao");
    }
}

@Service
public class IndexService {
    private IndexDao indexDao;
    public void setDao(IndexDao indexDao) {
        this.indexDao = indexDao;
    }
    public void testIndexService(){
        System.out.println("this in service");
        indexDao.beanTest();
    }
}

測試的類

/**
 * autor:liman
 * createtime:2020/2/25
 * comment:
 */
public class XmlApplication {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext classPathXmlApplicationContext =
                new ClassPathXmlApplicationContext("classpath:applicationContext.xml");

        IndexService indexService = (IndexService) classPathXmlApplicationContext.getBean("indexService");
        indexService.testIndexService();
    }
}

正常的調用而已,沒啥可說的,但是運行結果會出現這樣的問題

在這裏插入圖片描述

sorry還是會出現空指針異常的情況,因爲設置的是注入方式是byName,這個就是根據set方法的名稱來的,我們在service中定義的是set方法的方法名稱是setDao,則對應的是給我們注入name屬性(默認情況如果一個bean沒有配置name則id=name)值爲dao的bean,我們將setDao改成setIndexDao或者將bean中的indexDao改成dao,則能順利運行。

還是上面的實例,只是加入byType的注入方式,則依舊能順利運行。

<bean id="indexService" class="com.learn.ioc.service.IndexService" autowire="byType"></bean>

一個接口多個實現類

如下所示,多個實現類交給spring託管,配置service注入方式爲byType,但是,運行後會出現經典的一個異常

<?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 id="indexDaoOne" class="com.learn.ioc.dao.IndexDaoOne"></bean>
    <bean id="indexDaoTwo" class="com.learn.ioc.dao.IndexDaoTwo"></bean>

    <bean id="indexService" class="com.learn.ioc.service.IndexService" autowire="byType"></bean>
</beans>

運行之前的代碼,會出現如下異常。

在這裏插入圖片描述

畢竟根據byType注入,兩個實現類,就變得模糊了。如果變成byName的方式注入,則能正常完成操作。

註解的方式

其實談到註解的注入方式,我們用過spring的都應該知道,@Autowired、@Component和@Service等等一些註解(關於@Component和@Service,@Repository等的區別,spring官網中有這樣一句話:@Repository, @Service, and @Controller can also carry additional semantics in future releases of the Spring Framework. Thus, if you are choosing between using @Component or @Service for your service layer, @Service is clearly the better choice. 這句話的意思就是@Repository等註解後續會有其他含義,如果考慮到服程序的擴展性,建議用@Service這一類更加清晰的註解)

加入一個config類

/**
 * autor:liman
 * createtime:2020/2/25
 * comment:
 */
@Configuration
@ComponentScan("com.learn.ioc")//掃描指定的包
public class AppConfig {
}

原來的各個類中註解修改爲如下:

@Service
public class IndexService {
    @Autowired
    private IIndexDao indexDao;
    public void setDao(IIndexDao indexDao) {
        this.indexDao = indexDao;
    }
    public void testIndexService(){
        System.out.println("this in service");
        indexDao.beanTest();
    }
}

@Repository
public class IndexDaoOne implements IIndexDao{
    public void beanTest(){
        System.out.println("bean test dao one");
    }
}

@Repository
public class IndexDaoTwo implements IIndexDao{
    public void beanTest(){
        System.out.println("bean test dao two");
    }
}

測試類:這裏採用獲取註解配置的applicationContext

/**
 * autor:liman
 * createtime:2020/2/25
 * comment:
 */
public class AnnotationApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext
                = new AnnotationConfigApplicationContext(AppConfig.class);
        IndexService indexService = annotationConfigApplicationContext.getBean(IndexService.class);
        indexService.testIndexService();

    }
}

運行之後會發現異常,因爲這裏用的是@Autowired註解自動注入的,但是這個接口有兩個實現類,依舊會出現上述運行類型指向模糊的問題,如果將@Autowired改成@Resource並指定name屬性,則正常運行

如下所示:

@Service
public class IndexService {

    @Resource(name="indexDaoTwo")	//這裏通過resource的name屬性指定
    private IIndexDao indexDao;

    public void setDao(IIndexDao indexDao) {
        this.indexDao = indexDao;
    }

    public void testIndexService(){
        System.out.println("this in service");
        indexDao.beanTest();
    }
}

由於這個問題比較難以通過文本來表述,因此這裏直接拋出官網中的一個結論:@Autowired與@Resource最大的區別就在於@Autowired默認採用byType的方式注入,如果byType注入失敗則採用byName注入。@Resource則相反,默認採用byName的方式注入,如果默認的方式失敗會採用byType。兩種方式都找不到指定需要注入的bean則會拋出異常。

關於懶加載

在官網的1.4.4節

By default, ApplicationContext implementations eagerly create and configure all singleton beans as part of the initialization process. Generally, this pre-instantiation is desirable, because errors in the configuration or surrounding environment are discovered immediately, as opposed to hours or even days later. When this behavior is not desirable, you can prevent pre-instantiation of a singleton bean by marking the bean definition as being lazy-initialized. A lazy-initialized bean tells the IoC container to create a bean instance when it is first requested, rather than at startup.
翻譯:默認情況下,容器的初始化過程中是創建並配置所有的Singleton的bean實例的。通常來說,這種預先初始化的方式是值得推薦的,因爲配置或者環境變量方面的錯誤可以在這一階段直接暴露並被發現。如果不想這樣,我們通過將這個bean標記爲懶加載的方式來阻止容器提前初始化這個bean。一個被標記爲懶加載的bean就是告知IOC,讓IOC在第一次需要的時候進行加載而不是容器啓動的時候。

這句話似乎沒啥重要的,只是告知了我們可以通過配置一些屬性來實現懶加載,但是接下來官網中的話就有點意思了。

In XML, this behavior is controlled by the lazy-init attribute on the <bean> element, as the following example shows:

<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>

When the preceding configuration is consumed by an ApplicationContext, the lazy bean is not eagerly pre-instantiated when the ApplicationContext starts, whereas the not.lazy bean is eagerly pre-instantiated.

依舊來個翻譯:

在XML配置的方式下,可通過配置bean中的lazy-init屬性來告知容器這個bean是懶加載的。當進一步解析上述配置的時候,id爲lazy的bean就會被懶加載,而name爲“not.lazy”的bean則不會。

However, when a lazy-initialized bean is a dependency of a singleton bean that is not lazy-initialized, the ApplicationContext creates the lazy-initialized bean at startup, because it must satisfy the singleton’s dependencies. The lazy-initialized bean is injected into a singleton bean elsewhere that is not lazy-initialized.

個人覺得上述一段話比較重要,當一個被標記爲懶加載的bean爲一個Singleton(非懶加載)bean的依賴的時候,這個bean就不是懶加載的了,容器依舊會在啓動的時候就創建這個被標記爲懶加載的bean,因爲必須要滿足Singleton的bean的依賴,懶加載的bean依舊會在啓動的時候注入到非懶加載的bean中

總體來說懶加載用的比較少,但是面試到了上面一段話還是加分項。

作用域

這個在官網的1.5節中介紹,六種作用域如下表所示,這裏簡單總結一下Singleton和prototype兩種類型的作用域,其他作用域等在學習了spring mvc之後再繼續討論。

在這裏插入圖片描述

singleton——其實就是單例的意思,prototype是原型的意思,初步理解其實Singleton和prototype就是對應容器中實例的個數,前者初始化的實例在容器中只有一個,而標記爲後者的則在容器中會有多個。

先來一個基本的實例

prototype的實例

/**
 * autor:liman
 * createtime:2020/2/27
 * comment:
 */
@Service("prototypeBean")
@Scope("prototype")
public class PrototypeBean {

    public void testPrototype(){
        System.out.println("i am prototype bean");
    }
}

singleton的實例

/**
 * autor:liman
 * createtime:2020/2/27
 * comment:
 */
@Service("singletonBean")
@Scope("singleton")
public class SingletonBean {

    public void testSingleton(){
        System.out.println("i am singleton");
    }
}

然後通過getBean查看兩者的區別

/**
 * autor:liman
 * createtime:2020/2/27
 * comment:
 */
public class SingleAndPrototypeApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new
                AnnotationConfigApplicationContext(AppConfig.class);

        System.out.println("================prototype hashcode===============");
        PrototypeBean proOne = applicationContext.getBean(PrototypeBean.class);
        PrototypeBean proTwo = applicationContext.getBean(PrototypeBean.class);
        System.out.println(proOne.hashCode());
        System.out.println(proTwo.hashCode());

        System.out.println("=================singleton hashcode==============");
        SingletonBean singletonBeanOne = applicationContext.getBean(SingletonBean.class);
        SingletonBean singletonBeanTwo = applicationContext.getBean(SingletonBean.class);
        System.out.println(singletonBeanOne.hashCode());
        System.out.println(singletonBeanTwo.hashCode());

    }

}

運行結果如下所示:

在這裏插入圖片描述

prototype獲得的兩個實例並不一樣。singleton獲得的實例是一樣的。

如果singleton的依賴prototype的,會有啥情況呢?這個問題其實在spring文檔中的1.5.3節中也有解釋

When you use singleton-scoped beans with dependencies on prototype beans, be aware that dependencies are resolved at instantiation time. Thus, if you dependency-inject a prototype-scoped bean into a singleton-scoped bean, a new prototype bean is instantiated and then dependency-injected into the singleton bean. The prototype instance is the sole instance that is ever supplied to the singleton-scoped bean.

翻譯:當你想使用一個single作用域的bean且這個bean依賴prototype的bean,需要知道這個依賴是在初始化的時候解析的。因此,如果你往一個single的bean中注入prototype的bean,確實會注入一個prototype的bean。但是這個prototype的bean是唯一的。

However, suppose you want the singleton-scoped bean to acquire a new instance of the prototype-scoped bean repeatedly at runtime. You cannot dependency-inject a prototype-scoped bean into your singleton bean, because that injection occurs only once, when the Spring container instantiates the singleton bean and resolves and injects its dependencies. If you need a new instance of a prototype bean at runtime more than once, see Method Injection
翻譯:雖然你想通過singleton的bean獲取一個prototype的bean。但是你並不能往singleton的bean中注入一個prototype的bean,因爲這個注入只會發生一次,這個注入就發生在spring容器初始化的時候。如果你真的想在singleton的bean中獲取一個真正多例的bean的話,需要參考Method Injection這一節。

測試一下:

@Service("singletonBean")
@Scope("singleton")
public class SingletonBean {

    @Autowired
    private PrototypeBean prototypeBean; //這裏注入一個多例的bean

    public void testSingleton(){
        System.out.println("i am singleton");
    }

    public PrototypeBean getPrototypeBean() {
        return prototypeBean;
    }
}

運行結果如下:

在這裏插入圖片描述

並沒有給我們想要的多例,因爲單例的bean只能初始化一次。但是官網中給了一種方式,通過單例的bean獲取多例的bean的方式——實現ApplicationContextWare接口

@Service("singletonBean")
@Scope("singleton")
public class SingletonBean implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Autowired
    private PrototypeBean prototypeBean; //這裏注入一個多例的bean

    public void testSingleton(){
        System.out.println("i am singleton");
    }

    public PrototypeBean getPrototypeBean() {
        //在getPrototype的時候,從容器中去獲取
        return applicationContext.getBean(PrototypeBean.class);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

運行結果:

在這裏插入圖片描述

hashCode不同,則說明不是同一個實例。這樣處理與spring框架本身比較耦合。後面還有一種lookup的方式,這裏就不介紹了,官網中有相關實例。

bean的一些其他處理

可能初步看的時候這個標題有點讓人迷惑,好比我們在實際編碼過程中,我們寫了正常的核心邏輯之後,還需要做一個try/catch/finally的處理,這裏的其他處理就相當於bean初始化之後的操作。這一節其實是想總結spring官網中的1.6.1這一節

簡單來說,就是在bean創建完成正式使用之前,或者bean銷燬的時候,spring提供了一些我們自處理的回調方法。

Initialization Callbacks

初始化的回調

The org.springframework.beans.factory.InitializingBean interface lets a bean perform initialization work after the container has set all necessary properties on the bean.

翻譯:InitializingBean這個接口提供了一個容器設置完bean的必要屬性之後的一些處理邏輯

We recommend that you do not use the InitializingBean interface, because it unnecessarily couples the code to Spring. Alternatively, we suggest using the @PostConstruct annotation or specifying a POJO initialization method.In the case of XML-based configuration metadata, you can use the init-method attribute to specify the name of the method that has a void no-argument signature. With Java configuration, you can use the initMethod attribute of @Bean.

翻譯:但是我們並不推薦使用這個接口,因爲這樣與spring框架本身就耦合了。我們推薦使用@PostConstruct註解或者顯示指定POJO的初始化方法。如果使用的是XML的配置,則指定init-method的屬性,如果是java config則指定@Bean中的initMethod屬性。

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>

後兩者就好的多並沒有與spring框架耦合

Destruction Callbacks

Implementing the org.springframework.beans.factory.DisposableBean interface lets a bean get a callback when the container that contains it is destroyed.

同樣的,針對銷燬的操作,spring提供了DisposableBean這個接口,但是spring依舊不推薦這種方式,而是推薦採用@PreDestroy,或者在XML中配置destroy-method或者java config中配置destroyMethod的方式來完成。

實例

因爲比較簡單,這裏就直接利用spring不推薦的方式來實現了

定義的一個bean

@Component("beanCallback")
public class BeanCallback implements InitializingBean,DisposableBean {

    public BeanCallback() {
        System.out.println("this is construct method");
    }


    @Override
    public void destroy() throws Exception {
        System.out.println("bean destory");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("bean life cycle after properties set ");
    }
}

具體操作的實例

/**
 * autor:liman
 * createtime:2020/2/27
 * comment:
 */
public class BeanlifecycleApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext =
                new AnnotationConfigApplicationContext(AppConfig.class);

        BeanCallback bean = applicationContext.getBean(BeanCallback.class);
        applicationContext.close();
    }

}

運行結果

在這裏插入圖片描述

總結

本篇博客按照官網中總結了一些自己平時使用spring框架過程中容易忽略的東西,大部分都根據官網來梳理。基本梳理出了spring文檔中關於ioc的關鍵點。針對component-scan這個並沒有總結,這個可以直接參考官網。

針對spring的掃描可以增加掃描的效率,這個在1.10.9節中有簡單的介紹,需要引入這依賴包

 <dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.2.3.RELEASE</version>
        <optional>true</optional>
    </dependency>
</dependencies>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章