P7程序员教你用520的机会搞懂MyBatis中设计模式源码——代理模式

一、引入今天的主题

今天准备写代理模式的时候,苦思要找什么例子,刚好今天520就搜了下世界名牌口红的企业——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 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 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 {

private final Class mapperInterface;

private final Map methodCache = new ConcurrentHashMap();

//通过构造函数传入IUerDao接口Class对象

//学过反射的童鞋应该知道,拿到Class对象,相当于拿到IUserDao类

public MapperProxyFactory(Class mapperInterface) {

this.mapperInterface = mapperInterface;

}

public Class getMapperInterface() {

return this.mapperInterface;

}

public Map getMethodCache() {

return this.methodCache;

}

//先调用此方法

public T newInstance(SqlSession sqlSession) {

MapperProxy mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);

//调用下面newInstance方法

return this.newInstance(mapperProxy);

}

protected T newInstance(MapperProxy 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 implements InvocationHandler, Serializable {

private static final Method privateLookupInMethod;

private final SqlSession sqlSession;

private final Class mapperInterface;

private final Map 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 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 selectOne(String var1);

T selectOne(String var1, Object var2);

List selectList(String var1, Object var2, RowBounds var3);

....

}

最后交给SqlSession实现类DefaultSqlSession去执行findAll方法对应sql语句,并返回结果

这个和我们直接用SqlSession对象调用DefaultSqlSession的实现类的方法是一样的,转了一圈回来,就完成了动态代理

五、图总结

当代理对象userDao调用findAll()执行的代码流程

原文链接:https://blog.csdn.net/shang_0122/article/details/106105717

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345