Spring 常見面試題總結 | JavaGuide

首發於 JavaGuide 在線網站Spring 常見面試題總結

最近在對 JavaGuide 的內容進行重構完善,同步一下最新更新,希望能夠幫助你。

Spring 基礎

什麼是 Spring 框架?

Spring 是一款開源的輕量級 Java 開發框架,旨在提高開發人員的開發效率以及系統的可維護性。

我們一般說 Spring 框架指的都是 Spring Framework,它是很多模組的集合,使用這些模組可以很方便地協助我們進行開發,比如說 Spring 支援 IoC(Inverse of Control:控制反轉) 和 AOP(Aspect-Oriented Programming:面向切面編程)、可以很方便地對資料庫進行訪問、可以很方便地集成第三方組件(電子郵件,任務,調度,快取等等)、對單元測試支援比較好、支援 RESTful Java 應用程式的開發。

Spring 最核心的思想就是不重新造輪子,開箱即用,提高開發效率。

Spring 翻譯過來就是春天的意思,可見其目標和使命就是為 Java 程式設計師帶來春天啊!感動!

🤐 多提一嘴 : 語言的流行通常需要一個殺手級的應用,Spring 就是 Java 生態的一個殺手級的應用框架。

Spring 提供的核心功能主要是 IoC 和 AOP。學習 Spring ,一定要把 IoC 和 AOP 的核心思想搞懂!

Spring 包含的模組有哪些?

Spring4.x 版本

Spring4.x主要模組

Spring5.x 版本

Spring5.x主要模組

Spring5.x 版本中 Web 模組的 Portlet 組件已經被廢棄掉,同時增加了用於非同步響應式處理的 WebFlux 組件。

Spring 各個模組的依賴關係如下:

Spring 各個模組的依賴關係

Core Container

Spring 框架的核心模組,也可以說是基礎模組,主要提供 IoC 依賴注入功能的支援。Spring 其他所有的功能基本都需要依賴於該模組,我們從上面那張 Spring 各個模組的依賴關係圖就可以看出來。

  • spring-core :Spring 框架基本的核心工具類。
  • spring-beans :提供對 bean 的創建、配置和管理等功能的支援。
  • spring-context :提供對國際化、事件傳播、資源載入等功能的支援。
  • spring-expression :提供對表達式語言(Spring Expression Language) SpEL 的支援,只依賴於 core 模組,不依賴於其他模組,可以單獨使用。

AOP

  • spring-aspects :該模組為與 AspectJ 的集成提供支援。
  • spring-aop :提供了面向切面的編程實現。
  • spring-instrument :提供了為 JVM 添加代理(agent)的功能。 具體來講,它為 Tomcat 提供了一個織入代理,能夠為 Tomcat 傳遞類文 件,就像這些文件是被類載入器載入的一樣。沒有理解也沒關係,這個模組的使用場景非常有限。

Data Access/Integration

  • spring-jdbc :提供了對資料庫訪問的抽象 JDBC。不同的資料庫都有自己獨立的 API 用於操作資料庫,而 Java 程式只需要和 JDBC API 交互,這樣就屏蔽了資料庫的影響。
  • spring-tx :提供對事務的支援。
  • spring-orm : 提供對 Hibernate、JPA 、iBatis 等 ORM 框架的支援。
  • spring-oxm :提供一個抽象層支撐 OXM(Object-to-XML-Mapping),例如:JAXB、Castor、XMLBeans、JiBX 和 XStream 等。
  • spring-jms : 消息服務。自 Spring Framework 4.1 以後,它還提供了對 spring-messaging 模組的繼承。

Spring Web

  • spring-web :對 Web 功能的實現提供一些最基礎的支援。
  • spring-webmvc : 提供對 Spring MVC 的實現。
  • spring-websocket : 提供了對 WebSocket 的支援,WebSocket 可以讓客戶端和服務端進行雙向通訊。
  • spring-webflux :提供對 WebFlux 的支援。WebFlux 是 Spring Framework 5.0 中引入的新的響應式框架。與 Spring MVC 不同,它不需要 Servlet API,是完全非同步。

Messaging

spring-messaging 是從 Spring4.0 開始新加入的一個模組,主要職責是為 Spring 框架集成一些基礎的報文傳送應用。

Spring Test

Spring 團隊提倡測試驅動開發(TDD)。有了控制反轉 (IoC)的幫助,單元測試和集成測試變得更簡單。

Spring 的測試模組對 JUnit(單元測試框架)、TestNG(類似 JUnit)、Mockito(主要用來 Mock 對象)、PowerMock(解決 Mockito 的問題比如無法模擬 final, static, private 方法)等等常用的測試框架支援的都比較好。

Spring,Spring MVC,Spring Boot 之間什麼關係?

