首先明白一点,面向对象是啥意思。字面意思就是你面向的是对象,也就是java世界里面万物都是对象。在mybatis这个使用java开发的框架里面,也是一样的,也就是说你看到的所有东西,最后落地都是一个个对象,包括你的那些配置文件,那些xml文件都是一样的,最后都要被框架加载读取解析封装成为一个对象在mybatis的体系中发挥作用。OK,了解一下这个后面看到那些配置文件的处理也就自然了。
操作系统中io调用是要发起内核函数的调用的,mybatis程序是运行在用户态的,前面我们说读取配置文件转为对象,这个读取就是一次io,众所周知,用户态进入内核态是一件很"耗"的事情。所以我们尽量减少这个操作,所以你得那些配置文件尽量少读几次,那最少得读一次,好吧,那就读一次最少。
OK,这些基本就是前置要了解的东西,东西不多,就那么个意思。
我认识一个哥们,他问我要学啥东西,我说没事干看看Mybatis,这个哥们对我说:
不就是JDBC的封装吗,有啥可学的。
其实他说的不错,mybatis说的简单一点就是对JDBC的封装,那既然是封装,那最后一定是落脚到了JDBC上,你可以这么想,给了你一个JDBC的代码,让你封装一下,你肯定得知道JDBC有哪些东西,然后你才能去做封装。我们先来看一下JDBC有哪些组件。
Connection conn=DriverManager.getConnection(url, user, password);
String sql="select * from user";
//statement每次执行sql,相关数据库都要执行sql语句的编译,preparedstatement是预编 译的,
//preparedstatement支持批处理,处理多次请求的效率高 PreparedStatement
preparedStatement=conn.prepareStatement(sql);
//调用executeQuery()方法,返回一个结果集rs,到此,数据库查询数据的工作就完成了
ResultSet rs=preparedStatement.executeQuery();
上面就是jdbc的主要操作,可以看到主要就是三个类:
mybatis有这么几种对象:
我们在编写mybatis代码的时候,有两种配置文件,一种是mybatis的核心配置,一种是你的sql的mapper文件,这两种文件都是要封装在对象里面的,因为你不能在使用的时候随时做读取解析吧,这种io操作必然不能多做,只能是一次读取解析封装完毕了,就放在对象里面了,以后随用随拿。
这个对象就是用来封装mybatis的核心配置文件的,我们来看一下这个类。这个类里面有很多属性,我们先看几个核心的主要的。
这个类就是封装的核心配置文件,也就是我们之前例子里面的那个sqlMapConfig.xml文件,里面所有你的配置都可以来这里找到,进而做解析封装进来。我们来看几个。
// 封装了environment属性,这个是你jdbc数据源的环境配置
protected Environment environment;
// 这个是一级缓存是不是打开,默认是true,就是打开的
protected boolean cacheEnabled = true;
// 执行器,我们看到类型默认是SIMPLE,后面跟源码的时候可以看到就是这个的执行
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
// MappedStatement这个其实是你mapper。xml文件的封装。下面会详细说这个
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) ->
". please check " + savedValue.getResource() + " and " + targetValue.getResource());
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
这里列举了几个主要的,我只是想说明一个问题,就是你那个核心配置类里面的属性都能解析封装到这个对象里面,这里我没列全,实际上,这个类里面有多少属性,那个配置文件就能配置多少,所以有时候官方打不开了,可以来源码这里看看具体能配置啥。
此外这个类下面还有这么几个对象:
// 我们看到其实他还帮你new出来很多处理器,都在这个类里面,所以这个Configuration是十分全面的。
// 而且Configuration还帮我们创建了下面这几个核心对象,所以Configuration实际上是他们几个的工厂。
// 这几个类由Configuration来创建。所以他很牛逼。核心类。
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
public Executor newExecutor(Transaction transaction) {
return newExecutor(transaction, defaultExecutorType);
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
有了这个类,mybatis在启动的时候就加载了配置文件,然后读取,解析,最后把你的配置封装到这个对象里面,以后随处用的时候直接拿出来用就可以了。
我们还有一个配置文件,也就是你写sql的那些mapper.xml的文件,这类文件也是不能经常io,只读取一次就行了。那么他被封装到了这个MappedStatement的对象里面。
private String resource;
// 他这里其实还持有了Configuration 这个对象,Configuration 也封装了所有的MappedStatemen,他们能互相拿到。灵活度更高。
private Configuration configuration;
// 这就是你写sql标签里面的那个id,什么selectById就是这个玩意,在这里封装你看他这里的设计,一个id就是一个对象,所以他其实这个对象更细分的话他是封装的xml里面的一个增删改查的标签片段,但是我们又说了selectById这个只是方法名,每个mapper里面都可能有,所以他光靠这个不能确定唯一,他需要namespace结合一起来唯一确定,所以这个对象里面的id是由namespace+mapper中的id来组合确定的。
private String id;
// 每次查询的个数,批次大小,
private Integer fetchSize;
// 超时时间
private Integer timeout;
// 你使用的jdbc的Statement类型
private StatementType statementType;
private ResultSetType resultSetType;
private SqlSource sqlSource;
private Cache cache;
private ParameterMap parameterMap;
private List<ResultMap> resultMaps;
private boolean flushCacheRequired;
private boolean useCache;
private boolean resultOrdered;
private SqlCommandType sqlCommandType;
private KeyGenerator keyGenerator;
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;
// 这个类的构造方法,是个建造者模式
public static class Builder {
private MappedStatement mappedStatement = new MappedStatement();
public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
mappedStatement.configuration = configuration;
mappedStatement.id = id;
mappedStatement.sqlSource = sqlSource;
// 我们看到了StatementType 属性默认是PreparedStatement这个预编译的,可见他也支持用这个,当然了你可以在配置文件里面修改,类似下面的例子,所以他默认给你的是这个预编译的statement
mappedStatement.statementType = StatementType.PREPARED;
我们在Configuration里面看到了这么一类属性
// MappedStatement这个其实是你mapper。xml文件的封装。下面会详细说这个
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) ->
". please check " + savedValue.getResource() + " and " + targetValue.getResource());
我们看到了他封装了一个map,map里面是key-MappedStatement的一个key-value这个关系,那么他啥意思呢,他是在你封装了mapper.xml的MappedStatement的对象作为一个个的map,因为可能有多个xml所以最后是多个,那么这个多个他用key去做的区分,这个key要唯一区别的话第一个就是你写的那个id在xml里面要唯一,但是话又说回来了,我们可不止一个userMapper.xml,还可能有studentMapper.xml,那个id只是在自己的xml里面唯一的,但是你要封装到这个全局的map里面这个id就不能保证唯一key了,所以他实际上是依赖的userMapper.xml的两个部分来区分的,
也就是:
<mapper namespace="com.yx.dao.IUserDao">
<select id="findAll" resultType="com.yx.domain.User" statementType="CALLABLE">
SELECT * FROM `user`
select>
其实就是namespace和id合起来做唯一区分,至于你是用逗号隔开,还是空格啥的无所谓了,实际上mybatis是用.来隔开的。
基于以上我们可以得出这么一个结果,就是你的核心配置文件疯转成为Configuration,里面的每一个配置都在对象中有一个属性能对应上。
而你mapper.xml是封装在MappedStatement这个对象里面的,但是他封装的维度不是一整个文件,而是文件里面的一个个sql标签,区分这些sql标签的方式就是namesapce.id。具体可以参考下面这个图示。
上面我们已经解析了配置文件,把对应的信息封装到了对象里面,以后想用的时候随时拿来用就行了。我们这里先看一下我从网上找的一个架构图。而我们上面看到这类操作类的对象其实也是在Configuration里面创建的,Configuration是他们的工厂。这类对象就要负责对最终的数据库做操作,实现数据的操作。
大家都能看到,我们最终暴露给客户端调用的就是sqlSession这个类,他下面执行是调用的excutor这个类,看名字也知道是执行器的意思,我盲猜这里就是真正的操作的地方,我们去看一下这个类。
他是个接口,里面定义了方法的模板实现,他是mybatis中处理功能的核心。而代码中设计的操作只有query和update,这里需要说一下就是在代码中的update其实对应到数据库操作其实是增删改,而查就是query,其他的缓存操作,事务操作他也是支持的。
他的实现类包括:BatchExecutor(批量操作,在jdbc中每一次执行sql都要做一次连接都要创建statement,所以批量执行其实是很有必要支持的,所以其实也支持batch操作。),ReuseExecutor(复用statement对象的时候,sql完全不变,包括参数之类的,就可以复用statement来重复执行sql),SimpleExecutor(最基本的,最常用的,也是mybatis默认的。),
我们看到这些方法也都是对应的你对数据库做的操作方法。其主要方法也就是这么几个
实际上StatementHandler也就是完成了sql的执行,但是事务,缓存其实还在其他地方。他完成的是最基本的。
这里就是封装了jdbc的Statement,而jdbc中的sql就是Statement操作的,所以这里其实就是执行sql操作了。进行jdbc操作。
他也是一个接口,这种操作类定义成为接口做多态扩展的实现在mybatis里面层出不穷。我们看下他的实现类。
其中base是一个适配器,不是最终真正实现的,上面的Executor其实也是这样。
所以我们到这里先局部总结一下:
我们建立会话开始处理最外层的类是SqlSession:但是他啥也不干,就是做了个封装。
SqlSession把执行的具体操作交给了Excutor类,这个类负责大体三个功能,执行sql,管理缓存,操作事务。
其中他把执行sql这个操作交给了StatementHandler(看名字也知道,因为最后是和jdbc打交道,其实就是Statement),其他两个管理缓存,操作事务他不负责。
上面是把sql执行都做了,但是还有个问题就是参数,我们在mybatis写的sql来看都是那种#{}这种的,StatementHandler最后执行的时候人家从BoundSQL里面拿到的sql必须就是要一个最终的sql,所以还要做参数的替换,这种你自己也可以实现一下,就是个正则表达式替换,把我们mybatis的参数替换为jdbc的参数,最终sql封装在BoundSQL交给Statement。
然后mybatis把参数处理的工作交给了ParameterHandler下面的实现。你其实可以看到,他的这种职责划分的是很细致的。
最后执行的时候,以及执行完了做返回的时候,你执行的时候,要传参,最后执行完了返回的时候有结果,
这里面涉及到一个mybatis的java类型和数据库的mysql类型的转换映射,举个例子,你mybatis里面写的时候是string类型,到了数据库可能就是varchar类型。这种需要做转换,不然类型不对应,这个转换工作就是TypeHandler来做的,因为这个转换可能发生在执行sql的时候的参数转换以及查询结果的结果类型转换,所以其一定是和StatementHandler和ParameterHandler有关系的。
这个转换是双向的,你查的时候要把java的类型转去数据库,也要查回来把数据库类型转为java。
上面有了sql了,类型转换也完了,最后就是执行sql,那这个执行结果查出来之后这个结果集的封装就是在这里做的。返回给程序,他是和jdbc重的ResultSet差不多一个意思。
我们一直说mybatis是jdbc的封装,jdbc是java执行数据库sql的工具。那我们这里面其实就是涉及到了java执行以及jdbc这两个内容。
其中jdbc包括:
这几个东西,那我们换位思考,我们设计mybatis的时候就是封装这几个东西就行了,所以他拆分了几个功能,
每个功能都负责对应的事情。我们从底向上开始解析一下:
其中StatementHandler负责封装statement,最后和sql打交道执行。
其中ParameterHandler负责封装对你xml里面参数的转换处理。最后传进去sql文本。
其中ResultSetHandler负责封装查询结果集的结果。最后返回给客户端。
其中TypeHandler负责对传参时候以及查询结果的数据库和java之间的类型转换。
最后他们统一在Excutor这个类里面做调用,其中Excutor不仅仅是上面那些执行sql的操作,人家还管着缓存和事务的功能。Excutor再封装给sqlSession最后做统一调用。默认的是DefaultSqlSession。
最终他们这些功能处理的类,统一封装成门面也就是sqlSession暴露给程序员客户端做调用。至此为止,这就是mybatis执行类的所有内容。示意图如下。
再补充一个例子:假如我们来设计mybatis的类结构,我们在封装MappedStatement的时候会怎么做呢?
我的第一反应就是,我把mapper.xml这个文件解析了,然后封装成为一个个的小对象,MappedStatement里面持有一个这个小对象的集合。这个小对象就是包含了这些sql标签,我可能会直接把标签上面的属性和sql文本都封装到一起,一个大类。
但是mybatis设计的时候他把文本单独拆出去了,又组合了一部分的属性,参数,这样在后面解析sql的时候对象的职责就隔离的很开,改动的时候不会改了这里,影响到哪里,做到了单依职责的解耦。这一点需要我们注意。
@Test
public void test1() throws IOException {
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
List<User> userList = userDao.findAll();
}
我们上面分析了一大堆,最后知道了过程是sqlSession调用excutor以及后面的一堆handler处理的整个执行流程。那么问题来了,我实际写代码的时候是上面那两行,我调用的是我userDAO里面的findAll自己定义的方法,我这个方法是怎么和你的那些东西扯上关系的呢,而且你有没有发现一件事情。我的dao一直就是一个接口,也没有实现类,接口最后是怎么实现的呢?此时需要引出一个东西,叫做代理模式,后面我写一下静态代理和动态代理的东西。