一段Spring代码引起的调用绑定总结

129 篇文章 8 订阅
17 篇文章 0 订阅

代码

Java代码  收藏代码

  1. @Component  
  2. public class B {  
  3.     void test() {  
  4.         System.out.println("hello");  
  5.     }  
  6. }  

Java代码  收藏代码

  1. @Component  
  2. public class A {  
  3.     @Autowired  
  4.     private B b;  
  5.     public final void test() {  
  6.         b.test();  
  7.     }  
  8. }  

 

Java代码  收藏代码

  1. @Component  
  2. @Aspect  
  3. public class MyAspect {  
  4.     @Before("execution(* *(..))")  
  5.     public void before() {  
  6.   
  7.     }  
  8. }  

 

Java代码  收藏代码

  1. @Configuration  
  2. @ComponentScan  
  3. @EnableAspectJAutoProxy(proxyTargetClass = true)  
  4. public class Test {  
  5.     public static void main(String[] args) {  
  6.         AnnotationConfigApplicationContext ctx =  
  7.                 new AnnotationConfigApplicationContext(Test.class);  
  8.         A a = ctx.getBean(A.class);  
  9.         a.test();  
  10.     }  
  11. }  

 

问题 

1、A通过字段注入方式注入B ;

2、A的test方法是final的,因此该方法不能被代理;

3、被代理的对象的调用顺序:

    Proxy.test()

       --->Aspect Before/Around Advice

      ---->Target.test()

      ---->Aspect After/Around Advice

即当某个目标对象被代理后,我们首先调用代理对象的方法,其首先调用切面的前置增强/环绕增强,然后调用目标对象的方法,最后调用后置/环绕增强完成整个调用流程。

 

但是我们知道如果是基于CGLIB的代理:

final的类不能生成代理对象;因为final的类不能生成代理对象;

final的方法不能被代理;但是还是能生成代理对象的;

 

在我们的示例里,A的test方法是无法被代理的,但是A还是会生成一个代理对象(因为我们的切入点是execution(* *(..)),还是可以对如toString()之类的方法代理的):

 

即如果调用a.toString()相当于:

   proxy.toString() [com.github.zhangkaitao.A$$EnhancerByCGLIB$$73d79efe]

   ---->MyAspect.before() 

  ----->target.toString() [com.github.zhangkaitao.A]

 

但是如果调用a.test()相当于:

   proxy.test() [com.github.zhangkaitao.A$$EnhancerByCGLIB$$73d79efe]

 

 

当我们直接调用生成的代理对象的test方法。接着会得到空指针异常:

写道

Exception in thread "main" java.lang.NullPointerException
at com.github.zhangkaitao.A.test(A.java:16)
at com.github.zhangkaitao.Test.main(Test.java:21)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

从异常可以看出是A.test()方法中的b对象是空;

 

但是我们发现b对象是注入了,但是注入给的是目标对象,而代理对象是没有注入的,请看debug信息:

 

从上图可以看出,目标对象的b注入了;而生成的代理对象的b是没有值的;又因为我们调用“代理对象.final方法()”是属于编译期绑定,所以会抛出如上的空指针异常。也就是此问题还是因为对象与方法的绑定问题造成的。

 

调用绑定

所谓调用绑定,即当我们使用“对象.字段”/“对象.方法()”调用时,对象与字段/方法之间是如何绑定的;此处有两种绑定:编译期绑定与运行期绑定。

 

编译期绑定:对象与字段/方法之间的绑定关系发生在写代码期间(即编译期间),即它们的关系在编译期间(写完代码)就确定了,如:

Java代码  收藏代码

  1. public class StaticBindTest {  
  2.     static class A {  
  3.         public int i = 1;  
  4.         public static void hello() {  
  5.             System.out.println("1");  
  6.         }  
  7.     }  
  8.     static class B extends A {  
  9.         public int i = 2;  
  10.         public static void hello() {  
  11.             System.out.println("2");  
  12.         }  
  13.     }  
  14.   
  15.     public static void main(String[] args) {  
  16.         A a = new B();  
  17.         System.out.println(a.i);  
  18.         a.hello();  
  19.     }  
  20. }  

如上代码将输出1,即A的i值,而不是B的i值;这就是所谓的编译期绑定,即访问的字段/方法绑定到声明类型上,而不是运行时的那个对象的类型上。

 

还有如:

Java代码  收藏代码

  1. public class StaticBindTest2 {  
  2.     static class A {  
  3.         public void hello(Number i) {  
  4.             System.out.println("Number");  
  5.         }  
  6.         public void hello(Integer i) {  
  7.             System.out.println("Integer");  
  8.         }  
  9.         public void hello(Long i) {  
  10.             System.out.println("Long");  
  11.         }  
  12.     }  
  13.     public static void main(String[] args) {  
  14.         A a = new A();  
  15.         Number i = Integer.valueOf(1);  
  16.         Number l = Long.valueOf(1L);  
  17.         a.hello(i);  
  18.         a.hello(l);  
  19.     }  
  20. }  

都讲输出Number,而不是Integer和Long;这也是编译期绑定;即方法参数绑定时根据声明时的类型进行绑定也叫做静态绑定/早绑定。

 

如果我们使用“a.hello(null);”调用会发生什么情况呢?此时就会发生二义性,即绑定到Integer/Long参数上都可以的,所以我们应该使用“a.hello((Integer)null);”来强制调用。还有在绑定时都是先子类型(Integer/Long)到父类型(Number)进行绑定。

 

编译期绑定:调用的都是声明的类型的字段/方法或者根据参数声明时类型调用重载方法;静态字段/方法、private/final方法、实例对象的字段/重载方法都是编译期绑定,即除了方法覆盖都是编译期绑定;也可以说成除了运行期绑定之外的绑定都是编译期绑定。为什么这么说呢?接着往下看。

 

运行期绑定“对象.方法()”是根据程序运行期间对象的实际类型来绑定方法的,如:

Java代码  收藏代码

  1. public class DynamicBindTest {  
  2.     static class A {  
  3.         public void hello() {  
  4.             System.out.println("a");  
  5.         }  
  6.     }  
  7.     static class B extends A {  
  8.         public void hello() {  
  9.             System.out.println("b");  
  10.         }  
  11.     }  
  12.   
  13.     public static void main(String[] args) {  
  14.         A a = new B();  
  15.         a.hello();  
  16.     }  
  17. }  

如上代码将输出b,即说明了hello()方法调用不是根据声明时类型决定,而是根据运行期间的那个对象类型决定的;也叫做动态绑定/迟绑定。

 

运行期绑定:“对象.方法()”是根据运行期对象的实际类型决定的;即new的哪个对象就绑定该方法到那个对象类型上;只有方法覆盖是运行期绑定;其他都是编译期绑定;该机制用于实现多态。

 

在Java中,除了方法覆盖是运行期绑定,其他都是静态绑定就好理解了。

 

单分派与双分派

单分派:调用对象的方法是由对象的类型决定的;

多分派:调用对象的方法是由对象的类型决定的和其他因素(如方法参数类型)决定的;双分派是多分派的特例。

 

Java是一种单分派语言,可以通过如访问者设计模式来模拟多分派。

 

比如之前的重载的编译期绑定,和覆盖的运行期绑定,都是根据对象类型(不管是声明时类型/运行时类型)决定调用的哪个方法;跟方法参数实际运行时类型无关(而与声明时类型有关)。

 

接下来看一个双分派的例子:

Java代码  收藏代码

  1. public class DoubleDispatchTest {  
  2.   
  3.     static interface Element {  
  4.         public void accept(Visitor v);  
  5.     }  
  6.     static class AElement implements Element {  
  7.         public void accept(Visitor v) {  
  8.             v.visit(this);  
  9.         }  
  10.     }  
  11.     static class BElement implements Element {  
  12.         public void accept(Visitor v) {  
  13.             v.visit(this);  
  14.         }  
  15.     }  
  16.   
  17.     static interface Visitor {  
  18.         public void visit(AElement aElement);  
  19.         public void visit(BElement bElement);  
  20.     }  
  21.   
  22.     static class Visitor1 implements Visitor {  
  23.         public void visit(AElement aElement) {  
  24.             System.out.println("1A");  
  25.         }  
  26.         public void visit(BElement bElement) {  
  27.             System.out.println("1B");  
  28.         }  
  29.     }  
  30.   
  31.     static class Visitor2 implements Visitor {  
  32.         public void visit(AElement aElement) {  
  33.             System.out.println("2A");  
  34.         }  
  35.         public void visit(BElement bElement) {  
  36.             System.out.println("2B");  
  37.         }  
  38.     }  
  39.   
  40.   
  41.     public static void main(String[] args) {  
  42.         Element a = new AElement();  
  43.         Element b = new BElement();  
  44.         Visitor v1 = new Visitor1();  
  45.         Visitor v2 = new Visitor2();  
  46.         a.accept(v1);  
  47.         a.accept(v2);  
  48.         b.accept(v1);  
  49.         b.accept(v2);  
  50.     }  
  51. }  

此处可以看出如"a.accept(v)",根据Element类型和Visitor类型来决定调用的是哪个方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值