從零開始實現一個簡易的Java MVC框架(五)–引入aspectj實現AOP切點

zzzzbw發表於2019-02-24

前言

在上一節從零開始實現一個簡易的Java MVC框架(四)–實現AOP中我們實現了AOP的功能,已經可以生成對應的代理類了,但是對於代理物件的選擇只能通過指定的類,這樣確實不方便也不合理。這一節我們就利用aspectj來實現功能更強大的切點。

在spring初期的時候AOP功能使用起來也是很繁瑣麻煩的,到了後面整合了aspectj才有了現在這麼方便的AOP功能,比如下面這樣的程式碼,很簡便並且直觀的定義了切點。

@Component
@Aspect
public class LogAspect {
	@Pointcut("execution(* com.zbw.*.service..*Impl.*(..)) && @annotation(Log)")
	public void logPointcut() {
	}

	@Before("logPointcut()")
    public void before()
    {System.out.println("Before");}
}
複製程式碼

現在我們也來引入aspectj來實現AOP切點的功能

引入aspectj並實現aspectj的切點類

首先在pom.xml中加入aspectj的依賴

<properties>
    ...
    <aspectj.version>1.8.13</aspectj.version>
</properties>
<dependencies>
	...
	<!-- aspectj -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>${aspectj.version}</version>
    </dependency>
</dependencies>

複製程式碼

接下來就可以開始實現一個利用aspectj來判定的切點類,這個類主要是用於判斷aspectj表示式是否匹配一個指定類或者指定方法。

在zbw.aop包下建立一個類,起名叫ProxyPointcut

package com.zbw.aop;

import ...

/**
 * 代理切點類
 */
public class ProxyPointcut {
    /**
     * 切點解析器
     */
    private PointcutParser pointcutParser;

    /**
     * (AspectJ)表示式
     */
    private String expression;

    /**
     * 表示式解析器
     */
    private PointcutExpression pointcutExpression;

    /**
     * AspectJ語法集合
     */
    private static final Set<PointcutPrimitive> DEFAULT_SUPPORTED_PRIMITIVES = new HashSet<>();

    static {
        DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
        DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.ARGS);
        DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.REFERENCE);
        DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.THIS);
        DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.TARGET);
        DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.WITHIN);
        DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ANNOTATION);
        DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_WITHIN);
        DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ARGS);
        DEFAULT_SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_TARGET);
    }

    public ProxyPointcut() {
        this(DEFAULT_SUPPORTED_PRIMITIVES);
    }

    public ProxyPointcut(Set<PointcutPrimitive> supportedPrimitives) {
        pointcutParser = PointcutParser
                .getPointcutParserSupportingSpecifiedPrimitivesAndUsingContextClassloaderForResolution(supportedPrimitives);
    }

    /**
     * Class是否匹配切點表示式
     */
    public boolean matches(Class<?> targetClass) {
        checkReadyToMatch();
        return pointcutExpression.couldMatchJoinPointsInType(targetClass);
    }

    /**
     * Method是否匹配切點表示式
     */
    public boolean matches(Method method) {
        checkReadyToMatch();
        ShadowMatch shadowMatch = pointcutExpression.matchesMethodExecution(method);
        if (shadowMatch.alwaysMatches()) {
            return true;
        } else if (shadowMatch.neverMatches()) {
            return false;
        }
        return false;
    }

    /**
     * 初始化切點解析器
     */
    private void checkReadyToMatch() {
        if (null == pointcutExpression) {
            pointcutExpression = pointcutParser.parsePointcutExpression(expression);
        }
    }

    public void setExpression(String expression) {
        this.expression = expression;
    }

    public String getExpression() {
        return expression;
    }
複製程式碼

這個類中有三個變數:pointcutParser,expression,pointcutExpression

其中expression是String型別,用於存放我們要設定的aspectj表示式,比如execution(* com.zbw.*.service..*Impl.*(..))這樣的。

pointcutParserpointcutExpression就是aspectj裡面的類了,pointcutParser用於根據expression中的表示式建立pointcutExpression表示式解析器。而pointcutExpression可以用來判斷方法或者類是否匹配表示式。

這個類中最主要的兩個方法就matches(Class<?> targetClass)matches(Method method),這兩個方法分別用於判定目標的類和方法是否匹配expression中的aspectj表示式。

