手写Mybatis,首先要明确手写的目的,不是为了方方面面都去模仿实现,而是为了走通mybatis的核心流程,以及中间涉及到的部分核心组件。
我们从基础的查询走起,为了方便,我们只实现最多带一个参数的查询。
基本流程
一、读取配置初始化
相关数据结构:
1.1 MappedStatement
该类用于保存mapper.xml中sql节点的所有信息,在这里我们只保存基本的信息:
public class MappedStatement {
private String nameSpace;
private String id;
private String resultType;
private String sql;
private String parameterType;
/**
* @return 命名空间+id
*/
public String getSourceId(){
return String.format("%s.%s",getNameSpace(), getId());
}
........其他的getter-setter.......
}
1.1 Configuration
Configuration在Mybatis中是全局单例唯一的,保存了所有的配置信息。
这里我们只存储数据源的驱动、url、用户名密码、存储sql信息的map和mapper接口的动态代理注册中心。
public class Configuration {
private String jdbcDriver;
private String jdbcUrl;
private String jdbcUsername;
private String jdbcPassword;
private Map<String,MappedStatement>mappedStatementMap=new HashMap<>();
/**
* mapper接口的动态代理注册中心
*/
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public <T>void addMapper(Class<T> type){
mapperRegistry.addMapper(type);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession){
return mapperRegistry.getMapper(type,sqlSession);
}
...
}
1.2 SqlSessionFactory
按照流程,我们首先通过SqlSession工厂的openSession()方法获取一个SqlSession对象。
在工厂的build()方法中会完成mybatis配置文件的解析,这里简单起见,直接在SqlSessionFactory的构造中进行:
public class SqlSessionFactory {
//全局唯一
private final Configuration configuration = new Configuration();
private static final String DB_PROPERTIES = "db.properties";
private static final String MAPPERS_LOCATION = "mappers";
private static final String JDBC_DRIVER = "jdbc.driver";
private static final String JDBC_URL = "jdbc.url";
private static final String JDBC_USERNAME = "jdbc.username";
private static final String JDBC_PASSWORD = "jdbc.password";
public SqlSessionFactory() {
loadDB();
loadMapper();
}
/**加载mapper.xml**/
private void loadMapper() {
URL resource = SqlSessionFactory.class.getClassLoader().getResource(MAPPERS_LOCATION);
File mappersDir = new File(resource.getFile());
if (mappersDir.isDirectory()) {
File[] mapperXmls = mappersDir.listFiles();
for (File xml : mapperXmls) {
//加载mapper.xml文件
loadMapperXML(xml);
}
}
}
//加载单个Mapper.xml
private void loadMapperXML(File file) {
SAXReader reader = new SAXReader();
Document doc = null;
try {
doc = reader.read(file);
} catch (DocumentException e) {
e.printStackTrace();
}
MappedStatement mappedStatement = null;
//获取根节点mapper
Element mapper = doc.getRootElement();
String namespace = mapper.attributeValue("namespace");
List<Element> selectList = mapper.elements("select");
for (Element select : selectList) {
mappedStatement = new MappedStatement();
mappedStatement.setNameSpace(namespace);
mappedStatement.setId(select.attributeValue("id"));
mappedStatement.setResultType(select.attributeValue(Constants.RESULT_TYPE.getValue()));
String sql = select.getData().toString();
mappedStatement.setSql(sql);
mappedStatement.setParameterType(select.attributeValue(Constants.PARAMETER_TYPE.getValue()));
configuration.getMappedStatementMap().put(mappedStatement.getSourceId(), mappedStatement);
}
try {
Class<?> mapperInterface = Class.forName(namespace);
//将当前mapper.xml命名空间的接口注册到mapperRegistry中
configuration.addMapper(mapperInterface);
} catch (ClassNotFoundException e) {
throw new RuntimeException("命名空间"+namespace+"对应的mapper接口不存在");
}
}
//加载数据库配置
private void loadDB() {
InputStream dbStream = SqlSessionFactory.class.getClassLoader().getResourceAsStream(DB_PROPERTIES);
Properties properties = new Properties();
try {
properties.load(dbStream);
} catch (IOException e) {
e.printStackTrace();
}
//设置到configuration对象中
configuration.setJdbcDriver(properties.getProperty(JDBC_DRIVER));
configuration.setJdbcUrl(properties.getProperty(JDBC_URL));
configuration.setJdbcUsername(properties.getProperty(JDBC_USERNAME));
configuration.setJdbcPassword(properties.getProperty(JDBC_PASSWORD));
}
//返回一个默认的SqlSession
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
db.properties配置:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://120.27.212.10:3306/mybatis_c?useSSL=false&useUnicode=true&allowMultiQueries=true
jdbc.username=root
jdbc.password=xiaoyunshi
1.3 SqlSession&DefaultSqlSession
SqlSession提供查询修改相关接口,这里只提供查询:
public interface SqlSession {
/**
* 根据传入的条件查询单一结果
* @param statement sql对应的 namespace + id
* @return 返回指定的结果对象
*/
<T> T selectOne(String statement, Object parameter);
/**
*
* 根据条件经过查询,返回泛型集合
*
* @param statement sql对应的 namespace + id
* @return 返回指定的结果对象的list
*/
<E> List<E> selectList(String statement, Object parameter);
/**
* 根据mapper接口获取接口对应的动态代理实例
* @param type 指定的mapper接口
* @return
*/
<T> T getMapper(Class<T> type);
}
默认实现:
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration) {
this.configuration=configuration;
executor=new DefaultExecutor(configuration);
}
@Override
public <T> T selectOne(String statement, Object parameter) {
List<Object> list = this.selectList(statement, parameter);
if (list == null || list.size() == 0){
return null;
}
if (list.size() == 1){
return (T) list.get(0);
}
throw new RuntimeException("Results size is " + list.size() +", but expected one...");
}
@Override
public <E> List<E> selectList(String statement, Object parameter) {
//通过statement拿到对应的MappedStatement对象
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statement);
return executor.query(mappedStatement,parameter);
}
/**
* 获取 指定的mapper接口type的动态代理实例
* 每次都创建新的 MapperProxy
* @param type 指定的mapper接口
* @return type接口的动态代理实例
*/
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type,this);
}
}
这里暂时只提供查询的实现。
在构造中会初始化Executor对象,具体的查询都交给Executor执行。
getMapper
通过动态代理返回一个Mapper接口的实例。
二、代理封装
2.1 MapperProxy
public class MapperProxy<T> implements InvocationHandler {
private SqlSession sqlSession;
public MapperProxy(SqlSession sqlSession) {
super();
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Class<?> returnType = method.getReturnType();
String statement = method.getDeclaringClass().toString().substring(10)+"."+method.getName();
if (Collection.class.isAssignableFrom(returnType)){
return sqlSession.selectList(statement, getParameter(args));
}
return sqlSession.selectOne(statement,getParameter(args));
}
private Object getParameter(Object[] args) {
return args==null?null:args[0];
}
}
invoke中,判断方法返回值是否为集合,如果是集合就调用selectList,否则调用selectOne方法返回。
2.2 MapperRegistry
使用Map存储mapper接口和对应MapperProxyFactory的映射。
public class MapperRegistry {
private final Configuration configuration;
private final Map<Class<?>,MapperProxyFactory<?>>knownMappers=new HashMap<>();
public MapperRegistry(Configuration configuration) {
this.configuration = configuration;
}
public <T> void addMapper(Class<T> type ){
if (type.isInterface()){
if (knownMappers.containsKey(type)){
throw new RuntimeException("Type"+type+"已经在knownMappers中注册");
}
knownMappers.put(type,new MapperProxyFactory<T>(type));
}
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession){
MapperProxyFactory<?> mapperProxyFactory = knownMappers.get(type);
if (mapperProxyFactory==null){
throw new RuntimeException("Type:"+type+"未在knownMappers中注册");
}
return (T) mapperProxyFactory.newInstance(sqlSession);
}
}
addMaper中,先判断是否为接口类型,再判断当前接口对象是否已经注册过。
getMapper中先根据当前接口对象从注册中心拿到对应的代理工厂,如果为空则说明未注册,抛出异常,否则调用代理工厂创建一个动态代理类。
2.3 MapperProxyFactory
该类用于生产mapper接口的动态代理对象
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public T newInstance(SqlSession sqlSession){
final MapperProxy<T> mapperProxy=new MapperProxy(sqlSession);
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),new Class[]{mapperInterface},mapperProxy);
}
}
每次都创建新的MapperProxy
,再通过动态代理创建实例返回。
三、数据读写
3.1 Executor
具体操作数据库时,通过Executor实现,这里只提供查询的接口:
public interface Executor {
/**
*
* 查询接口
*
* @param ms 封装sql语句标签的MappedStatement对象
* @param parameter 传入sql的参数
* @return 将数据转换成指定对象结果集返回
*/
<E> List<E> query(MappedStatement ms, Object parameter);
}
3.2 DefaultExecutor
这里只提供默认的简单查询,无缓存。
知识点回顾:
Mybatis中如果开启了二级缓存,则会调用通过装饰器模式包装的CachingExecutor执行,如果二级缓存无数据,再调用BaseExecutor,否则默认走BaseExecutor(提供一级缓存)。
BaseExecutor提供了三个实现:BatchXXX(批量)、SimpleXXX(每次创建新的Statement)和ReuseXXX(通过缓存复用Statement)
Myabatis中,实际的执行并不是Executor,而是交给StatementHandler子类处理。
public class DefaultExecutor implements Executor {
private final Configuration conf;
public DefaultExecutor(Configuration conf) {
this.conf = conf;
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter) {
final List<E> results = new ArrayList<>();
try {
Class.forName(conf.getJdbcDriver());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Connection conn = null;
PreparedStatement preparedStatement=null;
ResultSet resultSet=null;
try {
conn= DriverManager.getConnection(conf.getJdbcUrl(),conf.getJdbcUsername(),conf.getJdbcPassword());
preparedStatement=conn.prepareStatement(ms.getSql());
parameterize(preparedStatement,parameter,ms.getParameterType());
resultSet = preparedStatement.executeQuery();
//处理结果集映射,添加到results中
handleResultSet(resultSet,results,ms.getResultType());
} catch (SQLException e) {
e.printStackTrace();
}
return results;
}
}
query()中就是原始的jdbc查询的流程,创建一个results集合保存处理完毕结果集中的实体对象。
但我们需要对参数占位符进行映射处理:
/**
* 占位符处理
* @param ps
* @param parameter
* @throws SQLException
*/
private void parameterize(PreparedStatement ps, Object parameter,String parameterType) throws SQLException {
if (parameter==null)return;
Class<?> type=null;
try {
type = Class.forName(parameterType);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//判断paramType和当前参数类型是否相同
if (parameter.getClass().isAssignableFrom(type)){
//根据不同的参数调用对应的方法填充占位符
if (parameter instanceof String){
ps.setString(1, (String) parameter);
}else if (parameter instanceof Long){
ps.setLong(1, (long) parameter);
}else if (parameter instanceof Integer){
ps.setInt(1, (int) parameter);
}else {
throw new RuntimeException("parameter not allowed ! ");
}
}else {
throw new RuntimeException("The current parameter type is different from the paramType");
}
}
接着调用executeQuery后拿到resultSet,需要处理结果集:
/**
* 处理结果集映射
* @param resultSet
* @param results
* @param resultType
* @param <E>
*/
private <E> void handleResultSet(ResultSet resultSet, List<E> results, String resultType) {
Class<E>target=null;
try {
target = (Class<E>) Class.forName(resultType);
while (resultSet.next()) {
//反射创建目标实体对象
E result = target.newInstance();
//将resultSet中的结果添加到目标实体的字段中
ReflectUtils.setPropToBeanFromResultSetOne(result,resultSet);
results.add(result);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
首先通过resultType反射获取目标的实体对象result,接着遍resultSet,通过反射将结果集中的值设置到该result对象的字段中,最后将result添加到最终返回的集合results中。
3.3 反射填充resultSet到字段
这里简单起见只处理几个简单的类型。
public class ReflectUtils {
/**
* 将一条resultSet的结果设置到目标实体的属性中
*
* @param target
* @param resultSet
*/
public static void setPropToBeanFromResultSetOne(Object target, ResultSet resultSet) throws SQLException {
Field[] fields = target.getClass().getDeclaredFields();
for (Field field : fields) {
String typeName = field.getType().getSimpleName();
String fieldName=field.getName();
switch (typeName) {
case "String":
setPropToFiled(target, field, resultSet.getString(fieldName));
break;
case "int":
case "Integer":
setPropToFiled(target, field, resultSet.getInt(fieldName));
break;
case "long":
case "Long":
setPropToFiled(target, field, resultSet.getLong(fieldName));
break;
default:
break;
}
}
}
}
/**
* 给字段设置值
*
* @param target 目标实体
* @param field 目标字段
* @param param 要设置的字段值
*/
private static void setPropToFiled(Object target, Field field, Object param) {
if (!field.isAccessible()){
field.setAccessible(true);
}
try {
field.set(target,param);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
OK到这就结束了。来测试一下
四、测试
public class Test {
@org.junit.Test
public void Test(){
SqlSessionFactory factory=new SqlSessionFactory();
SqlSession sqlSession = factory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = mapper.list();
System.out.println(userList);
User user = mapper.selectById(5);
System.out.println(user);
}
}
结果:
[User{id=1, username='zhangsan', password='1234', age=17, name='Zhangsan'}, User{id=2, username='test', password='123', age=22, name='wml'}, User{id=4, username='tes2t', password='1234', age=223, name='wml2'}, User{id=5, username='tes2t', password='1234', age=223, name='wml2'}]
User{id=5, username='tes2t', password='1234', age=223, name='wml2'}
哦对了,mapper接口:
public interface UserMapper {
User selectById(Integer id);
List<User> list();
}
xml:
<?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.wml.mybatis.mapper.UserMapper">
<select id="list" resultType="com.wml.mybatis.pojo.User">
SELECT id, username, password, age, name FROM tb_user
</select>
<select id="selectById" resultType="com.wml.mybatis.pojo.User" parameterType="java.lang.Integer">
select id, username, password, age, name from tb_user where id= ?
</select>
</mapper>
详细的源码分析可参考:https://blog.csdn.net/weixin_43696529/article/details/107396049