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方法,那么在编译代码时,将抛出错误“不能使用该方法”。