太不容易了,靠自己动手实现 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

复制代码

<?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>

创建数据库和表

 

sql

复制代码

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

复制代码

<?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&amp;useUnicode=true&amp;characterEncoding=UTF-8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </database>

创建WyxConfiguration类

 

java

复制代码

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注解

 

java

复制代码

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类

 

java

复制代码

public interface Executor { //泛型方法 public <T> T query(String statement, Object parameter); }

创建WyxExecutor实现类

 

java

复制代码

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测试类 增加方法

 

java

复制代码

@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类

 

java

复制代码

/** * 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增加测试方法

 

java

复制代码

@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

 

java

复制代码

/** * MonsterMapper: 声明对db的crud方法 */ public interface MonsterMapper { //查询方法 public Monster getMonsterById(Integer id); }

创建wyx_mybits.xml 

 

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 的方法信息

 

java

复制代码

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的信息 进行封装

 

java

复制代码

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

 

java

复制代码

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增加测试方法

 

java

复制代码

@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

 

java

复制代码

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")

 

java

复制代码

/** * 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

 

java

复制代码

package com.wyxedu.wyxmybatis.sqlsession; /** * WyxSessionFactory 会话工厂-返回会话WyxSqlSession */ public class WyxSessionFactory { public static WyxSqlSession openSession() { return new WyxSqlSession(); } }

完成测试 

修改 MonsterMapperTest增加测试方法

 

java

复制代码

@Test public void openSession() { WyxSqlSession wyxSqlSession = WyxSessionFactory.openSession(); MonsterMapper mapper = wyxSqlSession.getMapper(MonsterMapper.class); Monster monster = mapper.getMonsterById(1); System.out.println("monster===" + monster); }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值