源码分析 | 基于jdbc实现一个Demo版的Mybatis
作者 | 付政委
博客 | https://bugstack.cn
微信公众号:bugstack虫洞栈 | 博客:https://bugstack.cn
沉淀、分享、成长,专注于原创专题案例,以最易学习编程的方式分享知识,让自己和他人都能有所收获。目前已完成的专题有;Netty4.x实战专题案例、用Java实现JVM、基于JavaAgent的全链路监控、手写RPC框架、架构设计专题案例、源码分析等。
你用剑🗡、我用刀🔪,好的代码都很烧😏,望你不吝出招💨!
目录
一、前言介绍二、案例工程三、环境配置四、代码讲述1. 先测试下整个DemoJdbc框架2. 加载XML配置文件3. 解析XML配置文件4. 创建DefaultSqlSessionFactory5. 开启SqlSession6. 执行SQL语句7. Sql查询补充说明五、综上总结六、文末惊喜
一、前言介绍
在前面一篇分析了 mybatis 源码,从它为什么之后接口但是没有实现类就能执行数据库操作为入口,整个源码核心流程完全解释了一遍。对于一个3年以上的程序员来说,新知识的学习过程应该是从最开始 helloworld 到熟练使用 api 完成业务功能。下一步为了深入了解就需要阅读部分核心源码,从而在出问题后可以快速定位,迅速排查。从而减少线上事故的持续时长,提升个人影响力。但!这不是学习终点,因为无论是任何一个框架的源码,如果只是看那么就很难学习到它的实用技术。纸上得来终觉浅,唯有实战和操练。
那么,本章节我们去简单实现一个基于jdbc的demo版本Mybatis,从而更加清楚这样框架的设计。与此同时这份思想会让你可以在其他场景使用,比如给ES查询写一个EsBatis。实现了心情也好了;
二、案例工程
扩展上一篇源码分析工程;itstack-demo-mybatis,增加 like 包,模仿 Mybatis 工程。完整规程下载,关注公众号:bugstack虫洞栈 | 回复:源码分析
1itstack-demo-mybatis
2└── src
3 ├── main
4 │ ├── java
5 │ │ └── org.itstack.demo
6 │ │ ├── dao
7 │ │ │ ├── ISchool.java
8 │ │ │ └── IUserDao.java
9 │ │ ├── like
10 │ │ │ ├── Configuration.java
11 │ │ │ ├── DefaultSqlSession.java
12 │ │ │ ├── DefaultSqlSessionFactory.java
13 │ │ │ ├── Resources.java
14 │ │ │ ├── SqlSession.java
15 │ │ │ ├── SqlSessionFactory.java
16 │ │ │ ├── SqlSessionFactoryBuilder.java
17 │ │ │ └── SqlSessionFactoryBuilder.java
18 │ │ └── interfaces
19 │ │ ├── School.java
20 │ │ └── User.java
21 │ ├── resources
22 │ │ ├── mapper
23 │ │ │ ├── School_Mapper.xml
24 │ │ │ └── User_Mapper.xml
25 │ │ ├── props
26 │ │ │ └── jdbc.properties
27 │ │ ├── spring
28 │ │ │ ├── mybatis-config-datasource.xml
29 │ │ │ └── spring-config-datasource.xml
30 │ │ ├── logback.xml
31 │ │ ├── mybatis-config.xml
32 │ │ └── spring-config.xml
33 │ └── webapp
34 │ └── WEB-INF
35 └── test
36 └── java
37 └── org.itstack.demo.test
38 ├── ApiLikeTest.java
39 ├── MybatisApiTest.java
40 └── SpringApiTest.java
三、环境配置
JDK1.8
IDEA 2019.3.1
dom4j 1.6.1
四、代码讲述
关于整个 Demo 版本,并不是把所有 Mybatis 全部实现一遍,而是拨丝抽茧将最核心的内容展示给你,从使用上你会感受一模一样,但是实现类已经全部被替换,核心类包括;
Configuration
DefaultSqlSession
DefaultSqlSessionFactory
Resources
SqlSession
SqlSessionFactory
SqlSessionFactoryBuilder
XNode
1. 先测试下整个DemoJdbc框架
ApiLikeTest.test_queryUserInfoById()
1@Test
2public void test_queryUserInfoById() {
3 String resource = "spring/mybatis-config-datasource.xml";
4 Reader reader;
5 try {
6 reader = Resources.getResourceAsReader(resource);
7 SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
8 SqlSession session = sqlMapper.openSession();
9
10 try {
11 User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);
12 System.out.println(JSON.toJSONString(user));
13 } finally {
14 session.close();
15 reader.close();
16 }
17 } catch (Exception e) {
18 e.printStackTrace();
19 }
20}
一切顺利结果如下(新人往往会遇到各种问题);
1{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}
2
3Process finished with exit code 0
可能乍一看这测试类完全和 MybatisApiTest.java 测试的代码一模一样呀,也看不出区别。其实他们的引入的包是不一样;
MybatisApiTest.java 里面引入的包
1import org.apache.ibatis.io.Resources;
2import org.apache.ibatis.session.SqlSession;
3import org.apache.ibatis.session.SqlSessionFactory;
4import org.apache.ibatis.session.SqlSessionFactoryBuilder;
ApiLikeTest.java 里面引入的包
1import org.itstack.demo.like.Resources;
2import org.itstack.demo.like.SqlSession;
3import org.itstack.demo.like.SqlSessionFactory;
4import org.itstack.demo.like.SqlSessionFactoryBuilder;
好!接下来我们开始分析这部分核心代码。
2. 加载XML配置文件
这里我们采用 mybatis 的配置文件结构进行解析,在不破坏原有结构的情况下,最大可能的贴近源码。mybatis 单独使用的使用的时候使用了两个配置文件;数据源配置、Mapper 映射配置,如下;
mybatis-config-datasource.xml & 数据源配置
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
3 "http://mybatis.org/dtd/mybatis-3-config.dtd">
4
5<configuration>
6 <environments default="development">
7 <environment id="development">
8 <transactionManager type="JDBC"/>
9 <dataSource type="POOLED">
10 <property name="driver" value="com.mysql.jdbc.Driver"/>
11 <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack?useUnicode=true"/>
12 <property name="username" value="root"/>
13 <property name="password" value="123456"/>
14 </dataSource>
15 </environment>
16 </environments>
17
18 <mappers>
19 <mapper resource="mapper/User_Mapper.xml"/>
20 <mapper resource="mapper/School_Mapper.xml"/>
21 </mappers>
22
23</configuration>
User_Mapper.xml & Mapper 映射配置
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3<mapper namespace="org.itstack.demo.dao.IUserDao">
4
5 <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User">
6 SELECT id, name, age, createTime, updateTime
7 FROM user
8 where id = #{id}
9 </select>
10
11 <select id="queryUserList" parameterType="org.itstack.demo.po.User" resultType="org.itstack.demo.po.User">
12 SELECT id, name, age, createTime, updateTime
13 FROM user
14 where age = #{age}
15 </select>
16
17</mapper>
这里的加载过程与 mybaits 不同,我们采用 dom4j 方式。在案例中会看到最开始获取资源,如下;
ApiLikeTest.test_queryUserInfoById() & 部分截取
1String resource = "spring/mybatis-config-datasource.xml";
2 Reader reader;
3 try {
4 reader = Resources.getResourceAsReader(resource);
5 ...
从上可以看到这是通过配置文件地址获取到了读取流的过程,从而为后面解析做基础。首先我们先看 Resources 类,整个是我们的资源类。
Resources.java & 资源类
1/**
2 * 公众号 | bugstack虫洞栈
3 * 博 客 | https://bugstack.cn
4 * Create by 小傅哥 @2020
5 */
6public class Resources {
7
8 public static Reader getResourceAsReader(String resource) throws IOException {
9 return new InputStreamReader(getResourceAsStream(resource));
10 }
11
12 private static InputStream getResourceAsStream(String resource) throws IOException {
13 ClassLoader[] classLoaders = getClassLoaders();
14 for (ClassLoader classLoader : classLoaders) {
15 InputStream inputStream = classLoader.getResourceAsStream(resource);
16 if (null != inputStream) {
17 return inputStream;
18 }
19 }
20 throw new IOException("Could not find resource " + resource);
21 }
22
23 private static ClassLoader[] getClassLoaders() {
24 return new ClassLoader[]{
25 ClassLoader.getSystemClassLoader(),
26 Thread.currentThread().getContextClassLoader()};
27 }
28
29}
这段代码方法的入口是getResourceAsReader,直到往下以此做了;
获取 ClassLoader 集合,最大限度搜索配置文件
通过 classLoader.getResourceAsStream 读取配置资源,找到后立即返回,否则抛出异常
3. 解析XML配置文件
配置文件加载后开始进行解析操作,这里我们也仿照 mybatis 但进行简化,如下;
1SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
SqlSessionFactoryBuilder.build() & 入口构建类
1public DefaultSqlSessionFactory build(Reader reader) {
2 SAXReader saxReader = new SAXReader();
3 try {
4 Document document = saxReader.read(new InputSource(reader));
5 Configuration configuration = parseConfiguration(document.getRootElement());
6 return new DefaultSqlSessionFactory(configuration);
7 } catch (DocumentException e) {
8 e.printStackTrace();
9 }
10 return null;
11}
通过读取流创建 xml 解析的 Document 类
parseConfiguration 进行解析 xml 文件,并将结果设置到配置类中,包括;连接池、数据源、mapper关系
SqlSessionFactoryBuilder.parseConfiguration() & 解析过程
1private Configuration parseConfiguration(Element root) {
2 Configuration configuration = new Configuration();
3 configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
4 configuration.setConnection(connection(configuration.dataSource));
5 configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
6 return configuration;
7}
在前面的 xml 内容中可以看到,我们需要解析出数据库连接池信息 datasource,还有数据库语句映射关系 mappers
SqlSessionFactoryBuilder.dataSource() & 解析出数据源
1private Map<String, String> dataSource(List<Element> list) {
2 Map<String, String> dataSource = new HashMap<>(4);
3 Element element = list.get(0);
4 List content = element.content();
5 for (Object o : content) {
6 Element e = (Element) o;
7 String name = e.attributeValue("name");
8 String value = e.attributeValue("value");
9 dataSource.put(name, value);
10 }
11 return dataSource;
12}
这个过程比较简单,只需要将数据源信息获取即可
SqlSessionFactoryBuilder.connection() & 获取数据库连接
1private Connection connection(Map<String, String> dataSource) {
2 try {
3 Class.forName(dataSource.get("driver"));
4 return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
5 } catch (ClassNotFoundException | SQLException e) {
6 e.printStackTrace();
7 }
8 return null;
9}
这个就是jdbc最原始的代码,获取了数据库连接池
SqlSessionFactoryBuilder.mapperElement() & 解析SQL语句
1private Map<String, XNode> mapperElement(List<Element> list) {
2 Map<String, XNode> map = new HashMap<>();
3 Element element = list.get(0);
4 List content = element.content();
5 for (Object o : content) {
6 Element e = (Element) o;
7 String resource = e.attributeValue("resource");
8 try {
9 Reader reader = Resources.getResourceAsReader(resource);
10 SAXReader saxReader = new SAXReader();
11 Document document = saxReader.read(new InputSource(reader));
12 Element root = document.getRootElement();
13 //命名空间
14 String namespace = root.attributeValue("namespace");
15 // SELECT
16 List<Element> selectNodes = root.selectNodes("select");
17 for (Element node : selectNodes) {
18 String id = node.attributeValue("id");
19 String parameterType = node.attributeValue("parameterType");
20 String resultType = node.attributeValue("resultType");
21 String sql = node.getText();
22 // ? 匹配
23 Map<Integer, String> parameter = new HashMap<>();
24 Pattern pattern = Pattern.compile("(#\\{(.*?)})");
25 Matcher matcher = pattern.matcher(sql);
26 for (int i = 1; matcher.find(); i++) {
27 String g1 = matcher.group(1);
28 String g2 = matcher.group(2);
29 parameter.put(i, g2);
30 sql = sql.replace(g1, "?");
31 }
32 XNode xNode = new XNode();
33 xNode.setNamespace(namespace);
34 xNode.setId(id);
35 xNode.setParameterType(parameterType);
36 xNode.setResultType(resultType);
37 xNode.setSql(sql);
38 xNode.setParameter(parameter);
39
40 map.put(namespace + "." + id, xNode);
41 }
42 } catch (Exception ex) {
43 ex.printStackTrace();
44 }
45 }
46 return map;
47}
这个过程首先包括是解析所有的sql语句,目前为了测试只解析 select 相关
所有的 sql 语句为了确认唯一,都是使用;namespace + select中的id进行拼接,作为 key,之后与sql一起存放到 map 中。
在 mybaits 的 sql 语句配置中,都有占位符,用于传参。where id = #{id} 所以我们需要将占位符设置为问号,另外需要将占位符的顺序信息与名称存放到 map 结构,方便后续设置查询时候的入参。
4. 创建DefaultSqlSessionFactory
最后将初始化后的配置类 Configuration,作为参数进行创建 DefaultSqlSessionFactory,如下;
1public DefaultSqlSessionFactory build(Reader reader) {
2 SAXReader saxReader = new SAXReader();
3 try {
4 Document document = saxReader.read(new InputSource(reader));
5 Configuration configuration = parseConfiguration(document.getRootElement());
6 return new DefaultSqlSessionFactory(configuration);
7 } catch (DocumentException e) {
8 e.printStackTrace();
9 }
10 return null;
11}
DefaultSqlSessionFactory.java & SqlSessionFactory的实现类
1public class DefaultSqlSessionFactory implements SqlSessionFactory {
2
3 private final Configuration configuration;
4
5 public DefaultSqlSessionFactory(Configuration configuration) {
6 this.configuration = configuration;
7 }
8
9 @Override
10 public SqlSession openSession() {
11 return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
12 }
13
14}
这个过程比较简单,构造函数只提供了配置类入参
实现 SqlSessionFactory 的 openSession(),用于创建 DefaultSqlSession,也就可以执行 sql 操作
5. 开启SqlSession
1SqlSession session = sqlMapper.openSession();
上面这一步就是创建了DefaultSqlSession,比较简单。如下;
1@Override
2public SqlSession openSession() {
3 return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
4}
6. 执行SQL语句
1User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);
在 DefaultSqlSession 中通过实现 SqlSession,提供数据库语句查询和关闭连接池,如下;
SqlSession.java & 定义
1public interface SqlSession {
2
3 <T> T selectOne(String statement);
4
5 <T> T selectOne(String statement, Object parameter);
6
7 <T> List<T> selectList(String statement);
8
9 <T> List<T> selectList(String statement, Object parameter);
10
11 void close();
12}
接下来看具体的执行过程,session.selectOne
DefaultSqlSession.selectOne() & 执行查询
1public <T> T selectOne(String statement, Object parameter) {
2 XNode xNode = mapperElement.get(statement);
3 Map<Integer, String> parameterMap = xNode.getParameter();
4 try {
5 PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
6 buildParameter(preparedStatement, parameter, parameterMap);
7 ResultSet resultSet = preparedStatement.executeQuery();
8 List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
9 return objects.get(0);
10 } catch (Exception e) {
11 e.printStackTrace();
12 }
13 return null;
14}
selectOne 就objects.get(0);,selectList 就全部返回
通过 statement 获取最初解析 xml 时候的存储的 select 标签信息;
1<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User">
2 SELECT id, name, age, createTime, updateTime
3 FROM user
4 where id = #{id}
5</select>获取 sql 语句后交给 jdbc 的 PreparedStatement 类进行执行
这里还需要设置入参,我们将入参设置进行抽取,如下;
1private void buildParameter(PreparedStatement preparedStatement, Object parameter, Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException {
int size = parameterMap.size();
}
2// 单个参数
3if (parameter instanceof Long) {
4 for (int i = 1; i <= size; i++) {
5 preparedStatement.setLong(i, Long.parseLong(parameter.toString()));
6 }
7 return;
8}
9
10if (parameter instanceof Integer) {
11 for (int i = 1; i <= size; i++) {
12 preparedStatement.setInt(i, Integer.parseInt(parameter.toString()));
13 }
14 return;
15}
16
17if (parameter instanceof String) {
18 for (int i = 1; i <= size; i++) {
19 preparedStatement.setString(i, parameter.toString());
20 }
21 return;
22}
23
24Map<String, Object> fieldMap = new HashMap<>();
25// 对象参数
26Field[] declaredFields = parameter.getClass().getDeclaredFields();
27for (Field field : declaredFields) {
28 String name = field.getName();
29 field.setAccessible(true);
30 Object obj = field.get(parameter);
31 field.setAccessible(false);
32 fieldMap.put(name, obj);
33}
34
35for (int i = 1; i <= size; i++) {
36 String parameterDefine = parameterMap.get(i);
37 Object obj = fieldMap.get(parameterDefine);
38
39 if (obj instanceof Short) {
40 preparedStatement.setShort(i, Short.parseShort(obj.toString()));
41 continue;
42 }
43
44 if (obj instanceof Integer) {
45 preparedStatement.setInt(i, Integer.parseInt(obj.toString()));
46 continue;
47 }
48
49 if (obj instanceof Long) {
50 preparedStatement.setLong(i, Long.parseLong(obj.toString()));
51 continue;
52 }
53
54 if (obj instanceof String) {
55 preparedStatement.setString(i, obj.toString());
56 continue;
57 }
58
59 if (obj instanceof Date) {
60 preparedStatement.setDate(i, (java.sql.Date) obj);
61 }
62
63}
64单个参数比较简单直接设置值即可,Long、Integer、String …
如果是一个类对象,需要通过获取 Field 属性,与参数 Map 进行匹配设置
设置参数后执行查询 preparedStatement.executeQuery()
接下来需要将查询结果转换为我们的类(主要是反射类的操作),resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
1private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {
2 List<T> list = new ArrayList<>();
3 try {
4 ResultSetMetaData metaData = resultSet.getMetaData();
5 int columnCount = metaData.getColumnCount();
6 // 每次遍历行值
7 while (resultSet.next()) {
8 T obj = (T) clazz.newInstance();
9 for (int i = 1; i <= columnCount; i++) {
10 Object value = resultSet.getObject(i);
11 String columnName = metaData.getColumnName(i);
12 String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
13 Method method;
14 if (value instanceof Timestamp) {
15 method = clazz.getMethod(setMethod, Date.class);
16 } else {
17 method = clazz.getMethod(setMethod, value.getClass());
18 }
19 method.invoke(obj, value);
20 }
21 list.add(obj);
22 }
23 } catch (Exception e) {
24 e.printStackTrace();
25 }
26 return list;
27}主要通过反射生成我们的类对象,这个类的类型定义在 sql 标签上
时间类型需要判断后处理,Timestamp,与 java 不是一个类型
7. Sql查询补充说明
sql 查询有入参、有不需要入参、有查询一个、有查询集合,只需要合理包装即可,例如下面的查询集合,入参是对象类型;
ApiLikeTest.test_queryUserList()
1@Test
2public void test_queryUserList() {
3 String resource = "spring/mybatis-config-datasource.xml";
4 Reader reader;
5 try {
6 reader = Resources.getResourceAsReader(resource);
7 SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
8 SqlSession session = sqlMapper.openSession();
9
10 try {
11 User req = new User();
12 req.setAge(18);
13 List<User> userList = session.selectList("org.itstack.demo.dao.IUserDao.queryUserList", req);
14 System.out.println(JSON.toJSONString(userList));
15 } finally {
16 session.close();
17 reader.close();
18 }
19 } catch (Exception e) {
20 e.printStackTrace();
21 }
22
23}
测试结果:
1[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}]
2
3Process finished with exit code 0
五、综上总结
学习完 Mybaits 核心源码,再实现一下核心过程,那么就会很清晰这个过程是怎么个流程,也就不会觉得自己知识栈有漏洞
只有深入的学习才能将这样的技术赋能于其他开发上,例如给ES增加这样查询包,让ES更加容易操作。其实还可以有很多创造
知识往往是综合的使用,将各个知识点综合起来使用,才能更加熟练。不要总看不做,否则全套的流程不能在自己脑子流程下什么印象
六、文末惊喜
小傅哥 | 沉淀、分享、成长,让自己和他人都能有所收获!