官术网_书友最值得收藏!

1.1 Spring容器

本節(jié)討論Spring容器,并給出容器所具備的非常重要的兩個(gè)功能特性,即依賴注入和面向切面編程。

1.1.1 IoC

在介紹Spring容器之前,我們先來介紹一個(gè)概念,即控制反轉(zhuǎn)(Inversion of Control,IoC)。試想,如果想有效管理一個(gè)對(duì)象,就需要知道創(chuàng)建、使用以及銷毀這個(gè)對(duì)象的方法。這個(gè)過程顯然是繁雜而重復(fù)的。而通過控制反轉(zhuǎn),就可以把這部分工作交給一個(gè)容器,由容器負(fù)責(zé)控制對(duì)象的生命周期和對(duì)象之間的關(guān)聯(lián)關(guān)系。這樣,與一個(gè)對(duì)象控制其他對(duì)象的處理方式相比,現(xiàn)在所有對(duì)象都被容器控制,控制的方向做了一次反轉(zhuǎn),這就是“控制反轉(zhuǎn)”這一名稱的由來。而Spring扮演的角色就是這里的容器。

可以看到控制反轉(zhuǎn)的重點(diǎn)是在系統(tǒng)運(yùn)行中,按照某個(gè)對(duì)象的需要,動(dòng)態(tài)提供它所依賴的其他對(duì)象,而這一點(diǎn)可以通過依賴注入(Dependency Injection,DI)實(shí)現(xiàn)。Spring會(huì)在適當(dāng)?shù)臅r(shí)候創(chuàng)建一個(gè)Bean,然后像使用注射器一樣把它注入目標(biāo)對(duì)象中,這樣就完成了對(duì)各個(gè)對(duì)象之間關(guān)系的控制。

可以說,依賴注入是開發(fā)人員使用Spring框架的基本手段,我們可以通過依賴注入獲取所需的各種Bean。Spring為開發(fā)人員提供了3種不同的依賴注入方式,分別是字段注入、構(gòu)造器注入和Setter方法注入。

現(xiàn)在,假設(shè)我們有如下所示的HealthRecordService接口以及它的實(shí)現(xiàn)類:

public interface HealthRecordService {
 
     public void recordUserHealthData();
}
public class HealthRecordServiceImpl implements HealthRecordService {
     @Override
     public void recordUserHealthData () {
          System.out.println("HealthRecordService has been called.");
     }
}

下面我們來討論具體如何在Spring中完成對(duì)HealthRecordServiceImpl實(shí)現(xiàn)類的注入,并分析各種注入類型的優(yōu)缺點(diǎn)。

1.依賴注入的3種方式

首先,我們來看看字段注入,即在一個(gè)類中通過字段的方式注入某個(gè)對(duì)象,如下所示:

public class ClientService {
     @Autowired
     private HealthRecordService healthRecordService;
     public void recordUserHealthData() {
          healthRecordService.recordUserHealthData();
     }
}

可以看到,通過@Autowired注解,字段注入的實(shí)現(xiàn)方式非常簡(jiǎn)單而直接,代碼的可讀性也很高。事實(shí)上,字段注入是3種依賴注入方式中最常用、最容易使用的一種。但是,它也是3種注入方式中最應(yīng)該避免使用的一種。如果使用過IDEA,你可能遇到過這個(gè)提示—Field injection is not recommended,告訴你不建議使用字段注入。字段注入的最大問題是對(duì)象在外部是不可見的。正如在上面的ClientService類中,我們定義了一個(gè)私有變量HealthRecordService來注入該接口的實(shí)例。顯然,這個(gè)實(shí)例只能在ClientService類中被訪問,脫離了容器環(huán)境就無法訪問這個(gè)實(shí)例。

基于以上分析,Spring官方推薦的注入方式實(shí)際上是構(gòu)造器注入。這種注入方式也很簡(jiǎn)單,就是通過類的構(gòu)造函數(shù)來完成對(duì)象的注入,如下所示:

