本文將先介紹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代理實體的基本步驟如下:
-
撰寫業務介面
因為jdk代理是基于介面的,因此,只能將業務方法定義成介面,但它可以一次生成多個介面的代理物件 -
撰寫呼叫處理器
即撰寫一個java.lang.reflect.InvocationHandler介面的實作類,代理物件的業務邏輯就寫成該介面的invoke方法中 -
生成代理物件
有了業務介面和呼叫處理器后,將二者作為引數,通過Proxy.newProxyInstance方法便可以生成這個(或這些)介面的代理物件,比如上述示例代碼中的businessProxy物件,它擁有greeting()這個方法,呼叫該方法時,實際執行的就是invoke方法,
代理物件生成原理
代理的目的,是為介面動態生成一個實體物件,該物件有介面定義的所有方法,呼叫物件的這些方法時,都將執行生成該物件時,指定的“呼叫處理器”中的方法(即invoke方法),
生成代理物件的方法簽名如下:
Proxy.newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler)
classloader一般選擇當前類的類加載器,interfaces是一個介面陣列,newProxyInstance方法將為這組介面生成實體物件,handler中的代碼則是生成的實體物件實際要執行的內容,這些代碼就位于invoke方法中,在生成代理物件前,會先生成一個Class,這個Class實作了interfaces中的所有介面,且這些方法的內容為直接呼叫handler#invoke,如下圖所示:

下面將以小示例中的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的場景:
-
呼叫getQuantityByNameAndAage()方法根據姓名的年齡查詢演員數量,但并未真正執行JDBC查詢,只是將SQL進行了插值替換和輸出,然后隨機回傳了一個數字,這足以演示宣告性介面這一特性了,真實地執行jdbc查詢,那將一個代碼量巨大的作業,它的缺失并不影響本示例的主旨,
-
呼叫getRandomPoetryOf()方法查詢指定詩人的一段詩句,同樣沒有真正執行jdbc查詢,而是隨機回傳了一句詩文,
-
呼叫listAllOfAge()方法查詢指定年齡的所有演員,該方法有意設計為引發一個例外,因為介面方法上宣告的SQL中,pageSize這個插值變數并未在方面簽名中宣告,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/549603.html
標籤:Java
上一篇:簡單模仿mybatis plus
下一篇:Disruptor-簡單使用
