手写Mybatis框架,剖析底层结构原理
一,Mybatis框架介绍
MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架,其主要就完成2件事情:
- 封装JDBC操作
- 利用反射打通Java类与SQL语句之间的相互转换
MyBatis的主要设计目的就是让我们对执行SQL语句时对输入输出的数据管理更加方便,所以方便地写出SQL和方便地获取SQL的执行结果才是MyBatis的核心竞争力。
二,前提概述
Mybatis
的用法这里简单说一下,新建Mapper
接口文件,然后在mapper.xml
配置文件中写对应的sql
相关信息。
public interface UserMapper {
User selectUserById(Long id);
List<User> selectList();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.qfcwx.mapper.UserMapper" >
<select id="selectUserById" resultType="com.qfcwx.pojo.User">
SELECT
id,username,password
FROM user
WHERE id = ?
</select>
<select id="selectList" resultType="com.qfcwx.pojo.User">
SELECT
id,username,password
FROM user
</select>
</mapper>
然后调用mapper
接口的方法,就完成了对数据库的操作,对于我们而言,操作数据库的步骤明显减少了太多,其实都是Mybatis
框架帮我们实现的。
三,动手写Mybatis框架
下面手写一个简单化的Mybatis
框架,来分析Mybatis
的底层结构以及相互调用。
本框架的思维导图
1,新建Mapper接口类和mapper.xml配置文件
如上前提概述,先写好基本使用的用法内容,然后开始构造Mybatis
框架。
mapper.xml
文件在resources
静态资源文件夹下的mapper
文件夹下,可以随意放,只要在resources
文件夹下即可,方便获取!
2,引入相关依赖
需要的依赖有两个,mysql
驱动依赖以连接数据库,dom4j
来解析mapper.xml
。
<!-- 依赖dom4j来解析mapper.xml -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
3,自定义置文件
在resources
下新建一个配置文件db.properties
,里面配置连接数据库的相关信息。
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
username=root
password=1234
4,定义Configuration对象
定义一个Configuration
对象,其中包含数据源、事务等信息。
声明:本文章的所有内容都是按照Mybatis
的基本结构构造而成,虽没有源框架那样庞大,但底层结构完全吻合!
public class Configuration {
private String driver; //数据库驱动
private String url; //连接路径
private String userName; //用户名
private String passWord; //密码
//用于存放db.properties和mapper.xml配置文件的信息
private Map<String, MappedStatement> statementMap = new HashMap<String, MappedStatement>();
}
5,定义MappedStatement对象
需要创建一个与mapper.xml
中标签和属性对应的实体类。其中包含(namespace、id、resultType、sql…)
等。
MappedStatement
对象存放的是解析mapper.xml
文件的内容,一个MappedStatement
对象对应一条sql
语句。
public class MappedStatement {
private String namespace; //mapper接口类的全包名
//sourceId这里存包名+方法名 com.ftx.mapper.mapper.java selectList()
//存这个的意义是作为Configuration类的statementMap的键(唯一)
private String sourceId;
private String resultType; //返回类型
private String sql; //sql语句
6,定义SqlSessionFactory对象
SqlSession
是MyBatis
的关键对象,通过java
操作MyBatis
时,可看到,它是由SqlSessionFactory
这个工厂来创建的。所以需要先完成SqlSessionFactory
的相关代码。
Mybatis
预与数据库建立连接,首先需要创建SqlSessionFactory
对象,该对象的作用
- 读取
db.properties
配置文件,解析数据源的相关信息,填充到configuration
对象中。 - 生产
SqlSession
。
构造方法的作用:
创建该对象即读取数据库配置文件db.properties
的信息存到configuration
对象中;
获取指定文件下的所有mapper.xml
文件并解析所有节点信息存到configuration
对象中
public class SqlSessionFactory {
private final Configuration configuration = new Configuration();
/**
* 记录mapper.xml存放的位置
**/
private static final String MAPPER_CONFIG_LOCATION = "mapper";
/**
* 记录数据路连接信息存放的文件
**/
private static final String DB_CONFIG_FILE = "db.properties";
public SqlSessionFactory() {
loadDBInfo();
loadMappersInfo();
}
//TODO 读取数据库配置文件信息
private void loadDBInfo() {
//加载数据库信息配置文件
InputStream stream = SqlSessionFactory.class.getClassLoader().getResourceAsStream(DB_CONFIG_FILE);
Properties properties = new Properties();
try {
properties.load(stream);
} catch (IOException e) {
e.printStackTrace();
}
//将数据库配置信息写入configuration对象中
configuration.setDriver(properties.get("driver").toString());
configuration.setUrl(properties.get("url").toString());
configuration.setUserName(properties.get("username").toString());
configuration.setPassWord(properties.get("password").toString());
}
//TODO 获取指定文件下的所有mapper.xml文件
private void loadMappersInfo() {
URL resource = null;
resource = SqlSessionFactory.class.getClassLoader().getResource(MAPPER_CONFIG_LOCATION);
//获取指定文件夹信息
File file = new File(resource.getFile());
if (file.isDirectory()) {
File[] mappers = file.listFiles();
//遍历文件夹下所有的mapper.xml文件,解析后,注册到configuration中
for (File mapper : mappers) {
loadMapper(mapper);
}
}
}
//TODO 对mapper.xml文件解析
private void loadMapper(File mapper) {
//创建SAXReader对象
SAXReader saxReader = new SAXReader();
//通过read方法读取一个文件,转换成Document对象
Document document = null;
try {
document = saxReader.read(mapper);
} catch (DocumentException e) {
e.printStackTrace();
}
//获取根节点元素对象<mapper>
Element rootElement = document.getRootElement();
//获取命名空间
String namespace = rootElement.attribute("namespace").getData().toString();
//获取子节点<select>标签
List<Element> selects = rootElement.elements("select");
//遍历select节点,将信息记录到MappedStatement对象,并登记到Configuration对象中
for (Element element : selects) {
MappedStatement statement = new MappedStatement();
String id = element.attribute("id").getData().toString();
String resultType = element.attribute("resultType").getData().toString();
//读取sql语句信息
String sql = element.getData().toString();
String sourceId = namespace + "." + id;
//给MappedStatement对象赋值
statement.setSourceId(sourceId);
statement.setNamespace(namespace);
statement.setResultType(resultType);
statement.setSql(sql);
configuration.getStatementMap().put(sourceId, statement);
}
}
//获取SqlSession对象
public SqlSession openSession() {
//将数据源信息和sql信息传进去
return new DefaultSqlSession(configuration);
}
7,定义SqlSession对象
SqlSessionFactory
对象生产出SqlSession
对象之后,SqlSession
的作用
- 定义查询接口(泛型)
- 获取
mapper
对象
public interface SqlSession {
//TODO 根据传入的条件查询单一结果
<T> T selectOne(String statement, Object parameter);
//TODO 查询集合
// 方法对应的sql语句,namespace + id 传过来的值就是这种: com.qfcwx.mapper.UserMapper.selectList
<E> List<E> selectList(String statement, Object parameter);
//TODO 获取mapper对象
<T> T getMapper(Class<T> type);
}
由其子类来实现SqlSession
,并重写其中的方法。
SqlSession
的功能是基于Executor
来实现的。其实,MyBatis
中,SqlSession
对数据库的操作,是委托给执行器Executor
来完成的。并且每一个SqlSession
都拥有一个新的Executor
对象。
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private final Executor executor;
public DefaultSqlSession(Configuration configuration) {
super();
this.configuration = configuration;
this.executor = new DefaultExecutor(configuration);
}
//selectOne查询一个对象,对应的List集合中只能有一个值,所以直接使用list.get(0)则可以取出。若是出现了多值的情况, 则程序抛出异常,selectList则查询所有结果。
@Override
public <T> T selectOne(String statement, Object parameter) {
List<T> list = this.selectList(statement, parameter);
if (list == null || list.size() == 0) {
return null;
}
if (list.size() == 1) {
return list.get(0);
} else {
throw new RuntimeException("too man result");
}
}
@Override
public <E> List<E> selectList(String statement, Object parameter) {
MappedStatement smt = this.configuration.getStatementMap().get(statement);
return executor.query(smt, parameter);
}
//创建Mapper.java接口类的动态代理类
@Override
public <T> T getMapper(Class<T> type) {
MappedProxy mappedProxy = new MappedProxy(this);
return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, mappedProxy);
}
}
动态代理类
作用:上面创建代理类之后会返回一个新的Mapper
接口对象,然后调用这个新的Mapper
接口的任何方法,都会触发动态代理类。调用selectList()
查询方法然后会执行动态代理,判断该查询方法的返回值去执行查询!
public class MappedProxy implements InvocationHandler {
private SqlSession sqlSession;
public MappedProxy(SqlSession sqlSession) {
super();
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Class<?> returnType = method.getReturnType();
//判断返回值是否为Collection的子类
if (Collection.class.isAssignableFrom(returnType)) {
//method.getDeclaringClass().getName() + "." + method.getName() 类名名称+方法名 com.qfcwx.mapper.UserMapper.selectList
return sqlSession.selectList(method.getDeclaringClass().getName() + "." + method.getName(), args == null ? null : args[0]);
} else {
return sqlSession.selectOne(method.getDeclaringClass().getName() + "." + method.getName(), args == null ? null : args[0]);
}
}
}
8,定义Executor
接口
上面说到SqlSession
的功能是基于Executor
来实现的,Mybatis
的核心接口之一,定义了数据库操作最基本的方法,SqlSession
的功能都基于它来实现现在构造Executor
接口。
MyBatis
源码中Executor
的方法定义了查询及更新的方法。还有事务提交和回滚。这里就简单的定义一个查询方法。
public interface Executor {
<E> List<E> query(MappedStatement statement,Object parameter);
}
上面定义一个接口。写一个查询的方法。相信大家在写原生JDBC操作数据库的时候,只有Query
和Update
。
这是因为不管查询一个,还是查询多个,都是查询的方法。而增删改都只是数据的更新。
MyBatis
中使用了连接池,而这里就使用最原生的JDBC来操作数据库。
public class Connections {
public static Connection getConnection(Configuration configuration) {
Connection connection = null;
try {
//加载驱动
Class.forName(configuration.getDriver());
connection = DriverManager.getConnection(configuration.getUrl(), configuration.getUserName(), configuration.getPassWord());
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
}
实现Executor
接口的实现类
public class DefaultExecutor implements Executor {
private Configuration configuration;
public DefaultExecutor(Configuration configuration) {
super();
this.configuration = configuration;
}
@Override
public <E> List<E> query(MappedStatement statement, Object parameter) {
List<E> list = new ArrayList<E>();
//获取连接
Connection connection = Connections.getConnection(configuration);
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
//创建预编译PreparedStatement对象,从MappedStatement获取sql语句
preparedStatement = connection.prepareStatement(statement.getSql());
//处理sql中的占位符
parameterSize(preparedStatement, parameter);
//执行查询操作获取resultSet
resultSet = preparedStatement.executeQuery();
//将结果集通过反射技术,填充到list中
handleResult(resultSet, list, statement.getResultType());
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (resultSet != null) {
resultSet.close();
}
if (preparedStatement != null) {
preparedStatement.close();
}
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return list;
}
/**
* //TODO 对PreparedStatement中的占位符进行处理
*
* @param statement
* @param parameter
* @return void
*/
private void parameterSize(PreparedStatement statement, Object parameter) throws SQLException {
if (parameter instanceof Integer) {
statement.setInt(1, (Integer) parameter);
} else if (parameter instanceof Long) {
statement.setLong(1, (Long) parameter);
} else if (parameter instanceof String) {
statement.setString(1, (String) parameter);
}
}
/**
* //TODO 读取ResultSet中的数据,并 转换成目标对象
*
* @param resultSet
* @param ret
* @param className
* @return void
*/
private <E> void handleResult(ResultSet resultSet, List<E> ret, String className) {
Class<E> clazz = null;
//通过反射获取类的对象
try {
clazz = (Class<E>) Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
while (resultSet.next()) {
//通过反射实例化对象
Object model = clazz.newInstance();
//使用反射工具将ResultSet中的数据填充到entity中
long id = resultSet.getLong("id");
String username = resultSet.getString("username");
String password = resultSet.getString("password");
User user = (User) model;
user.setId(id);
user.setUsername(username);
user.setPassword(password);
ret.add((E) user);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
9,进行测试
基本上就是JDBC
操作数据库的代码,也没什么好说的。通过反射来进行结果的反向解析。
最后就是测试的代码了。
public static void main(String[] args) {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// User user = mapper.selectUserById(10L);
// System.out.println(user);
List<User> userList = mapper.selectList();
System.out.println(userList);
}
打印
[User{id=5, username='18838030468', password='123456'}, User{id=6, username='13333333333', password='123456'}]
-------------------------------------------
个性签名:独学而无友,则孤陋而寡闻。做一个灵魂有趣的人!
如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!
万水千山总是情,打赏一分行不行,所以如果你心情还比较高兴,也是可以扫码打赏博主,哈哈哈(っ•̀ω•́)っ✎⁾⁾!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)