文章目錄
- 7. 自動裝配 (Autowiring)
- 7.1 XML 自動裝配
- 7.2 使用注解實現自動裝配
- `@Autowired` vs `@Resource`
- 8. 使用注解開發(完全體)
- 8.1 定義 Bean (`@Component` 及其衍生注解)
- 8.2 注入屬性 (`@Value`)
- 8.3 注入對象
- 8.4 定義作用域 (`@Scope`)
- 8.5 小結:XML vs. 注解
- 9. 使用 Java 配置 (JavaConfig)
- 10、代理模式
- 10.1 靜態代理
- 角色分析
- 代碼實現
- 優缺點
- 10.2 靜態代理再理解
- 代碼實現
- 10.3 動態代理
- 代碼實現
- 10.4 動態代理再理解
- 通用處理器
- 測試
- 動態代理的好處
- 11、AOP(TODO:未手敲)
- 11.1 什么是AOP
- 11.2 AOP在Spring中的作用
- 11.3 使用Spring實現AOP
- 1. 導入AOP依賴
- 方式一:使用Spring原生API接口
- 方式二:自定義類來實現AOP
- 方式三:使用注解實現
- **知識點:`<aop:aspectj-autoproxy/>`**
- 11.4 三種實現 AOP 方式的對比
- 相同點 (Core Principles)
- 不同點 (Evolution & Style)
- 12、整合MyBatis
- 整合步驟概覽
- 1. 導入Maven依賴
- 2. 配置Maven靜態資源過濾
- 12.1 回憶MyBatis
- 1. 實體類 (User.java)
- 2. MyBatis核心配置文件 (mybatis-config.xml)
- 3. Mapper接口 (UserMapper.java)
- 4. Mapper XML文件 (UserMapper.xml)
- 5. 測試類
- 12.2 MyBatis-Spring核心概念
- 12.3 整合實現方式一:SqlSessionTemplate
- 1. 創建Spring數據層配置文件 (spring-dao.xml)
- 2. 創建Mapper實現類 (UserMapperImpl.java)
- 3. 創建Spring主配置文件 (applicationContext.xml)
- 4. 測試
- 12.4 整合實現方式二:SqlSessionDaoSupport
- 1. 修改Mapper實現類 (UserMapperImpl2.java)
- 2. 修改Spring配置 (applicationContext.xml)
- 3. 測試 (類似方式一)
- 13、聲明式事務
- 13.1 回顧事務
- 13.2 事務失效場景測試
- 13.3 Spring中的事務管理
- 配置聲明式事務(XML方式)
- 再次測試
- 寫在最后
- 參考
你好,我是 ZzzFatFish,歡迎回到我的 Spring 學習筆記。在 循序漸進學 Spring (上):從 IoC/DI 核心原理到 XML 配置實戰 中,我們深入探討了 Spring 的核心 IoC/DI 以及 XML 配置的各種方式。這次,我們將更進一步,探索如何讓 Spring 變得更“聰明”,從自動裝配開始,逐步走向現代化的注解和 Java 配置,并最終揭開 AOP 的神秘面紗,完成與 MyBatis 的整合實戰。
7. 自動裝配 (Autowiring)
自動裝配是 Spring IoC 容器的一項強大功能,它可以自動滿足 Bean 之間的依賴關系,減少 XML 中的顯式配置。
7.1 XML 自動裝配
通過在 <bean>
標簽上設置 autowire
屬性實現。
環境準備
public class Cat { public void shout() { System.out.println("喵~"); } }
public class Dog { public void shout() { System.out.println("汪~"); } }
public class People {private Cat cat;private Dog dog;private String name;// ... getters and setters and toString
}
beans.xml
<bean id="cat" class="com.github.subei.pojo.Cat"/>
<bean id="dog" class="com.github.subei.pojo.Dog"/><!-- 手動裝配 (傳統方式) -->
<bean id="peopleManual" class="com.github.subei.pojo.People"><property name="cat" ref="cat"/><property name="dog" ref="dog"/>
</bean><!-- 手動裝配 (傳統方式2) -->
<bean id="people" class="com.github.pojo.People" p:cat-ref="cat" p:dog-ref="dog"/><!-- 自動裝配 byName -->
<bean id="peopleByName" class="com.github.subei.pojo.People" autowire="byName"/><!-- 自動裝配 byType -->
<bean id="peopleByType" class="com.github.subei.pojo.People" autowire="byType"/>
autowire="byName"
: Spring 會在容器中查找 id 與People
類中屬性名(cat
,dog
)相同的 Bean,并進行注入。autowire="byType"
: Spring 會在容器中查找與People
類中屬性類型(Cat
,Dog
)相匹配的 Bean,并進行注入。- 注意:使用
byType
時,容器中同類型的 Bean 必須是唯一的,否則會拋出異常。
- 注意:使用
public class MyTest {@Testpublic void test(){ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");People people = context.getBean("people", People.class);people.getCat().shout();people.getDog().shout();}
}
7.2 使用注解實現自動裝配
注解是目前更主流的自動裝配方式,它將配置信息直接寫在 Java 類中,更為便捷。
步驟 1:開啟注解支持
在 XML 配置文件中,需要添加 context
命名空間,并開啟注解掃描。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="..."><!-- 開啟基于注解的配置,它會激活 @Autowired, @Resource 等注解 --><context:annotation-config/><!-- 仍然需要將 Bean 定義在 XML 中 --><bean id="cat" class="com.github.subei.pojo.Cat"/><bean id="dog" class="com.github.subei.pojo.Dog"/><bean id="people" class="com.github.subei.pojo.People"/></beans>
步驟 2:在類中使用注解
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import javax.annotation.Resource;public class People {@Autowired // 1. 使用 @Autowired@Qualifier("cat") // 2. 當有多個同類型Bean時,用 @Qualifier 指定名稱private Cat cat;// @Autowired// private Dog dog;@Resource(name = "dog") // 3. 使用 @Resource (更推薦)private Dog dog;// ...
}
@Autowired
vs @Resource
這是非常重要的一個區別點:
特性 | @Autowired (Spring 提供) | @Resource (JSR-250, Java 標準) |
---|---|---|
裝配順序 | 1. 按類型 (byType ) 在容器中查找。 | 1. 按名稱 (byName ) 查找。 |
2. 如果找到多個,再按名稱 (byName ) 查找。 | 2. 如果按名稱找不到,再按類型 (byType ) 查找。 | |
3. 如果仍未確定,可配合 @Qualifier("beanId") 使用。 | 3. 如果按類型也找不到或找到多個,則報錯。 | |
依賴 | 強依賴 Spring 框架。 | 是 Java 標準,減少了對 Spring 的耦合。 |
常用場景 | 簡單場景,或需要 @Qualifier 精準控制時。 | 推薦使用,其裝配順序更符合直覺,更明確。 |
@Autowired(required = false)
:可以標注某個屬性不是必須的,如果容器中找不到對應的 Bean,該屬性為null
而不會報錯。
觀點:有人認為“注解一時爽,維護火葬場”,是因為當項目龐大、依賴關系復雜時,注解分散在各個類中,不如 XML 集中管理來得清晰。但在現代開發中,注解的便利性已成為主流,配合良好的設計可以很好地管理。
8. 使用注解開發(完全體)
之前的注解只是用于自動裝配,Bean本身還是在XML中定義的。現在我們學習使用注解來定義Bean,從而可以完全替代XML。
步驟 1:開啟組件掃描
使用 <context:component-scan>
來代替 <context:annotation-config/>
。它不僅會開啟注解支持,還會掃描指定包下的類,并將帶有特定注解的類自動注冊為 Bean。
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"><!--指定要掃描的包,Spring 會自動查找這個包及其子包下所有帶注解的類。這個標簽包含了 <context:annotation-config/> 的功能。
--><context:component-scan base-package="com.github.pojo"/></beans>
步驟 2:使用注解定義 Bean 及其屬性
8.1 定義 Bean (@Component
及其衍生注解)
@Component
: 通用的組件注解,表示這個類是一個由 Spring 管理的 Bean。默認的 beanId 是類名首字母小寫(如User
->user
)。@Component("customId")
可以自定義 beanId。
為了更好地標識分層架構,@Component
有三個衍生注解,它們在功能上完全相同,但語義更清晰:
@Repository
: 用于標注數據訪問層 (DAO) 的組件。@Service
: 用于標注業務邏輯層 (Service) 的組件。@Controller
: 用于標注表現層 (Controller) 的組件。
import org.springframework.stereotype.Component;@Component // 等價于 <bean id="user" class="com.github.subei.pojo.User"/>
public class User {// 相當于 <property name="name" value="subeiLY"/>@Value("subeiLY")public String name;// ...
}
8.2 注入屬性 (@Value
)
@Value
注解用于注入基本類型和 String
類型的值。
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;@Component
public class User {// 相當于 <property name="name" value="subeiLY"/>@Value("subeiLY")public String name;
}
8.3 注入對象
使用 @Autowired
或 @Resource
,與第 7 節相同。
8.4 定義作用域 (@Scope
)
使用 @Scope
注解來定義 Bean 的作用域。
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;@Component
@Scope("prototype") // 等價于 <bean ... scope="prototype"/>
public class User { ... }
8.5 小結:XML vs. 注解
- XML:配置集中,一目了然,適用于所有場景。但當 Bean 數量多時,會很繁瑣。
- 注解:配置分散在代碼中,開發便捷。但對于第三方庫的類(我們無法修改源碼),則無法使用注解。
最佳實踐(混合使用):
- XML:負責整體配置,如數據源、事務管理器、組件掃描
<context:component-scan>
等。 - 注解:負責業務類(Controller, Service, DAO)的 Bean 定義和依賴注入。
9. 使用 Java 配置 (JavaConfig)
從 Spring 3.0 開始,官方提供了完全基于 Java 類來進行配置的方式,可以徹底擺脫 XML 文件。
步驟 1:創建配置類
配置類使用 @Configuration
標注,它本身也是一個 @Component
。
package com.github.subei.config;import com.github.subei.pojo.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;@Configuration // 聲明這是一個配置類,替代 beans.xml
@ComponentScan("com.github.subei.pojo") // 掃描組件,等同于 <context:component-scan>
@Import(AnotherConfig.class) // 導入其他配置類,等同于 <import>
public class MyConfig {// @Bean 注解表示這個方法將返回一個對象,該對象將被注冊為 Spring 容器中的 Bean。// 方法名 `getUser` 默認成為 bean 的 id。// 返回值類型 `User` 相當于 <bean> 標簽的 class 屬性。@Beanpublic User getUser() {return new User(); // 返回要注入到容器中的對象}
}
步驟 2:創建實體類(可以與JavaConfig配合使用)
package com.github.subei.pojo;import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;// @Component注解可以讓 @ComponentScan 掃描到
@Component
public class User {private String name;public String getName() { return name; }@Value("KANGSHIFU") // 注入屬性值public void setName(String name) { this.name = name; }// ... toString
}
步驟 3:測試
當完全使用 Java 配置時,需要用 AnnotationConfigApplicationContext
來加載容器。
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class MyTest {public static void main(String[] args) {// 通過 AnnotationConfigApplicationContext 來獲取容器,參數是配置類的 Class 對象ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);// beanId 默認為方法名 "getUser"User user = (User) context.getBean("getUser");System.out.println(user.getName());}
}
好的,我已經幫你把筆記重新整理和排版,并針對你遇到的問題進行了一些補充說明。我保留了你所有的原始內容,包括你的個人備注,只是讓整體結構更清晰、更易于閱讀。
10、代理模式
為什么要學習代理模式?
因為這就是 Spring AOP 的底層實現原理!
代理模式的分類:
- 靜態代理
- 動態代理
10.1 靜態代理
角色分析
- 抽象角色 (Subject):一般會使用接口或者抽象類來定義。
- 真實角色 (Real Subject):被代理的角色,真正執行業務邏輯的類。
- 代理角色 (Proxy):代理真實角色,在真實角色執行前后,可以附加一些操作。
- 客戶 (Client):訪問代理角色的人。
代碼實現
1. 接口 (Rent.java)
package com.github.subei.demo;// 租房
public interface Rent {public void rent();
}
2. 真實角色 (Host.java)
package com.github.subei.demo;// 房東
public class Host implements Rent{public void rent(){System.out.println("房東要出租房子!");}
}
3. 代理角色 (Proxy.java)
package com.github.subei.demo;public class Proxy implements Rent { // 注意:代理類也應該實現同一個接口private Host host;public Proxy() {}public Proxy(Host host) {this.host = host;}public void rent(){seeHouse();host.rent(); // 調用真實角色的方法contract();fare();}// 看房public void seeHouse(){System.out.println("中介帶你看房!");}// 收中介費public void fare(){System.out.println("收中介費!");}// 簽合同public void contract(){System.out.println("和你簽合同!");}
}
4. 客戶端 (Client.java)
package com.github.subei.demo;public class Client {public static void main(String[] args) {// 房東要租房子Host host = new Host();// 代理,中介幫房東租房子,但是代理角色一般會有一些附屬操作!Proxy proxy = new Proxy(host);// 我們不直接找房東,而是直接找中介租房proxy.rent();}
}
優缺點
- 優點:
- 可以使得我們的真實角色更加純粹,不再去關注一些公共的業務。
- 公共的業務由代理來完成,實現了業務的分工。
- 公共業務發生擴展時,方便集中管理。
- 缺點:
- 一個真實角色就會產生一個代理角色,代碼量會翻倍,開發效率會變低。
10.2 靜態代理再理解
以用戶管理業務為例,日志功能就是可以被代理的公共業務。
代碼實現
1. 抽象角色 (UserService.java)
package com.github.subei.demo2;// 實現增刪改查業務
public interface UserService {void add();void delete();void update();void query();
}
2. 真實角色 (UserServiceImpl.java)
package com.github.subei.demo2;public class UserServiceImpl implements UserService {public void add() {System.out.println("添加用戶");}public void delete() {System.out.println("刪除用戶");}public void update() {System.out.println("更新用戶");}public void query() {System.out.println("查詢用戶");}
}
3. 代理角色 (UserServiceProxy.java)
package com.github.subei.demo2;public class UserServiceProxy implements UserService {private UserServiceImpl userService;public void setUserService(UserServiceImpl userService) {this.userService = userService;}public void add() {log("add");userService.add();}public void delete() {log("delete");userService.delete();}public void update() {log("update");userService.update();}public void query() {log("query");userService.query();}// 日志方法public void log(String msg){System.out.println("執行了 " + msg + " 方法");}
}
4. 客戶端 (Client.java)
package com.github.subei.demo2;public class Client {public static void main(String[] args) {// 真實業務對象UserServiceImpl userService = new UserServiceImpl();// 代理類UserServiceProxy proxy = new UserServiceProxy();// 設置要代理的真實對象proxy.setUserService(userService);proxy.add();proxy.query();}
}
思考: 這種開發模式像是縱向的業務開發中,切入了橫向的功能(如日志)。
我們想要靜態代理的好處,又不想要它的缺點,于是就有了 動態代理!
10.3 動態代理
- 動態代理和靜態代理的角色一樣。
- 動態代理的代理類是動態生成的,不是我們直接寫好的。
- 動態代理分類:
- 基于接口的動態代理 — JDK 動態代理【本例使用】
- 基于類的動態代理 — CGLIB
- Java 字節碼實現 — Javassist
需要了解兩個核心類:
java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
。
代碼實現
1. 抽象角色 (Rent.java)
package com.github.subei.demo3;public interface Rent {void rent();
}
2. 真實角色 (Host.java)
package com.github.subei.demo3;public class Host implements Rent {public void rent(){System.out.println("房東要出租房子!");}
}
3. 代理處理程序 (ProxyInvocationHandler.java)
package com.github.subei.demo3;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;// 會用這個類,自動生成代理類
public class ProxyInvocationHandler implements InvocationHandler {// 被代理的接口private Rent rent;public void setRent(Rent rent){this.rent = rent;}// 生成得到代理類public Object getProxy(){return Proxy.newProxyInstance(this.getClass().getClassLoader(),rent.getClass().getInterfaces(),this);}// 處理代理實例,并返回代理結果// 這個方法是代理對象調用任何接口方法時都會執行的public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 動態代理的本質就是利用反射機制seeHouse();// 調用真實對象的方法Object result = method.invoke(rent, args);fare();return result;}// 附加操作public void seeHouse(){System.out.println("中介帶你看房!");}public void fare(){System.out.println("收中介費!");}
}
4. 客戶端 (Client.java)
package com.github.subei.demo3;public class Client {public static void main(String[] args) {// 真實角色Host host = new Host();// 代理角色:現在沒有具體的代理類,只有一個處理器ProxyInvocationHandler handler = new ProxyInvocationHandler();// 通過調用程序來處理我們要調用的接口對象!handler.setRent(host);// 動態生成對應的代理類!Rent proxy = (Rent) handler.getProxy(); proxy.rent();}
}
核心: 一個動態代理處理器,一般代理某一類業務,可以代理實現了同一接口的多個類。
10.4 動態代理再理解
我們可以編寫一個通用的動態代理處理器,讓它能代理任何對象。
通用處理器
package com.github.subei.Demo4;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;// 通用的動態代理處理器
public class ProxyInvocationHandler implements InvocationHandler {// 被代理的對象,設置為Object類型private Object target;public void setTarget(Object target){this.target = target;}// 生成得到代理類public Object getProxy(){return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this);}// 處理代理實例,并返回代理結果public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {log(method.getName());Object result = method.invoke(target, args);return result;}public void log(String methodName){System.out.println("執行了 " + methodName + " 方法");}
}
測試
package com.github.subei.Demo4;import com.github.subei.demo2.UserService;
import com.github.subei.demo2.UserServiceImpl;public class Client {public static void main(String[] args) {// 真實角色UserServiceImpl userService = new UserServiceImpl();// 代理角色處理器ProxyInvocationHandler pih = new ProxyInvocationHandler();// 設置要代理的對象pih.setTarget(userService);// 動態生成代理類!UserService proxy = (UserService)pih.getProxy();proxy.add();}
}
動態代理的好處
- 使得我們的真實角色更加純粹,不再關注公共業務。
- 公共業務由代理完成,實現了業務分工。
- 公共業務發生擴展時,方便集中管理。
- 一個動態代理處理器可以代理多個類,只要它們實現了接口。
11、AOP(TODO:未手敲)
11.1 什么是AOP
AOP(Aspect Oriented Programming),意為:面向切面編程。通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。
AOP是OOP(面向對象編程)的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生范型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。
11.2 AOP在Spring中的作用
提供聲明式事務;允許用戶自定義切面。
AOP 核心概念:
- 橫切關注點 (Cross-cutting concerns):
跨越應用程序多個模塊的功能。即與我們業務邏輯無關,但我們又需要關注的部分,就是橫切關注點。如:日志、安全、緩存、事務等等。 - 切面 (Aspect):
橫切關注點被模塊化的特殊對象。在代碼中通常是一個類。 - 通知 (Advice):
切面必須要完成的工作,即切面類中的方法。 - 目標 (Target):
被通知的對象,即被代理的真實對象。 - 代理 (Proxy):
向目標對象應用通知之后創建的對象。 - 切入點 (Pointcut):
切面通知**執行的“地點”**的定義。 - 連接點 (JoinPoint):
與切入點匹配的執行點,在Spring中,連接點就是方法的執行。
Spring AOP 中,通過 Advice 定義橫切邏輯,Spring 支持 5 種類型的 Advice。
核心思想: AOP 就是在不改變原有代碼的情況下,去增加新的功能。
11.3 使用Spring實現AOP
1. 導入AOP依賴
使用AOP織入,需要導入aspectjweaver
依賴包。
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.4</version> <!-- 版本號可根據項目情況調整 -->
</dependency>
方式一:使用Spring原生API接口
1. 業務接口和實現類
// UserService.java
package com.github.subei.service;
public interface UserService {void add();void delete();void select();void update();
}// UserServiceImpl.java
package com.github.subei.service;
public class UserServiceImpl implements UserService{public void add() { System.out.println("增加了一個用戶"); }public void delete() { System.out.println("刪除了一個用戶"); }public void select() { System.out.println("查詢了一個用戶"); }public void update() { System.out.println("更新了一個用戶"); }
}
2. 前置/后置增強類
// Log.java (前置通知)
package com.github.subei.log;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;public class Log implements MethodBeforeAdvice {// method: 要執行的目標對象的方法// args: 參數// target: 目標對象public void before(Method method, Object[] args, Object target) throws Throwable {System.out.println(target.getClass().getName() + "的" + method.getName() + "方法被執行了!");}
}// AfterLog.java (后置通知)
package com.github.subei.log;
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;public class AfterLog implements AfterReturningAdvice {// returnValue: 返回值public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {System.out.println("執行了" + target.getClass().getName() + "的" + method.getName() + "方法,返回結果為:" + returnValue);}
}
3. Spring配置 (applicationContext.xml)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"><!-- 注冊bean --><bean id="userService" class="com.github.subei.service.UserServiceImpl"/><bean id="log" class="com.github.subei.log.Log"/><bean id="afterLog" class="com.github.subei.log.AfterLog"/><!-- 方式一: 使用原生Spring API接口 --><aop:config><!-- 切入點: expression:表達式, execution(要執行的位置!* * * *) --><aop:pointcut id="pointcut" expression="execution(* com.github.subei.service.UserServiceImpl.*(..))"/><!-- 配置通知器 aop:advisor --><aop:advisor advice-ref="log" pointcut-ref="pointcut"/><aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/></aop:config></beans>
4. 測試
import com.github.subei.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class MyTest {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");// 動態代理代理的是接口,所以要用接口類型接收UserService userService = context.getBean("userService", UserService.class);userService.select();}
}
方式二:自定義類來實現AOP
1. 編寫自定義切面類 (POJO)
package com.github.subei.diy;public class DiyPointCut {public void before(){System.out.println("---------方法執行前---------");}public void after(){System.out.println("---------方法執行后---------");}
}
或:
public class DiyPointCut {// "前置通知"方法現在可以接收一個JoinPoint對象public void before(JoinPoint jp) {System.out.println("---------方法執行前---------");// 1. 獲取目標對象System.out.println("目標對象: " + jp.getTarget());// 2. 獲取方法簽名,從而得到方法名和類名System.out.println("攔截的方法: " + jp.getSignature().getName());System.out.println("方法所屬類: " + jp.getSignature().getDeclaringTypeName());// 3. 獲取方法的參數System.out.println("方法參數: " + Arrays.toString(jp.getArgs()));}// "后置通知"方法同樣可以獲取這些信息public void after(JoinPoint jp) {System.out.println("---------方法執行后---------");System.out.println("方法 " + jp.getSignature().getName() + " 執行完畢。");}
}
2. Spring配置
<!-- 注冊自定義切面 bean -->
<bean id="diy" class="com.github.subei.diy.DiyPointCut"/><aop:config><!-- 使用 aop:aspect 定義切面 --><aop:aspect ref="diy"><!-- 定義切入點 --><aop:pointcut id="point" expression="execution(* com.github.subei.service.UserServiceImpl.*(..))"/><!-- 定義通知 --><aop:before method="before" pointcut-ref="point" /><aop:after method="after" pointcut-ref="point"/></aop:aspect>
</aop:config>
3. 測試 (同上)
方式三:使用注解實現
1. 編寫注解實現的切面類
package com.github.subei.diy;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;// 使用注解方式實現AOP
@Aspect // 標注這個類是一個切面
public class AnnotationPointCut {@Before("execution(* com.github.subei.service.UserServiceImpl.*(..))")public void before(){System.out.println("---------方法執行前(注解)---------");}@After("execution(* com.github.subei.service.UserServiceImpl.*(..))")public void after(){System.out.println("---------方法執行后(注解)---------");}// 在環繞增強中,我們可以給定一個參數,代表我們要獲取處理切入的點@Around("execution(* com.github.subei.service.UserServiceImpl.*(..))")public void around(ProceedingJoinPoint jp) throws Throwable {System.out.println("環繞前");Signature signature = jp.getSignature(); // 獲得簽名System.out.println("簽名: " + signature);// 執行目標方法: proceedObject proceed = jp.proceed();System.out.println("環繞后");System.out.println("執行結果: " + proceed);}
}
2. Spring配置文件
<!-- 第三種方法: 使用注解方式實現 --><!-- 1. 注冊帶有@Aspect注解的切面類 -->
<bean id="annotationPointCut" class="com.github.subei.diy.AnnotationPointCut"/><!-- 2. 開啟注解支持!(這會讓Spring自動尋找@Aspect注解的bean并創建代理) -->
<aop:aspectj-autoproxy/>
知識點:<aop:aspectj-autoproxy/>
- 通過AOP命名空間的
<aop:aspectj-autoproxy />
聲明,可以自動為Spring容器中那些配置了@AspectJ
切面的bean創建代理,織入切面。 - 它有一個
proxy-target-class
屬性,默認為false
:proxy-target-class="false"
(默認): 使用 JDK動態代理 織入增強。目標類必須實現接口。proxy-target-class="true"
: 使用 CGLIB動態代理 技術織入增強。即使目標類沒有實現接口,也可以創建代理。- 注意: 即使
proxy-target-class
設置為false
,如果目標類沒有聲明任何接口,Spring 將自動切換到使用 CGLIB。
11.4 三種實現 AOP 方式的對比
對比維度 (Feature) | 方式一:Spring API接口 | 方式二:自定義類 + XML配置 | 方式三:注解方式 (@AspectJ) |
---|---|---|---|
實現方式 | 切面類必須實現Spring特定的接口,如 MethodBeforeAdvice 。 | 切面類是一個普通的Java類 (POJO),不需要實現任何接口。 | 切面類是一個普通的Java類 (POJO),但使用 @Aspect 注解標識。 |
與Spring框架的耦合度 | 高。代碼強依賴于org.springframework.aop.* 包,可移植性差。 | 低。切面類本身完全不依賴Spring,可以獨立存在。 | 中等。依賴于org.aspectj.* 注解包,但與Spring核心API解耦。 |
配置方式 | 完全通過XML配置。使用<aop:advisor> 標簽將通知和切入點綁定。 | 完全通過XML配置。使用<aop:aspect> 標簽引用切面Bean,并指定方法。 | XML中只需開啟注解支持<aop:aspectj-autoproxy/> ,具體邏輯在Java類中通過注解完成。 |
易用性與可讀性 | 差。代碼最繁瑣,需要為不同類型的通知創建不同的類,結構分散。 | 中等。邏輯與配置分離,需要來回查看XML和Java文件才能理解整體。 | 好。邏輯、切入點、通知類型都集中在一個類中,代碼即配置,內聚性高,可讀性最好。 |
功能強大性 | 較弱。僅提供基本的前置、后置等通知,功能有限。 | 較強。支持前置、后置、環繞、異常等所有通知類型,但配置在XML中。 | 最強。完全支持AspectJ的所有功能,特別是@Around 環繞通知,可以精確控制目標方法的執行。 |
當前主流用法 | 已過時,基本不再使用。 | 在一些需要將AOP配置與業務代碼完全解耦的遺留項目中可能見到。 | 絕對主流,是當前Spring/Spring Boot項目開發的首選和推薦方式。 |
相同點 (Core Principles)
- 核心思想一致:三種方式都是為了實現AOP(面向切面編程),將橫切關注點(如日志、事務)與業務邏輯代碼分離。
- 底層實現一致:它們的底層都依賴于Spring在運行時創建動態代理對象(JDK動態代理或CGLIB代理)來織入切面邏輯。
- 依賴一致:都需要在項目中引入
aspectjweaver
這個依賴包。 - 切入點表達式一致:定義切入點時,使用的
execution()
表達式語法是完全相同的。
不同點 (Evolution & Style)
-
最大區別在于“耦合度”和“配置風格”:
- 方式一是強耦合、純XML配置。代碼和Spring API綁死。
- 方式二是低耦合、純XML配置。代碼是干凈的POJO,但AOP的“身份”和行為完全由外部XML賦予。
- 方式三是中等耦合、注解驅動。代碼通過注解自我聲明其AOP“身份”和行為,XML只負責開啟總開關。
-
代碼的侵入性不同:
- 方式一侵入性最強,因為它強制你的類去實現它的接口。
- 方式二和方式三對業務代碼都是非侵入式的,這也是AOP提倡的。
-
發展趨勢和推薦度不同:
這三種方式清晰地展示了Spring AOP的演進路線:從早期與框架緊密綁定的API,到配置與代碼分離,再到最終使用注解將配置與邏輯內聚。方式三(注解)無疑是目前最佳的實踐,因為它兼顧了低耦合、高可讀性和強大的功能。
簡單來說,你可以這樣理解它們的演進:
- 方式一:“你(代碼)必須聽我的(框架),按我的規矩來寫。”
- 方式二:“你(代碼)做你自己的事,我(XML)來告訴你什么時候該做什么。”
- 方式三:“你(代碼)自己決定自己該做什么,并告訴大家(通過注解),我(框架)負責讓你生效就行。”
12、整合MyBatis
整合步驟概覽
- 導入相關Jar包
- 編寫配置文件
- 編寫代碼與測試
1. 導入Maven依賴
<!-- JUnit 測試框架 -->
<dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version>
</dependency><!-- MyBatis 核心包 -->
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.3</version>
</dependency><!-- MySQL 數據庫驅動 -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.47</version>
</dependency><!-- Spring 相關包 -->
<dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.2.12.RELEASE</version>
</dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>5.1.10.RELEASE</version>
</dependency><!-- AOP 織入器 -->
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.6</version>
</dependency><!-- 【關鍵】MyBatis-Spring 整合包 -->
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId><version>2.0.6</version>
</dependency>
2. 配置Maven靜態資源過濾
為了確保 src/main/java
目錄下的 .xml
和 .properties
文件能被正確打包,需要在 pom.xml
中添加以下配置:
<build><resources><resource><directory>src/main/java</directory><includes><include>**/*.properties</include><include>**/*.xml</include></includes><filtering>true</filtering></resource></resources>
</build>
12.1 回憶MyBatis
在整合前,我們先回顧一下原生MyBatis的開發流程。
1. 實體類 (User.java)
package com.github.subei.pojo;
import lombok.Data;@Data
public class User {private int id;private String name;private String pwd;// ... Constructors, Getters, Setters, toString() ...
}
2. MyBatis核心配置文件 (mybatis-config.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><typeAliases><package name="com.github.subei.pojo"/></typeAliases><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="com.mysql.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=UTF-8"/><property name="username" value="root"/><property name="password" value="root"/></dataSource></environment></environments><mappers><package name="com.github.subei.mapper"/></mappers>
</configuration>
3. Mapper接口 (UserMapper.java)
package com.github.subei.mapper;
import com.github.subei.pojo.User;
import java.util.List;public interface UserMapper {List<User> selectUser();
}
4. Mapper XML文件 (UserMapper.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.github.subei.mapper.UserMapper"><select id="selectUser" resultType="User">select * from user;</select>
</mapper>
5. 測試類
public class MyTest {@Testpublic void selectUser() throws IOException {String resource = "mybatis-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);SqlSession sqlSession = sqlSessionFactory.openSession();try {UserMapper mapper = sqlSession.getMapper(UserMapper.class);List<User> userList = mapper.selectUser();for(User user : userList){System.out.println(user);}} finally {sqlSession.close();}}
}
12.2 MyBatis-Spring核心概念
官方文檔地址: http://www.mybatis.org/spring/zh/index.html
MyBatis-Spring 的核心目標是幫助我們將 MyBatis 無縫地整合到 Spring 容器中,讓 Spring 來管理 MyBatis 的組件。
-
SqlSessionFactoryBean
:- 在 Spring 中,我們不再使用
SqlSessionFactoryBuilder
,而是使用SqlSessionFactoryBean
來創建SqlSessionFactory
。 - 它負責讀取配置,并創建一個由 Spring 管理的
SqlSessionFactory
實例。 - 其最重要的屬性是
dataSource
,用于接收 Spring 管理的數據源。
- 在 Spring 中,我們不再使用
-
SqlSessionTemplate
:SqlSessionTemplate
是 MyBatis-Spring 的核心,它是SqlSession
的一個線程安全實現。- 它能自動參與到 Spring 的事務管理中,負責 session 的生命周期(獲取、提交/回滾、關閉)。
- 在整合后,我們應該始終使用
SqlSessionTemplate
來代替原生的DefaultSqlSession
。
12.3 整合實現方式一:SqlSessionTemplate
1. 創建Spring數據層配置文件 (spring-dao.xml)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"><!-- 1. 配置數據源 (DataSource) --><bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"><property name="driverClassName" value="com.mysql.jdbc.Driver" /><property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=UTF-8"/><property name="username" value="root"/><property name="password" value="root"/></bean><!-- 2. 配置 SqlSessionFactory --><bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"><property name="dataSource" ref="dataSource" /><!-- 關聯MyBatis核心配置文件 (可選,用于settings, typeAliases等) --><property name="configLocation" value="classpath:mybatis-config.xml"/><!-- 掃描Mapper XML文件 --><property name="mapperLocations" value="classpath:com/github/subei/mapper/*.xml"/></bean><!-- 3. 注冊 SqlSessionTemplate --><bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"><!-- 只能使用構造器注入sqlSessionFactory,因為它沒有set方法 --><constructor-arg index="0" ref="sqlSessionFactory"/></bean>
</beans>
2. 創建Mapper實現類 (UserMapperImpl.java)
package com.github.subei.mapper;
import com.github.subei.pojo.User;
import org.mybatis.spring.SqlSessionTemplate;
import java.util.List;public class UserMapperImpl implements UserMapper {// 我們的所有操作,都使用SqlSessionTemplate來執行private SqlSessionTemplate sqlSession;public void setSqlSession(SqlSessionTemplate sqlSession) {this.sqlSession = sqlSession;}public List<User> selectUser() {UserMapper mapper = sqlSession.getMapper(UserMapper.class);return mapper.selectUser();}
}
3. 創建Spring主配置文件 (applicationContext.xml)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"><!-- 導入數據層配置 --><import resource="spring-dao.xml"/><!-- 注冊Mapper實現類的Bean --><bean id="userMapper" class="com.github.subei.mapper.UserMapperImpl"><property name="sqlSession" ref="sqlSessionTemplate"/></bean>
</beans>
4. 測試
@Test
public void testSelectUser() {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");UserMapper userMapper = context.getBean("userMapper", UserMapper.class);for (User user : userMapper.selectUser()){System.out.println(user);}
}
結果成功輸出! 此時,原生的 mybatis-config.xml
文件中的數據源和事務管理器配置已被 Spring 完全接管,可以被簡化。
12.4 整合實現方式二:SqlSessionDaoSupport
這是一種更便捷的方式,通過繼承 SqlSessionDaoSupport
類來簡化 Mapper 實現類的編寫。
1. 修改Mapper實現類 (UserMapperImpl2.java)
package com.github.subei.mapper;
import com.github.subei.pojo.User;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import java.util.List;public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper {public List<User> selectUser() {// 直接通過 getSqlSession() 獲取 SqlSessionTemplateUserMapper mapper = getSqlSession().getMapper(UserMapper.class);return mapper.selectUser();}
}
2. 修改Spring配置 (applicationContext.xml)
SqlSessionDaoSupport
需要注入 SqlSessionFactory
而不是 SqlSessionTemplate
。
<bean id="userMapper2" class="com.github.subei.mapper.UserMapperImpl2"><property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
3. 測試 (類似方式一)
@Test
public void testSelectUser2() {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");UserMapper userMapper = context.getBean("userMapper2", UserMapper.class);// ...
}
總結:整合到Spring后,可以完全移除MyBatis配置文件中的數據源和事務配置。除了XML配置,還可以使用注解方式實現整合,這是現代開發中更主流的方式。
13、聲明式事務
13.1 回顧事務
事務 是一組不可分割的業務操作單元,要么都成功,要么都失敗。它用于保證數據的完整性和一致性。
事務的ACID原則:
- 原子性 (Atomicity):事務中的所有操作是一個整體,不可分割。
- 一致性 (Consistency):事務完成后,數據必須保持業務規則上的一致狀態。
- 隔離性 (Isolation):多個事務并發執行時,應相互隔離,防止數據損壞。
- 持久性 (Durability):事務一旦提交,其結果就是永久性的。
13.2 事務失效場景測試
假設我們在一個方法內,先執行一個成功的插入操作,再執行一個失敗的刪除操作(SQL語法錯誤)。
1. 擴展Mapper接口和XML
// UserMapper.java
public interface UserMapper {List<User> selectUser();int addUser(User user);int deleteUser(int id);
}
<!-- UserMapper.xml -->
<insert id="addUser" ...>insert into user (id,name,pwd) values (#{id},#{name},#{pwd});
</insert>
<!-- 故意寫錯SQL,將 delete 寫成 deletes -->
<delete id="deleteUser" ...>deletes from user where id = #{id};
</delete>
2. 在一個方法中調用
// UserMapperImpl.java
public List<User> selectUser() {UserMapper mapper = getSqlSession().getMapper(UserMapper.class);// 先添加mapper.addUser(new User(6,"維維","123456"));// 再刪除(這個會失敗)mapper.deleteUser(6); return mapper.selectUser();
}
測試結果:程序會因SQL異常而中斷,但數據庫中用戶添加成功了!這破壞了數據的一致性,因為我們期望添加和刪除是一個整體。
13.3 Spring中的事務管理
Spring 提供了兩種事務管理方式:
- 編程式事務:在業務代碼中手動控制事務的開啟、提交、回滾。侵入性強,不推薦。
- 聲明式事務:通過配置(XML或注解)來管理事務,業務代碼無需關心事務邏輯。這是我們使用的重點。
配置聲明式事務(XML方式)
1. 配置事務管理器
首先,需要一個事務管理器 DataSourceTransactionManager
,并關聯我們的數據源。
<!-- applicationContext.xml --><!-- 1. 配置聲明式事務管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource" />
</bean>
2. 配置事務通知 (tx:advice)
在這里定義事務的規則,比如哪些方法需要事務,以及事務的傳播特性。
需要先引入 tx
命名空間及其約束。
<!-- 2. 配置事務通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager"><tx:attributes><!-- 配置哪些方法使用什么樣的事務, propagation是傳播特性REQUIRED: 如果當前沒有事務,就新建一個;如果已存在,就加入。這是最常用的。--><tx:method name="add*" propagation="REQUIRED"/><tx:method name="delete*" propagation="REQUIRED"/><tx:method name="update*" propagation="REQUIRED"/><tx:method name="select*" read-only="true"/><tx:method name="*" propagation="REQUIRED"/></tx:attributes>
</tx:advice>
3. 配置AOP,將事務織入
使用AOP將事務通知應用到指定的方法上。
<!-- 3. 配置AOP -->
<aop:config><!-- 定義切入點,這里是對mapper包下的所有方法 --><aop:pointcut id="txPointcut" expression="execution(* com.github.subei.mapper.*.*(..))"/><!-- 將事務通知和切入點綁定 --><aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
再次測試
在配置好聲明式事務后,再次運行之前的測試代碼。
結果:程序依然會報錯,但是數據庫中新用戶沒有被添加!事務成功回滾,保證了數據的一致性。
為什么需要事務?
- 保證數據一致性:防止因部分操作失敗而導致數據狀態混亂。
- 簡化開發:使用Spring的聲明式事務,開發者可以專注于業務邏輯,而無需手動管理復雜的事務代碼。
寫在最后
從 IoC/DI 的核心原理,到 XML、注解、JavaConfig 三種配置方式的演進;從代理模式的底層鋪墊,到 AOP 的切面思想;再到最后整合 MyBatis 并加上事務的保障。至此,我們已經走完了 Spring Framework 最核心、最常用的一段旅程。
即使在 Spring Boot 已成為主流的今天,這些底層的概念和配置思想依然是理解 Spring 生態、排查復雜問題、成為一名優秀 Java 開發者的基石。
希望這份筆記能幫助正在學習 Spring 的你,理清思路,夯實基礎。學習是一個持續迭代的過程,與君共勉。
🎉🎉🎉 Spring 核心部分完結撒花! 🎉🎉🎉
📦 對應代碼倉庫:https://gitee.com/zzzfatfish/spring-test
? 作者:fatfish
🕒 狀態:學習中,內容持續補充完善…
參考
【狂神說Java】Spring5最新完整教程IDEA版通俗易懂
Spring學習目錄(6天) - subeiLY - 博客園