spring aop的@target与@within的真正区别到底是什么?

37 篇文章 5 订阅

文档里面是这么说的:

@target: Limits matching to join points (the execution of methods when using Spring AOP) where the class of the executing object has an annotation of the given type.
@within: Limits matching to join points within types that have the given annotation (the execution of methods declared in types with the given annotation when using Spring AOP).

看不出来啥区别,文档后面还举了个例子:

@target(org.springframework.transaction.annotation.Transactional):Any join point (method execution only in Spring AOP) where the target object has a @Transactional annotation
@within(org.springframework.transaction.annotation.Transactional):Any join point (method execution only in Spring AOP) where the declared type of the target object has an @Transactional annotation

貌似一个是说目标object的类上有注解,一个是说声明方法的object的类上有注解,还是云山雾绕,不知道他要表达啥意思。

我们来结合一个具体的业务场景来说一下这个问题,比如,现在我们要用aop拦截系统中所有的@Controller,然后记录用户都访问了系统中的哪些接口,首先我们用@target来搞一下:

//这是aop的切面,用@target拦截所有的@RestController
@Aspect
public class DemoAdvice {
    @Pointcut("@target(org.springframework.web.bind.annotation.RestController)")
    public void pointCut(){
    }
    @Before("pointCut()")
    public void before(){
        log.info("---------before-------");
    }
}
//加一个配置类,把切面加入到容器
@Configuration
public class DemoConfig {
    @Bean
    public DemoAdvice demoAdvice(){
        return new DemoAdvice();
    }
    //同时加入了一个业务的Service
    @Bean
    public Service1 service(){
        return new Service1();
    }
}
//service里面啥也没有
public class Service1 {
}
//搞个controller测试一下
@RestController
public class DemoController {
    @Autowired
    private Service1 service1;
    @GetMapping("/hello")
    public String hello(){
        System.out.println("service1.getClass():"+service1.getClass().getName());
        return "hello";
    }
}

很不幸,系统竟然没法启动,日志中有很多类似这样的东西:

2020-06-17 20:31:43.218 INFO 1052 — [ main] o.s.aop.framework.CglibAopProxy :
Unable to proxy interface-implementing method [public final void org.springframework.web.filter.OncePerRequestFilter.doFilter(javax.servlet.ServletRequest,javax.servlet.ServletResponse,javax.servlet.FilterChain) throws javax.servlet.ServletException,java.io.IOException] because it is marked as final: Consider using interface-based JDK proxies instead!
2020-06-17 20:31:43.218 INFO 1052 — [ main] o.s.aop.framework.CglibAopProxy :
Unable to proxy interface-implementing method [public final void org.springframework.web.filter.GenericFilterBean.init(javax.servlet.FilterConfig) throws javax.servlet.ServletException] because it is marked as final: Consider using interface-based JDK proxies instead!

貌似是说没法给OncePerRequestFilter生成动态代理类,因为方法是final的,很奇怪啊,我们没有要求给它生成代理啊!如果把我们的切面换成@within试试,就正常了!

我们用一个相对简单的例子重现下这个问题,因为SpringMVC中自动配置了太多的东西,我们还是用相对简单的AnnotationConfigApplicationContext来测试下。

首先我们先自定义一个注解:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnno {
}

定义切面拦截自定义的注解:

@Aspect
public class Advice2 {
    //@Pointcut("@within(com.github.xjs.attarget.demo2.MyAnno)")
    @Pointcut("@target(com.github.xjs.attarget.demo2.MyAnno)")
    public void pointCut(){
    }
    @Before("pointCut()")
    public void before(){
        log.info("---------before-------");
    }
}

Service2上添加自定义的注解:

@MyAnno
public class Service2 {
    public void hello() {
        System.out.println("s2 hello");
    }
}

Service3就是三个普通的类:


public class Service3 {
    public void hello() {
        System.out.println("s3 hello");
    }
}

用一个配置类把他们都加载起来:

@EnableAspectJAutoProxy
@Configuration
public class Config2 {
    @Bean
    public Advice2 advice2(){
        return new Advice2();
    }
    @Bean
    public Service2 service2(){
        return new Service2();
    }
    @Bean
    public Service3 service3(){
        return new Service3();
    }
}

写一个测试类:

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config2.class);
    Service2 s2 = ctx.getBean(Service2.class);
    s2.hello();
    System.out.println(s2.getClass().getName());
    Service3 s3 = ctx.getBean(Service3.class);
    s3.hello();
    System.out.println(s3.getClass().getName());
}

输出结果如下:

---------before-------
s2 hello
com.github.xjs.attarget.demo2.Service2$$EnhancerBySpringCGLIB$$5503fe34
s3 hello
com.github.xjs.attarget.demo2.Service3$$EnhancerBySpringCGLIB$$689f86d7

以上代码可以看出来,s2和s3都是用cglib生成了动态代理类,s2我们能理解,因为s2上有要拦截的注解,s3上没有要拦截的注解为啥还要生成动态代理类呢?

我们换成@within再试一下,输出结果:

---------before-------
s2 hello
com.github.xjs.attarget.demo2.Service2$$EnhancerBySpringCGLIB$$9fae5acb
s3 hello
com.github.xjs.attarget.demo2.Service3

这回看上去就比较正常了。

stackoverflow上有老外提到了这个问题:https://stackoverflow.com/questions/52992365/spring-creates-proxy-for-wrong-classes-when-using-aop-class-level-annotation/53452483

When using spring AOP with class level annotations, spring context.getBean seems to always create and return a proxy or interceptor for every class, wether they have the annotation or not.
This behavior is only for class level annotation. For method level annotations, or execution pointcuts, if there is no need for interception, getBean returns a POJO.

对于这个问题,答案可以参考下aspectj的文档:
https://www.eclipse.org/aspectj/doc/released/adk15notebook/annotations-pointcuts-and-advice.html#runtime-type-matching-and-context-exposure

The this(), target(), and args() pointcut designators allow matching based on the runtime type of an object, as opposed to the statically declared type. In AspectJ 5, these designators are supplemented with three new designators : @this() (read, “this annotation”), @target(), and @args().
The forms of @this() and @target() that take a single annotation name are analogous to their counterparts that take a single type name. They match at join points where the object bound to this (or target, respectively) has an annotation of the specified type.
@within(Foo)
Matches any join point where the executing code is defined within a type which has an annotation of type Foo.

结合之前的文档,我们可以得出结论,@target是匹配的对象的运行时类型,而@within是匹配的定义正在执行的代码(方法)的类,一个方法定义在哪个类中这个是明确可以知道的,但是对象的运行时类型只有在运行时才能知道,所以@target才给所有的bean都生成了代理类,如果无法生成代理类就会出问题了,比如:

//明确指定用cglib
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Configuration
public class Config2 {
    @Bean
    public Advice2 advice2(){
      return new Advice2();
    }
    //显然,String是不可以有子类的
    @Bean
    public String hello(){
    return "hello";
    }
}

上面的配置中,我们明确设置了用cglib代理,再配置上hello那个bean以后,显然还是会报错的,因为String是不可以有子类的。

我们再来看下,运行时执行的方法的类与定义方法的类不是同一个类的情况,比如在继承的场景下。

首先定义两个注解:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AnnoFather {
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AnnoSon {
}

然后定义两个类,上面分别加上这两个注解:


//父类上添加@AnnoFather
@AnnoFather
public class Father {
    public void fun1(){
        System.out.println("father fun1");
    }
    public void fun2(){
        System.out.println("father fun2");
    }
}
//子类上添加@AnnoSon,并且子类重写了父类的fun1()方法
@AnnoSon
public class Son extends Father {
    @Override
    public void fun1(){
        System.out.println("son fun1");
    }
}

添加切面:


@Before("@within(com.github.xjs.attarget.demo3.AnnoFather)")
public void before1(){
  System.out.println("---------@within @AnnoFather-------");
}
@Before("@target(com.github.xjs.attarget.demo3.AnnoFather))")
public void before2() {
  System.out.println("---------@target @AnnoFather-------");
}
@Before("@within(com.github.xjs.attarget.demo3.AnnoSon)")
public void before3(){
  System.out.println("---------@within @AnnoSon-------");
}
@Before("@target(com.github.xjs.attarget.demo3.AnnoSon)")
public void before4(){
  System.out.println("---------@target @AnnoSon-------");
}

测试一下:

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config3.class);
    Father father = ctx.getBean("father", Father.class);
    father.fun1();
    /**
     *  ---------@within @AnnoFather-------
     * ---------@target @AnnoFather-------
     * father fun1
     * */
    father.fun2();
    /**
     * ---------@within @AnnoFather-------
     * ---------@target @AnnoFather-------
     * father fun2
     * */
    Son son = (Son)ctx.getBean("son", Son.class);
    son.fun1();
    /**
     * ---------@within @AnnoSon-------
     * ---------@target @AnnoSon-------
     * son fun1
     * */
    son.fun2();
    /**
     * ---------@within @AnnoFather-------
     * ---------@target @AnnoSon-------
     * father fun2
     * fun2()的运行时对象是son,但是fun2()却是在father中定义的。
     * */
}

此外,@target也可以用execution(* (@cn.javass…Secure ).(…))来代替,@annotation也可以用execution(@java.lang.Deprecated * *(…))来代替,如下:

@Component
@Aspect
public class AspectA {
  @Around("execution(* (@MyAnnotation *).*(..)) || execution(@MyAnnotation * *(..))")
  public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
    MyAnnotation myAnnotation = null;
    for (Annotation annotation : ((MethodSignature)joinPoint.getSignature()).getMethod().getDeclaredAnnotations()) {
      if (annotation instanceof MyAnnotation) {
        myAnnotation = (MyAnnotation) annotation;
        break;
      }
    }
    if (myAnnotation == null) {
      myAnnotation = joinPoint.getTarget().getClass().getAnnotationsByType(MyAnnotation.class)[0];
    }
    System.out.println("AspectA: myAnnotation target:" + joinPoint.getTarget().getClass().getSimpleName());
    System.out.println(" condition:" + myAnnotation.condition());
    System.out.println(" key:" + myAnnotation.key());
    System.out.println(" value:" + myAnnotation.value());
    return joinPoint.proceed();
  }
}

结论

  • 1.@target匹配的是执行方法的对象的class上是否有注解,@within匹配的是定义方法的对象的class上是否有注解,由于运行时多态的特性,执行方法的对象和定义方法的对象可能不是同一个对象。

  • 2.定义方法的类是可以提前知道的,这个是静态的,但是执行方法的类是无法提前知道的,是运行时动态的,因此,在@target的场景下,spring aop给所有的bean都生成了代理类,如果无法生成代理就会报错。比如:cglib遇到final的时候。

  • 3.@target一定要慎用,可以用execution(* (@cn.javass…Secure ).(…))来代替。

扫码查看全文:
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值