SpringBoot(一)-全註解下的IOC

注:“SpringBoot入門筆記”系列接下來的內容大部分將來自楊開振所著人民郵電出版社所出版的《深入淺出SpringBoot》一書,源碼均經過本人測試,侵刪。


IoC是一種通過描述來生成或者獲取對象的技術,這個技術不是Spring甚至不是Java所獨有的,它意味着通過描述來創建對象。SpringBoot並不建議使用XML,而是通過註解來生成對象。
對象之間並不只是孤立的,它們之間還可能存在依賴的關係。爲此Spring提供了依賴注入的功能,使得我們能夠通過描述來管理各個對象之間的關係。
在Spring中把每一個需要管理的對象稱之爲SpringBean,而Spring管理這些Bean的容器,被我們稱爲SpringIoC容器。
IoC容器需要具備兩個基本功能:

  • 通過描述管理Bean
  • 通過描述完成Bean的依賴關係

IoC容器

簡介

SpringIoC容器是管理Bean的容器,在Spring的定義中,它要求所有IoC容器都實現BeanFactory接口。
這個接口源碼如下所示:
看不懂也沒關係,只需要知道,IoC容器需要實現以下幾個方法:
1.允許在其它IoC中使用類型或者名稱獲取Bean;
2.判斷Bean在IoC中是否爲單例
3.判斷Bean是否爲原型
等等,即可。

package org.springframework.beans.factory;
/**
spring ioc 的核心接口,從配置資源(eg: XML)中加載Bean定義並提供對Bean操作的基礎方法
Bean:由一個String類型字符串唯一標識的java對象
*/
public interface BeanFactory {
/**
    獲取類型爲FactoryBean的對象時,在BeanName前加上&會獲取到該對象本身的引用,
    否則會獲取調用該對象的getObject方法
*/
    String FACTORY_BEAN_PREFIX = "&";

/**
    返回指定BeanName(或別名)的Bean
*/
    Object getBean(String name) throws BeansException;

/**
    返回同時匹配指定BeanName(或別名)和類型的Bean
*/
    <T> T getBean(String name, Class<T> requiredType) throws BeansException;

/**
    返回指定BeanName(或別名)的Bean,並把所提供的參數作爲構造函數參數或工廠方法參數初始化Bean
*/
    Object getBean(String name, Object... args) throws BeansException;

/**
    返回指定類型的Bean,如果存在多個bean定義有相同的類型,則拋NoUniqueBeanDefinitionException異常
*/
    <T> T getBean(Class<T> requiredType) throws BeansException;

/**
    返回指定類型的Bean,並把所提供的參數作爲構造函數參數或工廠方法參數初始化Bean
*/
    <T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

/**
    返回一個ObjectProvider<T>該類型允許對Bean處於not available情況和not-unique情況時做處理
*/
    <T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);

/**
    作用同上
*/
    <T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);

/**
    根據指定的BeanName或別名判斷factory是否包含bean定義(concrete
     or abstract, lazy or eager, in scope or not)或由外部註冊的單例,
    返回true時並不保證一定能通過getBean獲取相應實例(eg: 抽象Bean)
*/
    boolean containsBean(String name);

/**
    根據指定的BeanName或別名判斷factory中該Bean的scope是否是Singleton
    返回false時不代表scope是prototype(使用自定義的scope時)
*/
    boolean isSingleton(String name) throws NoSuchBeanDefinitionException;

/**
    根據指定的BeanName或別名判斷factory中該Bean的scope是否是Prototype
    返回false時不代表scope是Singleton(使用自定義的scope時)
*/
    boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

/**
    根據指定的BeanName或別名判斷factory中該Bean的類型是否與提供的類型匹配
*/
    boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;

/**
    根據指定的BeanName或別名判斷factory中該Bean的類型是否與提供的類型匹配
*/
    boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;

