国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Java代理之jdk動態(tài)代理+應用場景實戰(zhàn)

這篇具有很好參考價值的文章主要介紹了Java代理之jdk動態(tài)代理+應用場景實戰(zhàn)。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

本文將先介紹jdk動態(tài)代理的基本用法,并對其原理和注意事項予以說明。之后將以兩個最常見的應用場景為例,進行代碼實操。這兩個應用場景分別是攔截器聲明性接口,它們在許多開發(fā)框架中廣泛使用。比如在spring和mybatis中均使用了攔截器模式,在mybatis中還利用動態(tài)代理來實現(xiàn)聲明性接口的功能。因此,掌握動態(tài)代理的原理和代碼書寫方式,對閱讀理解這些開源框架非常有益。

文中的示例代碼基于jdk8編寫,且都經(jīng)過驗證,但在將代碼遷移到博客的過程中,難免存在遺漏。如果您將代碼復制到自己的IDE后無法運行,或存在語法錯誤,請在評論中留言指正 ??

小示例

先來看一個jdk代理的最小demo

點擊查看代碼
package demo.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JdkProxyBasicDemo {

    // ⑴ 定義業(yè)務接口
    interface BusinessInterface {
        void greeting(String str);
    }

    // ⑵ 編寫代理邏輯處理類
    static class ProxyLogicHandler implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.printf("運行的代理類為: %s\n", proxy.getClass().getName());
            System.out.printf("調用的代理方法為: %s\n", method.getName);
            System.out.printf("調用方法的參數(shù)為: %s\n", args[0]);
            System.out.println("請在這里插入代碼邏輯代碼...");  // ⑵.1
            return null;                                   // ⑵.2
        }
    }

    // ⑶ 生成代理實例,并使用
    public static void main(String[] args) {
        ProxyLogicHandler proxyLogicHandler = new ProxyLogicHandler();
        Class[] interfaces = new Class[]{BusinessInterface.class},
        BusinessInterface businessProxy = (BusinessInterface) Proxy.newProxyInstance(BusinessInterface.class.getClassLoader(), proxyLogicHandler);
        businessProxy.greeting("Hello, Jdk Proxy");
    }
}

上述代碼執(zhí)行后的輸出結果如下:

運行的代理類為: class com.sun.proxy.$Proxy0
調用的代理方法為: greeting
調用方法的參數(shù)為: Hello, Jdk Proxy
請在這里插入代理的邏輯代碼...

其中倒數(shù)第二行的businessProxy變量,就是一個代理對象,它是BusinessInterface接口的一個實例,但我們并沒有編寫這個接口的實現(xiàn)類,而是通過Proxy.newProxyInstance方法生成出了該接口的實例。那么這個動態(tài)代理實例對應的Class長什么樣子呢?上面的結果輸出中已經(jīng)打印出來了,這個代理類名稱為com.sun.proxy.$Proxy0。實際上,如果我們再為另外一個接口生成代理對象的話,它的Class名稱為com.sun.proxy.$Proxy1,依次類推。

還有一個值得關注的問題:最重要的邏輯代碼應該寫在哪里?答案是寫在InvocationHandler這個接口的invoke()方法中,也就是上面示例代碼的第⑵處。由此可以看出:代理對象實際要執(zhí)行的代碼,就是invoke()方法中的代碼,換言之,代理對象所代理的所有接口方法,最終要執(zhí)行的代碼都在invoke方法里,因此,這里是一切魔法的入口。

編寫一個jdk代理實例的基本步驟如下:

  1. 編寫業(yè)務接口
    因為jdk代理是基于接口的,因此,只能將業(yè)務方法定義成接口,但它可以一次生成多個接口的代理對象

  2. 編寫調用處理器
    即編寫一個java.lang.reflect.InvocationHandler接口的實現(xiàn)類,代理對象的業(yè)務邏輯就寫在該接口的invoke方法中

  3. 生成代理對象
    有了業(yè)務接口和調用處理器后,將二者作為參數(shù),通過Proxy.newProxyInstance方法便可以生成這個(或這些)接口的代理對象。比如上述示例代碼中的businessProxy對象,它擁有greeting()這個方法,調用該方法時,實際執(zhí)行的就是invoke方法。

代理對象生成原理

代理的目的,是為接口動態(tài)生成一個實例對象,該對象有接口定義的所有方法。調用對象的這些方法時,都將執(zhí)行生成該對象時,指定的“調用處理器”中的方法(即invoke方法)。

生成代理對象的方法簽名如下:

Proxy.newProxyInstance (ClassLoader loader,  Class<?>[] interfaces, InvocationHandler handler)

classloader一般選擇當前類的類加載器,interfaces是一個接口數(shù)組,newProxyInstance方法將為這組接口生成實例對象,handler中的代碼則是生成的實例對象實際要執(zhí)行的內容,這些代碼就位于invoke方法中。在生成代理對象前,會先生成一個Class,這個Class實現(xiàn)了interfaces中的所有接口,且這些方法的內容為直接調用handler#invoke,如下圖所示:

Java代理之jdk動態(tài)代理+應用場景實戰(zhàn)

特別說明
InvocationHandler的invoke方法簽名為:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable

在該方法的實現(xiàn)代碼中,不要調用proxy參數(shù)的toString方法, 這會導致遞歸死循環(huán)

下面將以小示例中的BusinessInterface接口和ProxyLogicHandler為基礎,用普通Java代碼的方式,模擬一下Proxy.newProxyInstance的代碼邏輯,如下:

點擊查看代碼
public static Object newProxyInstance(ClassLoader loader,  Class<?>[] interfaces, InvocationHandler handler) {
    return new Proxy0(handler);
}

static class Proxy0 implements BusinessInterface{
    private InvocationHandler handler;

    BusinessInterface(InvocationHandler handler) {
        this.handler = handler;
    }

    @Override
    public void greeting(String str) {
        handler.invoke(this, 'greeting', new Object[]{str});
    }
}

上面的代碼是示意性的,并不正確,比如它沒有使用到loader和interfaces參數(shù),調用hanlder.invoke方法時,對于method參數(shù)只是簡單的用'greeting'字符串替代,類型都不正確。但這段示意代碼很簡單明了地呈現(xiàn)了真實的Proxy.newProxyInstance方法內部的宏觀流程。

下面再提供一個與真實的newProxyInstance方法稍微接近一點的模擬實現(xiàn)(需要您對jdk里JavaCompiler類的使用有一定了解)

點擊查看代碼
package guzb.diy.proxy;

import javax.tools.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public class ImitateJdkProxy {

    public static void main(String[] args) throws Throwable{
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("執(zhí)行invocationHandler#invoke()方法");
                System.out.println("調用的代理方法名為:" + method.getName());
                System.out.println("調用時傳遞的參數(shù)為:" + args[0]);
                return null;
            }
        };
        Foo foo = (Foo) newProxyInstance(ImitateJdkProxy.class.getClassLoader(), Foo.class, handler);
        foo.sayHi("East Knight");
    }

    /** 
     * 模擬java.lang.reflect.Proxy#newProxyInstance方法
     * 這里簡化了代理類的類名,固定為:guzb.diy.$Proxy0
     */
    public static final Object newProxyInstance(ClassLoader loader, Class<?> interfaces, InvocationHandler handler) throws Exception {
        // 1. 構建代理類源碼對象
        JavaFileObject sourceCode = generateProxySourceCode();

        // 2. 編譯代理源代碼
        JavaBytesFileObject byteCodeFile = new JavaBytesFileObject("guzb.diy.$Proxy0");
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager stdFileManager = compiler.getStandardFileManager(null, Locale.CHINA, Charset.forName("utf8"));
        JavaFileManager fileManager = new ForwardingJavaFileManager(stdFileManager) {
            @Override
            public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
                return byteCodeFile;
            }
        };
        List<JavaFileObject> compilationUnits = new ArrayList<>();
        compilationUnits.add(sourceCode);
        JavaCompiler.CompilationTask compilationTask = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
        if (!compilationTask.call()) {
            return null;
        }

        // 3. 加載編譯后的代理類字節(jié)碼
        loader = new ClassLoader() {
            @Override
            public Class<?> findClass(String name) throws ClassNotFoundException {
                byte[] bytes = byteCodeFile.getBytes();
                return defineClass(name, bytes, 0, bytes.length);
            }
        };
        Class clazz = loader.loadClass("guzb.diy.$Proxy0");

        // 4. 創(chuàng)建代理類實例并返回
        Constructor constructor = clazz.getConstructor(new Class[]{InvocationHandler.class});
        return constructor.newInstance(handler);
    }

    /**
     * 生成代理Class的源代碼,該代碼將在運行期間動態(tài)編譯和加載。
     * 為了便于直觀查看代理類的原理,故意采用了這個使用源碼編譯的方式,實際上,
     * JDK真實的newProxyInstance方法,內部是采用純反射+直接生成字節(jié)碼數(shù)組的方式實現(xiàn)的,比較晦澀。
     * 這里也簡化了代理代碼,比如:
     *   1. 寫死了代理類的類名:guzb.diy.$Proxy0
     *   2. 寫死了要實現(xiàn)的接口和方法
     *      不寫死的話,需要通過反射遍歷所有接口的所有方法,并基于Method對象的方法名、返回類型、參數(shù)列表和異常列表,
     *      創(chuàng)建實現(xiàn)類的方法簽名文本,這樣的話,代碼就太冗長了,干擾了對代理主線邏輯的理解,也不是本文的重點
     *   3. 沒有使用調用者傳遞的ClassLoader來加載編譯后的字節(jié)碼文件,原因同上,涉及加載器的隔離問題,代碼過于冗長
     */
    private static JavaFileObject generateProxySourceCode() throws NoSuchMethodException {
        String[] codeLines = new String[]{
                "package guzb.diy;",

                "import java.lang.reflect.*;",
                "import guzb.diy.proxy.ImitateJdkProxy.Foo;",

                "public class $Proxy0 implements Foo {                    ",
                "    private InvocationHandler handler;                   ",
                "                                                         ",
                "    public $Proxy0 (InvocationHandler handler) {         ",
                "        this.handler = handler;                          ",
                "    }                                                    ",
                "                                                         ",
                "    @Override                                            ",
                "    public void sayHi(String name) throws Throwable {    ",
                "        Method method = Foo.class.getMethod(\"sayHi\", new Class[]{String.class}); ",
                "        this.handler.invoke(this, method, new Object[]{name}); ",
                "    }",
                "}"
        };

        String code = "";
        for (String codeLine : codeLines) {
            code += codeLine + "\n";
        }
        return new JavaStringFileObject("guzb.diy.$Proxy0", code);
    }

    /** 一個簡單的業(yè)務接口 */
    public interface Foo {
        void sayHi(String name) throws Throwable;
    }

    /** 基于字符串的Java源代碼對象 */
    public static class JavaStringFileObject extends SimpleJavaFileObject {
        // 源代碼文本
        final String code;

        /**
         *  @param name Java源代碼文件名,要包含完整的包名,比如guzb.diy.Proxy
         *  @param code Java源代碼文本
         */
        JavaStringFileObject(String name, String code) {
            super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension), Kind.SOURCE);
            this.code = code;
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            return code;
        }
    }

    /** 編譯后的字節(jié)碼文件 */
    public static class JavaBytesFileObject extends SimpleJavaFileObject {
        // 接收編譯后的字節(jié)碼
        private ByteArrayOutputStream byteCodesReceiver;

        /** @param name Java源代碼文件名,要包含完整的包名,比如guzb.diy.Proxy */
        protected JavaBytesFileObject(String name) {
            super(URI.create("bytes:///" + name + name.replace(".", "/")), Kind.CLASS);
            byteCodesReceiver = new ByteArrayOutputStream();
        }

        @Override
        public OutputStream openOutputStream() throws IOException {
            return byteCodesReceiver;
        }

        public byte[] getBytes() {
            return byteCodesReceiver.toByteArray();
        }
    }
}

