其实很早就想写一篇 iBatis 的源码分析了, 不过有段时间去学习 Go 了, Java 就放下了, 最近 重新捡起 Java 就把以前没填的坑,填一下.
Init
现在开始正片.
首先是 iBatis 的初始化工作.我们看下面的代码:
1// `BlogDataSourceFactory`的主要作用: 通过你的配置文件, 初始化一个DataSource
2DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
3// JdbcTransactionFactory一个New就能得到, 没什么依赖条件
4TransactionFactory transactionFactory = new JdbcTransactionFactory();
5// Environment要你交出数据源和事务工厂还有你的环境是开发还是生产
6Environment environment = new Environment("development", transactionFactory, dataSource);
7// Configuration有基本上你所有的配置
8Configuration configuration = new Configuration(environment);
9// 添加你的mapper到配置列表中, 等会我们去分析它
10configuration.addMapper(BlogMapper.class);
11// 通过你的配置类,让我们初始化一个SqlSessionFactory! 我们终于进入正题了!!
12// 可能你觉得很快... 其实本人在这里面分析还是花了很长时间
13SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
好, 上文有说configuration.addMapper(BlogMapper.class)这个方法, 现在我们来分析一下它.
1
2 // 这个是Configuration中的方法, 它实际上是委托mapperRegistry去执行
3 public <T> void addMapper(Class<T> type) {
4 mapperRegistry.addMapper(type);
5 }
6
7 public <T> void addMapper(Class<T> type) {
8 //mapper必须是接口
9 if (type.isInterface()) {
10 if (hasMapper(type)) {
11 //如果重复添加了,报错
12 throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
13 }
14 boolean loadCompleted = false;
15 try {
16 // 加入一个Mapper的代理生产工厂
17 knownMappers.put(type, new MapperProxyFactory<T>(type));
18 // It's important that the type is added before the parser is run
19 // otherwise the binding may automatically be attempted by the
20 // mapper parser. If the type is already known, it won't try.
21 // 这个是通过注解来构建Mapper, 暂时不看
22 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
23 parser.parse();
24 loadCompleted = true;
25 } finally {
26 //如果加载过程中出现异常需要再将这个mapper从mybatis中删除
27 if (!loadCompleted) {
28 knownMappers.remove(type);
29 }
30 }
31 }
32 }
SqlSessionFactory
既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。 SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。 你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。
1try (SqlSession session = sqlSessionFactory.openSession()) {
2 BlogMapper mapper = session.getMapper(BlogMapper.class);
3 Blog blog = mapper.selectBlog(101);
4}
好! 我们快进到曹丕sqlSessionFactory.openSession()
1 public SqlSession openSession() {
2 return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
3 }
4
5 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
6 // Transaction事务,包装了一个Connection, 包含commit,rollback,close方法
7 Transaction tx = null;
8 try {
9 // 还记得么, environment里封装了我们的数据源;事务工厂;还有环境
10 final Environment environment = configuration.getEnvironment();
11 // 得到一个事务工厂, 如果env或者env里的事务工厂是空的就返回一个托管事务工厂
12 // 托管事务工厂的特点就是每次执行完成SQL都会关闭连接, 如果你不希望关闭连接要在配置文件里设置它
13 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
14 //通过事务工厂来产生一个事务
15 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
16 //生成一个执行器(事务包含在执行器里)
17 final Executor executor = configuration.newExecutor(tx, execType);
18 //然后产生一个DefaultSqlSession
19 return new DefaultSqlSession(configuration, executor, autoCommit);
20 } catch (Exception e) {
21 //如果打开事务出错,则关闭它
22 closeTransaction(tx); // may have fetched a connection so lets call close()
23 throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
24 } finally {
25 //最后清空错误上下文
26 ErrorContext.instance().reset();
27 }
28 }
这样我们就得到了一个SqlSession.
2020.10.12 继续更新
有了SqlSession之后我们就可以操作数据库了。
我们来看看MyBatis是怎么实现session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);的。
1 public <T> T selectOne(String statement, Object parameter) {
2 // Popular vote was to return null on 0 results and throw exception on too many.
3 //转而去调用selectList,很简单的,如果得到0条则返回null,得到1条则返回1条,得到多条报TooManyResultsException错
4 // 特别需要主要的是当没有查询到结果的时候就会返回null。因此一般建议在mapper中编写resultType的时候使用包装类型
5 //而不是基本类型,比如推荐使用Integer而不是int。这样就可以避免NPE
6 List<T> list = this.<T>selectList(statement, parameter);
7 if (list.size() == 1) {
8 return list.get(0);
9 } else if (list.size() > 1) {
10 throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
11 } else {
12 return null;
13 }
14 }
15
16 // emm,这里其实啥都没有,我们去SelectList看看。
17
18 // 在下来解释一下这三个参数:
19 // statement 映射语句的位置,比如"org.mybatis.example.BlogMapper.selectBlog"
20 // parameter SQL语句中的参数
21 // RowBounds 分页限制,相当于SQL中的limit.
22 public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
23 try {
24 //根据statement id找到对应的MappedStatement
25 MappedStatement ms = configuration.getMappedStatement(statement);
26 //转而用执行器来查询结果,注意这里传入的ResultHandler是null
27 // wrapCollection:如果参数是Collection类型,转换成Map,key为parameter的type.
28 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
29 } catch (Exception e) {
30
31 throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
32 } finally {
33 ErrorContext.instance().reset();
34 }
35 }
36
37
38 // 接下来是执行器的部分
39 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
40 //得到绑定sql,就是将参数插入映射语句里,获得完整的SQL
41 BoundSql boundSql = ms.getBoundSql(parameter);
42 //创建缓存Key
43 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
44 //查询
45 return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
46 }
47
48 // 执行查询
49 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
50 // ErrorContext 是每个线程单独使用的错误上下文,它是用ThreadLocal制作的.
51 ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
52 //如果已经关闭,报错
53 if (closed) {
54 throw new ExecutorException("Executor was closed.");
55 }
56 //先清局部缓存,再查询.但仅查询堆栈为0,才清。为了处理递归调用
57 if (queryStack == 0 && ms.isFlushCacheRequired()) {
58 clearLocalCache();
59 }
60 List<E> list;
61 try {
62 //加一,这样递归调用到上面的时候就不会再清局部缓存了
63 queryStack++;
64 //先根据cachekey从localCache去查
65 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
66 if (list != null) {
67 //若查到localCache缓存,处理localOutputParameterCache
68 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
69 } else {
70 //从数据库查
71 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
72 }
73 } finally {
74 //清空堆栈
75 queryStack--;
76 }
77 if (queryStack == 0) {
78 //延迟加载队列中所有元素
79 for (DeferredLoad deferredLoad : deferredLoads) {
80 deferredLoad.load();
81 }
82
83 // issue #601
84 //清空延迟加载队列
85 deferredLoads.clear();
86 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
87 // issue #482
88 //如果是STATEMENT,清本地缓存
89 clearLocalCache();
90 }
91 }
92 return list;
93 }
以上是SqlSession.selectOne的流程。然而实际中我们直接使用SqlSession来执行数据库操作的情况很少。
大多数情况我们会这样使用MyBatis:
1try (SqlSession session = sqlSessionFactory.openSession()) {
2 BlogMapper mapper = session.getMapper(BlogMapper.class);
3 Blog blog = mapper.selectBlog(101);
4}
这个方法的详细过程我们使用MyBatis的单元测试来探索。
单元测试代码:
1 @Test
2 public void shouldSelectBlogWithPostsUsingSubSelect() throws Exception {
3 SqlSession session = sqlSessionFactory.openSession();
4 try {
5 BoundBlogMapper mapper = session.getMapper(BoundBlogMapper.class);
6 Blog b = mapper.selectBlogWithPostsUsingSubSelect(1);
7 assertEquals(1, b.getId());
8 session.close();
9 assertNotNull(b.getAuthor());
10 assertEquals(101, b.getAuthor().getId());
11 assertEquals("jim", b.getAuthor().getUsername());
12 assertEquals("********", b.getAuthor().getPassword());
13 assertEquals(2, b.getPosts().size());
14 } finally {
15 session.close();
16 }
17 }
快进到session.getMapper
1 //返回代理类
2 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
3 // 直接得到该类型Mapper代理工厂
4 final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
5 // 没有就离谱
6 if (mapperProxyFactory == null) {
7 throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
8 }
9 try {
10 // 通过当前Session生产一个代理
11 return mapperProxyFactory.newInstance(sqlSession);
12 } catch (Exception e) {
13 throw new BindingException("Error getting mapper instance. Cause: " + e, e);
14 }
15 }
实际上生产Mapper的逻辑并不多。
主要是执行代理方法时的动作。
执行mapper.selectBlogWithPostsUsingSubSelect(1);的逻辑如下:
1 // 由于是代理生成的,所以调用方法后会进入一下逻辑:
2 @Override
3 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
4 // 如果这个方法是来自Object,就直接执行,直接返回
5 if (Object.class.equals(method.getDeclaringClass())) {
6 try {
7 return method.invoke(this, args);
8 } catch (Throwable t) {
9 throw ExceptionUtil.unwrapThrowable(t);
10 }
11 }
12
13 // 去缓存中找MapperMethod,第一次的话会new一个
14 final MapperMethod mapperMethod = cachedMapperMethod(method);
15 //执行本体
16 return mapperMethod.execute(sqlSession, args);
17 }
下面就是整个代理的执行数据库操作的逻辑,比较长:
1 public Object execute(SqlSession sqlSession, Object[] args) {
2 Object result;
3 //可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法
4 if (SqlCommandType.INSERT == command.getType()) {
5 Object param = method.convertArgsToSqlCommandParam(args);
6 result = rowCountResult(sqlSession.insert(command.getName(), param));
7 } else if (SqlCommandType.UPDATE == command.getType()) {
8 Object param = method.convertArgsToSqlCommandParam(args);
9 result = rowCountResult(sqlSession.update(command.getName(), param));
10 } else if (SqlCommandType.DELETE == command.getType()) {
11 Object param = method.convertArgsToSqlCommandParam(args);
12 result = rowCountResult(sqlSession.delete(command.getName(), param));
13 } else if (SqlCommandType.SELECT == command.getType()) {
14 // 我们执行的是查询,直接跳到这里
15 if (method.returnsVoid() && method.hasResultHandler()) {
16 // 检查是不是没有返回值以及结果处理器: 我们执行的是查询,是有返回值的
17 executeWithResultHandler(sqlSession, args);
18 result = null;
19 } else if (method.returnsMany()) {
20 //如果结果有多条记录:我们只查一条
21 result = executeForMany(sqlSession, args);
22 } else if (method.returnsMap()) {
23 //如果结果是map:我们查的只是个对象
24 result = executeForMap(sqlSession, args);
25 } else {
26 //否则就是一条记录
27 // 我们仔细分析这个convertArgsToSqlCommandParam方法
28 Object param = method.convertArgsToSqlCommandParam(args);
29 // 之后我们又回到了SelectOne这个方法。
30 result = sqlSession.selectOne(command.getName(), param);
31 }
32 } else {
33 throw new BindingException("Unknown execution method for: " + command.getName());
34 }
35 if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
36 throw new BindingException("Mapper method '" + command.getName()
37 + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
38 }
39 return result;
40 }
41
42
43 // 将参数转换为SQL命令参数
44 public Object convertArgsToSqlCommandParam(Object[] args) {
45 // 这里有一个坑:
46 // args 是Mapper方法执行的参数
47 // param 是编写的SQL语句所需要的命令参数
48 // 它们有什么不同呢:
49 // 在MyBatis中你需要使用分页时可以不显式在SQL语句中使用limit命令
50 // 使用RowBounds对象作为Mapper的额外参数来做到数据分页
51 // 该参数不用在SQL语句中显式使用.
52 final int paramCount = params.size();
53 if (args == null || paramCount == 0) {
54 //如果没参数
55 return null;
56 } else if (!hasNamedParameters && paramCount == 1) {
57 //如果只有一个参数
58 return args[params.keySet().iterator().next().intValue()];
59 } else {
60 //否则,返回一个ParamMap,修改参数名,参数名就是其位置
61 final Map<String, Object> param = new ParamMap<Object>();
62 int i = 0;
63 for (Map.Entry<Integer, String> entry : params.entrySet()) {
64 //1.先加一个#{0},#{1},#{2}...参数
65 param.put(entry.getValue(), args[entry.getKey().intValue()]);
66 // issue #71, add param names as param1, param2...but ensure backward compatibility
67 final String genericParamName = "param" + String.valueOf(i + 1);
68 if (!param.containsKey(genericParamName)) {
69 //2.再加一个#{param1},#{param2}...参数
70 //你可以传递多个参数给一个映射器方法。如果你这样做了,
71 //默认情况下它们将会以它们在参数列表中的位置来命名,比如:#{param1},#{param2}等。
72 //如果你想改变参数的名称(只在多参数情况下) ,那么你可以在参数上使用@Param(“paramName”)注解。
73 param.put(genericParamName, args[entry.getKey()]);
74 }
75 i++;
76 }
77 return param;
78 }
79 }
OK, 我基本想说的都说完了。后续可能会额外补充一些内容,但是不会在本文中,会写新文章。