MyBatis 源码下载地址:https://github.com/MyBatis/MyBatis-3
MyBatis整体架构: MyBatis源码共16个模块,可以分成三层,如下图 :
基础支撑层:技术组件专注于底层技术实现,通用性较强无业务含义;
核心处理层:业务组件专注MyBatis的业务流程实现,依赖于基础支撑层;
接口层:MyBatis对外提供的访问接口,面向SqlSession编程;
从源码的架构分析,特别是接口层的设计,可以看出来MyBatis的整体架构符合门面模式的。
门面模式定义:提供了一个统一的接口,用来访问子系统中的一群接口。外观模式定义了一个高层接口,让子系统更容易使用。
门面模式优点:使复杂子系统的接口变的简单可用,减少了客户端对子系统的依赖,达到了解耦的效果;遵循了OO原则中的迪米特法则,对内封装具体细节,对外只暴露必要的接口。
门面模式使用场景:
一个复杂的模块或子系统提供一个供外界访问的接口
子系统相对独立 ― 外界对子系统的访问只要黑箱操作即可
单一职责原则:一个类或者一个接口只负责唯一项职责,尽量设计出功能单一的接口;
依赖倒转原则:高层模块不应该依赖低层模块具体实现,解耦高层与低层。既面向接口编程,当实现发生变化时,只需提供新的实现类,不需要修改高层模块代码;
开放-封闭原则:程序对外扩展开放,对修改关闭;换句话说,当需求发生变化时,我们可以通过添加新模块来满足新需求,而不是通过修改原来的实现代码来满足新需求;
迪米特法则:一个对象应该对其他对象保持最少的了解,尽量降低类与类之间的耦合度;实现这个原则,要注意两个点,一方面在做类结构设计的时候尽量降低成员的访问权限,能用private的尽量用private;另外在类之间,如果没有必要直接调用,就不要有依赖关系;这个法则强调的还是类之间的松耦合;
里氏代换原则:所有引用基类(父类)的地方必须能透明地使用其子类的对象;
接口隔离原则:客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上;
MyBatis没有提供日志的实现类,需要接入第三方的日志组件,但第三方日志组件都有各自的Log级别,且各不相同,而MyBatis统一提供了trace、debug、warn、error四个级别;
自动扫描日志实现,并且第三方日志插件加载优先级如下:slf4J → commonsLoging → Log4J2 → Log4J → JdkLog;
在org.apache.ibatis.logging.LogFactory中的静态代码块中,通过静态代码块确保第三方日志插件加载优先级如下:slf4J → commonsLoging → Log4J2 → Log4J → JdkLog;
static{
tryImplementation(LogFactory::useSlfjLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
日志的使用要优雅的嵌入到主体功能中;
日志模块的第一个需求是一个典型的使用适配器模式的场景,适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁,将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
适用场景:当调用双方都不太容易修改的时候,为了复用现有组件可以使用适配器模式;在系统中接入第三方组件的时候经常被使用到;注意:如果系统中存在过多的适配器,会增加系统的复杂性,设计人员应考虑对系统进行重构;
MyBatis日志模块是怎么使用适配器模式?实现如下:
Target:目标角色,期待得到的接口。org.apache.ibatis.logging.Log接口,对内提供了统一的日志接口;
Adaptee:适配者角色,被适配的接口。其他日志组件组件如slf4J 、commonsLoging 、Log4J2等被包含在适配器中。
Adapter:适配器角色将源接口转换成目标接口。针对每个日志组件都提供了适配器,每个适配器都对特定的日志组件进行封装和转换;如 Slf4jLoggerImpl 、 JakartaCommonsLoggingImpl 等;
日志模块适配器结构类图:
总结:日志模块实现采用适配器模式,日志组件(Target)、适配器以及统一接口(Log接口)定义清晰明确符合单一职责原则;同时,客户端在使用日志时,面向Log接口编程,不需要关心底层日志模块的实现,符合依赖倒转原则;最为重要的是,如果需要加入其他第三方日志框架,只需要扩展新的模块满足新需求,而不需要修改原有代码,这又符合了开闭原则;
代理模式定义:给目标对象提供一个代理对象,并由代理对象控制对目标对象的引用;目的:(1)通过引入代理对象的方式来间接访问目标对象,防止直接访问目标对象给系统带来的不必要复杂性;(2)通过代理对象对原有的业务增强;
代理模式有静态代理和动态代理两种实现方式。
静态代理这种代理方式需要代理对象和目标对象实现一样的接口。
优点:
可以在不修改目标对象的前提下扩展目标对象的功能。
缺点:
冗余。由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。
不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改。
静态代理代码示例
动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。静态代理与动态代理的区别主要在:
静态代理在编译时就已经实现,编译完成后代理类是一个实际的class文件
动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中。
注意:动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。
动态代理代码示例
JDK中生成代理对象主要涉及两个类,第一个类为java.lang.reflect.Proxy,通过静态方法newProxyInstance生成代理对象,第二个为java.lang.reflect.InvocationHandler接口,通过invoke方法对业务进行增强;
如何优雅的增强日志功能?
首先搞清楚那些地方需要打印日志?通过对日志的观察,如下几个位置需要打日志:
在创建prepareStatement时,打印执行的SQL语句;
访问数据库时,打印参数的类型和值;
查询出结构后,打印结果数据条数。
因此在日志模块中有BaseJdbcLogger、ConnectionLogger、PreparedStatementLogger和ResultSetLogge通过动态代理负责在不同的位置打印日志;几个相关类的类图如下:
ConnectionLogger:负责打印连接信息和SQL语句,并创建PreparedStatementLogger;
PreparedStatementLogger:负责打印参数信息,并创建ResultSetLogger;
ResultSetLogge:r负责打印数据结果信息;
BaseJdbcLogger:所有日志增强的抽象基类,用于记录JDBC那些方法需要增强,保存运行期间sql参数信息;
ConnectionLogger:负责打印连接信息和SQL语句。通过动态代理,对connection进行增强,如果是调用prepareStatement、prepareCall、createStatement的方法,打印要执行的sql语句并返回prepareStatement的代理对象(PreparedStatementLogger),让prepareStatement也具备日志能力,打印参数;
PreparedStatementLogger:对prepareStatement对象增强,增强的点如下:
①增强PreparedStatement的setxxx方法将参数设置到columnMap、columnNames、columnValues,为打印参数做好准备;
②增强PreparedStatement的execute相关方法,当方法执行时,通过动态代理打印参数,返回动态代理能力的resultSet;
③如果是查询,增强PreparedStatement的getResultSet方法,返回动态代理能力的resultSet;如果是更新,直接打印影响的行数
ResultSetLogger:负责打印数据结果信息;
常见的数据源组件都实现了javax.sql.DataSource接口;
MyBatis不但要能集成第三方的数据源组件,自身也提供了数据源的实现;
一般情况下,数据源的初始化过程参数较多,比较复杂;
工厂模式(Factory Pattern)属于创建型模式,它提供了一种创建对象的最佳方式。定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
产品接口(Product):产品接口用于定义产品类的功能,具体工厂类产生的所有产品都必须实现这个接口。调用者与产品接口直接交互,这是调用者最关心的接口;
具体产品类(ConcreteProduct):实现产品接口的实现类,具体产品类中定义了具体的业务逻辑;
工厂接口(Factory):工厂接口是工厂方法模式的核心接口,调用者会直接和工厂接口交互用于获取具体的产品实现类;
具体工厂类(ConcreteFactory):是工厂接口的实现类,用于实例化产品对象,不同的具体工厂类会根据需求实例化不同的产品实现类;
使用new关键字直接创建对象
优点
通过反射机制创建对象;
通过工厂类创建对象;
缺点
对象创建和对象使用的职责耦合在一起,违反单一原则;
当业务扩展时,必须修改代业务代码,违反了开闭原则;
使用工厂模式创建对象
优点
把对象的创建和使用的过程分开,对象创建和对象使用使用的职责解耦;
如果创建对象的过程很复杂,创建过程统一到工厂里管理,既减少了重复代码,也方
便以后对创建过程的修改维护;
当业务扩展时,只需要增加工厂子类,符合开闭原则;
PooledDataSource:一个简单,同步的、线程安全的数据库连接池;
PooledConnection:使用动态代理封装了真正的数据库连接对象;
PoolState:用于管理PooledConnection对象状态的组件,通过两个list分别 管理空闲状态的连接资源和活跃状态的连接资源。
请详细描述从数据库连接池中获取一个连接资源的过程?
获取连接:getConnection()
归还连接:pushConnection()
Ø MyBatis缓存的实现是基于Map的,从缓存里面读写数据是缓存模块的核心基础功能;
Ø 除核心功能之外,有很多额外的附加功能,如:防止缓存击穿,添加缓存清空策略(fifo、lru)、序列化功
能、日志能力、定时清空能力等;
Ø 附加功能可以以任意的组合附加到核心基础功能之上;
怎么样优雅的为核心功能添加多种附加能力?
使用动态代理或继承的办法扩展多种附加功能?
优化思路:装饰器模式是一种用于代替继承的技术,无需通过继承增加子类就能扩展对象的新功能。使用对象的关联关系代替继承关系,更加灵活,同时避免类型体系的快速膨胀;
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,是一种用于代替继承的技术,无需通过继承增加子类就能扩展对象的新功能。使用对象的关联关系代替继承关系,更加灵活,同时避免类型体系的快速膨胀;
- 组件(Component):组件接口定义了全部组件类和装饰器实现的行为;
- 组件实现类(ConcreteComponent):实现Component接口,组件实现类就是被装饰器装饰的原始对象,新功能或者附加功能都是通过装饰器添加到该类的对象上的;
- 装饰器抽象类(Decorator):实现Component接口的抽象类,在其中封装了一个Component 对象,也就是被装饰的对象;
- 具体装饰器类(ConcreteDecorator):该实现类要向被装饰的对象添加某些功能;
优点:相对于继承,装饰器模式灵活性更强,扩展性更强;
灵活性:装饰器模式将功能切分成一个个独立的装饰器,在运行期可以根据需要动态的添加功能,甚至对添加的新功能进行自由的组合;
扩展性:当有新功能要添加的时候,只需要添加新的装饰器实现类,然后通过组合方式添加这个新装饰器,无需修改已有代码,符合开闭原则;
装饰器模式使用举例:
- IO中输入流和输出流的设计
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("c://a.txt")));
-对网络爬虫的自定义增强,可增强的功能包括:多线程能力、缓存、自动生成报表、黑白名单、random触发等。
MyBatis缓存组件:
Cache:Cache接口是缓存模块的核心接口,定义了缓存的基本操作;
PerpetualCache:在缓存模块中扮演ConcreteComponent角色,使用HashMap来实现cache的相关操作;
BlockingCache:阻塞版本的缓存装饰器,保证只有一个线程到数据库去查找指定的key对应的数据;
缓存装饰器解读:
- FifoCache:先进先出缓存淘汰策略的缓存;
- LoggingCache:日志能力的缓存;
- ScheduledCache:定时清空的缓存;
- BlockingCache:阻塞式缓存;
- SerializedCache:序列化能力的缓存;
- SynchronizedCache:进行同步控制的缓存;
Mybatis的缓存功能使用HashMap实现会不会出现并发安全的问题?
CacheKey解读:MyBatis中涉及到动态SQL的原因,缓存项的key不能仅仅通过一个String来表示,所以通过CacheKey来封装缓存的Key值,CacheKey可以封装多个影响缓存项的因素;判断两个CacheKey是否相同关键是比较两个对象的hash值是否一致。
orm框架查询数据过程:
mybatis核心流程三大阶段:
为什么使用mapper接口就能对数据库进行访问?
为什么在spring容器中,并没有出现sqlSession的身影?
学习源码的步骤:
论学习源码的目的 请大牛现身说法: