深入淺出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.生命週期
請看下次博文