【问题标题】:How to intercept proceed() in another AspectJ aspect?如何在另一个 AspectJ 方面拦截proceed()?
【发布时间】:2012-08-18 12:33:40
【问题描述】:

我的情况如下:我有一个LoggingAspect,其中有几个切入点与我的主应用程序中的特定方法执行相匹配。对应的advice body基本都长得差不多,造成大量代码重复:

void around() : download() {
    String message = "Downloading, verifying (MD5) and unpacking";
    SimpleLogger.verbose(message, IndentMode.INDENT_AFTER);
    proceed();
    SimpleLogger.verbose(message + " - done", IndentMode.DEDENT_BEFORE);
}

不过还是有一些变化。有时切入点和建议有一个argthis 参数,这些参数也会打印到日志中。有时,如果它只是一个小调用而不包含许多其他调用,则不会打印“完成”消息,如下所示:

void around(BasicFilter filter) : fixFaultyLinkTargets()  && this(filter) {
    String message = "TOC file: checking for faulty link targets";
    SimpleLogger.verbose(message, IndentMode.INDENT_AFTER);
    proceed(filter);
    SimpleLogger.dedent();
}

不变的是我手动告诉记录器

  • 在打印第一条消息后增加缩进级别,即直接在调用 proceed() 之前,并且
  • 在打印最终消息(如果有的话)之前降低缩进级别,即直接在proceed() 返回之后。

我的想法是,我想编写一个带有切入点的元切面(或称为辅助切面),该切入点拦截LoggingAspect 中的proceed() 调用,以便相应地自动调整缩进级别。但似乎没有匹配proceed() 的切入点。我已经尝试过call(SomeMethodInMyMainApp),甚至是与日志记录方面的所有内容匹配的切入点,但切入点匹配我不需要的任何内容,但永远不会继续。

如果有人知道我如何做到这一点,我将不胜感激提示或代码 sn-p。

这样做的间接方法可能不是拦截建议本身,而是通过创建一个额外的切入点来拦截这些建议所建议的方法调用(或执行):

// ATTENTION: each new pointcut must also be added here
pointcut catchAll() : download() || fixFaultyLinkTargets() || ...;

void around() : catchAll() {
    SimpleLogger.indent();
    proceed();
    SimpleLogger.dedent();
}

不过,我更喜欢另一种方式,我不必记住每次更改日志记录方面的某些内容时都更新额外的 catchAll() 切入点。

