King's Studio

自定义MyBatis框架

字数统计: 3.6k阅读时长: 16 min
2019/07/27 Share

MyBatis是一个持久层的框架,使用Java语言书写的,它封装了很多jdbc操作的细节,使开发者只用关注SQL语句本身,无需关注注册驱动,创建连接等过程,它使用ORM思想实现结果集的封装。下面我们讨论一下细节。(注:ORM,Object Relational Mapping对象关系映射,含义:将数据库表和Java实体类及实体类的属性对应,使我们操作实体类就可以操作数据库表,做到这点需要实体类中的属性和数据库表的字段名保持一致。)

从MyBatis的使用看

想知道MyBatis内部的工作流程,我们从使用MyBatis进行查询所有的过程看,首先看配置MyBatis的环境,分为以下几步:

创建Maven工程导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<!--引入mybatis依赖(后面在手动实现过程中将依赖注释掉)-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!--mysql连接驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!--dom4j解析xml-->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>

创建实体类和dao层接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.jinqi.dao;

import com.jinqi.domain.User;
import com.jinqi.mybatis.annotations.Select;

import java.util.List;

/**
* 用户的持久层接口
*/
public interface UserDao {

/**
* 查询所有操作
* @return
*/
@Select("select * from user")//注解直接写在dao接口上
List<User> findAll();
}

创建MyBatis的主配置文件SqlMapConfig.xml

1
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
<?xml version="1.0" encoding="UTF-8" ?>
<!--mybatis的主配置文件-->
<configuration>
<!--配置环境-->
<environments default="mysql">
<!--配置mysql的环境-->
<environment id="mysql">
<!--配置事务的类型-->
<transactionManager type="JDBC"></transactionManager>
<!--配置数据源(连接池)-->
<dataSource type="POOLED">
<!--配置连接数据库的四个基本信息-->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/MyBatis"/>
<property name="username" value="root"/>
<property name="password" value="Jq576163960"/>
</dataSource>
</environment>
</environments>

<!--指定映射配置文件的位置,映射配置文件指的是每个dao独立的配置文件
如果使用注解来配置,此处应该使用class属性指定被注解的dao接口的全限定类名
-->
<mappers>
<!--注解的方式-->
<mapper class="com.jinqi.dao.UserDao"></mapper>

<!--xml映射配置文件的写法-->
<!--<mapper resource="com/jinqi/dao/UserDao.xml"></mapper>-->
</mappers>
</configuration>

创建映射配置文件UserDao.xml

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>

<mapper namespace="com.jinqi.dao.UserDao">
<!--配置查询所有-->
<select id="findAll" resultType="com.jinqi.domain.User">
select * from user
</select>
</mapper>

下面我们编写测试类具体看执行的过程:

1
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
package com.jinqi.test;

import com.jinqi.dao.UserDao;
import com.jinqi.domain.User;
import com.jinqi.mybatis.sqlsession.SqlSession;
import com.jinqi.mybatis.sqlsession.SqlSessionFactory;
import com.jinqi.mybatis.sqlsession.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.util.List;

public class MyBatisTest {

/**
* 测试Mybatis
* @param args
*/
public static void main(String[] args) throws Exception{

//1、读取配置文件
InputStream in = com.jinqi.mybatis.io.Resources.getResourceAsStream("SqlMapConfig.xml");
//2、创建SqlSessionFactory工厂(SqlSessionFactory是接口,不能直接创建对象,使用SqlSessionFactoryBuilder)
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
//3、使用工厂创建SqlSession对象
SqlSession session = factory.openSession();
//4、使用SqlSession创建dao接口的代理对象
UserDao userDao = session.getMapper(UserDao.class);
//5、使用代理对象执行方法(创建持久层接口的对象
List<User> userList = userDao.findAll();
for (User user: userList) {
System.out.println(user);
}
//6、释放资源
session.close();
in.close();
}
}

分析代码执行过程开始自定义MyBatis

1.读取SqlMapConfig.xml配置文件方式:

  • 使用类加载器,缺点是只能读取类路径下的文件
  • 使用ServletContext对象的getRealPath

2.创建SqlSessionFactory,使用了构建者模式,利用SqlSessionFactoryBuilder创建的builder对象就是构建者。

