深入淺出SpringBoot(1)----全註解下的Spring IoC(1)

深入淺出SpringBoot 2.x

來源於深入淺出springboot書籍

序言

  • Spring最成功的的是其提出的理念,而不是技術本身

  • 它所依賴的兩個核心理念,一個是控制反轉(Inversion of Control, loC), 另一個是面向切面編
    程(Aspect Oriented Programming, AOP)。 IoC 容器是Spring的核心,可以說Spring是一種基於 loC容器編程的框架。因爲Spring Boot是基於註解的開發Spring loC
    loC是一種通過描述 來生成或者獲取對象的技術,而這個技術不是Spring甚至不是Java獨有的。對於Java初學者更多的時候所熟悉的是使用new關鍵字來創建對象,而在Spring中則不是,它是通過描述來創建對象。只是Spring Boot並不建議使用XML,而是通過註解的描述生成對象

  • 一個系統可以生成各種對象,並且這些對象都需要進行管理。還值得一提的是, 對象之間並不是孤立的,它們之間還可能存在依賴的關係。例如,一個班級是由多個老師和同學組成的,那麼班級就依賴於多個老師和同學了。爲此Spring還提供了依賴注入的功能,使得我們能夠通過描述來管理各個對象之間的關係。

  • 爲了描述上述的班級、同學和老師這3個對象關係,我們需要一個容器。 在Spring中把每一個需要管理的對象稱爲Spring Bean (簡稱Bean),而Spring管理這些Bean的容器,被我們稱爲SpringIoC容器(或者簡稱IoC容器)。IoC 容器需要具備兩個基本的功能:

    • 通過描述管理Bean, 包括髮布和獲取Bean;

    • 通過描述完成Bean之間的依賴關係。

1. IoC容器簡介

SpringIoC容器是一個管理Bean的容器,在spring的定義中,它要求所有的IoC容器都需要實現接口BeanFactory,它是一個頂級的容器接口。爲了增加對它的理解,我們首先閱讀其源碼,並且討論幾個重要的方法,接口源碼如下:

package org.springframework.beans.factory;

import org.springframework.beans.BeansException;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;

public interface BeanFactory {
    String FACTORY_BEAN_PREFIX = "&";
	// 多個 getBean 方法
    Object getBean(String name) throws BeansException;

    <T> T getBean(String name, Class<T> requiredType) throws BeansException;

    Object getBean(String name, Object... ages) throws BeansException;

    <T> T getBean(Class<T> requiredType) throws BeansException;

    <T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

    // 是否包含bean
    boolean containsBean(String name);
	// bean是否單例
    boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
	// bean是否原型
    boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
	//bean是否類型匹配
    boolean isTypeMatch(String name, ResolvableType typetoMatch) throws NoSuchBeanDefinitionException;

    boolean isTypeMatch(String name, Class<?> typetoMatch) throws NoSuchBeanDefinitionException;
	//獲取bean的類型
    @Nullable
    Class<?> getType(String name) throws NoSuchBeanDefinitionException;
	//獲取bean的別名
    String[] getAliases(String name);
}

這段代碼中加入了中文註釋,通過它們就可以理解這些方法的含義。這裏值得注意的是的幾個方法。首先我們看到了多個getBean方法,這也是IoC容器最重要的方法之,它的意義是從IoC容器中獲取Bean。而從多個getBean方法中可以看到有按類型(by type)獲取Bean的,名稱(by name)獲取Bean的,這就意味着在Spring IoC容器中,允許我們按類型或者名稱獲取bean,這對理解後面將講到的Spring的依賴注入(Dependency Injection, DI)是十分重要的。

isSingeteon 方法則判斷Bean是否在Sring loC中爲單例。這裏需要記住的是在Spring IoC容器中,默認的情況下,Bean 都是以單例存在的,也就是使用getBean方法返回的都是同一個對象,與isSingeteon 方法相反的是isPrototype方法,如果它返回的是true, 那麼當我們使用getBean獲取
Bean的時候,SpringIoC容器就會創建一個新的Bean返回給調用者,這些與後面將討論的Bean的作用域相關。
由於BeanFactory的功能還不夠強大,因此Spring在BeanFactory的基礎上,還設計了一個更爲高級的接口ApplicationContext.它是BeanFactory的子接口之一,在Spring的體系中BeanFactory和ApplicationContext是最爲重要的接口設計,在現實中我們使用的大部分Spring IoC 容器是ApplicationContext接口的實現類,它們的關係如圖所示
在這裏插入圖片描述
在圖中可以看到,ApplicationContext 接口通過繼承上級接口,進而繼承BeanFactory 接口,但是在BeanFactory的基礎上,擴展了消息國際化接口(MessageSource)、 環境可配置接口(EnvironmentCapable)、應用事件發佈接口(ApplicationEventPublisher) 和資源模式解析接口( ResourcePattermResolver),所以它的功能會更爲強大。

在SpringBoot當中我們主要是通過註解來裝配Bean到SpringIoC容器中,爲了貼近SpringBoot的需要,這裏不再介紹與XML相關的IoC容器,而主要介紹一個基於註解的IoC容器,它就是AnnotationConfigApplicationContext,從名稱就可以看出它是一個基於註解的 IoC容器。之所以研究它,是因爲Spring Boot裝配和獲取Bean的方法與它如出一轍。
下面來看一個最爲簡單的例子。首先定義一個Java簡單對象文件User.java,如代碼所示。

package com.springboot.chapter3.pojo

public class User{
	private Long id;
	private String userName;
	private String note
	
	/** setter and getter**/
}

然後再定義一個java配置文件AppConfig.java 如代碼所示:

package com.springboot.chapter3.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.springboot.chapter3.pojo.User;


@Configuration
public class AppConfig {
    
	@Bean(name ="user")
    public User initUser () {
    	User user= new User();
    	user.setId (1L) ;
    	user.setUserName ("user_name_1"):
        user.setNote ("note_1" ) ;
        return user;
    }
}

這裏需要注意@Configuration代表這是一個Java配置文件,Spring的容器會根據它來生成IoC容器去裝配Bean;@Bean代表將initUser方法返回的POJO裝配到IoC容器中,根據其屬性name定義這個Bean的名稱,如果沒有配置它,就將方法initUser作爲Bean的名稱保存到SpringIoc容器。
使用AnnotationConfigApplicationContext來構建IoC容器

package com.springboot.chapter3.config;

import org.apache.log4j.Logger ;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.springboot.chapter3.pojo.User;
public class IoCTest {	
    private static Logger log= Logger . getLogger(IoCTest.class);
    public static void main (String [] args) {
        ApplicationContext ctx= new AnnotationConfigApplcationContext(AppConfig.class);
        User user= ctx.getBean(User . class);
        log.info(user.getId ());
   }
}

代碼中將Java配置文件AppConfig傳遞給AnnotationConfigApplicationContext的構造方法,這樣就可以讀取配置了。然後將配置中的Bean裝配給IoC容器,便可以使用getBean方法獲取對應的POJO,可以看到日誌打印

14:53:03.017 [main] DEBUG org . springframework. core . env . PropertySourcesPropertyResolver
Could not find key ' spring. liveBeansView . mbeanDomain' in any property source
14:53:03.018 [main] DEBUG org. springframework . beans. factory. support . DefaultListableBe
anFactory一Returning cached instance of singleton bean 'user '......

顯然,配置在配置文件中的名稱爲user的Bean已經被裝配到IoC容器中,並且可以通過getBean方法獲取對應的Bean, 並將Bean 的屬性信息輸出出來。當然這只是很簡單的方法,而註解@Bean也不是唯創建 Bean 的方法,還有其他的方法可以讓loC容器裝配Bean, 而且Bean 之間還有依賴的關係需要進一步處理。

2.裝配你的Bean

在Spring中允許我們通過XML或者Java配置文件裝配Bean,但是由於Spring Boot 是基於註解的方式,因此下面主要基於註解的方式來介紹Spring的用法,以滿足Spring Boot開發者的需要。

通過掃描裝配你的Bean

如果一個個的Bean使用註解@Bean注入Spring IoC 容器中,那將是件很 麻煩的事情。 好在Spring還允許我們進行掃描裝配Bean到IoC容器中,對於掃描裝配而言使用的註解是@Component和@ComponentScan。@Component 是標明哪個類被掃描進入Spring IoC容器,而@ComponentScan則是標明採用何種策略去掃描裝配Bean。

