AspectJ便于调试、测试和性能调整工作。定义的行为范围从简单的跟踪到分析,再到应用程序内部一致性到测试。AspectJ可以干净地模块化这类功能,从而可以在需要时轻松地启用和禁用这些功能。
1 基础
本节将继续介绍AspectJ到一些基础功能,为后面的应用提供使用基础。
1.1 thisJoinPoint
当前连接点,提供对连接点可用状态及其静态信息的反射访问。
thisJoinPoint分为静态及实例部分。静态部分包含了连接点类型、签名及位置等信息。实例部分包含了连接点所在的this,代理对象及方法参数。
1.1.1 staticPart静态部分
可通过aspectJ变量:thisJoinPointStaticPart(如果只对连接点的静态信息感兴趣,则应通过访问此变量来以获得更佳性能) 或者thisJoinPoint.getStaticPart()获取静态部分。
图 连接点静态部分的方法
getId 方法获取的id是从0开始的。同一个切点下不同的连接点id是从0开始递增的。
图 静态部分代码及运行结果
1.1.2 实例部分的this 与 target
this: 连接点所在的this对象,当此对象不存在时返回空。(不包括静态空间及方法)。
target:目标对象,即代理对象。
我们可以通过连接点的实例来获取this、target及advice的参数。在实际开发中,我们通常不采用这种方式,因为这种方式会更耗时。我们可以通过下面的方式来获取这些参数:
public aspect ThisAndTargetAspect {
after(ThisAndTarget.TestThisAndTarget obj, ThisAndTarget target, String inputStr) returning(String str):
call(String article.custom.ThisAndTarget.fun1(String))
&& args(inputStr) && target(target) && this(obj){
System.out.println("参数:" + inputStr);
System.out.println("返回值:" + str);
System.out.println("this:" + obj);
System.out.println("target:" + target);
}
}
1.2 切点的部分声明关键词
1.2.1 call 与 execution
call: 调用方法的地方。
execution:方法执行的地方。
图 call与execution的演示代码及运行结果
1.2.2 withincode
withincode 指定特定方法作用域内的所有连接点(不包括该方法里所调用方法的相关连接点)。而within则是指定类的所有连接点。
图 withincode 演示代码及运行结果
2 应用
AspectJ 可用于跟踪调试、日志记录及契约约束等场景。在开发中,我们可以通过一个配置文件来控制是否使用AspectJ,从而控制AspectJ模块的插拔。
2.1 跟踪调试
在调试代码过程中,我们需要写好多测试方法来测试某个类的函数,甚至有时需要在被测试的方法中插入些测试代码以获得某些结果值。在测试完后我们需要把这些代码删除。这样容易删除一些业务代码,同时也带来了一定的工作量。
业务上线时我们要把这些测试代码移除,但是有时我们希望保持它们以供下个版本的测试。
使用AspectJ让上面这些需求的实现变得更容易。
public class TestEntity {
public int fun1(int num1, int num2) {
if (num2 > 99) num2 = 0;
return num1 + num2;
}
public static void main(String[] args) {
TestEntity entity = new TestEntity();
entity.fun1(3,99);
entity.fun1(63,101);
}
}
public aspect TestEntityAspect {
after(int num1,int num2) returning(int sum): call(int article2.custom.TestEntity.fun1(int,int)) && args(num1,num2) {
System.out.println(thisJoinPoint.getSignature() + "的测试");
System.out.println("参数是num1:" + num1 + ";num2:" + num2);
System.out.println("结果是:" + sum);
int result = num1 + num2;
System.out.println("预期结果:" + result);
if (result != sum) {
throw new RuntimeException(thisJoinPoint.getSignature() + "结果值不对");
}
}
}
2.2 日志记录
我们借助log4j等框架记录日志时,通常会在业务代码里插入记录日志的代码。这样让业务代码变得更加繁杂。同时,如果我们想在第三方库的方法中记录日志,平常方法将无法实现。
借助AspectJ我们可以很轻松的记录日志及对第三方库中的方法进行日志记录。
需求:统计LogEntity 的fun方法在fun1中被调用的次数(fun可能在其他方法中也会被调用)。
public class LogEntity {
public void fun() {
System.out.println("fun");
}
public void fun1() {
fun();
System.out.println("fun1");
fun();
}
public void fun2() {
fun();
System.out.println("fun2");
fun();
}
public static void main(String[] args) {
LogEntity logEntity = new LogEntity();
Random random = new Random();
for (int i = 0; i < 10; i++) {
int nextInt = random.nextInt(11);
if (nextInt > 5) logEntity.fun1();
else logEntity.fun2();
}
}
}
public aspect LogEntityAspect {
private static int count = 0;
after(): call(void article2.custom.LogEntity.fun()) && withincode(void article2.custom.LogEntity.fun1()) {
count++;
System.out.println("被调用次数:" + count);
}
}
2.3 前置或后置条件
现在好多项目都采用“契约式设计”方法。Design By Contract,简称DbC。这种设计方法要求软件设计者为软件组件定义正式的,准确的并且可验证的接口。
契约式设计的三个部分:
1)前置条件:为了调用函数,必须为真的条件。在其违反时,函数绝不调用。
2)后置条件:函数完成时的状态。确保该状态符合预期结果。
3) 不变式:保证类的状态在任何功能被执行后都保持在一个可接受的状态。
AspectJ 可以很容易编写前置及后置条件。
public class ContractEntity {
public double division(double num1, double num2) {
if (num1 > 999) num1 = 0;
return num1 / num2;
}
public static void main(String[] args) {
ContractEntity entity = new ContractEntity();
entity.division(34,2);
// entity.division(42,0);
entity.division(2134,34);
}
}
public aspect ContractEntityAspect {
before(double num1,double num2):call(double article2.custom.ContractEntity.division(double,double )) && args(num1,num2) {
if (num2 == 0) {
throw new RuntimeException("被除数不能为0");
}
}
after(double num1,double num2) returning(double res):call(double article2.custom.ContractEntity.division(double,double )) && args(num1,num2){
String str = "除数:" + num1 + ";被除数:" + num2;
System.out.println(str);
double num = num1 / num2;
System.out.println("预期值:" + num);
System.out.println("结果:" + res);
if (num != res) {
throw new RuntimeException("计算结果有误:" + str);
}
}
}
2.4 定义编译器错误
declare error: 切点表达式 : “错误提示”; 定义一个错误,会在代码编译器抛出。
declare error: call(* article.custom.DeclareEntity.fun1(..)): "不能使用该方法";
上面这条语句的含义是:如果在代码中有调用DeclareEntity的fun1方法,那么在编译代码时,将抛出错误“不能使用该方法”。