MyBatis底层机制示意图
mybatis-config.xml
- mybatis-config.xml 是MyBatis全局配置文件,在项目中只能有一份。
- 通过该配置文件可以得到SqlSessionFactory对象
SqlSessionFactory
- 通过SqlSessionFactory可以得到SqlSession,拿到SqlSession就可以操作数据库了。
SqlSession
SqlSession底层是Excutor执行器。
Excutor
- Excutor是接口,定义了很多方法。
- 有两个重要的实现类,
基本执行器 BaseExcutor
和缓存执行器 CacheExcutor
。
Mapped Statement
- 对sql语句的参数及执行结果进行封装。
手写MyBatis框架
1. 思路分析
- 传统方式,通过Connection连接获取PrepareStatement对象,就可以执行sql了
- 使用MyBatis框架思想,则在执行sql之前,从mapper.xml中拿到sql和参数,结果类型。然后封装sql语句。执行sql之后,把得到结果封装到对应的类型之中。(动态代理实现)
2. 搭建Maven项目
配置pom.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.example</groupId>
<artifactId>mybatis_edu</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--解析xml-->
<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>
<!--简化java bean开发-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
</dependency>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
</dependency>
</dependencies>
</project>
阶段一 :读取配置文件,获取连接
(1)配置文件mybatis-config.xml简化版
<?xml version="1.0" encoding="UTF-8" ?>
<database>
<!--配置数据库连接信息-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="root1234"/>
</database>
(2)ConfigurationEdu,模拟Configuration
package com.mybatis.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;
/**
* 读取xml,建立连接
*/
public class ConfigurationEdu {
// 类加载器 ,作用:可以通过类加载器加载配置文件得到对应的流
private ClassLoader classLoader = ClassLoader.getSystemClassLoader();
// 读取配置文件并处理
public Connection build(String resource) {
Connection connection = null;
try {
// 加载配置文件,获取对应inputStream流
InputStream inputStream = classLoader.getResourceAsStream("mybatis-config.xml");
// 解析xml dom4j
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(inputStream);
// 获取配置文件根元素
Element root = document.getRootElement();
connection = evalElement(root);
} catch (DocumentException e) {
throw new RuntimeException(e);
}
return connection;
}
private Connection evalElement(Element node){
String driver = "",url = "",username = "",password = "";
Connection connection = null;
if (!"database".equals(node.getName())){
throw new RuntimeException("root 节点为 <database>");
}
// 遍历node节点下的字节点,获取属性值
for (Object item : node.elements()){
// e 就是 property
Element e = (Element)item;
String name = e.attributeValue("name");
String value = e.attributeValue("value");
// 判断是否得到 name 和 value
if( null == name || null == value){
throw new RuntimeException("未设置name和 value值");
}
switch (name){
case "url" :
url = value;
break;
case "username":
username = value;
break;
case "password" :
password = value;
break;
case "driver":
driver = value;
break;
default:
throw new RuntimeException("未匹配到属性值。");
}
}
try {
Class.forName(driver);
connection = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (SQLException e) {
throw new RuntimeException(e);
}
return connection;
}
}
(3)测试
测试一下,看看能不能获取到连接
阶段二:创建执行器Excutor
(1)通过Lombok创建Monster
package com.mybatis.entity;
import lombok.Data;
import java.util.Date;
@Data
public class Monster {
private Integer id;
private Integer age;
private String name;
private String email;
private Date birthday;
private double salary;
}
(2)Executor接口
package com.mybatis.sqlsession;
/**
* 执行器接口 定义操作数据库的通用方法
*/
public interface Executor {
<T> T query(String sql, Object... params);
}
(3)BaseExecutor
package com.mybatis.sqlsession;
import com.mybatis.entity.Monster;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 编写执行器
* 输入sql语句,完成数据库操作
*/
public class BaseExecutor implements Executor{
private ConfigurationEdu configuration = new ConfigurationEdu();
@Override
public <T> T query(String sql, Object... params) {
Connection connection = getConnection();
PreparedStatement pre = null;
ResultSet resultSet = null;
Monster monster = null;
try {
pre = connection.prepareStatement(sql);
if (params != null || params.length > 0){
for (int i = 0; i < params.length; i++) {
pre.setObject(i+1,params[i]);
}
}
resultSet = pre.executeQuery();
// 封装结果集,底层用反射,在这里先简化封装
monster = new Monster();
while(resultSet.next()){
monster.setId(resultSet.getInt("id"));
monster.setAge(resultSet.getInt("age"));
monster.setBirthday(resultSet.getDate("birthday"));
monster.setName(resultSet.getString("name"));
monster.setEmail(resultSet.getString("email"));
monster.setSalary(resultSet.getDouble("salary"));
}
return (T) monster;
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
try {
if (resultSet != null){
resultSet.close();
}
if (pre != null){
pre.close();
}
if (connection != null){
connection.close();
}
}catch (Exception e){
throw new RuntimeException("资源关闭失败。");
}
}
}
private Connection getConnection(){
Connection connection = configuration.build("mybatis-config.xml");
return connection;
}
}
(4) Junit测试
阶段三:编写SqlSession
(1)编写SqlSession
package com.mybatis.sqlsession;
public class SqlSession {
private Executor executor = new BaseExecutor();
private ConfigurationEdu configuration = new ConfigurationEdu();
public <T> T selectOne(String sql,Object... object){
return executor.query(sql,object);
}
}
(2) Junit测试
阶段四 : 编写MonsterMapper接口与MonsterMapper.xml
(1)MonsterMapper接口
package com.mybatis.mapper;
import com.mybatis.entity.Monster;
/**
* 声明对monster表的CRUD
*/
public interface MonsterMapper {
Monster getMonsterById(Integer id);
}
(2)MonsterMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.mybatis.mapper.MonsterMapper">
<select id="getMonsterById" resultType="com.mybatis.entity.Monster">
select * from `monster` where id = ?
</select>
</mapper>
阶段五 : 开发Function和MapperBean
传统方式 编写完接口之后,会编写实现类,调用实现类完成功能
MyBatis是通过XML配置文件,XML配置文件不可能像Java代码一样运行,因此需要一个类,封装Mapper接口中的方法,并去XML中找到对应的SQL保存到该对象中。
package com.mybatis.config;
/**
* 记录Mapper.xml中对应的方法
*/
public class Function {
private String sqlType; // insert、delete、update、select
private String funName; // 方法名
private String sql; // 执行sql
private Object resultType; // 结果类型
private Object parameterType; // 参数类型
public Function() {
}
public String getSqlType() {
return sqlType;
}
public void setSqlType(String sqlType) {
this.sqlType = sqlType;
}
public String getFunName() {
return funName;
}
public void setFunName(String funName) {
this.funName = funName;
}
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 Object getParameterType() {
return parameterType;
}
public void setParameterType(Object parameterType) {
this.parameterType = parameterType;
}
}
package com.mybatis.config;
import java.util.List;
/**
* 封装Mapper接口信息
*/
public class MapperBean {
private String interfaceName; // 保存接口全路径名
private List<Function> functions; // 接口中的所有方法
public MapperBean() {
}
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;
}
}
阶段六 : 在Configuration中加载mapper.xml,创建MapperBean对象
(1)Configuration
package com.mybatis.sqlsession;
import com.mybatis.config.Function;
import com.mybatis.config.MapperBean;
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.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* 读取xml,建立连接
*/
public class Configuration {
// 类加载器 ,作用:可以通过类加载器加载配置文件得到对应的流
private ClassLoader classLoader = ClassLoader.getSystemClassLoader();
// 读取配置文件并处理
public Connection build(String resource) {
Connection connection = null;
try {
// 加载配置文件,获取对应inputStream流
InputStream inputStream = classLoader.getResourceAsStream("mybatis-config.xml");
// 解析xml dom4j
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(inputStream);
// 获取配置文件根元素
Element root = document.getRootElement();
connection = evalElement(root);
} catch (DocumentException e) {
throw new RuntimeException(e);
}
return connection;
}
private Connection evalElement(Element node){
String driver = "",url = "",username = "",password = "";
Connection connection = null;
if (!"database".equals(node.getName())){
throw new RuntimeException("root 节点为 <database>");
}
// 遍历node节点下的字节点,获取属性值
for (Object item : node.elements()){
// e 就是 property
Element e = (Element)item;
String name = e.attributeValue("name");
String value = e.attributeValue("value");
// 判断是否得到 name 和 value
if( null == name || null == value){
throw new RuntimeException("未设置name和 value值");
}
switch (name){
case "url" :
url = value;
break;
case "username":
username = value;
break;
case "password" :
password = value;
break;
case "driver":
driver = value;
break;
default:
throw new RuntimeException("未匹配到属性值。");
}
}
try {
Class.forName(driver);
connection = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (SQLException e) {
throw new RuntimeException(e);
}
return connection;
}
public MapperBean readMapper(String resource){
InputStream inputStream = classLoader.getResourceAsStream(resource);
SAXReader saxReader = new SAXReader();
MapperBean mapperBean = null;
try {
Document document = saxReader.read(inputStream);
Element root = document.getRootElement();
System.out.println("root = " + root.getName());
mapperBean = evalMapperElement(root);
} catch (DocumentException e) {
throw new RuntimeException(e);
}
return mapperBean;
}
private MapperBean evalMapperElement(Element node){
if (!"mapper".equals(node.getName())){
throw new RuntimeException("mapper 应为根节点");
}
MapperBean mapperBean = new MapperBean();
Function function = null;
String interfaceName = node.attributeValue("namespace");
mapperBean.setInterfaceName(interfaceName);
Iterator iterator = node.elementIterator();
List<Function> functions = new ArrayList<>();
while (iterator.hasNext()){
function = new Function();
Object next = iterator.next();
Element e = (Element)next;
function.setSqlType(e.getName().trim());
function.setFunName(e.attributeValue("id").trim());
//function.setParameterType(e.attributeValue("parameterType").trim());
function.setSql(e.getText().trim());
//function.setResultType(e.attributeValue("resultType").trim());
// 反射生成resultType
Object o = null;
try {
o = Class.forName(e.attributeValue("resultType").trim()).newInstance();
function.setResultType(o);
} catch (InstantiationException ex) {
throw new RuntimeException(ex);
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
} catch (ClassNotFoundException ex) {
throw new RuntimeException(ex);
}
functions.add(function);
}
mapperBean.setFunctions(functions);
return mapperBean;
}
}
(2)Junit测试
阶段七 : 实现动态代理执行Executor方法
(1)创建代理类
package com.mybatis.sqlsession;
import com.mybatis.config.Function;
import com.mybatis.config.MapperBean;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
/**
* MapperProxy: 动态代理生成Mapper对象,调用HspExecutor方法
*/
public class MapperProxy implements InvocationHandler {
//属性
private SqlSession sqlSession;
private String mapperFile;
private Configuration configuration;
//构造器
public MapperProxy(Configuration configuration,
SqlSession sqlSession,
Class clazz) {
this.configuration = configuration;
this.sqlSession = sqlSession;
this.mapperFile = "com/mybatis/mapper/"+clazz.getSimpleName() + ".xml";
}
//当执行Mapper接口的代理对象方法时,会执行到invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MapperBean mapperBean =
configuration.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.getFunName())) {
//如果我们当前的function 要执行的sqlType是select
//我们就去执行selectOne
/**
*
* 老师说明:
* 1. 如果要执行的方法是select , 就对应执行selectOne
* 2. 因为老韩在HspSqlSession就写了一个 selectOne
* 3. 实际上HspSqlSession 应该对应不同的方法(多个方法)
* , 根据不同的匹配情况调用不同方法, 并且还需要进行参数解析处理, 还有比较复杂的字符串处理,拼接sql ,处理返回类型等等工作
* 4. 因为老韩主要是讲解mybatis 生成mapper动态代理对象, 调用方法的机制,所以我做了简化
*/
if("select".equalsIgnoreCase(function.getSqlType())) {
return sqlSession.selectOne(function.getSql(),String.valueOf(args[0]));
}
}
}
}
return null;
}
}
(2)SqlSession中添加getMapper方法
package com.mybatis.sqlsession;
import java.lang.reflect.Proxy;
public class SqlSession {
private Executor executor = new BaseExecutor();
private Configuration configuration = new Configuration();
public <T> T selectOne(String sql,Object... object){
return executor.query(sql,object);
}
/**
* 1. 返回mapper的动态代理对象
* 2. 这里clazz 到时传入的是 MonsterMapper.class
* 3. 返回的就是MonsterMapper接口代理对象
* 4. 当执行接口方法时(通过代理对象调用), 根据动态代理机制会执行到HspMapperProxy-invoke
* @param clazz
* @param <T>
* @return
*/
public <T> T getMapper(Class<T> clazz) {
//返回动态代理对象
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz},
new MapperProxy(configuration,this,clazz));
}
}