Java代理
導語
在Java實際開發過程中,開發者總會面臨着狠多類需要增強的問題,由於各種條件的限制,我們無法對原有的類進行修改,通常情況下開發者需要開發一個類去代替原有的類進行工作,這種替代原有的進行工作的類我們就稱之爲代理類,這種工作方式叫做代理模式。
1、Java代理模式分類
Java代理模式主要分爲靜態代理模式和動態代理模式。這種分類的方法是根據Java虛擬機運行的過程進行劃分的。一個Java的最終運行可以籠統的概括爲三個階段:Java源代碼編寫階段、Java虛擬機通過將Java源碼編譯爲class字節碼階段和Java虛擬機根據class字節碼運行得出結果階段, 如圖所示Java運行的三個階段。
一般情況下,開發者根據自己的業務需求編寫Java代碼,將編寫完的Java代碼交給Java虛擬機進行編譯、運行。在對代理類開發的過程中,開發者可以根據代理類需求開發出所需要得到的代理類的Java文件將其交給Java虛擬機進行編譯、運行,就可以得到我們需要運行的結果,這樣實行的代理功能模式成爲靜態代理模式。開發者也可以編寫一個類使其上傳一個代理工具類的Java代碼的字符串,然後對這個Java字符串生成Java文件,之後編譯執行,這種實行的代理功能模式成爲靜動態理模式。
在代理編程的工程中,目標對象和代理對象是兩個相對的概念、目標對象是指原有業務員邏輯所在類的對象,即需要被增強類的對象;代理對象就是指代理增強後的類所產生的對象。
2、代理實現
下面列舉具體是例子進行說明。
(1)目標對象的實現
在開發者對一般邏輯開發時一般採用MVC三層架構來實現我們的應用程序,現在列舉數據庫連接層作爲示例,闡述目標對象和代理對象。
編寫數據庫連接層接口類IndexDao.class
public interface IndexDao {
/**
* 目標對象數據庫連接層方法
*/
public void query();
}
編寫數據庫連接層實現類IndexDaoImpl.class
public class IndexDaoImpl implements IndexDao {
/**
* 模擬目標對象數據庫連接層方法實現
*/
@Override
public void query() {
System.out.println ("query");
}
}
編寫數據庫連接層測試類IndexDaoTest.class
public class IndexDaoImplTest {
/**
*目標對象測試方法
*/
@Test
public void query() {
IndexDao indexDao = new IndexDaoImpl ();
indexDao.query ();
}
}
運行結果:
運行結束後再控制檯成打印出模擬數據庫連接層的打印結果。
(2)靜態代理模式
靜態代理模式有兩種實現方式,一種實現方式爲繼承,另一種實現方式爲聚合,兩種代理形式都可以對目標對象的業務邏輯進行增強,但在實際開發過程中會存在一定的問題。
用繼承的形式實現靜態代理
繼承的形式實現動態代理就是繼承原有的邏輯的類從而實現對原有類的邏輯進行增強。
下面以繼承的方式實現靜態代理,完成對IndexDaoImpl.class日誌記錄的擴展:
public class LogProxy extends IndexDaoImpl {//繼承需要增強的目標類
@Override
public void query() {
// 編寫需要在原有邏輯運行之前的邏輯
System.out.println ("------Log-------");
// 調用父類的方法運行原有的邏輯
super.query ();
// 編寫需要在原有邏輯運行之前的邏輯
System.out.println ("-----Log End----");
}
}
在編寫這個類是,首先要繼承目標標對象所在的類,然後覆蓋原有的目標方法,在該方法中調用父類的目標方法,同時在父類目標方法的前後先寫需要增強業務邏輯,以完成對目標對象的增強。
編寫測試類,測試增強增強後的代理類,下面是以繼承的方式實現靜態代理,完成對IndexDao日誌記錄的擴展測試類
public class LogProxyTest {
@Test
public void query() {
// 創建代理對象
LogProxy logProxy = new LogProxy ();
// 調用代理類的代理方法
logProxy.query ();
}
}
運行結果:
從結果中可以看出,代理類在原有邏輯的前後都添加了增強的業務邏輯。依照相同的方法,可以對目標對象的進行不同邏輯的增強。
下面以繼承的方式實現靜態代理,完成對IndexDaoImpl.class時間記錄的擴展:
public class TimeProxy extends IndexDaoImpl {//繼承需要增強的目標類
@Override
public void query() {
// 編寫需要在原有邏輯運行之前的邏輯
System.out.println ("------Time-------");
// 調用父類的方法運行原有的邏輯
super.query ();
// 編寫需要在原有邏輯運行之前的邏輯
System.out.println ("-----Time End----");
}
}
編寫時間記錄擴展類的測試類:
public class TimeProxyTest {
@Test
public void query() {
// 創建代理對象
TimeProxy timeProxy = new TimeProxy ();
// 實現代理對象的代理方法
timeProxy.query ();
}
}
運行結果:
按照相同的邏輯,如果想把原始的目標對象及加上時間記錄的擴展邏輯又加上日誌的擴展邏輯,可以將LogProxy.class類作爲目標對象進行擴展完成TimeLogProxy.class的代理對象,代碼爲:
public class TimeLogProxy extends LogProxy {//繼承LogProxy
/**
* 將LogProxy作爲目標對象,該類爲LogProxy類的代理對象,
* 在這裏覆蓋的方法中調用父類的構造方法並家屬代理類的邏輯。
*/
@Override
public void query() {
// 編寫需要在原有邏輯運行之前的邏輯
System.out.println ("------Time-------");
// 調用父類的方法運行原有的邏輯
super.query ();
// 編寫需要在原有邏輯運行之前的邏輯
System.out.println ("-----Time End----");
}
}
運行結果爲:
在TimeLogProxy.class和LogProxy.class兩個類之間的關係中,後者爲前者的目標類產生的對象爲目標對象,而後者是前者發代理類,產生的對象爲代理對象,所以在代理開發的過程中目標對象和代理對象是兩個相對的概念而非絕對的概念,代理對象和目標對象會根據不同的開發環境和邏輯的改變而改變。
按照上述的繼承規律,如果想對TimeProxy.class類進行增強,就可以再開發一個類繼承TimeProxy,class類就可以實現。由於不同的開發環境和邏輯的改變,增強的邏輯也會變得複雜的多,因此需要開發的類也變得多,如果每次需要用這樣的方法進行類的增強那麼需要開發各種不同的類,有些人將這種現象叫做“類爆炸”。
用聚合的方式實現靜態代理
爲了儘量避免“類爆炸”的情況發生,可以採用聚合的方式對目標對象進行增強。用這種方法的前提是目標方法所在的類需要有被實現的接口,這樣代理對象所在的類實現與目標對象所在的類相同等接口,然後在代理對象中將目標對象注入,在代理對象所在類的代理方法中條用目標對象的方法在對目標對象的前後添加需要增強的邏輯,這樣也可以實現對目標對象的增強。
以聚合的方式實現靜態代理,對IndexDap.class日誌記錄的擴展類代碼如下:
public class LogProxy implements IndexDao {//實現與目標對象相同的IndexDao.class接口
// 添加與目標對象相同的IndexDao.class的屬性
private IndexDao indexDao;
/**
* 添加構造方法,把目標對象注入到代理類的目標對象中
* @param indexDao 目標對象
*/
public LogProxy(final IndexDao indexDao) {
// 把目標對象注入到代理類的目標對象中
this.indexDao = indexDao;
}
@Override
public void query() {
// 編寫需要在原有邏輯運行之前的邏輯
System.out.println ("------Log-------");
// 調用目標對象的目標方法運行原有的邏輯
indexDao.query ();
// 編寫需要在原有邏輯運行之前的邏輯
System.out.println ("-----Log End---");
}
}
編寫測試類:
public class LogProxyTest {
@Test
public void query() {
// 創建目標對象
IndexDao indexDao = new IndexDaoImpl ();
// 創建代理對象,將目標對象注入到代理對象中
LogProxy logProxy = new LogProxy (indexDao);
// 調用代理對象的代理方法
logProxy.query ();
}
}
運行結果:
按照相同的方法,可以得到以聚合的方式實現靜態代理,對IndexDap.class時間記錄的擴展類,代碼如下:
public class TimeProxy implements IndexDao {//實現與目標對象相同的IndexDao.class接口
// 添加與目標對象相同的IndexDao.class的屬性
private IndexDao indexDao;
/**
* 添加構造方法,把目標對象注入到代理類的目標對象中
* @param indexDao 目標對象
*/
public TimeProxy(final IndexDao indexDao) {
// 把目標對象注入到代理類的目標對象中
this.indexDao = indexDao;
}
@Override
public void query() {
// 編寫需要在原有邏輯運行之前的邏輯
System.out.println ("------Time-------");
// 調用目標對象的目標方法運行原有的邏輯
indexDao.query ();
// 編寫需要在原有邏輯運行之前的邏輯
System.out.println ("-----Time End----");
}
}
編寫IndexDap.class時間記錄的擴展類測試類,代碼如下:
public class TimeProxyTest {
@Test
public void query() {
// 創建目標對象
IndexDao indexDao = new IndexDaoImpl ();
// 創建代理對象,將目標對象注入到代理對象中
TimeProxy TimeProxy = new TimeProxy (indexDao);
// 調用代理對象的代理方法
TimeProxy.query ();
}
}
運行結果:
如果把LogProxy.class產生的對象注入到TimeProxy.claa的對象中去,就可以得到日誌和時間的雙創邏輯:
編寫測試代碼如下:
@Test
public void TimeLogProxyTest() {
// 創建目標對象
IndexDao indexDao = new IndexDaoImpl ();
// 創建日誌記錄代理對象,將IndexDao對象注入到日誌代理對象中
LogProxy logProxy = new LogProxy (indexDao);
// 創建代理對象,將日誌代理對象注入到時間代理對象中
TimeProxy TimeProxy = new TimeProxy (logProxy);
// 調用時間代理對象的代理方法
TimeProxy.query ();
}
運行結果:
這樣也可以實現多重代理,這樣只要寫好單獨的代理類後就可以根據業務邏輯的需求進行代理,但在實際開發過程中由於代理邏輯比較複雜,也會有“類爆炸”的問題發生,爲了避免這樣的問題,通常會採用動態代理的方式來解決這樣的問題。
(2)動態代理模式
動態代理模式,主要是根據目標類的對象動態生成代理類對象的Java代碼的字符串,將Java代碼寫成Java文件動態編譯成class文件,最後將代理對象的字節碼文件加載到內存中創建的代理對象實現對目標對象邏輯的增強。
定義全局變量
把代理類的名稱和存儲路徑最爲全局變量定義在代理工具類中:
// 定義代理對象類存儲路徑的靜態表裏
private static final String pathName = "d:\\org\\s2pe\\";
// 定義代理對象報名的靜態變量
private static final String pathPackage = "org.s2pe";
// 定義代理對象簡單名稱的靜態變量
private static final String proxySimpleName = "$proxy";
// 定義代理對象全名的靜態變量
private static final String proxyName = pathPackage + "." +proxySimpleName;
創建工具類和工具方法
創建工具方法,改方法傳入一個目標對象,最終放回代理對象,首先在代碼中定義一個返回的代理對象,爲了避免調用值爲空,將定義的代理對象的初始值設爲null,最終返回這個對象,代碼如下:
/**
* 創建代理示例的工具方法
* @param target 傳入目標對象
* @return 返回代理對象
*/
public static Object newInstance(Object target) {
// 定義返回的代理對象,默認爲空值,在方法最後返回
Object proxy = null;
// 返回代理對象
return proxy;
}
}
在創建代理對象是分爲四個步驟:創建、拼接代理類的字符串內容;將代理類生成的字符串內容寫到指定磁盤路徑下;將寫到磁盤中發Java文件編譯成class文件和將磁盤上的代理對象字節碼文件加到內存中,獲取動態代理對象。因此在代理對象工具類中創一次創建以上四個方法,代發分別爲:
創建、拼接代理類的字符串內容的方法的代碼:
/**
* 創建、拼接代理類的字符串內容
* @param targetInfClass 目標對象所在類的接口
* @return 拼接後代理類的字符串內容和
*/
private static String createProxyContent(Class<?> targetInfClass) {
String conntent = "";
return conntent;
}
將代理類生成的字符串內容寫到指定磁盤路徑下的方法的代碼:
/**
* 將代理類生成的字符串內容寫到指定磁盤路徑下
* @param proxyContent 代理類生成的字符串內容
* @return 生成的磁盤文件對象
*/
private static File writeProxyContent(String proxyContent) {
return null;
}
將寫到磁盤中發Java文件編譯成class文件的方法的代碼:
/**
* 將寫到磁盤中發Java文件編譯成class文件
* @param proxyContent 生成的磁盤文件對象
*/
private static void compileProxyContent(File proxyContent) {
}
將磁盤上的代理對象字節碼文件加到內存中的方法的代碼:
/**
* 將磁盤上的代理對象字節碼文件加到內存中,獲取動態代理對象
* @param proxyClassName 目標對象的字節碼文件的名稱
* @param target 目標對象
* @return 代理對象
*/
private static Object getProxyInstance(Object target) {
Object proxyInctance = null;
return proxyInctance;
}
完成createProxyContent方法編碼
createProxyContent方法就是根據目標類的接口類對象和代理類的包名完成動態代理類代碼的拼接,最後完成的拼接的內容爲:
//package org.s2pe.sta.polymerization;
//import org.s2pe.dao.IndexDao;
//public class LogProxy implements IndexDao {
// private IndexDao indexDao;
// public LogProxy(final IndexDao indexDao) {
// this.indexDao = indexDao;
// }
// @Override
// public void query() {
// System.out.println ("------Log-------");
// indexDao.query ();
// System.out.println ("-----Log End---");
// }
//}
在此方法中首先定義幾個常量,以便於後期的使用:
// 定義分號常量
final String SEMICOLON = ";";
// 定義換行常量
final String NEXTLINE = "\n";
// 定義左花括號常量
final String BRACKET_LEFT = "{";
// 定義左右括號常量
final String BRACKET_RUGHT = "}";
// 定義縮進字符常量
final String tab = "\t";
拼接生成代理類的Java代碼:
// 拼接Java文件包定義行 package org.s2pe.sta.polymerization;
String packageLine = "package " + pathPackage + SEMICOLON + NEXT_LINE;
// 在代理類生成的過程中利用報名加類名的形式進行拼接,在這裏就不同對導包的代碼行進行拼接
// import org.s2pe.dao.IndexDao;
// 獲取目標對象接口名稱
String targetInfClassName = targetInfClass.getName ();
// 拼接代理對象類定義行 public class LogProxy implements IndexDao {
String classLine = "public class " + proxySimpleName + " implements " + targetInfClassName + BRACKET_LEFT + NEXT_LINE;
// 定義代理對象屬性名
String targetParamName = "target";
// 拼接定義代理對象屬性行 private IndexDao indexDao;
String defindTargetParamLine = TAB + "private " + targetInfClassName + " " + targetParamName + SEMICOLON + NEXT_LINE;
// 新建代理對象構造方法字符對象
StringBuffer proxyConstructorStirngBuffer = new StringBuffer ("");
// 拼接代理對象構造方法首行字符串 public LogProxy(final IndexDao indexDao) {
String proxyConstructorFirst = TAB + "public " + proxySimpleName + "(" + targetInfClassName + " " + targetParamName + ")" + BRACKET_LEFT + NEXT_LINE;
// 拼接代理對象構造方法內部字符串 this.indexDao = indexDao;
String proxyConstructor = TAB + TAB + "this." + targetParamName + " = " + targetParamName + SEMICOLON + NEXT_LINE;
// 拼接代理對象構造方法尾行字符串 }
String proxyConstructorEnd = TAB + BRACKET_RIGHT + NEXT_LINE;
// 拼接代理對象構造方法代碼
proxyConstructorStirngBuffer.append (proxyConstructorFirst + proxyConstructor + proxyConstructorEnd);
// 創建覆蓋所有目標對象方法的代理對象方法代碼字符串對象
StringBuffer procyDeclaredMethods = new StringBuffer ();
// 獲取目標對象接口的公共方法
Method[] targetDeclaredMethods = targetInfClass.getDeclaredMethods ();
for (final Method targetDeclaredMethod : targetDeclaredMethods) {
// 創建覆蓋目標對象方法的代理對象方法代碼字符串對象 @Override
StringBuffer proxyDeclaredMethod = new StringBuffer (TAB + "@Override" + NEXT_LINE);
// 獲取目標對象接口的公共方法的返回值類型
Class<?> targetDeclaredMethodReturnType = targetDeclaredMethod.getReturnType ();
// 獲取目標對象接口的公共方法的名稱
String targetDeclaredMethodName = targetDeclaredMethod.getName ();
// 獲取目標對象接口的公共方法的所有參數類
Class<?>[] targetDeclaredMethodParameterTypes = targetDeclaredMethod.getParameterTypes ();
// 創建覆蓋目標對象方法的代理對象方法代碼字第一行符串對象
StringBuffer proxyDeclaredMethodFirstLine = new StringBuffer ();
// 拼接覆蓋目標對象方法的代理對象方法代碼字第一行前部分內容 public void query(
proxyDeclaredMethodFirstLine.append (TAB + "public " + targetDeclaredMethodReturnType.getName () + " " + targetDeclaredMethodName + "(");
// 定義覆蓋目標對象方法的代理對象方法傳入參數前綴
String targetDeclaredMethodParameterPreName = "arg";
// 判斷覆蓋目標對象方法的代理對象方法傳入是否參數內容
if (targetDeclaredMethodParameterTypes.length > 0) {
// 創建覆蓋目標對象方法的代理對象方法傳入參數字符串對象
StringBuffer targetDeclaredMethodParameterStr = new StringBuffer ();
// 遍歷目標對象接口的公共方法的所有參數類,拼接覆蓋目標對象方法的代理對象方法傳入參數字符串
for (int i = 0; i < targetDeclaredMethodParameterTypes.length; i++) {
// 拼接覆蓋目標對象方法的代理對象方法傳入參數內容
targetDeclaredMethodParameterStr.append (targetDeclaredMethodParameterTypes[i].getName () + " " + targetDeclaredMethodParameterPreName + i + ", ");
}
// 截取覆蓋目標對象方法的代理對象方法傳入參數字符串多餘的逗號
targetDeclaredMethodParameterStr.deleteCharAt (targetDeclaredMethodParameterStr.lastIndexOf (","));
// 將覆蓋目標對象方法的代理對象方法傳入參數字符串添加到覆蓋目標對象方法的代理對象方法代碼字第一行
proxyDeclaredMethodFirstLine.append (targetDeclaredMethodParameterStr);
}
// 拼接覆蓋目標對象方法的代理對象方法代碼字第一行後部分內容 ) {
proxyDeclaredMethodFirstLine.append (")" + BRACKET_LEFT + NEXT_LINE);
// ********************此處編寫代理方法的前置邏輯代碼*******************************
// System.out.println ("------Log-------");
// 創建覆蓋目標對象方法的代理對象方法內容代碼對象
StringBuffer proxyDeclaredMethodMess = new StringBuffer (TAB + TAB);
// 指目標對象方法返回值名稱
String tergerMethodRetrunObjectName = "targetReturn";
// 獲取目標隊形返回返回值類名
String targetDeclaredMethodReturnName = targetDeclaredMethod.getReturnType ().getName ();
// 判斷目標對象方法是否有返回值
if (!"void".equals (targetDeclaredMethodReturnName)){
// 添加接收目標方法代理返回值對象
proxyDeclaredMethodMess.append ( targetDeclaredMethodReturnName + " " + tergerMethodRetrunObjectName + "=("+ targetDeclaredMethodReturnName +") ");
}
// 定義覆蓋目標對象方法的代理對象方法內容代碼前部分字符串 indexDao.query (
String proxyDeclaredMethodMessFirst = targetParamName+ "." + targetDeclaredMethodName + "(";
// 創建覆蓋目標對象方法的代理對象方法內容代碼參數部分字符串對象
StringBuffer proxyDeclaredMethodMessPara = new StringBuffer ();
// 判斷覆蓋目標對象方法的代理對象方法是否有參數
if (targetDeclaredMethodParameterTypes.length > 0) {
for (int i = 0; i < targetDeclaredMethodParameterTypes.length; i++) {
// 拼接覆蓋目標對象方法的代理對象方法內容
proxyDeclaredMethodMessPara.append (targetDeclaredMethodParameterPreName + i + ", ");
}
// 截取覆蓋目標對象方法的代理對象方法內容多餘的逗號
proxyDeclaredMethodMessPara.deleteCharAt (proxyDeclaredMethodMessPara.lastIndexOf (","));
}
// 定義覆蓋目標對象方法的代理對象方法內容代碼後部分字符串 );
String proxyDeclaredMethodMessEnd = ")" + SEMICOLON + NEXT_LINE;
// 連接覆蓋目標對象方法的代理對象方法內容代碼字符串
proxyDeclaredMethodMess.append (proxyDeclaredMethodMessFirst + proxyDeclaredMethodMessPara + proxyDeclaredMethodMessEnd);
// ********************此處編寫代理方法的後置邏輯代碼*******************************
// System.out.println ("-----Log End---");
// 判斷目標對象方法是否有返回值
if (!"void".equals (targetDeclaredMethodReturnName)){
// 添加返回值對象代碼
proxyDeclaredMethodMess.append (TAB + TAB + "return " + tergerMethodRetrunObjectName + SEMICOLON + NEXT_LINE);
}
// 創建覆蓋目標對象方法的代理對象方法代碼字第一行符串對象 }
String proxyDeclaredMethodEndLine = TAB + BRACKET_RIGHT + NEXT_LINE;
proxyDeclaredMethod.append (proxyDeclaredMethodFirstLine).append (proxyDeclaredMethodMess + proxyDeclaredMethodEndLine);
// 將覆蓋目標對象方法的代理對象方法代碼字符串對象添加到所有覆蓋目標對象方法的代理對象方法代碼字符串對象中
procyDeclaredMethods.append (proxyDeclaredMethod);
}
// 拼接類最後一行代碼 }
String classLineEnd = BRACKET_RIGHT + NEXT_LINE;
// 定義代理類代碼內容
String content = "";
// 拼接代理類代碼
content = packageLine + classLine + defindTargetParamLine + proxyConstructorStirngBuffer + procyDeclaredMethods + classLineEnd;
編輯將代理類生產字符串寫入磁盤的方法:
// 創建問幾點對象
File prosyFile = new File (pathName + proxySimpleName + ".java");
try {
if (prosyFile.exists ()) {
prosyFile.createNewFile ();
}
FileWriter writer = new FileWriter (prosyFile);
writer.write (proxyContent);
writer.flush ();
writer.close ();
} catch (IOException e) {
e.printStackTrace ();
}
return prosyFile;
編寫將寫入磁盤類文編譯成字節碼文件代碼:
try {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler ();
StandardJavaFileManager standardFileManager = compiler.getStandardFileManager (null, null, null);
Iterable<? extends JavaFileObject> untis = standardFileManager.getJavaFileObjects (proxyContent);
JavaCompiler.CompilationTask task = compiler.getTask (null, standardFileManager, null, null, null, untis);
task.call ();
standardFileManager.close ();
} catch (IOException e) {
e.printStackTrace ();
}
編寫將磁盤上的代理對象字節碼文件加到內存中,獲取動態代理對象方法:
Object proxyInctance = null;
try {
URL[] urls = {new URL ("file:D:\\\\")};
URLClassLoader classLoader = new URLClassLoader (urls);
Class<?> clazz = classLoader.loadClass (proxyName);
Constructor<?> constructor = clazz.getConstructor (target.getClass ().getInterfaces ()[0]);
proxyInctance = constructor.newInstance (target);
} catch (Exception e) {
e.printStackTrace ();
}
return proxyInctance;
添加在目標對象接口中添加帶有參數和返回值的方法,以便後續進行測試:
public String list(int id, String name);
完善目標類實現:
@Override
public String list(int id, String name) {
return name;
}
編寫測試類:
@Test
public void newInstance() {
IndexDao indexDao = (IndexDao) ProxyUtil.newInstance (new IndexDaoImpl ());
indexDao.query ();
System.out.println (indexDao.list (1, "proxy"));
}
運行結果:
到此,自定義動態代理工具類測試成功。
以上代碼已傳到GitHub上,望大家批評指正。GitHub地址:https://github.com/s2pe/proxy