【问题标题】:aspectj pointcut for inherited interface methods继承接口方法的aspectj切入点
【发布时间】:2019-12-31 20:37:05
【问题描述】:

我想用aspectj拦截所有java.sql.DataSource.getConnection方法, 我使用了这个切入点:

"execution(public java.sql.Connection javax.sql.DataSource+.getConnection(..))"

它工作正常。 但是我遇到了一些类,例如 org.apache.tomcat.jdbc.pool.DataSource 在类层次结构中实现,该切入点不起作用,其中 DataSource 方法位于层次结构中不实现 DataSource 的类中,只有最顶层的类实现了DataSource:

class BaseDataSource {

    public Connection getConnection() throws SQLException {
        return null;
    }


    public Connection getConnection(String username, String password) throws SQLException {
        return null;
    }

   implements all DataSource Methods...
}

class MyDataSource extends BaseDataSource implements java.sql.DataSource{
           //does not implement DataSource methods
}

BaseDataSource 没有实现 DataSource,但具有所有 DataSource 方法的实现。

我发现唯一可行的切入点是:

execution(public java.sql.Connection *.getConnection(..)) && target(javax.sql.DataSource)

我的问题是是否有更好的方法,以及这个切入点的性能是否最差?

【问题讨论】:

  • 在我回答之前我有一个问题:根据Javadocorg.apache.tomcat.jdbc.pool.DataSource 类确实实现了javax.sql.DataSource,因此对于您解释的情况来说,这不是一个很好的例子。如果您对此层次结构中的类有问题,它们与您描述的不同。你能澄清一下吗?
  • 更新:啊,现在我明白了。该类实现了接口,但从其超类org.apache.tomcat.jdbc.pool.DataSourceProxy 继承了方法,后者实现了方法但不实现接口。你没有解释清楚。确实很奇怪。有趣的问题,我会研究一下。
  • 另一个问题:如果你说你的execution() 切入点有效并且方法没有被子类覆盖,这意味着你必须将你的方面直接编织到第 3 方代码中。您是使用二进制编译后编织、创建替换 JAR,还是使用 AspectJ 加载时编织代理启动容器?大概是后者。这对我的回答没有影响,我只是好奇。这里大多数与 AOP 相关的问题都是相当无聊的 Spring AOP 问题,更有趣的 AspectJ 东西很少见。 ;-)

标签: java aop aspectj pointcut


【解决方案1】:

我在MCVE 中复制了您的情况,如下所示:

基类实现DataSource 方法,但不是接口:

package de.scrum_master.app;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class BaseClass {
  public PrintWriter getLogWriter() throws SQLException { return null; }
  public void setLogWriter(PrintWriter out) throws SQLException {}
  public void setLoginTimeout(int seconds) throws SQLException {}
  public int getLoginTimeout() throws SQLException { return 0; }
  public Logger getParentLogger() throws SQLFeatureNotSupportedException { return null; }
  public <T> T unwrap(Class<T> iface) throws SQLException { return null; }
  public boolean isWrapperFor(Class<?> iface) throws SQLException { return false; }
  public Connection getConnection() throws SQLException { return null; }
  public Connection getConnection(String username, String password) throws SQLException { return null; }
}

子类实现接口DataSource,继承基类方法:

package de.scrum_master.app;

import javax.sql.DataSource;

public class SubClass extends BaseClass implements DataSource {}

驱动程序应用:

package de.scrum_master.app;

import java.sql.SQLException;

public class Application {
  public static void main(String[] args) throws SQLException {
    System.out.println("Aspect should not kick in");
    new BaseClass().getConnection();
    new BaseClass().getConnection("user", "pw");

    System.out.println("Aspect should kick in");
    new SubClass().getConnection();
    new SubClass().getConnection("user", "pw");
  }
}

方面:

这个切面使用你当前使用的切入点。

package de.scrum_master.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class DataSourceConnectionAspect {
  @Before("execution(public java.sql.Connection *.getConnection(..)) && target(javax.sql.DataSource)")
  public void myAdvice(JoinPoint thisJoinPoint) {
    System.out.println(thisJoinPoint);
  }
}

控制台日志:

Aspect should not kick in
Aspect should kick in
execution(Connection de.scrum_master.app.BaseClass.getConnection())
execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))

这里没有意外,一切都按预期进行。在我看来,这是一种有效的方法。当然方面代码将被编织到每个匹配public java.sql.Connection *.getConnection(..)) 的方法中,如果target(javax.sql.DataSource) 真的适用,将会有一个运行时检查,另见javap 输出:

Compiled from "BaseClass.java"
public class de.scrum_master.app.BaseClass {
  (...)