package com.springboot.chapter3.config;

@Component("user")

public class User{
	@Value("1")
	private Long id;
	@Value("user_name_1")
	private String userName;
	@Value("note_1")
	private String note
	
	/** setter and getter**/
}

這裏的註解@Component表明這個類將被Spring loC容器掃描裝配,其中配置的“user”則是作爲Bean的名稱,當然你也可以不配置這個字符串,那麼IoC容器就會把類名第一個字母作爲小寫, 其他不變作爲Bean名稱放入到IoC容器中;註解@Value 則是指定具體的值,使得Spring IoC給予對應的屬性注入對應的值。爲了讓Spring IoC容器裝配這個類,需要改造類AppConfig 如以下代碼:

package com.springboot.chapter3.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import com.springboot.chapter3.pojo.User;


@Configuration
@ComponentScan
public class AppConfig {
}

這裏加入了@Componentsan,意味着它會進行掃描,但是它只會掃描類AppC onfig 所在包和其子包,之前把Userjava 移到包com. sringbootot capter.config就是這個原因。這樣就可之前使用@Bean標註的創建對象方法。然後進行測試,測試代碼如下;

ApplicationContext ctx
=new AnnotationConfigApplicationContext{AppConfig.class) ;
User user= ctx.getBean(User.class);
log.info(user.getId());

這樣就能夠運行了。然而爲了使得User類能夠被掃描,上面我們把它遷移到了本不該放置它的包,這樣顯然就不太合理了。爲了更加合理,@ComponentScan還允許我們自定義掃描的包 現在探討它的配置項。
探討@ComponentScan源碼

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.core.annotation.AliasFor;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
//在一個類中可重複定義
@Repeatable(ComponentScans.class)
public @interface ComponentScan {

    //定義掃描的包
    @AliasFor("basePackages")
    String[] value() default {};

    //定義掃描的包
    @AliasFor("value")
    String[] basePackages() default {};

    //定義掃描的類
    Class<?>[] basePackageClasses() default {};

    //Bean name生成器
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

    //作用域解析器
    Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

    //作用域代理模式
    ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

    //資源匹配模式
    String resourcePattern() default "**/*.class";

    //是否啓動默認的過濾器
    boolean useDefaultFilters() default true;

    //當滿足過濾條件時掃描
    ComponentScan.Filter[] includeFilters() default {};

    //當不滿足過濾條件時掃描
    ComponentScan.Filter[] excludeFilters() default {};

    //是否延遲初始化
    boolean lazyInit() default false;

    //自定義過濾器
    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    public @interface Filter {

        //過濾器類型,可以按註解類型或者正則式等過濾
        FilterType type() default FilterType.ANNOTATION;

        //定義過濾的類
        @AliasFor("classes")
        Class<?>[] value() default {};

        //定義過濾的類
        @AliasFor("value")
        Class<?>[] classes() default {};

        //匹配方式
        String[] pattern() default {};
    }
}

首先可以通過配置項basePackages定義掃描的包名,在沒有定義的情況下,它只會掃描當前包和其子包下的路徑:還可以通過basePackageClasses定義掃描的類;其中還有includeFilters 和excludeFilters, includeFilters 是定義滿足過濾器(Filter) 條件的Bean纔去掃描,excludeFilters 則是排除過濾器條件的Bean,它們都需要通過一個註解@Filter去定義,它有一個type類型,這裏可以定義爲註解或者正則式等類型。classes定義註解類,pattern 定義正則式類。
此時我們再把User類放到包com. psrngoot.thapter3.pojo中,這樣User和AppConfig就不再同包, 那麼我們把AppConfig中的註解修改爲:

@ComponentScan ("com. springboot. chapter3.*")

@ComponentScan (basePackages = { "com. springboot . chapter3.pojo"})

無論採用何種方式都能夠使得lIoC 容器去掃描User類,而包名可以採用正則式去匹配,但縣的某些Bean。比方說,現在我們有一個UsrServicee 類,爲了標註它爲服務類,將類標註@Service()該標準注入了@Component,所以在默認的情況下它會被Spring掃描裝配到IoC容器中),這裏再設我們採用了策略:

@ComponenntScan ("com. springboot . chapter3. *")
package com.springboot.chapter3.service;

import org.springframework.stereotype.Service;

import com.springboot chapter3.pojo.User;

@Service

public class UserService
public void printUser (User user){
System.out.println("編號:" +user .getId()) ;
System.out.println("用戶名稱:" + user.getUserName()) ;
System. out.println ("備註:" + user.getNote ()) ;

按以上的裝配策略,它將會被掃描到Spring IoC容器中。爲了不被裝配,需要把掃描的策改爲:

@ComponentScan (basePackages = "com. springboot.chapter3. *",
	excludeFilters = {@Filter (classes = {UserService.class}) } )

這樣,由於加入了excludeFilters 的配置,使標註了@Service的類將不被IoC容器掃描注入樣就可以把UserService類排除到Spring IoC容器中了。事實上,之前在Spring Boot上述實例中的註解@SpringBootApplication也注入了@ComponentScan,這裏不妨探索其源碼
SpringBootApplication 源碼

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
//自定義排除的掃描類
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {

    //通過類型排除自動配置類
    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    Class<?>[] exclude() default {};

    //通過名稱排除自動配置類
    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    String[] excludeName() default {};

    //定義掃描包
    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackages"
    )
    String[] scanBasePackages() default {};

    //定義被掃描的類
    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackageClasses"
    )
    Class<?>[] scanBasePackageClasses() default {};

    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

自定義第三方Bean

現實的Java的應用往往需要引入許多來自第三方的包,並且很有可能希望把第三方包的類對象也放入到Spring IoC容器中,這時@Bean註解就可以發揮作用了。
例如,要引入一個DBCP數據源,我們先在pom.xml上加入項目所需要DBCP包和數據庫MySQL驅動程序的依賴,如

<dependency>

	<groupId>org.apache.commons</groupId>

	<artifactId>commons-dbcp2</artifactId>

</dependency>

<dependency>

	<groupId>mysql</groupId>

	<artifactId>mysql-connector-java</artifactId>

</dependency>

這樣DBCP和數據庫驅動就被加入到了項目中,接着將使用它提供的機制來生成數據源。這時候,可放置到AppConfig.java中。

使用DBCP生成數據源@Bean (name = “dataSource”)

@Bean(name = "dataSource") 
public DataSource getDataSource () {
	Properties props= new Properties();

	rops.setProperty("driver","com.mysql.jdbc.Driver" );
	props.setProperty("url","jdbc:mysql://localhost:3306/chapter3");
	props.setProperty ("username","root");
	props.setProperty ("password","123456");
	DataSource dataSource = null 
	try {
		dataSource = BasicDataSourceFactory.createDataSource (props);
	} catch (Exception e) {
		e.printStackTrace();
	}
	return dataSource;
}

3.依賴注入

本章的開始講述了Sping IoC的兩個作用,上一節只討論」瞭如何將Bean裝配到IoC容器中於如何進行獲取,還有一個作用沒有談及,那就是BeanBan之間的依賴,在Spring IoC的概念中,稱爲依賴注入(Dependency Injection, DI)。
例如,人類(Person) 有時候利用一些動物(Animal)去完成一些事情,比方說狗(Dog)是用來看門的,貓(Cat) 是用來抓老鼠的,鸚鵡(Parrot)是用來迎客…是做一些事情就依賴於那些可愛的動物了
爲了更好地展現這個過程,首先來定義兩個接口,一個是人類(Person), 另外一個是(Animal)。人類是通過動物去提供一些特殊服務的

/*********人類接口******/
package com.springboot.chapter3.pojo.definition;
public interface Person{
	//使用動物服務
	public void service() ;
	//設置動物
	public void setAnimal (Animal animal) ;
}



/*********動物接口******/
package com.springboot.chapter3.pojo.definition;
	public interface Animal{
		public void use();
}

這樣我們就擁有了兩個接口。接下來我們需要兩個實現類,如代碼清單3-14所示。代碼清單3-14兩個實現類

/*******人類實現類*******/

packagecom.springboot.chapter3.pojo;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