很多人對 Spring,Spring MVC,Spring Boot 這三者傻傻分不清楚!這裡簡單介紹一下這三者,其實很簡單,沒有什麼高深的東西。

Spring 包含了多個功能模組(上面剛剛提高過),其中最重要的是 Spring-Core(主要提供 IoC 依賴注入功能的支援) 模組, Spring 中的其他模組(比如 Spring MVC)的功能實現基本都需要依賴於該模組。

下圖對應的是 Spring4.x 版本。目前最新的 5.x 版本中 Web 模組的 Portlet 組件已經被廢棄掉,同時增加了用於非同步響應式處理的 WebFlux 組件。

Spring主要模組

Spring MVC 是 Spring 中的一個很重要的模組,主要賦予 Spring 快速構建 MVC 架構的 Web 程式的能力。MVC 是模型(Model)、視圖(View)、控制器(Controller)的簡寫,其核心思想是通過將業務邏輯、數據、顯示分離來組織程式碼。

使用 Spring 進行開發各種配置過於麻煩比如開啟某些 Spring 特性時,需要用 XML 或 Java 進行顯式配置。於是,Spring Boot 誕生了!

Spring 旨在簡化 J2EE 企業應用程式開發。Spring Boot 旨在簡化 Spring 開發(減少配置文件,開箱即用!)。

Spring Boot 只是簡化了配置,如果你需要構建 MVC 架構的 Web 程式,你還是需要使用 Spring MVC 作為 MVC 框架,只是說 Spring Boot 幫你簡化了 Spring MVC 的很多配置,真正做到開箱即用!

Spring IoC

談談自己對於 Spring IoC 的了解

IoC(Inverse of Control:控制反轉) 是一種設計思想,而不是一個具體的技術實現。IoC 的思想就是將原本在程式中手動創建對象的控制權,交由 Spring 框架來管理。不過, IoC 並非 Spring 特有,在其他語言中也有應用。

為什麼叫控制反轉?

  • 控制 :指的是對象創建(實例化、管理)的權力
  • 反轉 :控制權交給外部環境(Spring 框架、IoC 容器)

將對象之間的相互依賴關係交給 IoC 容器來管理,並由 IoC 容器完成對象的注入。這樣可以很大程度上簡化應用的開發,把應用從複雜的依賴關係中解放出來。 IoC 容器就像是一個工廠一樣,當我們需要創建一個對象的時候,只需要配置好配置文件/註解即可,完全不用考慮對象是如何被創建出來的。

在實際項目中一個 Service 類可能依賴了很多其他的類,假如我們需要實例化這個 Service,你可能要每次都要搞清這個 Service 所有底層類的構造函數,這可能會把人逼瘋。如果利用 IoC 的話,你只需要配置好,然後在需要的地方引用就行了,這大大增加了項目的可維護性且降低了開發難度。

在 Spring 中, IoC 容器是 Spring 用來實現 IoC 的載體, IoC 容器實際上就是個 Map(key,value),Map 中存放的是各種對象。

Spring 時代我們一般通過 XML 文件來配置 Bean,後來開發人員覺得 XML 文件來配置不太好,於是 SpringBoot 註解配置就慢慢開始流行起來。

相關閱讀:

什麼是 Spring Bean?

簡單來說,Bean 代指的就是那些被 IoC 容器所管理的對象。

我們需要告訴 IoC 容器幫助我們管理哪些對象,這個是通過配置元數據來定義的。配置元數據可以是 XML 文件、註解或者 Java 配置類。

<!-- Constructor-arg with 'value' attribute -->
<bean id="..." class="...">
   <constructor-arg value="..."/>
</bean>

下圖簡單地展示了 IoC 容器如何使用配置元數據來管理對象。

org.springframework.beansorg.springframework.context 這兩個包是 IoC 實現的基礎,如果想要研究 IoC 相關的源碼的話,可以去看看

將一個類聲明為 Bean 的註解有哪些?

  • @Component :通用的註解,可標註任意類為 Spring 組件。如果一個 Bean 不知道屬於哪個層,可以使用@Component 註解標註。
  • @Repository : 對應持久層即 Dao 層,主要用於資料庫相關操作。
  • @Service : 對應服務層,主要涉及一些複雜的邏輯,需要用到 Dao 層。
  • @Controller : 對應 Spring MVC 控制層,主要用戶接受用戶請求並調用 Service 層返回數據給前端頁面。