3.创建SqlSession对象,使用了工厂模式,取缔new关键字,让工厂帮我们创建所需的对象,降低程序间的耦合。

4.创建dao接口实现类采用了代理模式,在不修改源代码的基础上,对已有的方法进行增强。

自定义MyBatis的实现

1.SqlMapConfig.xml:标签用于配置连接信息,创建Connection对象,标签用于映射配置文件UserDao.xml。

配置

mapper

2.UserDao.xml:包含执行的select语句,获取PreparedStatement。

User

resultType:封装的实体类的全限定类名。

3.读取配置文件:这里使用的是解析xml文件的方式,使用的是dom4j进行解析xml,我们需要导入dom4j依赖。

1
2
3
4
5
6
<!--dom4j解析xml-->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>

我们创建一个Configuration实体类,用来封装连接信息,和有着sql语句与resultType的Map。

1
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
package com.jinqi.mybatis.cfg;

import java.util.HashMap;
import java.util.Map;

/**
* 自定义MyBatis的配置类
* 封装了连接信息,和有着sql语句与resultType的map
*/
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) {
//将每次遍历到的值都追加存放进map中
this.mappers.putAll(mappers);
}

public String getDriver() {
return driver;
}

public void setDriver(String driver) {
this.driver = driver;
}

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

4.首先我们需要创建一个SqlSessionFactoryBuilder类,用于创建一个SqlSessionFactory工厂对象,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.jinqi.mybatis.sqlsession;

import com.jinqi.mybatis.cfg.Configuration;
import com.jinqi.mybatis.sqlsession.defaults.DefaultSqlSessionFactory;
import com.jinqi.mybatis.utils.XMLConfigBuilder;
import java.io.InputStream;

/**
* 用于创建一个SqlSessionFactory工厂对象
*/
public class SqlSessionFactoryBuilder {

/**
* 根据参数的字节输入流来构建一个SqlSessionFactory工厂
* @param config
* @return
*/
public SqlSessionFactory build(InputStream config){
Configuration cfg = XMLConfigBuilder.loadConfiguration(config);
return new DefaultSqlSessionFactory(cfg);
}
}

在这边附上xml文件解析的工具类:

1
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package com.jinqi.mybatis.utils;

