主頁 > 後端開發 > Java代理之jdk動態代理+應用場景實戰

Java代理之jdk動態代理+應用場景實戰

2023-04-11 07:10:05 後端開發

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

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

小示例

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

點擊查看代碼
package demo.proxy;

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

public class JdkProxyBasicDemo {

    // ⑴ 定義業務介面
    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("呼叫方法的引數為: %s\n", args[0]);
            System.out.println("請在這里插入代碼邏輯代碼...");  // ⑵.1
            return null;                                   // ⑵.2
        }
    }

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

上述代碼執行后的輸出結果如下:

運行的代理類為: class com.sun.proxy.$Proxy0
呼叫的代理方法為: greeting
呼叫方法的引數為: Hello, Jdk Proxy
請在這里插入代理的邏輯代碼...

其中倒數第二行的businessProxy變數,就是一個代理物件,它是BusinessInterface介面的一個實體,但我們并沒有撰寫這個介面的實作類,而是通過Proxy.newProxyInstance方法生成出了該介面的實體,那么這個動態代理實體對應的Class長什么樣子呢?上面的結果輸出中已經列印出來了,這個代理類名稱為com.sun.proxy.$Proxy0,實際上,如果我們再為另外一個介面生成代理物件的話,它的Class名稱為com.sun.proxy.$Proxy1,依次類推,

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

撰寫一個jdk代理實體的基本步驟如下:

  1. 撰寫業務介面
    因為jdk代理是基于介面的,因此,只能將業務方法定義成介面,但它可以一次生成多個介面的代理物件

  2. 撰寫呼叫處理器
    即撰寫一個java.lang.reflect.InvocationHandler介面的實作類,代理物件的業務邏輯就寫成該介面的invoke方法中

  3. 生成代理物件
    有了業務介面和呼叫處理器后,將二者作為引數,通過Proxy.newProxyInstance方法便可以生成這個(或這些)介面的代理物件,比如上述示例代碼中的businessProxy物件,它擁有greeting()這個方法,呼叫該方法時,實際執行的就是invoke方法,

代理物件生成原理

代理的目的,是為介面動態生成一個實體物件,該物件有介面定義的所有方法,呼叫物件的這些方法時,都將執行生成該物件時,指定的“呼叫處理器”中的方法(即invoke方法),

生成代理物件的方法簽名如下:

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

classloader一般選擇當前類的類加載器,interfaces是一個介面陣列,newProxyInstance方法將為這組介面生成實體物件,handler中的代碼則是生成的實體物件實際要執行的內容,這些代碼就位于invoke方法中,在生成代理物件前,會先生成一個Class,這個Class實作了interfaces中的所有介面,且這些方法的內容為直接呼叫handler#invoke,如下圖所示:

JDK代理物件生成原理

下面將以小示例中的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引數,呼叫hanlder.invoke方法時,對于method引數只是簡單的用'greeting'字串替代,型別都不正確,但這段示意代碼很簡單明了地呈現了真實的Proxy.newProxyInstance方法內部的宏觀流程,

下面再提供一個與真實的newProxyInstance方法稍微接近一點的模擬實作(需要您對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("執行invocationHandler#invoke()方法");
                System.out.println("呼叫的代理方法名為:" + method.getName());
                System.out.println("呼叫時傳遞的引數為:" + 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. 加載編譯后的代理類位元組碼
        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. 創建代理類實體并回傳
        Constructor constructor = clazz.getConstructor(new Class[]{InvocationHandler.class});
        return constructor.newInstance(handler);
    }

    /**
     * 生成代理Class的源代碼,該代碼將在運行期間動態編譯和加載,
     * 為了便于直觀查看代理類的原理,故意采用了這個使用原始碼編譯的方式,實際上,
     * JDK真實的newProxyInstance方法,內部是采用純反射+直接生成位元組碼陣列的方式實作的,比較晦澀,
     * 這里也簡化了代理代碼,比如:
     *   1. 寫死了代理類的類名:guzb.diy.$Proxy0
     *   2. 寫死了要實作的介面和方法
     *      不寫死的話,需要通過反射遍歷所有介面的所有方法,并基于Method物件的方法名、回傳型別、引數串列和例外串列,
     *      創建實作類的方法簽名文本,這樣的話,代碼就太冗長了,干擾了對代理主線邏輯的理解,也不是本文的重點
     *   3. 沒有使用呼叫者傳遞的ClassLoader來加載編譯后的位元組碼檔案,原因同上,涉及加載器的隔離問題,代碼過于冗長
     */
    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);
    }

    /** 一個簡單的業務介面 */
    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;
        }
    }

    /** 編譯后的位元組碼檔案 */
    public static class JavaBytesFileObject extends SimpleJavaFileObject {
        // 接收編譯后的位元組碼
        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();
        }
    }
}

代碼運行結果為:

執行invocationHandler#invoke()方法
呼叫的代理方法名為:sayHi
呼叫時傳遞的引數為:East Knight

應用場景

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

攔截器功能

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