@Component 和 @Bean 的區別是什麼?

  • @Component 註解作用於類,而@Bean註解作用於方法。
  • @Component通常是通過類路徑掃描來自動偵測以及自動裝配到 Spring 容器中(我們可以使用 @ComponentScan 註解定義要掃描的路徑從中找出標識了需要裝配的類自動裝配到 Spring 的 bean 容器中)。@Bean 註解通常是我們在標有該註解的方法中定義產生這個 bean,@Bean告訴了 Spring 這是某個類的實例,當我需要用它的時候還給我。
  • @Bean 註解比 @Component 註解的自定義性更強,而且很多地方我們只能通過 @Bean 註解來註冊 bean。比如當我們引用第三方庫中的類需要裝配到 Spring容器時,則只能通過 @Bean來實現。

@Bean註解使用示例:

@Configuration
public class AppConfig {
    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }

}

上面的程式碼相當於下面的 xml 配置

<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

下面這個例子是通過 @Component 無法實現的。

@Bean
public OneService getService(status) {
    case (status)  {
        when 1:
                return new serviceImpl1();
        when 2:
                return new serviceImpl2();
        when 3:
                return new serviceImpl3();
    }
}

注入 Bean 的註解有哪些?

Spring 內置的 @Autowired 以及 JDK 內置的 @Resource@Inject 都可以用於注入 Bean。

Annotaion Package Source
@Autowired org.springframework.bean.factory Spring 2.5+
@Resource javax.annotation Java JSR-250
@Inject javax.inject Java JSR-330

@Autowired@Resource使用的比較多一些。

@Autowired 和 @Resource 的區別是什麼?

Autowired 屬於 Spring 內置的註解,默認的注入方式為byType(根據類型進行匹配),也就是說會優先根據介面類型去匹配並注入 Bean (介面的實現類)。

這會有什麼問題呢? 當一個介面存在多個實現類的話,byType這種方式就無法正確注入對象了,因為這個時候 Spring 會同時找到多個滿足條件的選擇,默認情況下它自己不知道選擇哪一個。

這種情況下,注入方式會變為 byName(根據名稱進行匹配),這個名稱通常就是類名(首字母小寫)。就比如說下面程式碼中的 smsService 就是我這裡所說的名稱,這樣應該比較好理解了吧。

// smsService 就是我們上面所說的名稱
@Autowired
private SmsService smsService;

舉個例子,SmsService 介面有兩個實現類: SmsServiceImpl1SmsServiceImpl2,且它們都已經被 Spring 容器所管理。

// 報錯,byName 和 byType 都無法匹配到 bean
@Autowired
private SmsService smsService;
// 正確注入 SmsServiceImpl1 對象對應的 bean
@Autowired
private SmsService smsServiceImpl1;
// 正確注入  SmsServiceImpl1 對象對應的 bean
// smsServiceImpl1 就是我們上面所說的名稱
@Autowired
@Qualifier(value = "smsServiceImpl1")
private SmsService smsService;

我們還是建議通過 @Qualifier 註解來顯示指定名稱而不是依賴變數的名稱。

@Resource屬於 JDK 提供的註解,默認注入方式為 byName。如果無法通過名稱匹配到對應的 Bean 的話,注入方式會變為byType

@Resource 有兩個比較重要且日常開發常用的屬性:name(名稱)、type(類型)。

public @interface Resource {
    String name() default "";
    Class<?> type() default Object.class;
}

如果僅指定 name 屬性則注入方式為byName,如果僅指定type屬性則注入方式為byType,如果同時指定nametype屬性(不建議這麼做)則注入方式為byType+byName

// 報錯,byName 和 byType 都無法匹配到 bean
@Resource
private SmsService smsService;
// 正確注入 SmsServiceImpl1 對象對應的 bean
@Resource
private SmsService smsServiceImpl1;
// 正確注入 SmsServiceImpl1 對象對應的 bean(比較推薦這種方式)
@Resource(name = "smsServiceImpl1")
private SmsService smsService;

簡單總結一下:

  • @Autowired 是 Spring 提供的註解,@Resource 是 JDK 提供的註解。
  • Autowired 默認的注入方式為byType(根據類型進行匹配),@Resource默認注入方式為 byName(根據名稱進行匹配)。
  • 當一個介面存在多個實現類的情況下,@Autowired@Resource都需要通過名稱才能正確匹配到對應的 Bean。Autowired 可以通過 @Qualifier 註解來顯示指定名稱,@Resource可以通過 name 屬性來顯示指定名稱。

Bean 的作用域有哪些?

Spring 中 Bean 的作用域通常有下面幾種:

  • singleton : IoC 容器中只有唯一的 bean 實例。Spring 中的 bean 默認都是單例的,是對單例設計模式的應用。
  • prototype : 每次獲取都會創建一個新的 bean 實例。也就是說,連續 getBean() 兩次,得到的是不同的 Bean 實例。
  • request (僅 Web 應用可用): 每一次 HTTP 請求都會產生一個新的 bean(請求 bean),該 bean 僅在當前 HTTP request 內有效。
  • session (僅 Web 應用可用) : 每一次來自新 session 的 HTTP 請求都會產生一個新的 bean(會話 bean),該 bean 僅在當前 HTTP session 內有效。
  • application/global-session (僅 Web 應用可用): 每個 Web 應用在啟動時創建一個 Bean(應用 Bean),,該 bean 僅在當前應用啟動時間內有效。
  • websocket (僅 Web 應用可用):每一次 WebSocket 會話產生一個新的 bean。

