本文转载于:https://www.qfcwx.top/2019/04/23/shou-xie-yi-ge-jian-dan-de-mybatis/
作者:清风
本文代码参考了:"享学课堂——手写MyBatis"的课程。如有侵权请立即与本人联系,本人将及时删除相关的代码和文献。
一、初始化阶段:读取xml配置文件和注解中的配置信息,创建配置对象,并完成各个模块的初始化工作。
二、代理阶段:封装iBatis的编程模型,使用mapper接口开发的初始化工作。
三、数据读写阶段:通过SqlSession完成SQL解析,参数的映射,SQL的执行,结果的反射解析过程。
首先创建连接数据库的配置文件db.properties
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=UTC&useSSL=false&nullNamePatternMatchesAll=true
username=root
password=root
pom中导入相关的约束,最重要的就是dom4j,用来解析mapper.xml文件。还有MySQL的驱动。
<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.0modelVersion>
<groupId>com.qfcwxgroupId>
<artifactId>simple-mybatisartifactId>
<version>1.0-SNAPSHOTversion>
<properties>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.4.6version>
dependency>
<dependency>
<groupId>dom4jgroupId>
<artifactId>dom4jartifactId>
<version>1.6.1version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.15version>
dependency>
dependencies>
project>
新建一个实体类。
package com.qfcwx.pojo;
import java.io.Serializable;
/**
* @ClassName: User
* @Description: TODO
* @Date: 2019/4/18 17:36
* @Version 1.0
**/
public class User implements Serializable {
private Long id;
private String username;
private String password;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
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;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
有三个属性,分别与数据库对应,然后创建mapper接口和mapper.xml文件。
public interface UserMapper {
/**
* //TODO 根据id查询user对象
*
* @param id 用户id
* @return com.qfcwx.pojo.User
*/
User selectUserById(Long id);
/**
* //TODO 查询所有用户
*
* @return java.util.List
*/
List<User> selectList();
}
然后在resource文件夹新建一个mapper文件夹,建立UserMapper.xml
<mapper namespace="com.qfcwx.mapper.UserMapper">
<select id="selectUserById" resultType="com.qfcwx.pojo.User">
SELECT
id,username,password
FROM
user
WHERE
id = ?
select>
<select id="selectList" resultType="com.qfcwx.pojo.User">
SELECT
id,username,password
FROM
user
select>
mapper>
准备工作完成,接下来就是重头戏了。
需要创建一个与mapper.xml中标签和属性对应的实体类。其中包含(namespace、id、resultType、sql…)等。
package com.qfcwx.config;
/**
* @ClassName: MappedStatement
* @Description: TODO 映射mapper.xml的实体类namespace,id,resultType,sql...等,对应了一条sql语句
* @Date: 2019/4/18 11:14
* @Version 1.0
**/
public class MappedStatement {
private String namespace;
private String sourceId;
private String resultType;
private String sql;
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public String getSourceId() {
return sourceId;
}
public void setSourceId(String sourceId) {
this.sourceId = sourceId;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
}
一个MappedStatement对象对应一条sql语句。接下来。新建一个配置类。读取所有配置文件(db.properties,mapper.xml)
package com.qfcwx.config;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName: Configuration
* @Description: TODO 对应mybatis-config.xml。读取所有配置文件,包括db.properties,mapper.xml
* @Date: 2019/4/18 11:20
* @Version 1.0
**/
public class Configuration {
private String driver;
private String url;
private String userName;
private String passWord;
private Map<String, MappedStatement> statementMap = 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> getStatementMap() {
return statementMap;
}
public void setStatementMap(Map<String, MappedStatement> statementMap) {
this.statementMap = statementMap;
}
}
接下来就进入第一个阶段,读取xml配置文件和注解中的配置信息,创建配置对象,并完成各个模块的初始化工作。
SqlSession是MyBatis的关键对象,通过java操作MyBatis时,可看到,它是由SqlSessionFactory这个工厂来创建的。所以需要先完成SqlSessionFactory的相关代码。
public class SqlSessionFactory {
private final Configuration configuration = new Configuration();
/**
* 记录mapper.xml存放的位置
**/
private static final String MAPPER_CONFIG_LOCATION = "mapper";
/**
* 记录数据库连接信息存放的文件
**/
private static final String DB_CONFIG_FILE = "db.properties";
public SqlSessionFactory() {
loadDBInfo();
loadMappersInfo();
}
/**
* //TODO 读取数据库配置文件信息
*
* @param
* @return void
**/
private void loadDBInfo() {
//加载数据库信息配置文件
InputStream stream = SqlSessionFactory.class.getClassLoader().getResourceAsStream(DB_CONFIG_FILE);
Properties properties = new Properties();
try {
properties.load(stream);
} catch (IOException e) {
e.printStackTrace();
}
//将数据库配置信息写入configuration对象中
configuration.setDriver(properties.get("driver").toString());
configuration.setUrl(properties.get("url").toString());
configuration.setUserName(properties.get("username").toString());
configuration.setPassWord(properties.get("password").toString());
}
/**
* //TODO 获取指定文件下的所有mapper.xml文件
*
* @param
* @return void
**/
private void loadMappersInfo() {
URL resource = null;
resource = SqlSessionFactory.class.getClassLoader().getResource(MAPPER_CONFIG_LOCATION);
//获取指定文件夹信息
File file = new File(resource.getFile());
if (file.isDirectory()) {
File[] mappers = file.listFiles();
//遍历文件夹下所有的mapper.xml文件,解析后,注册到configuration中
for (File mapper : mappers) {
loadMapper(mapper);
}
}
}
/**
* //TODO 对mapper.xml文件解析
*
* @param mapper
* @return void
**/
private void loadMapper(File mapper) {
//创建SAXReader对象
SAXReader saxReader = new SAXReader();
//通过read方法读取一个文件,转换成Document对象
Document document = null;
try {
document = saxReader.read(mapper);
} catch (DocumentException e) {
e.printStackTrace();
}
//获取根节点元素对象
Element rootElement = document.getRootElement();
//获取命名空间
String namespace = rootElement.attribute("namespace").getData().toString();
//获取子节点
List<Element> selects = rootElement.elements("select");
//遍历select节点,将信息记录到MappedStatement对象,并登记到Configuration对象中
for (Element element : selects) {
MappedStatement statement = new MappedStatement();
String id = element.attribute("id").getData().toString();
String resultType = element.attribute("resultType").getData().toString();
//读取sql语句信息
String sql = element.getData().toString();
String sourceId = namespace + "." + id;
//给MappedStatement对象赋值
statement.setSourceId(sourceId);
statement.setNamespace(namespace);
statement.setResultType(resultType);
statement.setSql(sql);
configuration.getStatementMap().put(sourceId, statement);
}
}
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
以上代码都有相应的注释,同时,也很基础,很容易理解。这里就不过多的进行解释了。
SqlSessionFactory在这里主要有两个功能:
①. 读取配置文件,解析信息,填充到configuration中。
②. 生产SqlSession
MyBatis的源码中,SqlSessionFactory是一个接口。同时,我们也可以定义为接口。由子类来实现。但是,这里为了方便,就省去了定义。
Configuration这个对象在全局中,只能存在一个,所以将其定义为final。
openSession()这个方法获得一个SqlSession对象。
定义一个SqlSession的接口。
public interface SqlSession {
/**
* //TODO 根据传入的条件查询单一结果
*
* @param statement 方法对应的sql语句,namespace + id
* @param parameter 要传入到sql语句中的查询参数
* @return T
*/
<T> T selectOne(String statement, Object parameter);
/**
* //TODO 查询集合
*
* @param statement 方法对应的sql语句,namespace + id
* @param parameter 要传入到sql语句中的查询参数
* @return java.util.List
*/
<E> List<E> selectList(String statement, Object parameter);
/**
* //TODO 获取mapper对象
*
* @param type 对象的类型
* @return T
*/
<T> T getMapper(Class<T> type);
}
由其子类来实现SqlSession,并重写其中的方法。
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private final Executor executor;
public DefaultSqlSession(Configuration configuration) {
super();
this.configuration = configuration;
this.executor = new DefaultExecutor(configuration);
}
@Override
public <T> T selectOne(String statement, Object parameter) {
List<T> list = this.selectList(statement, parameter);
if (list == null || list.size() == 0) {
return null;
}
if (list.size() == 1) {
return list.get(0);
} else {
throw new RuntimeException("too man result");
}
}
@Override
public <E> List<E> selectList(String statement, Object parameter) {
MappedStatement smt = this.configuration.getStatementMap().get(statement);
return executor.query(smt, parameter);
}
@Override
public <T> T getMapper(Class<T> type) {
MappedProxy mappedProxy = new MappedProxy(this);
return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, mappedProxy);
}
}
在这里会有很多疑惑的地方。
①. Executor这个对象是干什么的?
②. MappedProxy这个对象的干什么的?
③. selectOne为什么调用selectList的方法?
下面我们一一解答。
上面说到SqlSession的功能是基于Executor来实现的。其实,MyBatis中,SqlSession对数据库的操作,是委托给执行器Executor来完成的。并且每一个SqlSession都拥有一个新的Executor对象。
MyBatis源码中Executor的方法:
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
int update(MappedStatement ms, Object parameter) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
List<BatchResult> flushStatements() throws SQLException;
void commit(boolean required) throws SQLException;
void rollback(boolean required) throws SQLException;
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
boolean isCached(MappedStatement ms, CacheKey key);
void clearLocalCache();
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
Transaction getTransaction();
void close(boolean forceRollback);
boolean isClosed();
void setExecutorWrapper(Executor executor);
}
可以看到的是。Executor中定义了查询及更新的方法。还有事务提交和回滚。其实现类有五个:
基本上Executor就是这些了。详细的资料,读者可自行查看其源码。
这是一个代理类。因为Mapper接口没有具体的实现类。所以,通过代理的方式,创建Mapper的代理对象。让其执行相关的数据库操作。
因为查询单个对象还是集合。都进行了查询操作。
selectOne查询一个对象,对应的List集合中只能有一个值,所以直接使用list.get(0)则可以取出。若是出现了多值的情况,则程序抛出异常。
selectList则查询所有结果。
上面的问题都回答完了,若是读者还有不懂得地方,请给博主留言或者自查搜索引擎解决。
回到程序中。使用动态代理创建一个Mapper接口的实现类。
public class MappedProxy implements InvocationHandler {
private SqlSession sqlSession;
public MappedProxy(SqlSession sqlSession) {
super();
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Class<?> returnType = method.getReturnType();
//调用这个方法的class或接口与参数cls表示的类或接口相同,或者是参数cls表示的类或接口的父类,则返回tr。
//判断返回值是否为Collection的子类
if (Collection.class.isAssignableFrom(returnType)) {
return sqlSession.selectList(method.getDeclaringClass().getName() + "." + method.getName(), args == null ? null : args[0]);
} else {
return sqlSession.selectOne(method.getDeclaringClass().getName() + "." + method.getName(), args == null ? null : args[0]);
}
}
}
因为MappedStatement对象中封装了对应的sql语句。可以根据其返回值,判断代理对象调用哪个方法。
最后的操作就交给Executor对象了。由它来操作数据库。
package com.qfcwx.executor;
import com.qfcwx.config.MappedStatement;
import java.util.List;
/**
* @ClassName: Executor
* @Description: TODO Mybatis的核心接口之一,定义了数据库操作最基本的方法,SqlSession的功能都基于它来实现
* @Date: 2019/4/19 10:22
* @Version 1.0
**/
public interface Executor {
/**
* //TODO
*
* @param statement
* @param parameter
* @return java.util.List
**/
<E> List<E> query(MappedStatement statement, Object parameter);
}
上面定义一个接口。写一个查询的方法。相信大家在写原生JDBC操作数据库的时候,都用过Dbutils吧。里面的方法,只有Query和Update。
这是因为不管查询一个,还是查询多个,都是查询的方法。而增删改都只是数据的更新。
MyBatis中使用了连接池,而博主使用了最原生的JDBC来操作数据库。
public class Connections {
public static Connection getConnection(Configuration configuration) {
Connection connection = null;
try {
//加载驱动
Class.forName(configuration.getDriver());
connection = DriverManager.getConnection(configuration.getUrl(), configuration.getUserName(), configuration.getPassWord());
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
}
上面提取了一个获取数据库连接的对象,这里就以MySQL为例。最后一步就是让Executor的子类来重写query方法,进行数据库的操作。
package com.qfcwx.executor.impl;
import com.qfcwx.config.Configuration;
import com.qfcwx.config.MappedStatement;
import com.qfcwx.executor.Executor;
import com.qfcwx.jdbc.Connections;
import com.qfcwx.pojo.User;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName: DefaultExecutor
* @Description: TODO 操作数据库的操作,实则mybatis底层封装的连接池
* @Date: 2019/4/19 11:10
* @Version 1.0
**/
public class DefaultExecutor implements Executor {
private Configuration configuration;
public DefaultExecutor(Configuration configuration) {
super();
this.configuration = configuration;
}
@Override
public <E> List<E> query(MappedStatement statement, Object parameter) {
List<E> list = new ArrayList<E>();
//获取连接
Connection connection = Connections.getConnection(configuration);
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
//创建预编译PreparedStatement对象,从MappedStatement获取sql语句
preparedStatement = connection.prepareStatement(statement.getSql());
//处理sql中的占位符
parameterSize(preparedStatement, parameter);
//执行查询操作获取resultSet
resultSet = preparedStatement.executeQuery();
//将结果集通过反射技术,填充到list中
handleResult(resultSet, list, statement.getResultType());
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (resultSet != null) {
resultSet.close();
}
if (preparedStatement != null) {
preparedStatement.close();
}
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return list;
}
/**
* //TODO 对PreparedStatement中的占位符进行处理
*
* @param statement
* @param parameter
* @return void
*/
private void parameterSize(PreparedStatement statement, Object parameter) throws SQLException {
if (parameter instanceof Integer) {
statement.setInt(1, (Integer) parameter);
} else if (parameter instanceof Long) {
statement.setLong(1, (Long) parameter);
} else if (parameter instanceof String) {
statement.setString(1, (String) parameter);
}
}
/**
* //TODO 读取ResultSet中的数据,并转换成目标对象
*
* @param resultSet
* @param ret
* @param className
* @return void
*/
private <E> void handleResult(ResultSet resultSet, List<E> ret, String className) {
Class<E> clazz = null;
//通过反射获取类的对象
try {
clazz = (Class<E>) Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
while (resultSet.next()) {
//通过反射实例化对象
Object model = clazz.newInstance();
//使用反射工具将ResultSet中的数据填充到entity中
long id = resultSet.getLong("id");
String username = resultSet.getString("username");
String password = resultSet.getString("password");
User user = (User) model;
user.setId(id);
user.setUsername(username);
user.setPassword(password);
ret.add((E) user);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
基本上就是JDBC操作数据库的代码,也没什么好说的。通过反射来进行结果的反向解析。
最后就是测试的代码了。
public class Test {
public static void main(String[] args) {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
System.out.println(sqlSession);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectUserById(10L);
System.out.println(user);
List<User> userList = mapper.selectList();
System.out.println(userList);
}
}
测试结果,读者可以根据自己的实践去检验。
想要通过一篇技术文就了解MyBatis的底层原理,这是不现实的。但是,通过慢慢的积累,每一个猿类都能成为开源框架的贡献者之一。
感兴趣的读者,可以使用编辑器一行一行debug来看。上面的代码还是比较基础的,也许是博主没理解那么深刻,讲的云里雾里。但是,相信各位开发人员都能通过慢慢揣摩,弄懂真正的奥义。
下面也会给出项目的源码的地址。供大家下载学习。
GitHub地址: https://github.com/Qingfengchuiwoxin/Mybatis
相信奇迹的人,本身就和奇迹一样了不起。