  public java.sql.Connection getConnection() throws java.sql.SQLException;
    Code:
       0: aload_0
       1: instanceof    #76                 // class javax/sql/DataSource
       4: ifeq          21
       7: invokestatic  #70                 // Method de/scrum_master/aspect/DataSourceConnectionAspect.aspectOf:()Lde/scrum_master/aspect/DataSourceConnectionAspect;
      10: getstatic     #58                 // Field ajc$tjp_0:Lorg/aspectj/lang/JoinPoint$StaticPart;
      13: aload_0
      14: aload_0
      15: invokestatic  #64                 // Method org/aspectj/runtime/reflect/Factory.makeJP:(Lorg/aspectj/lang/JoinPoint$StaticPart;Ljava/lang/Object;Ljava/lang/Object;)Lorg/aspectj/lang/JoinPoint;
      18: invokevirtual #74                 // Method de/scrum_master/aspect/DataSourceConnectionAspect.myAdvice:(Lorg/aspectj/lang/JoinPoint;)V
      21: aconst_null
      22: areturn

  public java.sql.Connection getConnection(java.lang.String, java.lang.String) throws java.sql.SQLException;
    Code:
       0: aload_1
       1: astore        4
       3: aload_2
       4: astore        5
       6: aload_0
       7: instanceof    #76                 // class javax/sql/DataSource
      10: ifeq          31
      13: invokestatic  #70                 // Method de/scrum_master/aspect/DataSourceConnectionAspect.aspectOf:()Lde/scrum_master/aspect/DataSourceConnectionAspect;
      16: getstatic     #79                 // Field ajc$tjp_1:Lorg/aspectj/lang/JoinPoint$StaticPart;
      19: aload_0
      20: aload_0
      21: aload         4
      23: aload         5
      25: invokestatic  #82                 // Method org/aspectj/runtime/reflect/Factory.makeJP:(Lorg/aspectj/lang/JoinPoint$StaticPart;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Lorg/aspectj/lang/JoinPoint;
      28: invokevirtual #74                 // Method de/scrum_master/aspect/DataSourceConnectionAspect.myAdvice:(Lorg/aspectj/lang/JoinPoint;)V
      31: aconst_null
      32: areturn

  (...)
}

即如果当前实例不是DataSource,则运行时检查也会发生在实现这些非常特殊方法模式的类上。但这应该很少见。

还有一种涉及 ITD(类型间声明)的替代方案:您可以让基类直接实现接口,然后返回使用更有效的原始切入点。在基于注释的语法中,这将是这样的:

package de.scrum_master.aspect;

import javax.sql.DataSource;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.DeclareParents;

@Aspect
public class DataSourceConnectionAspect {
  @DeclareParents("de.scrum_master.app.BaseClass")
  private DataSource dataSource;

  @Before("execution(public java.sql.Connection javax.sql.DataSource+.getConnection(..))")
  public void myAdvice(JoinPoint thisJoinPoint) {
    System.out.println(thisJoinPoint);
  }
}

不幸的是,在我用来测试的 AspectJ 版本中,AspectJ 编译器会引发异常。这可能是一个错误,我稍后会研究它并向维护者报告。 更新:我为这个问题创建了AspectJ bug ticket #550494更新 2:该错误已在 AspectJ 1.9.5 中修复。

但是,如果您只使用本机 AspectJ 语法,它就可以工作。唯一的坏消息是,如果您使用 javac + LTW 并依赖 AspectJ 编织器在类加载期间完成切面,这将不再起作用。您必须使用 AspectJ 编译器 ajc 以本机语法编译方面。

package de.scrum_master.aspect;

import javax.sql.DataSource;

import de.scrum_master.app.BaseClass;

public aspect DataSourceConnectionAspect {
  declare parents: BaseClass implements DataSource;

  before() : execution(public java.sql.Connection javax.sql.DataSource+.getConnection(..)) {
    System.out.println(thisJoinPoint);
  }
}

现在控制台日志变为:

Aspect should not kick in
execution(Connection de.scrum_master.app.BaseClass.getConnection())
execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))
Aspect should kick in
execution(Connection de.scrum_master.app.BaseClass.getConnection())
execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))

当然,“方面不应该启动”在这里不再适用,因为现在我们确实希望它启动,当然,BaseClass 现在直接实现了DataSource 接口。

一点免责声明:这种方法只有在基类中确实存在所有接口方法时才有效,幸运的是org.apache.tomcat.jdbc.pool.DataSourceProxy 就是这种情况,即您可以相应地调整我的方面。如果基类只实现部分预期的接口方法,你也可以通过 ITD 以原生语法添加它们,但我不打算在这里详细说明,我的答案已经很长了。