代碼運行結果為:

執(zhí)行invocationHandler#invoke()方法
調用的代理方法名為:sayHi
調用時傳遞的參數(shù)為:East Knight

應用場景

上面提到:代理是在運行期,為接口動態(tài)生成了一個實現(xiàn)類,和這個實現(xiàn)類的實例。那這個功能有什么用呢?我們直接寫一個實現(xiàn)類不也是一樣的么?代理類與我們手動寫代碼的主要差異在于它的動態(tài)性,它允許我們在程序的運行期間動態(tài)創(chuàng)建Class,這對于框架類程序,為其預設的業(yè)務組件增加公共特性提供了技術支持。因為這種額外特性的加持,對業(yè)務代碼沒有直接的侵入性,因此效果非常好。動態(tài)代理的兩個最常用見應用場景為攔截器和聲明性接口,下面分別介紹。

攔截器功能

搭載器就是將目標組件劫持,在執(zhí)行目標組件代碼的前后,塞入一些其它代碼。比如在正式執(zhí)行業(yè)務方法前,先進行權限校驗,如果校驗不通過,則拒絕繼續(xù)執(zhí)行。對于此類操作,業(yè)界已經(jīng)抽象出一組通用的編程模型:面向切面編程AOP。

接下來,將以演員和導演為業(yè)務背景,實現(xiàn)一個簡易的攔截器,各個組件介紹如下:

  • Performer <Interface>
    演員接口,有play和introduction方法

  • DefaultActor <Class>
    代表男性演員,它實現(xiàn)了Performer接口,也是攔截器將要攔截的對象

  • Director <Interface>
    導演接口,只有一個getCreations方法, 該方法返回一個字符串列表,它代表導演的作品集

  • DefaultDirector <Class>
    Director接口的實現(xiàn)類,同時也是攔截器將要攔截的對象

  • ProxyForInterceptor <Class>
    攔截器核心類,實現(xiàn)了InvocationHandler接口,攔截器代碼位于接口的invoke方法中。

    攔截器將持有Performer和Direcotor的真實實現(xiàn)實例,并在調用Performer的play和introduction方法前,先執(zhí)行一段代碼。這里實現(xiàn)為打印一段文本,接著再調用play或introduction,執(zhí)行完后,再執(zhí)行一段代碼,也是打印一段文本。Director實例方法的攔截處理邏輯與此相同。這便是最簡單的攔截器效果了。

  • IntercepterTestMain <Class>
    攔截器測試類,在main方法中,驗證上述組件的攔截器功能效果。這個例子中,特意寫了兩個接口和兩個實現(xiàn)類,就是為了演示,JDK的動態(tài)代理是支持多接口的。

下面是各個組件的源代碼

Performer
package guzb.diy.proxy;

/**
 * 演員接口
 * 在這個示例中,將為該接口生成代理實例
 */
