手动实现MaBatis底层机制
- 实现任务阶段一
- 🍍完成读取配置文件, 得到数据库连接
- 🥦分析 + 代码实现
- 🥦完成测试
- 实现任务阶段二
- 🍍编写执行器, 输入SQL语句, 完成操作
- 🥦分析 + 代码实现
- 🥦完成测试
- 实现任务阶段三
- 🍍将Sqlsession封装到执行器
- 🥦分析 + 代码实现
- 🥦完成测试
- 实现任务阶段四
- 🍍开发Mapper接口和Mapper.xml
- 🥦分析 + 代码实现
- 实现任务阶段五
- 🍍开发Mapper接口相映射的MapperBean
- 🥦分析 + 代码实现
- 实现任务阶段六
- 🍍在ZzwConfiguration读取xxxMapper.xml, 能够创建MapperBean对象
- 🥦分析 + 代码实现
- 🥦完成测试
- 实现任务阶段七
- 🍍实现动态代理Mapper的方法
- 🥦分析 + 代码实现
- 🥦完成测试
- 🥦Debug原生MyBatis-DeaultSqlSession不同方法
- 🥦Debug执行流程
上一篇, 我们学习到了 mabatis 上
接下来我们学习, 手动实现MaBatis底层机制
实现任务阶段一
🍍完成读取配置文件, 得到数据库连接
🥦分析 + 代码实现
●分析示意图
1.创建src/main/resouces/zzw_mybatis.xml
不一定非叫 zzw_mybatis
<?xml version="1.0" encoding="UTF-8" ?>
<database>
<!--配置连接数据库的信息-->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/zzw_mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="zzw"/>
</database>
2.创建com.zzw.zzwmybatis.sqlsession.Zzwconfiguration.java
遍历xml指定元素 参考
数据库连接的5种方式 参考
/**
* @author 赵志伟
* @version 1.0
* 读取xml文件, 建立连接
*/
public class ZzwConfiguration {
//属性-类的加载器
private static ClassLoader loader = ClassLoader.getSystemClassLoader();
//读取xml文件信息, 并处理
//Connection 是java.sql包下的
public Connection build(String resource) {
Connection connection = null;//java.sql
//1.得到解析器, 解析配置文件 dom4j
SAXReader reader = new SAXReader();
//2.加载配置文件zzw_mybatis.xml, 获取到对应的InputStream
InputStream inputStream = loader.getResourceAsStream(resource);
try {
//3.得到xml文件的文档
Document document = reader.read(inputStream);
//4.获取rootElement / zzw_mybatis.xml的根元素, 即<database/>
Element rootElement = document.getRootElement();
System.out.println("root="+ rootElement.getName());//root=database
//5.解析rootElement, 返回Connection => 单独写一个方法
connection = evalDataSource(rootElement);
} catch (Exception e) {
throw new RuntimeException(e);
}
return connection;
}
//方法会解析zzw_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下的子节点, 获取属性值
List<Element> properties = node.elements("property");
for (Element property : properties) {
String name = property.attributeValue("name");
String value = property.attributeValue("value");
//判断是否得name和value
if (name == null || value == null) {
throw new RuntimeException("property 节点没有设置name或者value属性");
}
switch (name) {
case "driverClassName":
driverClassName = value;
break;
case "url":
url = value;
break;
case "username":
username = 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) {
throw new RuntimeException(e);
}
return connection; //返回Connection
}
}
🥦完成测试
com.zzw.test.ZzwMyBatisTest.java
public class ZzwMyBatisTest {
@Test
public void build() {
ZzwConfiguration zzwConfiguration = new ZzwConfiguration();
Connection connection = zzwConfiguration.build("zzw_mybatis.xml");
System.out.println("connection--" + connection);
}
}
实现任务阶段二
🍍编写执行器, 输入SQL语句, 完成操作
🥦分析 + 代码实现
●分析示意图
说明: 我们把对数据库的操作, 会封装到一套Executor
机制中, 程序具有更好的扩展性, 结构更加清晰
下图在原生mybatis的项目中可以看到
●代码实现
1.新建com.zzw.entity.Monster.java
/**
* @author 赵志伟
* @version 1.0
* Monster和 monster表有映射关系
*
* 解读
* @Getter 就会给所有属性 生成对应的getter方法
* @Setter 就会给所有属性 生成对应的setter方法
* @ToString 生成 toString...
* @NoArgsConstructor 生成无参构造器
* @AllArgsConstructor 生成全参构造器
* @Data 注解
* 如何选择主要还是看自己的需求
*/
//@Getter
//@Setter
//@ToString
//@NoArgsConstructor
//@AllArgsConstructor
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Monster {
private Integer id;
private Integer age;
private Date birthday;//java.util
private String email;
private Integer gender;
private String name;
private Double salary;
}
@Data注解包含了其他注解
2.新建com.zzw.zzwmybatis.sqlsession.Executor
接口
public interface Executor {
//泛型方法
public <T> T query(String sql, Object parameter);
}
3.新建com.zzw.zzwmybatis.sqlsession.ZzwExecutor.java
自定义泛型方法, 参考
public class ZzwExecutor implements Executor {
//属性
private ZzwConfiguration zzwConfiguration =
new ZzwConfiguration();
/**
* 根据 sql 查询结果
* @param statement
* @param parameter
* @return
* @param <T>
*/
@Override
public <T> T query(String sql, Object parameter) {
//得到连接Connection
Connection connection = getConnection();
//查询返回的结果集
ResultSet resultSet = null;
PreparedStatement preparedStatement = null;
try {
preparedStatement = connection.prepareStatement(sql);
//设置参数, 如果参数多, 可以使用数组处理
preparedStatement.setString(1, parameter.toString());
resultSet = preparedStatement.executeQuery();
//把resultSet数据封装到对象-monster
//说明: 这里做了简化处理
//认为放回的结果就是一个monster记录
//完善的写法是一套反射机制
Monster monster = new Monster();
//遍历结果集, 把数据封装到monster对象
while (resultSet.next()) {
monster.setId(resultSet.getInt("id"));
monster.setAge(resultSet.getInt("age"));
monster.setBirthday(resultSet.getDate("birthday"));
monster.setEmail(resultSet.getString("email"));
monster.setGender(resultSet.getInt("gender"));
monster.setName(resultSet.getString("name"));
monster.setSalary(resultSet.getDouble("salary"));
}
return (T) monster;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
if (resultSet != null) {
resultSet.close();
}
if (preparedStatement != null) {
preparedStatement.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
//编写方法, 通过ZzwConfiguration对象, 返回连接
private Connection getConnection() {
Connection connection = zzwConfiguration.build("zzw_mybatis.xml");
return connection;
}
}
🥦完成测试
public class ZzwMyBatisTest {
@Test
public void query() {
ZzwExecutor executor = new ZzwExecutor();
Monster monster
= executor.query("SELECT * FROM `monster` WHERE id = ?", 1);
System.out.println("monster--" + monster);
}
}
实现任务阶段三
🍍将Sqlsession封装到执行器
🥦分析 + 代码实现
●分析示意图. 先观察原生MyBatis
的SqlSession
接口和默认实现. 在原生mybatis项目中
●功能实现
●代码实现
1.创建com.zzw.zzwmybatis.sqlsession.ZzwSqlSession.java
/**
* @author 赵志伟
* @version 1.0
* ZzwSqlSession: 搭建Configuration (连接) 和 Executor 之间的桥梁
* 这里有操作DB的具体方法
*/
public class ZzwSqlSession {
//属性
//执行器
private Executor executor = new ZzwExecutor();
//配置
private ZzwConfiguration zzwConfiguration =
new ZzwConfiguration();
//编写方法selectOne, 返回一条记录-对象[做了简化]
//说明: 在原生的mybatis中, statement不是sql, 而是要执行的接口方法
//这里我们是做了简化
public <T> T selectOne(String statement, Object parameter) {
return executor.query(statement, parameter);
}
//selectList - update - delete - insert
}
🥦完成测试
public class ZzwMyBatisTest {
@Test
public void selectOne() {
ZzwSqlSession zzwSqlSession = new ZzwSqlSession();
Monster monster =
zzwSqlSession.selectOne("select * from `monster` where id = ?", 1);
System.out.println("monster--" + monster);
}
}
实现任务阶段四
🍍开发Mapper接口和Mapper.xml
🥦分析 + 代码实现
●分析示意图
●代码实现
1.创建com.zzw.mapper.MonsterMapper
接口
/**
* @author 赵志伟
* @version 1.0
* MonsterMapper: 声明对db的crud方法
*/
public interface MonsterMapper {
//查询方法
public Monster getMonsterById(Integer id);
}
2.src/main/resources
(类路径)下新建 MonsterMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.zzw.mapper.MonsterMapper">
<!--实现配置接口方法getMonsterById-->
<select id="getMonsterById" resultType="com.zzw.entity.Monster">
select * from monster where id = ?
</select>
</mapper>
实现任务阶段五
🍍开发Mapper接口相映射的MapperBean
🥦分析 + 代码实现
●分析示意图
●代码实现
1.创建com.zzw.zzwmybatis.config.Function.java
/**
* @author 赵志伟
* @version 1.0
* Function: 记录对应的Mapper的方法信息
*/
@Setter
@Getter
@ToString
public class Function {
//属性
private String sqlType;//sql类型. 比如select, insert, update, delete
private String funcName;//方法名
private String sql;//执行sql语句
private Object resultType;//返回类型
private String parameter;//参数类型
}
2.创建com.zzw.zzwmybatis.config.MapperBean.java
/**
* @author 赵志伟
* @version 1.0
* MapperBean: 将Mapper信息, 进行封装
*/
@Setter
@Getter
@ToString
public class MapperBean {
private String interfaceName;//接口的全路径-接口名
//接口下的所有方法-集合
private List<Function> functions;
}
实现任务阶段六
🍍在ZzwConfiguration读取xxxMapper.xml, 能够创建MapperBean对象
🥦分析 + 代码实现
●分析示意图
无
●代码实现
1.修改com.zzw.zzwmybatis.sqlsession.Zzwconfiguration.java
, 增加方法
//读取xxxMapper.xml, 能够创建MapperBean对象
//path 就是xml的路径+文件名, 是从类的加载路径计算的
//如果: xxxMapper.xml 文件是放在resources目录下, 直接传入xml文件名即可
public MapperBean readMapper(String path) {
MapperBean mapperBean = new MapperBean();
//1.得到解析器 -> dom4j
SAXReader reader = new SAXReader();
//2.获取到xml文件对应的InputStream
InputStream inputStream = loader.getResourceAsStream(path);
try {
//3.得到xml文件的文档
Document document = reader.read(inputStream);
//4.获取xml文档的根元素 <mapper/>
Element rootElement = document.getRootElement();
System.out.println("root="+ rootElement);
//获取到namespace
String namespace = rootElement.attributeValue("namespace").trim();
//设置mapperBean的属性interfaceName
mapperBean.setInterfaceName(namespace);
//得到rootElement的迭代器-可以遍历它的子节点/子元素-生成Function
Iterator<Element> rootIterator = rootElement.elementIterator();
//保存接口下所有的方法信息
List<Function> functions = new ArrayList<>();
//遍历它的子节点/子元素-生成Function
while (rootIterator.hasNext()) {
//取出一个子元素-dom4j.Element
Element element = rootIterator.next();
/*
<select id="getMonsterById" resultType="com.zzw.entity.Monster">
select * from monster where id = ?
</select>
*/
Function function = new Function();
String sqlType = element.getName().trim();
String funcName = element.attributeValue("id").trim();
String sql = element.getTextTrim();//等价于: getText().trim()
//resultType是返回类型的全路径-即全类名
String resultType = element.attributeValue("resultType").trim();
//开始封装
function.setSqlType(sqlType);
function.setFuncName(funcName);
function.setSql(sql);
//这里多说一句 function-private Object resultType; 是resultType实例
//所以我们使用反射生成一个对象, setResultType
Object instance = Class.forName(resultType).newInstance();
function.setResultType(instance);
//将封装好的function对象放人到 list
functions.add(function);
}
//while循环结束后, 将function的list设置
mapperBean.setFunctions(functions);
} catch (Exception e) {
throw new RuntimeException(e);
}
return mapperBean;
}
🥦完成测试
public class ZzwMyBatisTest {
@Test
public void readMapper() {
ZzwConfiguration zzwConfiguration = new ZzwConfiguration();
MapperBean mapperBean = zzwConfiguration.readMapper("MonsterMapper.xml");
System.out.println("mapperBean--" + mapperBean);
System.out.println("ok~~");
}
}
实现任务阶段七
🍍实现动态代理Mapper的方法
🥦分析 + 代码实现
●分析示意图
前面我们有2个地方学习过动态代理, 切面编程的底层支撑是动态代理
动态代理: AOP切面编程
动态代理: 手动实现spring底层机制
●代码实现
1.新增com.zzw.zzwmybatis.sqlsession.ZzwMapperProxy.java
转String类型
/**
* @author 赵志伟
* @version 1.0
* ZzwMapperProxy: 动态代理生成Mapper对象, 调用ZzwExecutor方法
*/
@SuppressWarnings({"all"})
public class ZzwMapperProxy implements InvocationHandler {
//属性
private ZzwSqlSession zzwSqlSession;
private String mapperFile;
private ZzwConfiguration zzwConfiguration;
//构造器
public ZzwMapperProxy(ZzwSqlSession zzwSqlSession,
ZzwConfiguration zzwConfiguration,
Class clazz) {
this.zzwSqlSession = zzwSqlSession;
this.zzwConfiguration = zzwConfiguration;
this.mapperFile = clazz.getSimpleName() + ".xml";
}
//前面讲解spring时, 讲过动态代理知识
//提示: 当执行Mapper接口的代理对象方法时, 会执行到invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MapperBean mapperBean =
zzwConfiguration.readMapper(this.mapperFile);
//判断是否是xml文件对应的接口
if (!method.getDeclaringClass().getName().equals(mapperBean.getInterfaceName())) {
return null;
}
//取出mapperBean的functions
List<Function> functions = mapperBean.getFunctions();
//判断当前mapperBean解析对应MapperXML后, 有方法
if (functions != null && functions.size() != 0) {
for (Function function : functions) {
//当前要执行的方法和function.getFuncName()一样
//说明我们可以从当前遍历的function对象中, 取出相应的信息sql, 并执行方法
if (method.getName().equals(function.getFuncName())) {{
//如果我们当前的function 要执行的sqlType是select
//我们就去执行selectOne
/**
* 说明:
* 1. 如果要执行的方法是select, 就对应执行selectOne
* 2. 因为我们在ZzwSqlSession就写了一个 selectOne
* 3. 实际上ZzwSqlSession对应不同的方法(多个方法)
* , 根据不同的匹配情况调用不同方法, 并且还需要进行参数解析处理, 还有比较复杂的字符串处理, 拼接sql, 处理返回类型等等工作
* 4. 因为我们主要是想讲解mybatis 生成mapper动态代理对象, 调用方法的机制, 所以我们做了简化
*/
if ("select".equals(function.getSqlType())) {
return zzwSqlSession.selectOne(function.getSql(), String.valueOf(args[0]));
}
}}
}
}
return null;
}
}
2.修改com.zzw.zzwmybatis.sqlsession.ZzwSqlSession
, 增加方法
/**
* 1. 返回mapper的动态代理对象
* 2. 这里的clazz 到时传入的是 MonsterMapper.class
* 3. 放回的就是MonsterMapper接口代理对象
* 4. 当执行接口方法时(通过代理对象调用), 根据动态代理机制, 会执行到ZzwMapperProxy-invoke
* @param clazz
* @return
* @param <T>
*/
public <T> T getMapper(Class<T> clazz) {
//返回动态代理对象
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new ZzwMapperProxy(this, zzwConfiguration, clazz));
}
🥦完成测试
1.测试com.zzw.test.ZzwMyBatisTest
public class ZzwMyBatisTest {
@Test
public void getMapper() {
ZzwSqlSession zzwSqlSession = new ZzwSqlSession();
MonsterMapper mapper = zzwSqlSession.getMapper(MonsterMapper.class);
System.out.println("mapper运行类型=" + mapper.getClass());//mapper是一个代理对象
Monster monster = mapper.getMonsterById(1);
System.out.println("monster--" + monster);
}
}
2.新建com.zzw.zzwmybatis.sqlsession.ZzwSessionFactory
/**
* @author 赵志伟
* @version 1.0
* ZzwSessionFactory: 会话工厂-返回会话ZzwSqlSession
*/
public class ZzwSessionFactory {
public static ZzwSqlSession openSession() {
return new ZzwSqlSession();
}
}
3.测试com.zzw.test.ZzwMyBatisTest
public class ZzwMyBatisTest {
@Test
public void openSession() {
ZzwSqlSession zzwSqlSession = ZzwSessionFactory.openSession();
MonsterMapper mapper = zzwSqlSession.getMapper(MonsterMapper.class);
Monster monster = mapper.getMonsterById(1);
System.out.println("monster--" + monster);
}
}
🥦Debug原生MyBatis-DeaultSqlSession不同方法
找到mybatis项目, 在DefaultSqlSession
的selectOne
方法打上断点, 测试select
方法
同时证明:
原生的mybatis
中,selectOne
方法的statement
参数不是sql
, 而是要执行的接口方法
Step Into, 追到BaseExecutor
的doQuery
方法
继续 Step Into
继续 Step Into
继续 Step Into
继续 Step Into
继续 Step Into
继续 Step Into
继续 Step Into, doQuery
里面就是原生的Jdbc
代码了
在DefaultSqlSession
的insert
方法打上断点, 测试insert
方法
Step Into
继续 Step Into
继续 Step Into
继续 Step Into, doUpdate
方法里面就是原生的Jdbc
代码了
在DefaultSqlSession
的delete
方法打上断点, 测试delete
方法
在DefaultSqlSession
的update
方法打上断点, 测试update
方法
🥦Debug执行流程
回到自己写的zzw-mybatis
项目, 开始debug
Step Into
Step Into
Step Out
估值
Step Into
Step Out
估值, mapper
是个代理对象
在ZzwMapperProxy
的invoke
方法下个断点, 直接放行
对mapperBean
估值
Step Into
继续 Step Into
查看传进来的参数
拿到Jdbc的结果集
Step Out
对monster
进行估值