最后但并非最不重要的一点是,使用新方法的字节码如下所示:

Compiled from "BaseClass.java"
public class de.scrum_master.app.BaseClass implements javax.sql.DataSource {
  (...)

  public java.sql.Connection getConnection() throws java.sql.SQLException;
    Code:
       0: getstatic     #58                 // Field ajc$tjp_0:Lorg/aspectj/lang/JoinPoint$StaticPart;
       3: aload_0
       4: aload_0
       5: invokestatic  #64                 // Method org/aspectj/runtime/reflect/Factory.makeJP:(Lorg/aspectj/lang/JoinPoint$StaticPart;Ljava/lang/Object;Ljava/lang/Object;)Lorg/aspectj/lang/JoinPoint;
       8: astore_1
       9: invokestatic  #70                 // Method de/scrum_master/aspect/DataSourceConnectionAspect.aspectOf:()Lde/scrum_master/aspect/DataSourceConnectionAspect;
      12: aload_1
      13: invokevirtual #74                 // Method de/scrum_master/aspect/DataSourceConnectionAspect.ajc$before$de_scrum_master_aspect_DataSourceConnectionAspect$1$19879111:(Lorg/aspectj/lang/JoinPoint;)V
      16: aconst_null
      17: areturn

  public java.sql.Connection getConnection(java.lang.String, java.lang.String) throws java.sql.SQLException;
    Code:
       0: aload_1
       1: astore        4
       3: aload_2
       4: astore        5
       6: getstatic     #77                 // Field ajc$tjp_1:Lorg/aspectj/lang/JoinPoint$StaticPart;
       9: aload_0
      10: aload_0
      11: aload         4
      13: aload         5
      15: invokestatic  #80                 // Method org/aspectj/runtime/reflect/Factory.makeJP:(Lorg/aspectj/lang/JoinPoint$StaticPart;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Lorg/aspectj/lang/JoinPoint;
      18: astore_3
      19: invokestatic  #70                 // Method de/scrum_master/aspect/DataSourceConnectionAspect.aspectOf:()Lde/scrum_master/aspect/DataSourceConnectionAspect;
      22: aload_3
      23: invokevirtual #74                 // Method de/scrum_master/aspect/DataSourceConnectionAspect.ajc$before$de_scrum_master_aspect_DataSourceConnectionAspect$1$19879111:(Lorg/aspectj/lang/JoinPoint;)V
      26: aconst_null
      27: areturn

  (...)
}

如果您比较两个 javap 日志,您不仅会注意到现在它显示为 implements javax.sql.DataSource,而且在旧版本中这两种方法都有 22/32 字节码指令,而在新版本中有仅 17/27。例如,在旧版本中,您会看到 instanceof #76 // class javax/sql/DataSource。在新版本中,不再需要instanceof 检查。

您可以自行决定这是否值得您使用 ITD 和本机语法。无论如何,我个人使用本机语法和 ajc,所以我会这样做。如果您以前从未使用过 AspectJ 编译器并且只使用 LTW,那么您的决定可能会有所不同。是否会有可衡量的性能提升是另一个问题。我假设在涉及 SQL 数据库调用的场景中,可能不是 AspectJ 会消耗您的性能。 ;-) 我只是想知道并回答你的问题。


更新:没有 ITD 的替代解决方案

根据您的评论,您想避免 ITD,即使我认为这是一个干净而优雅的解决方案。但是还有一种方法可以像这样优化切入点匹配和性能:

package de.scrum_master.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AlternativeSolutionAspect {
  @Pointcut("execution(public java.sql.Connection getConnection(..))")
  private static void getConnection() {}

  @Pointcut("within(javax.sql.DataSource+)")
  private static void withinDataSource() {}

  @Pointcut("target(javax.sql.DataSource)")
  private static void targetDataSource() {}

  @Before("withinDataSource() && getConnection()")
  public void interceptStatically(JoinPoint thisJoinPoint) {
    System.out.println("[static] " + thisJoinPoint);
  }

  @Before("!withinDataSource() && getConnection() && targetDataSource()")
  public void interceptDynamically(JoinPoint thisJoinPoint) {
    System.out.println("[dynamic] " + thisJoinPoint);
  }
}

解释:

  • Advice interceptStatically 负责查找“正常”情况下的所有方法执行,即实现接口和相应方法的(基)类。
  • 建议interceptDynamically 负责(外来)其余部分,即实际实例实现接口的方法执行,但方法是在实现接口的(基)类中定义的。与您自己的纯动态解决方案的不同之处在于,我在这里明确排除了可以静态确定的情况。

