前言
在上一節從零開始實現一個簡易的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已經精準的匹配到了想要的目標。
原始碼地址:doodle