【问题讨论】:

    标签: aop aspectj pointcut


    【解决方案1】:

    建议将proceed() 封装在一个匿名类中。并编写一个处理此执行的方面(但不要忘记proceed()的潜在异常)。

    我的建议:

    // AspectProceedCaller.java
    public abstract class AspectProceedCaller { 
        public abstract Object doProceed(); 
    };
    
    // aspect ProceedCallerAspect.aj
    aspect ProceedCallerAspect {
         pointcut execProceedCaller() : execution( * AspectProceedCaller+.doProceed() );
    
         Object around() : execProceedCaller() {
             try {
                  SimpleLogger.indent();
                  return proceed();
             }
             finally {
                  SimpleLogger.dedent();
             }
         }
    };
    
    
    // Your application aspect 
    aspect AnyAspect {
        pointcut anyPointcut() : ...;
    
        Object around() : anyPointcut() {
            AspectProceedCaller apc=new AspectProceedCaller() {
                public Object doProceed() {
                    return proceed();
                }
            };  
    
            // DO Stuff before ....
    
            Object retval = apc.doProceed();
    
            // ... and after calling proceed.
    
            return retval;
        }
    };
    

    最好的问候马尔科

    【讨论】:

    • 虽然这是拦截proceed() 的一种创造性甚至可行的方法,但我上述避免代码重复的目标无法实现,因为我为了避免重复的日志记录和缩进代码,我必须用它来换取重复的匿名类声明。显然,我不能将匿名类声明为建议之外的单例并重用它,因为这样我就不能再调用proceed()。而且我有很多日志记录建议......所以我很抱歉没有选择你的答案,因为它仍然是开放的。总之非常感谢。 :-)
    • 更新:我赞成您的回答,因为我现在使用的是类似的方法。这不是我想要的,但没有我以前那么糟糕。请参阅this changeset 和我的LoggingAspect 的两个对应版本,了解我重构前后的样子。感谢您的启发。
    • 恕我直言,没有其他可能性,因为proceed()不是一个函数。它是环绕建议中的保留关键字,不是连接点。
    • ... 这一点几乎不可能改变,因为 AspectJ 必须能够将切面编入 JAR 或 LTW。马尔科
    • 我知道它不是一个连接点,正如我在原始帖子中所说的那样:“似乎没有切入点匹配proceed()”。但我看不出为什么它不能由 AJ 开发人员制作。成员变量赋值也不是函数调用,但无论如何都是一个连接点。代价可能是保留一些额外的状态(如果必要的信息在连接点的元数据中不可用)。让我暂时保留这个问题,如果我在这里或AspectJ users 没有得到更好的答案,我可能会接受你的。
    【解决方案2】:

    请注意:我将在这里回答我自己的问题,为 loddar2012 建议的解决方案添加更多信息和参数化的附加功能。因为他的回答将我引向了正确的方向,所以我会接受它,即使这里的回答确实满足了我对原始问题的所有需求,例如(引用自己的话):

    不过还是有一些变化。有时切入点和建议有一个 arg 或这个参数,它也会打印到日志中。有时,如果它只是一个小调用而不包含许多其他调用,则不会打印“完成”消息

    我们在这里处理的基本问题是 Ramnivas Laddad 在他的书 AspectJ in Action 中称之为 worker 对象模式。他(和 loddar2012 的)想法是,简单的散文

    • 将调用包装到匿名类(工作对象)的实例中,其中
      • 基类或实现的接口提供了用于完成工作的方法,
      • worker对象提供worker方法的具体实现,并在其中具体调用proceed()
      • worker 方法可以在对象创建后立即调用(正如我们将在此处执行的那样)或稍后调用,甚至可以在它自己的线程中调用,
      • worker 对象可能会被传递或添加到调度队列中(我们在这里都不需要)。

    如果您需要异步执行proceed() 调用,一个优雅的解决方案是创建匿名Runnable 类的实例。不过,我们将使用我们自己的抽象基类LogHelper,因为我们想要在我们的茶中加入更多的糖,特别是传递一条日志消息和一些其他影响日志输出到每个工作人员的参数的选项。所以这就是我所做的(示例代码中未显示包名称和导入):

    抽象工作者基类:

    abstract class LogHelper {
        // Object state needed for logging
        String message;
        boolean logDone;
        boolean indent;
        LogType type;
    
        // Main constructor
        LogHelper(String message, boolean logDone, boolean indent, LogType type) {
            this.message = message;
            this.logDone = logDone;
            this.indent = indent;
            this.type = type;
        }
        // Convenience constructors for frequent use cases
        LogHelper(String message, boolean logDone) {
            this(message, logDone, true, LogType.VERBOSE);
        }
        LogHelper(String message) {
            this(message, true);
        }
    
        // Worker method to be overridden by each anonymous subclass
        abstract void log();
    }
    

    记录工作对象执行的建议:

    aspect LoggingAspect
    {
        void around(LogHelper logHelper) :
            execution(* LogHelper.log()) && this(logHelper)
        {
            try {
                SimpleLogger.log(logHelper.type, logHelper.message);
                if (logHelper.indent)
                    SimpleLogger.indent();
                proceed(logHelper);
            } finally {
                if (logHelper.indent)
                    SimpleLogger.dedent();
                if (logHelper.logDone)
                    SimpleLogger.log(logHelper.type, logHelper.message + " - done");
            }
        }
        // (...)
    }
    

    如你所见,日志通知在调用proceed(logHelper)之前做了一些事情(即执行worker对象的log()方法)和之后的一些事情,使用存储在worker对象中的状态信息,例如

    • 要记录的消息,
    • 日志级别(这里称为“类型”),
    • 标志指定在继续之前是否应提高缩进级别,
    • 标志指定是否应在工作人员执行后打印“完成”消息。

    因为在我的用例中,所有记录的方法都返回void,所以不需要实现返回值传递,但如果有必要,这很容易实现。通知的返回值将是 Object,我们会将 proceed() 的结果传回给我们的调用者,没什么大不了的。

    捕获要记录的连接点并利用参数化工作对象完成工作的一些建议:

    aspect LoggingAspect
    {
        // (...)
    
        pointcut processBook()     : execution(* OpenbookCleaner.downloadAndCleanBook(Book));
        pointcut download()        : execution(* Downloader.download());
        pointcut cleanBook()       : execution(* OpenbookCleaner.cleanBook(Book));
        pointcut cleanChapter()    : execution(* OpenbookCleaner.cleanChapter(Book, File));
        pointcut initialiseTitle() : execution(* *Filter.initialiseTitle(boolean));
    
        void around(final Book book) : processBook() && args(book) {
            new LogHelper("Book: " + book.unpackDirectory) {
                void log() { proceed(book); } }.log();
        }
        void around() : download() {
            new LogHelper("Downloading, verifying (MD5) and unpacking") {
                void log() { proceed(); } }.log();
        }
        void around() : cleanBook() {
            new LogHelper("Filtering") {
                void log() { proceed(); } }.log();
        }
        void around(final File origFile) : cleanChapter() && args(*, origFile) {
            new LogHelper("Chapter: " + origFile.getName()) {
                void log() { proceed(origFile); } }.log();
        }
        void around() : initialiseTitle() {
            new LogHelper("Initialising page title", false) {
                void log() { proceed(); } }.log();
        }
    }
    

    这些例子告诉你如何做

    • 将匿名LogHelper 实例化为具有一个或多个构造函数参数的工作对象,并设置其状态
    • 实现log() 方法,可选择使用通过this()args() 绑定的连接点状态,
    • 调用/运行工作对象(调用将被日志建议的切入点拦截,并在那里完成真正的日志业务)。

    【讨论】:

    • 很好,正是我正在寻找的,直到知道我使用 runnables 作为解决方法
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多