import com.springboot.chapter3.pojo.definition.Animal;

import com.springboot.chapter3.pojo.definition.Person;


@Component

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 com.springboot.chapter3.pojo;

import org.springframework.stereotype .Component;

import com.springboot.chapter3.pojo.definition.Animal;
@Component
public class Dog implements Animal{
	@Override
	public void use () {
		System.out.println(" 狗[" + Dog. class.getSimpleName()+"]是開門的. ");
	}

	
}

	

這裏應注意加粗的註解@Autowired,這也是我們在Spring 中最常用的註解之一,十分重要,它會根據屬性的類型(by type)找到對應的Bean進行注入。這裏的Dog類是動物的一種, 所以SprinigIoC容器會把Dog的實例注入BussinesPerson中。這樣通過Sprig IoC容器獲取BussinesPerson實例的時候就能夠使用Dog實例來提供服務了,下面是測試的代碼。

ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class)
Person person = ctx.getBean(BussinesPerson.class)
person.service() ;

註解@Autowired

@Autowired是我們使用得最多的註解之一,因此在這裏需要進一步地探討它
它注入的機制最基本的一條是根據類型by type,我們回顧IoC容器的頂級接口BeanFactory,就可以知道IoC容器是通過getBean方法獲取對應Bean的,而getBean又支持根據類型by type或者根據名稱by name
autowired有4種模式,byname、bytype、constructor、autodectect
其中@Autowired註解是使用byType方式的
byType方式是根據屬性類型在容器中尋找bean類
這裏還要注意的是@Autowired是一個默認必須找到對應Bean的註解,如果不能確定其標註屬性一定會存在並且允許這個被標註的屬性爲null,那麼你可以配置@Autowired屬性required爲false,例如,像下面一樣:

@Autowired(required=false)

同樣,它除了可以標註屬性外,還可以標註方法,如setAnimal方法,如下所示:


@Override
@Autowired
public void setAnimal (Animal animal){
	this. animal = animal;
}

消除歧義性------@Primary和@Qualifier

在上面我們發現有貓有狗的時候,爲了使@Autowired能夠繼續使用,我們做了一個決定,將BusisessPeron的屬性名稱從animal修改爲dog。顯然這是一個憋屈的做法,好好的一個動物,卻被我們定,義爲了狗。產生注入失敗的問題根本是按類型(by type)查找,正如動物可以有多種類型,這樣會造成Spring IoC容器注入的困擾,我們把這樣的一個問題稱爲歧義性。知道這個原因後,那麼這兩個註解是從哪個角,度去解決這些問題的呢?這是本節要解決的問題。

首先是一個註解@Primary,它是一個修改優先權的註解,當我們有貓有狗的時候,假設這次需要使用貓,那麼只需要在貓類的定義上加入@Primary就可以了,類似下面這樣:

@Component
@Primary
public class Cat implements Animal{
.........
}

這裏的@Primary的含義告訴Spring IoC容器,當發現有多個同樣類型的Bean時候,請優先使用我進行注入,進行注入的時候,於是再進行測試時會發現,系統將用貓爲你提供服務。 因爲當Spring進行注入的時候,雖然它發現存在多個動物,但因爲Cat被標註爲了@Primary,所以優先採用Cat的實例進行注入,這樣就通過優先級的變換使得IoC容器知道注入哪個具體的實例來滿足依賴注入。
有時候@Primary也可以使用在多個類上,也許無論是貓還是狗都帶上@Primary注入解,其結果是IoC容器還是無法區分採用哪個Bean的實例進行注入那麼@Qualifier可以滿足你的這個願望。它的配置項value需要一個字符串去定義,它將與@Autowired組合在一起,通過類型和名稱一起找到Bean。我們知道Bean名稱在Spring IoC容器中是唯一標識,通過這個就能消除歧義性

<T> T getBean(String name, Class<T> requiredType) throws BeansException;

通過它就能 夠按照名稱和類型的結合找到對象了。
下面假設貓已經標註了@Primary,而我們需要的是狗提供服務, 因此需要修改BussinessPerson屬性animal的標註適合我們的需求。:

@Autowired
@Qualifier ("dog")
private Animal animal = null;

4.生命週期

請看下次博文

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