文章目錄
一、引入今天的主題
今天準備寫代理模式的時候,苦思要找什麼例子,就搜了下世界名牌口紅的企業——YSL(聖羅蘭),就問下了女朋友,知道這個嘛。上圖的回答,簡直讓我懷疑找了個假女朋友(😂)。
搜YSL也是看見微商在朋友圈發的廣告,不知道大家有沒有發現,微商簡直就是代理模式的完美例子,畫個問號?,向下看👇
二、正文開始——代理模式
是什麼
- 代理模式是給某一個對象提供一個代理對象,並且代理對象持有原對象的引用
- 在不更改原對象源碼的情況下對原對象的方法進行修改和加強,符合開閉原則
- 屬於對象的結構型模式
看不太懂?,沒有關係,下面講例子
舉上面的例子——微商
- 原對象(真實對象):YSL官方商店,買YSL的產品(原對象方法)
- 代理對象:微商,代理YSL官方商店買YSL的產品(原對象方法),爲了提高競爭力,並送一些小禮物(方法修改和加強)
三、代理模式分類
- 靜態代理:指在編譯階段,代理類由程序員寫好,在程序運行時直接獲取代理對象的源碼進行編譯
- 動態代理:編譯階段程序員不寫代理類,而是在程序運行時,根據用戶定義的增加規則來動態生成原對象的代理對象,(不用想,肯定用到了多態)
動態代理分爲面向接口的jdk動態代理和Cglib動態代理(暫不做討論,Mybatis中使用的是jdk動態代理)。
3.1靜態代理
3.1.1實現靜態代理兩個要求
- 1.原對象和代理對象實現同一個接口
- 2.代理對象持有原對象的引用,並在方法中對原對象的方法進行增強
如:
- 原對象:YSL的官方商店
- 代理對象:微商,持有YSL的官方商店的引用
- 實現同一個接口:賣產品
3.1.2代碼實現
/**
* @Author Think-Coder
* @Data 2020/5/14 10:55
* @Version 1.0
*/
//定義一個賣化妝品的接口
public interface MakeUpSeller {
//銷售的方法
//name爲化妝品名字,price是價格
void sell(String name,double price);
}
//原對象—————YSL官方商店
public class YSLSeller implements MakeUpSeller {
@Override
public void sell(String name, double price) {
System.out.println("感謝購買"+name+",一共是"+price+"元");
}
}
//代理對象————微商代理YSL官方商店
public class WeiShangProxy implements MakeUpSeller {
//持有YSL官方商店的引用
private YSLSeller yslSeller;
public WeiShangProxy(YSLSeller yslSeller) {
this.yslSeller = yslSeller;
}
//實現接口的sell方法,並增強原對象YSL官方商店的方法
//增強原對象的方法:兩個輸出方法
@Override
public void sell(String name, double price) {
System.out.println("我要發朋友圈,介紹商品優勢");
//YSL官方商店對象調用賣產品的接口
yslSeller.sell(name,price);
System.out.println("並送您一瓶卸妝水,歡迎下次再來");
}
}
測試類ProxyTest
public class ProxyTest {
public static void main(String[] args) {
//將new的YSLSeller官方商店原對象傳入微商代理對象
//微商代理對象實現了客戶對YSL官方商店的訪問控制
WeiShangProxy weiShangProxy = new WeiShangProxy(new YSLSeller());
//微商代理對象調用賣產品方法
weiShangProxy.sell("YSL口紅",1000);
}
}
看下面的結果是不是很暖心
我要發朋友圈,介紹商品優勢
感謝購買YSL口紅,一共是1000.0元
並送您一瓶卸妝水,歡迎下次再來
Process finished with exit code 0
用類圖做個總結:
在測試類中最重要的就是將new YSLSeller()對象放入WeiShangProxy構造函數中
也就是說客戶直接訪問了微商代理類,從而微商代理控制了客戶對YSL官方商店的訪問
靜態代理缺點:
靜態代理是面向實現編程(YSLSeller實現了MakeUpSeller接口)而不是面向接口編程,就把程序寫死了,不利於程序的擴展,即如果原對象增加或刪除方法,代理對象也會跟着改變,極大提高代碼維護成本
於是就有了JDK動態代理
3.2jdk動態代理
3.2.1定義
- 在程序運行時,根據用戶的定義規則,動態生成原對象的代理對象,
- 用上邊的例子解釋就是,不寫微商代理類,而是在程序運行時利用Proxy類及InvocationHandler接口等動態生成代理類及代理實例。
3.2.2jdk動態代理的兩個核心方法
- Proxy類的newProxyInstance方法:生成原對象的代理對象
- InvocationHandler接口的invoke方法:包裝原對象的方法,並增強
Proxy類的newProxyInstance方法
生成代理對象
/**
* 參數1:ClassLoader loader,原對象的類加載器
* 參數2:Class<?>[] interfaces,原對象繼承(實現)的類和接口Class類數組
* 參數3:InvocationHandler h,用戶自定義增強原對象的方法接口
**/
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
//上面省略
/*
* Look up or generate the designated proxy class.
* 查找或生成指定的代理類
*/
Class<?> cl = getProxyClass0(loader, intfs);
//下面省略
}
InvocationHandler接口的invoke方法
用戶自定義的規則接口需要實現此接口,invoke方法用於增加原代理對象方法
public interface InvocationHandler {
/**
* 參數1:Object proxy,代理對象
* 參數2:Method method,原對象方法對應的反射類,method.invoke反射調用原對象方法
* 參數3:Object[] args,傳入方法參數
**/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
3.2.3拿上面的例子舉例
微商代理類已經不需要了,可以動態生成
MakeUpSeller接口及YSLSeller官方商店類不發生變化
加入MakeUpSellerHandler類實現InvocationHandler接口,用於增強原對象方法
完整代碼如下
/**
* @Author Think-Coder
* @Data 2020/5/14 10:55
* @Version 1.0
*/
//定義一個賣化妝品的接口
public interface MakeUpSeller {
//銷售的方法
//name爲化妝品名字,price是價格
void sell(String name,double price);
}
//原對象—————YSL官方商店
public class YSLSeller implements MakeUpSeller {
@Override
public void sell(String name, double price) {
System.out.println("感謝購買"+name+",一共是"+price+"元");
}
}
//實現InvocationHandler接口
public class MakeUpSellerHandler implements InvocationHandler {
//持有原對象的父類的引用,父類引用指向子類對象,多態的體現
private MakeUpSeller makeUpSeller;
public MakeUpSellerHandler(MakeUpSeller makeUpSeller) {
this.makeUpSeller = makeUpSeller;
}
@Override
//增強原對象的方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("我要發朋友圈,介紹商品優勢");
//反射調用原對象的方法
method.invoke(makeUpSeller,args);
System.out.println("並送您一瓶卸妝水,歡迎下次再來");
return null;
}
}
看下測試類
public class ProxyTest {
public static void main(String[] args) {
/**
* 參數1:MakeUpSeller.class.getClassLoader(),MakeUpSeller的類加載器
* 參數2:new Class[]{MakeUpSeller.class},MakeUpSeller繼承(實現)的類和接口Class數組
* 參數3:new MakeUpSellerHandler(new YSLSeller()),用戶自定義增強原對象的方法接口
**/
MakeUpSeller yslProxy = (MakeUpSeller) Proxy.newProxyInstance(MakeUpSeller.class.getClassLoader(),
new Class[]{MakeUpSeller.class},
new MakeUpSellerHandler(new YSLSeller()));
yslProxy.sell("YSL口紅",1000);
}
}
看測試結果
我要發朋友圈,介紹商品優勢
感謝購買YSL口紅,一共是1000.0元
並送您一瓶卸妝水,歡迎下次再來
Process finished with exit code 0
至此動態代理就實現了
不過,還有兩個疑問沒有解決
- 1.爲什麼Proxy.newProxyInstance方法生成的代理對象可以強轉成MakeUpSeller接口類型?
- 2.爲什麼代理對象調用sell方法,會調用MakeUpSellerHandler的invoke方法?
帶着這兩個疑問,咱們反編譯下生成動態代理類
編譯是.java文件編譯爲.class文件,反編譯爲.class文件變爲.java文件的過程
反編譯生成動態代理類
改下測試類代碼
public static void main(String[] args) throws IOException {
MakeUpSeller yslProxy = (MakeUpSeller) Proxy.newProxyInstance(MakeUpSeller.class.getClassLoader(),new Class[]{MakeUpSeller.class},
new MakeUpSellerHandler(new YSLSeller()));
yslProxy.sell("YSL口紅",1000);
createProxyClass();
}
public static void createProxyClass() throws IOException {
byte[] bytes = ProxyGenerator.generateProxyClass("MakeUpSeller$proxy", new Class[]{MakeUpSeller.class});
Files.write(new File("D:\\ITProject\\javaproj\\selfproj\\ProxyTest\\out\\production\\ProxyTest\\MakeUpSeller$proxy.class").toPath(),bytes);
}
生成的文件如下
代碼如下,做了部分省略,
//繼承Proxy代理類,實現了MakeUpSeller接口
//這個就可以回答第一個問題,可以轉成MakeUpSeller類型
public final class MakeUpSeller$proxy extends Proxy implements MakeUpSeller {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public MakeUpSeller$proxy(InvocationHandler var1) throws {
super(var1);
}
//實現MakeUpSeller接口sell類
public final void sell(String var1, double var2) throws {
try {
//這行代碼很重要,回答了第二個問題
//該類繼承proxy類,h便爲InvocationHandler接口,因此可以調用invoke方法
//而MakeUpSellerHandler實現了InvocationHandler接口,因此直接調用了
//MakeUpSellerHandler類中invoke方法
super.h.invoke(this, m3, new Object[]{var1, var2});
} catch (RuntimeException | Error var5) {
throw var5;
} catch (Throwable var6) {
throw new UndeclaredThrowableException(var6);
}
}
}
如此就可以解釋上面的兩個問題了
最後也用類圖總結一下
main方法用代理對象調用sell方法時,其實是動態生成的MakeUpSeller$proxy類實例調用的sell方法
根據上面反編譯類中sell方法中,調用的是MakeUpSellerHandler接口中invoke方法,invoke方法中包裝了原對象YSLSeller的sell方法,最後實現了動態代理。
接下來看jdk動態代理在Mybatis中的應用,終於到了
四、動態代理在MyBatis中的應用
4.1手寫的MyBtatis框架的測試類
public static void main(String[] args) throws IOException {
//1.讀取配置文件,連接數據庫
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.創建SqlSessionFactory工廠
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
//3.使用工廠生產SqlSession對象,用於操作數據庫
SqlSession session = factory.openSession();
//4.使用SqlSession創建Dao接口的代理對象,因爲IUserDao接口沒有實現類
IUserDao userDao = session.getMapper(IUserDao.class);
//5.使用代理對象執行方法
List<User> users = userDao.findAll();
for (User user:users){
System.out.println(user);
}
//6.釋放資源
session.close();
in.close();
}
在短短的測試類中就使用了三個設計模式,確實對初學者不太友好,所以一點一點拆開來看未免不是一個好的學習習慣,所以今天主要看兩行代碼
//4.使用SqlSession創建Dao接口的代理對象,因爲IUserDao接口沒有實現類
IUserDao userDao = session.getMapper(IUserDao.class);
//5.使用代理對象執行方法
List<User> users = userDao.findAll();
看完上面的動態代理,再看這兩行代碼就能解開初學Mybatis時候的疑惑,
爲什麼只有Dao層接口,沒有Dao層的接口實現類就可以操作數據庫?
就是用到了jdk的動態代理生成了Dao層接口的代理對象userDao
下面從源碼分析一下,Mybatis底層是怎麼創建Dao層接口的代理對象的
4.2MapperProxyFactory類創建Dao層接口代理對象
也就是研究下面的代碼
IUserDao userDao = session.getMapper(IUserDao.class);
當調用幾個類的getMapper方法後,會調用下面類第1個newInstance方法
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap();
//通過構造函數傳入IUerDao接口Class對象
//學過反射的童鞋應該知道,拿到Class對象,相當於拿到IUserDao類
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return this.mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return this.methodCache;
}
//先調用此方法
public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
//調用下面newInstance方法
return this.newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
/**
* 有沒有很熟悉!
* mapperInterface就是Dao層接口 IUserDao
* 參數1:this.mapperInterface.getClassLoader(),IUserDao的類加載器
* 參數2:new Class[]{this.mapperInterface},IUserDao繼承(實現)的類和接口Class數組
* 參數3:mapperProxy,上邊的newInstace方法返回的,實現了InvocationHandler接口,用於方法增強
**/
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
}
上面的代碼,寫註釋的地方是重點
MapperProxyFactory類就是創建代理對象的工廠類,自定義Dao層接口傳入構造函數,通過newInstance方法返回自定義Dao層接口的代理對象
4.3使用代理對象執行findAll方法
List<User> users = userDao.findAll();
看到代碼不得不提出兩個問題
- 1.代理對象userDao是如何執行findAll()方法的
- 2.findAll方法是如何找到對應的sql語句進行增刪改查的
4.3.1首先看MapperProxy類
- 該類實現InvocationHandler接口,重寫的invoke方法包裝了原對象IUserDao接口中findAll方法
- 也就是說,當執行userDao.findAll();時,會調用該類的invoke方法
invoke方法作用:生成findAll方法對應的MapperMethod類實例,MapperMethod類是最重要的,在下面
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final Method privateLookupInMethod;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//上面省略
//下面兩行代碼很重要
//method爲Dao層自定義接口方法
//調用下面的cachedMapperMethod找到與要執行的Dao層接口方法對應的MapperMethod
MapperMethod mapperMethod = this.cachedMapperMethod(method);
//調用execute方法來執行findAll方法
//先把sqlSession傳入到MapperMethod內部
//在MapperMethod內部將要執行的方法名和參數再傳入sqlSession對應方法中去執行
return mapperMethod.execute(this.sqlSession, args);
}
//根據的傳入IUserDao接口自定義方法findAll,生成對應的MapperMethod類實例
private MapperMethod cachedMapperMethod(Method method) {
return (MapperMethod)this.methodCache.computeIfAbsent(method, (k) -> {
return new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());
});
}
}
4.3.2再看MapperMethod類
該類的兩個作用
- 1.解析接口自定義的findAll方法
- 2.並找到執行對應的sql語句的方法
先看是如何解析的
public class MapperMethod {
//SqlCommand內部類解析自定義接口方法的方法名稱和SQL語句類型,
private final MapperMethod.SqlCommand command;
//MethodSignature內部類解析接口方法的簽名,即接口方法和參數名稱和參數值映射關係,如String a="0"
private final MapperMethod.MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);
}
}
那麼問題來了,該類是如何找到findAll方法對應的sql語句呢?
答案就是Configuration對象,通過MapperMethod構造函數傳進來的
如圖所示Configuration中的mapperedStatements字段中的MapperedStatement對象是一個Map類型
key爲findAll方法,value中包含sql語句,可以通過方法名findAll找到對應的sql語句(這個就是上面第二個問題的答案)
再看execute方法爲findAll方法找到的sql語句類型匹配方法
execute方法源碼
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
//根據SqlCommand解析出來的sql語句類型,爲增刪改查類型匹配方法
switch(this.command.getType()) {
case INSERT:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case DELETE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case SELECT:
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}
根據sql語句類型匹配對應的方法後,其實是調用SqlSession接口的實現類執行sql語句
如根據查找到executeForMany方法
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
Object param = this.method.convertArgsToSqlCommandParam(args);
List result;
if (this.method.hasRowBounds()) {
RowBounds rowBounds = this.method.extractRowBounds(args);
//最後執行sqlSession接口中的selectList方法
result = sqlSession.selectList(this.command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(this.command.getName(), param);
}
if (!this.method.getReturnType().isAssignableFrom(result.getClass())) {
return this.method.getReturnType().isArray() ? this.convertToArray(result) : this.convertToDeclaredCollection(sqlSession.getConfiguration(), result);
} else {
return result;
}
}
SqlSession接口
public interface SqlSession extends Closeable {
/**
* var1:Dao層自定義接口的方法名稱,即findAll()
* var2:方法的參數
* var3:用於分頁查詢
**/
<T> T selectOne(String var1);
<T> T selectOne(String var1, Object var2);
<E> List<E> selectList(String var1, Object var2, RowBounds var3);
....
}
最後交給SqlSession實現類DefaultSqlSession去執行findAll方法對應sql語句,並返回結果
這個和我們直接用SqlSession對象調用DefaultSqlSession的實現類的方法是一樣的,轉了一圈回來,就完成了動態代理
五、圖總結
當代理對象userDao調用findAll()執行的代碼流程