一段Spring代碼引起的調用綁定總結

代碼

@Component
public class B {
    void test() {
        System.out.println("hello");
    }
}
@Component
public class A {
    @Autowired
    private B b;
    public final void test() {
        b.test();
    }
}

 

@Component
@Aspect
public class MyAspect {
    @Before("execution(* *(..))")
    public void before() {

    }
}

 

@Configuration
@ComponentScan
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx =
                new AnnotationConfigApplicationContext(Test.class);
        A a = ctx.getBean(A.class);
        a.test();
    }
}

 

問題 

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方法()”是屬於編譯期綁定,所以會拋出如上的空指針異常。也就是此問題還是因爲對象與方法的綁定問題造成的。

 

調用綁定

所謂調用綁定,即當我們使用“對象.字段”/“對象.方法()”調用時,對象與字段/方法之間是如何綁定的;此處有兩種綁定:編譯期綁定與運行期綁定。

 

編譯期綁定:對象與字段/方法之間的綁定關係發生在寫代碼期間(即編譯期間),即它們的關係在編譯期間(寫完代碼)就確定了,如:

public class StaticBindTest {
    static class A {
        public int i = 1;
        public static void hello() {
            System.out.println("1");
        }
    }
    static class B extends A {
        public int i = 2;
        public static void hello() {
            System.out.println("2");
        }
    }

    public static void main(String[] args) {
        A a = new B();
        System.out.println(a.i);
        a.hello();
    }
}

如上代碼將輸出1,即A的i值,而不是B的i值;這就是所謂的編譯期綁定,即訪問的字段/方法綁定到聲明類型上,而不是運行時的那個對象的類型上。

 

還有如:

public class StaticBindTest2 {
    static class A {
        public void hello(Number i) {
            System.out.println("Number");
        }
        public void hello(Integer i) {
            System.out.println("Integer");
        }
        public void hello(Long i) {
            System.out.println("Long");
        }
    }
    public static void main(String[] args) {
        A a = new A();
        Number i = Integer.valueOf(1);
        Number l = Long.valueOf(1L);
        a.hello(i);
        a.hello(l);
    }
}

都講輸出Number,而不是Integer和Long;這也是編譯期綁定;即方法參數綁定時根據聲明時的類型進行綁定也叫做靜態綁定/早綁定。

 

如果我們使用“a.hello(null);”調用會發生什麼情況呢?此時就會發生二義性,即綁定到Integer/Long參數上都可以的,所以我們應該使用“a.hello((Integer)null);”來強制調用。還有在綁定時都是先子類型(Integer/Long)到父類型(Number)進行綁定。

 

編譯期綁定:調用的都是聲明的類型的字段/方法或者根據參數聲明時類型調用重載方法;靜態字段/方法、private/final方法、實例對象的字段/重載方法都是編譯期綁定,即除了方法覆蓋都是編譯期綁定;也可以說成除了運行期綁定之外的綁定都是編譯期綁定。爲什麼這麼說呢?接着往下看。

 

運行期綁定“對象.方法()”是根據程序運行期間對象的實際類型來綁定方法的,如:

public class DynamicBindTest {
    static class A {
        public void hello() {
            System.out.println("a");
        }
    }
    static class B extends A {
        public void hello() {
            System.out.println("b");
        }
    }

    public static void main(String[] args) {
        A a = new B();
        a.hello();
    }
}

如上代碼將輸出b,即說明了hello()方法調用不是根據聲明時類型決定,而是根據運行期間的那個對象類型決定的;也叫做動態綁定/遲綁定。

 

運行期綁定:“對象.方法()”是根據運行期對象的實際類型決定的;即new的哪個對象就綁定該方法到那個對象類型上;只有方法覆蓋是運行期綁定;其他都是編譯期綁定;該機制用於實現多態。

 

在Java中,除了方法覆蓋是運行期綁定,其他都是靜態綁定就好理解了。

 

單分派與雙分派

單分派:調用對象的方法是由對象的類型決定的;

多分派:調用對象的方法是由對象的類型決定的和其他因素(如方法參數類型)決定的;雙分派是多分派的特例。

 

Java是一種單分派語言,可以通過如訪問者設計模式來模擬多分派。

 

比如之前的重載的編譯期綁定,和覆蓋的運行期綁定,都是根據對象類型(不管是聲明時類型/運行時類型)決定調用的哪個方法;跟方法參數實際運行時類型無關(而與聲明時類型有關)。

 

接下來看一個雙分派的例子:

public class DoubleDispatchTest {

    static interface Element {
        public void accept(Visitor v);
    }
    static class AElement implements Element {
        public void accept(Visitor v) {
            v.visit(this);
        }
    }
    static class BElement implements Element {
        public void accept(Visitor v) {
            v.visit(this);
        }
    }

    static interface Visitor {
        public void visit(AElement aElement);
        public void visit(BElement bElement);
    }

    static class Visitor1 implements Visitor {
        public void visit(AElement aElement) {
            System.out.println("1A");
        }
        public void visit(BElement bElement) {
            System.out.println("1B");
        }
    }

    static class Visitor2 implements Visitor {
        public void visit(AElement aElement) {
            System.out.println("2A");
        }
        public void visit(BElement bElement) {
            System.out.println("2B");
        }
    }


    public static void main(String[] args) {
        Element a = new AElement();
        Element b = new BElement();
        Visitor v1 = new Visitor1();
        Visitor v2 = new Visitor2();
        a.accept(v1);
        a.accept(v2);
        b.accept(v1);
        b.accept(v2);
    }
}

此處可以看出如"a.accept(v)",根據Element類型和Visitor類型來決定調用的是哪個方法。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章