自己实现 MyBatis 底层机制
自己实现 MyBatis 底层机制【封装 Sqlsession 到执行器 + Mapper 接口和 Mapper.xml + MapperBean + 动态代理 代理 Mapper 的方法】
MyBatis 整体架构分析
对上图的解读
1) mybatis 的核心配置文件mybatis-config.xml: 进行全局配置,全局只能有一个这样的配置文件XxxMapper.xml 配置多个 SQL,可以有多个 XxxMappe.xml 配置文件
2) 通过 mybatis-config.xml 配置文件得到 SqlSessionFactory
3) 通过 SqlSessionFactory 得到 SqlSession,用 SqlSession 就可以操作数据了
4) SqlSession 底层是 Executor(执行器), 有 2 个重要的实现类, 有很多很多方法
5) MappedStatement 是通过 XxxMapper.xml 中定义, 生成的 statement 对象
6) 参数输入执行并输出结果集, 无需手动判断参数类型和参数下标位置, 且自动将结果集 映射为 Java 对象
搭建 MyBatis 底层机制开发环境
创建maven项目
流程就不多说了上一篇和手写Spring 和Spring MVC已经介绍过了
配置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>com.wyxdu</groupId>
<artifactId>wyx-mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
<!--定义编译器 / source / target 版本即可-->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<java.version>1.8</java.version>
</properties>
<!--引入必要的依赖-->
<dependencies>
<!--引入dom4j-->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!--引入mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!--lombok-简化entity/javabean/pojo开发 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
</dependency>
</project>
创建数据库和表
CREATE DATABASE `wyx_mybatis`;
USE `wyx_mybatis`;
CREATE TABLE `monster` (
`id` INT NOT NULL AUTO_INCREMENT,
`age` INT NOT NULL,
`birthday` DATE DEFAULT NULL,
`email` VARCHAR(255) NOT NULL,
`gender` TINYINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`salary` DOUBLE NOT NULL,
PRIMARY KEY (`id`)
) CHARSET=utf8
INSERT INTO `monster` VALUES(NULL, 200, '2000-11-11', 'nmw@sohu.com', 1,'牛魔王', 8888.88)
设计思路
实现任务阶段 1- 完成读取配置文件,得到数据库的连接
分析示意图
创建wyx_config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<database>
<!--配置连接数据库的信息-->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/wyx_mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</database>
创建WyxConfiguration类
package com.wyxdu.wyxmybatis.sqlsession;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
/**
* 读取xml文件, 建立连接
*/
public class WyxConfiguration {
//属性-类的加载器
private static ClassLoader loader =
ClassLoader.getSystemClassLoader();
//读取xml文件信息,并处理
public Connection build(String resource) {
Connection connection = null;
try {
//加载配置wyx_mybatis.xml 获取到对应的InputStream
InputStream stream =
loader.getResourceAsStream(resource);
//解析wyx_mybatis.xml => dom4j
SAXReader reader = new SAXReader();
Document document = reader.read(stream);
//获取到wyx_mybatis.xml 的根元素 <database>
Element root = document.getRootElement();
System.out.println("root=" + root);
//解析root元素,返回Connection => 单独的编写一个方法
connection = evalDataSource(root);
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
//方法会解析wyx_config.xml 信息,并返回Connection
//eval: 评估/解析
private Connection evalDataSource(Element node) {
if (!"database".equals(node.getName())) {
throw new RuntimeException("root 节点应该是<database>");
}
//连接DB的必要参数
String driverClassName = null;
String url = null;
String username = null;
String password = null;
//遍历node下的子节点,获取属性值
for (Object item : node.elements("property")) {
Element i = (Element) item;//i 就是 对应property节点
String name = i.attributeValue("name");
String value = i.attributeValue("value");
//判断是否得到name 和 value
if (name == null || value == null) {
throw new RuntimeException("property 节点没有设置name或者value属性");
}
switch (name) {
case "url":
url = value;
break;
case "username":
username = value;
break;
case "driverClassName":
driverClassName = value;
break;
case "password":
password = value;
break;
default:
throw new RuntimeException("属性名没有匹配到...");
}
}
Connection connection = null;
try {
Class.forName(driverClassName);
connection = DriverManager.getConnection(url,username,password);
} catch (Exception e) {
e.printStackTrace();
}
return connection; //返回Connection
}
}
完成测试
创建 WyxMyBatisTest测试类
实现任务阶段 2- 编写执行器,输入 SQL 语句,完成操作
分析示意图
创建Monster类
解读
@Getter 就会给所有属性 生成对应的getter
@Setter 就会给所有属性 生成对应的setter
@ToString 生成 toString...
@NoArgsConstructor 生成无参构造器
@AllArgsConstructor 生成要给全参构造器
@Data 注解相当于
Getter, Setter, RequiredArgsConstructor, ToString, EqualsAndHashCode, Value注解
package com.wyxdu.entity;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Date;
/**
* Monster 和 monster表有映射关系
*
* 解读
* @Getter 就会给所有属性 生成对应的getter
* @Setter 就会给所有属性 生成对应的setter
* @ToString 生成 toString...
* @NoArgsConstructor 生成无参构造器
* @AllArgsConstructor 生成要给全参构造器
* @Data 注解相当于Getter, Setter, RequiredArgsConstructor, ToString, EqualsAndHashCode, Value注解
* 如何选择主要还是看自己需要
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Monster {
private Integer id;
private Integer age;
private String name;
private String email;
private Date birthday;
private double salary;
private Integer gender;
}
创建Executor.java类
public interface Executor {
//泛型方法
public <T> T query(String statement, Object parameter);
}
创建WyxExecutor实现类
package com.wyxdu.wyxmybatis.sqlsession;
import com.wyxdu.entity.Monster;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class WyxExecutor implements Executor {
//属性
private WyxConfiguration wyxConfiguration =
new WyxConfiguration();
/**
* 根据 sql 查找结果
*
* @param sql
* @param parameter
* @param <T>
* @return
*/
@Override
public <T> T query(String sql, Object parameter) {
//得到连接Connection
Connection connection = getConnection();
//查询返回的结果集
ResultSet set = null;
PreparedStatement pre = null;
try {
pre = connection.prepareStatement(sql);
//设置参数, 如果参数多, 可以使用数组处理.
pre.setString(1, parameter.toString());
set = pre.executeQuery();
//把set数据封装到对象-monster
//说明: 这里做了简化处理
//认为返回的结果就是一个monster记录
//完善的写法是一套反射机制.
Monster monster = new Monster();
//遍历结果集, 把数据封装到monster对象
while (set.next()) {
monster.setId(set.getInt("id"));
monster.setName(set.getString("name"));
monster.setEmail(set.getString("email"));
monster.setAge(set.getInt("age"));
monster.setGender(set.getInt("gender"));
monster.setBirthday(set.getDate("birthday"));
monster.setSalary(set.getDouble("salary"));
}
return (T) monster;
} catch (Exception throwables) {
throwables.printStackTrace();
} finally {
try {
if (set != null) {
set.close();
}
if (pre != null) {
pre.close();
}
if (connection != null) {
connection.close();
}
} catch (Exception throwables) {
throwables.printStackTrace();
}
}
return null;
}
//编写方法,通过WyxConfiguration对象,返回连接
private Connection getConnection() {
Connection connection =
wyxConfiguration.build("wyx_mybatis.xml");
return connection;
}
}
测试效果
修改 WyxMyBatisTest测试类 增加方法
@Test
public void query() {
Executor executor = new WyxExecutor();
Monster monster =
executor.query("select * from monster where id=?", 1);
System.out.println("monster-- " + monster);
}
实现任务阶段 3- 将 Sqlsession 封装到执行器
分析示意图, 先观察原生 MyBatis 的 SqlSession 接口和默认接口和默认实现
流程图
创建 WyxSqlSession类
/**
* WyxSqlSession: 搭建Configuration(连接)和Executor之间桥梁
* 这里有操作DB的方法-
*/
public class WyxSqlSession {
//属性
//执行器
private Executor executor = new WyxExecutor();
//配置
private WyxConfiguration wyxConfiguration =
new WyxConfiguration();
//编写方法SelectOne 返回一条记录-对象 [做了简化]
//说明: 在原生的Mybatis中 statement 不是sql ,而是要执行的接口方法
//这里是做简化
public <T> T selectOne(String statement, Object parameter) {
return executor.query(statement, parameter);
}
}
完成测试
修改 MonsterMapperTest增加测试方法
@Test
public void selectOne() {
WyxSqlSession wyxSqlSession = new WyxSqlSession();
Monster monster =
wyxSqlSession.selectOne("select * from monster where id=?", 1);
System.out.println("monster--" + monster);
}
实现任务阶段 4- 开发 Mapper 接口和 Mapper.xml
分析【示意图】
创建Monster Mapper.java
/**
* MonsterMapper: 声明对db的crud方法
*/
public interface MonsterMapper {
//查询方法
public Monster getMonsterById(Integer id);
}
创建wyx_mybits.xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.wyxedu.mapper.MonsterMapper">
<!--实现配置接口方法getMonsterById-->
<select id="getMonsterById" resultType="com.wyxedu.entity.Monster">
select * from monster where id = ?
</select>
</mapper>
实现任务阶段 5- 开发和 Mapper 接口相映射的 MapperBean
分析示意图
创建Function.java , 对应 Mapper 的方法信息
package com.wyxedu.wyxmybatis.config;
/**
* Function: 记录对应的Mapper的方法信息
*/
public class Function {
//属性
private String sqlType; //sql类型,比如select,insert,update, delete
private String funcName; //方法名
private String sql;//执行的sql语句
private Object resultType;//返回类型
private String parameterType;//参数类型
public String getSqlType() {
return sqlType;
}
public void setSqlType(String sqlType) {
this.sqlType = sqlType;
}
public String getFuncName() {
return funcName;
}
public void setFuncName(String funcName) {
this.funcName = funcName;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public Object getResultType() {
return resultType;
}
public void setResultType(Object resultType) {
this.resultType = resultType;
}
public String getParameterType() {
return parameterType;
}
public void setParameterType(String parameterType) {
this.parameterType = parameterType;
}
@Override
public String toString() {
return "Function{" +
"sqlType='" + sqlType + '\'' +
", funcName='" + funcName + '\'' +
", sql='" + sql + '\'' +
", resultType=" + resultType +
", parameterType='" + parameterType + '\'' +
'}';
}
}
创建 MapperBean将mapper的信息 进行封装
package com.wyxedu.wyxmybatis.config;
import java.util.List;
/**
* MapperBean: 将Mapper信息,进行封装
*/
public class MapperBean {
private String interfaceName;//接口名
//接口下的所有方法-集合
private List<Function> functions;
public String getInterfaceName() {
return interfaceName;
}
public void setInterfaceName(String interfaceName) {
this.interfaceName = interfaceName;
}
public List<Function> getFunctions() {
return functions;
}
public void setFunctions(List<Function> functions) {
this.functions = functions;
}
@Override
public String toString() {
return "MapperBean{" +
"interfaceName='" + interfaceName + '\'' +
", functions=" + functions +
'}';
}
}
实现任务阶段 6- 在 WyxConfiguration, 读取 XxxMapper.xml,能够创建 MappperBean 对象
修改WyxConfiguration
public MapperBean readMapper(String path) {
MapperBean mapperBean = new MapperBean();
try {
//获取到xml文件对应的InputStream
InputStream stream = loader.getResourceAsStream(path);
SAXReader reader = new SAXReader();
//获取到xml文件对应的document-dom4j
Document document = reader.read(stream);
//得到xml文件的根元素/根节点
/*
<mapper namespace="com.wyxedu.mapper.MonsterMapper">
<!--实现配置接口方法getMonsterById-->
<select id="getMonsterById" resultType="com.wyxedu.entity.Monster">
select * from monster where id = ?
</select>
</mapper>
*/
Element root = document.getRootElement();
//获取到namespace
String namespace = root.attributeValue("namespace").trim();
//设置mapperBean的属性interfaceName
mapperBean.setInterfaceName(namespace);
//得到root的迭代器-可以遍历它的子节点/子元素-生成Function
Iterator rootIterator = root.elementIterator();
//保存接口下所有的方法信息
List<Function> list = new ArrayList<>();
//遍历它的子节点/子元素-生成Function
while (rootIterator.hasNext()) {
//取出一个子元素- dom4j Element
/**
* <select id="getMonsterById" resultType="com.wyxedu.entity.Monster">
* select * from monster where id = ?
* </select>
*/
Element e = (Element) rootIterator.next();
Function function = new Function();
String sqlType = e.getName().trim();
String funcName = e.attributeValue("id").trim();
//resultType是返回类型的全路径-即全类名
String resultType = e.attributeValue("resultType").trim();
String sql = e.getText().trim();
//开始封装
function.setSql(sql);
function.setFuncName(funcName);
function.setSqlType(sqlType);
//这里多说一句 function-private Object resultType; 是resultType实例
//使用反射生成一个对象, setResultType
Object newInstance = Class.forName(resultType).newInstance();
function.setResultType(newInstance);
//将封装好的function对象加入到 list
list.add(function);
}
//while循环结束后, 将function的list设置
mapperBean.setFunctions(list);
} catch (Exception e) {
e.printStackTrace();
}
return mapperBean;
}
完成测试
修改 MonsterMapperTest增加测试方法
@Test
public void readMapper() {
WyxConfiguration wyxConfiguration = new WyxConfiguration();
MapperBean mapperBean =
wyxConfiguration.readMapper("MonsterMapper.xml");
System.out.println("mapperBean---" + mapperBean);
System.out.println("ok~~");
}
实现任务阶段 7- 实现动态代理 Mapper 的方
创建WyxMapperProxy
package com.wyxedu.wyxmybatis.sqlsession;
import com.wyxedu.wyxmybatis.config.Function;
import com.wyxedu.wyxmybatis.config.MapperBean;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
/**
* WyxMapperProxy: 动态代理生成Mapper对象,调用WyxExecutor方法
*/
public class WyxMapperProxy implements InvocationHandler {
//属性
private WyxSqlSession wyxSqlSession;
private String mapperFile;
private WyxConfiguration wyxConfiguration;
//构造器
public WyxMapperProxy(WyxConfiguration wyxConfiguration,
WyxSqlSession wyxSqlSession,
Class clazz) {
this.wyxConfiguration = wyxConfiguration;
this.wyxSqlSession = wyxSqlSession;
this.mapperFile = clazz.getSimpleName() + ".xml";
}
//前面讲解spring 时,讲过动态代理机制=>提示, 如果忘记了动态代理知识
//去spring的博客笔记和代码回顾
//提示:当执行Mapper接口的代理对象方法时,会执行到invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MapperBean mapperBean =
wyxConfiguration.readMapper(this.mapperFile);
//判断是否是xml文件对应的接口
if (!method.getDeclaringClass().getName().equals(mapperBean.getInterfaceName())) {
return null;
}
//取出mapperBean的functions
List<Function> functions = mapperBean.getFunctions();
//判断当前mapperBean解析对应MappperXML后 , 有方法
if (null != functions && 0 != functions.size()) {
for (Function function : functions) {
//当前要执行的方法和function.getFuncName()一样
//说明我们可以从当前遍历的function对象中,取出相应的信息sql, 并执行方法
if(method.getName().equals(function.getFuncName())) {
//如果我们当前的function 要执行的sqlType是select
//我们就去执行selectOne
/**
*
* 说明:
* 1. 如果要执行的方法是select , 就对应执行selectOne
* 2. 因为就在WyxSqlSession就写了一个 selectOne
* 3. 实际上WyxSqlSession 应该对应不同的方法(多个方法)
* , 根据不同的匹配情况调用不同方法, 并且还需要进行参数解析处理, 还有比较复杂的字符串处理,拼接sql ,处理返回类型等等工作
* 4. 因为主要是讲解mybatis 生成mapper动态代理对象, 调用方法的机制,所以我做了简化
*/
if("select".equalsIgnoreCase(function.getSqlType())) {
return wyxSqlSession.selectOne(function.getSql(),String.valueOf(args[0]));
}
}
}
}
return null;
}
}
说明:
1. 如果要执行的方法是select , 就对应执行selectOne
2. 因为就在WyxSqlSession就写了一个 selectOne
3. 实际上WyxSqlSession
应该对应不同的方法(多个方法) 根据不同的匹配情况调用不同方法, 并且还需要进行参数解析处理,还有比较复杂的字符串处理,拼接sql ,处理返回类型等等工作
4. 因为主要是讲解mybatis 生成mapper动态代理对象, 调用方法的机制,所以我做了简化
修改WyxSqlsession.java
1. 返回mapper的动态代理对象
2. 这里clazz 到时传入的是 MonsterMapper.class
3. 返回的就是MonsterMapper接口代理对象
4. 当执行接口方法时(通过代理对象调用), 根据动态代理机制会执行到HspMapperProxy-invoke
如果感觉高亮警告不舒服可以在方法上设置@SuppressWarnings("unchecked")
如果需要整个类没有警告可以使用 @SuppressWarnings("all")
/**
* 1. 返回mapper的动态代理对象
* 2. 这里clazz 到时传入的是 MonsterMapper.class
* 3. 返回的就是MonsterMapper接口代理对象
* 4. 当执行接口方法时(通过代理对象调用), 根据动态代理机制会执行到HspMapperProxy-invoke
*/
public <T> T getMapper(Class<T> clazz) {
//返回动态代理对象
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz},
new WyxMapperProxy(wyxConfiguration,this,clazz));
}
创建WyxSessionFactory类
会话工厂-返回会话WyxSqlSession
package com.wyxedu.wyxmybatis.sqlsession;
/**
* WyxSessionFactory 会话工厂-返回会话WyxSqlSession
*/
public class WyxSessionFactory {
public static WyxSqlSession openSession() {
return new WyxSqlSession();
}
}
完成测试
修改 MonsterMapperTest增加测试方法
@Test
public void openSession() {
WyxSqlSession wyxSqlSession = WyxSessionFactory.openSession();
MonsterMapper mapper = wyxSqlSession.getMapper(MonsterMapper.class);
Monster monster = mapper.getMonsterById(1);
System.out.println("monster===" + monster);
}
转载自:https://juejin.cn/post/7241148626814550074