作者:小傅哥
部落格:https://bugstack.cn
沉澱、分享、成長,讓自己和他人都能有所收穫!?
一、前言
為什麼,你的程式碼總是糊到豬圈上?
?怎麼辦,知道你在網際網路,不知道你在哪個大廠。知道你在加班,不知道你在和哪個產品爭辯。知道你在偷懶,不知道你要摸魚到幾點。知道你在搬磚,不知道你在蓋哪個豬圈。
當你特別辛苦夜以繼日的完成著,每天、每週、每月重複性的工作時,你能獲得的成長是最小,得到的回報也是少的。留著最多的汗、拿著最少的錢
可能你一激動開始看原始碼,但不知道看完的原始碼能用到什麼地方。看設計模式,看的時候懂,但改自己的程式碼又下不去手。其實一方面是本身技術棧的知識面不足,另外一方面是自己儲備的程式碼也不夠。最終也就導致根本沒法把一些列的知識串聯起來,就像你看了 HashMap,但也聯想不到分庫分表元件中的資料雜湊也會用到了 HashMap 中的擾動函式思想和泊松分佈驗證
、看了Spring 原始碼,也讀不出來 Mybatis 是如何解決只定義 Dao 介面就能使用配置或者註解對資料庫進行 CRUD 操作
、看來 JDK 的動態代理,也想不到 AOP 是如何設計的
。所以成體系學習,加強技術棧知識的完整性,才能更好的用上這些學習到的編碼能力。
二、目標
到本章節我們將要從 IOC 的實現,轉入到關於 AOP(Aspect Oriented Programming
) 內容的開發。在軟體行業,AOP 意為:面向切面程式設計,通過預編譯的方式和執行期間動態代理實現程式功能功能的統一維護。其實 AOP 也是 OOP 的延續,在 Spring 框架中是一個非常重要的內容,使用 AOP 可以對業務邏輯的各個部分進行隔離,從而使各模組間的業務邏輯耦合度降低,提高程式碼的可複用性,同時也能提高開發效率。
關於 AOP 的核心技術實現主要是動態代理的使用,就像你可以給一個介面的實現類,使用代理的方式替換掉這個實現類,使用代理類來處理你需要的邏輯。比如:
@Test
public void test_proxy_class() {
IUserService userService = (IUserService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{IUserService.class}, (proxy, method, args) -> "你被代理了!");
String result = userService.queryUserInfo();
System.out.println("測試結果:" + result);
}
代理類的實現基本都大家都見過,那麼有了一個基本的思路後,接下來就需要考慮下怎麼給方法做代理呢,而不是代理類。另外怎麼去代理所有符合某些規則的所有類中方法呢。如果可以代理掉所有類的方法,就可以做一個方法攔截器,給所有被代理的方法新增上一些自定義處理,比如列印日誌、記錄耗時、監控異常等。
三、方案
在把 AOP 整個切面設計融合到 Spring 前,我們需要解決兩個問題,包括:如何給符合規則的方法做代理
,以及做完代理方法的案例後,把類的職責拆分出來
。而這兩個功能點的實現,都是以切面的思想進行設計和開發。如果不是很清楚 AOP 是啥,你可以把切面理解為用刀切韭菜,一根一根切總是有點慢,那麼用手(代理
)把韭菜捏成一把,用菜刀或者斧頭這樣不同的攔截操作來處理。而程式中其實也是一樣,只不過韭菜變成了方法,菜刀變成了攔截方法。整體設計結構如下圖:
- 就像你在使用 Spring 的 AOP 一樣,只處理一些需要被攔截的方法。在攔截方法後,執行你對方法的擴充套件操作。
- 那麼我們就需要先來實現一個可以代理方法的 Proxy,其實代理方法主要是使用到方法攔截器類處理方法的呼叫
MethodInterceptor#invoke
,而不是直接使用 invoke 方法中的入參 Method method 進行method.invoke(targetObj, args)
這塊是整個使用時的差異。 - 除了以上的核心功能實現,還需要使用到
org.aspectj.weaver.tools.PointcutParser
處理攔截表示式"execution(* cn.bugstack.springframework.test.bean.IUserService.*(..))"
,有了方法代理和處理攔截,我們就可以完成設計出一個 AOP 的雛形了。
四、實現
1. 工程結構
small-spring-step-11
└── src
├── main
│ └── java
│ └── cn.bugstack.springframework
│ ├── aop
│ │ ├── aspectj
│ │ │ └── AspectJExpressionPointcut.java
│ │ ├── framework
│ │ │ ├── AopProxy.java
│ │ │ ├── Cglib2AopProxy.java
│ │ │ ├── JdkDynamicAopProxy.java
│ │ │ └── ReflectiveMethodInvocation.java
│ │ ├── AdvisedSupport.java
│ │ ├── ClassFilter.java
│ │ ├── MethodMatcher.java
│ │ ├── Pointcut.java
│ │ └── TargetSource.java
│ ├── beans
│ │ ├── factory
│ │ │ ├── config
│ │ │ │ ├── AutowireCapableBeanFactory.java
│ │ │ │ ├── BeanDefinition.java
│ │ │ │ ├── BeanFactoryPostProcessor.java
│ │ │ │ ├── BeanPostProcessor.java
│ │ │ │ ├── BeanReference.java
│ │ │ │ ├── ConfigurableBeanFactory.java
│ │ │ │ └── SingletonBeanRegistry.java
│ │ │ ├── support
│ │ │ │ ├── AbstractAutowireCapableBeanFactory.java
│ │ │ │ ├── AbstractBeanDefinitionReader.java
│ │ │ │ ├── AbstractBeanFactory.java
│ │ │ │ ├── BeanDefinitionReader.java
│ │ │ │ ├── BeanDefinitionRegistry.java
│ │ │ │ ├── CglibSubclassingInstantiationStrategy.java
│ │ │ │ ├── DefaultListableBeanFactory.java
│ │ │ │ ├── DefaultSingletonBeanRegistry.java
│ │ │ │ ├── DisposableBeanAdapter.java
│ │ │ │ ├── FactoryBeanRegistrySupport.java
│ │ │ │ ├── InstantiationStrategy.java
│ │ │ │ └── SimpleInstantiationStrategy.java
│ │ │ ├── support
│ │ │ │ └── XmlBeanDefinitionReader.java
│ │ │ ├── Aware.java
│ │ │ ├── BeanClassLoaderAware.java
│ │ │ ├── BeanFactory.java
│ │ │ ├── BeanFactoryAware.java
│ │ │ ├── BeanNameAware.java
│ │ │ ├── ConfigurableListableBeanFactory.java
│ │ │ ├── DisposableBean.java
│ │ │ ├── FactoryBean.java
│ │ │ ├── HierarchicalBeanFactory.java
│ │ │ ├── InitializingBean.java
│ │ │ └── ListableBeanFactory.java
│ │ ├── BeansException.java
│ │ ├── PropertyValue.java
│ │ └── PropertyValues.java
│ ├── context
│ │ ├── event
│ │ │ ├── AbstractApplicationEventMulticaster.java
│ │ │ ├── ApplicationContextEvent.java
│ │ │ ├── ApplicationEventMulticaster.java
│ │ │ ├── ContextClosedEvent.java
│ │ │ ├── ContextRefreshedEvent.java
│ │ │ └── SimpleApplicationEventMulticaster.java
│ │ ├── support
│ │ │ ├── AbstractApplicationContext.java
│ │ │ ├── AbstractRefreshableApplicationContext.java
│ │ │ ├── AbstractXmlApplicationContext.java
│ │ │ ├── ApplicationContextAwareProcessor.java
│ │ │ └── ClassPathXmlApplicationContext.java
│ │ ├── ApplicationContext.java
│ │ ├── ApplicationContextAware.java
│ │ ├── ApplicationEvent.java
│ │ ├── ApplicationEventPublisher.java
│ │ ├── ApplicationListener.java
│ │ └── ConfigurableApplicationContext.java
│ ├── core.io
│ │ ├── ClassPathResource.java
│ │ ├── DefaultResourceLoader.java
│ │ ├── FileSystemResource.java
│ │ ├── Resource.java
│ │ ├── ResourceLoader.java
│ │ └── UrlResource.java
│ └── utils
│ └── ClassUtils.java
└── test
└── java
└── cn.bugstack.springframework.test
├── bean
│ ├── IUserService.java
│ ├── UserService.java
│ └── UserServiceInterceptor.java
└── ApiTest.java
工程原始碼:公眾號「bugstack蟲洞棧」,回覆:Spring 專欄,獲取完整原始碼
AOP 切點表示式和使用以及基於 JDK 和 CGLIB 的動態代理類關係,如圖 12-2
- 整個類關係圖就是 AOP 實現核心邏輯的地方,上面部分是關於方法的匹配實現,下面從 AopProxy 開始是關於方法的代理操作。
- AspectJExpressionPointcut 的核心功能主要依賴於 aspectj 元件並處理 Pointcut、ClassFilter,、MethodMatcher 介面實現,專門用於處理類和方法的匹配過濾操作。
- AopProxy 是代理的抽象物件,它的實現主要是基於 JDK 的代理和 Cglib 代理。在前面章節關於物件的例項化 CglibSubclassingInstantiationStrategy,我們也使用過 Cglib 提供的功能。
2. 代理方法案例
在實現 AOP 的核心功能之前,我們先做一個代理方法的案例,通過這樣一個可以概括代理方法的核心全貌,可以讓大家更好的理解後續拆解各個方法,設計成解耦功能的 AOP 實現過程。
單元測試
@Test
public void test_proxy_method() {
// 目標物件(可以替換成任何的目標物件)
Object targetObj = new UserService();
// AOP 代理
IUserService proxy = (IUserService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), targetObj.getClass().getInterfaces(), new InvocationHandler() {
// 方法匹配器
MethodMatcher methodMatcher = new AspectJExpressionPointcut("execution(* cn.bugstack.springframework.test.bean.IUserService.*(..))");
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (methodMatcher.matches(method, targetObj.getClass())) {
// 方法攔截器
MethodInterceptor methodInterceptor = invocation -> {
long start = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
System.out.println("監控 - Begin By AOP");
System.out.println("方法名稱:" + invocation.getMethod().getName());
System.out.println("方法耗時:" + (System.currentTimeMillis() - start) + "ms");
System.out.println("監控 - End\r\n");
}
};
// 反射呼叫
return methodInterceptor.invoke(new ReflectiveMethodInvocation(targetObj, method, args));
}
return method.invoke(targetObj, args);
}
});
String result = proxy.queryUserInfo();
System.out.println("測試結果:" + result);
}
- 首先整個案例的目標是給一個 UserService 當成目標物件,對類中的所有方法進行攔截新增監控資訊列印處理。
- 從案例中你可以看到有代理的實現 Proxy.newProxyInstance,有方法的匹配 MethodMatcher,有反射的呼叫 invoke(Object proxy, Method method, Object[] args),也用使用者自己攔截方法後的操作。這樣一看其實和我們使用的 AOP 就非常類似了,只不過你在使用 AOP 的時候是框架已經提供更好的功能,這裡是把所有的核心過程給你展示出來了。
測試結果
監控 - Begin By AOP
方法名稱:queryUserInfo
方法耗時:86ms
監控 - End
測試結果:小傅哥,100001,深圳
Process finished with exit code 0
- 從測試結果可以看到我們已經對 UserService#queryUserInfo 方法進行了攔截監控操作,其實後面我們實現的 AOP 就是現在體現出的結果,只不過我們需要把這部分測試的案例解耦為更具有擴充套件性的各個模組實現。
拆解案例
- 拆解過程可以參考截圖 12-3,我們需要把代理物件拆解出來,因為它可以是 JDK 的實現也可以是 Cglib 的處理。
- 方法匹配器操作其實已經是一個單獨的實現類了,不過我們還需要把傳入的目標物件、方法匹配、攔截方法,都進行統一的包裝,方便外部呼叫時進行一個入參透傳。
- 最後其實是
ReflectiveMethodInvocation
的使用,它目前已經是實現MethodInvocation
介面的一個包裝後的類,引數資訊包括:呼叫的物件、呼叫的方法、呼叫的入參。
3. 切點表示式
定義介面
cn.bugstack.springframework.aop.Pointcut
public interface Pointcut {
/**
* Return the ClassFilter for this pointcut.
* @return the ClassFilter (never <code>null</code>)
*/
ClassFilter getClassFilter();
/**
* Return the MethodMatcher for this pointcut.
* @return the MethodMatcher (never <code>null</code>)
*/
MethodMatcher getMethodMatcher();
}
- 切入點介面,定義用於獲取 ClassFilter、MethodMatcher 的兩個類,這兩個介面獲取都是切點表示式提供的內容。
cn.bugstack.springframework.aop.ClassFilter
public interface ClassFilter {
/**
* Should the pointcut apply to the given interface or target class?
* @param clazz the candidate target class
* @return whether the advice should apply to the given target class
*/
boolean matches(Class<?> clazz);
}
- 定義類匹配類,用於切點找到給定的介面和目標類。
cn.bugstack.springframework.aop.MethodMatcher
public interface MethodMatcher {
/**
* Perform static checking whether the given method matches. If this
* @return whether or not this method matches statically
*/
boolean matches(Method method, Class<?> targetClass);
}
- 方法匹配,找到表示式範圍內匹配下的目標類和方法。在上文的案例中有所體現:
methodMatcher.matches(method, targetObj.getClass())
實現切點表示式類
public class AspectJExpressionPointcut implements Pointcut, ClassFilter, MethodMatcher {
private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<PointcutPrimitive>();
static {
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
}
private final PointcutExpression pointcutExpression;
public AspectJExpressionPointcut(String expression) {
PointcutParser pointcutParser = PointcutParser.getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution(SUPPORTED_PRIMITIVES, this.getClass().getClassLoader());
pointcutExpression = pointcutParser.parsePointcutExpression(expression);
}
@Override
public boolean matches(Class<?> clazz) {
return pointcutExpression.couldMatchJoinPointsInType(clazz);
}
@Override
public boolean matches(Method method, Class<?> targetClass) {
return pointcutExpression.matchesMethodExecution(method).alwaysMatches();
}
@Override
public ClassFilter getClassFilter() {
return this;
}
@Override
public MethodMatcher getMethodMatcher() {
return this;
}
}
- 切點表示式實現了 Pointcut、ClassFilter、MethodMatcher,三個介面定義方法,同時這個類主要是對 aspectj 包提供的表示式校驗方法使用。
- 匹配 matches:
pointcutExpression.couldMatchJoinPointsInType(clazz)
、pointcutExpression.matchesMethodExecution(method).alwaysMatches()
,這部分內容可以單獨測試驗證。
匹配驗證
@Test
public void test_aop() throws NoSuchMethodException {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut("execution(* cn.bugstack.springframework.test.bean.UserService.*(..))");
Class<UserService> clazz = UserService.class;
Method method = clazz.getDeclaredMethod("queryUserInfo");
System.out.println(pointcut.matches(clazz));
System.out.println(pointcut.matches(method, clazz));
// true、true
}
- 這裡單獨提供出來一個匹配方法的驗證測試,可以看看你攔截的方法與對應的物件是否匹配。
4. 包裝切面通知資訊
cn.bugstack.springframework.aop.AdvisedSupport
public class AdvisedSupport {
// 被代理的目標物件
private TargetSource targetSource;
// 方法攔截器
private MethodInterceptor methodInterceptor;
// 方法匹配器(檢查目標方法是否符合通知條件)
private MethodMatcher methodMatcher;
// ...get/set
}
- AdvisedSupport,主要是用於把代理、攔截、匹配的各項屬性包裝到一個類中,方便在 Proxy 實現類進行使用。這和你的業務開發中包裝入參是一個道理
- TargetSource,是一個目標物件,在目標物件類中提供 Object 入參屬性,以及獲取目標類 TargetClass 資訊。
- MethodInterceptor,是一個具體攔截方法實現類,由使用者自己實現 MethodInterceptor#invoke 方法,做具體的處理。像我們本文的案例中是做方法監控處理
- MethodMatcher,是一個匹配方法的操作,這個物件由 AspectJExpressionPointcut 提供服務。
5. 代理抽象實現(JDK&Cglib)
定義介面
cn.bugstack.springframework.aop.framework
public interface AopProxy {
Object getProxy();
}
- 定義一個標準介面,用於獲取代理類。因為具體實現代理的方式可以有 JDK 方式,也可以是 Cglib 方式,所以定義介面會更加方便管理實現類。
cn.bugstack.springframework.aop.framework.JdkDynamicAopProxy
public class JdkDynamicAopProxy implements AopProxy, InvocationHandler {
private final AdvisedSupport advised;
public JdkDynamicAopProxy(AdvisedSupport advised) {
this.advised = advised;
}
@Override
public Object getProxy() {
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), advised.getTargetSource().getTargetClass(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (advised.getMethodMatcher().matches(method, advised.getTargetSource().getTarget().getClass())) {
MethodInterceptor methodInterceptor = advised.getMethodInterceptor();
return methodInterceptor.invoke(new ReflectiveMethodInvocation(advised.getTargetSource().getTarget(), method, args));
}
return method.invoke(advised.getTargetSource().getTarget(), args);
}
}
- 基於 JDK 實現的代理類,需要實現介面 AopProxy、InvocationHandler,這樣就可以把代理物件 getProxy 和反射呼叫方法 invoke 分開處理了。
- getProxy 方法中的是代理一個物件的操作,需要提供入參 ClassLoader、AdvisedSupport、和當前這個類 this,因為這個類提供了 invoke 方法。
- invoke 方法中主要處理匹配的方法後,使用使用者自己提供的方法攔截實現,做反射呼叫 methodInterceptor.invoke 。
- 這裡還有一個 ReflectiveMethodInvocation,其他它就是一個入參的包裝資訊,提供了入參物件:目標物件、方法、入參。
cn.bugstack.springframework.aop.framework.Cglib2AopProxy
public class Cglib2AopProxy implements AopProxy {
private final AdvisedSupport advised;
public Cglib2AopProxy(AdvisedSupport advised) {
this.advised = advised;
}
@Override
public Object getProxy() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(advised.getTargetSource().getTarget().getClass());
enhancer.setInterfaces(advised.getTargetSource().getTargetClass());
enhancer.setCallback(new DynamicAdvisedInterceptor(advised));
return enhancer.create();
}
private static class DynamicAdvisedInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
CglibMethodInvocation methodInvocation = new CglibMethodInvocation(advised.getTargetSource().getTarget(), method, objects, methodProxy);
if (advised.getMethodMatcher().matches(method, advised.getTargetSource().getTarget().getClass())) {
return advised.getMethodInterceptor().invoke(methodInvocation);
}
return methodInvocation.proceed();
}
}
private static class CglibMethodInvocation extends ReflectiveMethodInvocation {
@Override
public Object proceed() throws Throwable {
return this.methodProxy.invoke(this.target, this.arguments);
}
}
}
- 基於 Cglib 使用 Enhancer 代理的類可以在執行期間為介面使用底層 ASM 位元組碼增強技術處理物件的代理物件生成,因此被代理類不需要實現任何介面。
- 關於擴充套件進去的使用者攔截方法,主要是在 Enhancer#setCallback 中處理,使用者自己的新增的攔截處理。這裡可以看到 DynamicAdvisedInterceptor#intercept 匹配方法後做了相應的反射操作。
五、測試
1. 事先準備
public class UserService implements IUserService {
public String queryUserInfo() {
try {
Thread.sleep(new Random(1).nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
return "小傅哥,100001,深圳";
}
public String register(String userName) {
try {
Thread.sleep(new Random(1).nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
return "註冊使用者:" + userName + " success!";
}
}
- 在 UserService 中提供了2個不同方法,另外你還可以增加新的類來加入測試。後面我們的測試過程,會給這個兩個方法新增我們的攔截處理,列印方法執行耗時。
2. 自定義攔截方法
public class UserServiceInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
long start = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
System.out.println("監控 - Begin By AOP");
System.out.println("方法名稱:" + invocation.getMethod());
System.out.println("方法耗時:" + (System.currentTimeMillis() - start) + "ms");
System.out.println("監控 - End\r\n");
}
}
}
- 使用者自定義的攔截方法需要實現 MethodInterceptor 介面的 invoke 方法,使用方式與 Spring AOP 非常相似,也是包裝 invocation.proceed() 放行,並在 finally 中新增監控資訊。
3. 單元測試
@Test
public void test_dynamic() {
// 目標物件
IUserService userService = new UserService();
// 組裝代理資訊
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTargetSource(new TargetSource(userService));
advisedSupport.setMethodInterceptor(new UserServiceInterceptor());
advisedSupport.setMethodMatcher(new AspectJExpressionPointcut("execution(* cn.bugstack.springframework.test.bean.IUserService.*(..))"));
// 代理物件(JdkDynamicAopProxy)
IUserService proxy_jdk = (IUserService) new JdkDynamicAopProxy(advisedSupport).getProxy();
// 測試呼叫
System.out.println("測試結果:" + proxy_jdk.queryUserInfo());
// 代理物件(Cglib2AopProxy)
IUserService proxy_cglib = (IUserService) new Cglib2AopProxy(advisedSupport).getProxy();
// 測試呼叫
System.out.println("測試結果:" + proxy_cglib.register("花花"));
}
- 整個案例測試了 AOP 在於 Spring 結合前的核心程式碼,包括什麼是目標物件、怎麼組裝代理資訊、如何呼叫代理物件。
- AdvisedSupport,包裝了目標物件、使用者自己實現的攔截方法以及方法匹配表示式。
- 之後就是分別呼叫 JdkDynamicAopProxy、Cglib2AopProxy,兩個不同方式實現的代理類,看看是否可以成功攔截方法
測試結果
監控 - Begin By AOP
方法名稱:public abstract java.lang.String cn.bugstack.springframework.test.bean.IUserService.queryUserInfo()
方法耗時:86ms
監控 - End
測試結果:小傅哥,100001,深圳
監控 - Begin By AOP
方法名稱:public java.lang.String cn.bugstack.springframework.test.bean.UserService.register(java.lang.String)
方法耗時:97ms
監控 - End
測試結果:註冊使用者:花花 success!
Process finished with exit code 0
- 如 AOP 功能定義一樣,我們可以通過這樣的代理方式、方法匹配和攔截後,在對應的目標方法下,做了攔截操作進行監控資訊列印。
六、總結
- 從本文對 Proxy#newProxyInstance、MethodInterceptor#invoke,的使用驗證切面核心原理以及再把功能拆解到 Spring 框架實現中,可以看到一個貌似複雜的技術其實核心內容往往沒有太多,但因為需要為了滿足後續更多的擴充套件就需要進行職責解耦和包裝,通過這樣設計模式的使用,以此讓呼叫方能更加簡化,自身也可以不斷按需擴充套件。
- AOP 的功能實現目前還沒有與 Spring 結合,只是對切面技術的一個具體實現,你可以先學習到如何處理代理物件、過濾方法、攔截方法,以及使用 Cglib 和 JDK 代理的區別,其實這與的技術不只是在 Spring 框架中有所體現,在其他各類需要減少人工硬編碼的場景下,都會用到。比如RPC、Mybatis、MQ、分散式任務
- 一些核心技術的使用上,都是具有很強的關聯性的,它們也不是孤立存在的。而這個能把整個技術棧串聯起來的過程,需要你來大量的學習、積累、由點到面的鋪設,才能在一個知識點的學習擴充到一個知識面和知識體系的建設。