如何配置 bean 的作用域呢?

xml 方式:

<bean id="..." class="..." scope="singleton"></bean>

註解方式:

@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person personPrototype() {
    return new Person();
}

單例 Bean 的執行緒安全問題了解嗎?

大部分時候我們並沒有在項目中使用多執行緒,所以很少有人會關注這個問題。單例 Bean 存在執行緒問題,主要是因為當多個執行緒操作同一個對象的時候是存在資源競爭的。

常見的有兩種解決辦法:

  1. 在 Bean 中盡量避免定義可變的成員變數。
  2. 在類中定義一個 ThreadLocal 成員變數,將需要的可變成員變數保存在 ThreadLocal 中(推薦的一種方式)。

不過,大部分 Bean 實際都是無狀態(沒有實例變數)的(比如 Dao、Service),這種情況下, Bean 是執行緒安全的。

Bean 的生命周期了解么?

下面的內容整理自://yemengying.com/2016/07/14/spring-bean-life-cycle/ ,除了這篇文章,再推薦一篇很不錯的文章 ://www.cnblogs.com/zrtqsk/p/3735273.html

  • Bean 容器找到配置文件中 Spring Bean 的定義。
  • Bean 容器利用 Java Reflection API 創建一個 Bean 的實例。
  • 如果涉及到一些屬性值 利用 set()方法設置一些屬性值。
  • 如果 Bean 實現了 BeanNameAware 介面,調用 setBeanName()方法,傳入 Bean 的名字。
  • 如果 Bean 實現了 BeanClassLoaderAware 介面,調用 setBeanClassLoader()方法,傳入 ClassLoader對象的實例。
  • 如果 Bean 實現了 BeanFactoryAware 介面,調用 setBeanFactory()方法,傳入 BeanFactory對象的實例。
  • 與上面的類似,如果實現了其他 *.Aware介面,就調用相應的方法。
  • 如果有和載入這個 Bean 的 Spring 容器相關的 BeanPostProcessor 對象,執行postProcessBeforeInitialization() 方法
  • 如果 Bean 實現了InitializingBean介面,執行afterPropertiesSet()方法。
  • 如果 Bean 在配置文件中的定義包含 init-method 屬性,執行指定的方法。
  • 如果有和載入這個 Bean 的 Spring 容器相關的 BeanPostProcessor 對象,執行postProcessAfterInitialization() 方法
  • 當要銷毀 Bean 的時候,如果 Bean 實現了 DisposableBean 介面,執行 destroy() 方法。
  • 當要銷毀 Bean 的時候,如果 Bean 在配置文件中的定義包含 destroy-method 屬性,執行指定的方法。

圖示:

Spring Bean 生命周期

與之比較類似的中文版本:

Spring Bean 生命周期

Spring AoP

談談自己對於 AOP 的了解

AOP(Aspect-Oriented Programming:面向切面編程)能夠將那些與業務無關,卻為業務模組所共同調用的邏輯或責任(例如事務處理、日誌管理、許可權控制等)封裝起來,便於減少系統的重複程式碼,降低模組間的耦合度,並有利於未來的可拓展性和可維護性。

Spring AOP 就是基於動態代理的,如果要代理的對象,實現了某個介面,那麼 Spring AOP 會使用 JDK Proxy,去創建代理對象,而對於沒有實現介面的對象,就無法使用 JDK Proxy 去進行代理了,這時候 Spring AOP 會使用 Cglib 生成一個被代理對象的子類來作為代理,如下圖所示:

SpringAOPProcess

當然你也可以使用 AspectJ !Spring AOP 已經集成了 AspectJ ,AspectJ 應該算的上是 Java 生態系統中最完整的 AOP 框架了。

AOP 切面編程設計到的一些專業術語:

術語 含義
目標(Target) 被通知的對象
代理(Proxy) 向目標對象應用通知之後創建的代理對象
連接點(JoinPoint) 目標對象的所屬類中,定義的所有方法均為連接點
切入點(Pointcut) 被切面攔截 / 增強的連接點(切入點一定是連接點,連接點不一定是切入點)
通知(Advice) 增強的邏輯 / 程式碼,也即攔截到目標對象的連接點之後要做的事情
切面(Aspect) 切入點(Pointcut)+通知(Advice)
Weaving(織入) 將通知應用到目標對象,進而生成代理對象的過程動作

