• AspectJ中的类型间声明(成员注入)


    在上一篇博客初窥AspectJ中,我们提到AspectJ给java提供了三种新的结构,pointcut,advice以及inter-type declaration(ITD),而且我们通过一个简单的Demo介绍了如何使用pointcut和advice。而本文将介绍inter-type declaration是什么,可以做什么,最后同样会通过一个Demo来介绍如何使用。后文将主要用ITD来表示inter-type declaration。

    本文中Demo的代码可以在github aspect-demo中找到。

    ITD与成员注入

    inter-type declaration (ITD),翻译成中文是类型间声明。即使看到中文翻译,相信大家还是一头雾水,不知所云,所以我不是很喜欢对一些英文名字,尤其是技术名字进行生硬的翻译,这只会增加大家的理解负担。其实,换一种说法可能更好理解,member introduction(成员注入),其目的就是通过aspect的方式,在现有的类中注入一些新的成员变量或者成员方法。通过aspect,我们可以向一个类中注入如下成员:

    • 成员变量(final或者非final)
    • 方法
    • 构造函数

    除了往类里面添加内容,aspect还可以修改java中的interface(接口),实现在现有接口中注入:

    • 方法的默认实现
    • 非final的域

    通过ITD注入的成员的访问修饰符可以是:

    • private: 通过private声明的私有成员属于目标类,但是呢,只对aspect脚本可见,而对目标类不可见;
    • public: 声明为public的成员对所有类和apsect都可见;
    • default package protected:这里的包可见性是相对于aspect所在的包,而不是相对于目标类所在的包。

    inter-type declaration示例

    在编写aspect之前,先准备一个简单的java类:

    package cc.databus.aspect.intertype;
    
    public class Point {
        private int x;
        private int y;
    
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    
        public int getX() {
            return x;
        }
    
        public void setX(int x) {
            this.x = x;
        }
    
        public int getY() {
            return y;
        }
    
        public void setY(int y) {
            this.y = y;
        }
    }
    

    有了这个基础类,下面来看看如何通过aspect修改这个类实现的接口,成员变量以及成员方法。这里是我们的aspect代码:

    package cc.databus.aspect.intertype;
    
    public aspect PointAspect {
        // creates a new interface named HasName
        private interface HasName{}
        // make class Ppint implements HashName
        declare parents: Point implements HasName;
        // make HasName has a field named name
        private String HasName.name;
        // make HasName has a method getName() and default implemented
        public String HasName.getName() {
            return name;
        }
    
        // make HasName has a method named setName and default
        public void HasName.setName(String name) {
            this.name = name;
        }
    
        // add a field named created to class Point
        // with default value 0
        long Point.created = 0;
    
    
        // add a field named lastUpdated to class Point
        // with default value 0
        private long Point.lastUpdated = 0;
    
    
        // add a private method setUpdated()
        private void Point.setUpdated() {
            this.lastUpdated = System.currentTimeMillis();
        }
    
        // implement toString() for Point
        // include the fields added in the aspect file
        public String Point.toString() {
            return String.format(
                    "Point: {name=%s, x=%d; y=%d, created=%d, updated=%d}",
                    getName(), getX(), getY(), created, lastUpdated);
        }
    
        // pointcut the constructor, and set the value for created
        after() returning(Point p) : call(Point.new(..)) && !within(PointAspect) {
            System.out.println(thisJoinPointStaticPart);
            System.out.println("Set created");
            p.created = System.currentTimeMillis();
        }
    
        // define a pointcut for setX and setY
        pointcut update(Point p): target(p) && call(void Point.set*(..));
    
        // make the lastUpdated updated every time
        // setX or setY invoked
        after(Point p): update(p) && !within(PointAspect) {
            System.out.println("set updated for Point due to " + thisJoinPointStaticPart);
            p.setUpdated();
        }
    }
    

    在上面的aspect文件中,我们首先定义了一个接口,并且让Point类实现该接口,且给该新接口加了一个成员变量(name)并实现了对应的setter/getter:

        // creates a new interface named HasName
        private interface HasName{}
        // make class Ppint implements HashName
        declare parents: Point implements HasName;
        // make HasName has a field named name
        private String HasName.name;
        // make HasName has a method getName() and default implemented
        public String HasName.getName() {
            return name;
        }
    
        // make HasName has a method named setName and default
        public void HasName.setName(String name) {
            this.name = name;
        }
    
    

    随后,我们给Point类加了两个成员变量,并实现了两个成员方法。其中,实现toString()接口的时候,我们把通过aspect注入的成员变量也都包含在结果里面:

        // add a field named created to class Point
        // with default value 0
        long Point.created = 0;
    
        // add a field named lastUpdated to class Point
        // with default value 0
        private long Point.lastUpdated = 0;
    
        // add a private method setUpdated()
        private void Point.updated() {
            this.lastUpdated = System.currentTimeMillis();
        }
    
        // implement toString() for Point
        // include the fields added in the aspect file
        public String Point.toString() {
            return String.format(
                    "Point: {name=%s, x=%d; y=%d, created=%d, updated=%d}",
                    getName(), getX(), getY(), created, lastUpdated);
        }
    

    最后,我们加了两个pointcut一级advice,分别实现在调用Point构造函数之后为created的赋值,以及调用setX(int), set(int)以及setName(string)的时候更新lastUpdated成员变量(这里使用!within(PointAspect)排除掉在aspect脚本里面调用set*的情况):

        // pointcut the constructor, and set the value for created
        after() returning(Point p) : call(Point.new(..)) && !within(PointAspect) {
            System.out.println(thisJoinPointStaticPart);
            System.out.println("Set created");
            p.created = System.currentTimeMillis();
        }
    
        // define a pointcut for setX and setY
        pointcut update(Point p): target(p) && call(void Point.set*(..));
    
        // make the lastUpdated updated every time
        // setX or setY invoked
        after(Point p): update(p) && !within(PointAspect) {
            System.out.println("set updated for Point due to " + thisJoinPointStaticPart);
            p.setUpdated();
        }
    

    同样,我们可以新建一个单元测试类来进行测试:

    package cc.databus.aspect.intertype;
    
    import org.junit.Test;
    
    public class TestPointAspect {
    
        @Test
        public void test() {
            Point point = new Point(1,1);
            point.setName("test");
            point.setX(12);
            point.setY(123);
            System.out.println(point);
        }
    }
    

    运行测试,我们能看到如下结果:

    call(cc.databus.aspect.intertype.Point(int, int))
    Set created
    set updated for Point due to call(void cc.databus.aspect.intertype.Point.setName(String))
    set updated for Point due to call(void cc.databus.aspect.intertype.Point.setX(int))
    set updated for Point due to call(void cc.databus.aspect.intertype.Point.setY(int))
    Point: {name=test, x=12; y=123, created=1536153649547, updated=1536153649548}
    
    

    可以看到,通过aspect注入的成员对象和成员方法都是工作的。

    总结

    ITD着实是一个强大的功能,能够方便给现有类注入新的功能。但是,笔者认为使用这种方法相对容易出错,尤其在大项目的情况下,如果通过大量的aspect脚本来实现功能,相信对后期的维护是一个很大的挑战。所以,我建议在没有spring这种框架做支撑的情况下,不要大量的使用这种方法为项目造血。

    Reference

    1. Advanced AspectJ Part II : Inter-type declaration
    2. Inter-type declarations

    文章同步发布在我的个人博客https://jianyuan.me上,欢迎拍砖。
    传送门: AspectJ中的类型间声明(成员注入)

    个人博客地址: https://jianyuan.me
  • 相关阅读:
    debug error 错误日志的调试模式
    fork(2)
    Fundamental theorem of arithmetic 为什么1不是质数
    Compile-time Dependency Injection With Go Cloud's Wire 编译时依赖注入 运行时依赖注入
    LevelDB
    MySQL Bugs: #34354: Feature request: EXPLAIN ALTER TABLE https://bugs.mysql.com/bug.php?id=34354
    explain 分析 聚合统计语句的性能
    (原创)《Android编程权威指南》学习笔记01-- Android应用初体验--005
    (原创)《Android编程权威指南》学习笔记01-- Android应用初体验--004
    (原创)《Android编程权威指南》学习笔记01-- Android应用初体验--003
  • 原文地址:https://www.cnblogs.com/yflog/p/9596791.html
Copyright © 2020-2023  润新知