public class ClientService {
     private HealthRecordService healthRecordService;
     @Autowired
     public ClientService(HealthRecordService healthRecordService) {
          this.healthRecordService = healthRecordService;
     }
     public void recordUserHealthData() {
          healthRecordService.recordUserHealthData();
     }
}

可以看到構(gòu)造器注入能解決對(duì)象外部可見性的問題,因?yàn)镠ealthRecordService是通過ClientService構(gòu)造函數(shù)進(jìn)行注入的,所以勢(shì)必可以脫離ClientService而獨(dú)立存在。構(gòu)造器注入的顯著問題就是當(dāng)構(gòu)造函數(shù)中存在較多依賴對(duì)象時(shí),大量的構(gòu)造器參數(shù)會(huì)讓代碼顯得比較冗長(zhǎng)。這時(shí)就可以使用Setter方法注入。我們同樣先來看一下Setter方法注入的實(shí)現(xiàn)代碼,如下所示:

public class ClientService {
     private HealthRecordService healthRecordService;
     @Autowired
     public void setHealthRecordService(HealthRecordService healthRecordService) {
          this.healthRecordService = healthRecordService;
     }
     public void recordUserHealthData() {
          healthRecordService.recordUserHealthData();
     }
}

Setter方法注入和構(gòu)造器注入看上去有些類似,但Setter方法比構(gòu)造函數(shù)更具可讀性,因?yàn)槲覀兛梢园讯鄠€(gè)依賴對(duì)象分別通過Setter方法逐一進(jìn)行注入。而且,Setter方法注入對(duì)于非強(qiáng)制依賴注入很有用,我們可以有選擇地注入一部分想要注入的依賴對(duì)象。換句話說,可以實(shí)現(xiàn)按需注入,幫助開發(fā)人員只在需要時(shí)注入依賴關(guān)系。

作為總結(jié),我們用一句話來概括Spring中所提供的3種依賴注入方式:構(gòu)造器注入適用于強(qiáng)制對(duì)象注入;Setter方法注入適用于可選對(duì)象注入;而字段注入是應(yīng)該避免的,因?yàn)閷?duì)象無法脫離容器而獨(dú)立運(yùn)行。

2.Bean的作用域

所謂Bean的作用域,描述了Bean在Spring容器上下文中的生命周期和可見性。在這里,我們將討論Spring框架中不同類型的Bean的作用域以及使用上的指導(dǎo)規(guī)則。

如果想要通過注解來設(shè)置Bean的作用域,可以使用如下所示的代碼:

@Configuration
public class AppConfig {
     @Bean
     @Scope("singleton")
     public HealthRecordService createHealthRecordService() {
          return new HealthRecordServiceImpl();
     }
}

可以看到這里使用了一個(gè)@Scope注解來指定Bean的作用域?yàn)閱卫摹皊ingleton”。在Spring中,除了單例作用域之外,還有一個(gè)“prototype”,即原型作用域,也可以稱為多例作用域來與單例作用域進(jìn)行區(qū)別。在使用方式上,我們同樣可以使用如下所示的枚舉值來對(duì)它們進(jìn)行設(shè)置:

@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)

在Spring IoC容器中,Bean的默認(rèn)作用域是單例作用域,也就是說不管對(duì)Bean的引用有多少個(gè),容器只會(huì)創(chuàng)建一個(gè)實(shí)例。而原型作用域則不同,每次請(qǐng)求Bean時(shí),Spring IoC容器都會(huì)創(chuàng)建一個(gè)新的對(duì)象實(shí)例。

從兩種作用域的效果而言,我們總結(jié)一條開發(fā)上的結(jié)論,即對(duì)于無狀態(tài)的Bean,我們應(yīng)該使用單例作用域,反之則應(yīng)該使用原型作用域。

