MyBatis 是个持久层框架,以我曾经的眼光那些框架就是个辅助开发的工具,今年出个A,明年又出来个B,只要弄清楚原理来的再多也不过是老酒装新坛,换汤不换药的东西,但是遇到面试官刁钻的面试题就分分钟教你做人了!(我是谁?我在哪?555~) 所以本着做学问的态度,我觉得有必要剖析一下分享出来~
分享内容大致分为:MyBatis 基本使用,MyBatis 原理剖析,实现自己的 XXBatis
一、MyBatis基本使用
MyBatis
采用ORM
思想将实体类与数据库表进行映射,完成了数据库的持久化操作,其内部对JDBC
进行了封装,使我们不再重复编写麻烦的数据库连接等相关代码,只关注SQL
本身就好了~
1. 准备测试数据
首先创建数据库,生成数据库结构,插入测试数据。这里我使用的MySql
数据库,测试库为test
,表为test1
-- ----------------------------
-- Table structure for test1
-- ----------------------------
DROP TABLE IF EXISTS `test1`;
CREATE TABLE `test1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
`content` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of test1
-- ----------------------------
INSERT INTO `test1` VALUES ('1', 'aaa', 'bbb');
2. 创建工程并导入jar包
为了方便管理jar
包,这里使用Maven
进行配置,直接pom.xml
文件添加坐标即可
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
<scope>runtime</scope>
</dependency>
3. 编写实体类、接口、映射文件
实体类文件,需根据数据库表结构进行编写,总体比较简单
/**
* Test1实体类
*
* @author Wenx
* @date 2021/3/16
*/
public class Test1 implements Serializable {
private Integer id;
private String name;
private String content;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "Test1{" +
"id=" + id +
", name='" + name + '\'' +
", content='" + content + '\'' +
'}';
}
}
持久层接口,则根据实际业务需要进行编写,大致的增删改查是少不了的,这里为了演示只写了个查询方法
/**
* Test1持久层接口
*
* @author Wenx
* @date 2021/3/16
*/
public interface ITest1 {
/**
* 查询所有数据
*
* @return
*/
List<Test1> findAll();
}
xml
映射文件对应持久层接口进行编写即可。需注意该文件一般放在resources
目录下,具有与持久层接口相同包名,该资源包目录与代码目录不同,需要一层层建立,否则不会建立com.xxxx.xxxx
多级结构,而是名为com.xxxx.xxxx
的一级目录
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wenx.demo.basis.mybatis.dao.ITest1">
<!-- 配置查询所有操作 -->
<select id="findAll" resultType="com.wenx.demo.basis.mybatis.entity.Test1">
select * from test1
</select>
</mapper>
4. 编写相关配置文件
创建SqlMapConfig.xml
文件,配置下数据库连接及xml
映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置 mybatis 的环境 -->
<environments default="mysql">
<!-- 配置 mysql 的环境 -->
<environment id="mysql">
<!-- 配置事务的类型 -->
<transactionManager type="JDBC"></transactionManager>
<!-- 配置连接数据库的信息:用的是数据源(连接池) -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.126.129:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="1234@abcd"/>
</dataSource>
</environment>
</environments>
<!-- 告知 mybatis 映射配置的位置 -->
<mappers>
<mapper resource="com/wenx/demo/basis/mybatis/dao/ITest1.xml"/>
<!--<mapper class="com.wenx.demo.basis.mybatis.dao.ITest1"/>-->
</mappers>
</configuration>
5. 编写Mybatis测试代码
Mybatis
还有很多如注解、spring
整合等使用方式,将会使开发更加简单,但这里为介绍原理仅演示Mybatis
最常规的用法
/**
* 原生Mybatis演示
*
* @author Wenx
* @date 2021/3/17
*/
public class MybatisDemo {
public static void main(String[] args) throws Exception {
// 1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2.创建 SqlSessionFactory 的构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 3.使用构建者创建工厂对象 SqlSessionFactory
SqlSessionFactory factory = builder.build(in);
// 4.使用 SqlSessionFactory 生产 SqlSession 对象
SqlSession session = factory.openSession();
// 5.使用 SqlSession 创建 dao 接口的代理对象
ITest1 test1Dao = session.getMapper(ITest1.class);
// 6.使用代理对象执行查询所有方法
List<Test1> test1s = test1Dao.findAll();
for (Test1 test1 : test1s) {
System.out.println(test1);
}
// 7.释放资源
session.close();
in.close();
}
}
二、MyBatis原理剖析
针对MyBatis
测试代码我们一步步进行原理剖析,对其过程中运用到的编程思想和设计模式进行简明扼要的表述
1. 通过类加载器读取配置文件
读取配置文件是我们常用的功能,每个项目基本都会有很多配置,写在多个代码中改起来太麻烦,要是忘记写哪个文件里找起来更会让你痛不欲生,而将配置参数统一写到配置文件是必然的趋势,下面我们举例几种常见加载配置文件的方式
// MyBatis测试代码使用的读取配置文件方式,内部使用ClassLoder类加载器
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
// 1.基于ClassLoder读取配置文件,该方式只能加载类路径下的配置文件
InputStream is1 = MybatisTest.class.getClassLoader().getResourceAsStream("SqlMapConfig.xml");
// 2.通过ServletContext的getRealPath方法获取绝对路径,该方式只能在servlet内使用
String realPath = ServletContext.getRealPath("SqlMapConfig.xml");
InputStream is2 = new FileInputStream(new File(realPath));
2. 创建构造者对象 SqlSessionFactoryBuilder
看到Builder
后缀我们能想到构造者模式,对象的创建可能需要较多的配置参数,而对象构造过程相对复杂,构造者模式能够隐藏构造细节让使用者直接获取到对象。MyBatis
构造者对象实际就是解析了SqlMapConfig.xml
和ITest1.xml
文件,通过读取配置文件流将配置信息存到Configuration
中,来构造出工厂对象SqlSessionFactory
// 创建 SqlSessionFactory 的构造者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 通过 build 构造出工厂对象 SqlSessionFactory
SqlSessionFactory factory = builder.build(in);
3. 创建工厂对象 SqlSessionFactory
看到Factory
后缀我们能想到工厂模式,这个设计模式避免我们在代码中使用new
来创建对象,防止由初始化对象方式改变而通篇去修改new
代码,降低类之间的依赖关系达到解耦目的。另外还应用了单例模式,一旦被创建在应用的运行期间将一直存在并保持全局唯一,下面我们来看下其内部的SqlSessionFactory
是如何创建的
// 内部通过 XMLConfigBuilder 类来解析xml配置文件生成 Configuration 对象,来构造出工厂对象 SqlSessionFactory
SqlSessionFactory factory = builder.build(in);
SqlSessionFactoryBuilder
类build
方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
}
}
return var5;
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
4. 创建会话对象 SqlSession
在SqlSessionFactory
内部,加载配置文件中的配置信息来生产SqlSession
对象
// 使用 SqlSessionFactory 生产 SqlSession 对象,而不用关心 SqlSession 对象是怎么 new 来的
// 我们 ctrl 键一路点下去,发现 DefaultSqlSessionFactory 类的 openSessionFromDataSource 方法
// 其中 Environment environment = this.configuration.getEnvironment(); 这句正是加载配置文件中的配置信息
// 而 Executor executor = this.configuration.newExecutor(tx, execType); 来创建 Executor 实例用于执行 SQL
SqlSession session = factory.openSession();
DefaultSqlSessionFactory
类openSessionFromDataSource
方法
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession var8;
try {
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
Executor executor = this.configuration.newExecutor(tx, execType);
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception var12) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12);
} finally {
ErrorContext.instance().reset();
}
return var8;
}
Configuration
类newExecutor
方法,这里CachingExecutor
用了装饰器模式,是否开启二级缓存,相当于不改变原有executor
的情况下包装了层壳,来提供额外的功能
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? this.defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Object executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (this.cacheEnabled) {
executor = new CachingExecutor((Executor)executor);
}
Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
return executor;
}
5. 创建持久层接口的代理对象
使用SqlSession
创建dao
接口的代理对象,内部运用代理模式,在不改变源码前提下对已有方法进行增强。其实把面向对象的三种特性封装、继承、多态整明白了,设计模式就那么回事了~
// 使用 SqlSession 创建 dao 接口的代理对象,ctrl 键一路点下去我们可以发现其内部使用了动态代理,也可以理解为将持久层接口与mapper映射文件做了关联,调用接口方法实际获取了mapper映射文件的SQL来执行
ITest1 test1Dao = session.getMapper(ITest1.class);
MapperRegistry
类getMapper
方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
MapperProxyFactory
类newInstance
方法,内部用了动态代理
protected T newInstance(MapperProxy<T> mapperProxy) {
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
6. 执行查询方法
使用代理对象执行查询方法
// 使用代理对象执行查询所有方法,实际获取了mapper映射文件的SQL来执行
List<Test1> test1s = test1Dao.findAll();
for (Test1 test1 : test1s) {
System.out.println(test1);
}
7. 释放资源
执行完成后,可释放sql
会话,释放配置文件流
// 释放资源
session.close();
in.close();
三、实现自己的XXBatis
上面一节我们对MyBatis
原理进行了剖析,下面我们删除MyBatis
包,创建与MyBatis
保持相同的结构和类名来实现自己的XXBatis
,看看是否可以达到MyBatis
的效果
1. 编写XXBatis测试代码
XXBatis
测试代码与原始原生Mybatis
代码完全相同,但由于删除MyBatis
包导致代码变红,很多类找不到,我们接下来逐步补全代码
package com.wenx.demo.basis.mybatis;
import com.wenx.demo.basis.mybatis.dao.ITest1;
import com.wenx.demo.basis.mybatis.entity.Test1;
import com.wenx.demo.basis.mybatis.session.SqlSession;
import com.wenx.demo.basis.mybatis.session.SqlSessionFactory;
import com.wenx.demo.basis.mybatis.session.SqlSessionFactoryBuilder;
import com.wenx.demo.basis.mybatis.utils.Resources;
import java.io.InputStream;
import java.util.List;
/**
* 自制XXbatis演示
*
* @author Wenx
* @date 2021/3/17
*/
public class XXbatisDemo {
public static void main(String[] args) throws Exception {
// 1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2.创建 SqlSessionFactory 的构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 3.使用构建者创建工厂对象 SqlSessionFactory
SqlSessionFactory factory = builder.build(in);
// 4.使用 SqlSessionFactory 生产 SqlSession 对象
SqlSession session = factory.openSession();
// 5.使用 SqlSession 创建 dao 接口的代理对象
ITest1 test1Dao = session.getMapper(ITest1.class);
// 6.使用代理对象执行查询所有方法
List<Test1> test1s = test1Dao.findAll();
for (Test1 test1 : test1s) {
System.out.println(test1);
}
// 7.释放资源
session.close();
in.close();
}
}
2. Resources加载配置文件
首先是Resources
类,我们可以直接使用ClassLoader
加载配置文件
package com.wenx.demo.basis.mybatis.utils;
import java.io.InputStream;
/**
* 使用ClassLoader加载配置文件
*
* @author Wenx
* @date 2021/3/17
*/
public class Resources {
/**
* 获取文件字节输入流
*
* @param filePath 配置文件地址
* @return
*/
public static InputStream getResourceAsStream(String filePath) {
return Resources.class.getClassLoader().getResourceAsStream(filePath);
}
}
3. SqlSessionFactoryBuilder构造者
接下来是SqlSessionFactoryBuilder
,我们模仿mybatis
结构,简化下代码保留核心类XMLConfigBuilder
和DefaultSqlSessionFactory
package com.wenx.demo.basis.mybatis.session;
import com.wenx.demo.basis.mybatis.session.defaults.DefaultSqlSessionFactory;
import com.wenx.demo.basis.mybatis.utils.XMLConfigBuilder;
import java.io.InputStream;
/**
* SqlSessionFactory构造者
*
* @author Wenx
* @date 2021/3/17
*/
public class SqlSessionFactoryBuilder {
/**
* 构建SqlSessionFactory工厂对象
*
* @param inputStream 配置文件字节输入流
* @return
*/
public SqlSessionFactory build(InputStream inputStream) {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream);
return new DefaultSqlSessionFactory(parser.parse());
}
}
4. XMLConfigBuilder配置解析
配置解析方式有2种,一种是XML
方式,另一种是注解方式。XML
我们使用dom4j
+xpath
来解析配置文件,注解我们用反射来弄,最后将配置信息填充到Configuration
和MappedStatement
中去
package com.wenx.demo.basis.mybatis.utils;
import com.wenx.demo.basis.mybatis.annotations.Select;
import com.wenx.demo.basis.mybatis.session.Configuration;
import com.wenx.demo.basis.mybatis.session.MappedStatement;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 解析XML配置文件
*
* @author Wenx
* @date 2021/3/17
*/
public class XMLConfigBuilder {
private final InputStream inputStream;
public XMLConfigBuilder(InputStream inputStream) {
this.inputStream = inputStream;
}
/**
* 获得Configuration对象
*
* @return
*/
public Configuration parse() {
return parsingConfiguration(inputStream);
}
/**
* 解析XML配置文件,将配置信息填充到Configuration中
* 使用的技术:dom4j+xpath
*
* @param inputStream 配置文件字节输入流
* @return 封装数据库连接信息和映射文件信息的Configuration对象
*/
public static Configuration parsingConfiguration(InputStream inputStream) {
try {
// 定义封装数据库连接信息和映射文件信息的配置对象
Configuration config = new Configuration();
// 开始解析XML
// 1.获取SAXReader对象
SAXReader reader = new SAXReader();
// 2.根据字节输入流获取Document对象
Document document = reader.read(inputStream);
// 3.获取根节点
Element root = document.getRootElement();
// 4.获取数据库配置节点,使用xpath中选择指定节点的方式,获取所有property节点
List<Element> propertyElements = root.selectNodes("//property");
// 5.遍历节点
for (Element propertyElement : propertyElements) {
// 取出节点的name属性的值,判断是否为数据库连接信息
String name = propertyElement.attributeValue("name");
if ("driver".equals(name)) {
// 表示驱动,获取property标签value属性的值
String driver = propertyElement.attributeValue("value");
config.setDriver(driver);
}
if ("url".equals(name)) {
// 表示连接字符串,获取property标签value属性的值
String url = propertyElement.attributeValue("value");
config.setUrl(url);
}
if ("username".equals(name)) {
// 表示用户名,获取property标签value属性的值
String username = propertyElement.attributeValue("value");
config.setUsername(username);
}
if ("password".equals(name)) {
// 表示密码,获取property标签value属性的值
String password = propertyElement.attributeValue("value");
config.setPassword(password);
}
}
// 6.获取映射文件信息,取出mappers中的所有mapper标签,判断他们使用了resource还是class属性
List<Element> mapperElements = root.selectNodes("//mappers/mapper");
// 7.遍历集合
for (Element mapperElement : mapperElements) {
// 判断mapperElement使用的是哪个属性
Attribute attribute = mapperElement.attribute("resource");
if (attribute != null) {
// 表示有resource属性,使用的是XML,取出属性的值
String mapperPath = attribute.getValue();
// 把映射配置文件的内容获取出来,封装成一个map
Map<String, MappedStatement> mappers = parsingXML(mapperPath);
// 给configuration中的mappers赋值
config.setMappers(mappers);
} else {
// 表示没有resource属性,使用的是注解,获取class属性的值
String daoClassPath = mapperElement.attributeValue("class");
// 根据daoClassPath获取封装的必要信息
Map<String, MappedStatement> mappers = parsingAnnotation(daoClassPath);
// 给configuration中的mappers赋值
config.setMappers(mappers);
}
}
// 返回Configuration
return config;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 解析XML,并且封装到Map中
*
* @param mapperPath 映射配置文件的位置
* @return map中包含了获取的唯一标识(key是由dao的全限定类名和方法名组成)
* 以及执行所需的必要信息(value是一个MappedStatement对象,里面存放的是执行的SQL语句和要封装的实体类全限定类名)
* @throws IOException
*/
private static Map<String, MappedStatement> parsingXML(String mapperPath) throws IOException {
InputStream in = null;
try {
// 定义返回值对象
Map<String, MappedStatement> mappers = new HashMap<String, MappedStatement>();
// 开始解析XML
// 1.根据路径获取字节输入流
in = Resources.getResourceAsStream(mapperPath);
// 2.根据字节输入流获取Document对象
SAXReader reader = new SAXReader();
Document document = reader.read(in);
// 3.获取根节点
Element root = document.getRootElement();
// 4.获取根节点的namespace属性的值,作为map中key的前半部分
String namespace = root.attributeValue("namespace");
// 5.获取所有的select节点
List<Element> selectElements = root.selectNodes("//select");
// 6.遍历select节点集合
for (Element selectElement : selectElements) {
// 取出id属性的值,作为map中key的后半部分
String id = selectElement.attributeValue("id");
// 取出resultType属性的值,作为map中value的一部分
String resultType = selectElement.attributeValue("resultType");
// 取出文本内容,作为map中value的一部分
String sqlStatement = selectElement.getText();
// 创建Key
String key = namespace + "." + id;
// 创建Value
MappedStatement mapper = new MappedStatement();
mapper.setSqlStatement(sqlStatement);
mapper.setResultType(resultType);
// 把key和value存入mappers中
mappers.put(key, mapper);
}
return mappers;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
in.close();
}
}
/**
* 解析dao中所有被select注解标注的方法,并且封装到Map中
* 根据方法名称和类名,以及方法上注解value属性的值,作为MappedStatement的必要信息
*
* @param daoClassPath dao类文件的位置
* @return map中包含了获取的唯一标识(key是由dao的全限定类名和方法名组成)
* 以及执行所需的必要信息(value是一个MappedStatement对象,里面存放的是执行的SQL语句和要封装的实体类全限定类名)
* @throws Exception
*/
private static Map<String, MappedStatement> parsingAnnotation(String daoClassPath) throws Exception {
// 定义返回值对象
Map<String, MappedStatement> mappers = new HashMap<String, MappedStatement>();
// 1.得到dao接口的字节码对象
Class daoClass = Class.forName(daoClassPath);
// 2.得到dao接口中的方法数组
Method[] methods = daoClass.getMethods();
// 3.遍历Method数组
for (Method method : methods) {
// 取出每一个方法,判断是否有select注解
boolean isAnnotated = method.isAnnotationPresent(Select.class);
if (isAnnotated) {
// 创建MappedStatement对象
MappedStatement mapper = new MappedStatement();
// 取出注解的value属性值
Select selectAnnotation = method.getAnnotation(Select.class);
String sqlStatement = selectAnnotation.value();
mapper.setSqlStatement(sqlStatement);
// 获取当前方法的返回值,还要求必须带有泛型信息 List<T>
Type type = method.getGenericReturnType();
// 判断type是不是参数化的类型
if (type instanceof ParameterizedType) {
// 强转
ParameterizedType ptype = (ParameterizedType) type;
// 得到参数化类型中的实际类型参数
Type[] types = ptype.getActualTypeArguments();
// 取出第一个
Class domainClass = (Class) types[0];
// 获取domainClass的类名
String resultType = domainClass.getName();
// 给MappedStatement赋值
mapper.setResultType(resultType);
}
// 组装key的信息
// 获取方法的名称
String methodName = method.getName();
String className = method.getDeclaringClass().getName();
String key = className + "." + methodName;
// 给map赋值
mappers.put(key, mapper);
}
}
return mappers;
}
}
5. Configuration配置类
我们从配置文件解析出来的配置信息,数据库连接信息与映射文件信息都存到配置类中,其中MappedStatement
是映射文件配置信息
package com.wenx.demo.basis.mybatis.session;
import java.util.HashMap;
import java.util.Map;
/**
* 自制xxbatis配置类
*
* @author Wenx
* @date 2021/3/17
*/
public class Configuration {
/**
* mysql数据库驱动
*/
private String driver;
/**
* mysql数据库连接字符串
*/
private String url;
/**
* mysql数据库用户名
*/
private String username;
/**
* mysql数据库密码
*/
private String password;
/**
* 映射文件配置信息
*/
private Map<String, MappedStatement> mappers = new HashMap<String, MappedStatement>();
public String getDriver() {
return driver;
}
public void setDriver(String driver) {
this.driver = driver;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Map<String, MappedStatement> getMappers() {
return mappers;
}
public void setMappers(Map<String, MappedStatement> mappers) {
// 使用追加的方式
this.mappers.putAll(mappers);
}
}
6. MappedStatement映射配置类
映射配置类我们主要用于存储映射XML
文件中的SQL
语句和返回结果实体类的全限定类名
package com.wenx.demo.basis.mybatis.session;
/**
* 封装执行的SQL语句和结果类型的全限定类名
*
* @author Wenx
* @date 2021/3/17
*/
public class MappedStatement {
/**
* 执行的SQL语句
*/
private String sqlStatement;
/**
* 结果类型实体类的全限定类名
*/
private String resultType;
public String getSqlStatement() {
return sqlStatement;
}
public void setSqlStatement(String sqlStatement) {
this.sqlStatement = sqlStatement;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
}
7. 注解方式的Select注解
使用注解方式可以省略映射XML
文件,直接在dao
接口类方法上使用注解填写SQL
语句即可使用,但修改SQL
语句需要重新编译,可谓是各有利弊需要看实际情况使用,这里编写个Select
注解为测试使用
package com.wenx.demo.basis.mybatis.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 查询语句的注解
*
* @author Wenx
* @date 2021/3/17
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
/**
* SQL语句
*
* @return
*/
String value();
}
8. SqlSessionFactory接口
接下来的就是SqlSessionFactory
接口,其中主要方法为openSession()
方法
package com.wenx.demo.basis.mybatis.session;
/**
* SqlSession工厂
*
* @author Wenx
* @date 2021/3/17
*/
public interface SqlSessionFactory {
/**
* 打开一个会话,获得SqlSession对象
*
* @return
*/
SqlSession openSession();
}
9. DefaultSqlSessionFactory实现类
DefaultSqlSessionFactory
类为SqlSessionFactory
接口的实现。JdbcTransaction
类使用Jdbc
建立数据库连接并操作事务,Executor
类主要用来执行具体SQL
语句,DefaultSqlSession
类为SqlSession
接口实现,用来创建dao
接口的代理对象
package com.wenx.demo.basis.mybatis.session.defaults;
import com.wenx.demo.basis.mybatis.session.Configuration;
import com.wenx.demo.basis.mybatis.session.SqlSession;
import com.wenx.demo.basis.mybatis.session.SqlSessionFactory;
import com.wenx.demo.basis.mybatis.utils.Executor;
import com.wenx.demo.basis.mybatis.utils.JdbcTransaction;
/**
* SqlSessionFactory接口实现
*
* @author Wenx
* @date 2021/3/17
*/
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
/**
* 获取一个数据库连接会话
*
* @return
*/
@Override
public SqlSession openSession() {
JdbcTransaction tx = new JdbcTransaction(configuration, false);
Executor executor = new Executor(tx);
return new DefaultSqlSession(configuration, executor);
}
}
10. JdbcTransaction实现类
JdbcTransaction
类为Transaction
接口的实现,这里为减少复杂程度Transaction
接口就没有创建,其内部是对Jdbc
连接数据库操作的封装
package com.wenx.demo.basis.mybatis.utils;
import com.wenx.demo.basis.mybatis.session.Configuration;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* Jdbc事务相关
*
* @author Wenx
* @date 2021/3/17
*/
public class JdbcTransaction {
private final Configuration configuration;
protected Connection connection;
protected boolean autoCommmit;
public JdbcTransaction(Configuration configuration, boolean desiredAutoCommit) {
this.configuration = configuration;
this.autoCommmit = desiredAutoCommit;
}
/**
* 获取一个数据库连接
*
* @return
* @throws SQLException
*/
public Connection getConnection() throws SQLException {
if (this.connection == null) {
this.openConnection();
}
return this.connection;
}
/**
* 创建一个数据库连接
*
* @throws SQLException
*/
protected void openConnection() throws SQLException {
try {
Class.forName(this.configuration.getDriver());
this.connection = DriverManager.getConnection(
this.configuration.getUrl(),
this.configuration.getUsername(),
this.configuration.getPassword());
this.connection.setAutoCommit(this.autoCommmit);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 事务提交
*
* @throws SQLException
*/
public void commit() throws SQLException {
if (this.connection != null && !this.connection.getAutoCommit()) {
this.connection.commit();
}
}
/**
* 事务回滚
*
* @throws SQLException
*/
public void rollback() throws SQLException {
if (this.connection != null && !this.connection.getAutoCommit()) {
this.connection.rollback();
}
}
/**
* 关闭数据库连接
*
* @throws SQLException
*/
public void close() throws SQLException {
if (this.connection != null) {
this.resetAutoCommit();
this.connection.close();
}
}
/**
* 重置事务自动提交
*/
protected void resetAutoCommit() {
try {
if (!this.connection.getAutoCommit()) {
this.connection.setAutoCommit(true);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
11. Executor类
Executor
类主要用来执行具体SQL
语句,实际就是Jdbc
中操作PreparedStatement
,执行executeQuery
方法来运行SQL
语句
package com.wenx.demo.basis.mybatis.utils;
import com.wenx.demo.basis.mybatis.session.MappedStatement;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* 用于执行SQL语句
*
* @author Wenx
* @date 2021/3/17
*/
public class Executor {
private final JdbcTransaction transaction;
public Executor(JdbcTransaction transaction) {
this.transaction = transaction;
}
public void close() {
try {
if (this.transaction != null) {
this.transaction.close();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public <E> List<E> query(MappedStatement mapper) {
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 1.取出MappedStatement中的数据
String sqlStatement = mapper.getSqlStatement();
String resultType = mapper.getResultType();
Class entityClass = Class.forName(resultType);
// 2.获取PreparedStatement对象
Connection conn = this.transaction.getConnection();
pstmt = conn.prepareStatement(sqlStatement);
// 3.执行SQL语句,获取结果集
rs = pstmt.executeQuery();
// 4.封装结果集
List<E> list = new ArrayList<E>();
while (rs.next()) {
// 实例化要封装的实体类对象
E obj = (E) entityClass.newInstance();
// 取出结果集的元信息:ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
// 取出总列数
int columnCount = rsmd.getColumnCount();
// 遍历总列数
for (int i = 1; i <= columnCount; i++) {
// 获取每列的名称,列名的序号是从1开始的
String columnName = rsmd.getColumnName(i);
// 根据得到列名,获取每列的值
Object columnValue = rs.getObject(columnName);
// 给obj赋值:使用Java内省机制(借助PropertyDescriptor实现属性的封装)
// 要求:实体类的属性和数据库表的列名保持一种
PropertyDescriptor pd = new PropertyDescriptor(columnName, entityClass);
// 获取它的写入方法
Method writeMethod = pd.getWriteMethod();
// 把获取的列的值,给对象赋值
writeMethod.invoke(obj, columnValue);
}
// 把赋好值的对象加入到集合中
list.add(obj);
}
this.transaction.commit();
return list;
} catch (Exception e) {
e.printStackTrace();
try {
this.transaction.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
} finally {
release(pstmt, rs);
}
return null;
}
private void release(PreparedStatement pstmt, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (pstmt != null) {
try {
pstmt.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
12. SqlSession接口
SqlSession
接口,其中getMapper
方法用来创建dao
接口的代理对象,在不改变源码前提下对已有方法进行增强
package com.wenx.demo.basis.mybatis.session;
/**
* 数据库连接会话,可创建dao接口的代理对象
*
* @author Wenx
* @date 2021/3/17
*/
public interface SqlSession {
/**
* 创建dao接口的代理对象
*
* @param daoInterfaceClass dao的接口字节码
* @param <T>
* @return
*/
<T> T getMapper(Class<T> daoInterfaceClass);
/**
* 释放资源
*/
void close();
}
13. DefaultSqlSession实现类
DefaultSqlSession
类为SqlSession
接口的实现。用来创建dao
接口的代理对象,使用动态代理方式通过MapperProxy
类来为持久层接口与mapper
映射文件建立关联,调用接口方法实际获取了mapper
映射文件的SQL
来执行
package com.wenx.demo.basis.mybatis.session.defaults;
import com.wenx.demo.basis.mybatis.session.Configuration;
import com.wenx.demo.basis.mybatis.session.MapperProxy;
import com.wenx.demo.basis.mybatis.session.SqlSession;
import com.wenx.demo.basis.mybatis.utils.Executor;
import java.lang.reflect.Proxy;
/**
* SqlSession接口实现
*
* @author Wenx
* @date 2021/3/17
*/
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
public DefaultSqlSession(Configuration configuration, Executor executor) {
this.configuration = configuration;
this.executor = executor;
}
/**
* 创建dao接口的代理对象
*
* @param daoInterfaceClass dao的接口字节码
* @param <T>
* @return
*/
@Override
public <T> T getMapper(Class<T> daoInterfaceClass) {
return (T) Proxy.newProxyInstance(
daoInterfaceClass.getClassLoader(),
new Class[]{daoInterfaceClass},
new MapperProxy(configuration.getMappers(), this.executor));
}
/**
* 释放资源
*/
@Override
public void close() {
if (executor != null) {
try {
executor.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
14. MapperProxy代理
MapperProxy
类内部主要是个invoke
增强方法,用于对dao
接口方法进行增强,调用接口时直接获取对应mapper
映射文件的SQL
语句来执行
package com.wenx.demo.basis.mybatis.session;
import com.wenx.demo.basis.mybatis.utils.Executor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
/**
* @author Wenx
* @date 2021/3/17
*/
public class MapperProxy implements InvocationHandler {
/**
* map的key是dao的全限定类名+方法名
*/
private final Map<String, MappedStatement> mappers;
private final Executor executor;
public MapperProxy(Map<String, MappedStatement> mappers, Executor executor) {
this.mappers = mappers;
this.executor = executor;
}
/**
* 用于对dao方法进行增强,实际就是与mapper映射文件做了关联,获取了mapper映射文件的SQL来执行
*
* @param proxy 类加载器
* @param method dao接口方法
* @param args 参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1.获取方法名
String methodName = method.getName();
// 2.获取方法所在类的名称
String className = method.getDeclaringClass().getName();
// 3.组合key
String key = className + "." + methodName;
// 4.获取mappers中的MappedStatement对象
MappedStatement mapper = this.mappers.get(key);
// 5.判断是否有MappedStatement
if (mapper == null) {
throw new IllegalArgumentException("参数不合法");
}
// 6.调用工具类执行SQL语句
return this.executor.query(mapper);
}
}
总结: 所有代码撸完一运行,好使~ 脱离MyBatis
包也可以用,说明我们对MyBatis
原理剖析的没错,跟着我做一遍之后是不是你对MyBatis
的理解又加深了呢?那就是我分享的动力,感谢大家能看到文章最底下~