接下來,將以演員和導演為業務背景,實作一個簡易的攔截器,各個組件介紹如下:

  • Performer <Interface>
    演員介面,有play和introduction方法

  • DefaultActor <Class>
    代碼男性演員,它實作了Performer介面,也是攔截器將要攔截的物件

  • Director <Interface>
    導演介面,只有一個getCreations方法, 該方法回傳一個字串串列,它代表導演的作品集

  • DefaultDirector <Class>
    Director介面的實作類,同時也是攔截器將要攔截的物件

  • ProxyForInterceptor <Class>
    攔截器核心類,實作了InvocationHandler介面,攔截器代碼位于介面的invoke方法中,

    攔截器將持有Performer和Direcotor的真實實作實體,并在呼叫Performer的play和introduction方法前,先執行一段代碼,這里實作為列印一段文本,接著再呼叫play或introduction,執行完后,再執行一段代碼,也是列印一段文本,Director實體方法的攔截處理邏輯與此相同,這便是最簡單的攔截器效果了,

  • IntercepterTestMain <Class>
    攔截器測驗類,在main方法中,驗證上述組件的攔截器功能效果,這個例子中,特意寫了兩個介面和兩個實作類,就是為了演示,JDK的動態代理是支持多介面的,

下面是各個組件的源代碼

Performer
package guzb.diy.proxy;

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

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

/**
 * 這是演員介面的默認實作類
 * 在本示例中,它將作為原始的介面實作者,被代理(攔截)
 */
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;

/**
 * 這是導演介面的默認實作類
 * 在本示例中,它將作為原始的介面實作者,被代理(攔截)
 */
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;

/**
 * 代理應用場景一:攔截器
 * 即在原來的業務邏輯上追加額外的代碼,這是代理功能最常見的應用場景,
 *
 * 在本示例中,導演與演員實體代表原始業務,
 * 由于代理的目的是在執行真實的介面實作類方法的前后,執行一段其它代碼,
 * 因此,本類需要持有原始的導演和演員實體,
 */
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方法
        // 根據Actor#play方法的引數定義,它只有一個String引數,所以直接取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);

        // 要代理的介面,這里稱之為委托介面,即委托給代理實體,去實作相應的功能
        Class[] principalInterfaces = new Class[]{Director.class, Performer.class};

        // 創建一個代理實體,該實體實作了委托介面所定義的方法,因此,這個實體可以強轉為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);
        }
    }
}

以上代碼的執行結果如下:

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

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

宣告性介面

宣告性介面的特點是:開發者只需要提供介面,并在介面方法中宣告該方法要完成的功能(通常是以多個注解的方式宣告),但不用撰寫具體的功能實作代碼,而是通過框架的工廠方法來獲取該介面的實體,當然,該實體會完成介面方法中所宣告的那些功能,比較典型的產品是MyBatis的Mapper介面,實作手段也是采用jdk動態代理,在InvocationHandler的invoke方法中,完成該介面方法所宣告的那些特性功能,

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

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

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

  • ParamName <Annotation>
    與MyBatis的Param注解等效,用于標識Mapper介面的方法引數名稱,以便用于Select注解中sql陳述句的插值替換

  • PerformerMapper <Interface>
    演員物體的資料庫訪問介面,與開發者使用MyBatis時,日常撰寫的各類Mapper介面一樣,在里邊定義各種資料庫查詢介面方法,并利用Select和ParamName注解,宣告資料操作的具體功能,

  • ProxyForDeclaration <Class>
    整個Mapper功能的核心類,實作了InvocationHandler介面,在invoke方法中,完成Mapper的所有功能

  • DeclarationTestMain <Class>
    宣告性介面的功能測驗類,在main方法中,通過jdk代理獲得一個PerformerMapper實體,并呼叫其中的getQuantityByNameAndAage、getRandomPoetryOf和listAllOfAge方法,分別傳入不的SQL和引數,用以驗證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方法的引數,指定一個名稱,以便在sql陳述句中進行插值替換
 * 本類用于模擬MyBatis的mapper功能
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamName {
    /** 引數的名稱 */
    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的變數,由于方法簽名中沒有宣告這個引數,因此會導致SQL在插值替換階段發生例外
    @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;

/**
 * 〔宣告性介面〕功能的核心實作類
 */
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陳述句,無法執行,請通過@Select注解指定Sql\n", method.getName());
            return null;
        }
        System.out.printf("[ProxyForDeclaration]: 原始sql為:%s\n", rawSql);

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

        // 3. 模擬執行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);

        // 提取出方法引數名稱與引數物件的對應關系,key為引數名(通過@ParamName注解指定),value為引數物件
        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("未知引數:" + parameterName);
            }

            Object value = https://www.cnblogs.com/sandgull/p/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);
            // 這里不用檢查陣列越界問題,因為args引數本身就是呼叫介面方法時的傳遞的引數,只要是正常呼叫(不是通過反射)就不會越界
            sqlParamMap.put(paramName.value(), args[i]);
        }
        return sqlParamMap;
    }

    /** 模擬執行jdbc sql, 這里僅對數字和字串進行了模擬,其它回傳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);

        // ③ 本方法呼叫后將發生例外,因為PerformerMapper中的②處,宣告的SQL有未知的插值變數,這里特意測驗驗證
        performerMapper.listAllOfAge(100);
    }

}

以上代碼的執行結果為:

[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: 未知引數: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()方法根據姓名的年齡查詢演員數量,但并未真正執行JDBC查詢,只是將SQL進行了插值替換和輸出,然后隨機回傳了一個數字,這足以演示宣告性介面這一特性了,真實地執行jdbc查詢,那將一個代碼量巨大的作業,它的缺失并不影響本示例的主旨,

  2. 呼叫getRandomPoetryOf()方法查詢指定詩人的一段詩句,同樣沒有真正執行jdbc查詢,而是隨機回傳了一句詩文,

  3. 呼叫listAllOfAge()方法查詢指定年齡的所有演員,該方法有意設計為引發一個例外,因為介面方法上宣告的SQL中,pageSize這個插值變數并未在方面簽名中宣告,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/549603.html

標籤:Java

上一篇:簡單模仿mybatis plus

下一篇:Disruptor-簡單使用

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more