那么,什么樣的Bean屬于有狀態(tài)的呢?結(jié)合Web應(yīng)用程序,我們可以明確,對(duì)每次HTTP請(qǐng)求而言,都應(yīng)該創(chuàng)建一個(gè)Bean來代表這一次的請(qǐng)求對(duì)象。同樣,對(duì)會(huì)話而言,我們也需要針對(duì)每個(gè)會(huì)話創(chuàng)建一個(gè)會(huì)話狀態(tài)對(duì)象。這些都是常見的有狀態(tài)的Bean。為了更好地管理這些Bean的生命周期,Spring還專門針對(duì)Web開發(fā)場(chǎng)景提供了對(duì)應(yīng)的“request”和“session”作用域。

1.1.2 AOP

在本小節(jié)中,我們將討論Spring容器的另一項(xiàng)核心功能,即面向切面編程(Aspect Oriented Programming,AOP)。我們將介紹AOP的概念以及實(shí)現(xiàn)這些概念的方法。

所謂切面,本質(zhì)上解決的是關(guān)注點(diǎn)分離的問題。在面向?qū)ο缶幊痰氖澜缰校覀儼岩粋€(gè)應(yīng)用程序按照職責(zé)和定位拆分成多個(gè)對(duì)象,這些對(duì)象構(gòu)成了不同的層次。而AOP可以說是面向?qū)ο缶幊痰囊环N補(bǔ)充,目標(biāo)是將一個(gè)應(yīng)用程序抽象成各個(gè)切面。

舉個(gè)例子,假設(shè)一個(gè)Web應(yīng)用中存在ServiceA、ServiceB和ServiceC這3個(gè)服務(wù),而每個(gè)服務(wù)都需要考慮安全校驗(yàn)、日志記錄、事務(wù)處理等非功能性需求。這時(shí),就可以引入AOP的思想把這些非功能性需求從業(yè)務(wù)需求中拆分出來,構(gòu)成獨(dú)立的關(guān)注點(diǎn),如圖1-1所示。

圖1-1 AOP的思想示意

從圖1-1可以很形象地看出,所謂切面相當(dāng)于應(yīng)用對(duì)象間的橫切面,我們可以將其抽象為單獨(dú)的模塊進(jìn)行開發(fā)和維護(hù)。

為了理解AOP的具體實(shí)現(xiàn)過程,我們需要引入一組特定的術(shù)語,具體如下。

連接點(diǎn)(Join Point):連接點(diǎn)表示應(yīng)用執(zhí)行過程中能夠插入切面的一個(gè)點(diǎn)。這種連接點(diǎn)可以是方法調(diào)用、異常處理、類初始化或?qū)ο髮?shí)例化。在Spring框架中,連接點(diǎn)只支持方法的調(diào)用。

通知(Advice):通知描述了切面何時(shí)執(zhí)行以及如何執(zhí)行對(duì)應(yīng)的業(yè)務(wù)邏輯。通知有很多種類型,在Spring中提供了一組注解用來表示通知,包括@Before、@After、@Around、@AfterThrowing和@AfterReturning等。我們會(huì)在后續(xù)代碼示例中看到這些注解的使用方法。

切點(diǎn)(Point Cut):切點(diǎn)是連接點(diǎn)的集合,用于定義必須執(zhí)行的通知。通知不一定應(yīng)用于所有連接點(diǎn),因此切點(diǎn)提供了在應(yīng)用程序中的組件上執(zhí)行通知的細(xì)粒度控制。在Spring中,可以通過表達(dá)式來定義切點(diǎn)。

切面(Aspect):切面是通知和切點(diǎn)的組合,用于定義應(yīng)用程序中的業(yè)務(wù)邏輯及其應(yīng)執(zhí)行的位置。Spring提供了@Aspect注解來定義切面。

現(xiàn)在,假設(shè)有這樣一個(gè)代表轉(zhuǎn)賬操作的TransferService接口:

public interface TransferService {
 
     boolean transfer(Account source, Account dest, int amount) throws MinimumAmountException;
}

然后我們提供它的實(shí)現(xiàn)類:

package com.demo;
 
public class TransferServiceImpl implements TransferService {
 
     private static final Logger LOGGER = Logger.getLogger(TransferServiceImpl.class);
 
