12-底層源碼分析 Spring 的核心功能和執行流程?(下)

上一課時我們講了 Bean 相關的內容,它其實也是屬於 IoC 的具體實現之一,本課時我們就來講講 Spring 中其他幾個高頻的面試點,希望能起到拋磚引玉的作用,能爲你理解 Spring 打開一扇門。因爲 Spring 涉及的內容和知識點太多了,用它來寫一本書也綽綽有餘,因此這裏我們只講核心的內容,希望下來你能查漏補缺,完善自己的 Spring 技術棧。

我們本課時的面試題是,談一談你對 IoC 和 DI 的理解。

典型回答

IoC(Inversion of Control,翻譯爲“控制反轉”)不是一個具體的技術,而是一種設計思想。與傳統控制流相比,IoC 會顛倒控制流,在傳統的編程中需要開發者自行創建並銷燬對象,而在 IoC 中會把這些操作交給框架來處理,這樣開發者就不用關注具體的實現細節了,拿來直接用就可以了,這就是控制反轉

IoC 很好的體現出了面向對象的設計法則之一——好萊塢法則:“別找我們,我們找你”。即由 IoC 容器幫對象找到相應的依賴對象並注入,而不是由對象主動去找。

舉個例子,比如說傳統找對象,先要設定好你的要求,如身高、體重、長相等,然後再一個一個的主動去找符合要求的對象,而 IoC 相當於,你把這些要求直接告訴婚介中心,由他們直接給你匹配到符合要求的對象,理想情況下是直接會幫你找到合適的對象,這就是傳統編程模式和 IoC 的區別。

DI(Dependency Injection,翻譯爲“依賴注入”)表示組件間的依賴關係交由容器在運行期自動生成,也就是說,由容器動態的將某個依賴關係注入到組件之中,這樣就能提升組件的重用頻率。通過依賴注入機制,我們只需要通過簡單的配置,就可指定目標需要的資源,完成自身的業務邏輯,而不需要關心資源來自哪裏、由誰實現等問題。

IoC 和 DI 其實是同一個概念從不同角度的描述的,由於控制反轉這個概念比較含糊(可能只理解成了容器控制對象這一個層面,很難讓人想到誰來維護對象關係),所以 2004 年被開發者尊稱爲“教父”的 Martin Fowler(世界頂級專家,敏捷開發方法的創始人之一)又給出了一個新的名字“依賴注入”,相對 IoC 而言,“依賴注入”明確描述了“被注入對象依賴 IoC 容器配置依賴對象”。

考點分析

IoC 和 DI 爲 Spring 框架設計的精髓所在,也是面試中必問的考點之一,這個優秀的設計思想對於初學者來說可能理解起來比較困難,但對於 Spring 的使用者來說可以很快的看懂。因此如果對於此概念還有疑問的話,建議先上手使用 Spring 實現幾個小功能再回頭來看這些概念,相信你會豁然開朗。

Spring 相關的高頻面試題,還有以下這些:

  • Spring IoC 有哪些優勢?
  • IoC 的注入方式有哪些?
  • 談一談你對 AOP 的理解。

知識擴展

1.Spring IoC 的優點

IoC 的優點有以下幾個:

  • 使用更方便,拿來即用,無需顯式的創建和銷燬的過程;
  • 可以很容易提供衆多服務,比如事務管理、消息服務等;
  • 提供了單例模式的支持;
  • 提供了 AOP 抽象,利用它很容易實現權限攔截、運行期監控等功能;
  • 更符合面向對象的設計法則;
  • 低侵入式設計,代碼的污染極低,降低了業務對象替換的複雜性。
    2.Spring IoC 注入方式彙總
    IoC 的注入方式有三種:構造方法注入、Setter 注入和接口注入。

構造方法注入

構造方法注入主要是依賴於構造方法去實現,構造方法可以是有參的也可以是無參的,我們平時 new 對象時就是通過類的構造方法來創建類對象的,每個類對象默認會有一個無參的構造方法,Spring 通過構造方法注入的代碼示例如下:

public class Person {
    public Person() {
	}
	public Person(int id, String name) {
		this.id = id;
		this.name = name;
	}
	private int id;
	private String name;
    // 忽略 Setter、Getter 的方法
}

applicationContext.xml 配置如下:

<bean id="person" class="org.springframework.beans.Person">
    <constructor-arg value="1" type="int"></constructor-arg>
    <constructor-arg value="Java" type="java.lang.String"></constructor-arg>
</bean>

Setter 注入

Setter 方法注入的方式是目前 Spring 主流的注入方式,它可以利用 Java Bean 規範所定義的 Setter/Getter 方法來完成注入,可讀性和靈活性都很高,它不需要使用聲明式構造方法,而是使用 Setter 注入直接設置相關的值,實現示例如下:

<bean id="person" class="org.springframework.beans.Person">
    <property name="id" value="1"/>
    <property name="name" value="Java"/>
</bean>

接口注入

接口注入方式是比較古老的注入方式,因爲它需要被依賴的對象實現不必要的接口,帶有侵入性,因此現在已經被完全捨棄了,所以本文也不打算做過多的描述,大家只要知道有這回事就行了。

3.Spring AOP

AOP(Aspect-OrientedProgramming,面向切面編程)可以說是 OOP(Object-Oriented Programing,面向對象編程)的補充和完善,OOP 引入封裝、繼承和多態性等概念來建立一種公共對象處理的能力,當我們需要處理公共行爲的時候,OOP 就會顯得無能爲力,而 AOP 的出現正好解決了這個問題。比如統一的日誌處理模塊、授權驗證模塊等都可以使用 AOP 很輕鬆的處理。

Spring AOP 目前提供了三種配置方式:

  • 基於 Java API 的方式;
  • 基於 @AspectJ(Java)註解的方式;
  • 基於 XML 標籤的方式。

基於 Java API 的方式

此配置方式需要實現相關的接口,例如 MethodBeforeAdvice 和 AfterReturningAdvice,並且在 XML 配置中定義相應的規則即可實現。

我們先來定義一個實體類,代碼如下:

package org.springframework.beans;


public class Person {
   public Person findPerson() {
      Person person = new Person(1, "JDK");
      System.out.println("findPerson 被執行");
      return person;
   }
   public Person() {
   }
   public Person(Integer id, String name) {
      this.id = id;
      this.name = name;
   }
   private Integer id;
   private String name;
   // 忽略 Getter、Setter 方法
}

再定義一個 advice 類,用於對攔截方法的調用之前和調用之後進行相關的業務處理,實現代碼如下:

import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

public class MyAdvice implements MethodBeforeAdvice, AfterReturningAdvice {
   @Override
   public void before(Method method, Object[] args, Object target) throws Throwable {
      System.out.println("準備執行方法: " + method.getName());
   }

   @Override
   public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
      System.out.println(method.getName() + " 方法執行結束");
   }

然後需要在 application.xml 文件中配置相應的攔截規則,配置如下:

<!-- 定義 advisor -->
<bean id="myAdvice" class="org.springframework.advice.MyAdvice"></bean>
<!-- 配置規則,攔截方法名稱爲 find* -->
<bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
    <property name="advice" ref="myAdvice"></property>
    <property name="pattern" value="org.springframework.beans.*.find.*"></property>
</bean>

<!-- 定義 DefaultAdvisorAutoProxyCreator 使所有的 advisor 配置自動生效 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"></bean>

從以上配置中可以看出,我們需要配置一個攔截方法的規則,然後定義一個 DefaultAdvisorAutoProxyCreator 讓所有的 advisor 配置自動生效。

最後,我們使用測試代碼來完成調用:

public class MyApplication {
   public static void main(String[] args) {
      ApplicationContext context =
            new ClassPathXmlApplicationContext("classpath*:application.xml");
      Person person = context.getBean("person", Person.class);
      person.findPerson();
   }
}

以上程序的執行結果爲:

準備執行方法: findPerson
findPerson 被執行
findPerson 方法執行結束

可以看出 AOP 的攔截已經成功了。

基於 @AspectJ 註解的方式

首先需要在項目中添加 aspectjweaver 的 jar 包,配置如下:

<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