接下來就可以把ProxyPointcut這個切點類加入到我們之前實現的AOP功能中了。

實現AOP的切點功能

首先改裝Aspect註解,把之前target()改成pointcut()來儲存aspectj表示式。

package com.zbw.aop.annotation;
import ...;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Aspect {
    /**
     * 切點表示式
     */
    String pointcut() default "";
}
	
複製程式碼

然後改裝ProxyAdvisor這個類,把切點表示式匹配器放入其中,並且使用匹配器來判定目標類是否要被增強。

...

public class ProxyAdvisor {

	...

    /**
     * AspectJ表示式切點匹配器
     */
    private ProxyPointcut pointcut;

    /**
     * 執行代理方法
     */
    public Object doProxy(Object target, Class<?> targetClass, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        if (!pointcut.matches(method)) {
            return proxy.invokeSuper(target, args);
        }

        ...
    }
}
複製程式碼

doProxy()這個方法的最前面通過pointcut.matches()來判定目標方法是否匹配這個表示式,如果匹配的話就往下執行之前編寫的各種通知,如果不匹配那麼就直接執行目標方法。通過這種方式來使aspectj表示式控制目標類的增強。

接下來改裝Aop類,由於改變了匹配目標類的規則,所以要重寫之前的doAop()方法。

...

public class Aop {
	...

    public void doAop() {
        beanContainer.getClassesBySuper(Advice.class)
                .stream()
                .filter(clz -> clz.isAnnotationPresent(Aspect.class))
                .map(this::createProxyAdvisor)
                .forEach(proxyAdvisor -> beanContainer.getClasses()
                        .stream()
                        .filter(target -> !Advice.class.isAssignableFrom(target))
                        .filter(target -> !target.isAnnotationPresent(Aspect.class))
                        .forEach(target -> {
                            if (proxyAdvisor.getPointcut().matches(target)) {
                                Object proxyBean = ProxyCreator.createProxy(target, proxyAdvisor);
                                beanContainer.addBean(target, proxyBean);
                            }
                        }));
    }

    /**
     * 通過Aspect切面類建立代理通知類
     */
    private ProxyAdvisor createProxyAdvisor(Class<?> aspectClass) {
        String expression = aspectClass.getAnnotation(Aspect.class).pointcut();
        ProxyPointcut proxyPointcut = new ProxyPointcut();
        proxyPointcut.setExpression(expression);
        Advice advice = (Advice) beanContainer.getBean(aspectClass);
        return new ProxyAdvisor(advice, proxyPointcut);
    }
}
複製程式碼

雖然重寫了doAop()方法,但是實現原理依舊是相同的。只不過現在把建立ProxyAdvisor的過程分離出來單獨寫了一個方法createProxyAdvisor()
然後再遍歷Bean容器中的除了切面類的所有Bean,如果這個Bean匹配ProxyAdvisor中的切點表示式,那麼就會生成對應的代理類。

引入aspectj實現AOP切點完成了,又到測試用例來測試功能是否成功的時候了。

測試用例

在上一篇文章從零開始實現一個簡易的Java MVC框架(四)–實現AOP中的測試用例的基礎上修改測試用例。

先修改切面類DoodleAspect上的Aspect註解

package com.zbw.bean;
import ...

@Slf4j
@Aspect(pointcut = "execution(* com.zbw.bean.DoodleController.helloForAspect(..))")
public class DoodleAspect implements AroundAdvice {

	...

}
複製程式碼

這個Aspect@pointcut()中的值會讓其只匹配DoodleController中的helloForAspect()方法。

接下來在DoodleController新增helloForAspect()方法

...

public class DoodleController {
   	...

    public void helloForAspect() {
        log.info("Hello Aspectj");
    }
}

複製程式碼

最後再重新編寫AopTest的測試用例。

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();
        controller.helloForAspect();
    }
}

複製程式碼
從零開始實現一個簡易的Java MVC框架(五)–引入aspectj實現AOP切點

從結果的圖中可以看到在DoodleControllerhello()前後沒有列印多餘的日誌,而在helloForAspect()方法的前面和後面都列印了DoodleAspect中的通知方法裡的內容,說明我們的AOP已經精準的匹配到了想要的目標。


原始碼地址:doodle

原文地址:從零開始實現一個簡易的Java MVC框架(五)–引入aspectj實現AOP切點

相關文章