public interface Performer {
    /**
     * 根據(jù)主題即興表演一段
     * @param subject 表演的主題
     */
    void play(String subject);

    /** 自我介紹 */
    String introduction();
}
DefaultActor
package guzb.diy.proxy;

/**
 * 這是演員接口的默認實現(xiàn)類
 * 在本示例中,它將作為原始的接口實現(xiàn)者,被代理(攔截)
 */
public class DefaultActor implements Performer {
    @Override
    public void play(String subject) {
        System.out.println("[DefaultActor]: 默認男演員正在即興表演《"+ subject +"》");
    }

    @Override
    public String introduction() {
        return "李白·上李邕: 大鵬一日同風起,扶搖直上九萬里。假令風歇時下來,猶能顛卻滄溟水。世人見我恒殊調,聞余大言皆冷笑。宣父尚能畏后生,丈夫未可輕年少。";
    }
}
Director
package guzb.diy.proxy;

import java.util.List;

/**
 * 導演接口
 * 在這個示例中,將為該接口生成代理實例
 */
public interface Director {
    /**
     * 獲取曾導演過的作品集
     * @return 作品名稱列表
     */
    List<String> getCreations();
}

DefaultDirector
package guzb.study.javacore.proxy.jdk;

import java.util.ArrayList;
import java.util.List;

/**
 * 這是導演接口的默認實現(xiàn)類
 * 在本示例中,它將作為原始的接口實現(xiàn)者,被代理(攔截)
 */
public class DefaultDirector implements Director{
    @Override
    public List<String> getCreations() {
        return new ArrayList(){
            {
                add("活著");
                add("盲井");
                add("走出夾邊溝");
                add("少年派的奇幻漂流");
            }
        };
    }
}
ProxyForInterceptor
package guzb.diy.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * 代理應用場景一:攔截器
 * 即在原來的業(yè)務邏輯上追加額外的代碼,這是代理功能最常見的應用場景。
 *
 * 在本示例中,導演與演員實例代表原始業(yè)務,
 * 由于代理的目的是在執(zhí)行真實的接口實現(xiàn)類方法的前后,執(zhí)行一段其它代碼。
 * 因此,本類需要持有原始的導演和演員實例。
 */
public class ProxyForInterceptor implements InvocationHandler {

    // 原始的演員對象
    private Performer performer;

    // 原始的導演對象
    private Director director;

    public ProxyForInterceptor(Director director, Performer performer) {
        this.director = director;
        this.performer = performer;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        System.out.printf("[DirectorActorProxyHandler]: 調用的代理方法為:%s\n", methodName);

        System.out.printf("[DirectorActorProxyHandler]: >>> 調用 %s 之前的邏輯\n", methodName);

        Object result = null;
        // 因為本代理處理器,只針對Director和Actor接口,因此,如果方法名為play,則一定調用的是Actor的play方法
        // 根據(jù)Actor#play方法的參數(shù)定義,它只有一個String參數(shù),所以直接取args[0]即可
        if(methodName.equals("play")) {
            performer.play((String)args[0]);
        } else if (methodName.equals("introduction")) {
            result = performer.introduction();
        } else if (methodName.equals("getCreations")) {
            result = director.getCreations();
        }

        System.out.printf("[DirectorActorProxyHandler]: <<< 調用 %s 之后的邏輯\n", methodName);
        return result;
    }
}
IntercepterTestMain
package guzb.diy.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.List;

public class IntercepterTestMain {
    public static void main(String[] args) {
        Performer actor = new DefaultActor();
        Director director = new DefaultDirector();
        InvocationHandler interceptor = new ProxyForInterceptor(director, actor);

        // 要代理的接口,這里稱之為委托接口,即委托給代理實例,去實現(xiàn)相應的功能
        Class[] principalInterfaces = new Class[]{Director.class, Performer.class};

        // 創(chuàng)建一個代理實例,該實例實現(xiàn)了委托接口所定義的方法,因此,這個實例可以強轉為Performer和Director
        Object directorPerformerProxy = Proxy.newProxyInstance(IntercepterTestMain .class.getClassLoader(), principalInterfaces, interceptor);

        Performer performerProxy = (Performer) directorPerformerProxy;
        Director directorProxy = (Director) directorPerformerProxy;

        // ① 調用代理實例中,Performer接口相關的方法
        performerProxy.play("長板坡");
        String introduction = performerProxy.introduction();
        System.out.printf("[IntercepterTestMain ]: 代理對象返回的個人簡介內容為: %s\n", introduction);

        // 調用代理實例中,Director接口相關的方法
        List<String> creations = directorProxy.getCreations();
        System.out.println("[IntercepterTestMain ]: 代理對象返回的導演作品列表:");
        for (String creation : creations) {
            System.out.printf("    · %s\n", creation);
        }
    }
}

以上代碼的執(zhí)行結果如下:

[DirectorActorProxyHandler]: 調用的代理方法為:play
[DirectorActorProxyHandler]: >>> 調用 play 之前的邏輯
[DefaultActor]: 默認男演員正在即興表演《長板坡》
[DirectorActorProxyHandler]: <<< 調用 play 之后的邏輯
[DirectorActorProxyHandler]: 調用的代理方法為:introduction
[DirectorActorProxyHandler]: >>> 調用 introduction 之前的邏輯
[DirectorActorProxyHandler]: <<< 調用 introduction 之后的邏輯
[IntercepterTestMain ]: 代理對象返回的個人簡介內容為: 李白·上李邕: 大鵬一日同風起,扶搖直上九萬里。假令風歇時下來,猶能顛卻滄溟水。世人見我恒殊調,聞余大言皆冷笑。宣父尚能畏后生,丈夫未可輕年少。
[DirectorActorProxyHandler]: 調用的代理方法為:getCreations
[DirectorActorProxyHandler]: >>> 調用 getCreations 之前的邏輯
[DirectorActorProxyHandler]: <<< 調用 getCreations 之后的邏輯
[IntercepterTestMain ]: 代理對象返回的導演作品列表:
    · 活著
    · 盲井
    · 走出夾邊溝
    · 少年派的奇幻漂流

可以看到,在main方法中,調用代理類的play方法后(位于代碼的①處),在執(zhí)行真實的DefaultActor#play方法前后,均有額外的文本輸出,這些都不是DefaultActor#play方法的邏輯。這便實現(xiàn)了攔截器效果,且對于使用者而言(即編寫DefaultActor類的開發(fā)者),是無侵入無感知的。

聲明性接口

聲明性接口的特點是:開發(fā)者只需要提供接口,并在接口方法中聲明該方法要完成的功能(通常是以多個注解的方式聲明),但不用編寫具體的功能實現(xiàn)代碼,而是通過框架的工廠方法來獲取該接口的實例。當然,該實例會完成接口方法中所聲明的那些功能。比較典型的產(chǎn)品是MyBatis的Mapper接口。實現(xiàn)手段也是采用jdk動態(tài)代理,在InvocationHandler的invoke方法中,完成該接口方法所聲明的那些特性功能。

接下來,本文將模擬MyBatis的Mapper功能,組件說明如下:

  • SqlMapper <Annotaton>
    與MyBatis的Mapper注解等效,用于標識一個接口為Sql映射接口,但在本示例中,這個接口并未使用到。因為這個標識接口的真實用途,是在SpringBoot環(huán)境中,用于自動掃描和加載Mapper接口的。本示例僅模擬Mapper本身的聲明性功能,因此用不上它。保留這個接口,只是為了顯得更完整。

  • Select <Annotation>
    與MyBatis的Select注解等效,它有一個sql屬性,用于指定要執(zhí)行的SQL語句,且支持#{}形式的插值

  • ParamName <Annotation>
    與MyBatis的Param注解等效,用于標識Mapper接口的方法參數(shù)名稱,以便用于Select注解中sql語句的插值替換

  • PerformerMapper <Interface>
    演員實體的數(shù)據(jù)庫訪問接口,與開發(fā)者使用MyBatis時,日常編寫的各類Mapper接口一樣。在里邊定義各種數(shù)據(jù)庫查詢接口方法,并利用Select和ParamName注解,聲明數(shù)據(jù)操作的具體功能。

  • ProxyForDeclaration <Class>
    整個Mapper功能的核心類,實現(xiàn)了InvocationHandler接口,在invoke方法中,完成Mapper的所有功能

  • DeclarationTestMain <Class>
    聲明性接口的功能測試類,在main方法中,通過jdk代理獲得一個PerformerMapper實例,并調用其中的getQuantityByNameAndAage、getRandomPoetryOf和listAllOfAge方法,分別傳入不的SQL和參數(shù),用以驗證3種不同的情況。

下面是各個組件的源代碼:

SqlMapper
package guzb.diy.proxy;

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

/**
 * 標識一個接口是一個SQL映射類,用于模擬MyBatis的mapper功能
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SqlMapper {
}
Select
package guzb.diy.proxy;

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

/**
 * 為一個mapper方法指定查詢類sql語句
 * 本類用于模擬MyBatis的mapper功能
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Select {
    /**
     * 查詢sql語句,支持#{}這樣的插值占位符
     */
    String sql();
}

ParamName
package guzb.diy.proxy;

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

/**
 * 為一個mapper方法的參數(shù),指定一個名稱,以便在sql語句中進行插值替換
 * 本類用于模擬MyBatis的mapper功能
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamName {
    /** 參數(shù)的名稱 */
    String value();
}
PerformerMapper
package guzb.diy.proxy;

/**
 * 演員實體查詢接口。
 * 本類用于模擬MyBatis的mapper功能
 */
@SqlMapper
public interface PerformerMapper {

    @Select(sql = "select count(*) from performer where name=#{name} and age = #{ age }")
    Long getQuantityByNameAndAage(@ParamName("name") String name, @ParamName("age") Integer age);