此 jar 包來自於 AspectJ,因爲 Spring 使用了 AspectJ 提供的一些註解,因此需要添加此 jar 包。之後,我們需要開啓 @AspectJ 的註解,開啓方式有兩種。

可以在 application.xml 配置如下代碼中開啓 @AspectJ 的註解:

<aop:aspectj-autoproxy/>

也可以使用 @EnableAspectJAutoProxy 註解開啓,代碼如下:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

之後我們需要聲明攔截器的類和攔截方法,以及配置相應的攔截規則,代碼如下:

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class MyAspectJ {

   // 配置攔截類 Person
   @Pointcut("execution(* org.springframework.beans.Person.*(..))")
   public void pointCut() {
   }

   @Before("pointCut()")
   public void doBefore() {
      System.out.println("執行 doBefore 方法");
   }

   @After("pointCut()")
   public void doAfter() {
      System.out.println("執行 doAfter 方法");

然後我們只需要在 application.xml 配置中添加註解類,配置如下:

<bean class="org.springframework.advice.MyAspectJ"/>

緊接着,我們添加一個需要攔截的方法:

package org.springframework.beans;

// 需要攔截的 Bean
public class Person {
   public Person findPerson() {
      Person person = new Person(1, "JDK");
      System.out.println("執行 findPerson 方法");
      return person;
   }
    // 獲取其他方法
}

最後,我們開啓測試代碼:

import org.springframework.beans.Person;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyApplication {
   public static void main(String[] args) {
      ApplicationContext context =
            new ClassPathXmlApplicationContext("classpath*:application.xml");
      Person person = context.getBean("person", Person.class);
      person.findPerson();
   }
}

以上程序的執行結果爲:

執行 doBefore 方法
執行 findPerson 方法
執行 doAfter 方法

可以看出 AOP 攔截成功了。

基於 XML 標籤的方式

基於 XML 的方式與基於註解的方式類似,只是無需使用註解,把相關信息配置到 application.xml 中即可,配置如下:

<!-- 攔截處理類 -->
<bean id="myPointcut" class="org.springframework.advice.MyPointcut"></bean>
<aop:config>
    <!-- 攔截規則配置 -->
    <aop:pointcut id="pointcutConfig"
                    expression="execution(* org.springframework.beans.Person.*(..))"/>
    <!-- 攔截方法配置 -->
    <aop:aspect ref="myPointcut">
        <aop:before method="doBefore" pointcut-ref="pointcutConfig"/>
        <aop:after method="doAfter" pointcut-ref="pointcutConfig"/>
    </aop:aspect>
</aop:config>

之後,添加一個普通的類來進行攔截業務的處理,實現代碼如下:

public class MyPointcut {
   public void doBefore() {
      System.out.println("執行 doBefore 方法");
   }
   public void doAfter() {
      System.out.println("執行 doAfter 方法");
   }
}

攔截的方法和測試代碼與第二種註解的方式相同,這裏就不在贅述。

最後執行程序,執行結果爲:

執行 doBefore 方法
執行 findPerson 方法
執行 doAfter 方法

可以看出 AOP 攔截成功了。

Spring AOP 的原理其實很簡單,它其實就是一個動態代理,我們在調用 getBean() 方法的時候返回的其實是代理類的實例,而這個代理類在 Spring 中使用的是 JDK Proxy 或 CgLib 實現的,它的核心代碼在 DefaultAopProxyFactory#createAopProxy(…) 中,源碼如下:

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

	@Override
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
            // 判斷目標類是否爲接口
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                // 是接口使用 jdk 的代理
				return new JdkDynamicAopProxy(config);
			}
            // 其他情況使用 CgLib 代理
			return new ObjenesisCglibAopProxy(config);
		}
		else {
			return new JdkDynamicAopProxy(config);
		}
	}
    // 忽略其他代碼
}

小結

本課時講了 IoC 和 DI 概念,以及 IoC 的優勢和 IoC 注入的三種方式:構造方法注入、Setter 注入和接口注入,最後講了 Spring AOP 的概念與它的三種配置方式:基於 Java API 的方式、基於 Java 註解的方式和基於 XML 標籤的方式。

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