     @Override
     public boolean transfer(Account source, Account dest, int amount) throws MinimumAmountException {
          LOGGER.info("Tranfering " + amount + " from " + source.getAccountName() + " to " + dest.getAccountName());
 
          if (amount < 10) {
               throw new MinimumAmountException("轉(zhuǎn)賬金額必須大于10");
          }
          return true;
     }
}

針對(duì)轉(zhuǎn)賬操作,我們希望在該操作之前、之后以及執(zhí)行過程進(jìn)行切入,并添加對(duì)應(yīng)的日志記錄,那么可以實(shí)現(xiàn)如下所示的TransferServiceAspect類:

@Aspect
public class TransferServiceAspect {
 
     private static final Logger LOGGER = Logger.getLogger(TransferServiceAspect.class);
 
     @Pointcut("execution(* com.demo.TransferService.transfer(..))")
     public void transfer() {}
 
     @Before("transfer()")
     public void beforeTransfer(JoinPoint joinPoint) {
          LOGGER.info("在轉(zhuǎn)賬之前執(zhí)行");
     }
 
     @After("transfer()")
     public void afterTransfer(JoinPoint joinPoint) {
          LOGGER.info("在轉(zhuǎn)賬之后執(zhí)行");
     }
 
     @AfterReturning(pointcut = "transfer() and args(source, dest, amount)", returning = "isTransferSucessful")
     public void afterTransferReturns(JoinPoint joinPoint, Account source, Account dest, Double amount, boolean isTransferSucessful) {
          if (isTransferSucessful) {
               LOGGER.info("轉(zhuǎn)賬成功了");
          }
     }
     @AfterThrowing(pointcut = "transfer()", throwing = "minimumAmountException")
     public void exceptionFromTransfer(JoinPoint joinPoint, MinimumAmountException minimumAmountException) {
          LOGGER.info("轉(zhuǎn)賬失敗了:" + minimumAmountException.getMessage());
     }
     @Around("transfer()")
     public boolean aroundTransfer(ProceedingJoinPoint proceedingJoinPoint){
          LOGGER.info("方法執(zhí)行之前調(diào)用");
          boolean isTransferSuccessful = false;
          try {
               isTransferSuccessful = (Boolean)proceedingJoinPoint.proceed();
          } catch (Throwable e) {
               LOGGER.error(e.getMessage(), e);
          }
          LOGGER.info("方法執(zhí)行之后調(diào)用");
          return isTransferSuccessful;
     }
}

上述代碼代表了Spring AOP機(jī)制的典型使用方法。使用@Pointcut注解定義了一個(gè)切入點(diǎn),并通過“execution”指示器限定該切入點(diǎn)匹配的包結(jié)構(gòu)為“com.demo”,匹配的方法是TransferService類的transfer()方法。

請(qǐng)注意,在TransferServiceAspect中綜合使用了@Before、@After、@Around、@AfterThrowing和@AfterReturning注解用來設(shè)置5種不同類型的通知。其中@Around注解會(huì)將目標(biāo)方法封裝起來,并執(zhí)行動(dòng)態(tài)添加返回值、異常信息等操作。這樣@AfterThrowing和@AfterReturning注解就能獲取這些返回值或異常信息并做出響應(yīng),而@Before和@After注解可以在方法調(diào)用的前后分別添加自定義的處理邏輯。

主站蜘蛛池模板: 栖霞市| 荥阳市| 山东省| 上蔡县| 大连市| 玉田县| 成都市| 滕州市| 齐河县| 郯城县| 资源县| 长寿区| 周至县| 延长县| 大方县| 石门县| 随州市| 宁远县| 新安县| 诏安县| 邢台县| 景洪市| 东莞市| 泰兴市| 安康市| 雷波县| 庆云县| 宜兰县| 加查县| 广东省| 长春市| 麻江县| 家居| 鹤山市| 开远市| 安新县| 太湖县| 长武县| 富民县| 航空| 正镶白旗|