/**
    根據指定的BeanName或別名獲取factory中該Bean的類型,對於FactoryBean類型,將調用它的
    getObjectType()
*/
    @Nullable
    Class<?> getType(String name) throws NoSuchBeanDefinitionException;

/**
    根據指定的BeanName或別名返回factory中該Bean的別名,
    如果提供的時別名將返回BeanName和其他別名的數組,BeanName
    置與第一個元素
*/
    String[] getAliases(String name);

}

由於BeanFactory的功能還不夠強大,因此,Spring在BeanFactory的基礎上,還設計了更爲高級的接口Application,它是BeanFactory的子接口之一。這兩個接口是Spring體系中最爲重要的接口設計。

案例——IoC容器裝配Bean

我們學習SpringBoot裝配和獲取Bean的方法從IoC容器AnnotationConfigApplicationContext開始。
首先定義一個Java對象User.java

package springboot.chapter3.pojo;

public class User {
    private Long id;
    private String userName;
    private String note;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

然後定義java配置文件

package springboot.chapter3.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// @Configuration代表這是一個Java配置文件,Spring容器會根據這個文件去裝配Bean
@Configuration
public class AppConfig {
    //@Bean代表將initUser方法返回的POJO裝配到IoC容器中
    @Bean(name="user")
    public User initUser(){
        User user = new User();
        user.setId(1L);
        user.setUserName("user_name_1");
        user.setNote("note_1");
        return user;
    }
}

入口函數

package springboot.chapter3.config;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import org.apache.log4j.Logger;


public class IoCTeat {
    private static Logger log = Logger.getLogger(IoCTeat.class);

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        User user = ctx.getBean(User.class);
        log.info(user.getId());
    }
}

不要忘了maven加入一個依賴

<dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
</dependency>

運行之後,名稱爲user的Bean就被裝配到IoC容器中了。
我們可以通過getBean()方法獲取對應的Bean,並且將Bean的屬性信息輸出出來
如果實操一下上面的代碼的話,邏輯是十分清楚的:
定義一個對象–>config文件實例化bean並且裝配–>主函數加載config並且使用bean

裝配你的Bean

掃描來裝配Bean

使用@Bean裝配每一個Bean,是一件十分麻煩的事情。
幸好,Spring允許我們掃描來裝配Bean
可以使用@Component和@ComponentScan兩個註解掃描裝配Bean。
@Component標明哪個類被掃描進入,@ComponentScan則標明何種策略去掃描裝配Bean。
把User.class放在config包中,如下面代碼所示

package springboot.chapter3.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component("user")
//這個類將作爲“user”被IoC掃描裝配

public class User {
//    @Value制定了具體的值
    @Value("1")
    private Long id;
    @Value("user_name_1")
    private String userName;
    @Value("note_1")
    private String note;
/*setter and getter*/
}

Java配置文件中加入@ComponentScan,如下所示

package springboot.chapter3.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

// @Configuration代表這是一個Java配置文件,Spring容器會根據這個文件去裝配Bean
@Configuration
@ComponentScan
//掃描也僅僅配置類所在的包
public class AppConfig {
}