    @Select(sql = "select poetry_item from poetry where performer_name = #{ name }")
    String getRandomPoetryOf(@ParamName("name") String name);

    // ② SQL中故障引入了一個pageSize的變量,由于方法簽名中沒有聲明這個參數(shù),因此會導致SQL在插值替換階段發(fā)生異常
    @Select(sql = "select * from performer where age >= #{age} limit #{ pageSize }")
    Object listAllOfAge(@ParamName("age") int age);

}
ProxyForDeclaration
package guzb.diy.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 〔聲明性接口〕功能的核心實現(xiàn)類
 */
public class ProxyForDeclaration implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.printf("[ProxyForDeclaration]: 調用的方法名為:%s\n", method.getName());

        // 1. 先提取出原始的SQL
        String rawSql = extractSql(method);
        if (rawSql == null || rawSql.trim().length() == 0) {
            System.out.printf("[ProxyForDeclaration]: 方法%s()未指定SQL語句,無法執(zhí)行。請通過@Select注解指定Sql\n", method.getName());
            return null;
        }
        System.out.printf("[ProxyForDeclaration]: 原始sql為:%s\n", rawSql);

        // 2. 對原始SQL做插值替換,String類型的參數(shù)追加''號,其它類型原樣替換
        String finalSql = interpolateSql(rawSql, method, args);
        System.out.printf("[ProxyForDeclaration]: 插值替換后的sql為:%s\n", finalSql);

        // 3. 模擬執(zhí)行SQL語句
        return imitateJdbcExecution(finalSql, method.getReturnType());
    }

    private String extractSql(Method method) {
        Select selectAnnotation = method.getAnnotation(Select.class);
        return selectAnnotation == null ? null : selectAnnotation.sql();
    }

    private String interpolateSql(String rawSql, Method method, Object[] args) {
        // 使用正則表達式來完成插值表達式#{}的內容替換
        Pattern interpolationTokenPattern = Pattern.compile("(#\\{\\s*([a-zA-Z0-9]+)\\s*\\})");
        Matcher matcher = interpolationTokenPattern.matcher(rawSql);

        // 提取出方法參數(shù)名稱與參數(shù)對象的對應關系,key為參數(shù)名(通過@ParamName注解指定),value為參數(shù)對象
        Map<String, Object> paramMap = extractParameterMap(method, args);

        // 插值替換
        String finalSql = rawSql;
        while (matcher.find()) {
            String interpolationToken = matcher.group(1);
            String parameterName = matcher.group(2);
            if (!paramMap.containsKey(parameterName)) {
                throw new SqlMapperExecuteException("未知參數(shù):" + parameterName);
            }

            Object value = paramMap.get(parameterName);
            String valueStr = value instanceof String ? "'" + value.toString() + "'" : value.toString();
            finalSql = finalSql.replace(interpolationToken, valueStr);
        }
        return finalSql;
    }

    private Map<String, Object> extractParameterMap(Method method, Object[] args) {
        Parameter[] parameters = method.getParameters();
        if (parameters.length == 0) {
            return Collections.EMPTY_MAP;
        }

        Map<String, Object> sqlParamMap = new HashMap<>();
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            ParamName paramName = parameter.getAnnotation(ParamName.class);
            // 這里不用檢查數(shù)組越界問題,因為args參數(shù)本身就是調用接口方法時的傳遞的參數(shù),只要是正常調用(不是通過反射)就不會越界
            sqlParamMap.put(paramName.value(), args[i]);
        }
        return sqlParamMap;
    }

    /** 模擬執(zhí)行jdbc sql, 這里僅對數(shù)字和字符串進行了模擬,其它返回null */
    private Object imitateJdbcExecution(String finalSql, Class<?> returnType) {
        if(Number.class.isAssignableFrom(returnType)){
            return (long)(Math.random() * 1000 + 1);
        }

        if (returnType == String.class) {
            String[] poetry = new String[]{
                    "黃四娘家花滿蹊,千朵萬朵壓枝低。",
                    "留連戲蝶時時舞,自在妖鶯恰恰啼。",
                    "荷盡已無擎雨蓋,菊殘猶有傲霜枝。",
                    "一年好景君須記,最是橙黃橘綠時。"
            };
            int index = (int)(Math.random() * 4);
            return poetry[index];
        }

        return null;
    }

    static class SqlMapperExecuteException extends RuntimeException {
        public SqlMapperExecuteException(String message) {
            super(message);
        }
    }
}

DeclarationTestMain
package guzb.diy.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.List;

/**
 * 〔聲明性接口〕功能測試入口類
 */
public class DeclarationTestMain {

