前言
在上一节从零开始实现一个简易的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.*(..))
这样的。
pointcutParser
和pointcutExpression
就是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();
}
}
复制代码
从结果的图中可以看到在DoodleController
的hello()
前后没有打印多余的日志,而在helloForAspect()
方法的前面和后面都打印了DoodleAspect
中的通知方法里的内容,说明我们的AOP已经精准的匹配到了想要的目标。