從零開始實現一個簡易的Java MVC框架(四)--實現AOP

zzzzbw發表於2018-07-12

前言

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介面的類,這些類便是切面
  • 獲取切面上的註解Aspecttarget()的值,這個值就是要被代理的類的註解。比如說有個切面的註解為@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();
    }
}

複製程式碼

從零開始實現一個簡易的Java MVC框架(四)--實現AOP

可以看到在執行DoodleController@hello()方法的前後分別執行了DoodleAspect@before()DoodleAspect@afterReturning()方法。說明AOP的功能已經完成了。

目前缺陷

雖然完成了AOP功能,但是還是有幾個比較嚴重的缺陷的

  • 對目標類的篩選不是很便捷,現在是用Aspect.target()的值,來篩選出被這個值註解的類,這樣太籠統了。假如Aspect.target()=Controller.class,那麼所有被Controller註解的controller裡的左右方法都要被代理。我們希望能夠像spring那樣如execution(* com.zbw.*.service..*Impl.*(..)),用一些表示式來篩選目標類。
  • 一個目標類只能被一個切面作用。目前來說比如有DoodleAspect1DoodleAspect2兩個切面,都作用於DoodleController上,只有一個切面能生效,這也不合理。

所以在後面的章節會完善實現這兩個問題。


原始碼地址:doodle

原文地址:從零開始實現一個簡易的Java MVC框架(四)--實現AOP

相關文章