最近在看《通用源码阅读指导书:Mybatis源码详解》,这本书一一介绍了Mybatis中的各个包的功能,同时也涉及讲了一些阅读源码的技巧,还讲了一些源码中涉及的设计模式,这是本篇文章介绍的内容

在多说一点这本书,Mybatis是大部分Java开发者都熟悉的一个框架,通过这本书去学习如何阅读源码非常合适,引用书中的一句话:”通过功能猜测源码要比通过源码猜测功能简单得多“,所以在熟悉这个框架的情况下更容易阅读它的源码,通过这本书,可以看到Mybatis是如何使用反射、代理、异常、插件、缓存、配置、注解、设计模式等

1. 装饰器模式

通常的使用场景是在一个核心基本类的基础上,提供大量的装饰类,从而使核心基本类经过不同的装饰类修饰后获得不同的功能。

1.1 例子

装饰器最经典的例子还是JDK本身的InputStream相关类, InputStream通过不断被装饰,提供的功能越来越多

InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
//...
}

1.2 Mybatis实例

Mybatis的缓存功能大家应该都清楚,分为一级缓存和二级缓存,但讲到缓存,可能就要涉及到缓存大小、过期、是不是阻塞等这些操作,我们可以设计一个功能大而全的类,挑选自己想要的功能就行。但Mybatis不是这样设计的,它设计了多个装饰类,每个类负责一个功能,然后可以按需使用,分别维护。

这些装饰类如下:

  • BlockingCache,阻塞缓存,当根据key获取不到value时,会阻塞等待
  • FifoCache,先进先出缓存,会根据指定的大小淘汰缓存,按照FIFO的方式
  • LoggingCache,日志缓存,会记录缓存的使用情况,命中率等
  • LruCache,最近最少使用缓存,根据指定的大小淘汰缓存,按照LRU的方式
  • ScheduledCache,定时清理缓存
  • SerializedCache,序列化缓存,防止被取出来的value被修改
  • SoftCache,软引用缓存
  • SynchronizedCache,同步缓存,防止并发问题
  • TransactionalCache,事务缓存,在事务中查询语句要放到事务结束后执行,不如会读取事务中的一些脏数据
  • WeakCache,弱引用缓存

PerpetualCache是一个基础的缓存,其实就是一个HashMap

public class PerpetualCache implements Cache {

  // Cache的id,一般为所在的namespace
private final String id;
// 用来存储要缓存的信息
private Map<Object, Object> cache = new HashMap<>(); public PerpetualCache(String id) {
this.id = id;
}
...
}

LruCache是一个装饰器缓存,用来装饰传进来的缓存,可以是被装饰过的或者PerpetualCache

public class LruCache implements Cache {

  // 被装饰对象
private final Cache delegate;
// 使用LinkedHashMap保存的缓存数据的键
private Map<Object, Object> keyMap;
// 最近最少使用的数据的键
private Object eldestKey; /**
* LruCache构造方法
* @param delegate 被装饰对象
*/
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
...
}

在使用阶段,Mybatis根据配置,一层层的给cache进行装饰

  private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
// 设置缓存大小
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
// 如果定义了清理间隔,则使用定时清理装饰器装饰缓存
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
// 如果允许读写,则使用序列化装饰器装饰缓存
if (readWrite) {
cache = new SerializedCache(cache);
}
// 使用日志装饰器装饰缓存
cache = new LoggingCache(cache);
// 使用同步装饰器装饰缓存
cache = new SynchronizedCache(cache);
// 如果启用了阻塞功能,则使用阻塞装饰器装饰缓存
if (blocking) {
cache = new BlockingCache(cache);
}
// 返回被层层装饰的缓存
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}

2. 模板模式

在模板模式中,需要使用一个抽象类定义一套操作的整体步骤(即模板),而抽象类的子类则完成每个步骤的具体实现。这样,抽象类的不同子类遵循了同样的一套模板。

2.1 例子

JDK中的AbstractList是大部分List、Queue、Stack的父类,其中的addAll方法使用了模板方法,将具体的add方法交给了子类实现

    public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
boolean modified = false;
for (E e : c) {
add(index++, e);
modified = true;
}
return modified;
} public void add(int index, E element) {
throw new UnsupportedOperationException();
}

2.2 Mybatis实例

作为ORM框架,Mybatis会负责将Java对象写为数据库中的字段,或者是反过来。例如,会将Java中的String字段写为varchar,Integer字段写为int

那么,这些字段类型就需要一个映射,Mybatis就是通过不同TypeHandler来处理的,例如IntegerTypeHandler是来处理Integer的

BaseTypeHandler是一个特定类型处理的父类,这个父类中定义的一些模板方法,其中的setParameter定义了写到数据库的模板,统一处理了空值和非空值,getResult定义了从数据库读的模板,统一处理了异常,具体的实现由子类来负责

  @Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
if (jdbcType == null) {
throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
}
try {
ps.setNull(i, jdbcType.TYPE_CODE);
} catch (SQLException e) {
throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
+ "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
+ "Cause: " + e, e);
}
} else {
try {
setNonNullParameter(ps, i, parameter, jdbcType);
} catch (Exception e) {
throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
+ "Try setting a different JdbcType for this parameter or a different configuration property. "
+ "Cause: " + e, e);
}
}
} @Override
public T getResult(ResultSet rs, int columnIndex) throws SQLException {
try {
return getNullableResult(rs, columnIndex);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column #" + columnIndex + " from result set. Cause: " + e, e);
}
} public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; /**
* @param columnName Colunm name, when configuration <code>useColumnLabel</code> is <code>false</code>
*/
public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException; public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException; public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;

2.3 技巧

  • 类中的模板方法使用final, 避免子类重写它

  • 类中的步骤方法使用abstrat或者抛出异常,强迫子类重写它

3. 责任链模式

3.1 例子

最经典的例子是Servlet的Filter,我们配置了多个Filter,这多个Filter会一个接一个执行,执行的过程中是可以中止的

	@Override
public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain )
throws IOException, ServletException{
doSomething();
filterChain.doFilter( servletRequest, servletResponse );
}

3.2 Mybatis实例

Mybatis的插件功能可能没有听说过,但PageHelper一定用过,它就是使用Mybatis的插件来实现的,有兴趣的读者可以看下这篇文章5分钟!彻底搞懂MyBatis插件+PageHelper原理 - 知乎 (zhihu.com)

通过一系列的拦截器插件,会对Mybatis的一些核心类进行增强,责任链模式使它很容易扩展,即是可拔插的

public class InterceptorChain {
// 拦截器链
private final List<Interceptor> interceptors = new ArrayList<>(); // target是支持拦截的几个类的实例。该方法依次向所有拦截器插入这几个类的实例
// 如果某个插件真的需要发挥作用,则返回一个代理对象即可。如果不需要发挥作用,则返回原对象即可 /**
* 向所有的拦截器链提供目标对象,由拦截器链给出替换目标对象的对象
* @param target 目标对象,是MyBatis中支持拦截的几个类(ParameterHandler、ResultSetHandler、StatementHandler、Executor)的实例
* @return 用来替换目标对象的对象
*/
public Object pluginAll(Object target) {
// 依次交给每个拦截器完成目标对象的替换工作
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
} /**
* 向拦截器链增加一个拦截器
* @param interceptor 要增加的拦截器
*/
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
} /**
* 获取拦截器列表
* @return 拦截器列表
*/
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
} }

3.3 一些区别

上面的两个责任链其实是有些区别的,Servlet的Filter需要主动调用FiterChain的doFilter方法,来确保继续执行下一个Filter,所以也可以不执行,使流程中止,而Mybatis的插件则不可以中止

4. 代理模式

代理模式(Proxy Pattern)是指建立某一个对象的代理对象,并且由代理对象控制对原对象的引用。

  • 静态代理
  • 动态代理,JDK、CGLIB

4.1 例子

下面是一个JDK动态代理的例子,实现InvocationHandler接口和使用Proxy.newProxyInstance,功能是为控制层增加一个耗时统计的功能

public class MetricsCollectorProxy {
private MetricsCollector metricsCollector; public MetricsCollectorProxy() {
this.metricsCollector = new MetricsCollector();
} public Object createProxy(Object proxiedObject) {
Class<?>[] interfaces = proxiedObject.getClass().getInterfaces();
DynamicProxyHandler handler = new DynamicProxyHandler(proxiedObject);
return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, handler);
} private class DynamicProxyHandler implements InvocationHandler {
private Object proxiedObject; public DynamicProxyHandler(Object proxiedObject) {
this.proxiedObject = proxiedObject;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long startTimestamp = System.currentTimeMillis();
Object result = method.invoke(proxiedObject, args);
long endTimeStamp = System.currentTimeMillis();
long responseTime = endTimeStamp - startTimestamp;
String apiName = proxiedObject.getClass().getName() + ":" + method.getName();
RequestInfo requestInfo = new RequestInfo(apiName, responseTime, startTimestamp);
metricsCollector.recordRequest(requestInfo);
return result;
}
}
} //MetricsCollectorProxy使用举例
MetricsCollectorProxy proxy = new MetricsCollectorProxy();
IUserController userController = (IUserController) proxy.createProxy(new UserController());

