实现一个微型的 Mybatis-配置文件版
# 50.实现一个微型的 Mybatis-配置文件版
实现一个微型的 Mybatis
我们入门案例中,主要用到了 Mybatis 的这些类:
- class Resources
- class SqlSessionFactoryBuilder
- interface SqlSessionFactory
- interface SqlSession
本文我们就自己实现上述类,来做一个微型的 Mybatis,能够通过读取配置文件或者注解的方式,来封装 SQL 并执行;本文我们实现读取配置文件的方式来实现 Mybatis,下一节在讲读取注解的方式。
# 整体步骤
我们这里先说下整体的设计思路,让读者心里有个大纲,以免迷失在后面的细节里:
- 删除 Mybatis 的依赖
- 创建 Resources、SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession 接口、Mapper 等类
- 使用工具类
XMLConfigBuilder
读取配置文件,Executor
类执行 selectList 方法,DataSourceUtil
返回数据库连接池对象
建议读者在项目 Git 分支 demo1 的基础上,跟着本文一起自己做一遍。
# 依赖梳理
这里我们删除 Mybatis 的依赖,并引入读取 XML 文件的依赖 dom4j 和 jaxen
<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
</dependencies>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 删除 Mybatis 配置文件的约束
由于我们不再使用 Mybatis 了,所以删除 XML 文件中的相关约束:
IUserDao.xml:
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.peterjxl.dao.IUserDao">
<!-- 配置查询所有用户,id要写方法名称-->
<select id="findAll" resultType="com.peterjxl.domain.User">
select * from user
</select>
</mapper>
2
3
4
5
6
7
SqlMapConfig.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!-- Mybatis的主配置文件 -->
<configuration>
<!--配置环境-->
<environments default="mysql">
<environment id="mysql">
<!-- 配置事务的类型 -->
<transactionManager type="JDBC"/>
<!-- 配置数据源(连接池) -->
<dataSource type="POOLED">
<!-- 配置连接数据库的4个基本信息 -->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///LearnMybatis"/>
<property name="username" value="LearnMybatisUser"/>
<property name="password" value="LearnMybatisUserPassword"/>
</dataSource>
</environment>
</environments>
<!--
指定映射配置文件的位置,映射配置文件指的是每个dao独立的配置文件
如果是用注解来配置的话,此处应该使用class属性指定被注解的dao全限定类名
-->
<mappers>
<mapper resource="com/peterjxl/dao/IUserDao.xml"/>
<!-- <mapper class="com.peterjxl.dao.IUserDao"/>-->
</mappers>
</configuration>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 创建 Resources
package com.peterjxl.mybatis.io;
import java.io.InputStream;
/**
* 使用类加载器读取配置文件的类
*/
public class Resources {
/**
* 根据传入的参数,获取一个字节输入流
* @param filePath
* @return
*/
public static InputStream getResourceAsStream(String filePath){
return Resources.class.getClassLoader().getResourceAsStream(filePath);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
我们首先要做的就是读取配置文件,因此我们创建 Resources 类,返回一个输入流。
# 创建 SqlSessionFactoryBuilder
读取配置文件后,下一步就是 new 一个构建者类,这里我们先返回一个 null,后续完善
package com.peterjxl.mybatis.sqlsession;
import java.io.InputStream;
/**
* 用于创建一个SqlSessionFactory对象
*/
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream config){
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 创建 SqlSessionFactory
接口
有了工厂的 builder 后,下一步就是创建工厂,该工厂有一个 openSession 方法,用来返回一个 SqlSession。
package com.peterjxl.mybatis.sqlsession;
public interface SqlSessionFactory {
/**
* 用于打开一个新的SqlSession对象
*/
SqlSession openSession();
}
2
3
4
5
6
7
8
9
10
11
# 创建 SqlSession
接口
package com.peterjxl.mybatis.sqlsession;
/**
* 自定义Mybatis中和数据库交互的核心类,可以创建dao接口的代理对象
*/
public interface SqlSession {
<T> T getMapper(Class<T> daoInterfaceClass);
/**
* 是否资源
*/
void close();
}
2
3
4
5
6
7
8
9
10
11
12
13
# 修改测试类
至此,我们修改测试类所引入的 class 为自定义的,此时代码应该是没有报错的。
package com.peterjxl.test;
import com.peterjxl.dao.IUserDao;
import com.peterjxl.domain.User;
import com.peterjxl.mybatis.io.Resources;
import com.peterjxl.mybatis.sqlsession.SqlSession;
import com.peterjxl.mybatis.sqlsession.SqlSessionFactory;
import com.peterjxl.mybatis.sqlsession.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;
public class MybatisTest {
@Test
public void helloMybatis() throws Exception{
// 1. 读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2. 创建SqlSessionFactory工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
// 3. 使用工厂生成SqlSession对象
SqlSession session = factory.openSession();
// 4. 使用SqlSession创建Dao接口的代理对象
IUserDao userDao = session.getMapper(IUserDao.class);
// 5. 使用代理对象执行方法
List<User> users = userDao.findAll();
for(User user : users){
System.out.println(user);
}
// 6. 释放资源
session.close();
in.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 使用解析 XML 的工具类
接下来,我们就应该是读取 XML 配置文件的内容,来创建对象了。由于解析 XML 并不是本节课的重点,这里直接使用工具类:XMLConfigBuilder
,读者可以在我的 Git 项目上下载,该工具类主要分为 3 个方法:
loadConfiguration
:用来读取配置文件,加载数据源信息;同时读取 mappers 标签,并判断其使用了 resource 还是 class 属性- 如果使用的是 resource 属性,说明用的是 XML,则调用
loadMapperConfiguration
方法,该方法会通过读取 XML 文件的内容,生成 Mapper 对象,并存储到一个 Map 中 - 如果使用的是 class 属性,说明用的是注解,则调用
loadMapperAnnotation
方法,该方法会通过反射来获取接口的所有方法,并判断是否有 select 注解,然后获取属性值,生成 Mapper 对象,并存储到一个 Map 中。由于本文我们使用的是读取配置文件,因此loadMapperAnnotation
方法可以先注释掉
工具类 XMLConfigBuilder
用到了不少其他工具类,例如
Configuration
对象,用来存储数据库连接信息,以及存储 Mapper 的 map 对象Mapper
对象,存储要查询的 SQL,以及实体类的全限定类名SqlSession
对象.- ............
这里我们逐一创建。
# 创建 Configuration 类
package com.peterjxl.mybatis.cfg;
/**
* 自定义Mybatis的配置类
*/
public class Configuration {
private String driver;
private String url;
private String username;
private String password;
private Map<String, Mapper> mappers = new HashMap<String, Mapper>();
public Map<String, Mapper> getMappers() {
return mappers;
}
public void setMappers(Map<String, Mapper> mappers) {
this.mappers.putAll(mappers);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
注意的是,XMLConfigBuilder
工具类会调用 Configuration
对象的 setMappers 方法,表明添加一个 mapper 对象,因此这里使用的是 map 的 putAll 方法。
请读者自行创建其他的 getter 和 setter
# 创建 Mapper 类
之前我们说过,Mapper 应该包含两个部分:
- 第一:执行的 SQL 语句
- 第二:封装结果的实体类全限定类名。
package com.peterjxl.mybatis.cfg;
/**
* 用于封装执行的SQL语句和结果类型的全限定类名
*/
public class Mapper {
private String queryString; // SQL
private String resultType; //实体类的全限定类名
}
2
3
4
5
6
7
8
请读者自行创建 getter 和 setter
# 创建 DefaultSqlSessionFactory
我们之前创建的 SqlSessionFactory
是一个接口,现在我们创建其实现类
package com.peterjxl.mybatis.sqlsession.defaults;
import com.peterjxl.cfg.Configuration;
import com.peterjxl.mybatis.sqlsession.SqlSession;
import com.peterjxl.mybatis.sqlsession.SqlSessionFactory;
/**
* SqlSessionFactory的实现类
*/
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration cfg;
public DefaultSqlSessionFactory(Configuration cfg) {
this.cfg = cfg;
}
/**
* 用于创建一个新的操作数据库对象
* @return
*/
@Override
public SqlSession openSession() {
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 修改 SqlSessionFactoryBuilder
有了 DefaultSqlSessionFactory
,然后我们就可以在 Builder 里调用它,来返回一个工厂 SqlSessionFactory
package com.peterjxl.mybatis.sqlsession;
import com.peterjxl.cfg.Configuration;
import com.peterjxl.mybatis.sqlsession.defaults.DefaultSqlSessionFactory;
import com.peterjxl.utils.XMLConfigBuilder;
import java.io.InputStream;
/**
* 用于创建一个SqlSessionFactory对象
*/
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream config){
Configuration cfg = XMLConfigBuilder.loadConfiguration(config);
return new DefaultSqlSessionFactory(cfg);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然后我们就可以通过 SqlSessionFactory,来返回一个 Session,然后通过 Session 来获取一个代理对象,查询数据库了。
# 小结
创建了这么多类,我们先暂停下,梳理下类之间的关系,我们从测试类 MybatisTest
的执行顺序来说明:
public void helloMybatis() throws Exception{
// 1. 读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2. 创建SqlSessionFactory工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
// 3. 使用工厂生成SqlSession对象
SqlSession session = factory.openSession();
// 4. 使用SqlSession创建Dao接口的代理对象
IUserDao userDao = session.getMapper(IUserDao.class);
// 5. 使用代理对象执行方法
List<User> users = userDao.findAll();
for(User user : users){
System.out.println(user);
}
// 6. 释放资源
session.close();
in.close();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
步骤说明:
- 首先,我们创建了 Resources 类,该类用来读取配置文件,返回了一个输入流
- 然后我们创建了 SqlSessionFactoryBuilder,其中有个 build 方法,调用了工具类
XMLConfigBuilder
读取 XML 配置文件,并返回DefaultSqlSessionFactory
工厂 - 在
DefaultSqlSessionFactory
工厂中,调用 openSession 方法,返回的是默认的DefaultSqlSession
对象 DefaultSqlSession
关键的就是要返回代理对象,也就是我们接下来要做的事情。
看上去创建的类很多,实际上只要通过执行的过程来梳理,还是有迹可循的。
# 创建 DataSourceUtils 工具类
我们新建一个 DataSourceUtils 类,用来返回一个 Connection 对象
package com.peterjxl.mybatis.utils;
import com.peterjxl.cfg.Configuration;
import java.sql.Connection;
import java.sql.DriverManager;
public class DataSourceUtil {
public static Connection getConnection(Configuration cfg){
try {
Class.forName(cfg.getDriver());
return DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());
} catch (Exception e) {
throw new RuntimeException("获取数据源异常!");
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建 DefaultSqlSession
类
接下来,我们就可以创建 DefaultSqlSession
类,并返回一个代理对象了。
我们将增强的方法挪到一个新的类里去实现:MapperProxy
package com.peterjxl.mybatis.sqlsession.defaults;
import com.peterjxl.cfg.Configuration;
import com.peterjxl.mybatis.sqlsession.SqlSession;
import com.peterjxl.mybatis.sqlsession.proxy.MapperProxy;
import com.peterjxl.utils.DataSourceUtil;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
/**
* SqlSession的实现类
*/
public class DefaultSqlSession implements SqlSession {
private Configuration cfg;
private Connection connection;
public DefaultSqlSession(Configuration cfg) {
this.cfg = cfg;
connection = DataSourceUtil.getConnection(cfg);
}
/**
* 用于创建代理对象
* @param daoInterfaceClass
* @return
* @param <T>
*/
@Override
public <T> T getMapper(Class<T> daoInterfaceClass) {
Object o = Proxy.newProxyInstance(
daoInterfaceClass.getClassLoader(),
new Class[]{daoInterfaceClass},
new MapperProxy(cfg.getMappers(), connection) {
});
return (T) o;
}
/**
* 用于释放资源
*/
@Override
public void close() throws SQLException {
connection.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 创建 MapperProxy
接下来我们创建 MapperProxy,该类主要是拼接全类名+方法名,来返回 map 集合里的 mapper
package com.peterjxl.mybatis.sqlsession.proxy;
import com.peterjxl.cfg.Mapper;
import com.peterjxl.utils.Executor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Map;
public class MapperProxy implements InvocationHandler {
private Map<String, Mapper> mappers;
private Connection conn;
public MapperProxy(Map<String, Mapper> mappers, Connection conn) {
this.mappers = mappers;
this.conn = conn;
}
/**
* 用来对方法进行增强,我们的增强就是调用selectList方法
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1。获取方法名
String methodName = method.getName();
// 2. 获取方法所在类的名称
String className = method.getDeclaringClass().getName();
// 3. 组合key
String key = className + '.' + methodName;
// 4。 获取Mapper对象
Mapper mapper = mappers.get(key);
// 5. 判断是否有mapper
if( null == mapper){
throw new IllegalArgumentException("传入的参数有误");
}
// 6. 调用工具类,查询所有
return new Executor().selectList(mapper, conn);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 创建工具类 Executor
为了简单起见,这里提供一个工具类 MapperProxy
,封装了 selectList 方法
package com.peterjxl.utils;
import com.peterjxl.cfg.Mapper;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;
/**
* 负责执行SQL语句,并且封装结果集
*/
public class Executor {
public <E> List<E> selectList(Mapper mapper, Connection conn) {
PreparedStatement pstm = null;
ResultSet rs = null;
try {
//1.取出mapper中的数据
String queryString = mapper.getQueryString();//select * from user
String resultType = mapper.getResultType();//com.peterjxl.domain.User
Class domainClass = Class.forName(resultType);
//2.获取PreparedStatement对象
pstm = conn.prepareStatement(queryString);
//3.执行SQL语句,获取结果集
rs = pstm.executeQuery();
//4.封装结果集
List<E> list = new ArrayList<E>();//定义返回值
while(rs.next()) {
//实例化要封装的实体类对象
E obj = (E)domainClass.newInstance();
//取出结果集的元信息:ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
//取出总列数
int columnCount = rsmd.getColumnCount();
//遍历总列数
for (int i = 1; i <= columnCount; i++) {
//获取每列的名称,列名的序号是从1开始的
String columnName = rsmd.getColumnName(i);
//根据得到列名,获取每列的值
Object columnValue = rs.getObject(columnName);
//给obj赋值:使用Java内省机制(借助PropertyDescriptor实现属性的封装)
PropertyDescriptor pd = new PropertyDescriptor(columnName,domainClass);//要求:实体类的属性和数据库表的列名保持一种
//获取它的写入方法
Method writeMethod = pd.getWriteMethod();
//把获取的列的值,给对象赋值
writeMethod.invoke(obj,columnValue);
}
//把赋好值的对象加入到集合中
list.add(obj);
}
return list;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
release(pstm,rs);
}
}
private void release(PreparedStatement pstm,ResultSet rs){
if(rs != null){
try {
rs.close();
}catch(Exception e){
e.printStackTrace();
}
}
if(pstm != null){
try {
pstm.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# 修改实体类 User
由于我们数据库中,user 表的 birthday 列,用的是 DateTime 类型;而我们自己写的工具类中,并没有做太智能的封装,因此我们将 User 类中的 date 类型改为 LocalDatetime 类型
public class User implements Serializable {
private Integer id;
private String username;
private LocalDateTime birthday;
private String sex;
private String address;
}
2
3
4
5
6
7
请读者自行修改 birthday 的 getter 和 setter
# 测试
然后我们运行测试方法,可以看到能正常查询数据库。
使用的是XML
User{id=41, username='张三', birthday=2018-02-27T17:47:08, sex='男', address='北京'}
User{id=42, username='李四', birthday=2018-03-02T15:09:37, sex='女', address='北京'}
User{id=43, username='王五', birthday=2018-03-04T11:34:34, sex='女', address='北京'}
User{id=45, username='赵六', birthday=2018-03-04T12:04:06, sex='男', address='北京'}
User{id=46, username='小七', birthday=2018-03-07T17:37:26, sex='男', address='北京'}
User{id=48, username='老八', birthday=2018-03-08T11:44, sex='男', address='北京'}
2
3
4
5
6
7
8
# 总结
我们梳理下本文所有的类之间的关系。
XMLConfigBuilder
类,用来读取配置文件,设置数据源信息,mappers 的信息Configuration
类,用来存储数据源信息,一个存储所有 mapper 的 map。DataSourceUtil
类:用来加载驱动,返回 Connection 对象Executor
:根据 mapper 里的信息,执行 SQL 并返回 ListMapper
:存储了查询 SQL,以及要封装的实体类的全限定类名Resources
:返回一个读取配置文件的 IO 流SqlSessionFactoryBuilder
:构建者,用来构建工厂的DefaultSqlSessionFactory
:默认的构建者SqlSessionFactory
:接口,有一个openSession
方法,返回SqlSession
DefaultSqlSessionFactory
:工厂类的实现类,返回一个SqlSession
SqlSession
:接口,返回一个代理对象DefaultSqlSession
:SqlSession 的实现类,用来返回一个代理对象MapperProxy
:代理对象的具体业务逻辑,主要是获取 mapper,然后调用Executor
类的 selectList 方法
IDEA 一览图:
本文自己简单实现了一个小型的 Mybatis 框架,所有代码已上传到了 GitHub (opens new window) 和 Gitee (opens new window) 上,并且创建了分支 demo4,读者可以通过切换分支来查看本文的示例代码。