手动实现 MyBatis 底层机制
手动实现 MyBatis 底层机制
https://www.bilibili.com/video/BV1h64y1q7az?share_source=copy_web
1.MyBatis 整体架构分析
1.Mybatis 核心框架示意图
- mybatis 的核心配置文件 mybatis-config.xml: 进行全局配置,全局只能有一个这样的配置文件 XxxMapper.xml 配置多个 SQL,可以有多个 XxxMappe.xml 配置文件
- 通过 mybatis-config.xml 配置文件得到 SqlSessionFactory
- 通过 SqlSessionFactory 得到 SqlSession,用 SqlSession 就可以操作数据了
- SqlSession 底层是 Executor(执行器), 有2个重要的实现类, 有很多方法
5.MappedStatement 是通过 XxxMapper.xml 中定义, 生成的 statement 对象
6.参数输入执行并输出结果集, 无需手动判断参数类型和参数下标位置, 且自动将结果集 映射为 Java 对象
2.搭建MyBatis底层机制开发环境
1、创建 Maven 项目 llp-mybatis
2.修改pom.xml 引入相关依赖
<!--定义编译器 / 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>
<!--junit依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
3、创建数据库和表
CREATE DATABASE `llp_mybatis`;
USE `llp_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)
4、到此: 项目开发环境搭建 OK
3.MyBatis的设计思路
1.示意图
4.自己实现 MyBatis 底层机制 【封装 Sqlsession 到执行器 + Mapper 接口和 Mapper.xml + MapperBean + 动态代理代理 Mapper 的方法
1.实现任务阶段 1- 完成读取配置文件,得到数据库连
1.说明: 通过配置文件,获取数据库连接
2.分析+代码实现
● 分析示意图
1.resource目录下创建llp_mybatis.xml
<?xml version="1.0" encoding="UTF-8" ?>
<database>
<property name="driverName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/llp_mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8"></property>
<property name="userName" value="root"></property>
<property name="password" value="root"></property>
</database>
2.创建src\main\java\com\llp\llpmybatis\sqlsession\LLPConfiguration.java
/**
* 读取xml文件,建立连接
*/
public class LLPConfiguration {
//属性-类的加载器
private static ClassLoader loader = ClassLoader.getSystemClassLoader();
//读取xml文件信息并处理
public Connection build(String resource) {
Connection connection = null;
try {
//1.加载配置文件 llP_mybatis.xml 获取到对应的InputStream
InputStream stream = loader.getResourceAsStream(resource);
//2.解析llp_mybatis.xml =>dom4j
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(stream);
Element rootElement = document.getRootElement();
System.out.println("root=" + rootElement);
//3.解析root元素,返回Connection
connection = evalDataSource(rootElement);
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
/**
* 这个方法会解析我们的llp_mybatis.xml信息,并返回连接
*
* @param node
* @return
*/
private Connection evalDataSource(Element node) throws ClassNotFoundException, SQLException {
if (!"database".equals(node.getName())) {
throw new RuntimeException("root 节点应该是<database>");
}
//连接DB的必要参数
String driverName = null;
String url = null;
String userName = null;
String password = null;
List propertys = node.elements("property");
for (Object property : propertys) {
Element element = (Element) property;
String name = element.attributeValue("name");
String value = element.attributeValue("value");
if (name == null || value == null) {
throw new RuntimeException("property 节点没有设置name或者value属性");
}
switch (name) {
case "driverName":
driverName = value;
break;
case "url":
url = value;
break;
case "userName":
userName = value;
break;
case "password":
password = value;
break;
}
}
Class.forName(driverName);//建议写上
Connection connection = DriverManager.getConnection(url, userName, password);
return connection;
}
}
3.完成测试
public class LLPMyBatisTest {
@Test
public void build(){
LLPConfiguration llpConfiguration = new LLPConfiguration();
Connection connection = llpConfiguration.build("llp_mybatis.xml");
System.out.println(connection);
}
}
2.实现任务阶段 2- 编写执行器,输入 SQL 语句,完成操作
1.说明:通过实现执行器机制,对数据表操作
● 分析示意图
说明:我们把对数据库的操作,会封装到一套 Executor 机制中,程序具有更好的扩展性, 结构更加清晰
2.分析+代码实现
1.创建Monster.java
@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;
}
2.创建Executor.java接口
public interface Executor {
//泛型方法
public <T> T query(String sql, Object... parameter);
}
3.创建LLPExecutor.java实现类
public class LLPExecutor implements Executor {
@Override
public <T> T query(String sql, Object... parameter) {
//1.获取连接
Connection connection = getConnection();
ResultSet resultSet = null;
PreparedStatement preparedStatement = null;
try {
preparedStatement = connection.prepareStatement(sql);
for (int i = 0; i < parameter.length; i++) {
preparedStatement.setString(i + 1, parameter[i].toString());
}
resultSet = preparedStatement.executeQuery();
Monster monster = new Monster();
while (resultSet.next()) {
monster.setAge(resultSet.getInt("age"));
monster.setBirthday(resultSet.getDate("birthday"));
monster.setEmail(resultSet.getString("email"));
monster.setGender(resultSet.getInt("gender"));
monster.setId(resultSet.getInt("id"));
monster.setSalary(resultSet.getDouble("salary"));
monster.setName(resultSet.getString("name"));
}
return (T) monster;
} catch (SQLException throwables) {
throwables.printStackTrace();
}finally {
if(connection!=null){
try {
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if(resultSet!=null){
try {
resultSet.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if(preparedStatement!=null){
try {
preparedStatement.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
return null;
}
private Connection getConnection() {
LLPConfiguration llpConfiguration = new LLPConfiguration();
Connection connection = llpConfiguration.build("llp_mybatis.xml");
return connection;
}
}
3.完成测试
1.测试方法
@Test
public void query() {
Executor executor = new LLPExecutor();
//lombok @Data注解会生成toString方法
Monster monster =
executor.query("select * from monster where id=?", 1);
System.out.println("monster-- " + monster);
}
2.测试结果
3.实现任务阶段 3- 将 Sqlsession 封装到执行器
1.分析+代码实现
● 分析示意图, 先观察原生 MyBatis 的 SqlSession 接口和默认实现
在SqlSession接口中,封装了执行器
通过执行器,完成对数据库的操作,比如selectone
● 完成功能
1.创建LLPSqlSession.java
public class LLPSqlSession {
//属性
//执行器
private Executor executor = new LLPExecutor();
//配置
private LLPConfiguration llpConfiguration = new LLPConfiguration();
//编写方法SelectOne 返回一条记录-对象
public <T> T selectOne(String statement, Object parameter) {
return executor.query(statement, parameter);
}
}
2.测试
@Test
public void selectOne() {
Monster monster = new LLPSqlSession().selectOne("select * from monster where id=?", 1);
System.out.println(monster);
}
4.实现任务阶段 4- 开发 Mapper 接口和 Mapper.xml
1.分析+ 代码实现
● 分析【示意图】
● 代码实现
1.创建src\main\java\com\llp\entity\Monster.java
@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;
}
2.创建src\main\resources\MonsterMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.llp.mapper.MonsterMapper">
<!--实现配置接口方法-->
<select id="getMonsterById" resultType="com.llp.entity.Monster">
select * from monster where id = ?
</select>
</mapper>
5.实现任务阶段 5- 开发和 Mapper 接口相映射的 MapperBean
● 分析示意图
● 代码实现
1.创建src\main\java\com\llp\llpmybatis\config\Function.java 对应 Mapper 的方法信息
/**
* 记录对应的mapper的方法信息
*/
@Data
public class Function {
//属性
//sql类型,比如select,inster,update,delete
private String sqlType;
//方法名
private String funcName;
//要执行的sql语句
private String sql;
//返回类型
private Object resultType;
//参数类型
private String parameterType;
}
2.创建src\main\java\com\llp\llpmybatis\config\MapperBean.java, 将 Mapper 的信息,进行封装
/**
* 将mapper信息,进行封装
*/
@Data
public class MapperBean {
//接口名
private String interfaceName;
//接口下的所以方法-集合
private List<Function> functions;
}
6.实现任务阶段 6- 在 LLPConfiguration, 读取 XxxMapper.xml,能够创建 MappperBean 对象
1.分析+代码实现
● 分析示例
MapperBean(interfaceName=com.llp.mapper.MonsterMapper, functions=[Function(sqlType=select, funcName=getMonsterById, sql=select * from monster where id = ?, resultType=Monster(id=null, age=null, name=null, email=null, birthday=null, salary=0.0, gender=null), parameterType=null)])
● 代码实现
1.src\main\java\com\llp\llpmybatis\sqlsession\LLPConfiguration.java增加方法
//读取XxxMapper.xml,能够创建MapperBean对象
//path 就是xml的路径+文件名 是从类的加载路径计算的
//说明:如果XxxMapper.xml 文件是防在resources目录下,直接传入xml文件名即可
public MapperBean readMapper(String path) {
MapperBean mapperBean = new MapperBean();
try {
InputStream resourceAsStream = loader.getResourceAsStream(path);
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(resourceAsStream);
/**
* <?xml version="1.0" encoding="UTF-8" ?>
* <mapper namespace="com.llp.mapper.MonsterMapper">
* <!--实现配置接口方法-->
* <select id="getMonsterById" resultType="com.llp.entity.Monster">
* select * from monster where id = ?
* </select>
*
* </mapper>
*/
Element rootElement = document.getRootElement();
List<Function> functionList = new ArrayList<>();
//获取命名空间
String namespace = rootElement.attributeValue("namespace").trim();
//接口名
mapperBean.setInterfaceName(namespace);
//获取迭代器
Iterator iterator = rootElement.elementIterator();
while (iterator.hasNext()) {
Element element = (Element) iterator.next();
//select
String sqlType = element.getName().trim();
//getMonsterById
String funcName = element.attributeValue("id").trim();
//这里得到resultType是全类名
String resultType = element.attributeValue("resultType").trim();
//select * from monster where id = ?
String sql = element.getText().trim();
Function function = new Function();
function.setSqlType(sqlType);
function.setFuncName(funcName);
function.setSql(sql);
Class<?> aClass = Class.forName(resultType);
Object o = aClass.newInstance();
function.setResultType(o);
functionList.add(function);
}
mapperBean.setFunctions(functionList);
} catch (Exception e) {
e.printStackTrace();
}
return mapperBean;
}
2.完成测试
@Test
public void readMapper(){
LLPConfiguration llpConfiguration = new LLPConfiguration();
MapperBean mapperBean = llpConfiguration.readMapper("MonsterMapper.xml");
System.out.println(mapperBean);
}
7.实现任务阶段 7- 实现动态代理 Mapper 的方法
1.分析+代码实现
● 分析示意图【看下示意图】
● 代码实现
1.创建src\main\java\com\llp\llpmybatis\sqlsession\LLPMapperProxy.java
/**
* 动态代理生成Mapper对象,调用LLPExecutor方法
*/
public class LLPMapperProxy implements InvocationHandler {
//属性
private LLPSqlSession llpSqlSession;
private LLPConfiguration llpConfiguration;
private String mapperFile;
//构造器
public LLPMapperProxy(LLPSqlSession llpSqlSession, LLPConfiguration llpConfiguration, Class clazz) {
this.llpConfiguration = llpConfiguration;
this.llpSqlSession = llpSqlSession;
this.mapperFile = clazz.getSimpleName() + ".xml";
}
/**
*
* @param proxy 表示代理对象
* @param method 就是通过代理对象调用方法时,的哪个方法 代理对象.run()
* @param args : 表示调用 代理对象.run(xx) 传入的参数
* @return 表示 代理对象.run(xx) 执行后的结果.
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//读取mapper.xml配置文件封装接口名和function集合
MapperBean mapperBean = llpConfiguration.readMapper(this.mapperFile);
//判断是否是xml文件对应的接口
if (!method.getDeclaringClass().getName().equals(mapperBean.getInterfaceName())) {
return null;
}
//Function记录对应的mapper的方法信息:方法名、sql类型、要执行的sql语句、返回类型等
List<Function> functions = mapperBean.getFunctions();
if (functions != null && 0 != functions.size()) {
for (Function function : functions) {
//判断动态代理执行的方法名是否和function对象的方法名一致
if(function.getFuncName().equals(method.getName())){
//如果我们当前的function 要执行的sqlType是select
//我们就去执行selectOne
/**
*
* 说明:
* 1. 如果要执行的方法是select , 就对应执行selectOne
* 2. 因为在LLPSqlSession就写了一个 selectOne
* 3. 实际上LLPSqlSession 应该对应不同的方法(多个方法)
* , 根据不同的匹配情况调用不同方法, 并且还需要进行参数解析处理, 还有比较复杂的字符串处理,拼接sql ,处理返回类型等等工作
*/
if("select".equalsIgnoreCase(function.getSqlType())) {
return llpSqlSession.selectOne(function.getSql(),String.valueOf(args[0]));
}
}
}
}
return null;
}
}
2.修改src\main\java\com\llp\llpmybatis\sqlsession\LLPSqlSession.java
public <T> T getMapper(Class clazz){
//返回动态代理对象
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz},
new LLPMapperProxy(this,llpConfiguration,clazz));
}
3.创建LLPSessionFactory.java
public class LLPSessionFactory {
public LLPSqlSession openSession(){
return new LLPSqlSession();
}
}
4.测试方法
@Test
public void getMapper(){
LLPSessionFactory llpSessionFactory = new LLPSessionFactory();
LLPSqlSession llpSqlSession = llpSessionFactory.openSession();
MonsterMapper mapper = llpSqlSession.getMapper(MonsterMapper.class);
System.out.println(mapper.getClass());
/**
* 执行流程梳理
* 1.首先 llpSqlSession.getMapper(MonsterMapper.class); 获取到的是代理对象class com.sun.proxy.$Proxy4
* 2.mapper.getMonsterById(1)执行时,代理对象调用invoke方法
* 3. return llpSqlSession.selectOne(function.getSql(),String.valueOf(args[0]));
* 在动态地理执行目标方法时,会调用LLPExecutor的query方法,从而获取连接执行sql并封装返回结果
*/
//Monster(id=1, age=200, name=牛魔王, email=nmw@sohu.com, birthday=2000-11-11, salary=8888.88, gender=1)
System.out.println(mapper.getMonsterById(1));
}
5.测试结果