(一)AOP 原理解析
衆生周知,AOP實現原理是基於動態代理。
什麼是代理?
增強一個類,或者一個對象的功能。就可以說是代理。
如:買火車票? app 12306 就是一個代理,他代理了火車站。
java實現代理的兩種方式:
代理的名詞:
代理對象
:增強後的對象
目標對象
:被增強的對象
他們的身份不是絕對的,會根據情況發生變化
1.1靜態代理
繼承
代理對象繼承目標對象,重寫需要增強的方法。完成靜態代理!調用即可。。面向接口編程就是靜態代理
繼承方式代理會有一個問題。每次增強都需要繼承。。如果需要多次(如下聚合)
增強,會產生很多代理類。相對複雜。
聚合
目標對象,和代理對象實現同一個接口。並且代理對象包含 目標對象;
核心示例代碼:
傳入目標對象,對目標對象增強!切換不同的類,就可以對不同的類完成代理。
//代理對象1
public class UserDaoLog implements UserDao {
//裝飾者模式,傳入對象
private UserDao dao;
public UserDaoLog(UserDao dao){
this.dao=dao;
}
/**
* 對目標對象做增強處理
*/
@Override
public void query() {
System.out.println("打印日誌。");
dao.query();
}
//代理對象2
public class UserDaoTime implements UserDao {
//裝飾者模式,傳入對象
private UserDao dao;
public UserDaoTime(UserDao dao){
this.dao=dao;
}
/**
* 對目標對象做增強處理
*/
@Override
public void query() {
System.out.println("打印時間。");
dao.query();
}
}
//傳入目標對象完成代理,要代理其他類,只需要切換對象。
如果老闆說,既要完成打印時間,又要完成打印日誌該怎麼辦呢?
public static void main(String[] args) {
UserDao dao = new UserDaoImpl();
// UserDao prox = new UserDaoTime(dao);
UserDao prox = new UserDaoLog(dao);
prox.query();
}
思考:如果我們要完成多重代理呢?改怎麼辦?(既要完成打印時間,又要完成打印日誌)
1.1.1 多重代理(多次增強!)
對多個代理對象,代理。。完成雙重代理。(如下代碼片段,只需要傳入 UserDaoLog+UserDaoImpl)
然後再對他們做代理。。繼承的方式 就會產生很多類。聚合實現起來就更加簡單。
public static void main(String[] args) {
UserDao traget = new UserDaoLog(new UserDaoImpl());
UserDao prox = new UserDaoTime(traget);
prox.query();
}
---------------------控制檯輸出------------------------
打印時間。
打印日誌。
假裝查詢數據庫
假設這個時候,老闆要求先打印日誌,再打印時間呢?
public static void main(String[] args) {
UserDao traget = new UserDaoTime(new UserDaoImpl());
UserDao prox = new UserDaoLog(traget);
prox.query();
}
---------------------控制檯輸出------------------------
打印日誌。
打印時間。
假裝查詢數據庫
缺點:如上,我們只是對一個類(userDao)
做代理,如果對多個類做代理呢?就算用聚合的方式一樣會產生很多類。只不過比繼承少一點,如上每個類做一次雙重代理,就需要兩個代理類。這個問題該怎麼解決呢?
總結:如果在不確定的情況下,儘量不要使用靜態代理。因爲一但寫代碼就會產生類。一但產生類,就會寫到爆炸。
那什麼場景會用到靜態代理呢?JDK中的IO流,BufferIO流,其實都是使用的裝飾者模式。就是靜態代理的方式。
1.2 手寫動態代理
面試官:什麼是動態代理?
初中級開發:基於java反射,增強啊,調用者和實現者之間解耦啊。張口就來了
網上博客:1:抽象類接口,2:被代理類(具體實現抽象接口的類),3:動態代理類:實際調用被代理類的方法和屬性的類。InvocationHandler 方法啊等等等,都是隻懂皮毛
思考:先拋開以上的所有想法,如果我們不想創建任何類的情況下要增強一個類,該怎麼辦?
我們是不是得先有一個類?去繼承或者實現目標對象?才能去反射?可是我們連類都沒有,拿什麼去反射?所以反射只是動態代理的冰山一角。那麼現在我們不能在項目中去創建一個類,該怎麼辦?可以把這個類定義在代碼中嗎?不管可不可以我們先來試一下
。
如下:假設我們要對UserDaoImpl 做代理。如下圖所示,我們已經得到了要代理UserDaoImpl的所有內容
public class UserDaoImpl implements UserDao{
public void query(){
System.out.println("假裝查詢數據庫");
}
public String query(String aa){
return aa;
}
}
public static void main(String[] args) {
//獲取UserDaoImpl實現的接口 我們這是山寨版的動態代理,暫時只處理第一個接口
Class targetInf = new UserDaoImpl().getClass().getInterfaces()[0];
//獲取到接口的方法
Method methods[] = targetInf.getDeclaredMethods();
//定義換行
String line = "\n";
//定義tab空格
String tab = "\t";
//獲取到實現的接口名字 這裏就是UserDao
String infName = targetInf.getSimpleName();
String content = "";
//我們要寫一個類的類容 第一行是不是就是外面的包名?這裏就寫的叼一點google
String packageContent = "package com.google;" + line;
//第二行 是不是我們要導包?類.getName是不是就得到了類的全路徑 com.luban.dao.UserDao
String importContent = "import " + targetInf.getName() + ";" + line;
//創建一個類,類名爲$Proxy 實現了接口UserDao
String clazzFirstLineContent = "public class $Proxy implements " + infName + "{" + line;
//定義私有變量 private UserDao target;
String filedContent = tab + "private " + infName + " target;" + line;
//定義構造方法 傳入 UserDao target 並且給私有成員變量賦值
String constructorContent = tab + "public $Proxy (" + infName + " target){" + line
+ tab + tab + "this.target =target;"
+ line + tab + "}" + line;
String methodContent = "";
//遍歷接口的所有方法
for (Method method : methods) {
//獲取到方法的返回類型
String returnTypeName = method.getReturnType().getSimpleName();
//獲取到方法名稱
String methodName = method.getName();
// 獲取到方法的入參類型,如Sting.class String.class
Class argse[] = method.getParameterTypes();
String argsContent = "";
String paramsContent = "";
int flag = 0;
for (Class arg : argse) {
//遍歷入參類型數組,獲取到入參的對象名稱 如 String
String temp = arg.getSimpleName();
System.out.println(temp);
//拼裝參數 如 ,String p0,Sting p1,
argsContent += temp + " p" + flag + ",";
//拼裝形參(需要調用父類方法用到) 如 , p0, p1,
paramsContent += "p" + flag + ",";
flag++;
}
//截取掉參數的最後一個逗號
if (argsContent.length() > 0) {
argsContent = argsContent.substring(0, argsContent.lastIndexOf(",") - 1);
paramsContent = paramsContent.substring(0, paramsContent.lastIndexOf(",") - 1);
}
//我們有了 返回類型,有了方法名,有了入參,還有了調用父類的方法。就可以拼裝 實現的方法
methodContent += tab + "public " + returnTypeName + " " + methodName + "(" + argsContent + ") {" + line;
//判斷返回值不等於void return
if (!returnTypeName.equals("void")) {
methodContent+=tab + tab + "System.out.println(\"帶參數測試增強打印log\");" + line
+ tab + tab + "return target." + methodName + "(" + paramsContent + ");" + line
+ tab + "}" + line;
}else {
methodContent+= tab + tab + "System.out.println(\"測試假裝增強打印log\");" + line
+tab + tab + "target." + methodName + "(" + paramsContent + ");" + line
+ tab + "}" + line;
}
}
//最後把全部加起來。就是外面一個類裏面的內容
content = packageContent + importContent + clazzFirstLineContent + filedContent + constructorContent + methodContent + "}";
System.out.println(content);
}
---------------- ---------------------控制檯輸出-------------------------------------------------
package com.google;
import com.luban.dao.UserDao;
public class $Proxy implements UserDao{
private UserDao target;
public $Proxy (UserDao target){
this.target =target;
}
public void query() {
System.out.println("測試假裝增強打印log");
target.query();
}
public String query(String p) {
System.out.println("測試假裝增強打印log");
return target.query(p);
}
}
如上圖,我們已經得到了要代理UserDaoImpl代碼內容,得到了代碼內容我們是不是還得將內容產生一個.java文件?是不是還得編譯.java文件得到.class文件?是不是還得new 這個對象 才能完成代理?
思考一下,我們怎麼把內容變成.java文件呢?是不是可以用IO流的字符流?
1、代碼接上面部分。(伸手黨拷貝的同學注意了)
如下,我們把內容寫到G盤上產生.java文件。並且編譯它。
//代碼接上面部分+
File file1 =new File("G:\\com\\google");
File file =new File("G:\\com\\google\\$Proxy.java");
//判斷是否是一個目錄
if (!file1.isDirectory())
//創建目錄
file1.mkdirs();
if (!file.exists())
//創建文件
file.createNewFile();
//將字符串 寫出到磁盤
FileWriter fw = new FileWriter(file);
fw.write(content);
fw.flush();
fw.close();
//引用java編譯器。編譯java文件
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
//傳入要編譯的文件
Iterable units = fileMgr.getJavaFileObjects(file);
JavaCompiler.CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
t.call();
fileMgr.close();
在G盤上就產生了一個 $Proxy.java的文件與.class文件,內容如下。
2.接下來我們.java 文件也有了,.class文件也有了,我們怎麼得到這個對象呢?這些代碼都在我們的本地磁盤中,怎麼加載到我們的項目中呢?還記得我們的類加載器嘛?這裏就派上用場了。
1、代碼接上面部分。(伸手黨拷貝
的同學注意了)
//創建需要加載.class的地址
URL[] urls = new URL[]{new URL("file:G:\\\\")};
URLClassLoader urlClassLoader = new URLClassLoader(urls);
//加載class對象
Class clazz = urlClassLoader.loadClass("com.google.$Proxy");
//反射獲取到構造方法 構造方法帶參數,所以傳入接口對象
Constructor constructor = clazz.getConstructor(targetInf);
//構造方法傳入目標對象 就拿到了增強後的代理對象
UserDao proxy = (UserDao)constructor.newInstance(new UserDaoImpl());
System.out.println(proxy);
//代理對象調用無參方法
proxy.query();
//代理對象調用有參方法
System.out.println(proxy.query("哈哈哈哈哈哈哈哈測試成功"));
---------------- ---------------------控制檯輸出-------------------------------------------------
com.google.$Proxy@482f8f11
測試假裝增強打印log
假裝查詢數據庫
帶參數測試增強打印log
哈哈哈哈哈哈哈哈測試成功