什麼是代理模式
所謂代理,就是一個人或者一個機構代表另一個人或者另一個機構採取行動。在一些情況下,一個客戶不想或者不能直接引用一個對象,而代理對象可以在客戶端和目標對象之間起到中介的左右。
代理模式:給某一個對象提供一個代理或佔位符,並由代理對象來控制對原對象的訪問,通過代理對象訪問目標對象,這樣可以在不修改原目標對象的前提下,提供額外的功能操作,擴展目標對象的功能。
說簡單點,代理模式就是設置一箇中間代理來控制訪問原目標對象,以達到增強原對象的功能和簡化訪問方式。一般而言會分三種:靜態代理、動態代理和CGLIB代理
代理模式結構如下:
靜態代理
靜態代理需要代理對象和被代理對象實現一樣的接口,我們來看個例子就清楚了
示例代理
https://gitee.com/youzhibing/proxy/tree/master/static-proxy
代理類:UserDaoProxy.java
<code>/**
* 代理邏輯在代理類中,而不是由用戶自定義
*/
public class UserDaoProxy implements IUserDao {
private IUserDao target; // 被代理對象
public UserDaoProxy(IUserDao target) {
this.target = target;
}
/**
* 前置/後置 處理一旦寫完,就固定死了,後續想修改的話需要改此代理類
* @param id
* @return
*/
public int delete(int id) {
// 前置處理,例如開啟事務
System.out.println("前置處理...");
// 調用目標對象方法
int count = target.delete(id);
// 後置處理,例如提交事務或事務回滾
System.out.println("前置處理...");
return count;
}
}/<code>
UserDaoProxy代理IUserDao類型,此時也只能代理IUserDao類型的被代理對象。測試結果就不展示了,相信大家看了代碼也知道了
優點:可以在不修改目標對象的前提下擴展目標對象的功能
缺點:如果需要代理多個類,每個類都會有一個代理類,會導致代理類無限制擴展;如果類中有多個方法,同樣的代理邏輯需要反覆實現、應用到每個方法上,一旦接口增加方法,目標對象與代理對象都要進行修改
一個靜態代理只能代理一個類,那麼有沒有什麼方式可以實現同一個代理類來代理任意對象呢?肯定有的,也就是下面講到的:動態代理。更多設計模式,可以參考:設計模式內容聚合
動態代理
代理類在程序運行時創建的代理方式被成為動態代理。也就是說,這種情況下,代理類並不是在Java代碼中定義的,而是在運行時根據我們在Java代碼中的“指示”動態生成的。
下面我們一步一步手動來實現動態代理。下面的示例都是直接針對接口的,就不是針對接口的具體實現類了,靜態代理示例中,UserDaoProxy代理的是IUserDao的實現類:UserDaoImpl,那麼動態代理示例就直接針對接口了,下面示例針對的都是UserMapper接口,模擬的mybatis,但不侷限於UserMapper接口
代理類源代碼持久化
1、先利用反射動態生成代理類,並持久化代理類到磁盤(也就是生成代理類的java源文件),generateJavaFile方法如下
<code>/**
* 生成接口實現類的源代碼, 並持久化到java文件
* @param interface_
* @param proxyJavaFileDir
* @throws Exception
*/
private static void generateJavaFile(Class> interface_, String proxyJavaFileDir) throws Exception {
StringBuilder proxyJava = new StringBuilder();
proxyJava.append("package ").append(interface_.getPackage().getName()).append(";").append(ENTER).append(ENTER)
.append("public class ").append(PROXY_CLASS_NAME).append(" implements ").append(interface_.getName()).append(" {");
Method[] methods = interface_.getMethods();
for(Method method : methods) {
Type returnType = method.getGenericReturnType();
Type[] paramTypes = method.getGenericParameterTypes();
proxyJava.append(ENTER).append(ENTER).append(TAB_STR).append("@Override").append(ENTER)
.append(TAB_STR).append("public ").append(returnType.getTypeName()).append(" ").append(method.getName()).append("(");
for(int i=0; i<paramtypes.length> if (i != 0) {
proxyJava.append(", ");
}
proxyJava.append(paramTypes[i].getTypeName()).append(" param").append(i);
}
proxyJava.append(") {").append(ENTER)
.append(TAB_STR).append(TAB_STR)
.append("System.out.println(\"數據庫操作, 並獲取執行結果...\");").append(ENTER); // 真正數據庫操作,會有返回值,下面的return返回應該是此返回值
if (!"void".equals(returnType.getTypeName())) {
proxyJava.append(TAB_STR).append(TAB_STR).append("return null;").append(ENTER); // 這裡的"null"應該是上述中操作數據庫後的返回值,為了演示寫成了null
}
proxyJava.append(TAB_STR).append("}").append(ENTER);
}
proxyJava .append("}");
// 寫入文件
File f = new File(proxyJavaFileDir + PROXY_CLASS_NAME + ".java");
FileWriter fw = new FileWriter(f);
fw.write(proxyJava.toString());
fw.flush();
fw.close();
}/<paramtypes.length>/<code>
生成的代理類:$Proxy0.java 如下
<code>package com.lee.mapper;
public class $Proxy0 implements com.lee.mapper.UserMapper {
@Override
public java.lang.Integer save(com.lee.model.User param0) {
System.out.println("數據庫操作, 並獲取執行結果...");
return null;
}
@Override
public com.lee.model.User getUserById(java.lang.Integer param0) {
System.out.println("數據庫操作, 並獲取執行結果...");
return null;
}
}/<code>
這個代理類的生成過程是我們自己實現的,實現不難,但排版太繁瑣,我們可以用javapoet來生成代理類源代碼,generateJavaFileByJavaPoet方法如下
<code>/**
* 用JavaPoet生成接口實現類的源代碼,並持久化到java文件
* @param interface_ 目標接口類
* @return
*/
public static void generateJavaFileByJavaPoet(Class> interface_) throws Exception {
// 類名、實現的接口,以及類訪問限定符
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("JavaPoet$Proxy0")
.addSuperinterface(interface_)
.addModifiers(Modifier.PUBLIC);
Method[] methods = interface_.getDeclaredMethods();
for (Method method : methods) {
// 方法參數列表
List<parameterspec> paramList = new ArrayList<>();
Type[] paramTypes = method.getGenericParameterTypes();
int count = 1 ;
for (Type param : paramTypes) {
ParameterSpec paramSpec = ParameterSpec.builder(Class.forName(param.getTypeName()), "param" + count)
.build();
count ++;
paramList.add(paramSpec);
}
// 方法名、方法訪問限定符、參數列表、返回值、方法體等
Class> returnType = method.getReturnType();
MethodSpec.Builder builder = MethodSpec.methodBuilder(method.getName())
.addModifiers(Modifier.PUBLIC)
.addParameters(paramList)
.addAnnotation(Override.class)
.returns(returnType)
.addCode("\\n")
.addStatement("$T.out.println(\"數據庫操作, 並獲取執行結果...\")", System.class) // 真正數據庫操作,會有返回值,下面的return返回應該是此返回值
.addCode("\\n");
if (!"void".equals(returnType.getName())) {
builder.addStatement("return null"); // 這裡的"null"應該是上述中操作數據庫後的返回值,為了演示寫成了null
}
MethodSpec methodSpec = builder.build();
typeSpecBuilder.addMethod(methodSpec);
}
JavaFile javaFile = JavaFile.builder(interface_.getPackage().getName(), typeSpecBuilder.build()).build();
javaFile.writeTo(new File(SRC_JAVA_PATH));
}/<parameterspec>/<code>
生成的代理類:JavaPoet$Proxy0.java 如下
<code>package com.lee.mapper;
import com.lee.model.User;
import java.lang.Integer;
import java.lang.Override;
import java.lang.System;
public class JavaPoet$Proxy0 implements UserMapper {
@Override
public Integer save(User param1) {
System.out.println("數據庫操作, 並獲取執行結果...");
return null;
}
@Override
public User getUserById(Integer param1) {
System.out.println("數據庫操作, 並獲取執行結果...");
return null;
}
}/<code>
利用javapoet生成的代理類更接近我們平時手動實現的類,排版更符合我們的編碼習慣,看上去更自然一些;兩者的實現過程是一樣的,只是javapoet排版更好
2、既然代理類的源代碼已經有了,那麼需要對其編譯了,compileJavaFile方法如下
<code>/**
* 編譯代理類源代碼生成代理類class
* @param proxyJavaFileDir
*/
private static void compileJavaFile(String proxyJavaFileDir) throws Exception {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//獲得文件管理者
StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null);
Iterable extends JavaFileObject> fileObjects = manager.getJavaFileObjects(proxyJavaFileDir + PROXY_CLASS_NAME + ".java");
//編譯任務
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, fileObjects);
//開始編譯,執行完可在指定目錄下看到.class文件
task.call();
//關閉文件管理者
manager.close();
}/<code>
會在指定目錄下看到:$Proxy0.class
3、加載$Proxy0.class,並創建其實例對象(代理實例對象)
<code>public staticT newInstance(Class /<code>interface_) throws Exception{
String proxyJavaFileDir = SRC_JAVA_PATH + interface_.getPackage().getName().replace(".", File.separator) + File.separator;
// 1、生成interface_接口的實現類,並持久化到磁盤:$Proxy0.java
generateJavaFile(interface_, proxyJavaFileDir);
// 2、編譯$Proxy0.java,生成$Proxy0.class到磁盤
compileJavaFile(proxyJavaFileDir);
// 3、加載$Proxy0.class,並創建其實例對象(代理實例對象)
MyClassLoader loader = new MyClassLoader(proxyJavaFileDir, interface_);
Class> $Proxy0 = loader.findClass(PROXY_CLASS_NAME);
return (T)$Proxy0.newInstance();
}
private static class MyClassLoaderextends ClassLoader {
private String proxyJavaFileDir;
private Classinterface_;
public MyClassLoader(String proxyJavaFileDir, Classinterface_) {
this.proxyJavaFileDir = proxyJavaFileDir;
this.interface_ = interface_;
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
File clazzFile = new File(proxyJavaFileDir, name + ".class");
//如果字節碼文件存在
if (clazzFile.exists()) {
//把字節碼文件加載到VM
try {
//文件流對接class文件
FileInputStream inputStream = new FileInputStream(clazzFile);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
baos.write(buffer, 0, len); // 將buffer中的內容讀取到baos中的buffer
}
//將buffer中的字節讀到內存加載為class
return defineClass(interface_.getPackage().getName() + "." + name, baos.toByteArray(), 0, baos.size());
} catch (Exception e) {
e.printStackTrace();
}
}
return super.findClass(name);
}
}
有了代理實例對象,我們就可以利用它進行操作了,演示結果如下
完整工程地址:
https://gitee.com/youzhibing/proxy/tree/master/proxy-java-file
完整流程圖如下
此時的Proxy類能創建任何接口的實例,解決了靜態代理存在的代理類氾濫、多個方法中代理邏輯反覆實現的問題;但有個問題不知道大家注意到:$Proxy0.java有必要持久化到磁盤嗎,我們能不能直接編譯內存中的代理類的字符串源代碼,得到$Proxy0.class呢?
代理類源代碼不持久化
$Proxy0.java和$Proxy0.class是沒必要生成到磁盤的,我們直接編譯內存中的代理類的字符串源代碼,同時直接在內存中加載$Proxy0.class,不用寫、讀磁盤,可以提升不少性能
完整工程地址:
https://gitee.com/youzhibing/proxy/tree/master/proxy-none-java-file
此時的流程圖如下
Proxy.java源代碼如下
<code>package com.lee.proxy;
import com.itranswarp.compiler.JavaStringCompiler;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Map;
public class Proxy {
private static final String ENTER = "\\r\\n";
private static final String TAB_STR = " ";
private static final String PROXY_FILE_NAME = "$Proxy0";
/**
* 生成接口實現類的源代碼, 並持久化到java文件
* @param interface_
* @throws Exception
*/
private static String generateJavaFile(Class> interface_) throws Exception {
StringBuilder proxyJava = new StringBuilder();
proxyJava.append("package ").append(interface_.getPackage().getName()).append(";").append(ENTER).append(ENTER)
.append("public class ").append(PROXY_FILE_NAME).append(" implements ").append(interface_.getName()).append(" {");
Method[] methods = interface_.getMethods();
for(Method method : methods) {
Type returnType = method.getGenericReturnType();
Type[] paramTypes = method.getGenericParameterTypes();
proxyJava.append(ENTER).append(ENTER).append(TAB_STR).append("@Override").append(ENTER)
.append(TAB_STR).append("public ").append(returnType.getTypeName()).append(" ").append(method.getName()).append("(");
for(int i=0; i<paramtypes.length> if (i != 0) {
proxyJava.append(", ");
}
proxyJava.append(paramTypes[i].getTypeName()).append(" param").append(i);
}
proxyJava.append(") {").append(ENTER)
.append(TAB_STR).append(TAB_STR)
.append("System.out.println(\"數據庫操作, 並獲取執行結果...\");").append(ENTER); // 真正數據庫操作,會有返回值,下面的return返回應該是此返回值
if (!"void".equals(returnType.getTypeName())) {
proxyJava.append(TAB_STR).append(TAB_STR).append("return null;").append(ENTER); // 這裡的"null"應該是上述中操作數據庫後的返回值,為了演示寫成了null
}
proxyJava.append(TAB_STR).append("}").append(ENTER);
}
proxyJava .append("}");
return proxyJava.toString();
}
private final static Class> compile(String className, String content) throws Exception {
JavaStringCompiler compiler = new JavaStringCompiler();
Map<string> byteMap = compiler.compile(PROXY_FILE_NAME + ".java", content);
Class> clazz = compiler.loadClass(className, byteMap);
return clazz;
}
public staticT newInstance(Class /<string>/<paramtypes.length>/<code>interface_) throws Exception{
// 1、生成源代碼字符串
String proxyCodeStr = generateJavaFile(interface_);
// 2、字符串編譯成Class對象
Class> clz = compile(interface_.getPackage().getName() + "." + PROXY_FILE_NAME, proxyCodeStr);
return (T)clz.newInstance();
}
}
相比有代理類源代碼持久化,核心的動態代理生成過程不變,只是減少了.java和.class文件的持久化;其中用到了第三方工具:com.itranswarp.compile(我們也可以拓展jdk,實現內存中操作),完成了字符串在內存中的編譯、class在內存中的加載,直接用jdk的編譯工具,會在磁盤生成$Proxy0.class
測試結果如下
可以看到,沒有.java和.class的持久化
此時就完美了嗎?如果現在有另外一個接口ISendMessage,代理邏輯不是
<code>System.out.println("數據庫操作, 並獲取執行結果...")/<code>
我們該怎麼辦?針對ISendMessage又重新寫一個Proxy?顯然還不夠靈活,說的簡單點:此種代理可以代理任何接口,但是代理邏輯確是固定死的,不能自定義,這樣會造成一種代理邏輯會有一個代理工廠(Proxy),會造成代理工廠的泛濫。 更多的設計模式內容,可以在咱們Java知音公眾號回覆“設計模式聚合”,完整講解23種設計模式
代理邏輯接口化,供用戶自定義
既然無代理類源代碼持久化中的代理邏輯不能自定義,那麼我們就將它抽出來,提供代理邏輯接口
完整工程地址:
https://gitee.com/youzhibing/proxy/tree/master/proxy-none-java-file-plus
流程圖與無代理類源代碼持久化中一樣,此時代理類的生成過程複雜了不少,涉及到代理邏輯接口:InvacationHandler的處理
generateJavaFile(…)方法
<code>/**
* 生成接口實現類的源代碼
* @param interface_
* @throws Exception
*/
private static String generateJavaFile(Class> interface_, InvocationHandler handler) throws Exception {
StringBuilder proxyJava = new StringBuilder();
proxyJava.append("package ").append(PROXY_PACKAGE_NAME).append(";").append(ENTER).append(ENTER)
.append("import java.lang.reflect.Method;").append(ENTER).append(ENTER)
.append("public class ").append(PROXY_FILE_NAME).append(" implements ").append(interface_.getName()).append(" {").append(ENTER)
.append(ENTER).append(TAB_STR).append("private InvocationHandler handler;").append(ENTER).append(ENTER);
// 代理對象構造方法
proxyJava.append(TAB_STR).append("public ").append(PROXY_FILE_NAME).append("(InvocationHandler handler) {").append(ENTER)
.append(TAB_STR).append(TAB_STR).append("this.handler = handler;").append(ENTER)
.append(TAB_STR).append("}").append(ENTER);
// 接口方法
Method[] methods = interface_.getMethods();
for(Method method : methods) {
String returnTypeName = method.getGenericReturnType().getTypeName();
Type[] paramTypes = method.getGenericParameterTypes();
proxyJava.append(ENTER).append(TAB_STR).append("@Override").append(ENTER)
.append(TAB_STR).append("public ").append(returnTypeName).append(" ").append(method.getName()).append("(");
List<string> paramList = new ArrayList<>(); // 方法參數值
List<string> paramTypeList = new ArrayList<>(); // 方法參數類型
for(int i=0; i<paramtypes.length> if (i != 0) {
proxyJava.append(", ");
}
String typeName = paramTypes[i].getTypeName();
proxyJava.append(typeName).append(" param").append(i);
paramList.add("param" + i);
paramTypeList.add(typeName+".class");
}
proxyJava.append(") {").append(ENTER)
.append(TAB_STR).append(TAB_STR).append("try {").append(ENTER)
.append(TAB_STR).append(TAB_STR).append(TAB_STR)
.append("Method method = ").append(interface_.getName()).append(".class.getDeclaredMethod(\"")
.append(method.getName()).append("\",").append(String.join(",", paramTypeList)).append(");")
.append(ENTER).append(TAB_STR).append(TAB_STR).append(TAB_STR);
if (!"void".equals(returnTypeName)) {
proxyJava.append("return (").append(returnTypeName).append(")");
}
proxyJava.append("handler.invoke(this, method, new Object[]{")
.append(String.join(",", paramList)).append("});").append(ENTER)
.append(TAB_STR).append(TAB_STR).append("} catch(Exception e) {").append(ENTER)
.append(TAB_STR).append(TAB_STR).append(TAB_STR).append("e.printStackTrace();").append(ENTER)
.append(TAB_STR).append(TAB_STR).append("}").append(ENTER);
if (!"void".equals(returnTypeName)) {
proxyJava.append(TAB_STR).append(TAB_STR).append("return null;").append(ENTER);
}
proxyJava.append(TAB_STR).append("}").append(ENTER);
}
proxyJava .append("}");
// 這裡可以將字符串生成java文件,看看源代碼對不對
/*String proxyJavaFileDir = System.getProperty("user.dir") + File.separator + "proxy-none-java-file-plus"
+ String.join(File.separator, new String[]{"","src","main","java",""})
+ PROXY_PACKAGE_NAME.replace(".", File.separator) + File.separator;
File f = new File(proxyJavaFileDir + PROXY_FILE_NAME + ".java");
FileWriter fw = new FileWriter(f);
fw.write(proxyJava.toString());
fw.flush();
fw.close();*/
return proxyJava.toString();
}/<paramtypes.length>/<string>/<string>/<code>
測試結果如下
此時各組件之間關係、調用情況如下
此時Proxy就可以完全通用了,可以生成任何接口的代理對象了,也可以實現任意的代理邏輯;至此,我們完成了一個簡易的仿JDK實現的動態代理
JDK的動態代理
我們來看看JDK下動態代理的實現,示例工程:
https://gitee.com/youzhibing/proxy/tree/master/proxy-jdk
測試結果就不展示了,我們來看看JDK下Proxy.newInstance方法,有三個參數1.Classloader:類加載器,我們可以使用自定義的類加載器;上述手動實現示例中,直接在Proxy寫死了;
2.Class>[]:接口類數組,這個其實很容易理解,我們應該允許我們自己實現的代理類同時實現多個接口。我們上述手動實現中只傳入一個接口,是為了簡化實現;
3.InvocationHandler:這個沒什麼好說的,與我們的實現一致,用於自定義代理邏輯
我們來追下源碼,看看JDK的動態代理是否與我們的手動實現是否一致
與我們的自定義實現差不多,利用反射,逐個接口、逐個方法進行處理;
ProxyClassFactory負責生成代理類的Class對象,主要由apply方法負責,調用了
<code>byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);/<code>
來生成代理類的Class;ProxyGenerator中有個是有靜態常量:saveGeneratedFiles,標識是否持久化代理類的class文件,默認值是false,也就是不持久化,我們可以通過設置jdk系統參數,實現JDK的動態代理持久化代理類的class文件
CGLIB代理
對cglib不做深入研究了,只舉個使用案例:proxy-cglib,使用方式與JDK的動態代理類似,實現的效果也基本一致,但是實現原理上還是有差別的
JDK的動態代理有一個限制,就是使用動態代理的對象必須實現一個或多個接口,而CGLIB沒有這個限制,具體區別不是本文範疇了,大家自行去查閱資料
應用場景
長篇大論講了那麼多,我們卻一直沒有講動態代理的作用,使用動態代理我們可以在不改變源碼的情況下,對目標對象的目標方法進行前置或後置增強處理。這有點不太符合我們的一條線走到底的編程邏輯,這種編程模型有一個專業名稱叫AOP,面向切面編程,具體案例有如下:
1、spring的事務,事務的開啟可以作為前置增強,事務的提交或回滾作為後置增強,數據庫的操作處在兩者之間(目標對象需要完成的事);
2、日誌記錄,我們可以在不改變原有實現的基礎上,對目標對象進行日誌的輸出,可以前置處理,記錄參數情況,也可以後置處理,記錄返回的結果;
3、web編程,傳入參數的校驗;
4、web編程,權限的控制也可以用aop來實現;
只要明白了AOP,那麼哪些場景能使用動態代理也就比較明瞭了
總結
1、示例代碼中的Proxy是代理工廠,負責生產代理對象的,不是代理對象類
2、手動實現動態代理,我們分了三版
第一版:代理類源代碼持久化,為了便於理解,我們將代理類的java文件和class文件持久化到了磁盤,此時解決了靜態代理中代理類氾濫的問題,我們的代理類工廠(Proxy)能代理任何接口;
第二版:代理類源代碼不持久化,代理類的java文件和和class文件本來就只是臨時文件,將其去掉,不用讀寫磁盤,可以提高效率;但此時有個問題,我們的代理邏輯卻寫死了,也就說一個代理類工廠只能生產一種代理邏輯的代理類對象,如果我們有多種代理邏輯,那麼就需要有多個代理類工廠,顯然靈活性不夠高,還有優化空間;
第三版:代理邏輯接口化,供用戶自定義,此時代理類工廠就可以代理任何接口、任何代理邏輯了,反正代理邏輯是用戶自定義傳入,用戶想怎麼定義就怎麼定義;
3、示例參考的是mybatis中mapper的生成過程,雖然只是簡單的模擬,但流程卻是一致的
閱讀更多 Java桔煙 的文章