搞懂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()执行的代码流程
在这里插入图片描述

  • 79
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 74
    评论
评论 74
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿成长轨迹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值