入口函數main不變,這樣運行仍然可以裝配我們想要配置的Bean。
細心的人會發現,我們將User類放在了config包,這樣就不太合理了。
實際上,@ComponentScan還允許我們自定義掃描配置項。
首先,可以通過配置項basePackages定義掃描的包名,在沒有定義的情況下,它只會掃描當前包和其子包下的路徑;還可以通過basePackageClasses定義掃描的類;其中還有includeFilters和excludeFilters,前者是定義滿足過濾器條件的Bean纔去掃描,後者則是派出過濾器條件的Bean,它們都需要通過一個註解@Filter去定義。
我們把AppConfig中的註解修改爲:
@ComponentScan(“springboot.chapter3.")
或者@ComponentScan(basePackages={"springboot.chapter3.
”})
無論採用何種方式都能使得IoC容器掃描User類,而包名可以通過正則式匹配。

掃描裝配排除項

現在,假設我們有一個UserService類,爲了標註它爲服務類,將類標註@Service,這個標準注入了@component,所以會被Spring掃描
這時我們可以修改註解爲:
@ComponentScan(“springboot.chapter3.*”,
excludeFilters={@Filter(classes = {UserService.class})})

自定義第三方Bean

現實中的java應用往往需要引入第三方的包,並且很有可以希望把第三方包的類對象也放入到SpringIoC容器中,這時候@Bean註解就發揮作用了.
例如對於DBCP數據源的引用:
加入依賴

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

然後直接配置即可(在AppConfig中加入):

    @Bean(name = "dataSource")
    public DataSource getDataSource() {
        Properties props = new Properties();
        props.setProperty("driver", "com.mysql.jdbc.Driver");
        props.setProperty("url", "jdbc:mysql");
        props.setProperty("usename", "root");
        props.setProperty("password", "123456");
        DataSource dataSource = null;
        try {
            dataSource = (DataSource) BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
            return dataSource;
    }

依賴注入

在Spring框架下,當Bean實例 A運行過程中需要引用另外一個Bean實例B時,Spring框架會創建Bean的實例B,並將實例B通過實例A的構造函數、set方法、自動裝配和註解方式注入到實例A,這種注入實例Bean到另外一個實例Bean的過程稱爲依賴注入。
爲了說明依賴注入在Spring中的用法,書上給出了這麼一個例子:
人類依賴於動物
首先定義兩個接口:

package springboot.chapter3.pojo.definition;

public interface Animal {
    
    public void use();
    
}
package springboot.chapter3.pojo.definition;

public interface Person {
//    使用動物服務
    public void service();
//    設置動物
    public void setAnimal(Animal animal);
    
}

兩個實現類:

package springboot.chapter3.pojo;

import org.springframework.beans.factory.annotation.Autowired;
import springboot.chapter3.pojo.definition.Animal;
import springboot.chapter3.pojo.definition.Person;

public class BussinessPerson implements Person {
    @Autowired
    private Animal animal = null;

    @Override
    public void service() {
        this.animal.use();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }
}
package springboot.chapter3.pojo;

import springboot.chapter3.pojo.definition.Animal;

public class Dog implements Animal {
    @Override
    public void use() {
//        class.getSimpleName()用於得到類的簡寫名稱
        System.out.println("狗"+Dog.class.getSimpleName()+"是看門用的");
    }
}

注意這裏的註解 @Autowired,它會根據屬性的類型找到相應的Bean進行注入,因爲Dog類繼承了animal,所以會把Dog實例注入到BussinessPerson中.
同樣可以使用:

ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        Person user = ctx.getBean(BussinessPerson.class);
//        log.info(user.getId());
        user.service();

進行測試
(如果你不是很懂得話,沒關係,可以繼續往下看,因爲我看到這兒也不是很懂)

實現依賴注入的註解@Autowired

@Autowired是我們使用最多的註解之一。
我們很容易想到一個問題,如果Animal有兩個繼承類(假設另一個是Cat),那麼Autowired怎麼確定哪個注入呢?
事實上,這種情況會報錯。
我們可以使用:

@Autowired
private Animal dog = null;

來進行匹配。
除了能夠標註屬性之外,@Autowired還可以標註方法。

消除歧義——@Primary和Qualifier

使用上面所述的方法消除bug會導致代碼耦合,
@Primary可以標識兩個類實例中更優先的實例
當要選擇多個標識@Primary的類中的一個類時,可以使用:

@Autowired
@Qualifier("dog")
private Animal dog = null;
帶參數的構造方法類的裝配

如下所示,參數之前加入@Autowired即可

public BussinessPerson(@Autowired @Qualifier("dog") Animal animal){
this.animal = animal;
}

####生命週期
Bean的生命週期大致分爲:
Bean定義、Bean初始化、Bean的生存期和Bean的銷燬四個部分。
其中Bean定義過程大致如下:

  • Spring通過我們的配置,如通過@ComponentScan定義的路徑掃描找到帶有@Component的類,這是一個資源定位的過程
  • 一旦找到了資源,它就開始解析,並且將定義的信息保存起來
  • 然後將Bean定義發佈到SpringIoC容器中,此時,Ioc容器只有Bean的定義,還是沒有實例生成
    以上是Bean的發佈過程,當我們取出來時才做初始化和依賴注入等操作。

ComponentScan中還有一個配置項lazyInit,只可以配置Boolean值,且默認值爲false,也就是默認不進行延遲初始化,因此在默認情況下Spring會對Bean進行實例化和依賴注入對應的屬性值

下面這張圖說明了SpringBean的生命週期和可以實現的接口:
在這裏插入圖片描述
在這裏插入圖片描述
根據這張圖,我們還可以使用@PostConstruct標識自定義初始化方法
使用@PreDestroy標誌銷燬方法
還可以定義所有bean的後置處理器。

使用屬性文件

加入依賴:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

之後,就可以直接使用屬性文件application.properties爲你工作了

加入以下內容:

database.driverName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/chapter3
database.username=root
database.password=123456

我們可以通過@value註解,使用${……}這樣的佔位符讀取配置在屬性文件裏的內容,例如:

@value("${database.drivername}")
private String driverName = null;

爲了減少註解,我們可以(在類名稱之前)使用@ConfigurationProperties(“database”),database將與pojo的屬性名稱組成屬性的全限定名去配置文件中查找。

於此同時,我們可以注意到,如果只有一個配置文件,那麼這個文件就會過長而不便於更改。

我們可以使用@PropertySource去定義對應的屬性文件,把它加載到Spring的上下文中。

例如:
@PropertySource(value={“classpath:jdbc.properties”},ignoreResourceNotFound=true)

ignoreResourceNotFound的值爲true,意味着如果找不到這個配置文件的話就忽略這一條語句;爲false的話,找不到救會報錯。

條件裝配Bean

設想一種場景,如果在數據庫連接池的配置中漏掉一些配置,這時候,如果Ioc還在進行數據源的裝配,則系統將會拋出異常,所以這時候我們反而更希望IoC不去裝配數據源。
爲了處理這種場景,Spring提供了@Conditional註解幫助我們,它需要配合另一個接口Condition來完成對應的功能。

(這個地方我不太理解,並且代碼編譯沒有通過,請參考其它地方,十分抱歉)

Bean的作用域

我們根據BeanFactory(IoC容器頂級接口)的源碼,可以看到isSingleton和isPrototype兩個方法,
其中,isSingleton方法如果返回true,則bean在IoC中以單例存在;如果isPrototype方法返回true,則當我們每次獲取bean時,都會創建一個新的bean,這顯然存在很大不同,這便是SpringBean作用域的問題。
我們都知道javaweb四種作用域:page、request、session、application(不知道的百度),因爲page是針對於jsp頁面的作用域,所以spring無法支持。

Bean作用域
作用域類型 使用範圍 描述
singleton 所有Spring應用 默認值,IoC容器只存在單例
prototype 所有Spring應用 每當從IoC容器中取出一個Bean,則創建一個新的Bean
session Springweb應用 http會話
application Springweb應用 web工程生命週期
request Springweb應用 web工程單次請求
globalSession springweb應用 在一個全局的HttpSession中,一個bean對應一個實例

怎麼用呢?一個示例如下所示:

package springboot.chapter3.pojo;
/*imports*/
import org.springframework.beans.factory.config.ConfigurableBeanFactory;import org.springframework.stereotype.Component;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ScopeBean{
}

這裏的ConfigurableBeanFactory只能提供單例和原型兩種作用域,如果是SpringMVC環境中,還可以使用WebApplicationContext去定義其它作用域

使用@Profile

可以使用這個註解配置兩個bean,在沒有修改spring參數的情況下,profile機制將不會被啓動
啓用後,可以很方便在各個環境中切換

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