💡 **封装 Sqlsession 到执行器 + Mapper 接口和 Mapper.xml + MapperBean + 动态代理 **
代理 Mapper 的方法
GitHub地址:https://github.com/1072344372/lh-mybatis
分阶段完成 每个阶段可以github提交记录
先来看看mybatis原本的执行流程
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 对象
package asia.lhweb.lhmybatis.sqlsession;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
/**
* lh配置
*
* @author 罗汉
* @date 2023/09/16
*/
public class LhConfiguration {
//属性-类加载器
private static ClassLoader loader=ClassLoader.getSystemClassLoader();
/**
* 读取xml文件信息,并处理
*
* @param resource 资源
* @return {@link Connection}
*/
public Connection build(String resource){
Connection connection=null;
try {
//加载配置文件lh-mybatis.xml 获取到对应的InputStream
InputStream stream = loader.getResourceAsStream(resource);
//解析xml文件 dom4j
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(stream);
//获取lh-mybatis.xml的database标签
Element rootElement = document.getRootElement();//拿到根元素
// System.out.println(rootElement);
//根据rootElement去解析
connection = evalDataSource(rootElement);
} catch (DocumentException e) {
e.printStackTrace();
}
return connection;
}
/**
* 解析xml文件返回连接
*
* @param root 根元素
* @return {@link Connection}
*/
private Connection evalDataSource(Element root){
if (!"database".equals(root.getName())){
throw new RuntimeException("root节点不是<database>");
}
//用来储存name和value
Map<String, String> properties = new HashMap<>();
//遍历root下的节点
for (Object element : root.elements("property")) {
Element i=(Element)element;//i就是 对应property节点
// <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
String name = i.attributeValue("name");
String value = i.attributeValue("value");
//判断是否得到了name和value
if (name==null||value==null){
throw new RuntimeException("property节点没有设置name或者value属性");
}
properties.put(name, value);
}
String driverClassName = properties.get("driverClassName");
String url = properties.get("url");
String username = properties.get("username");
String password = properties.get("password");
Connection connection=null;
try {
Class.forName(driverClassName);
connection= DriverManager.getConnection(url, username, password);
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<database>
<!--配置连接数据库的信息-->
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/lh_mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</database>
直接通过Executor对db进行操作,走的线路1(BaseExecutor)
package asia.lhweb.entity;
import lombok.*;
import java.util.Date;
/**
* 怪物
*
* @author 罗汉
* @date 2023/09/16
*/
// @Getter
// @Setter
// @ToString
@Data//相当于上面全部的注解 不包括构造器
// @AllArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class Monster {
private Integer id;
private Integer age;
private Date birthday;
private String email;
private Integer gender;
private String name;
private Double salary;
}
package asia.lhweb.lhmybatis.sqlsession;
/**
* 遗嘱执行人
*
* @author 罗汉
* @date 2023/09/16
*/
public interface Executor {
/**
* 查询
*
* @param statement 声明
* @param parameter 参数
* @return {@link T}
*/
public <T>T query(String statement,Object parameter);
}
package asia.lhweb.lhmybatis.sqlsession;
import asia.lhweb.entity.Monster;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author :罗汉
* @date : 2023/9/16
*/
public class LhExecutor implements Executor{
private LhConfiguration lhConfiguration =new LhConfiguration();
/**
* 根据sql查询
*
* @param sql 声明
* @param parameter 参数
* @return {@link T}
*/
@Override
public <T> T query(String sql, Object parameter) {
//得到一个连接
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();
//遍历结果集
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.setSalary(set.getDouble("salary"));
monster.setBirthday(set.getDate("birthday"));
}
return (T)monster;
} catch (SQLException e) {
e.printStackTrace();
}finally {
try {
if (set!=null) set.close();
if (pre!=null) pre.close();
if (connection!=null) connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return null;
}
private Connection getConnection(){
Connection connection = lhConfiguration.build("lh-mybatis.xml");
return connection;
}
}
ctrl+n查找类
会封装2个重要的属性
一个LhConfiguration和一个LhExecutor
只有一个文件
package asia.lhweb.lhmybatis.sqlsession;
/**
* SQL会话
* LhSqlSession:搭建Configuration(连接)和Executor之间的桥梁
* 注意!!原生的有很多方法,这里只写一个 体验流程
* @author 罗汉
* @date 2023/09/16
*/
public class LhSqlSession {
//执行器
private Executor executor=new LhExecutor();
//配置
private LhConfiguration lhConfiguration=new LhConfiguration();
//编写方法SelectOne 返回一条记录(一个对象)
public <T>T selectOne(String statement, Object parameter){
return executor.query(statement, parameter);
}
}
开发Mapper接口和Mapper.xml文件
package asia.lhweb.mapper;
import asia.lhweb.entity.Monster;
/**
* @author :罗汉
* @date : 2023/9/16
*/
public interface MonsterMapper {
//查询方法
Monster getMonsterById(Integer id);
}
这里只是模仿过程,所以用"?"占位符
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="asia.lhweb.mapper.MonsterMapper">
<select id="getMonsterById" resultType="asia.lhweb.entity.Monster">
select * from monster where id=?
</select>
</mapper>
为什么要有这个MapperBean?
将来这个Mapper里面可能有很多方法(虽然目前只写了一个),最终是要用动态代理方法去联系Mapper接口和Mapper.xml。就用这个来记录Mapper里的所有信息。传统方法是通过一个实现类
package asia.lhweb.lhmybatis.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author :罗汉
* @date : 2023/9/16
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Function {
//属性
private String sqlType;//sql类型 比如select,insert,update,delete
private String funcName;//方法名
private String sql;//sql语句
private Object resultType;//返回类型
private String parameterType;//入参类型
}
package asia.lhweb.lhmybatis.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 映射器bean
* MapperBean:将Mapper信息进行封装
*
* @author 罗汉
* @date 2023/09/16
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MapperBean {
private String interfaceName;// 接口全路径
// 接口下的所有方法-集合
private List<Function> functions;
}
要做到的效果 读取到以下设置
MapperBean(interfaceName=asia.lhweb.mapper.MonsterMapper, functions=[Function(sqlType=select, funcName=getMonsterById, sql=select * from monster where id=?, resultType=Monster(id=null, age=null, birthday=null, email=null, gender=null, name=null, salary=null), parameterType=null)])
LhConfiguration.java中添加如下方法readMapper()
public MapperBean readMapper(String path){
MapperBean mapperBean = new MapperBean();
//获取到xml对应的输入流
InputStream resourceAsStream = loader.getResourceAsStream(path);
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(resourceAsStream);
/**
* <mapper namespace="asia.lhweb.mapper.MonsterMapper">
* <select id="getMonsterById" resultType="asia.lhweb.entity.Monster">
* select * from monster where id=?
* </select>
* </mapper>
*/
Element rootElement = document.getRootElement();//得到文件的root元素
//rootElement=mapper
//设置mapperBean的InterfaceName 就是接口的全路径
String namespace = rootElement.attributeValue("namespace").trim();
mapperBean.setInterfaceName(namespace);
//得到rootElement的迭代器
Iterator rootIterator = rootElement.elementIterator();
//保存接口信息
ArrayList<Function> functions = new ArrayList<>();
//开始遍历 生成function
while (rootIterator.hasNext()) {
/**
* 相当于拿到
* <select id="getMonsterById" resultType="asia.lhweb.entity.Monster">
* select * from monster where id=?
* </select>
*/
//取出一个子元素
Element element = (Element) rootIterator.next();//dom4j下的
// System.out.println("element-------------------:"+element);
Function function = new Function();
//设置属性
String sqlType = element.getName().trim();
String funName = element.attributeValue("id");
String resultType = element.attributeValue("resultType");
String sql = element.getText().trim();
function.setFuncName(funName);
function.setSql(sql);
function.setSqlType(sqlType);
//反射创建对象
Class<?> aClass = Class.forName(resultType);
Object instance = aClass.newInstance();
function.setResultType(instance);
functions.add(function);
}
mapperBean.setFunctions(functions);
} catch (Exception e) {
throw new RuntimeException(e);
}
return mapperBean;
}
新建LhMapperProxy.java
package asia.lhweb.lhmybatis.sqlsession;
import asia.lhweb.lhmybatis.config.Function;
import asia.lhweb.lhmybatis.config.MapperBean;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
/**
* Lh映射代理
* 动态代理生成Mapper对象,调用LhExecutor方法
*
* @author 罗汉
* @date 2023/09/18
*/
public class LhMapperProxy implements InvocationHandler {
// 属性
private LhSqlSession lhSqlSession;
private String mapperFile;
private LhConfiguration lhConfiguration;
public LhMapperProxy(LhSqlSession lhSqlSession, Class aClass, LhConfiguration lhConfiguration) {
this.lhSqlSession = lhSqlSession;
this.mapperFile = aClass.getSimpleName() + ".xml";
this.lhConfiguration = lhConfiguration;
}
/**
* 调用
* 当执行接口的动态代理对方方法时,会执行到invoke方法
*
* @param proxy 代理
* @param method 方法
* @param args arg游戏
* @return {@link Object}
* @throws Throwable throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MapperBean mapperBean = lhConfiguration.readMapper(this.mapperFile);
// 判断是否是xml文件对应的接口
if (!method.getDeclaringClass().getName().equals(mapperBean.getInterfaceName())) {
return null;
}
// 取出mapperBean的functions
List<Function> functions = mapperBean.getFunctions();
// 判断当前的mapperBean解析Mapper.xml后有方法
if (null != functions && 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. 因为只是体验流程 这里就做了简化
* 3 主要讲解mybatis 生成mapper动态代理对象,调用方法的机制
*
*/
if ("select".equals(function.getSqlType())) {
return lhSqlSession.selectOne(function.getSql(), String.valueOf(args[0]));
}
}
}
}
return null;
}
}
在LhSqlSession中添加新方法getMapper()
/**
* 1 返回mapper的动态代理对象
* 2 clazz到时传入的是MonsterMapper.class
* 3 放回的就是MonsterMapper接口类型的代理对象
* 4 当执行接口方法时(通过代理对象调用),根据动态代理机制会执行到invoke方法
* @param clazz clazz
* @return {@link T}
*/
public <T> T getMapper(Class<T> clazz) {
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}
, new LhMapperProxy(this, clazz, lhConfiguration));
}
新建LhSessionFactory.java
package asia.lhweb.lhmybatis.sqlsession;
/**
* Lh session工厂
* 返回LhSqlSession
* @author 罗汉
* @date 2023/09/18
*/
public class LhSessionFactory {
public static LhSqlSession openSession(){
return new LhSqlSession();
}
}
完善的写法是一套注解和反射机制-jdbc,这里只是体验流程。多debug体验过程就好了