AspectJ入门(二)— 应用

发布时间:2024年01月04日

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

文章来源:https://blog.csdn.net/qq_25308331/article/details/135397015
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。