4.2 Mybatis实例

Mybatis中使用代理的地方很多,我这挑了一个从功能上我们最熟悉的例子

用Mybatis都知道,只需要写一个Mapper接口和xml文件,然后就可以根据Mapper接口的方法来执行xml文件中对应的SQL语句,那这块是怎么实现的呢?其实就是Mybatis自动帮我们生成了一个代理类

下面是一个实现了InvocationHandler的代理类,在invoke方法中,对Object方法和默认方法不处理,其他的方法则使用MapperMethod来处理,MapperMethod其实就是真正执行SQL语句的类,我们的Mapper接口生成了代理类MapperProxy,代理了MapperMethod这个类

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
// 该Map的键为方法,值为MapperMethod对象。通过该属性,完成了MapperProxy内(即映射接口内)方法和MapperMethod的绑定
private final Map<Method, MapperMethod> methodCache; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) { // 继承自Object的方法
// 直接执行原有方法
return method.invoke(this, args);
} else if (method.isDefault()) { // 默认方法
// 执行默认方法
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 找对对应的MapperMethod对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 调用MapperMethod中的execute方法
return mapperMethod.execute(sqlSession, args);
} private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
} private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
throws Throwable {
final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
.getDeclaredConstructor(Class.class, int.class);
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
final Class<?> declaringClass = method.getDeclaringClass();
return constructor
.newInstance(declaringClass,
MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
| MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
.unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
} }

下面是生成MapperProxy的工厂,可以看到也是用了Proxy.newProxyInstance生成了代理类

public class MapperProxyFactory<T> {

  ...
private final Class<T> mapperInterface;
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
} @SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
// 三个参数分别是:
// 创建代理对象的类加载器、要代理的接口、代理类的处理器(即具体的实现)。
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
} public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
} }

