前言
AOP全稱是Aspect Oriented Programming,叫做面向切面程式設計,和麵向物件程式設計(OOP)一樣也是一種程式設計思想,也是spring中一個重要的部分。
其實現基於代理模式,對原來的業務進行增強。比如說原來的功能是增刪改查,想要不修改原始碼的情況下增強原來的功能,那麼就可以對原來的業務類生成一個代理的物件,在代理物件中實現方法對原來的業務增強。
而代理又分靜態代理和動態代理,通常我們都是用動態代理,因為靜態代理都是硬編碼,不適合拿來用在實現框架這種需求裡。在java中通常有兩種代理方式,一個是jdk自帶的代理,另一個是cglib實現的代理方式,這兩個代理各有特點,不大瞭解的話可以自行查詢資料看看。
在spring的底層這兩種代理方式都支援,在預設的情況下,如果bean實現了一個介面,spring會使用jdk代理,否則就用cglib代理。
在doodle框架裡用了cglib代理的方式,因為這種方式代理的類不用實現介面,實現更靈活
實現準備
在具體實現AOP功能前,先做一些準備。
因為cglib代理不是jdk自帶的,所以先在pom.xml引入cglib。
<properties>
...
<cglib.version>3.2.6</cglib.version>
</properties>
<dependencies>
...
<!-- cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>${cglib.version}</version>
</dependency>
</dependencies>
複製程式碼
然後在zbw.aop包下建立一個annotation包,然後再建立一個Aspect
註解。這個註解是用於標記在''切面''中,即實現代理功能的類上面。
package com.zbw.aop.annotation;
import ...;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Aspect {
/**
* 目標代理類的範圍
*/
Class<? extends Annotation> target();
}
複製程式碼
接著在zbw.aop包下建立一個advice包,這個包下放一系列的通知介面(Advice)。其中包括:
- 基礎通知介面
Advice
,所有通知介面都要繼承這個介面 - 前置通知介面
MethodBeforeAdvice
,繼承這個通知介面並實現其前置方法,可以前置增強目標類,即目標方法執行前會先執行這個前置方法。 - 後置通知介面
AfterReturningAdvice
,繼承這個通知介面並實現其返回後方法,可以後置增強目標類,即目標方法執後並放回結果時,會執行這個返回方法。 - 異常通知介面
ThrowsAdvice
,繼承這個通知介面並實現其異常方法,可以增強目標類的異常,即目標方法發生異常時,會執行這個異常方法。 - 環繞通知介面
AroundAdvice
,這個介面繼承了MethodBeforeAdvice
,AfterReturningAdvice
,ThrowsAdvice
這三個介面,相當於這三個介面的合集。
在spring中還有其他幾種的通知,這裡暫時就不一一實現,我們就實現這幾種相對來說最常用的。
/**
* 通知介面
*/
public interface Advice {
}
/**
* 前置通知介面
*/
public interface MethodBeforeAdvice extends Advice {
/**
* 前置方法
*/
void before(Class<?> clz, Method method, Object[] args) throws Throwable;
}
/**
* 返回通知介面
*/
public interface AfterReturningAdvice extends Advice {
/**
* 返回後方法
*/
void afterReturning(Class<?> clz, Object returnValue, Method method, Object[] args) throws Throwable;
}
/**
* 異常通知介面
*/
public interface ThrowsAdvice extends Advice {
/**
* 異常方法
*/
void afterThrowing(Class<?> clz, Method method, Object[] args, Throwable e);
}
/**
* 環繞通知介面
*/
public interface AroundAdvice extends MethodBeforeAdvice, AfterReturningAdvice, ThrowsAdvice {
}
複製程式碼
實現AOP
剛才實現了幾種通知介面,我們先將這些通知介面使用起來,實現代理類。
package com.zbw.aop;
import ...
/**
* 代理通知類
*/
@Slf4j
@AllArgsConstructor
@NoArgsConstructor
@Data
public class ProxyAdvisor {
/**
* 通知
*/
private Advice advice;
/**
* 執行代理方法
*/
public Object doProxy(Object target, Class<?> targetClass, Method method, Object[] args, MethodProxy proxy) throws Throwable {
Object result = null;
if (advice instanceof MethodBeforeAdvice) {
((MethodBeforeAdvice) advice).before(targetClass, method, args);
}
try {
//執行目標類的方法
result = proxy.invokeSuper(target, args);
if (advice instanceof AfterReturningAdvice) {
((AfterReturningAdvice) advice).afterReturning(targetClass, result, method, args);
}
} catch (Exception e) {
if (advice instanceof ThrowsAdvice) {
((ThrowsAdvice) advice).afterThrowing(targetClass, method, args, e);
} else {
throw new Throwable(e);
}
}
return result;
}
}
複製程式碼
這個類就是代理類ProxyAdvisor
,即到時候我們的目標類執行的時候,實際上就是執行我們這個代理類。在ProxyAdvisor
中有屬性Advice
便是剛才編寫的通知介面,然後在目標方法執行的時候,就會執行doProxy()
方法,通過判定Advice
介面的型別來執行在介面中實現的方法。
執行的順序就是 MethodBeforeAdvice@before() -> MethodProxy@invokeSuper() -> AfterReturningAdvice@afterReturning(),如果目標方法出現異常則會執行ThrowsAdvice@afterThrowing()方法。
接下來就是實現AOP的執行器
package com.zbw.aop;
import ...
/**
* Aop執行器
*/
@Slf4j
public class Aop {
/**
* Bean容器
*/
private BeanContainer beanContainer;
public Aop() {
beanContainer = BeanContainer.getInstance();
}
public void doAop() {
beanContainer.getClassesBySuper(Advice.class)
.stream()
.filter(clz -> clz.isAnnotationPresent(Aspect.class))
.forEach(clz -> {
final Advice advice = (Advice) beanContainer.getBean(clz);
Aspect aspect = clz.getAnnotation(Aspect.class);
beanContainer.getClassesByAnnotation(aspect.target())
.stream()
.filter(target -> !Advice.class.isAssignableFrom(target))
.filter(target -> !target.isAnnotationPresent(Aspect.class))
.forEach(target -> {
ProxyAdvisor advisor = new ProxyAdvisor(advice);
Object proxyBean = ProxyCreator.createProxy(target, advisor);
beanContainer.addBean(target, proxyBean);
});
});
}
}
複製程式碼
和上一節實現IOC的執行器的時候類似,先在AOP執行器的建構函式獲取到單例化得BeanContainer容器。
然後在doAop()
方法中實現AOP功能。
- 遍歷在BeanContainer容器被
Aspect
註解的Bean,並找到實現了Advice
介面的類,這些類便是切面 - 獲取切面上的註解
Aspect
的target()
的值,這個值就是要被代理的類的註解。比如說有個切面的註解為@Aspect(target = Controller.class)
,那麼這個切面會作用在被Controller
註解的類上。 - 遍歷BeanContainer容器被
aspect.target()的值
註解的Bean,找到目標代理類 - 建立
ProxyAdvisor
代理類並通過cglib建立出這個代理類的例項,並把這個類例項放回到BeanContainer容器中。
在方法中有一個代理類創造器ProxyCreator
,他就是通過cglib來建立代理類的,最後實現一下這個創造器。
package com.zbw.aop;
import ...
/**
* 代理類建立器
*/
public final class ProxyCreator {
/**
* 建立代理類
*/
public static Object createProxy(Class<?> targetClass, ProxyAdvisor proxyAdvisor) {
return Enhancer.create(targetClass,
(MethodInterceptor) (target, method, args, proxy) ->
proxyAdvisor.doProxy(target, targetClass, method, args, proxy));
}
}
複製程式碼
以上我們最基本的AOP功能就實現了,但是目前來說,我們的Advice實現類是不會被Bean容器BeanContainer
載入的,所有要在Bean容器的BEAN_ANNOTATION
屬性新增@Aspect註解
(感謝無邪丶的指正)
//BeanContainer
...
/**
* 載入bean的註解列表
*/
private static final List<Class<? extends Annotation>> BEAN_ANNOTATION
= Arrays.asList(Component.class, Controller.class, Service.class, Repository.class,Aspect.class);
...
複製程式碼
測試用例
在上一篇文章從零開始實現一個簡易的Java MVC框架(三)--實現IOC中的測試用例的基礎上,在實現一個DoodleAspect
切面,這切面實現了AroundAdvice
的通知介面並實現其中的三個方法。
package com.zbw.bean;
import ...
@Slf4j
@Aspect(target = Controller.class)
public class DoodleAspect implements AroundAdvice {
@Override
public void before(Class<?> clz, Method method, Object[] args) throws Throwable {
log.info("Before DoodleAspect ----> class: {}, method: {}", clz.getName(), method.getName());
}
@Override
public void afterReturning(Class<?> clz, Object returnValue, Method method, Object[] args) throws Throwable {
log.info("After DoodleAspect ----> class: {}, method: {}", clz, method.getName());
}
@Override
public void afterThrowing(Class<?> clz, Method method, Object[] args, Throwable e) {
log.error("Error DoodleAspect ----> class: {}, method: {}, exception: {}", clz, method.getName(), e.getMessage());
}
}
複製程式碼
然後再編寫AopTest
的測試用例,這裡要注意,Aop執行器必須要在Ioc執行器之前執行,不然注入到Bean中的例項將可能不是代理類。
package com.zbw.aop;
import ...
@Slf4j
public class AopTest {
@Test
public void doAop() {
BeanContainer beanContainer = BeanContainer.getInstance();
beanContainer.loadBeans("com.zbw");
new Aop().doAop();
new Ioc().doIoc();
DoodleController controller = (DoodleController) beanContainer.getBean(DoodleController.class);
controller.hello();
}
}
複製程式碼
可以看到在執行DoodleController@hello()
方法的前後分別執行了DoodleAspect@before()
和DoodleAspect@afterReturning()
方法。說明AOP的功能已經完成了。
目前缺陷
雖然完成了AOP功能,但是還是有幾個比較嚴重的缺陷的
- 對目標類的篩選不是很便捷,現在是用
Aspect.target()
的值,來篩選出被這個值註解的類,這樣太籠統了。假如Aspect.target()=Controller.class
,那麼所有被Controller
註解的controller裡的左右方法都要被代理。我們希望能夠像spring那樣如execution(* com.zbw.*.service..*Impl.*(..))
,用一些表示式來篩選目標類。 - 一個目標類只能被一個切面作用。目前來說比如有
DoodleAspect1
和DoodleAspect2
兩個切面,都作用於DoodleController
上,只有一個切面能生效,這也不合理。
所以在後面的章節會完善實現這兩個問題。
- 從零開始實現一個簡易的Java MVC框架(一)--前言
- 從零開始實現一個簡易的Java MVC框架(二)--實現Bean容器
- 從零開始實現一個簡易的Java MVC框架(三)--實現IOC
- 從零開始實現一個簡易的Java MVC框架(四)--實現AOP
- 從零開始實現一個簡易的Java MVC框架(五)--引入aspectj實現AOP切點
- 從零開始實現一個簡易的Java MVC框架(六)--加強AOP功能
- 從零開始實現一個簡易的Java MVC框架(七)--實現MVC
- 從零開始實現一個簡易的Java MVC框架(八)--製作Starter
- 從零開始實現一個簡易的Java MVC框架(九)--優化MVC程式碼
原始碼地址:doodle