Mybatis执行流程(简略)
JDBC执行流程
需求
Q:什么是MyBatis?
A:一种基于java的持久层框架,相比在代码中拼接sql语句和参数,直接操作JDBC的方式来说,MyBatis的最大优势在于三处——
- 简化数据库交互过程:将sql语句从代码中提到Xml文件中,需要修改Sql语句时无需改动源码(提高可维护性)
- 简化结果集映射操作:通过反射实现了从resultSet到pojo类的映射,无需手动实现映射操作(减少操作步骤)
- 动态sql拼接:将根据if-else语句拼接sql的过程,改为在xml文件中实现(降低操作难度)
我将Mybatis的三大功能,分在三篇文章中实现,本次我们将实现第一部分
实现
简单映射器代理工厂-2
我现在有一个UserMapper接口,有一个UserMapper.xml文件,一个定义了方法签名,一个定义了sql语句。现在我们假想有两个个人分别拿着两个文件,A负责传递传递方法,B负责执行sql
数据结构:
UserMapper(接口)
MapperProxy(代理类)
MapperProxyFactory(工厂类)
InvocationHandler(java代理接口)
Proxy(java代理工厂)
object(真-代理类)
算法:
MapperProxy类实现Invocation接口,实现invoke方法
Proxy调用newProxyInstance方法,传入mapperProxy对象,UserMapper的class对象,类加载器。获得一个Object类型的真-代理类
这个Object类型的代理类被视作实现了UserMapper接口,因此可以通过UserMapper接口用上转型进行操作
这是平时,通过接口上转型操作对象的流程
这是现在,通过代理来操作对象的流程
需要使用到的技术:
- 代理:你有没有发现过一个问题,我们在Mybatis中写代码时,只是定义了接口和xml文件,从没创建过一次实现类?然而使用时,很显然Mybatis生成了对应接口的实现类,还把xml文件中的sql语句转换成了类中的方法体,这背后依靠的技术就是代理,代理可以通过传入的参数动态决定实现哪些接口,还可以通过invoke方法截获方法调用,封装复杂逻辑
- 泛型:除了代理外,还使用了另一个重要技术——泛型,泛型指的是一个类中的某些属性或方法参数的类型在文件阶段不固定,在使用时才确定(因此编译后会进行泛型擦除,替换成真实的类型),泛型让我们只需要定义一个MapperProxy<T>类,我们只需传入不同的MapperInterface.class文件,就能创建n个不同参数类型的MapperProxy类
- 简单工厂模式:简单工厂是所有设计模式中最简单的一类,只涉及到两个模块,它的作用是将对象的创建与使用解耦,使用交给xxx对象本身,而创建过程交给xxxFactory,这种设计模式虽然简单却很实用,如果我们未来有一些创建对象的特殊逻辑,比如校验参数合法性等,就不需要在对象的构造函数中写复杂的逻辑,而是交给xxxFactory类完成。
技术的实现:
1.代理
使用 java.lang.reflect 包,让MapperProxy类实现InvocationHandler接口的invoke方法,再使用 UserMapper userDao = Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{UserMapper.class}, mapperProxy);方法创建和使用代理类实例
public interface UserMapper{
selectUser(int id);
}
public class MapperProxy<T> implements InvocationHandler{
private Class<T> MapperInterface;
public Object invoke(Object proxy,Method method,Object[] args){
System.out.println("调用方法:" + method.getName());
for(Object arg:args){
System.out.println(arg.toString());
}
}
}
public class ApiTest{
@Test
public void test(){
MapperProxy<T> mapperProxy = new MapperProxy<>(mapperInterface);
UserMapper userDao = Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{UserMapper.class}, mapperProxy);
}
}
2.泛型
在方法和类上标明使用到的泛型类型(T、K、V...),在参数和属性上使用
3.简单工厂模式
将创建MapperProxy实例的过程封装成一个类,命名为MapperProxyFactory,今后想要获取MapperProxy,就通过MapperProxyFactory.newInstance()方法获取。
package com.simon.mybatis.binding;
import com.simon.mybatis.session.SqlSession;
import java.lang.reflect.Proxy;
import java.util.Map;
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public T newInstance() {
//使用final修饰,防止被修改
final MapperProxy<T> mapperProxy = new MapperProxy<>(mapperInterface);
//动态代理机制要求在运行时使用与目标接口相同的类加载器,
//否则即使代理实现了接口,如果是在不同的类加载器中加载的,JVM也会将它们视为不同的类。
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperProxy.getMapperInterface()}, mapperProxy);
}
}
映射器注册与使用-5
需要使用的技术:
- 扫描:我们需要一项功能,可以批量获取指定路径下的接口类文件,这种技术就是扫描,扫描就是将指定路径下的文件读取到内存系统中,文件可以是.class字节码文件,也可以是.xml配置文件,但在java中,它们最终都需要转换成对象的方式供我们使用。扫描的意义是实现程序与文件系统的数据交互,在任何程序或框架中都极其有用
- 工厂模式:本次编码要新增一个SqlSession类,SqlSession负责接收来自代理类的请求(包含sql语句和参数),并以此调用执行器进行数据库操作,是相当重要的模块,因此我们使用工厂模式管理其创建过程。工厂模式要解决的问题和简单工厂模式一样,但流程更复杂,工厂模式需要两个接口和两个实现类(最少),从更高的抽象维度定义操作逻辑,这么做的好处是当业务需求改变时,可以随时更换新的工厂实现类。代码因此更加的灵活。
技术的实现:
1.扫描
在MapperRegistry.addMappers方法中使用Hutool包下的ClassScanner.scanPackage(packageName);方法,packageName为接口类文件所在的路径,格式为"com.xxx.xxx"
这里也运用了简单工厂模式,MapperRegistry负责创建MapperProxyFactory,并将其与Interface.class文件的映射关系存储在一个HashMap中
package com.simon.mybatis.binding;
import cn.hutool.core.lang.ClassScanner;
import com.simon.mybatis.session.Configuration;
import com.simon.mybatis.session.SqlSession;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapperRegistry {
// 存放 Mapper 接口和 MapperProxyFactory 的映射关系
private final Map<Class<?>,MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new IllegalArgumentException("Type " + type + " is not known to the MapperRegistry.");
}
try{
return mapperProxyFactory.newInstance(sqlSession);
}catch (Exception e){
throw new RuntimeException("Error getting mapper instance. Cause: "+e, e);
}
}
public <T> void addMapper(Class<T> type) {
/* Mapper 必须是接口才会注册 */
if (type.isInterface()) {
// 注册映射器代理工厂
knownMappers.put(type, new MapperProxyFactory<>(type));
}
}
public void addMappers(String packageName) {
Set<Class<?>> mapperSet = ClassScanner.scanPackage(packageName);
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
}
2.工厂模式
这里我们把工厂方法用在SqlSession和SqlSessionFactory接口上,并新建DefaultSession和DefaultSessionFactory
package com.simon.mybatis.session;
public interface SqlSession {
/**
* Retrieve a single row mapped from the statement key
* 根据指定的SqlID获取一条记录的封装对象
*
* @param <T> the returned object type 封装之后的对象类型
* @param statement sqlID
* @return Mapped object 封装之后的对象
*/
<T> T selectOne(String statement);
/**
* Retrieve a single row mapped from the statement key and parameter.
* 根据指定的SqlID获取一条记录的封装对象,只不过这个方法容许我们可以给sql传递一些参数
* 一般在实际使用中,这个参数传递的是pojo,或者Map或者ImmutableMap
*
* @param <T> the returned object type
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @return Mapped object
*/
<T> T selectOne(String statement, Object parameter);
/**
* Retrieves a mapper.
* 得到映射器,这个巧妙的使用了泛型,使得类型安全
*
* @param <T> the mapper type
* @param type Mapper interface class
* @return a mapper bound to this SqlSession
*/
<T> T getMapper(Class<T> type);
}
package com.simon.mybatis.session;
public interface SqlSessionFactory {
/**
* 打开一个 session
* @return SqlSession
*/
SqlSession openSession();
}
两个默认实现类:
package com.simon.mybatis.session.defaults;
import com.simon.mybatis.binding.MapperRegistry;
import com.simon.mybatis.session.SqlSession;
public class DefaultSqlSession implements SqlSession {
/**
* 映射器注册机
*/
private MapperRegistry mapperRegistry;
// 构造函数
public DefaultSqlSession(MapperRegistry mapperRegistry){
this.mapperRegistry = mapperRegistry;
}
@Override
public <T> T selectOne(String statement){
System.out.println("statement: " + statement);
return (T) ("你被代理了!" + "sql语句:" + statement);
}
@Override
public <T> T selectOne(String statement, Object parameter) {
return (T) ("你被代理了!" + "方法:" + statement + " 入参:" + parameter);
}
@Override
public <T> T getMapper(Class<T> type) {
return mapperRegistry.getMapper(type, this);
}
}
package com.simon.mybatis.session.defaults;
import com.simon.mybatis.binding.MapperRegistry;
import com.simon.mybatis.session.SqlSession;
import com.simon.mybatis.session.SqlSessionFactory;
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final MapperRegistry mapperRegistry;
public DefaultSqlSessionFactory(MapperRegistry mapperRegistry) {
this.mapperRegistry = mapperRegistry;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(mapperRegistry);
}
}
Mapper.xml的解析、注册和使用-8
读取xml文件后,所有的sql语句被包装到MappedStatement中,并传入到Configuration中的HashMap中加以存储,需要使用时,通过get方法就能获取。此外,由于xml文件和接口类同名,Resource类获取到xml文件名还可以直接通过Class.forName(className)获得类文件,从此以后MapperRegistry只负责制造代理类,不再涉足加载类。
Configuration还通过MapperRegistry的依赖,向sqlSession提供代理类获取服务
从这一节开始,我们增加了8个类,并对所有旧类进行了修改,这导致系统的复杂度暴增,于是我绘制了这样一张图,以便从宏观角度理解项目——
- 红色代表sqlSession子系统,直接面向使用者提供服务
- 蓝色是新添加的文件读取与处理子系统,负责加载接口class文件和Mapper.xml文件
- 黄色是我们在第一节编写的代理类生成子系统,如今只负责接收接口class文件,返回代理类
这张图看起来很复杂,实际上也很复杂,因此我把整个流程的处理拆成了三个部分:
1.使用
- 我们创建一个Resources类,输入路径,得到了一个Reader文件
- Reader文件需要有人处理,我们调用SqlSessionFactoryBuilder().build(reader),得到了一个SqlSessionFactory
- SqlSessionFactory得到SqlSession对象
- 从SqlSession对象getMapper方法得到代理类对象,通过接口调用方法使用
这个流程看起来很简单,因为这是面向使用者的操作流程,所有操作已被封装好了,但是看起来还是有些疑点,比如,SqlSession类的用处是提供一批已经写好的半成品数据库操作方法,它本应处在操作的下游,但如今却能直接向我们提供生成好的代理类,这似乎有些奇怪,如果你打开方法,却发现它提供给我们的代理类是从Configutation对象那里获得的,这个Configuration是从哪里来的?它是何方神圣,能干原本属于MapperProxyFactory的活?
2.读取
这一条路径逐渐揭开了Configuration的神秘面纱——
- 原来,在第一步取得的reader对象将传入XmlConfigBuilder,在这里,xml文件中的每一条配置项,都将被转换成一个MappedStatement对象(映射),添加到Configuration中,此外,XmlConfigBuilder还扫描并获取了所有的接口class文件,将其注册到MapperRegistry,再将MapperProxy传递给Configuration
- 打开Configuration,这个类堪称整个项目的心脏,它拥有一个Map<String, MappedStatement>属性,这意味这项目中所有的sql语句都可以通过Configuraution获取,它还有一个MapperRegistry属性,因此通过Configuration可以获取所有的代理类实例,而任何类只要依赖Configuration,就可以获得Configuration的全部能力,怪不得SqlSession可以返回代理类实例!
3.方法调用
这一条路径最为复杂,也最为重要,因为这是从参数传入到方法执行的步骤,是核心业务的执行步骤
- SqlSession接到了获取代理类的需求,转头交给Configuration处理,并将自己作为参数传递了进去
- Configuration接到请求,转头交给MapperRegistry处理,继续传递SqlSession
- MapperRegistry创建MapperFactory,MapperFactory里出现了一个名叫methodCache的Map,存储所有Method和MapperMethod的映射关系,MapperMethod同时关联了Method和MappedStatement,还能接接收参数调用方法,是class类、xml文件、参数的集成
- MapperFactory创建MapperProxy,到这里,第一阶段的任务已经完成了,我们已经得到了代理类实例,而MapperProxy通过invoke接收到的方法调用,全部转交给对应的MapperMethod.execute进行执行
- 进入到MapperMethod,这是我们的最后一站,因为这个对象实现了Mybatis的核心功能——解析sql语句和参数,决定调用哪个SqlSession方法。
为什么费这么大的力气传递SqlSession,而不是直接让每个MapperMethod持有一个?因为每个SqlSession都对应一个数据库连接,而数据库连接是非常宝贵的资源,使用完毕后需要立刻销毁,否则数据库就会因创建太多连接而负担过重。
其他
本章节并没有介绍具体的实现技术,而是介绍了模块间的关系和执行流程,这是因为随着项目规模的增长,我们需要开始忽略技术的实现细节,专注于项目模块内的聚合和模块间的解耦,从一个更宏观的角度俯瞰项目。
现在,让我们想一下,如果我们现在已经完成了章节二的内容,有了一个简单的
需要使用的技术:
- 扫描-xml文件:dom4j,javaIO
- 建造者模式和静态内部类:MappedStatement
- 字符串模式匹配:正则表达式
技术的实现:
@Test
public void test_MapperProxyFactory4() {
try {
// 创建SAXReader实例
SAXReader reader = new SAXReader();
// 读取XML文件
Document document = reader.read(getClass().getClassLoader().getResourceAsStream("mapper/userMapper.xml"));
// 获取根元素
Element root = document.getRootElement();
System.out.println(root.getName());
// 获取所有select元素
List<Element> selectList = root.elements("select");
// 遍历select元素
for (Element select : selectList) {
System.out.println(select.attributeValue("id"));
System.out.println(select.attributeValue("parameterType"));
System.out.println(select.attributeValue("resultType"));
System.out.println(select.getStringValue());
}
} catch (Exception e) {
e.printStackTrace();
}
}
随笔
数据线视角
一个系统,数据的流动方向就是其生命线,观察一个类的重要性,只需要看它参与了几条数据处理线路。
Mybatis系统有三条数据线路——xml文件,.class文件,参数
Configuration为什么强大?因为它参与到了xml和class两条线路中,为什么能参与?靠他自己?不是,是XmlConfiguration给它的。从Configuration开始的两条支流,连同第三条参数线路一起,汇聚在MapperProxy,最终进入SqlSession
设计模式与“高内聚,低耦合”
所有的设计模式,要做的事都是一个字——拆,把一个大的,复杂的类,拆成许多小的,简单的类。这么做是为了降低代码的理解难度,理清业务逻辑。
“高内聚,低耦合”,这两个要求实际上是相互冲突的,"高内聚"就是指一个类完成的事要尽可能的少、简单。因此我们需要创建多个类让它们共同完成一个功能,但因此类与类之间的耦合关系又会提高,而"低耦合"又要求类与类间的关系要尽可能简单,这是一个典型的"既要,又要"的需求,然而,在长久的实践中,人们还真的找出符合这种要求的编码方式——这就是所谓的23种设计模式