Spring AOP 和 AspectJ AOP 有什麼區別?

Spring AOP 屬於運行時增強,而 AspectJ 是編譯時增強。 Spring AOP 基於代理(Proxying),而 AspectJ 基於位元組碼操作(Bytecode Manipulation)。

Spring AOP 已經集成了 AspectJ ,AspectJ 應該算的上是 Java 生態系統中最完整的 AOP 框架了。AspectJ 相比於 Spring AOP 功能更加強大,但是 Spring AOP 相對來說更簡單,

如果我們的切面比較少,那麼兩者性能差異不大。但是,當切面太多的話,最好選擇 AspectJ ,它比 Spring AOP 快很多。

AspectJ 定義的通知類型有哪些?

  • Before(前置通知):目標對象的方法調用之前觸發
  • After (後置通知):目標對象的方法調用之後觸發
  • AfterReturning(返回通知):目標對象的方法調用完成,在返回結果值之後觸發
  • AfterThrowing(異常通知) :目標對象的方法運行中拋出 / 觸發異常後觸發。AfterReturning 和 AfterThrowing 兩者互斥。如果方法調用成功無異常,則會有返回值;如果方法拋出了異常,則不會有返回值。
  • Around: (環繞通知)編程式控制目標對象的方法調用。環繞通知是所有通知類型中可操作範圍最大的一種,因為它可以直接拿到目標對象,以及要執行的方法,所以環繞通知可以任意的在目標對象的方法調用前後搞事,甚至不調用目標對象的方法

多個切面的執行順序如何控制?

1、通常使用@Order 註解直接定義切面順序

// 值越小優先順序越高
@Order(3)
@Component
@Aspect
public class LoggingAspect implements Ordered {

2、實現Ordered 介面重寫 getOrder 方法。

@Component
@Aspect
public class LoggingAspect implements Ordered {

    // ....

    @Override
    public int getOrder() {
        // 返回值越小優先順序越高
        return 1;
    }
}

Spring MVC

說說自己對於 Spring MVC 了解?

MVC 是模型(Model)、視圖(View)、控制器(Controller)的簡寫,其核心思想是通過將業務邏輯、數據、顯示分離來組織程式碼。

網上有很多人說 MVC 不是設計模式,只是軟體設計規範,我個人更傾向於 MVC 同樣是眾多設計模式中的一種。java-design-patterns 項目中就有關於 MVC 的相關介紹。

想要真正理解 Spring MVC,我們先來看看 Model 1 和 Model 2 這兩個沒有 Spring MVC 的時代。

Model 1 時代

很多學 Java 後端比較晚的朋友可能並沒有接觸過 Model 1 時代下的 JavaWeb 應用開發。在 Model1 模式下,整個 Web 應用幾乎全部用 JSP 頁面組成,只用少量的 JavaBean 來處理資料庫連接、訪問等操作。

這個模式下 JSP 即是控制層(Controller)又是表現層(View)。顯而易見,這種模式存在很多問題。比如控制邏輯和表現邏輯混雜在一起,導致程式碼重用率極低;再比如前端和後端相互依賴,難以進行測試維護並且開發效率極低。

mvc-mode1

Model 2 時代

學過 Servlet 並做過相關 Demo 的朋友應該了解「Java Bean(Model)+ JSP(View)+Servlet(Controller) 」這種開發模式,這就是早期的 JavaWeb MVC 開發模式。

  • Model:系統涉及的數據,也就是 dao 和 bean。
  • View:展示模型中的數據,只是用來展示。
  • Controller:處理用戶請求都發送給 ,返回數據給 JSP 並展示給用戶。

Model2 模式下還存在很多問題,Model2 的抽象和封裝程度還遠遠不夠,使用 Model2 進行開發時不可避免地會重複造輪子,這就大大降低了程式的可維護性和復用性。

於是,很多 JavaWeb 開發相關的 MVC 框架應運而生比如 Struts2,但是 Struts2 比較笨重。

Spring MVC 時代

隨著 Spring 輕量級開發框架的流行,Spring 生態圈出現了 Spring MVC 框架, Spring MVC 是當前最優秀的 MVC 框架。相比於 Struts2 , Spring MVC 使用更加簡單和方便,開發效率更高,並且 Spring MVC 運行速度更快。

MVC 是一種設計模式,Spring MVC 是一款很優秀的 MVC 框架。Spring MVC 可以幫助我們進行更簡潔的 Web 層的開發,並且它天生與 Spring 框架集成。Spring MVC 下我們一般把後端項目分為 Service 層(處理業務)、Dao 層(資料庫操作)、Entity 層(實體類)、Controller 層(控制層,返回數據給前台頁面)。

Spring MVC 的核心組件有哪些?

記住了下面這些組件,也就記住了 SpringMVC 的工作原理。

