前言:本文章總結於馬士兵老師系列教程,是根據視頻中提出的問題的思維爲大綱寫的
體會設計模式
可能接觸過設計模式的人都會有一種疑惑:感覺很多設計模式的實現方式都非常的相似,就比如說代理模式和裝飾模式。確實有些設計模式的實現方式是差不多的,但是他們是從不同的場景出發,解決不同的問題的,我們需要從思想的角度來體會設計模式。
代理模式
由一個實際問題來表達代理模式的思想
注:源代碼在最後,過程中的代碼不一定能運行,請參看源代碼
提出問題
新建一個Car類,包含一個drive方法,現在要求是在不改變Car代碼的前提下計算drive運行的時間
Car代碼:
public class Car {
public void drive() throws InterruptedException {
System.out.println("開車了...");
Thread.sleep(1000);
}
}
解決統計時間的問題
如果要統計時間,我們現在要將代碼寫成這樣:
public class Car {
/**
* 解決這個問題我們需要在drive運行開始和結束的位置加入系統當前時間
* 再得到兩個時間差
* @throws InterruptedException
*/
@Test
public void drive() throws InterruptedException {
//獲取開始時間點
long i = System.currentTimeMillis();
System.out.println("開車了...");
Thread.sleep(1000);
//獲取結束時間點
long j = System.currentTimeMillis();
System.out.println("車開了"+(j-i)+"millis");
}
}
解決不能使用改動drive代碼的問題
現在是不能改動drive的那要怎麼把計算時間的代碼插入進去呢?
這個時候我們就需要用到代理模式了,Car的代碼不能改但是它的代理類可以,而代理方式也有兩種如下:
1. 通過繼承Car來代理
public class ExtendsCar extends Car {
//這樣我們可以通過重寫Car的drive方法來實現代理
@Override
public void drive() throws InterruptedException {
long i = System.currentTimeMillis();
super.drive();
long j = System.currentTimeMillis();
System.out.println("車開了:"+(i+j)+"millis");
}
}
- 另外一種方式通過聚合的代理
聚合,一個類中包含另外一個類。
用聚合的方式代理時,我們需要代理類和被代理類有同樣的行爲,我們可以通過實現相同的接口來實現
接口代碼:
public interface Move {
public void drive();
}
讓Car類implement接口,這裏就不貼代碼了
代理類實現接口:
public class TimeCarProxy implements Move {
Car c ;
public TimeCarProxy(Car c){
this.c=c;
}
@Override
public void drive() {
long i = System.currentTimeMillis();
System.out.println("開始時間:" + i);
move.drive();
long j = System.currentTimeMillis();
System.out.println("結束時間:" + j);
System.out.println("車開了:"+(j-i)+"millis");
}
}
問題:兩種實現方式那個比較好?
這裏還是以一個問題來表達:
如果我還要在前面drive前後加上啓動和停止怎麼辦?
- 以繼承的方式實現,我們的思路是讓它的代理類再被代理,代碼如下:
public class CarAction extends ExtendsCar {
@Override
public void drive() {
System.out.println("啓動車!");
super.drive();
System.out.println("停止車!");
}
}
這種方式的問題:
1. 繼承被佔用,不能再繼承其它類
2. 主要的問題:如果我要將他們的順序反過來,先啓動停止在計算時間的話,那不就意味着我們要重新寫它的代理類嗎?此時繼承的侷限性就顯現出來了
2. 以聚合的方式實現:我們可以再來一個代理實現Move接口,代碼如下:
public class Action implements Move {
ImplementMove move ;
public Action(ImplementMove move){
this.move=move;
}
@Override
public void drive() {
System.out.println("車啓動!");
move.drive();
System.out.println("車停止!");
}
}
到這裏可能會有疑問,這不是跟繼承存在一樣的問題嗎?別急,請看下面一種實現:
public class ActionCarProxy implements Move {
/**
* 因爲它們有一個共同的特點就是實現了Move,若我們把聚合對象換成
* Move接口不就想讓誰代理就誰代理了嗎?
* 同樣我們也要將代理時間的類改成Move
*/
Move move ;
public ActionCarProxy (Move move){
this.move=move;
}
@Override
public void drive() {
System.out.println("車啓動!");
move.drive();
System.out.println("車停止!");
}
}
可能會有點繞,這裏寫個測試類來理一理思路,測試類代碼:
public class TestCar {
public static void main(String args[]){
Car car = new Car();
//若我們想先開始計算時間再啓動停止
// TimeCarProxy t = new TimeCarProxy(car);
// ActionCarProxy a = new ActionCarProxy(t);
// a.drive();
//若我們想先啓動停止再計算時間
ActionCarProxy a = new ActionCarProxy(car);
TimeCarProxy t = new TimeCarProxy(a);
t.drive();
}
}
運行效果:
1. 先計算時間再啓動
2. 先啓動再計算時間
以上就實現了一個靜態代理。從上面的效果可以看出實現了預期的效果,我們可以任意的指定誰先代理誰後代理,可以看作是橫向擴展了。
到這裏會有很多人有疑惑,這不是裝飾設計模式嗎?從實現語法的角度來講確實是很像裝飾,但是兩者的着重點不同,裝飾模式旨在擴展功能,這裏是以代理Car類去解決問題,語義,出發點是不同的,前面講到過設計模式的體會,這裏還需要讀者慢慢去體會。
動態代理
注意:這裏開始模仿JDK實現Proxy
這裏跟着上面的思路來,我們引入一個新的問題:
如過Car裏有多個方法要求計算運行時間怎麼處理?
這樣的話我們需要在TimeCarProxy 中的每個方法前後獲取當前時間並計算,那這樣的話我們會發現TimeCarProxy中出現了很多的重複代碼,當然我們可以給重複的代碼簡單封裝,當那也沒從根本上解決問題。
這個問題暫且擱置,先看下一個問題,這個時候請注意,請將重點放到TimeCarProxy 上來。
如果我們需要TimeCarProxy 不僅代理Car還能代理其它類對象
也就是一個萬能的TimeProxy代理,可以代理任意對象執行計算方法運行時間,這個時候我們需要怎麼辦?
首先我們需要一個代理對象:
//jdk中Proxy就是動態代理
public class Proxy {
//產生並返回一個代理對象
public static Object newProxyInstance(){
//我們需要在這裏動態的生成代理對象
return null;
}
}
那我們要怎麼動態生成對象呢?
- 我們首先要得到要有生成對象的代碼,但是代碼不能交給程序處理,所以我們要將代碼轉化成程序能處理的形式,那就是字符串。
- 用字符串表示代碼後我們就可以任意的構造出我們想要的代碼,讓後將字符串輸出到一個java文件中交給程序去編譯
- 那程序要怎麼編譯java文件呢?
JDK6爲我們提供了Complier API ,另外還有CGlib、ASM插件可以直接生成二進制文件不用再編譯了,Spring中也支持通過CGlib方式實現動態代理
現在暫時不管生成字符串的邏輯,我們先解決編譯的問題
代碼:
public class FileTest {
@Test
public void test() throws IOException {
//用來生成代理對象的代理類的字符串形式
String src="" +
"package net.hncu.test;\n" +
"public class TmpProxy implements Move {\n" +
" Move move ;\n" +
" public TmpProxy(Move move){\n" +
" this.move=move;\n" +
" }\n" +
" @Override\n" +
" public void drive() {\n" +
" long i = System.currentTimeMillis();\n" +
" System.out.println(\"開始時間:\" + i);\n" +
" move.drive();\n" +
" long j = System.currentTimeMillis();\n" +
" System.out.println(\"結束時間:\" + j);\n" +
" System.out.println(\"車開了:\"+(j-i)+\"millis\");\n" +
" }\n" +
"}";
//將字符串保存成一個java文件System.getProperty("user.dir")獲得項目路徑
String filename=System.getProperty("user.dir")+"/src/net/hncu/test/TmpProxy.java";
//新建文件
File file = new File(filename);
//將字符串寫到文件
FileWriter fileWriter =new FileWriter(file);
fileWriter.write(src);
fileWriter.close();
/**
* 編譯java文件這裏對編譯過程不做過多闡述,如果感興趣可以去查看api
*/
//獲取java編譯器jdk6支持
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//獲取一個file管理器(三個參數diagnosticListener監聽編譯過程的監聽器
// locale國際化相關,charset指定字符集)所有參數爲空時,指定默認配置
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null);
//根據文件名字拿到java文件對象(可以填多個文件,獲得多個對象)返回一個文件對象的迭代器
Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(filename);
//建立一次編譯任務(參數:out輸出位置,fileManager文件管理器,diagnosticListener監聽器,
// options編譯時的參數,暫不填,classes編譯時所需要的class文件,compilationUnits需要編譯的單元)
JavaCompiler.CompilationTask task = compiler.getTask(null,fileManager,null,null,null,units);
//執行編譯
task.call();
//關閉文件管理器
fileManager.close();
}
}
效果圖:
從圖中我們可以生成了TmpProxy 的java文件和class文件
接下來我們又要考慮下一個問題了。
我們需要把我們生成的class文件加載到內存來生成一個代理對象
這裏只貼部分代碼:
//加載class 文件到內存,
//直接從指定URL位置加載class文件到內存,其實我們也可以直接將class存到bin目錄下,但是可能會造成衝突
// 首先我們需要一個URL數組指定加載class文件的路徑,
URL[] urls = new URL[]{new URL("file:/"+System.getProperty("user.dir")+"/src")};
//新建一個URL類加載器
URLClassLoader classLoader = new URLClassLoader(urls);
//加載路徑下的指定class文件
Class aClass = null;
try {
aClass = classLoader.loadClass("net.hncu.test.TmpProxy");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//System.out.println("aClass: "+aClass+" from: "+"FileTest.test");
//利用反射操作class對象
//構造一個實例
Constructor constructor = aClass.getConstructor(inter);
return constructor.newInstance(object);
好了,到此爲止,我們達到了動態生成代理對象的目的了,但是我們會發現還是隻能動態生成TimeProxy,但是你別忘了,TimeProxy是由字符串生成的,而我們動態修改字符串是不是容易多了。接下來我們要做的就是修改字符串。
問題來了,我們需要如何修改字符串讓其動態生成我們需要的代碼呢?
首先我們需要生成任意對象的代理類,我們需要告訴它我們要生成代理類的規範,即被代理類的接口
"public class TmpProxy implements "+inter.getName()+" {\n" + " public TmpProxy("+handler.getClass().getName()+" tmp){\n" + " this.tmp=tmp;\n" + " }\n" +
- 然後我們需要得到接口裏的方法對代理類中的方法進行重新編排
//根據接口的class文件動態實現多個方法的代理字符串拼接
String methodStr="";
Method[] methods = inter.getMethods();
for (Method method : methods) {
methodStr+=
" @Override\n" +
//這裏方法名要改成當前的方法名
" public void "+method.getName()+"() {\n" +
" try{\n"+
" Method method = "+inter.getName()+".class.getMethod(\""+method.getName()+"\");\n"+
" tmp.invoke(this,method);\n" +
" }catch(Exception e){\n"+
" e.printStackTrace();\n"+
" }\n"+
" }\n";
- 修改好後我們可以動態的生成任何對象的代理對象,只是生成的代理對象固定的只能統計運行時間業務,所以我們還需要一個處理業務的邏輯。
那麼我們需要怎樣來修改業務邏輯呢?
因爲業務邏輯是需要用戶自己來定義的,所以不能寫死在字符串中,當是業務邏輯需要有一定的編寫規範,所以最好的選擇就是通過一個接口來規範業務邏輯處理,讓後讓用戶來實現接口定義自己的業務邏輯。
public interface ProxyHandler {
//在自定義方法模塊時我們肯定要執行被代理類本身的方法,
//所以我們至少需要以下兩個參數
public void invoke(Object object,Method m);
}
分析:該接口定義了目標類方法的實現規則,所以我們在實現該接口的時候需要告知它目標類。
實現代碼例子:
public class TimeHandler implements ProxyHandler {
Object target;
public TimeHandler(Object target){
this.target=target;
}
@Override
public void invoke(Object object,Method m) {
long start = System.currentTimeMillis();
System.out.println("開始時間:"+start);
try {
m.invoke(target);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
long end= System.currentTimeMillis();
System.out.println("結束時間:"+end);
System.out.println("用時:"+(end-start));
}
}
注意點:Proxy中的invoke實際上是調用的是handler中的invoke
如此一來,就可以實現處理任何的業務邏輯了,同時簡單的代理模式也實現了。
結論
目前這個只是簡單的實現,只能做沒有參數列表、返回值的,後續有時間再去完善
源碼
注意:
在第一遍運行時會出現ClassNoFound異常,那是因爲編譯的class文件並沒有馬上寫到目錄下,重新運行就可以出來結果了,至於如何改這個bug我還沒研究出來。
Car:
package net.hncu.test;
import org.junit.Test;
/**
* Project: String1
* Desc: proxy test
* Author: AMX50B
* Date: 2017-10-20 19:08
*/
public class Car implements Move {
/**
* 解決這個問題我們需要在drive運行開始和結束的位置加入系統當前時間
* 再得到兩個時間差
*/
@Test
public void drive(){
System.out.println("開車了...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void flay() {
System.out.println("起飛了...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Move:
package net.hncu.test;
/**
* Created by AMX50B on 2017/10/20
*/
public interface Move {
public void drive();
public void flay();
}
Proxy:
package net.hncu.test;
import org.junit.Test;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
/**
* Project: String1
* Desc: proxy
* Author: AMX50B
* Date: 2017-10-21 12:51
*/
public class Proxy {
//產生並返回一個代理對象
@Test
public static Object newProxyInstance(Class inter,ProxyHandler handler) throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//用來生成代理對象的代理類的字符串形式
//根據接口的class文件動態實現多個方法的代理字符串拼接
String methodStr="";
Method[] methods = inter.getMethods();
for (Method method : methods) {
methodStr+=
" @Override\n" +
//這裏方法名要改成當前的方法名
" public void "+method.getName()+"() {\n" +
" try{\n"+
" Method method = "+inter.getName()+".class.getMethod(\""+method.getName()+"\");\n"+
" tmp.invoke(this,method);\n" +
" }catch(Exception e){\n"+
" e.printStackTrace();\n"+
" }\n"+
" }\n";
}
String src="" +
"package net.hncu.test;\n" +
"import java.lang.reflect.Method;\n"+
//傳入接口名稱,使代理類能動態代理任意我們指定的接口
"public class TmpProxy implements "+inter.getName()+" {\n" +
" "+handler.getClass().getName()+" tmp ;\n" +
" public TmpProxy("+handler.getClass().getName()+" tmp){\n" +
" this.tmp=tmp;\n" +
" }\n" +
methodStr +
"}\n";
//將字符串保存成一個java文件System.getProperty("user.dir")獲得項目路徑
String filename=System.getProperty("user.dir")+"/src/net/hncu/test/TmpProxy.java";
//新建文件
File file = new File(filename);
//將字符串寫到文件
FileWriter fileWriter =new FileWriter(file);
fileWriter.write(src);
fileWriter.close();
/**
* 編譯java文件這裏對編譯過程不做過多闡述,如果感興趣可以去查看api
*/
//獲取java編譯器jdk6支持
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//獲取一個file管理器(三個參數diagnosticListener監聽編譯過程的監聽器
// locale國際化相關,charset指定字符集)所有參數爲空時,指定默認配置
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null);
//根據文件名字拿到java文件對象(可以填多個文件,獲得多個對象)返回一個文件對象的迭代器
Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(filename);
//建立一次編譯任務(參數:out輸出位置,fileManager文件管理器,diagnosticListener監聽器,
// options編譯時的參數,暫不填,classes編譯時所需要的class文件,compilationUnits需要編譯的單元)
JavaCompiler.CompilationTask task = compiler.getTask(null,fileManager,null,null,null,units);
//執行編譯
task.call();
//關閉文件管理器
fileManager.close();
//加載class 文件到內存,
//直接從指定URL位置加載class文件到內存,其實我們也可以直接將class存到bin目錄下,但是可能會造成衝突
// 首先我們需要一個URL數組指定加載class文件的路徑,
URL[] urls = new URL[]{new URL("file:/"+System.getProperty("user.dir")+"/src")};
//新建一個URL類加載器
URLClassLoader classLoader = new URLClassLoader(urls);
//加載路徑下的指定class文件
Class aClass = null;
try {
aClass = classLoader.loadClass("net.hncu.test.TmpProxy");
} catch (ClassNotFoundException e) {
try {
Thread.sleep(1000);
aClass= classLoader.loadClass("net.hncu.test.TmpProxy");
} catch (InterruptedException e1) {
e1.printStackTrace();
} catch (ClassNotFoundException e1) {
e1.printStackTrace();
}
}
//System.out.println("aClass: "+aClass+" from: "+"FileTest.test");
//利用反射操作class對象
//構造一個實例
Constructor constructor = aClass.getConstructor(handler.getClass());
return constructor.newInstance(handler);
// move.drive();
}
}
ProxyHandler:
package net.hncu.test;
import java.lang.reflect.Method;
/**
* Created by AMX50B on 2017/10/23
*/
public interface ProxyHandler {
//在自定義方法模塊時我們肯定要執行被代理類本身的方法,
//所以我們至少需要以下兩個參數
public void invoke(Object object,Method m);
}
TimeProxy:
package net.hncu.test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Project: String1
* Desc: time handler
* Author: AMX50B
* Date: 2017-10-23 18:40
*/
public class TimeHandler implements ProxyHandler {
Object target;
public TimeHandler(Object target){
this.target=target;
}
@Override
public void invoke(Object object,Method m) {
long start = System.currentTimeMillis();
System.out.println("開始時間:"+start);
try {
m.invoke(target);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
long end= System.currentTimeMillis();
System.out.println("結束時間:"+end);
System.out.println("用時:"+(end-start));
}
}
Test:
package net.hncu.test;
/**
* Project: String1
* Desc: test car
* Author: AMX50B
* Date: 2017-10-21 9:58
*/
public class TestCar {
public static void main(String args[]){
Car car = new Car();
//若我們想先開始計算時間再啓動停止
// TimeCarProxy t = new TimeCarProxy(car);
// ActionCarProxy a = new ActionCarProxy(t);
// a.drive();
//若我們想先啓動停止再計算時間
// ActionCarProxy a = new ActionCarProxy(car);
// TimeCarProxy t = new TimeCarProxy(a);
// t.drive();
try {
ProxyHandler proxyHandler = new TimeHandler(car);
Move m = (Move) Proxy.newProxyInstance(Move.class,proxyHandler);
m.drive();
m.flay();
} catch (Exception e) {
e.printStackTrace();
}
}
}