This website requires JavaScript.

从零开始实现一个简易的 Java MVC 框架(五)--引入 aspectj 实现 AOP 切点

前言

在上一节 从零开始实现一个简易的 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;
        }
        // TODO: 还有个情况不判断了!详情见 org.springframework.aop.aspectj.AspectJExpressionPointcut@matches() 方法
        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();
    }
}

从结果的图中可以看到在DoodleControllerhello()前后没有打印多余的日志,而在helloForAspect()方法的前面和后面都打印了DoodleAspect中的通知方法里的内容,说明我们的 AOP 已经精准的匹配到了想要的目标。


源码地址:doodle

原文地址:从零开始实现一个简易的 Java MVC 框架(五)--引入 aspectj 实现 AOP 切点

0条评论
avatar