  • DispatcherServlet核心的中央處理器,負責接收請求、分發,並給予客戶端響應。
  • HandlerMapping處理器映射器,根據 uri 去匹配查找能處理的 Handler ,並會將請求涉及到的攔截器和 Handler 一起封裝。
  • HandlerAdapter處理器適配器,根據 HandlerMapping 找到的 Handler ,適配執行對應的 Handler
  • Handler請求處理器,處理實際請求的處理器。
  • ViewResolver視圖解析器,根據 Handler 返回的邏輯視圖 / 視圖,解析並渲染真正的視圖,並傳遞給 DispatcherServlet 響應客戶端

SpringMVC 工作原理了解嗎?

Spring MVC 原理如下圖所示:

SpringMVC 工作原理的圖解我沒有自己畫,直接圖省事在網上找了一個非常清晰直觀的,原出處不明。

流程說明(重要):

  1. 客戶端(瀏覽器)發送請求, DispatcherServlet攔截請求。
  2. DispatcherServlet 根據請求資訊調用 HandlerMappingHandlerMapping 根據 uri 去匹配查找能處理的 Handler(也就是我們平常說的 Controller 控制器) ,並會將請求涉及到的攔截器和 Handler 一起封裝。
  3. DispatcherServlet 調用 HandlerAdapter適配執行 Handler
  4. Handler 完成對用戶請求的處理後,會返回一個 ModelAndView 對象給DispatcherServletModelAndView 顧名思義,包含了數據模型以及相應的視圖的資訊。Model 是返回的數據對象,View 是個邏輯上的 View
  5. ViewResolver 會根據邏輯 View 查找實際的 View
  6. DispaterServlet 把返回的 Model 傳給 View(視圖渲染)。
  7. View 返回給請求者(瀏覽器)

統一異常處理怎麼做?

推薦使用註解的方式統一異常處理,具體會使用到 @ControllerAdvice + @ExceptionHandler 這兩個註解 。

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(BaseException.class)
    public ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) {
      //......
    }

    @ExceptionHandler(value = ResourceNotFoundException.class)
    public ResponseEntity<ErrorReponse> handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {
      //......
    }
}

這種異常處理方式下,會給所有或者指定的 Controller 織入異常處理的邏輯(AOP),當 Controller 中的方法拋出異常的時候,由被@ExceptionHandler 註解修飾的方法進行處理。

ExceptionHandlerMethodResolvergetMappedMethod 方法決定了異常具體被哪個被 @ExceptionHandler 註解修飾的方法處理異常。

@Nullable
	private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
		List<Class<? extends Throwable>> matches = new ArrayList<>();
    //找到可以處理的所有異常資訊。mappedMethods 中存放了異常和處理異常的方法的對應關係
		for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
			if (mappedException.isAssignableFrom(exceptionType)) {
				matches.add(mappedException);
			}
		}
    // 不為空說明有方法處理異常
		if (!matches.isEmpty()) {
      // 按照匹配程度從小到大排序
			matches.sort(new ExceptionDepthComparator(exceptionType));
      // 返回處理異常的方法
			return this.mappedMethods.get(matches.get(0));
		}
		else {
			return null;
		}
	}

從源程式碼看出:getMappedMethod()會首先找到可以匹配處理異常的所有方法資訊,然後對其進行從小到大的排序,最後取最小的那一個匹配的方法(即匹配度最高的那個)。

Spring 框架中用到了哪些設計模式?

關於下面一些設計模式的詳細介紹,可以看筆主前段時間的原創文章《面試官:「談談 Spring 中都用到了那些設計模式?」。》

  • 工廠設計模式 : Spring 使用工廠模式通過 BeanFactoryApplicationContext 創建 bean 對象。
  • 代理設計模式 : Spring AOP 功能的實現。
  • 單例設計模式 : Spring 中的 Bean 默認都是單例的。
  • 模板方法模式 : Spring 中 jdbcTemplatehibernateTemplate 等以 Template 結尾的對資料庫操作的類,它們就使用到了模板模式。
  • 包裝器設計模式 : 我們的項目需要連接多個資料庫,而且不同的客戶在每次訪問中根據需要會去訪問不同的資料庫。這種模式讓我們可以根據客戶的需求能夠動態切換不同的數據源。
  • 觀察者模式: Spring 事件驅動模型就是觀察者模式很經典的一個應用。
  • 適配器模式 : Spring AOP 的增強或通知(Advice)使用到了適配器模式、spring MVC 中也是用到了適配器模式適配Controller
  • ……

Spring 事務