现在,如果我们将我的DataSourceConnectionAspect 与这个AlternativeSolutionAspect 进行比较,这意味着什么?首先让我添加另一个示例类以使其更清晰:

package de.scrum_master.app;

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

public class SubClassOverridingMethods extends BaseClass implements DataSource {
  @Override
  public Connection getConnection() throws SQLException {
    return super.getConnection();
//    return null;
  }

  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return super.getConnection(username, password);
//    return null;
  }
}

现在我们通过额外的方法调用来扩展驱动应用程序:

package de.scrum_master.app;

import java.sql.SQLException;

public class Application {
  public static void main(String[] args) throws SQLException {
    System.out.println("Aspect should not kick in without ITD, but should with ITD");
    new BaseClass().getConnection();
    new BaseClass().getConnection("user", "pw");

    System.out.println("Aspect should kick in");
    new SubClass().getConnection();
    new SubClass().getConnection("user", "pw");

    System.out.println("Aspect should kick in");
    new SubClassOverridingMethods().getConnection();
    new SubClassOverridingMethods().getConnection("user", "pw");
  }
}

其余的和我上面的例子一样。

DataSourceConnectionAspect 的控制台日志:

Aspect should not kick in without ITD, but should with ITD
execution(Connection de.scrum_master.app.BaseClass.getConnection())
execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))
Aspect should kick in
execution(Connection de.scrum_master.app.BaseClass.getConnection())
execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))
Aspect should kick in
execution(Connection de.scrum_master.app.SubClassOverridingMethods.getConnection())
execution(Connection de.scrum_master.app.BaseClass.getConnection())
execution(Connection de.scrum_master.app.SubClassOverridingMethods.getConnection(String, String))
execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))

在案例 3 中,您会看到 2 个方法调用的 4 行日志输出,因为覆盖方法调用 super.getConnection(..)。如果他们只做某事而不使用超级调用,那么每个方法调用当然只有一个日志行。

AlternativeSolutionAspect 的控制台日志:

Aspect should not kick in without ITD, but should with ITD
Aspect should kick in
[dynamic] execution(Connection de.scrum_master.app.BaseClass.getConnection())
[dynamic] execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))
Aspect should kick in
[static] execution(Connection de.scrum_master.app.SubClassOverridingMethods.getConnection())
[dynamic] execution(Connection de.scrum_master.app.BaseClass.getConnection())
[static] execution(Connection de.scrum_master.app.SubClassOverridingMethods.getConnection(String, String))
[dynamic] execution(Connection de.scrum_master.app.BaseClass.getConnection(String, String))

由于我们在此处不使用 ITD,因此案例 1 不会拦截任何内容。案例 2 是动态拦截的,而在案例 3 中,覆盖方法可以静态确定,超级方法可以动态确定。同样,如果没有超级调用,对于案例 3,每个方法调用我们将只有一行日志输出。

P.S.:你自己的解决方案在超级调用的情况下也会匹配两次,以防万一你想知道。但它会动态匹配两次,使其变慢。

【讨论】:

  • 感谢您的回答。你对 ITD 的想法很好,但我需要按名称知道所有这些类。也可能它改变了应用程序的类层次结构,我不确定我想这样做。顺便说一句,我使用 LTW。我将继续使用我现在使用的切入点。
  • 什么?!我的回答不仅仅是一个“想法”,它正好解决了你的问题。而且它不会改变类层次结构,它只是使基类(例如 Tomcat DataSource)实现接口。 IMO 这是一个错误,它没有实现它。当然,你可以保留你的切入点,但你要求一个性能更好的替代方案,我把它给了你。这就是你问题的重点!
  • 至少有礼貌地接受我的正确答案,而不是拒绝它,现在声称还有其他类似的情况。哪个?命名一个。我无法想象有超过一两个。为此添加另一行代码而不是使用 target() 使用只能在运行时评估的动态切入点编织数百个误报的问题在哪里。感谢您真正“值得”投入时间和精力来正确回答您的问题,然后收到这样的评论。
  • 无论如何,我找到了一个更好的替代方案来替代您自己的不使用 ITD 的解决方案。看我的更新。基本思想是静态确定可以确定的任何内容,并且仅在需要它的情况下使用动态target() 切入点,明确排除静态可确定的内容。性能应该比 ITD 解决方案差,但比您自己的解决方案好。我希望你现在快乐。
  • 请注意 AspectJ bug ticket #550494 在 1.9.5 中已修复。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-10
  • 2014-11-17
  • 1970-01-01
相关资源
最近更新 更多