一步一步搞懂MyBatis中設計模式源碼——代理模式

一、引入今天的主題

在這裏插入圖片描述
今天準備寫代理模式的時候,苦思要找什麼例子,就搜了下世界名牌口紅的企業——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()執行的代碼流程
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章