收藏這三篇筆記,完整回顧Spring常見問題及使用方式速查:
0. 基本概念
- 面向切面編程,它將業務邏輯的各個部分進行隔離,使開發人員在編寫業務邏輯時可以專心於核心業務,從而提高了開發效率。
- 關注點(切入點)代碼,就是指重複執行的代碼。
- 業務代碼與關注點代碼分離:關注點代碼寫一次即可;開發者只需要關注核心業務,運行時期,執行核心業務代碼時候動態植入關注點代碼。
// 關注點代碼舉例
public void add(User user) {
Session session = null;
Transaction trans = null;
try {
session = HibernateSessionFactoryUtils.getSession(); // 【關注點代碼】
trans = session.beginTransaction(); // 【關注點代碼】
session.save(user); // 核心業務代碼:如何保存、用戶有效性校驗
trans.commit(); //…【關注點代碼】
} catch (Exception e) {
e.printStackTrace();
if(trans != null){
trans.rollback(); //..【關注點代碼】
}
} finally{
HibernateSessionFactoryUtils.closeSession(session); ////..【關注點代碼】
}
}
1. AOP概念及術語
術語 | 解釋 |
---|---|
Joinpoint(連接點) | 指那些被攔截到的點,在 Spring 中,可以被動態代理攔截目標類的方法。 |
Pointcut(切入點) | 指要對哪些 Joinpoint 進行攔截,即被攔截的連接點(方法)。 |
Advice(通知) | 指攔截到 Joinpoint 之後要做的事情,即對切入點增強的內容。 |
Target(目標) | 指代理的目標對象。 |
Weaving(植入) | 指把增強代碼應用到目標上,生成代理對象的過程。 |
Proxy(代理) | 指生成的代理對象。 |
Aspect(切面) | 切入點和通知的結合。 |
2. 動態代理
2.1 代理模式
爲其他對象提供一個代理以控制對某個對象的訪問,代理類不現實具體服務,而是利用委託類來完成服務,並將執行結果封裝處理。在Spring中被用來做無侵入的代碼增強。
和裝飾器模式有什麼不同?答:不會產生太多的裝飾類。
2.1.1 靜態代理
- 被代理類承擔、插入自己的方法。
- 創建一個代理類,持有被代理類(目標對象)的引用,實現接口(但具體業務由創建一個接口,被代理類(目標對象)實現接口。
顯然,一個代理類只能代理一個目標對象,會造成目標類的泛濫。這也是“靜態”的意思。
業務邏輯的接口:
public interface TargetInterface {
void doSomething();
}
目標對象:
public class TargetImpl implements TargetInterface{
@Override
public void doSomething() {
System.out.println("Hello World!");
}
}
代理類:
public class TargetProxy implements TargetInterface{
private TargetInterface target = new TargetImpl(); // 持有引用
@Override
public void doSomething() {
System.out.println("Before invoke" );
this.target.doSomething();
System.out.println("After invoke");
}
}
UML圖:
2.1.2 動態代理
- 目標接口和目標對象和靜態代理類一致。
- 運用反射來創建代理類。
代理類對象:
public class ProxyHandler implements InvocationHandler{
private Object object;
public ProxyHandler(Object object){
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before invoke");
method.invoke(object, args);
System.out.println("After invoke");
return null;
}
}
最後基於反射完成代理過程,詳見2.2小節:
InvocationHandler handler = new ProxyHandler(new TargetImpl());
TargetInterface targetProxy = (TargetInterface) Proxy.newProxyInstance(TargetImpl.getClassLoader(), TargetImpl.getInterfaces(), handler);
targetProxy.doSomething();
2.1.3 代理模式的缺點
- 靜態代理:由於靜態代理需要實現目標對象的相同接口,那麼可能會導致代理類會非常非常多,不好維護。
- 動態代理:目標對象一定是要有接口的,沒有接口就不能實現動態代理。
2.2 java.lang.reflect.Proxy
java.lang.reflect.Proxy
是基於反射的動態代理,是屬於JDK的原生實現。
2.2.1 實現Invoke接口、注入增強代碼
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
例如:
public class ProxyHandler implements InvocationHandler{
private Object object;
public ProxyHandler(Object object){
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before invoke"); // 增強代碼1
Object obj = method.invoke(object, args);
System.out.println("After invoke"); // 增強代碼2
return obj;
}
}
2.2.2 基於JDK的Proxy獲得代理對象
利用 static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler invocationHandler )
構建代理對象。其中各參數如下:
ClassLoader loader
:指定當前target對象使用類加載器,獲取加載器的方法是固定的,如TargetInterface.class.getClassLoader()
。Class<?>[] interfaces
:target對象實現的接口的類型,使用泛型方式確認類型,如new Class[] { TargetInterface.class}
。InvocationHandler invocationHandler
:事件處理,執行target對象的方法時,會觸發事件處理器的方法,會把當前執行target對象的方法作爲參數傳入。
2.3 CGLib
相較於基於JDK的動態代理仍有侷限性,即其目標對象必須要實現至少一個接口。而借用CGlib則不需要,其憑藉一個小而快的字節碼處理框架ASM轉換字節碼並生成新的類。由於其基於內存構建出一個子類來擴展目標對象的功能,也被稱爲“子類代理”。
需要注意的是,目標類不能爲不可繼承的
final
類型或目標對象的方法不能爲靜態類型。
public class TargetProxyFactory {
public static TargetProxy getProxyBean() {
// 1. 準備目標類和自定義的切面類(用於增強目標對象)
final Target goodsDao = new Target();
final Aspect aspect = new Aspect();
// 2. 構建CgLib的核心類`Enhancer`
Enhancer enhancer = new Enhancer();
// 3. 確定需要增強的類
enhancer.setSuperclass(goodsDao.getClass());
// 4. 添加回調函數:實現一個MethodInterceptor接口
enhancer.setCallback(() -> {
// intercept 相當於 jdk invoke,前三個參數與 jdk invoke—致
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
aspect.myBefore(); // 前增強
Object obj = method.invoke(goodsDao, args); // 目標方法執行
aspect.myAfter(); // 後增強
return obj;
}
});
// 5. 創建代理類
TargetProxy targetProxy = (TargetProxy) enhancer.create();
return targetProxy;
}
}
構建CGLib依賴的pom.xml
文件爲:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
3. Spring中的AOP
3.1 pom.xml文件
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<!-- Spring 2.0 以後,新增了對 AspectJ 方式的支持,新版本的 Spring 框架,建議使用 AspectJ 方式開發 AOP -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
3.2 基於通知類型代理增強的Bean
3.2.1 AOP通知類型
- 通知(Advice)其實就是對目標切入點進行增強的內容。
名稱 | 說明 |
---|---|
org.springframework.aop.MethodBeforeAdvice(前置通知) | 在方法之前自動執行的通知稱爲前置通知,可以應用於權限管理等功能。 |
org.springframework.aop.AfterReturningAdvice(後置通知) | 在方法之後自動執行的通知稱爲後置通知,可以應用於關閉流、上傳文件、刪除臨時文件等功能。 |
org.aopalliance.intercept.MethodInterceptor(環繞通知) | 在方法前後自動執行的通知稱爲環繞通知,可以應用於日誌、事務管理等功能。 |
org.springframework.aop.ThrowsAdvice(異常通知) | 在方法拋出異常時自動執行的通知稱爲異常通知,可以應用於處理異常記錄日誌等功能。 |
org.springframework.aop.IntroductionInterceptor(引介通知) | 在目標類中添加一些新的方法和屬性,可以應用於修改舊版本程序(增強類)。 |
3.2.2 示例:攔截器與工廠方法
現在,假設要增強 UserDao
,切入點是 save()
方法,要在之前加入自己面向 User
的增強方法,如校驗等切面業務。核心要點有:
- 目標對象要實現一個通用接口(除非使用CGlib)。
- 代碼增強類(切面類)實現一種通知的接口,在其中做增強。
- 在配置文件中利用
org.springframework.aop.framework.ProxyFactoryBean
創建代理類,需要給出proxyInterfaces
(目標對象的接口)、target
(目標對象的引用)、interceptorNames
(攔截器/切面類的名字)。
@Repository("userDao")
public class UserDao implements UserDaoInterface { // 實現一個通用接口
public void save(User user){
System.out.println("數據庫已保存" + user); // 業務代碼
}
}
代碼切面類:
public class UserDaoAspect implements MethodInterceptor { // 此處以環繞通知爲例子
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("Dao enhanced before"); // 增強1(重複的切入點代碼)
Object obj = methodInvocation.proceed(); // 這裏會由Spring替我們注入target對象
System.out.println("Dao enhanced after"); // 增強2(重複的切入點代碼)
return obj;
}
}
創建配置文件:
<beans>
<bean id="userDaoAspect" class="MVC.Aspect.UserDaoAspect"/> <!-- 攔截器/切面類-->
<bean id="targetUserDao" class="MVC.Model.Dao.UserDao"/> <!-- 目標類 -->
<bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--代理對象實現的接口、目標對象、攔截者(切面類) -->
<property name="proxyInterfaces" value="MVC.Model.Dao.UserDaoInterface"/>
<property name="target" ref="targetUserDao"/> <!-- 此處是一個引用ref -->
<property name="interceptorNames" value="userDaoAspect"/>
<!-- 如何生成代理,true:使用CGLib; false :使用JDK動態代理(默認) -->
<property name="proxyTargetClass" value="true"/>
</bean>
</beans>
其中, UserService
類需要進行修改:
@Service("userService")
public class UserService {
@Resource(name = "userDaoProxy") // 注入ProxyFactoryBean工廠方法獲得的代理類(增強類)
private UserDaoInterface userDao; // 修改爲其接口
public void service(User user){
System.out.println("MVC Service sth. with " + user);
this.userDao.save(user);
System.out.println("MVC Service Over.");
}
}
工程結構:
3.3 使用AspectJ開發AOP(推薦)
- AspectJ 是一個基於 Java 語言的 AOP 框架,它擴展了 Java 語言。Spring 2.0 以後,新增了對 AspectJ 方式的支持,新版本的 Spring 框架,建議使用 AspectJ 方式開發 AOP。
- 主要有兩種開發方法:“基於XML的聲明式開發”和“基於註解的聲明式開發”。
3.3.1 示例①:基於XML的聲明式開發
需要引入命名空間:
<beans xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
編寫切面類:
public class UserDaoAspect { // 以前、後通知爲例
public void asBefore() {
System.out.println("Dao enhanced before"); // 一些重複的代碼
}
public void asAfter() {
System.out.println("Dao enhanced after");
}
}
被增強的目標類(不再需要接口):
@Repository("userDao")
public class UserDao {
public void save(User user){
System.out.println("數據庫已保存" + user); // 業務代碼
}
}
編寫配置文件:
<beans>
<bean id="userDaoAspect" class="MVC.Aspect.UserDaoAspect"/> <!-- 切面類 -->
<bean id="targetUserDao" class="MVC.Model.Dao.UserDao"/> <!-- 目標類(隨後都會變成代理類) -->
<!-- 面向切面編程,交由Spring管理-->
<aop:config>
<!-- 配置切入點 -->
<aop:pointcut expression="execution(* MVC.Model.Dao.UserDao.save(..))" id="pointcut-save"/>
<!-- 對切入點配置切面 -->
<aop:aspect ref="userDaoAspect">
<!-- 配置前置增強 -->
<aop:before method="asBefore" pointcut-ref="pointcut-save" />
<aop:after method="asAfter" pointcut-ref="pointcut-save"/>
</aop:aspect>
</aop:config>
<!-- 如何生成代理,true:使用CGLib; false :使用JDK動態代理(默認)|如果目標類沒有聲明接口,則Spring將自動使用CGLib動態代理 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
注意此處
proxy-target-class="false"
的話注入會報錯...but was actually of type ‘com.sun.proxy.$Proxy7'
。
獲取增強類:
UserDao userDaoProxy = (UserDao) applicationContext.getBean("targetUserDao");
3.3.2 示例中的標籤及對應的切面類解析
附 <aop>
標籤格式概覽:
<aop:config>
<!-- 配置切入點,通知最後增強哪些方法 -->
<aop:pointcut expression="execution ( * target.* (..))" id="pointcut-id-x" />
<aop:aspect ref="myAspect">
<!--前置通知,關聯通知 Advice和切入點PointCut -->
<aop:before method="myBefore" pointeut-ref="pointcut-id-x" />
<!--後置通知,在方法返回之後執行,就可以獲得返回值returning 屬性 -->
<aop:after-returning method="myAfterReturning" pointcut-ref="pointcut-id-x" returning="returnVal" />
<!--環繞通知 -->
<aop:around method="myAround" pointcut-ref="pointcut-id-x" />
<!--拋出通知:用於處理程序發生異常,可以接收當前方法產生的異常 -->
<!-- * 注意:如果程序沒有異常,則不會執行增強 -->
<!-- * throwing屬性:用於設置通知第二個參數的名稱,類型Throwable -->
<aop:after-throwing method="myAfterThrowing" pointcut-ref="pointcut-id-x" throwing="e" />
<!--最終通知:無論程序發生任何事情,都將執行 -->
<aop:after method="myAfter" pointcut-ref="pointcut-id-x" />
</aop:aspect>
</aop:config>
對應的切面類:
//切面類
public class MyAspect {
// 前置通知
public void myBefore(JoinPoint joinPoint) {
System.out.print("前置通知,目標:" + joinPoint.getTarget() + " 方法名稱: " + joinPoint.getSignature().getName());
}
// 後置通知
public void myAfterReturning(JoinPoint joinPoint) {
System.out.print("後置通知,方法名稱:" + joinPoint.getSignature().getName());
}
// 環繞通知
public Object myAround(ProceedingJoinPoint proceedingJoinPoint)
throws Throwable {
System.out.println("環繞開始"); // 開始
Object obj = proceedingJoinPoint.proceed(); // 執行當前目標方法
System.out.println("環繞結束"); // 結束
return obj;
}
// 異常通知
public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
System.out.println("異常通知" + "出錯了" + e.getMessage());
}
// 最終通知
public void myAfter() {
System.out.println("最終通知");
}
}
3.3.3 示例②:基於註解的聲明式開發
名稱 | 說明 |
---|---|
@Aspect | 用於定義一個切面。 |
@Before | 用於定義前置通知,相當於 BeforeAdvice。 |
@AfterReturning | 用於定義後置通知,相當於 AfterReturningAdvice。 |
@Around | 用於定義環繞通知,相當於MethodInterceptor。 |
@AfterThrowing | 用於定義拋出通知,相當於ThrowAdvice。 |
@After | 用於定義最終final通知,不管是否異常,該通知都會執行。 |
@DeclareParents | 用於定義引介通知,相當於IntroductionInterceptor。 |
編寫配置文件:
<context:component-scan base-package="MVC"/> <!-- 掃描註解 -->
<aop:aspectj-autoproxy proxy-target-class="true"/> <!-- 使用CGlib植入切面代碼 -->
構建切面類:
@Aspect
@Component
public class UserDaoAspect {
@Pointcut("execution(* MVC.Model.Dao.UserDao.save(..))") // 配置切入點
private void pointCut(){} // 要求:方法必須是private,沒有值,名稱自定義,沒有參數
@Before("pointCut()")
public void asBefore() {
System.out.println("Dao enhanced before");
}
@After("pointCut()")
public void asAfter() {
System.out.println("Dao enhanced after");
}
}
3.4 一對多的切面及相關問題
3.4.1 實現多切面的有序運行
- 當有多個切面時,它不會存在任何順序,這些順序代碼會隨機生成,但是有時候我們希望它按照指定的順序運行。
- 此時,需要藉助
org.springframework.core.annotation.Order
註解類或實現org.springframework.core.Ordered
接口。
構建一個新的切面:
@Aspect
@Component
@Order(value = 2) // 會在第二個執行
public class UserDaoAspect2 {
@Pointcut("execution(* MVC.Model.Dao.UserDao.save(..))")
private void pointCut(){} // 要求:方法必須是private,沒有值,名稱自定義,沒有參數
@Before("pointCut()")
public void asBefore() {
System.out.println("Dao enhanced before 2");
}
@After("pointCut()")
public void asAfter() {
System.out.println("Dao enhanced after 2");
}
}
3.4.2 在註解中實現多個切入點
假設有一個新的業務需要被 UserDao
切入:
@Repository
public class ShopDao {
public void load(){
System.out.println("載入商品");
}
}
則 UserDao
需要修改爲:
@Aspect
@Component
public class UserDaoAspect {
@Pointcut("execution(* MVC.Model.Dao.UserDao.save(..))")
private void pointCut(){}
@Pointcut("execution(* MVC.Model.Dao.ShopDao.load())")
private void pointCut2(){} // 新的切入點
@Before("pointCut()")
public void asBefore() {
System.out.println("Dao enhanced before");
}
@After("pointCut() || pointCut2()") // 修改表達式語句,植入代碼
public void asAfter() {
System.out.println("Dao enhanced after");
}
}
3.4.3 獲取代理對象的目標對象
由於被CGLib植入之後,IoC容器中所有的目標對象都會變成代理對象,且Spring沒有提供獲取原生對象的API。
參考解決方法:CSDN@在spring中獲取代理對象代理的目標對象工具類。
import java.lang.reflect.Field;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.framework.AopProxy;
import org.springframework.aop.support.AopUtils;
public class AopTargetUtils {
public static Object getTarget(Object proxy) throws Exception {
return !AopUtils.isAopProxy(proxy) ? proxy :
(AopUtils.isJdkDynamicProxy(proxy) ? getJDKDynamicProxyTargetObject(proxy) : getCGlibProxyTargetObject(proxy))
}
// 獲取CGLib 代理的對象
private static Object getCGlibProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
h.setAccessible(true);
Object dynamicAdvisedInterceptor = h.get(proxy);
Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
advised.setAccessible(true);
return ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
}
// 獲取JDK代理的對象
private static Object getJDKDynamicProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
h.setAccessible(true);
AopProxy aopProxy = (AopProxy) h.get(proxy);
Field advised = aopProxy.getClass().getDeclaredField("advised");
advised.setAccessible(true);
return ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();
}
}
3.5 切入點表達式
切入點表達式爲:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
符號講解:
?
號代表0或1,表明可選參數。*
號代表任意類型取0或多,常用作通配符。
表達式匹配參數講解:
modifiers-pattern?
:【可選】連接點的類型。ret-type-pattern
:【必填】連接點返回值類型,常用*
做匹配。declaring-type-pattern?
:【可選】連接點的類型(包.類
),如com.example.User
,通常不省略。name-pattern(param-pattern)
:【必填】要匹配的連接點名稱,即方法
(如果給出了連接點的類型,要用.
隔開),如save(..)
;括號裏面是方法的參數(匹配方法見下)。throws-pattern?
:【可選】連接點拋出的異常類型。
方法參數****的匹配方法:
()
匹配不帶參數的方法。(..)
匹配帶參數的方法(任意個)。(*, String)
匹配帶兩個參數的方法且第二個必爲String。
3.6 PointCut指示符
除了使用 execution
作爲切入點表達式進行配置,還可以使用以下表達式內容(需要保證所有的連接點都在IoC容器內):
within
:匹配所有在指定子包內的類的連接點,如within(com.xyz.service.*)
、within(com.xyz.service..*)
;嚴格匹配目標對象,不理會繼承關係 。this
: 匹配所有代理對象爲目標類型中的連接點,如this(com.xyz.service.AccountService)
。target
:匹配所有實現了指定接口的目標對象中的連接點,如target(com.xyz.service.UserDaoInterfece)
。bean
:匹配所有指定命名方式的類的連接點,如bean(userDao)
。args
:匹配任何方法參數是指定的類型的連接點,如args(*, java.lang.String)
、args(java.lang.Long, ..)
。@within
:匹配標註有指定註解的類(不可爲接口)的所有連接點(要求註解的Retention級別爲CLASS),如@within(com.google.common.annotations.Beta)
;對子類不起效,除非使用@within(xxxx)+
或者子類中繼承的方法未進行重載。@target
:匹配標註有指定註解的類(不可爲接口)的所有連接點(要求註解的Retention級別爲RUNTIME),如@target(org.springframework.stereotype.Repository)
;對子類不起效。@args
:匹配傳入的參數類標註有指定註解的所有連接點,如@args(org.springframework.stereotype.Repository)
。@anntation
:匹配所有標註有指定註解的連接點,如@annotation(com.aop.annotation.AdminOnly)
。
除此之外,表達式還可以用 &&
、 ||
、 !
進行合併,詳見3.4.2小節。
@within 和 @target的區別:
@within
:若當前類有註解,則該類對父類重載及自有方法被攔截。子類中未對父類的方法進行重載時,亦被攔截。@target
:若當前類有註解,則該類對父類繼承、重載及自有的方法被攔截;對子類不起效。