Spring/SpringBoot 模組下專門有一篇是講 Spring 事務的,總結的非常詳細,通俗易懂。

Spring 管理事務的方式有幾種?

  • 編程式事務 : 在程式碼中硬編碼(不推薦使用) : 通過 TransactionTemplate或者 TransactionManager 手動管理事務,實際應用中很少使用,但是對於你理解 Spring 事務管理原理有幫助。
  • 聲明式事務 : 在 XML 配置文件中配置或者直接基於註解(推薦使用) : 實際是通過 AOP 實現(基於@Transactional 的全註解方式使用最多)

Spring 事務中哪幾種事務傳播行為?

事務傳播行為是為了解決業務層方法之間互相調用的事務問題

當事務方法被另一個事務方法調用時,必須指定事務應該如何傳播。例如:方法可能繼續在現有事務中運行,也可能開啟一個新事務,並在自己的事務中運行。

正確的事務傳播行為可能的值如下:

1.TransactionDefinition.PROPAGATION_REQUIRED

使用的最多的一個事務傳播行為,我們平時經常使用的@Transactional註解默認使用就是這個事務傳播行為。如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。

2.TransactionDefinition.PROPAGATION_REQUIRES_NEW

創建一個新的事務,如果當前存在事務,則把當前事務掛起。也就是說不管外部方法是否開啟事務,Propagation.REQUIRES_NEW修飾的內部方法會新開啟自己的事務,且開啟的事務相互獨立,互不干擾。

3.TransactionDefinition.PROPAGATION_NESTED

如果當前存在事務,則創建一個事務作為當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價於TransactionDefinition.PROPAGATION_REQUIRED

4.TransactionDefinition.PROPAGATION_MANDATORY

如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常。(mandatory:強制性)

這個使用的很少。

若是錯誤的配置以下 3 種事務傳播行為,事務將不會發生回滾:

  • TransactionDefinition.PROPAGATION_SUPPORTS: 如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事務方式運行,如果當前存在事務,則把當前事務掛起。
  • TransactionDefinition.PROPAGATION_NEVER: 以非事務方式運行,如果當前存在事務,則拋出異常。

Spring 事務中的隔離級別有哪幾種?

和事務傳播行為這塊一樣,為了方便使用,Spring 也相應地定義了一個枚舉類:Isolation

public enum Isolation {

    DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),

    READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),

    READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

    REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),

    SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);

    private final int value;

    Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }

}

下面我依次對每一種事務隔離級別進行介紹:

  • TransactionDefinition.ISOLATION_DEFAULT :使用後端資料庫默認的隔離級別,MySQL 默認採用的 REPEATABLE_READ 隔離級別 Oracle 默認採用的 READ_COMMITTED 隔離級別.
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED :最低的隔離級別,使用這個隔離級別很少,因為它允許讀取尚未提交的數據變更,可能會導致臟讀、幻讀或不可重複讀
  • TransactionDefinition.ISOLATION_READ_COMMITTED : 允許讀取並發事務已經提交的數據,可以阻止臟讀,但是幻讀或不可重複讀仍有可能發生
  • TransactionDefinition.ISOLATION_REPEATABLE_READ : 對同一欄位的多次讀取結果都是一致的,除非數據是被本身事務自己所修改,可以阻止臟讀和不可重複讀,但幻讀仍有可能發生。
  • TransactionDefinition.ISOLATION_SERIALIZABLE : 最高的隔離級別,完全服從 ACID 的隔離級別。所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止臟讀、不可重複讀以及幻讀。但是這將嚴重影響程式的性能。通常情況下也不會用到該級別。

@Transactional(rollbackFor = Exception.class)註解了解嗎?

Exception 分為運行時異常 RuntimeException 和非運行時異常。事務管理對於企業應用來說是至關重要的,即使出現異常情況,它也可以保證數據的一致性。

@Transactional 註解作用於類上時,該類的所有 public 方法將都具有該類型的事務屬性,同時,我們也可以在方法級別使用該標註來覆蓋類級別的定義。如果類或者方法加了這個註解,那麼這個類裡面的方法拋出異常,就會回滾,資料庫裡面的數據也會回滾。

@Transactional 註解中如果不配置rollbackFor屬性,那麼事務只會在遇到RuntimeException的時候才會回滾,加上 rollbackFor=Exception.class,可以讓事務在遇到非運行時異常時也回滾。

Spring Data JPA

JPA 重要的是實戰,這裡僅對小部分知識點進行總結。

如何使用 JPA 在資料庫中非持久化一個欄位?

假如我們有下面一個類:

@Entity(name="USER")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private Long id;

    @Column(name="USER_NAME")
    private String userName;

    @Column(name="PASSWORD")
    private String password;

    private String secrect;

}

如果我們想讓secrect 這個欄位不被持久化,也就是不被資料庫存儲怎麼辦?我們可以採用下面幾種方法:

static String transient1; // not persistent because of static
final String transient2 = "Satish"; // not persistent because of final
transient String transient3; // not persistent because of transient
@Transient
String transient4; // not persistent because of @Transient

一般使用後面兩種方式比較多,我個人使用註解的方式比較多。

JPA 的審計功能是做什麼的?有什麼用?

審計功能主要是幫助我們記錄資料庫操作的具體行為比如某條記錄是誰創建的、什麼時間創建的、最後修改人是誰、最後修改時間是什麼時候。

@Data
@AllArgsConstructor
@NoArgsConstructor
@MappedSuperclass
@EntityListeners(value = AuditingEntityListener.class)
public abstract class AbstractAuditBase {

    @CreatedDate
    @Column(updatable = false)
    @JsonIgnore
    private Instant createdAt;

    @LastModifiedDate
    @JsonIgnore
    private Instant updatedAt;

    @CreatedBy
    @Column(updatable = false)
    @JsonIgnore
    private String createdBy;

    @LastModifiedBy
    @JsonIgnore
    private String updatedBy;
}
  • @CreatedDate: 表示該欄位為創建時間欄位,在這個實體被 insert 的時候,會設置值

  • @CreatedBy :表示該欄位為創建人,在這個實體被 insert 的時候,會設置值

    @LastModifiedDate@LastModifiedBy同理。

實體之間的關聯關係註解有哪些?

  • @OneToOne : 一對一。
  • @ManyToMany :多對多。
  • @OneToMany : 一對多。
  • @ManyToOne :多對一。

利用 @ManyToOne@OneToMany 也可以表達多對多的關聯關係。

Spring Security

Spring Security 重要的是實戰,這裡僅對小部分知識點進行總結。

有哪些控制請求訪問許可權的方法?

  • permitAll() :無條件允許任何形式訪問,不管你登錄還是沒有登錄。
  • anonymous() :允許匿名訪問,也就是沒有登錄才可以訪問。
  • denyAll() :無條件決絕任何形式的訪問。
  • authenticated():只允許已認證的用戶訪問。
  • fullyAuthenticated() :只允許已經登錄或者通過 remember-me 登錄的用戶訪問。
  • hasRole(String) : 只允許指定的角色訪問。
  • hasAnyRole(String) : 指定一個或者多個角色,滿足其一的用戶即可訪問。
  • hasAuthority(String) :只允許具有指定許可權的用戶訪問
  • hasAnyAuthority(String) :指定一個或者多個許可權,滿足其一的用戶即可訪問。
  • hasIpAddress(String) : 只允許指定 ip 的用戶訪問。

hasRole 和 hasAuthority 有區別嗎?

可以看看松哥的這篇文章:Spring Security 中的 hasRole 和 hasAuthority 有區別嗎?,介紹的比較詳細。

如何對密碼進行加密?

如果我們需要保存密碼這類敏感數據到資料庫的話,需要先加密再保存。

Spring Security 提供了多種加密演算法的實現,開箱即用,非常方便。這些加密演算法實現類的父類是 PasswordEncoder ,如果你想要自己實現一個加密演算法的話,也需要繼承 PasswordEncoder

PasswordEncoder 介面一共也就 3 個必須實現的方法。

public interface PasswordEncoder {
    // 加密也就是對原始密碼進行編碼
    String encode(CharSequence var1);
    // 比對原始密碼和資料庫中保存的密碼
    boolean matches(CharSequence var1, String var2);
    // 判斷加密密碼是否需要再次進行加密,默認返回 false
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

官方推薦使用基於 bcrypt 強哈希函數的加密演算法實現類。

如何優雅更換系統使用的加密演算法?

如果我們在開發過程中,突然發現現有的加密演算法無法滿足我們的需求,需要更換成另外一個加密演算法,這個時候應該怎麼辦呢?

推薦的做法是通過 DelegatingPasswordEncoder 兼容多種不同的密碼加密方案,以適應不同的業務需求。

從名字也能看出來,DelegatingPasswordEncoder 其實就是一個代理類,並非是一種全新的加密演算法,它做的事情就是代理上面提到的加密演算法實現類。在 Spring Security 5.0 之後,默認就是基於 DelegatingPasswordEncoder 進行密碼加密的。

參考

後記

專註 Java 原創乾貨分享,大三開源 JavaGuide (「Java學習+面試指南」一份涵蓋大部分 Java 程式設計師所需要掌握的核心知識。準備 Java 面試,首選 JavaGuide!),目前已經 120k+ Star。

如果本文對你有幫助的話,歡迎點贊分享,這對我繼續分享&創作優質文章非常重要。感謝 🙏🏻