import com.jinqi.mybatis.cfg.Configuration;
import com.jinqi.mybatis.cfg.Mapper;
import com.jinqi.mybatis.io.Resources;
import com.jinqi.mybatis.annotations.Select;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
*SAXReader解析xml文件工具类
* @author ross
*/
public class XMLConfigBuilder {

/**
* 解析主配置文件,把里面的内容填充到DefaultSqlSession所需要的地方
* 使用dom4j和xpath
* @param config
* @return
*/
public static Configuration loadConfiguration(InputStream config){
try{
//定义封装连接信息的配置对象(mybatis的配置对象)
Configuration cfg = new Configuration();

//1、获取SAXReader对象
SAXReader reader = new SAXReader();

//2、根据字节输入流获取Document对象
Document document = reader.read(config);

//3、获取根节点
Element root = document.getRootElement();

//4、使用xpath中选择指定节点的方式,获取所有property节点
List<Element> propertyElements = root.selectNodes("//property");

//5、遍历节点
for (Element propertyElement : propertyElements){
//判断节点是连接数据库的哪部分信息
//取出name属性的值
String name = propertyElement.attributeValue("name");
if ("driver".equals(name)){
//表示驱动
//获取property标签value属性的值
String driver = propertyElement.attributeValue("value");
cfg.setDriver(driver);
}
if ("url".equals(name)){
//表示连接字符串
//获取property标签value属性的值
String url = propertyElement.attributeValue("value");
cfg.setUrl(url);
}
if ("username".equals(name)){
//表示用户名
//获取property标签value属性的值
String username = propertyElement.attributeValue("value");
cfg.setUsername(username);
}
if ("password".equals(name)){
//表示密码
//获取property标签value属性的值
String password = propertyElement.attributeValue("value");
cfg.setPassword(password);
}
}
//取出mappers中的所有mapper标签,判断它们使用了resource还是class属性
List<Element> mapperElements = root.selectNodes("//mappers/mapper");
//遍历集合
for (Element mapperElement:mapperElements){
//判断mapperElement使用的是哪个属性
Attribute attribute = mapperElement.attribute("resource");
if (attribute!=null){
System.out.println("使用的是xml");
//表示有resource属性,用的是xml
//取出属性的值
String mapperPath = attribute.getValue();//"com/jinqi/dao/UserDao.xml"
//把映射配置文件的内容获取出来,封装成一个Map
Map<String,Mapper> mappers = loadMapperConfiguration(mapperPath);
//给configuration中的mappers赋值
cfg.setMappers(mappers);
}else {
System.out.println("使用的是注解");
//表示没有resource属性,用的是注解
String daoClassPath = mapperElement.attributeValue("class");
//根据dalClassPath获取封装的必要信息
Map<String,Mapper> mappers = loadMapperAnnotation(daoClassPath);
//给configuration中的mappers赋值
cfg.setMappers(mappers);
}
}
return cfg;
}catch (Exception e){
throw new RuntimeException(e);
}finally {
try {
config.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
*根据传入的参数,解析xml,并且封装到map中
* @param mapperPath 映射配置文件的位置
* @return map中包含了获取的位置标识(key是由dao的全限定类名和方法名组成)
* 以及执行所必需的信息(value是一个mapper对象,里面存放的是执行的sql语句和要封装的实体类全限定类名)
* @throws IOException
*/
public static Map<String,Mapper> loadMapperConfiguration(String mapperPath) throws IOException{

InputStream in = null;
try{
//定义返回值对象
Map<String,Mapper> mappers = new HashMap<String, Mapper>();
//1、根据路径获取字节输入流
in = Resources.getResourceAsStream(mapperPath);
//2、根据字节输入流获取Document对象
SAXReader reader = new SAXReader();
Document document = reader.read(in);
//3、获取根节点(mapper)
Element root = document.getRootElement();
//4、获取根节点的namespace属性
String namespace = root.attributeValue("namespace");
//5、获取所有select节点
List<Element> selectElements = root.selectNodes("//select");
//6、遍历select节点集合
for (Element selectElement :selectElements){
//取出id属性的值 组成map中的key部分
String id = selectElement.attributeValue("id");
//取出resultType属性的值 组成map中的value部分
String resultType = selectElement.attributeValue("resultType");
//取出文本内容 组成map中的value部分
String queryString = selectElement.getText();
//创建key
String key = namespace+"."+id;
//创建value
Mapper mapper = new Mapper();
mapper.setQueryString(queryString);
mapper.setResultType(resultType);
//将key和mapper放进map中
mappers.put(key,mapper);
}
return mappers;
}catch (Exception e){
throw new RuntimeException(e);
}finally {
in.close();
}
}


/**
* 获取有select注解标注的方法
* @param daoClassPath
* @return
* @throws IOException
*/
public static Map<String,Mapper> loadMapperAnnotation(String daoClassPath) throws Exception{
//定义返回值对象
Map<String,Mapper> mappers = new HashMap<>();

//1、得到dao接口的字节码对象
Class daoClass = Class.forName(daoClassPath);
//2、得到dao接口的方法数组
Method[] methods = daoClass.getMethods();
//3、遍历Method数组
for (Method method: methods) {
//取出每一个方法,判断是否有select注解
boolean isAnnotated = method.isAnnotationPresent(Select.class);
if (isAnnotated){
//创建mapper对象
Mapper mapper = new Mapper();
//取出注解的value属性值
Select selectAnno = method.getAnnotation(Select.class);
String queryString = selectAnno.value();
mapper.setQueryString(queryString);
//获取当前方法的返回值,还要求必须带有泛型的类型
Type type = method.getGenericReturnType();//List<User>
//判断type是不是参数化的类型
if (type instanceof ParameterizedType){
//强转
ParameterizedType ptype = (ParameterizedType)type;
//得到参数化类型中的实际类型参数
Type[] types = ptype.getActualTypeArguments();
//取出第一个
Class domainClass = (Class) types[0];
//获取domainClass的类名
String resultType = domainClass.getName();
//给mapper赋值
mapper.setResultType(resultType);
}
//组装key的信息
//获取方法的名称
String methodName = method.getName();
String className = method.getDeclaringClass().getName();
String key = className+"."+methodName;
mappers.put(key,mapper);
}
}
return mappers;
}

}

5.由于我们定义的SqlSessionFactory工厂是一个接口,因此我们需要写一个SqlSessionFactory的实现类DefaultSqlSessionFactory用于创建一个新的操作数据库对象SqlSession。

1
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
package com.jinqi.mybatis.sqlsession.defaults;

import com.jinqi.mybatis.cfg.Configuration;
import com.jinqi.mybatis.sqlsession.SqlSession;
import com.jinqi.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 new DefaultSqlSession(cfg);
}
}

6.SqlSession是自定义MyBatis中和数据库交互的核心接口,它可以创建dao接口的代理对象,我们需要一个SqlSession的实现类,具体思路是根据dao接口的字节码创建dao接口的代理对象,而不是直接写dao接口的实现类,核心方法是getMapper方法,该方法在使用代理创建对象时,要传入三个参数,分别是:类加载器,使用和目标类相同的加载器;代理类要实现的接口和目标类要实现的接口相同;如何代理,即要扩展的功能,此处应该给一个InvocationHandler的实现类,在实现类中利用反射调用selectList方法。

1
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
package com.jinqi.mybatis.sqlsession.defaults;

import com.jinqi.mybatis.cfg.Configuration;
import com.jinqi.mybatis.sqlsession.SqlSession;
import com.jinqi.mybatis.sqlsession.proxy.MapperProxy;
import com.jinqi.mybatis.utils.DataSourceUtil;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;

/**
* SqlSession的实现类
* 自定义MyBatis中和数据库交互的核心类
* 它可以创建dao接口的代理对象
*/
public class DefaultSqlSession implements SqlSession {

private Configuration cfg;

private Connection conn;

public DefaultSqlSession(Configuration cfg) {
this.cfg = cfg;
conn = DataSourceUtil.getConnection(cfg);
}

/**
* 用于创建代理对象
* @param daoInterfaceClass dao的接口字节码
* @param <T>
* @return
*/
@Override
public <T> T getMapper(Class<T> daoInterfaceClass) {
//参数分别是类加载器,代理类要实现的接口的字节码数组,如何代理
return (T) Proxy.newProxyInstance(daoInterfaceClass.getClassLoader(),new Class[]{daoInterfaceClass},new MapperProxy(cfg.getMappers(),conn));
}

/**
* 释放资源
*/
@Override
public void close() {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
1
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
package com.jinqi.mybatis.sqlsession.proxy;

import com.jinqi.mybatis.cfg.Mapper;
import com.jinqi.mybatis.utils.Executor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Map;

/**
* 代理类,添加调用selectList方法
*/
public class MapperProxy implements InvocationHandler {

//map的key是映射配置文件中的全限定类名+方法名
private Map<String,Mapper> mappers;

private Connection conn;

public MapperProxy(Map<String, Mapper> mappers,Connection conn) {
this.mappers = mappers;
this.conn=conn;
}

/**
* 用于对方法进行扩展的,此处的扩展就是调用selectList方法
* @param proxy
* @param method
* @param args
* @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、获取mappers中的Mapper对象
Mapper mapper = mappers.get(key);
// 5、判断是否有mapper
if (mapper ==null){
throw new IllegalArgumentException("传入的参数有误!");
}
//6、调用工具类执行查询所有方法
return new Executor().selectList(mapper,conn);
}
}

最后我们再次执行测试类代码进行测试,同样能够得到需要的结果。

结果

最后,附上完整源代码的github地址:https://github.com/jqdelove/Customize-MyBatis.git。

原文作者:金奇

原文链接:https://www.rossontheway.com/2019/07/27/自定义MyBatis框架/

发表日期:July 27th 2019, 12:00:00 am

更新日期:July 27th 2019, 4:28:06 pm

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可,除特别声明外,转载请注明出处!

CATALOG
  1. 1. 从MyBatis的使用看
    1. 1.1. 创建Maven工程导入依赖
    2. 1.2. 创建实体类和dao层接口
    3. 1.3. 创建MyBatis的主配置文件SqlMapConfig.xml
    4. 1.4. 创建映射配置文件UserDao.xml
  2. 2. 分析代码执行过程开始自定义MyBatis
  3. 3. 自定义MyBatis的实现