    public static void main(String[] args) {
        Class[] principalInterfaces = new Class[]{PerformerMapper.class};
        ProxyForDeclaration declarationHandler = new ProxyForDeclaration();
        PerformerMapper performerMapper = (PerformerMapper) Proxy.newProxyInstance(JdkProxyStudyMain.class.getClassLoader(), principalInterfaces, declarationHandler);

        Long count = performerMapper.getQuantityByNameAndAage("Jane Lotus", 47);
        System.out.printf("[DeclarationTestMain]: 代理實例方法方法的返回值為:%s\n\n", count);

        String poetryItem = performerMapper.getRandomPoetryOf("杜甫");
        System.out.printf("[DeclarationTestMain]: 代理實例方法的返回值為:%s\n\n", poetryItem);

        // ③ 本方法調用后將發(fā)生異常,因為PerformerMapper中的②處,聲明的SQL有未知的插值變量,這里特意測試驗證
        performerMapper.listAllOfAge(100);
    }

}

以上代碼的執(zhí)行結果為:

[ProxyForDeclaration]: 調用的方法名為:getQuantityByNameAndAage
[ProxyForDeclaration]: 原始sql為:select count(*) from performer where name=#{name} and age = #{ age }
[ProxyForDeclaration]: 插值替換后的sql為:select count(*) from performer where name='Jane Lotus' and age = 47
[DeclarationTestMain]: 代理實例方法方法的返回值為:40

[ProxyForDeclaration]: 調用的方法名為:getRandomPoetryOf
[ProxyForDeclaration]: 原始sql為:select poetry_item from poetry where performer_name = #{ name }
[ProxyForDeclaration]: 插值替換后的sql為:select poetry_item from poetry where performer_name = '杜甫'
[DeclarationTestMain]: 代理實例方法的返回值為:黃四娘家花滿蹊,千朵萬朵壓枝低。

[ProxyForDeclaration]: 調用的方法名為:listAllOfAge
[ProxyForDeclaration]: 原始sql為:select * from performer where age >= #{age} limit #{ pageSize }
Exception in thread "main" guzb.diy.proxy.ProxyForDeclaration$SqlMapperExecuteException: 未知參數(shù):pageSize
	at guzb.diy.proxy.ProxyForDeclaration.interpolateSql(ProxyForDeclaration.java:55)
	at guzb.diy.proxy.ProxyForDeclaration.invoke(ProxyForDeclaration.java:29)
	at com.sun.proxy.$Proxy1.listAllOfAge(Unknown Source)
	at guzb.diy.proxy.DeclarationTestMain.main(JdkProxyStudyMain.java:24)

以上代碼共模擬了3個調用Mapper的場景:

  1. 調用getQuantityByNameAndAage()方法根據(jù)姓名的年齡查詢演員數(shù)量。但并未真正執(zhí)行JDBC查詢,只是將SQL進行了插值替換和輸出,然后隨機返回了一個數(shù)字。這足以演示聲明性接口這一特性了,真實地執(zhí)行jdbc查詢,那將一個代碼量巨大的工作,它的缺失并不影響本示例的主旨。

  2. 調用getRandomPoetryOf()方法查詢指定詩人的一段詩句。同樣沒有真正執(zhí)行jdbc查詢,而是隨機返回了一句詩文。

  3. 調用listAllOfAge()方法查詢指定年齡的所有演員。該方法有意設計為引發(fā)一個異常,因為接口方法上聲明的SQL中,pageSize這個插值變量并未在方面簽名中聲明。文章來源地址http://www.zghlxwxcb.cn/news/detail-409607.html

到了這里,關于Java代理之jdk動態(tài)代理+應用場景實戰(zhàn)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如若轉載,請注明出處: 如若內容造成侵權/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

  • [Java]靜態(tài)代理、動態(tài)代理(基于JDK1.8)

    [Java]靜態(tài)代理、動態(tài)代理(基于JDK1.8)

    【版權聲明】未經(jīng)博主同意,謝絕轉載?。ㄕ堊鹬卦瓌?chuàng),博主保留追究權) https://www.cnblogs.com/cnb-yuchen/p/18002823 出自【進步*于辰的博客】 參考筆記一,P83;筆記二,P75.4。 目錄 1、概述 2、靜態(tài)代理的兩種形式 2.1 面向接口 2.2 面向繼承 3、動態(tài)代理的兩種形式 3.1 JDK動態(tài)代理

    2024年03月09日
    瀏覽(26)
  • 【Java】jdk1.8 Java代理模式,Jdk動態(tài)代理講解(非常詳細,附帶class文件)

    【Java】jdk1.8 Java代理模式,Jdk動態(tài)代理講解(非常詳細,附帶class文件)

    ? ???個人主頁:哈__ 期待您的關注? 想要學代理模式,我們就要先弄清一個概念 “什么是代理”? 在我們的現(xiàn)實生活中,你或許不少聽過關于代理的名詞,如:代理商。那什么又叫做代理商?讓我一個詞來形容就是 中間商。 舉個例子,在你買二手房的時候,你一般不會直

    2024年04月15日
    瀏覽(27)
  • 代理模式 靜態(tài)代理和動態(tài)代理(jdk、cglib)——Java入職第十一天

    代理模式 靜態(tài)代理和動態(tài)代理(jdk、cglib)——Java入職第十一天

    ? ? ? ? 一個類代表另一個類去完成擴展功能,在主體類的基礎上,新增一個代理類,擴展主體類功能,不影響主體,完成額外功能。比如買車票,可以去代理點買,不用去火車站,主要包括靜態(tài)代理和動態(tài)代理兩種模式。 代理類中包含了主體類 無法根據(jù)業(yè)務擴展,每一次

    2024年02月10日
    瀏覽(26)
  • 【面試精講】Java動態(tài)代理是如何實現(xiàn)的?JDK Proxy 和 CGLib 有什么區(qū)別?

    【面試精講】Java動態(tài)代理是如何實現(xiàn)的?JDK Proxy 和 CGLib 有什么區(qū)別?

    Java動態(tài)代理是如何實現(xiàn)的?JDK Proxy 和 CGLib 有什么區(qū)別? 一、Java動態(tài)代理的實現(xiàn) 1、使用JDK Proxy實現(xiàn)動態(tài)代理 2、使用CGLib實現(xiàn)動態(tài)代理 二、JDK Proxy 與 CGLib 的區(qū)別 三、Spring中的動態(tài)代理 四、?Lombok代理原理 總結 本文深入探討了Java動態(tài)代理的實現(xiàn)機制,分別介紹了使用JDK

    2024年03月14日
    瀏覽(28)
  • Java 代理模式的基本概念、使用場景、應用示例和實現(xiàn)方法

    代理模式是一種常見的設計模式,在 Java 開發(fā)中被廣泛應用。它允許我們通過添加一個代理對象來控制對另一個對象的訪問,從而提供了一種間接訪問實際對象的方法。本文將詳細介紹 Java 代理模式的基本概念、使用場景、應用示例和實現(xiàn)方法等相關內容。 代理模式是一種結

    2024年02月05日
    瀏覽(30)
  • 【動態(tài)代理】JDK動態(tài)代理與cglib動態(tài)代理源碼解析

    【動態(tài)代理】JDK動態(tài)代理與cglib動態(tài)代理源碼解析

    UserService ,接口類 UserServiceImpl ,實現(xiàn)類 使用代理,測試效果。 控制臺輸出: Proxy#newProxyInstance ,生成代理類的實例。 核心在于 getProxyClass0 ,生成代理類的類信息 使用自定義的InvocationHandler作為參數(shù),調用構造函數(shù)獲取代理類對象實例 WeakCache#get ProxyClassFactory#apply ,實現(xiàn)了

    2024年02月04日
    瀏覽(21)
  • JDK動態(tài)代理和CGLIB動態(tài)代理

    JDK動態(tài)代理和CGLIB動態(tài)代理 ① JDK動態(tài)代理只提供接口的代理,不支持類的代理,要求被代理類實現(xiàn)接口。JDK動態(tài)代理的核心是InvocationHandler接口和Proxy類,在獲取代理對象時,使用Proxy類來動態(tài)創(chuàng)建目標類的代理類(即最終真正的代理類,這個類繼承自Proxy并實現(xiàn)了我們定義的

    2024年02月07日
    瀏覽(16)
  • cglib動態(tài)代理、jdk動態(tài)代理及spring動態(tài)代理使用

    cglib動態(tài)代理、jdk動態(tài)代理及spring動態(tài)代理使用

    NickelBeforeAdvice.java方法執(zhí)行之前 NickelAfterReturningAdvice.java方法執(zhí)行之后 NickelAroundadvice.java環(huán)繞方法 NickelThrowAdvice.java拋異常方法 UserService.java拋異常方法 SpringProxtFactoryTest.java測試方法 NickelStaticMethodMatherPointcut.java方法匹配的方法 NickelPointcutAdvisor.java切面方法 SpringProxtFactoryTest.j

    2024年02月15日
    瀏覽(21)
  • 溫故知新之:代理模式,靜態(tài)代理和動態(tài)代理(JDK動態(tài)代理)

    溫故知新之:代理模式,靜態(tài)代理和動態(tài)代理(JDK動態(tài)代理)

    代理模式可以在不修改被代理對象的基礎上,通過擴展代理類,進行一些功能的附加與增強。 靜態(tài)代理 是一種代理模式的實現(xiàn)方式,它在編譯期間就已經(jīng)確定了代理對象,需要為每一個被代理對象創(chuàng)建一個代理類。靜態(tài)代理的實現(xiàn)比較簡單,但是每個被代理對象都需要創(chuàng)建

    2024年02月11日
    瀏覽(30)
  • 代理模式:靜態(tài)代理+JDK/CGLIB 動態(tài)代理

    代理模式:靜態(tài)代理+JDK/CGLIB 動態(tài)代理

    代理模式是一種比較好理解的設計模式。簡單來說就是 我們使用代理對象來代替對真實對象(real object)的訪問,這樣就可以在不修改原目標對象的前提下,提供額外的功能操作,擴展目標對象的功能。 代理模式的主要作用是擴展目標對象的功能,比如說在目標對象的某個方法

    2024年02月13日
    瀏覽(24)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包