接触Mybatis框架有些时日了,但我仅仅处于“会使用它”的这个层面,并没有对它进行深入地理解。所以,现在抽时间想对它一探究竟。
Mybatis工作原理参考了此博文:
Mybatis工作原理
Mybatis框架大致分为四层:引导层、接口层、数据处理层、框架支撑层。
如下图:
Mybatis的底层还是采用了原生JDBC技术来实现对数据库的操作。只是通过 SqlSessionFactory,SqlSession,Executor,StatementHandler,ParameterHandler,ResultHandler和TypeHandler等几个处理器封装了这些过程。
几个组件的功能如下:
其中,StatementHandler通过ParameterHandler与ResultHandler分别进行参数预编译 与结果处理。而ParameterHandler与ResultHandler都使用TypeHandler进行映射。
Mybatis的核心运行流程可分为三个阶段:
按照Mybatis的核心运行流程的三个阶段一步步编码,每完成一个阶段,就进行测试。
读取XML配置文件和注解中的配置信息,创建配置对象,并完成各个模块的初始化工作
1. MappedStatement
MappedStatement对象是对mapper文件中的每一个标签进行抽象
@Data
public class MappedStatement {
// 命名空间
private String namespace;
// id
private String resourceId;
// 返回类型
private String resultType;
// sql语句
private String sql;
}
2. Confifuration
mapper文件中包含多个标签,所有会有个对象来存储标签的集合
Confifuration是用来存储所有配置信息的,且此配置信息全局唯一
/**
* 存储所有配置信息:
* 数据库配置信息、mapper配置信息
*/
@Data
public class Configuration {
// jdbc的驱动
private String jdbcDriver;
// jdbc的url
private String jdbcUrl;
// jdbc的username
private String jdbcUsername;
// jdbc的password
private String jdbcPassword;
/**
* mapper文件中可能有多条SQL语句(MapperStatement对象)
* Map:可实现快速访问
*/
private Map mapperStatements = new HashMap<>();
}
3. SqlSessionFactory
我们在使用Mybatis时,会写如下代码:
在通过工厂模式进行实例化SqlSessionFactory时,会加载mybatis的核心配置文件mybatis-config.xml。所以,这里也仿照mybatis源码,新建个SqlSessionFactory类
public class SqlSessionFactory {
private final Configuration configuration = new Configuration();
public SqlSessionFactory() {
loadDbInfo();
loadMappersInfo();
}
...
}
此类有两个作用:
loadDbInfo()
// 记录mapper.xml文件存放的位置
public static final String MAPPER_CONFIG_LOCATION = "mappers";
// 记录数据库连接信息文件存放的位置
public static final String DB_CONFIG_FILE = "db.properties";
// 加载数据库配置信息
private void loadDbInfo() {
// 加载数据库信息配置文件
InputStream dbIn = SqlSessionFactory.class.getClassLoader().getResourceAsStream(DB_CONFIG_FILE);
Properties p = new Properties();
try {
// 将配置信息写入到Properties对象
p.load(dbIn);
} catch (IOException e) {
e.printStackTrace();
}
// 将数据库配置信息写入到Configuration对象
configuration.setJdbcDriver(p.get("jdbc.driver").toString());
configuration.setJdbcUrl(p.get("jdbc.url").toString());
configuration.setJdbcUsername(p.get("jdbc.username").toString());
configuration.setJdbcPassword(p.get("jdbc.password").toString());
}
loadMappersInfo()
// 加载指定文件夹下的所有mapper.xml文件
private void loadMappersInfo() {
URL resource = null;
resource = SqlSessionFactory.class.getClassLoader().getResource(MAPPER_CONFIG_LOCATION);
// 获取指定文件夹信息
File mappers = new File(resource.getFile());
if (mappers.isDirectory()) {
File[] files = mappers.listFiles();
// 遍历文件夹下所有的mapper.xml,并解析信息后,注入到Configuration对象中
for (File file : files) {
loadMapperInfo(file);
}
}
}
loadMapperInfo()
使用dom4j技术对mapper文件进行解析
// 加载指定的mapper.xml文件
private void loadMapperInfo(File file) {
// 1.获取解析器
SAXReader reader = new SAXReader();
// 2.通过read()方法读取一个文件,转换成document对象
Document document = null;
try {
document = reader.read(file);
} catch (DocumentException e) {
e.printStackTrace();
}
// 获取根节点:
Element root = document.getRootElement();
// 获取命令空间namespace属性
String namespace = root.attribute("namespace").getData().toString();
// 获取子节点select列表:
4. SqlSession
SqlSessionFactory类的第二个作用:生产SqlSession
什么是SqlSession?简单理解:使用mybatis访问数据库时,与数据库的每一次连接都SqlSession。并且SqlSession中要保存Configuration的引用,因为此对象中有数据库连接信息,这样连接数据库时就可以获取连接信息
/**
*mybatis暴露给外部的接口,实现增删改查
*/
public interface SqlSession {
T selectOne(String statement, Object parameter);
List selectList(String statement, Object parameter);
T getMapper(Class type);
}
5. DefaultSqlSession
DefaultSqlSession是SqlSession接口的实现类。
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
super();
this.configuration = configuration;
}
...
// 接口中的方法暂不实现
}
DefaultSqlSession中的Configuration引用是由外界传入的。
openSession()
在SqlSessionFactory类中,添加此方法,用于生产SqlSession对象
// 生产SqlSession对象
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
初始化阶段:读取XML配置文件和数据库配置文件中的信息加载到Configuration对象中去
第一阶段测试:
public class Test {
public static void main(String[] args) {
SqlSessionFactory factory = new SqlSessionFactory();
SqlSession sqlSession = factory.openSession();
System.out.println(sqlSession);
}
}
Debug时,可以看到Configuration对象中有值,并且和配置文件中的信息一致。
封装Mybatis的编程模型,使用Mapper接口开发的初始化工作
SqlSession意味着创建数据库会话,代表了一次与数据库的连接
SqlSession是Mybatis对外提供数据访问的主要API
SqlSession的作用就是对外提供数据访问的主要API,为什么这么说呢?
下面给出两个mybatis入门的代码示例:
可到Mybats官网查看:mybatis入门
使用mapper接口(推荐):
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(2l);
System.out.println(user);
使用SqlSession:
全限定名= namespace + mapper文件中标签的Id。
此种编程方式是Mybatis的前身ibatis的编程方式
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
User user = session.selectOne("com.zzc.mapper.UserMapper.selectById", 1);
System.out.println(user);
读者可以测试一番,两者结果是一致的。
SqlSession的功能都是基于Excutor来实现的
查看源码的SqlSession查询接口的嵌套关系。如下图:
由此可知,SqlSession中的方法最终调用了Executor中的方法
那么为什么不直接在SqlSession的方法中直接完成对数据库的操作,而要交给Executor呢?这里采用了面向对象的单一职责原则:SqlSession只对外提供服务,内部由Executor完成。
1. Executor
此接口定义了数据库基本的操作方法
public interface Executor {
// 查询接口
List query(MappedStatement ms, Object parameter);
}
2. DefaultExecutor
DefaultExecutor实现了Executor接口
public class DefaultExecutor implements Executor {
private final Configuration configuration;
public DefaultExecutor(Configuration configuration) {
this.configuration = configuration;
}
@Override
public List query(MappedStatement ms, Object parameter) {
System.out.println(ms.getSql());
return null;
}
}
DefaultExecutor也要操作数据库,所以需要Configuration对象
3.DefaultSqlSession
在此类中添加Executor属性并初始化
/**
* 1.对外提供数据访问的api
* 2.对内将请求转发给Executor
*/
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration) {
super();
this.configuration = configuration;
this.executor = new DefaultExecutor(configuration);
}
}
重写此类的方法
selectList()
@Override
public List selectList(String statement, Object parameter) {
MappedStatement ms = configuration.getMappedStatements().get(statement);
return executor.query(ms, parameter);
}
selectOne()
@Override
public T selectOne(String statement, Object parameter) {
List
getMapper()
在实现此方法之前,先谈谈Mybatis的两种编程模型:
很显然,是用Mapper接口编程更受人欢迎。因为它更雅观,易于维护,接口中的方法还有业务含义。
问题
那么为什么使用mapper接口就能对数据库进行访问呢?mapper接口并没有实现类的。
对于上面那两种编程模型,实际上只有一种,因为使用mapper接口最终会转换为使用SqlSession方式,这种转换对用户来说是透明的。
那么是如何转换的呢?
配置文件的解读+动态代理的增强。主要有三个方面:
找到session中的对应的方法执行
根据mapper接口中方法的返回值就能知道对应sqlSession中的方法
找到命名空间和方法名
就是mapper接口的所在的包名+方法名
传递参数
直接传递参数
getMapper()的实现:
@Override
public T getMapper(Class type) {
// MapperProxy(实现了InvocationHandler接口)只负责业务逻辑
MapperProxy mapperProxy = new MapperProxy(this);
// Proxy只生成代理类
return (T)Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type}, mapperProxy);
}
4. MapperProxy
@Data
public class MapperProxy implements InvocationHandler {
private SqlSession session;
public MapperProxy(SqlSession session) {
this.session = session;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Collection.class.isAssignableFrom(method.getReturnType())) {
return session.selectList(method.getDeclaringClass().getName() +
"." + method.getName(), args == null ? null : args[0]);
} else {
return session.selectOne(method.getDeclaringClass().getName() +
"." + method.getName(), args == null ? null : args[0]);
}
}
}
invoke()方法的业务逻辑将转换的那“三方面”表现得淋漓尽致。
致此,第二阶段已经结束了。接下来进行测试:
public class Test {
public static void main(String[] args) {
SqlSessionFactory factory = new SqlSessionFactory();
SqlSession sqlSession = factory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List users = mapper.selectAll();
}
}
通过SqlSession完成SQL的解析、参数的映射、SQL的执行、结果的反射解析过程
Executor对数据库操作也是遵循JDBC规范的。接下来重写DefaultExecutor中的query()方法
query()
@Override
public List query(MappedStatement ms, Object parameter) {
// 定义返回结果集
List ret = new ArrayList<>();
try {
// 加载驱动
Class.forName(configuration.getJdbcDriver());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
connection = DriverManager.getConnection(configuration.getJdbcUrl(),
configuration.getJdbcUsername(), configuration.getJdbcPassword());
preparedStatement = connection.prepareStatement(ms.getSql());
// 处理SQL语句中的占位符
parameterize(preparedStatement, parameter);
resultSet = preparedStatement.executeQuery();
// 将结果通过反射技术填充到list中
handlerResultSet(resultSet, ret, ms.getResultType());
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
resultSet.close();
preparedStatement.close();
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return ret;
}
parameterize()
// 对preparedStatement中的占位符进行处理(mapper文件中最多只有一个占位符)
private void parameterize(PreparedStatement preparedStatement, Object parameter) throws SQLException {
if (parameter instanceof Integer) {
preparedStatement.setInt(1, (int) parameter);
} else if (parameter instanceof Long) {
preparedStatement.setLong(1, (long) parameter);
} else if (parameter instanceof String) {
preparedStatement.setString(1, (String) parameter);
}
}
handlerResultSet
private void handlerResultSet(ResultSet resultSet, List ret, String className) {
Class clazz = null;
try {
clazz = (Class) Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
while (resultSet.next()) {
Object o = clazz.newInstance();
ReflectUtil.setPropToBeanFromResult(o, resultSet);
ret.add((E) o);
}
} catch (Exception e) {
e.printStackTrace();
}
}
1. ReflectUtil
这里面涉及到的是一些Java反射技术
public class ReflectUtil {
/**
* 为指定的bean的propName属性的值设为value
*
* @param bean 目标对象
* @param propName 对象的属性名
* @param value 设定的属性值
*/
public static void setPropToBean(Object bean, String propName, Object value) {
Field f = null;
try {
// 获取对象指定的属性
f = bean.getClass().getDeclaredField(propName);
f.setAccessible(true);
f.set(bean, value);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 从ResultSet中读取一行数据,并填充至指定的实体bean
*
* @param entity 待填充的实体
* @param resultSet 从数据库加载的数据
* @throws SQLException
*/
public static void setPropToBeanFromResult(Object entity, ResultSet resultSet) throws SQLException {
// 获取对象的所有字段
Field[] fields = entity.getClass().getDeclaredFields();
// 遍历所有字段,从ResultSet中读取相应的字段,并填充到对象的属性中
for (int i = 0; i < fields.length; i++) {
if (fields[i].getType().getSimpleName().equals("String")) {
setPropToBean(entity, fields[i].getName(), resultSet.getString(fields[i].getName()));
} else if (fields[i].getType().getSimpleName().equals("Integer")) {
setPropToBean(entity, fields[i].getName(), resultSet.getInt(fields[i].getName()));
} else if (fields[i].getType().getSimpleName().equals("Long")) {
setPropToBean(entity, fields[i].getName(), resultSet.getLong(fields[i].getName()));
}
}
}
}
至此,第三阶段已经结束了。再次测试,能获取到结果
【总结】:
简化版mybatis实现思路:
源码存放在github上面,点击下载:github