@target和@within之间的区别(Spring AOP)

15

Spring手册中提到:

目标对象具有@Transactional注释的任何连接点(仅在Spring AOP中进行方法执行): @target(org.springframework.transaction.annotation .Transactional)

目标对象的声明类型具有@Transactional注解的任何连接点(仅在Spring AOP中进行方法执行): @within(org.springframework.transaction.annotation .Transactional)

但我没有看到它们之间的区别!

我试着Google一下:

两者之间的一个区别是,@within()在静态匹配时需要对应的注释类型只具有CLASS保留。而@target()在运行时匹配,需要相同的注释具有RUNTIME保留。除此之外,在Spring的上下文中,这两个选择的连接点没有区别。

因此,我试着添加带有CLASS保留的自定义注释,但Spring抛出了异常(因为注释必须具有RUNTIME保留)。

2个回答

23

你之所以没有看到任何不同,是因为Spring AOP虽然使用了AspectJ语法,但实际上只模拟了其功能的有限子集。由于Spring AOP基于动态代理,它仅提供公共、非静态方法执行的拦截。(使用CGLIB代理时,还可以拦截软件包级别和受保护的方法。)而AspectJ则可以拦截方法调用(不仅仅是执行)、成员字段访问(静态和非静态)、构造函数调用/执行、静态类初始化等等。

那么,让我们构建一个非常简单的AspectJ示例:

标记注释:

package de.scrum_master.app;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {}

驱动程序应用:

package de.scrum_master.app;

@MyAnnotation
public class Application {
  private int nonStaticMember;
  private static int staticMember;

  public void doSomething() {
    System.out.println("Doing something");
    nonStaticMember = 11;
  }

  public void doSomethingElse() {
    System.out.println("Doing something else");
    staticMember = 22;
  }

  public static void main(String[] args) {
    Application application = new Application();
    application.doSomething();
    application.doSomethingElse();
  }
}

方面:

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 MyAspect {
  @Before("@within(de.scrum_master.app.MyAnnotation) && execution(public !static * *(..))")
  public void adviceAtWithin(JoinPoint thisJoinPoint) {
    System.out.println("[@within] " + thisJoinPoint);
  }

  @Before("@target(de.scrum_master.app.MyAnnotation) && execution(public !static * *(..))")
  public void adviceAtTarget(JoinPoint thisJoinPoint) {
    System.out.println("[@target] " + thisJoinPoint);
  }
}
请注意,我在这里通过向两个切入点添加 && execution(public !static * *(..)) 来模拟Spring AOP的行为。 控制台日志:
[@within] execution(void de.scrum_master.app.Application.doSomething())
[@target] execution(void de.scrum_master.app.Application.doSomething())
Doing something
[@within] execution(void de.scrum_master.app.Application.doSomethingElse())
[@target] execution(void de.scrum_master.app.Application.doSomethingElse())
Doing something else

并不意外。这正是您在Spring AOP中看到的内容。 现在,如果您从两个切入点中删除&& execution(public !static * *(..))部分,在Spring AOP中输出仍然相同,但在AspectJ中(例如,在Spring中激活AspectJ LTW时),其会发生变化:

[@within] staticinitialization(de.scrum_master.app.Application.<clinit>)
[@within] execution(void de.scrum_master.app.Application.main(String[]))
[@within] call(de.scrum_master.app.Application())
[@within] preinitialization(de.scrum_master.app.Application())
[@within] initialization(de.scrum_master.app.Application())
[@target] initialization(de.scrum_master.app.Application())
[@within] execution(de.scrum_master.app.Application())
[@target] execution(de.scrum_master.app.Application())
[@within] call(void de.scrum_master.app.Application.doSomething())
[@target] call(void de.scrum_master.app.Application.doSomething())
[@within] execution(void de.scrum_master.app.Application.doSomething())
[@target] execution(void de.scrum_master.app.Application.doSomething())
[@within] get(PrintStream java.lang.System.out)
[@within] call(void java.io.PrintStream.println(String))
Doing something
[@within] set(int de.scrum_master.app.Application.nonStaticMember)
[@target] set(int de.scrum_master.app.Application.nonStaticMember)
[@within] call(void de.scrum_master.app.Application.doSomethingElse())
[@target] call(void de.scrum_master.app.Application.doSomethingElse())
[@within] execution(void de.scrum_master.app.Application.doSomethingElse())
[@target] execution(void de.scrum_master.app.Application.doSomethingElse())
[@within] get(PrintStream java.lang.System.out)
[@within] call(void java.io.PrintStream.println(String))
Doing something else
[@within] set(int de.scrum_master.app.Application.staticMember)

仔细观察后,您会发现被拦截的@within()连接点要多得多,但也有一些额外的@target()连接点,例如之前提到的 call() 连接点,以及非静态字段的 set() 和在构造函数执行之前发生的对象 initialization()

仅查看@target()时,我们可以看到这个:

[@target] initialization(de.scrum_master.app.Application())
[@target] execution(de.scrum_master.app.Application())
[@target] call(void de.scrum_master.app.Application.doSomething())
[@target] execution(void de.scrum_master.app.Application.doSomething())
Doing something
[@target] set(int de.scrum_master.app.Application.nonStaticMember)
[@target] call(void de.scrum_master.app.Application.doSomethingElse())
[@target] execution(void de.scrum_master.app.Application.doSomethingElse())
Doing something else
对于这些方面输出行的每一行,我们还可以看到相应的@within()匹配。现在让我们集中精力于不同的地方,过滤掉差异的输出:
[@within] staticinitialization(de.scrum_master.app.Application.<clinit>)
[@within] execution(void de.scrum_master.app.Application.main(String[]))
[@within] call(de.scrum_master.app.Application())
[@within] preinitialization(de.scrum_master.app.Application())
[@within] get(PrintStream java.lang.System.out)
[@within] call(void java.io.PrintStream.println(String))
Doing something
[@within] get(PrintStream java.lang.System.out)
[@within] call(void java.io.PrintStream.println(String))
Doing something else
[@within] set(int de.scrum_master.app.Application.staticMember)

这里你可以看到,按照出现的顺序有:

  • 静态类初始化
  • 静态方法执行
  • 构造函数调用(但还未执行)
  • 在另一个类中访问成员变量(如System.out
  • 从另一个类调用方法(如PrintStream.println(String)
  • 设置静态类成员

所有这些连接点的共同之处是什么?没有目标对象,因为我们要么谈论静态方法或成员、静态类初始化、对象预初始化(尚未定义this),或从其他没有承载我们正在拦截的注释的类中调用/访问内容。

所以你可以看到,在AspectJ中这两个连接点之间存在重大差异,在Spring AOP中由于其限制,它们并不明显。

我给你的建议是,如果您的意图是拦截目标对象实例中的非静态行为,请使用@target()。如果您决定在Spring中激活AspectJ模式,甚至将一些代码移植到非Spring、启用方面的应用程序中,这将使转换更容易。


更新2022-07-15:当我最初撰写这个答案时,我忘记了注释不在类类型上而在接口类型上的情况。在这种情况下,即使在Spring AOP中,您也会注意到一个区别,因为@within仍然会匹配代理的接口,而@target则不会。为什么呢?

  • @within 询问实际类型是否已注释。由于类继承,实现或扩展接口类型@Marker MyInterface,也是MyInterface,因此询问该接口类型是否具有@Marker注释会得出积极结果。

  • @target 询问运行时类型,即Spring AOP代理,是否具有@Marker注释,但它没有。注释,即使带有@Inherited元注释,也只能从类到类继承,不能从接口到类或从超级方法到覆盖方法继承,参见javadoc。因此,实现接口的代理类不具有与实现的接口相同的注释。因此,在依赖于@target的连接点将不匹配。


更新于2022年7月15日:当我最初撰写这篇答案时,我忘记了注解不是在类类型上而是在接口类型上的情况。请查看我的更新以获取更多信息。 - kriegaex

3
你引用的信息是正确的,然而只有 `@target` 切入点需要使用 `RUNTIME` 保留注释,而 `@within` 只需要 `CLASS` 保留。
让我们考虑以下两个简单的注释:

ClassRetAnnotation.java

package mypackage;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.CLASS)
public @interface ClassRetAnnotation {}

RuntimeRetAnnotation.java

package mypackage;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimeRetAnnotation {}

现在,如果您定义了以下这种方面,就不会在运行时出现异常
@Component
@Aspect
public class MyAspect {

    @Before("@within(mypackage.ClassRetAnnotation)")
    public void within() { System.out.println("within"); }

    @Before("@target(mypackage.RuntimeRetAnnotation)")
    public void target() { System.out.println("target"); }
}

我希望这个例子有助于澄清您指出的微妙差别。
Spring参考文档:https://docs.spring.io/spring/docs/5.0.x/spring-framework-reference/core.html#aop-pointcuts

3
我看不出这如何回答楼主的问题。 - kriegaex
@kriegaex OP指出Spring会抛出异常,因为注释必须具有RUNTIME保留;我想证明这仅适用于@target切入点设计器。无论如何,我非常喜欢您详细的答案,但我注意到问题的上下文显然仅限于Spring AOP,因此我的答案仅限于OP报告的问题。干杯。 - Robert Hume
2
你对问题的范围和你的回答是正确的。但是因为Spring AOP借用了AspectJ的切入点描述符,并且它们的行为完全相同(除了注释保留方面的轻微差异),我认为在聚焦于Spring AOP中剩余部分之前,扩大范围以先了解整个情况确实是必要的。我的最终建议(这里没有双关语)也包含了一个提示,即为什么了解所有这些对于Spring AOP用户来说可能很重要:他们中的许多人后来会迁移到AspectJ,以克服Spring AOP的限制。 - kriegaex
2
他们迁移后发现方面的行为不同,但由于他们不理解为什么以及如何轻松地驯服AspectJ方面,他们很快放弃了,回到Spring AOP,对于可以用AspectJ美观简单地完成的事情使用可怕的解决方法。 - kriegaex
1
@kriegaex 我同意,而且在Spring AOP中它们的行为是相同的!再次感谢您,我只是在我的回答中留下了我的意见。 - Robert Hume

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接