这个是MapperRegistry的一个方法,通过这个方法将一个Mapper接口生成一个代理类

  public <T> void addMapper(Class<T> type) {
// 要加入的肯定是接口,否则不添加
if (type.isInterface()) {
// 加入的是接口
if (hasMapper(type)) {
// 如果添加重复
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}

通过上面这几个类,就把Mapper接口对应的一系列的MapperMethod到上面,至于MapperMethod是怎么和SQL关联上的,有兴趣的读者可以自己去看一下,其实也很简单,根据方法名中xml文件取对应的SQL就可以,但还是涉及很多参数处理的东西

5. 其他模式

除了这些模式之外,Mybatis还用了很多其他的模式,因为也比较简单,所以就没列出来了,例如单例模式、建造者模式、工厂模式等

6. 总结

  • 我们主要介绍了Mybatis的装饰器模式、模板模式、责任链模式、代理模式。

  • 平时我们在学习设计模式的过程中,常常会见到一些和实际工程无关的例子代码,感觉这些设计模式只能用于这些例子,无法用于实际的项目。通过学习Mybatis的实际运用,可以加深我们对设计模式的理解

  • 既然Mybatis可以做到很流行,它的代码必然是有可取之处的,所以运用这些模式到自己的项目中,毫无疑问的会提供项目的质量,使用代码更容易扩展和维护,这也是我们学习设计模式的目的。

Mybatis中的设计模式的更多相关文章

  1. mybatis中使用到的设计模式

    Mybatis中使用到了哪些设计模式呢?下面就简单的来介绍下: 1.构造者模式: 构造者模式是在mybatis初始化mapper映射文件的过程中,为<cache>节点创建Cache对象的方 ...

  2. JDK、Spring和Mybatis中使用到的设计模式

    一.JDK中的设计模式 (1)结构性模式 1.适配器模式 java.util.Arrays#asList() java.io.InputStreamReader(InputStream) java.i ...

  3. Mybatis 中经典的 9 种设计模式!面试可以吹牛了

    虽然我们都知道有23个设计模式,但是大多停留在概念层面,真实开发中很少遇到.Mybatis源码中使用了大量的设计模式,阅读源码并观察设计模式在其中的应用,能够更深入的理解设计模式. Mybatis至少 ...

  4. Mybatis中9种经典的设计模式!你知道几个?

    虽然我们都知道有23个设计模式,但是大多停留在概念层面,真实开发中很少遇到.Mybatis源码中使用了大量的设计模式,阅读源码并观察设计模式在其中的应用,能够更深入的理解设计模式. Mybatis至少 ...

  5. [原创]关于mybatis中一级缓存和二级缓存的简单介绍

    关于mybatis中一级缓存和二级缓存的简单介绍 mybatis的一级缓存: MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候 ...

  6. 记录一次bug解决过程:mybatis中$和#的使用

    一.总结 mybatis中使用sqlMap进行sql查询时,经常需要动态传递参数.动态SQL是mybatis的强大特性之一,也是它优于其他ORM框架的一个重要原因.mybatis在对sql语句进行预编 ...

  7. mybatis中#{}与${}的差别(如何防止sql注入)

    默认情况下,使用#{}语法,MyBatis会产生PreparedStatement语句中,并且安全的设置PreparedStatement参数,这个过程中MyBatis会进行必要的安全检查和转义. # ...

  8. mybatis 中的where标签

    mybatis中的where标签可以去除 开头的 and 或者 or 但是放在后面的不行 失败的: <select id="countNotesByParam" parame ...

  9. [Head First设计模式]山西面馆中的设计模式——观察者模式

    系列文章 [Head First设计模式]山西面馆中的设计模式——装饰者模式 引言 不知不自觉又将设计模式融入生活了,吃个饭也不得安生,也发现生活中的很多场景,都可以用设计模式来模拟.原来设计模式就在 ...

  10. [Head First设计模式]山西面馆中的设计模式——建造者模式

    系列文章 [Head First设计模式]山西面馆中的设计模式——装饰者模式 [Head First设计模式]山西面馆中的设计模式——观察者模式 引言 将学习融入生活中,是件很happy的事情,不会感 ...

随机推荐

  1. docker-compose部署django+nginx+minio

    总体文件结构 docker-compose.yml文件 version: "3" # volumes: # 自定义数据卷 networks: # 自定义网络(默认桥接) web_n ...

  2. 10分钟讲清int 和 Integer 的区别

    其实在Java编程中,int和Integer都是非常常用的数据类型,但它们之间存在一些关键的区别,特别是在面向对象编程中.所以接下来,就让我们一起来探讨下关于int和Integer的区别这个问题吧. ...

  3. Hugging News #0710: 体验 MusicGen、Diffusers 库发布一周年、我们的内容政策更新

    每一周,我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新,包括我们的产品和平台更新.社区活动.学习资源和内容更新.开源库和模型更新等,我们将其称之为「Hugging Ne ...

  4. 了解web网络基础

    TCP/IP 协议:一种规则,规定不同计算机操作系统,硬件之间怎么通信的一种规则 像这样把互联网相关联的协议集合起来总称为TCP/IP协议. TCP/IP分层管理 按照组层次分为以下四层: 应用层:决 ...

  5. 基于thumbnailator封装图片处理工具类,实现图片的裁剪、压缩、图片水印、文字水印、多行文字水印等功能

    目录 一.前言 二.工具类的依赖和简单介绍 1.添加依赖 2.简单的使用 3.加载需要处理的图片 4.添加图片处理规则 4.1 Builder的方式 4.2 使用规则工厂的方式 5.输出处理后的图片 ...

  6. SDK 接入|游戏语音之“范围语音”接入实践

    语音是线上游戏用户的主要交流方式,大多数用户会通过游戏中的内置语音功能与其他玩家沟通,而一些用户在游戏没有内置语音功能的情况下,通过其他语音软件与玩家沟通. 并且,游戏语音在玩家开黑时承担着至关重要的 ...

  7. MiniNK WEB 选拔题 by F12

    Start 除了梦想外一无所有的我们,将会和蔑视与困境做最后的斗争,这是最后一舞 N0wayBack 联合战队成立以来一直致力于信息安全技术的研究,作为联合战队活跃在各大 CTF (信息安全竞赛)赛事 ...

  8. 数据标注工具 Label-Studio

    文档抽取任务Label Studio使用指南 目录 1. 安装 2. 文档抽取任务标注 2.1 项目创建 2.2 数据上传 2.3 标签构建 2.4 任务标注 2.5 数据导出 2.6 数据转换 2. ...

  9. nrm工具

    nrm 工具 nrm(npm registry manager)是npm镜像源管理工具.可快速帮助查看.切换.管理npm镜像源. 安装 npm install -g nrm 查看 nrm ls 切换 ...

  10. SSL 证书过期巡检脚本 (Python 版)

    哈喽大家好,我是咸鱼 之前写了个 shell 版本的 SSL 证书过期巡检脚本 (文章:<SSL 证书过期巡检脚本>),后台反响还是很不错的 那么今天咸鱼给大家介绍一下 python 版本 ...