如何在另一个AspectJ方面拦截proceed()?

时间:2012-08-18 12:33:41

标签: aop aspectj pointcut

我的情况如下:我有一个LoggingAspect,其中有几个切入点与我的主应用程序中的特定方法执行相匹配。相应的建议机构基本上看起来都很相似,导致很多代码重复:

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()返回之后。

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

如果有人知道我该怎么做,我会很感激提示或代码片段。

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

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

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

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

2 个答案:

答案 0 :(得分:2)

建议将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;
    }
};

最好的问候Marko

答案 1 :(得分:2)

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

  

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

我们在这里处理的基本事情是Ramnivas Laddad在他的书AspectJ in Action中称之为工人对象模式的内容。他的(和loddar2012的)想法是简单的散文

  • 将一个调用包装到一个匿名类(worker对象)的实例中
    • 基类或实现的接口提供了一个用于完成工作的方法,
    • 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()绑定的连接点状态,
  • 调用/运行worker对象(该调用将被日志记录建议的切入点截获,并且真正的日志记录业务在那里完成)。