MyBatis框架简单的手写与实现,今天我来为大家介绍一下我的思路:
1.准备工作
我是从MyBatis本体应用层的使用入手的,在使用MyBatis时我们需要有对应的Config文件,如下:
SqlConfig.xml
XML
<configuration>
<environments default="development">
<environment id="development">
<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?characterEncoding=utf8&useUnicode=true&serverTimezone=GMT%2B8&useSSL=false"/>
<property name="username" value="你的数据库用户名"/>
<property name="password" value="你的数据库密码"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/djw/mapper/StudentMapper.xml"></mapper>
<mapper class="com/djw/mapper/StudentDao"></mapper>
</mappers>
</configuration>
这里面我已经加入了相应的功能配置项标签;
在main方法中是这样的:
Test
java
package com.djw.test;
import com.djw.mapper.StudentMapper;
import com.djw.model.Student;
import com.mysql.cj.conf.PropertyDefinitions;
import com.djw.util.*;
import java.io.InputStream;
import java.util.List;
public class Test {
public static void main(String[] args) {
InputStream inputStream = Resources.getResourceAsStream("SqlConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = mapper.selectList();
students.forEach(System.out::println);
sqlSession.close();
}
}
针对main方法的操作流程我们进行相应代码的编写
已知的我们需要解析XML文件,访问数据库进行jdbc等操作所以我在Maven下的pom文件配置如下:
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>MyBatis-day03-custom</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--解析xml文件-->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!--解析xpath-->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.2.0</version>
</dependency>
<!--取消从父工程引入的mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.7</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
</project>
我们还需要创建对应的实体类来方便进行ORM
Student
java
package com.djw.model;
import lombok.Data;
/**
* @author djw
*/
@Data
public class Student {
private int id;
private String name;
private int age;
private String gender;
}
我们要实现MyBatis对应的mapper功能,所以我们需要编写在使用MyBatis时用到的Mapper接口和xml文件
StudentMapper
java
package com.djw.mapper;
import com.djw.model.Student;
import java.util.List;
/**
* @author djw
*/
public interface StudentMapper {
List<Student> selectList();
}
StudentMapper.xml
XML
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.djw.mapper.StudentMapper">
<select id="selectList" resultType="com.djw.model.Student">
select * from student;
</select>
</mapper>
2.编写Util类
我们知道在使用MyBatis时需要用到SqlSession获取mapper,具体的流程是SqlSessionFactoryBuilder---build(InputStream)--->SqlSessionFactory---openSession()--->SqlSession所以我们就编写这三个功能
SqlSessionFactoryBuilder
java
package com.djw.util;
import java.io.InputStream;
/**
* @author djw
*/
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream in) {
try {
DbProfile dbProfile = XMLParser.parseXml(in);
return new CostumSqlSessionFactory(dbProfile);
}catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
在上一节中,我们提到了 SqlSessionFactoryBuilder 会调用 XMLParser.parseXml(in) 来读取配置。接下来,我们需要实现这个解析器,以及它所依赖的数据模型。
2.1 定义配置模型 DbProfile 和 Mapper
为了方便存储 XML 中读取出来的数据,我们需要定义两个简单的 POJO 类。
DbProfile.java (用于存储数据库连接信息和 Mapper 集合)
java
package com.djw.util;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class DbProfile {
private String driver;
private String url;
private String username;
private String password;
// 关键点:这里存储了所有的 SQL 映射信息
// Key: 接口全限定名 + "." + 方法名 (例如: com.djw.mapper.StudentMapper.selectList)
// Value: 对应的 SQL 语句和返回类型
private Map<String, Mapper> mappers = new HashMap<>();
}
Mapper.java (用于存储单条 SQL 的信息)
1package com.djw.util;
2
3import lombok.Data;
4
5@Data
6public class Mapper {
7 // 存储 SQL 语句
8 private String sqlStatement;
9 // 存储返回值类型 (全限定类名)
10 private String className;
11}
2.2 实现 XML 解析器 XMLParser
这是 MyBatis 的"心脏"起搏器,负责将 XML 文件中的字符串转化为 Java 对象。
1package com.djw.util;
2
3import org.dom4j.Document;
4import org.dom4j.DocumentException;
5import org.dom4j.Element;
6import org.dom4j.io.SAXReader;
7
8import java.io.InputStream;
9import java.util.List;
10import java.util.Map;
11
12public class XMLParser {
13
14 /**
15 * 解析 mybatis-config.xml 和 mapper.xml 文件
16 */
17 public static DbProfile parseXml(InputStream inputStream) throws DocumentException {
18 DbProfile profile = new DbProfile();
19 SAXReader reader = new SAXReader();
20 Document document = reader.read(inputStream);
21 Element rootElement = document.getRootElement();
22
23 // 1. 解析 environments 标签,获取数据库连接信息
24 Element dataSource = (Element) rootElement
25 .selectSingleNode("//dataSource"); // 使用 XPath 简化查找
26
27 if (dataSource != null) {
28 profile.setDriver(dataSource.elementTextTrim("driver"));
29 profile.setUrl(dataSource.elementTextTrim("url"));
30 profile.setUsername(dataSource.elementTextTrim("username"));
31 profile.setPassword(dataSource.elementTextTrim("password"));
32 }
33
34 // 2. 解析 mappers 标签,加载所有的 Mapper XML 文件
35 List<Element> mapperElements = rootElement.selectNodes("//mappers/mapper");
36 for (Element mapperElement : mapperElements) {
37 String resource = mapperElement.attributeValue("resource");
38 if (resource != null) {
39 // 调用方法解析具体的 Mapper 文件
40 parseMapperResource(resource, profile);
41 }
42 }
43 return profile;
44 }
45
46 /**
47 * 解析具体的 Mapper.xml 文件
48 */
49 private static void parseMapperResource(String resourcePath, DbProfile profile) {
50 try {
51 InputStream mapperStream = Resources.getResourceAsStream(resourcePath);
52 SAXReader reader = new SAXReader();
53 Document document = reader.read(mapperStream);
54 Element root = document.getRootElement();
55
56 // 获取命名空间 (通常是接口的全限定名)
57 String namespace = root.attributeValue("namespace");
58
59 // 解析 <select> 标签
60 List<Element> selectNodes = root.selectNodes("//select");
61 for (Element select : selectNodes) {
62 String id = select.attributeValue("id"); // 方法名
63 String resultType = select.attributeValue("resultType"); // 返回类型
64 String sql = select.getTextTrim(); // SQL 语句
65
66 // 组装 Key
67 String key = namespace + "." + id;
68 Mapper mapper = new Mapper();
69 mapper.setSqlStatement(sql);
70 mapper.setClassName(resultType);
71
72 // 存入配置对象
73 profile.getMappers().put(key, mapper);
74 }
75 } catch (Exception e) {
76 e.printStackTrace();
77 }
78 }
79}
3. 实现核心执行流程
解析完配置后,我们需要创建 SqlSessionFactory 和 SqlSession,这是用户与数据库交互的入口。
3.1 创建 SqlSessionFactory
1package com.djw.util;
2
3public interface SqlSessionFactory {
4 SqlSession openSession();
5}
CostumSqlSessionFactory.java (实现类)
1package com.djw.util;
2
3public class CostumSqlSessionFactory implements SqlSessionFactory {
4
5 // 持有解析好的配置信息
6 private final DbProfile dbProfile;
7
8 public CostumSqlSessionFactory(DbProfile dbProfile) {
9 this.dbProfile = dbProfile;
10 }
11
12 @Override
13 public SqlSession openSession() {
14 // 每次打开会话,都创建一个新的连接和执行器
15 return new CostumSqlSession(dbProfile);
16 }
17}
3.2 实现 SqlSession 与 动态代理
这是最精彩的部分。我们知道在 MyBatis 中,我们只需要写接口,不需要写实现类。这是怎么做到的?答案是 JDK 动态代理。
SqlSession.java (接口)
1package com.djw.util;
2
3public interface SqlSession {
4 <T> T getMapper(Class<T> clazz);
5 void close();
6}
CostumSqlSession.java (核心实现)
1package com.djw.util;
2
3import java.lang.reflect.Proxy;
4import java.sql.Connection;
5
6public class CostumSqlSession implements SqlSession {
7
8 private final DbProfile dbProfile;
9 private final Connection connection;
10
11 public CostumSqlSession(DbProfile dbProfile) {
12 this.dbProfile = dbProfile;
13 // 初始化数据库连接
14 this.connection = DBUtil.getConnection(dbProfile);
15 }
16
17 @Override
18 public <T> T getMapper(Class<T> mapperInterfaceClass) {
19 // 动态代理:拦截接口方法的调用
20 return (T) Proxy.newProxyInstance(
21 mapperInterfaceClass.getClassLoader(),
22 new Class[]{mapperInterfaceClass},
23 // 传入一个 InvocationHandler,即我们的 ProxyImpl
24 new ProxyImpl(dbProfile.getMappers(), connection)
25 );
26 }
27
28 @Override
29 public void close() {
30 if (connection != null) {
31 try {
32 connection.close();
33 } catch (Exception e) {
34 e.printStackTrace();
35 }
36 }
37 }
38}
3.3 编写代理处理器 ProxyImpl
当我们在测试代码中调用 mapper.selectList() 时,实际上会跳转到这里。
1package com.djw.util;
2
3import org.slf4j.Logger;
4import org.slf4j.LoggerFactory;
5
6import java.lang.reflect.InvocationHandler;
7import java.lang.reflect.Method;
8import java.sql.Connection;
9import java.util.Map;
10
11public class ProxyImpl implements InvocationHandler {
12
13 private static final Logger log = LoggerFactory.getLogger(ProxyImpl.class);
14
15 private final Map<String, Mapper> mapperMap;
16 private final Connection connection;
17
18 public ProxyImpl(Map<String, Mapper> mapperMap, Connection connection) {
19 this.mapperMap = mapperMap;
20 this.connection = connection;
21 }
22
23 @Override
24 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
25 // 1. 拼接 Key: 接口全限定名 + 方法名
26 String key = method.getDeclaringClass().getName() + "." + method.getName();
27
28 log.debug("正在执行方法: " + key);
29
30 // 2. 根据 Key 获取 SQL 信息
31 Mapper mapper = mapperMap.get(key);
32 if (mapper == null) {
33 throw new RuntimeException("未找到对应的 SQL 映射: " + key);
34 }
35
36 // 3. 执行 SQL (这里简化处理,只处理查询)
37 // 实际上这里应该根据 SQL 的类型(insert/update/select)来调用不同的 DBUtil 方法
38 return DBUtil.selectList(mapper, connection);
39 }
40}
4. 封装 JDBC 工具类 DBUtil
最后,我们需要一个工具类来处理底层的 JDBC 操作,包括加载驱动、获取连接、处理结果集。
1package com.djw.util;
2
3import java.sql.*;
4
5public class DBUtil {
6
7 public static Connection getConnection(DbProfile profile) {
8 try {
9 Class.forName(profile.getDriver());
10 return DriverManager.getConnection(
11 profile.getUrl(),
12 profile.getUsername(),
13 profile.getPassword()
14 );
15 } catch (Exception e) {
16 e.printStackTrace();
17 return null;
18 }
19 }
20
21 /**
22 * 简单的查询列表实现(简化版)
23 */
24 public static Object selectList(Mapper mapper, Connection conn) {
25 try {
26 // 1. 准备 Statement
27 PreparedStatement ps = conn.prepareStatement(mapper.getSqlStatement());
28
29 // 2. 执行查询
30 ResultSet rs = ps.executeQuery();
31
32 // 3. 这里为了简化,假设我们只需要打印结果
33 // 实际的 MyBatis 会通过反射将 rs 映射为 mapper.getClassName() 对应的实体类
34 ResultSetMetaData metaData = rs.getMetaData();
35 int columnCount = metaData.getColumnCount();
36
37 while (rs.next()) {
38 for (int i = 1; i <= columnCount; i++) {
39 System.out.print(metaData.getColumnName(i) + ": " + rs.getObject(i) + " ");
40 }
41 System.out.println();
42 }
43
44 // 注意:这里只是演示流程,实际应返回映射后的 List 对象
45 return null;
46 } catch (Exception e) {
47 e.printStackTrace();
48 return null;
49 }
50 }
51}
5. 总结
运行流程回顾:
- 启动:
Test.main调用SqlSessionFactoryBuilder.build()。 - 解析:
XMLParser读取SqlConfig.xml和StudentMapper.xml,将数据库配置和 SQL 语句存入DbProfile。 - 创建工厂:
SqlSessionFactory拿到DbProfile。 - 打开会话:
SqlSession.openSession()创建数据库连接。 - 获取代理:
SqlSession.getMapper()利用 JDK 动态代理生成接口的实现类(ProxyImpl)。 - 执行: 调用
mapper.selectList()-> 被代理拦截 -> 拼接 Key -> 从 Map 中查找 SQL ->DBUtil执行 JDBC -> 返回结果。
1. SAXReader (DOM4J) 与 XPath ------ 配置的"读取器"
在 MyBatis 启动时,它需要读取 SqlMapConfig.xml 和 Mapper.xml 里的数据库连接信息和 SQL 语句。
- SAXReader:这是 DOM4J 库中的核心类。它的作用是将 XML 文件解析成一棵"树"(Document Object Model)。
- XPath :这是一门在 XML 文档中查找信息的语言。在代码中,我们使用
selectSingleNode("//dataSource")或selectNodes("//select")。这就是 XPath 语法,它能让我们像写 SQL 一样精准地从 XML 树中提取节点,而不需要我们手动去遍历整个树结构。 - 用途 :在
XMLParser类中,我们用它把 XML 字符串变成了 Java 对象(如DbProfile和Mapper)。
2. 注解(Annotation)与反射(Reflection)------ 代码的"元数据"与"窥探者"
MyBatis 的注解(如 @Select)和 XML 其实是两种不同的配置存储方式。要让程序"看到"代码上的注解,必须用到反射。
2.1 RetentionPolicy 与生命周期
Java 注解有三个生命周期阶段,定义了注解保留到什么时候:
表格
| 类型 | 说明 | 生命周期范围 | 使用场景 |
|---|---|---|---|
| SOURCE | 源码级 | 只在 .java 文件中保留,编译时被丢弃。 |
仅用于编译时检查(如 @Override)。 |
| CLASS | 字节码级 | 保留在 .class 文件中,但 JVM 加载时通常丢弃。 |
一般很少用,某些字节码处理工具使用。 |
| RUNTIME | 运行时级 | 保留在 .class 文件中,并且在 JVM 运行时仍然存在。 |
MyBatis 必须用这个,因为程序运行时需要通过反射读取 SQL。 |
在框架中的应用:
我们在定义 @Select 时,必须写 @Retention(RetentionPolicy.RUNTIME)。如果写成 SOURCE 或 CLASS,当 XMLParser 运行 method.isAnnotationPresent(Select.class) 时,是找不到这个注解的。
2.2 反射机制(Reflection)
反射是 Java 的"自省"能力,它允许程序在运行时:
- 加载类 :
Class.forName(className) - 获取方法 :
clazz.getMethods() - 判断注解 :
method.isAnnotationPresent(Select.class) - 获取注解值 :
method.getAnnotation(Select.class).value()
用途 :在 XMLParser 的 parseMapperAnnotation 方法中,我们利用反射加载 Mapper 接口,遍历其方法,读取 @Select 里的 SQL 字符串,并将其塞进 Mapper 对象中。
3. PropertyDescriptor 与 getWriteMethod() ------ 结果集的"自动填充器"
这是手写 ORM(对象关系映射)中最核心、也是最巧妙的部分。我们在 DBUtil 的 selectList 方法中用到了它。
3.1 为什么要用它?
JDBC 查询出来的结果是 ResultSet(结果集),它本质上是一个表格(行和列)。而我们需要的是 Java 对象(Object)。
我们需要把数据库的一行数据(如 id=1, name='Tom')自动映射到一个 Java 对象(Student)的属性中。
3.2 它们的关系与功能
想象一下,如果我们不用框架,我们需要这样写:
1Student s = new Student();
2s.setId(1); // 手动调用 setter
3s.setName("Tom");
在手写框架中,我们不知道具体的类是 Student 还是 User,所以我们不能写死 s.setId。我们需要动态 地找到并调用这个 setId 方法。
这就是 PropertyDescriptor 的作用:
-
PropertyDescriptor(属性描述器):- 功能:它是一个桥梁,连接了"属性名"和"getter/setter 方法"。
- 原理 :Java Bean 规范规定,如果有一个属性叫
name,那么它通常对应一个方法setName(setter) 和getName(getter)。 - 用法 :
new PropertyDescriptor("name", clazz)。它能帮我们通过属性名找到对应的方法对象。
-
getWriteMethod()(写方法):- 功能 :
PropertyDescriptor有两个方法:getWriteMethod():获取 Setter 方法(用于赋值)。getReadMethod():获取 Getter 方法(用于取值)。
- 关系 :我们用
PropertyDescriptor封装了"属性名 -> 方法名"的关系,然后调用它的getWriteMethod()拿到Method对象,最后通过method.invoke(obj, value)利用反射执行这个 Setter 方法。
- 功能 :
3.3 在 DBUtil 中的完整流程
结合你手写框架中的代码,这段逻辑的执行流如下:
- 遍历结果集 :
ResultSet rs有一行数据。 - 获取元数据 :
ResultSetMetaData告诉我们列名(如"name")。 - 实例化对象 :
E obj = (E) clazz.getConstructor().newInstance(null);(new 一个空对象)。 - 核心映射循环 :
- 获取列名 :
String columnName = meta.getColumnName(i);(例如 "name") - 获取列值 :
Object columnValue = rs.getObject(i);(例如 "Tom") - 建立桥梁 :
PropertyDescriptor descriptor = new PropertyDescriptor(columnName, clazz);- 此时,descriptor 知道了:属性名是 "name",它应该去找 setName 方法。
- 获取 Setter :
Method method = descriptor.getWriteMethod();- 此时,method 就是 Student 类中的 setName 方法。
- 执行赋值 :
method.invoke(obj, columnValue);- 相当于执行了:
student.setName("Tom");
- 相当于执行了:
- 获取列名 :
针对一些工具类的总结:
1. Document(整本书)
- 是什么 :
Document代表整个 XML 文档。当你使用SAXReader读取完一个 XML 文件后,内存中生成的就是这棵完整的"树"。 - 作用 :它是所有节点的根容器。就像一本书包含了封面、目录、所有章节和页码一样,
Document包含了 XML 里的所有标签、属性和文本。 - 获取方式 :通常通过
new SAXReader().read(inputStream)得到。
2. Element(具体的章节、段落或文字)
- 是什么 :
Element代表 XML 中的一个节点(标签)。 - 作用 :它是
Document树上的具体组成部分。比如<configuration>是根元素,<dataSource>是子元素,<property>是更深层的子元素。 - 关系 :
Document是由无数个嵌套的Element组成的。你可以通过getRootElement()拿到书的"第一章"(根节点),也可以通过父Element去获取它的子Element。
3. selectNodes(全书内容检索/精准定位)
- 是什么 :
selectNodes是Document和Element都具备的一个核心方法 。它的作用是接收 XPath 语法作为参数,返回符合条件的Element列表(List<Node>)。 - 作用 :它就像书的**"智能搜索引擎"** 。
- 如果你对着
Document调用selectNodes("//property"),相当于在全书 中搜索所有叫property的段落。 - 如果你对着某个特定的
Element(比如<dataSource>)调用selectNodes("property"),相当于只在当前章节 里查找property。
- 如果你对着
它们在实际代码中的联动关系
结合你手写 MyBatis 的 XMLParser 场景,它们的协作流程是这样的:
-
SAXReader 生成 Document :
首先,
SAXReader把SqlConfig.xml文件读入内存,生成一个Document对象(整本书)1Document document = reader.read(inputStream); -
Document 获取根 Element :
我们需要先找到这本书的入口,也就是根标签
<configuration>1Element root = document.getRootElement(); // 拿到了 <configuration> 这个 Element -
Element 调用 selectNodes 精准查找 :
现在我们已经站在
<configuration>这个节点上了,我们想找到它下面所有的<property>标签来提取数据库账号密码1// 从 root 元素出发,利用 XPath 查找所有 property 子节点 2List<Element> list = root.selectNodes("//property"); -
遍历 Element 提取数据 :
最后,遍历找到的这些
Element,提取出我们想要的属性值1for(Element element : list) { 2 // element 就是每一个具体的 <property> 标签 3 String name = element.attributeValue("name"); 4 String value = element.attributeValue("value"); 5}
总结一下:
SAXReader 负责把 XML 变成内存中的 Document(整棵树);Document 和 Element 是树上的节点(整体与局部);而 selectNodes 是我们手里拿着 XPath 地图,在 Document 或 Element 上快速定位并抓取目标节点的神器。