本文將先介紹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代理實例的基本步驟如下:
-
編寫業(yè)務接口
因為jdk代理是基于接口的,因此,只能將業(yè)務方法定義成接口,但它可以一次生成多個接口的代理對象 -
編寫調用處理器
即編寫一個java.lang.reflect.InvocationHandler接口的實現(xiàn)類,代理對象的業(yè)務邏輯就寫在該接口的invoke方法中 -
生成代理對象
有了業(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,如下圖所示:
特別說明
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的場景:
-
調用getQuantityByNameAndAage()方法根據(jù)姓名的年齡查詢演員數(shù)量。但并未真正執(zhí)行JDBC查詢,只是將SQL進行了插值替換和輸出,然后隨機返回了一個數(shù)字。這足以演示聲明性接口這一特性了,真實地執(zhí)行jdbc查詢,那將一個代碼量巨大的工作,它的缺失并不影響本示例的主旨。
-
調用getRandomPoetryOf()方法查詢指定詩人的一段詩句。同樣沒有真正執(zhí)行jdbc查詢,而是隨機返回了一句詩文。文章來源:http://www.zghlxwxcb.cn/news/detail-409607.html
-
調用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)!