上一課時我們講了 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 <aop />` 標簽的方式。
* ① 基于 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 <aop />` 標簽的方式
基于 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 標簽的方式。
- 前言
- 開篇詞
- 開篇詞:大廠技術面試“潛規則”
- 模塊一:Java 基礎
- 第01講:String 的特點是什么?它有哪些重要的方法?
- 第02講:HashMap 底層實現原理是什么?JDK8 做了哪些優化?
- 第03講:線程的狀態有哪些?它是如何工作的?
- 第04講:詳解 ThreadPoolExecutor 的參數含義及源碼執行流程?
- 第05講:synchronized 和 ReentrantLock 的實現原理是什么?它們有什么區別?
- 第06講:談談你對鎖的理解?如何手動模擬一個死鎖?
- 第07講:深克隆和淺克隆有什么區別?它的實現方式有哪些?
- 第08講:動態代理是如何實現的?JDK Proxy 和 CGLib 有什么區別?
- 第09講:如何實現本地緩存和分布式緩存?
- 第10講:如何手寫一個消息隊列和延遲消息隊列?
- 模塊二:熱門框架
- 第11講:底層源碼分析 Spring 的核心功能和執行流程?(上)
- 第12講:底層源碼分析 Spring 的核心功能和執行流程?(下)
- 第13講:MyBatis 使用了哪些設計模式?在源碼中是如何體現的?
- 第14講:SpringBoot 有哪些優點?它和 Spring 有什么區別?
- 第15講:MQ 有什么作用?你都用過哪些 MQ 中間件?
- 模塊三:數據庫相關
- 第16講:MySQL 的運行機制是什么?它有哪些引擎?
- 第17講:MySQL 的優化方案有哪些?
- 第18講:關系型數據和文檔型數據庫有什么區別?
- 第19講:Redis 的過期策略和內存淘汰機制有什么區別?
- 第20講:Redis 怎樣實現的分布式鎖?
- 第21講:Redis 中如何實現的消息隊列?實現的方式有幾種?
- 第22講:Redis 是如何實現高可用的?
- 模塊四:Java 進階
- 第23講:說一下 JVM 的內存布局和運行原理?
- 第24講:垃圾回收算法有哪些?
- 第25講:你用過哪些垃圾回收器?它們有什么區別?
- 第26講:生產環境如何排除和優化 JVM?
- 第27講:單例的實現方式有幾種?它們有什么優缺點?
- 第28講:你知道哪些設計模式?分別對應的應用場景有哪些?
- 第29講:紅黑樹和平衡二叉樹有什么區別?
- 第30講:你知道哪些算法?講一下它的內部實現過程?
- 模塊五:加分項
- 第31講:如何保證接口的冪等性?常見的實現方案有哪些?
- 第32講:TCP 為什么需要三次握手?
- 第33講:Nginx 的負載均衡模式有哪些?它的實現原理是什么?
- 第34講:Docker 有什么優點?使用時需要注意什么問題?
- 彩蛋
- 彩蛋:如何提高面試成功率?