
手写Mybatis之02
在前一文章,探讨了映射器代理和映射器代理工厂类,但是需要编码告知 MapperProxyFactory 要对哪个接口进行代理,以及自己编写一个假的 SqlSession 处理实际调用接口时的返回结果。这是很原始的操作,我们将在这一节进行进一步的摸索。
思考:前一节这样的方式和实际使用中有什么不一样,如何进行规范化的操作,让映射器的创建和代理更加符合实际需求?
本章重点:提出思考后,有以下几处优化:1:对映射器的注册提供注册机处理,满足用户可以在使用的时候提供一个包的路径即可完成扫描和注册;2:同时需要对 SqlSession 进行规范化处理,让它可以把我们的映射器代理和方法调用进行包装,建立一个生命周期模型结构,便于后续的内容的添加。
1、思路
鉴于我们希望把整个工程包下关于数据库操作的 DAO 接口与 Mapper 映射器关联起来,那么就需要包装一个可以扫描包路径的完成映射的注册器类。
当然我们还要把上一章节中简化的 SqlSession 进行完善,由 SqlSession 定义数据库处理接口和获取 Mapper 对象的操作,并把它交给映射器代理类进行使用。
有了 SqlSession 以后,你可以把它理解成一种功能服务,有了功能服务以后还需要给这个功能服务提供一个工厂,来对外统一提供这类服务。比如我们在 Mybatis 中非常常见的操作,开启一个 SqlSession。
整体的设计思路见下图:
2、实现
本章节树形结构,MapperProxy和MapperProxyFactory都是上一节中的,调整其中map结构的SqlSession为本章新的SqlSession即可。
mybatis-02
└── src
├── main
│ └── java
│ └── cn.sunnyy.mybatis
│ ├── binding
│ │ ├── MapperProxy.java
│ │ ├── MapperProxyFactory.java
│ │ └── MapperRegistry.java
│ └── session
│ ├── defaults
│ │ ├── DefaultSqlSession.java
│ │ └── DefaultSqlSessionFactory.java
│ ├── SqlSession.java
│ └── SqlSessionFactory.java
└── test
└── java
└── cn.sunnyy.mybatis.test.dao
├── dao
│ ├── ISchoolDao.java
│ └── IUserDao.java
└── ApiTest.java
以下是映射器和sqlSession中间的定义
MapperRegistry 提供包路径的扫描和映射器代理类注册机服务,完成接口对象的代理类注册处理。
SqlSession、DefaultSqlSession 用于定义执行 SQL 标准、获取映射器以及将来管理事务等方面的操作。基本我们平常使用 Mybatis 的 API 接口也都是从这个接口类定义的方法进行使用的。
SqlSessionFactory 是一个简单工厂模式,用于提供 SqlSession 服务,屏蔽创建细节,延迟创建过程。
sqlSession.getMapper是重要的获取映射器方法
3、代码编写
3.1 映射器注册机
package cn.sunnyy.mybatis.binding;
import cn.hutool.core.lang.ClassScanner;
import cn.sunnyy.mybatis.session.SqlSession;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author sunzhen
* @version 1.0
* @date 2024/8/26 22:02
* 映射器注册机
* 在这里管理映射器(mapper的实例对象)
*/
public class MapperRegistry {
/**
* 将已添加的映射器代理加入到 HashMap
*/
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
/**
* 映射器注册机通过类和SqlSession获取到映射器(mapper实例)
* @param type
* @param sqlSession
* @param <T>
* @return
*/
public <T> T getMapper(Class<T> type, SqlSession sqlSession){
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new RuntimeException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new RuntimeException("Error getting mapper instance. Cause: " + e, e);
}
}
public <T> void addMapper(Class<T> type) {
/* Mapper 必须是接口才会注册 */
if (type.isInterface()) {
if (hasMapper(type)) {
// 如果重复添加了,报错
throw new RuntimeException("Type " + type + " is already known to the MapperRegistry.");
}
// 注册映射器代理工厂
knownMappers.put(type, new MapperProxyFactory<>(type));
}
}
public <T> boolean hasMapper(Class<T> type) {
return knownMappers.containsKey(type);
}
public void addMappers(String packageName) {
Set<Class<?>> mapperSet = ClassScanner.scanPackage(packageName);
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
}
MapperRegistry 映射器注册类的核心主要在于提供了
ClassScanner.scanPackage
扫描包路径,调用addMapper
方法,给接口类创建MapperProxyFactory
映射器代理类,并写入到 knownMappers 的 HashMap 缓存中。另外就是这个类也提供了对应的 getMapper 获取映射器代理类的方法,其实这步就包装了我们上一章节手动操作实例化的过程,更加方便在 DefaultSqlSession 中获取 Mapper 时进行使用。
3.2 SqlSession 标准定义和实现
package cn.sunnyy.mybatis.session;
/**
* @author sunzhen
* @version 1.0
* @date 2024/8/26 21:56
* 提供的SqlSession 用来执行SQL,获取映射器,管理事务。
* PS:通常情况下,我们在应用程序中使用的Mybatis的API就是这个接口定义的方法。
*/
public interface SqlSession {
/**
* Retrieve a single row mapped from the statement key
* 根据指定的SqlID获取一条记录的封装对象
*
* @param <T> the returned object type 封装之后的对象类型
* @param statement sqlID
* @return Mapped object 封装之后的对象
*/
<T> T selectOne(String statement);
/**
* Retrieve a single row mapped from the statement key and parameter.
* 根据指定的SqlID获取一条记录的封装对象,只不过这个方法容许我们可以给sql传递一些参数
* 一般在实际使用中,这个参数传递的是pojo,或者Map或者ImmutableMap
*
* @param <T> the returned object type
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @return Mapped object
*/
<T> T selectOne(String statement, Object parameter);
/**
* Retrieves a mapper.
* 得到映射器,这个巧妙的使用了泛型,使得类型安全
*
* @param <T> the mapper type
* @param type Mapper interface class
* @return a mapper bound to this SqlSession
*/
<T> T getMapper(Class<T> type);
}
在 SqlSession 中定义用来执行 SQL、获取映射器对象以及后续管理事务操作的标准接口。
目前这个接口中对于数据库的操作仅仅只提供了 selectOne,后续还会有相应其他方法的定义。
默认实现
package cn.sunnyy.mybatis.session.impl;
import cn.sunnyy.mybatis.binding.MapperRegistry;
import cn.sunnyy.mybatis.session.SqlSession;
import com.alibaba.fastjson.JSON;
/**
* @author sunzhen
* @version 1.0
* @date 2024/8/26 22:14
* 默认SqlSession实现类
*/
public class DefaultSqlSession implements SqlSession {
/**
* 映射器注册机
*/
private MapperRegistry mapperRegistry;
public DefaultSqlSession(MapperRegistry mapperRegistry) {
this.mapperRegistry = mapperRegistry;
}
@Override
public <T> T selectOne(String statement) {
return (T) ("你的操作被代理了!" + statement);
}
@Override
public <T> T selectOne(String statement, Object parameter) {
return (T) ("你的操作被代理了!" + "方法:" + statement + " 入参:" + JSON.toJSONString(parameter));
}
@Override
public <T> T getMapper(Class<T> type) {
return mapperRegistry.getMapper(type, this);
}
}
通过 DefaultSqlSession 实现类对 SqlSession 接口进行实现。
getMapper 方法中获取映射器对象是通过 MapperRegistry 类进行获取的,现阶段先将映射器注册机作为成员变量进行传递使用,后续这部分会被配置类进行替换。
在 selectOne 中是一段简单的内容返回,目前还没有与数据库进行关联,这部分在我们渐进式的开发过程中逐步实现。
3.3 SqlSessionFactory 工厂定义和实现
package cn.sunnyy.mybatis.session;
/**
* @author sunzhen
* @version 1.0
* @date 2024/8/26 21:59
* 工厂模式接口,构建SqlSession的工厂
*/
public interface SqlSessionFactory {
/**
* 打开一个 session
*
* @return SqlSession
*/
SqlSession openSession();
}
默认实现
package cn.sunnyy.mybatis.session.impl;
import cn.sunnyy.mybatis.binding.MapperRegistry;
import cn.sunnyy.mybatis.session.SqlSession;
import cn.sunnyy.mybatis.session.SqlSessionFactory;
/**
* @author sunzhen
* @version 1.0
* @date 2024/8/26 22:16
* 默认的 DefaultSqlSessionFactory
*/
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final MapperRegistry mapperRegistry;
public DefaultSqlSessionFactory(MapperRegistry mapperRegistry) {
this.mapperRegistry = mapperRegistry;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(mapperRegistry);
}
}
默认的简单工厂实现,处理开启 SqlSession 时,对 DefaultSqlSession 的创建以及传递 mapperRegistry,这样就可以在使用 SqlSession 时获取每个代理类的映射器对象了。
4、测试
└── test
└── java
└── cn.sunnyy.mybatis.test.dao
├── dao
│ ├── ISchoolDao.java
│ └── IUserDao.java
└── ApiTest.java
4.1 同一个包路径下,提供2个以上的 Dao 接口
package cn.sunnyy.mybatis.test.dao;
public interface ISchoolDao {
String querySchoolName(String uId);
}
package cn.sunnyy.mybatis.test.dao;
public interface IUserDao {
String queryUserName(String uId);
Integer queryUserAge(String uId);
}
4.2 单元测试
package cn.sunnyy.mybatis.test;
import cn.sunnyy.mybatis.binding.MapperRegistry;
import cn.sunnyy.mybatis.session.SqlSession;
import cn.sunnyy.mybatis.session.impl.DefaultSqlSessionFactory;
import cn.sunnyy.mybatis.test.dao.IUserDao;
import com.sun.org.slf4j.internal.Logger;
import com.sun.org.slf4j.internal.LoggerFactory;
import org.junit.Test;
/**
* @author sunzhen
* @version 1.0
* @date 2024/8/26 22:18
*/
public class ApiTest {
private Logger logger = LoggerFactory.getLogger(ApiTest.class);
@Test
public void test_MapperProxyFactory(){
/*
先初始化映射器注册机,扫描并添加映射器
*/
MapperRegistry mapperRegistry = new MapperRegistry();
mapperRegistry.addMappers("cn.sunnyy.mybatis.test.dao");
/*
通过DefaultSqlSessionFactory获取到SqlSession实例
*/
SqlSession sqlSession = new DefaultSqlSessionFactory(mapperRegistry).openSession();
/*
实际上是通过 映射器注册机 获取到映射器实例
*/
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 4. 测试验证
String res = userDao.queryUserName("10001");
System.out.println("测试结果:" + res);
}
}
在单元测试中通过注册机扫描包路径注册映射器代理对象,并把注册机传递给 SqlSessionFactory 工厂,这样完成一个链接过程。
之后通过 SqlSession 获取对应 DAO 类型的实现类,并进行方法验证。
测试结果:
junit4 cn.sunnyy.mybatis.test.ApiTest
测试结果:你的操作被代理了!方法:queryUserName 入参:["10001"]
Process finished with exit code 0
5、总结
首先要从设计结构上了解工厂模式对具体功能结构的封装,屏蔽过程细节,限定上下文关系,把对外的使用减少耦合。
使用 SqlSessionFactory 的工厂实现类包装了 SqlSession 的标准定义实现类,并由 SqlSession 完成对映射器对象的注册和使用。
要注意几个重要的知识点,包括:映射器、代理类、注册机、接口标准、工厂模式、上下文。这些工程开发的技巧都是在手写 Mybatis 的过程中非常重要的部分,了解和熟悉才能更好的在自己的业务中进行使用。