上一課時我們講了 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 標籤的方式。