精讀《Spring 概念》

黃子毅發表於2020-08-24

spring 是 Java 非常重要的框架,且蘊含了一系列設計模式,非常值得研究,本期就通過 Spring學習 這篇文章瞭解一下 spring。

spring 為何長壽

spring 作為一個後端框架,擁有 17 年曆史,這在前端看來是不可思議的。前端幾乎沒有一個框架可以流行超過 5 年,就最近來看,react、angular、vue 三大框架可能會活的久一點,他們都是前端相對成熟階段的產物,我們或多或少可以看出一些設計模式。然而這些前端框架與 spring 比起來還是差距很大,我們來看看 spring 到底強大在哪。

設計模式

設計模式是一種思想,不依附於任何程式語言與開發框架。比如你學會了工廠設計模式,可以在後端用,也可以轉到前端用,可以在 Go 語言用,也可以在 Typescript 用,可以在 React 框架用,也可以在 Vue 裡用,所以設計模式是一種具有遷移能力的知識,學會後可以受益整個職業生涯,而語言、框架則不具備遷移性,前端許多同學都把精力花在學習框架特性上,遇到前端技術迭代時期就尷尬了,這就是為什麼大公司面試要問框架原理,就是看看你能否抓住一些不變的東西,所以洋洋灑灑的說上下文相關的細節也不是面試官想要的,真正想聽到的是你抽象後對框架原理共性的總結。

spring 框架就用到了許多設計模式,包括:

工廠模式:用工廠生產物件例項來代替原始的 new。所謂工廠就是遮蔽例項話的細節,呼叫處無需關心例項化物件需要的環境引數,提升可維護性。spring 的 BeanFactory 建立 bean 物件就是工廠模式的體現。 代理模式:允許通過代理物件訪問目標物件。Spring 實現 AOP 就是通過動態代理模式。 單例模式:單例項。spring 的 bean 預設都是單例。 包裝器模式:將幾個不同方法通用部分抽象出來,呼叫時通過包裝器內部引導到不同的實現。比如 spring 連線多種資料庫就使用了包裝器模式簡化。 觀察者模式:這個前端同學很熟悉,就是事件機制,spring 中可以通過 ApplicationEvent 實踐觀察者模式。 介面卡模式:通過介面卡將介面轉換為另一個格式的介面。spring AOP 的增強和通知就使用了介面卡模式。 模板方法模式:父類先定義一些函式,這些函式之間存在呼叫關聯,將某些設定為抽象函式等待子類繼承時去重寫。spring 的 jdbcTemplatehibernateTemplate 等資料庫操作類使用了模版方法模式。

全家桶

spring 作為一個全面的 java 框架,提供了系列全家桶滿足各種場景需求:spring mvc、spring security、spring data、spring boot、spring cloud。

  • spring boot:簡化了 spring 應用配置,約定大於配置的思維。
  • spring data:是一個資料操作與訪問工具集,比如支援 jdbc、redis 等資料來源操作。
  • spring cloud:是一個微服務解決方案,基於 spring boot,整合了服務發現、配置管理、訊息匯流排、負載均衡、斷路器、資料監控等各種服務治理能力。
  • spring security:支援一些安全模型比如單點登入、令牌中繼、令牌交換等。
  • spring mvc:MVC 思想的 web 框架。

IOC

IOC(Inverse of Control)控制反轉。IOC 是 Spring 最核心部分,因為所有物件呼叫都離不開 IOC 模式。

假設我們有三個類:Country、Province、City,最大的類別是國家,其次是省、城市,國家類需要呼叫省類,省類需要呼叫城市類:

public class Country {  private Province province;  public Country(){    this.province = new Province()  }}public class Province {  private City city;  public Province(){    this.city = new City()  }}public class City {  public City(){    // ...  }}

假設來了一個需求,City 例項化時需增加人口(people)引數,我們就要改動所有類程式碼:

public class Country {  private Province province;  public Country(int people){    this.province = new Province(people)  }}public class Province {  private City city;  public Province(int people){    this.city = new City(people)  }}public class City {  public City(int people){    // ...  }}

那麼在真實業務場景中,一個底層類可能被數以千計的類使用,這麼改顯然難以維護。IOC 就是為了解決這個問題,它使得我們可以只改動 City 的程式碼,而不用改動其他類的程式碼:

public class Country {  private Province province;  public Country(Province province){    this.province = province  }}public class Province {  private City city;  public Province(City city){    this.city = city  }}public lass City {  public City(int people){    // ...  }}

可以看到,增加 people 屬性只需要改動 city 類。然而這樣做也是有成本的,就是類例項化步驟會稍微繁瑣一些:

City city = new City(1000);Province province = new Province(city);Country country = new Country(province);

這就是控制反轉,由 Country 依賴 Province 變成了類依賴框架(上面的例項化程式碼)注入。

然而手動維護這種初始化依賴是繁瑣的,spring 提供了 bean 容器自動做這件事,我們只需要利用裝飾器 Autowired 就可以自動注入依賴:

@Componentpublic class Country {      @Autowired      private Province province;}@Componentpublic class Province {      @Autowired    public City city;}@Componentpublic class City {  }

實際上這種自動分析並例項化的手段,不僅比手寫方便,還能解決迴圈依賴的問題。在實際場景中,兩個類相互呼叫是很常見的,假設現在有 A、B 類相互依賴:

@Componentpublic class A {      @Autowired      private B b;}@Componentpublic class B {      @Autowired    public A a;}

那麼假設我們想獲取 A 例項,會經歷這樣一個過程:

獲取 A 例項 -> 例項化不完整 A -> 檢測到注入 B -> 例項化不完整 B -> 檢測到注入 A -> 注入不完整 A -> 得到完整 B -> 得到完整 A -> 返回 A 例項

其實 spring 僅支援單例模式下非構造器的迴圈依賴,這是因為其內部有一套機制,讓 bean 在初始化階段先提前持有對方引用地址,這樣就可以同時例項化兩個物件了。

除了方便之外,IOC 配合 spring 容器概念還可以使獲取例項時不用關心一個類例項化需要哪些引數,只需要直接申明獲取即可,這樣在類的數量特別多,尤其是大量程式碼不是你寫的情況下,不需要閱讀類原始碼也可以輕鬆獲取例項,實在是大大提升了可維護性。

說到這就提到了 Bean 容器,在 spring 概念中,Bean 容器是對 class 的加強,如果說 Class 定義了類的基本含義,那 Bean 就是對類進行使用擴充,告訴我們應該如何例項化與使用這個類。

舉個例子,比如利用註解描述的這段 Bean 類:

@Configurationpublic class CityConfig {  @Scope("prototype")  @Lazy  @Bean(initMethod = "init", destroyMethod = "destroy")  public City city() {    return new City()  }}

可以看到,額外描述了是否延遲載入,是否單例,初始化與解構函式分別是什麼等等。

下面給出一個從 Bean 獲取例項的例子,採用比較古老的 xml 配置方式:

public interface City {  Int getPeople();}
public class CityImpl implements City {  public Int getPeople() {    return 1000;  }}

接下來用 xml 描述這個 bean:

<?xml version="1.0" encoding="UTF-8" ?><beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xmlns="http://www.springframework.org/schema/beans"  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" default-autowire="byName">  <bean id="city" class="xxx.CityImpl"/></beans>

bean 支援的屬性還有很多,由於本文並不做入門教學,就不一一列舉了,總之 id 是一個可選的唯一標誌,接下來我們可以通過 id 訪問到 city 的例項。

public class App {  public static void main(String[] args) {    ApplicationContext context = new ClassPathXmlApplicationContext("classpath:application.xml");    // 從 context 中讀取 Bean,而不 new City()    City city = context.getBean(City.class);    System.out.println(city.getPeople());  }}

可以看到,程式任何地方使用 city 例項,只需要呼叫 getBean 函式,就像一個工廠把例項化過程給承包了,我們不需要關心 City 建構函式要傳遞什麼引數,不需要關心它依賴哪些其他的類,只要這一句話就可以拿到例項,是不是在維護專案時省心了很多。

AOP

AOP(Aspect Oriented Program)面向切面程式設計。

AOP 是為了解決主要業務邏輯與次要業務邏輯之間耦合問題的。主要業務邏輯比如登陸、資料獲取、查詢等,次要業務邏輯比如效能監控、異常處理等等,次要業務邏輯往往有:不重要、和業務關聯度低、貫穿多處業務邏輯的特性,如果沒有好的設計模式,只能在業務程式碼裡將主要邏輯與次要邏輯混合起來,但 AOP 可以做到主要、次要業務邏輯隔離。

使用 AOP 就是在定義在哪些地方(類、方法)切入,在什麼地方切入(方法前、後、前後)以及做什麼。

比如說,我們想在某個方法前後分別執行兩個函式計算執行時間,下面是主要業務邏輯:

@Component("work")public class Work {  public void do() {    System.out.println("執行業務邏輯");  }}

再定義切面方法:

@Component@Aspectclass Broker {  @Before("execution(* xxx.Work.do())")  public void before(){    // 記錄開始時間  }  @After("execution(* xxx.Work.do())")  public void after(){    // 計算時間  }}

再通過 xml 定義掃描下這兩個 Bean,就可以在執行 work.do() 之前執行 before(),之後執行 after()

還可以完全覆蓋原函式,利用 joinPoint.proceed() 可以執行原函式:

@Component@Aspectclass Broker {  @Around("execution(* xxx.Work.do())")  public void around(ProceedingJoinPoint joinPoint) {    // 記錄開始時間    try {      joinPoint.proceed();    } catch (Throwable throwable) {      throwable.printStackTrace();    }    // 計算時間  }}

關於表示式 "execution(* xxx.Work.do())" 是用正則的方式匹配,* 表示任意返回型別的方法,後面就不用解釋了。

可以看到,我們可以在不修改原方法的基礎上,在其執行前後增加自定義業務邏輯,或者監控其報錯,非常適合做次要業務邏輯,且由於不與主要業務邏輯程式碼耦合,保證了程式碼的簡潔,且次要業務邏輯不容易遺漏。

總結

IOC 特別適合描述業務模型,後端天然需要這一套,然而隨著前端越做越重,如果某個業務場景下需要將部分業務邏輯放到前端,也是非常推薦使用 IOC 設計模式來做,這是後端沉澱了近 20 年的經驗,沒有必要再另闢蹊徑。

AOP 對前端有幫助但沒有那麼大,因為前端業務邏輯較為分散,如果要進行切面程式設計,往往用 window 事件監聽來做會更徹底,可能這都是前端沒有流行 AOP 的原因。當然前端約定大於配置的趨勢下,比如打點或監控都整合到框架內部,往往也做到了業務程式碼無感,剩下的業務程式碼也就沒有 AOP 的需求。

最後,spring 的低侵入式設計,使得業務程式碼不用關心框架,讓業務程式碼能夠快速在不同框架間切換,這不僅方便了業務開發者,更使得 spring 走向成功,這是前端還需要追趕的。

討論地址是:精讀《Spring 概念》· Issue #265 · dt-fe/weekly

如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公眾號

版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章