- Spring核心技術(shù)和案例實(shí)戰(zhàn)
- 鄭天民
- 3031字
- 2023-06-21 18:56:28
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)用的前后分別添加自定義的處理邏輯。
- 編程卓越之道(卷3):軟件工程化
- Vue.js 3.x從入門到精通(視頻教學(xué)版)
- Learning SAP Analytics Cloud
- INSTANT Weka How-to
- 零基礎(chǔ)學(xué)Java(第4版)
- 游戲程序設(shè)計(jì)教程
- Practical Game Design
- Bootstrap 4 Cookbook
- PHP從入門到精通(第4版)(軟件開發(fā)視頻大講堂)
- C#開發(fā)案例精粹
- Cocos2d-x Game Development Blueprints
- Spring技術(shù)內(nèi)幕:深入解析Spring架構(gòu)與設(shè)計(jì)原理(第2版)
- Python自然語言理解:自然語言理解系統(tǒng)開發(fā)與應(yīng)用實(shí)戰(zhàn)
- 算法圖解
- Appcelerator Titanium:Patterns and Best Practices