title: 09.搭建ORM基础框架
tag: 笔记 手写SSM ORM
搭建一个仿Mybatis框架的基础框架逻辑,包括XML解析,代理对象工厂MapperProxyFactory,映射器注册MapperRegistry。
目标
目前我们的Summer实现Spring的IOC和AOP,基本还原了Spring框架的功能。现在我们想要整合一个持久层的框架,因此我们选择再实现一个Mybatis并将其整合到我们自己实现的Summer框架中。我们首先来搭建Mybatis的基础框架,并且本节不会设计到JDBC数据库的操作,我们只是还原一下Mybatis的基本原理。
实现映射器代理工厂
在学习Mybatis使用的时候我们知道我们调用接口的方法时拿到的是Mybatis生成的Mapper代理对象。这里需要使用我们在实现AOP时使用到的动态代理技术,Mybatis生成代理对象是根据接口来生成,因此我们只需要使用JDK自带的Proxy类即可实现。下面我们将实现一个映射器代理工厂来创建代理类:
MapperProxy
Mapper代理类:
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private Map<String, String> sqlSession;
private final Class<T> mapperInterface;
public MapperProxy(Map<String, String> sqlSession, Class<T> mapperInterface) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
return "你的被代理了!" + sqlSession.get(mapperInterface.getName() + "." + method.getName());
}
}
}
- 我们使用一个Map来模拟SqlSession,Mybatis中的SqlSession包含着Mybatis需要的信息。
- 它实现了
InvocationHandler
,在invoke方法执行到这个方法在SqlSession中的执行信息。
MapperProxyFactory
Mapper代理类工厂:
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public T newInstance(Map<String, String> sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface);
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
}
- 这里使用了工厂设计模式封装了Mapper代理类的创建过程。
Proxy.newProxyInstance
用于根据接口创建代理对象。
总结
这里的URL图:
![image-20240127134519711](https://i-blog.csdnimg.cn/blog_migrate/b580b767e39f300f2f7b5a6d1e267efe.png)
MapperProxy
负责实现InvocationHandler
接口的invoke
方法,最终所有的实际调用都会调用到这个方法包装的逻辑。MapperProxyFactory
是对MapperProxy
的包装,对外提供实例化对象的操作。当我们后面开始给每个操作数据库的接口映射器注册代理的时候,就需要使用到这个工厂类了。
实现映射器注册和使用
在实现了映射器代理工厂之后,我们可以使用工厂来创建代理Mapper。但目前我们还存在两个问题:
- 需要编码告知 MapperProxyFactory 要对哪个接口进行代理
- 编写一个假的 SqlSession 处理实际调用接口时的返回结果
针对以上两点,我们需要对映射器的注册提供一个注册机处理用户可以在使用的时候提供一个包的路径即可完成扫描和注册。与此同时需要对 SqlSession
进行规范化处理,让它可以把我们的映射器代理和方法调用进行包装,建立一个生命周期模型结构,便于后续的内容的添加。
MapperRegistry
public class MapperRegistry {
/**
* 将已添加的映射器代理加入到 HashMap
*/
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final 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 void addMappers(String packageName) {
Set<Class<?>> mapperSet = ClassScanner.scanPackage(packageName);
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
}
- 这个类使用了一个HashMap保存了**接口类型(Key)和映射器工厂MapperProxyFactory(Value)**之间的映射关系。
- 该类提供了注册Mapper和获取Mapper的方式。
- 获取Mapper的方式可以传入扫描包的方式添加或者传入接口添加,但本质上都是通过接口来添加。
SqlSession
我们将SqlSession定义为一个接口:
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,后续还会有相应其他方法的定义。
我们提供一个该接口的默认实现DefaultSqlSession
:
public class DefaultSqlSession implements SqlSession {
/**
* 映射器注册机
*/
private MapperRegistry mapperRegistry;
// 省略构造函数
@Override
public <T> T selectOne(String statement, Object parameter) {
return (T) ("你被代理了!" + "方法:" + statement + " 入参:" + parameter);
}
@Override
public <T> T getMapper(Class<T> type) {
return mapperRegistry.getMapper(type, this);
}
}
SelSession
具有一个MapperRegistry
注册机的属性。SelSession
同样需要实现getMapper
方法,并且这个方法委托给了MapperRegistry
,将来这里将变换委托给配置类Configration
,而Configration
实际上也是委托给MapperRegistry
,但在Configration
中除了Mapper
的注册信息还包含着其它的信息。
SqlSessionFactory
SqlSessionFactory
是一个接口,用于打开一个SqlSession,目前仅有一个方法:
public interface SqlSessionFactory {
/**
* 打开一个 session
* @return SqlSession
*/
SqlSession openSession();
}
我们同样给SqlSessionFactory
提供一个默认的实现:
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
时获取每个代理类的映射器对象了。
总结
xxxxxxxxxx @Componentpublic class AOPProxyCreator implements BeanPostProcessor, BeansAware { Map<String, Object> originBeans = new HashMap<>(); public Map<String, BeanDefinition> beans; public final Map<Class<? extends Annotation>, List> proxyRule = new ConcurrentHashMap<>(); public List aspectInstance = new ArrayList<>(8); ProxyFactory proxyResolver = new ProxyFactory(); private final Logger logger = LoggerFactory.getLogger(getClass()); @Override public Object postProcessBeforeInitialization(Object bean, String beanName) { Class<?> beanClass = bean.getClass(); if(checkJoinPoint(beanClass)){ originBeans.put(beanName, bean); return proxyResolver.createProxy(bean, new DynamicAopProxy(proxyRule, bean)); } return bean; } private boolean checkJoinPoint(Class<?> beanClass) { for (Annotation annotation : beanClass.getAnnotations()) { if(proxyRule.containsKey(annotation.annotationType())){ return true; } } for (Method method : beanClass.getMethods()) { for (Annotation annotation : method.getAnnotations()) { if(proxyRule.containsKey(annotation.annotationType())){ return true; } } } return false; } @Override public Object postProcessOnSetProperty(Object bean, String beanName) { Object origin = this.originBeans.get(beanName); return origin != null ? origin : bean; } @Override public void setApplicationContext(Map<String, BeanDefinition> beans) { this.beans = beans; aspectInstance = getAspectInstance(); parseAspectjClass(); logger.debug(“解析后的拦截规则为:{}”, proxyRule); } private void parseAspectjClass(){ for (Object aspect : aspectInstance) { for (Method method : aspect.getClass().getMethods()) { Around around = method.getAnnotation(Around.class); if(around != null){ Advice advice = new Advice(method, aspect); Class<? extends Annotation> targetAnno = around.targetAnno(); if(proxyRule.containsKey(targetAnno)){ proxyRule.get(targetAnno).add(advice); }else { List proxyChains = new ArrayList<>(); proxyChains.add(advice); proxyRule.put(targetAnno, proxyChains); } } } } } public List getAspectInstance() { List aspectDef = beans.values(). stream() .filter(definition -> definition.getBeanClass().isAnnotationPresent(Aspect.class)) .toList(); return aspectDef.stream().map(BeanDefinition::getInstance).toList(); }}java
MapperRegistry
提供包路径的扫描和映射器代理类注册机服务,完成接口对象的代理类注册处理。SqlSession
、DefaultSqlSession
用于定义执行 SQL 标准、获取映射器以及将来管理事务等方面的操作。基本我们平常使用Mybatis
的 API 接口也都是从这个接口类定义的方法进行使用的。SqlSessionFactory
是一个简单工厂模式,用于提供 SqlSession 服务,屏蔽创建细节,延迟创建过程。
Mapper XML解析和注册
现在我们已经实现了一个包含Mapper
注册信息的SqlSession
,但我们实现一个可用的SqlSession
还需要更多的配置信息。因此,我们会将框架需要的全部配置信息都存放到Configration
中,包括之前实现的映射器注册机MapperRegisry
。而这些信息我们需要从相应的配置文件中去获取,比如XML配置文件。因此,在本节我们需要实现下面几个目标:
- 实现
Configration
类用来保存SqlSession
需要的配置信息。 - 通过读取配置文件拿到构建
Configration
的配置信息。 - 通过
Configration
构建一个SqlSession
。
构建SqlSessionFactory建造者
我们最终目标是为了构建一个可以使用的SqlSession
,而我们使用工厂模式创建SqlSession,最后我们再提供一个建造者SqlSessionFactory
包装 XML 解析处理作为Mybatis
的入口:
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(Reader reader) {
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(reader);
return build(xmlConfigBuilder.parse());
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
SqlSessionFactoryBuilder
作为框架的入口类,通过配置文件XML的IO来引导Mybatis的启动。- 在这个类我们引入了
XMLConfigBuilder
和之前提过的Configration
中,XMLConfigBuilder
用于解析XML配置文件并通过配置信息构建Configration。
解析XML
我们解析XML的信息后需要构建一个Configuration
,因此我们可以先抽象一个基类ConfigBuilder
:
public abstract class ConfigBuilder {
protected final Configuration configuration;
public abstract Configuration parse();
public ConfigBuilder(Configuration configuration) {
this.configuration = configuration;
}
public Configuration getConfiguration() {
return configuration;
}
}
- 这个基类有一个
Configuration
的属性,并且可以通过parse
方法解析配置信息构建Configration
并且通过getConfiguration
获得到Configuration
。
我们目前仅提供XML配置的方式构建Configuration
,因此我们提供一个XMLConfigBuilder
继承ConfigBuilder
:
public class XMLConfigBuilder extends ConfigBuilder {
private Element root;
public XMLConfigBuilder(Reader reader) {
// 1. 调用父类初始化Configuration
super(new Configuration());
// 2. dom4j 处理 xml
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(new InputSource(reader));
root = document.getRootElement();
} catch (DocumentException e) {
e.printStackTrace();
}
}
/**
* 解析配置;类型别名、插件、对象工厂、对象包装工厂、设置、环境、类型转换、映射器
*
* @return Configuration
*/
public Configuration parse() {
try {
// 解析映射器
mapperElement(root.element("mappers"));
} catch (Exception e) {
throw new RuntimeException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
return configuration;
}
private void mapperElement(Element mappers) throws Exception {
List<Element> mapperList = mappers.elements("mapper");
for (Element e : mapperList) {
String resource = e.attributeValue("resource");
Reader reader = Resources.getResourceAsReader(resource);
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(new InputSource(reader));
Element root = document.getRootElement();
//命名空间
String namespace = root.attributeValue("namespace");
// SELECT
List<Element> selectNodes = root.elements("select");
for (Element node : selectNodes) {
String id = node.attributeValue("id");
String parameterType = node.attributeValue("parameterType");
String resultType = node.attributeValue("resultType");
String sql = node.getText();
// ? 匹配
Map<Integer, String> parameter = new HashMap<>();
Pattern pattern = Pattern.compile("(#\\{(.*?)})");
Matcher matcher = pattern.matcher(sql);
for (int i = 1; matcher.find(); i++) {
String g1 = matcher.group(1);
String g2 = matcher.group(2);
parameter.put(i, g2);
sql = sql.replace(g1, "?");
}
String msId = namespace + "." + id;
String nodeName = node.getName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
MappedStatement mappedStatement = new MappedStatement.Builder(configuration, msId, sqlCommandType, parameterType, resultType, sql, parameter).build();
// 添加解析 SQL
configuration.addMappedStatement(mappedStatement);
}
// 注册Mapper映射器
configuration.addMapper(Resources.classForName(namespace));
}
}
}
- 我们在该类中需要从XML中解析到类型别名、插件、对象工厂、对象包装工厂、设置、环境、类型转换、映射器,但目前我们还不需要那么多,所以只做一些必要的 SQL 解析处理。
- 我们增加了一个类
MappedStatement
,这个类代表一个接口方法的映射信息,我们将在下面介绍。
Configration配置类
现在我们来介绍存储配置信息的类Configration
,目前我们需要的配置信息其实只有两个:
Mapper
注册信息,就是我们之前实现的MapperRegistry
。Mapper
映射语句的信息,我们使用一个Map来实现,key是接口方法的全路径,value即MappedStatement
映射语句的信息。
public class Configuration {
/**
* 映射注册机
*/
protected MapperRegistry mapperRegistry = new MapperRegistry(this);
/**
* 映射的语句,存在Map里
*/
protected final Map<String, MappedStatement> mappedStatements = new HashMap<>();
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
public boolean hasMapper(Class<?> type) {
return mapperRegistry.hasMapper(type);
}
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms);
}
public MappedStatement getMappedStatement(String id) {
return mappedStatements.get(id);
}
}
- 我们为两个属性分别提供了注册
MappedStatement
和注册Mapper
的方法。
MappedStatement表示一个映射语句的信息:
public class MappedStatement {
private Configuration configuration;
private String id;
private SqlCommandType sqlCommandType;
private String parameterType;
private String resultType;
private String sql;
private Map<Integer, String> parameter;
MappedStatement() {
// constructor disabled
}
/**
* 建造者
*/
public static class Builder {
private MappedStatement mappedStatement = new MappedStatement();
public Builder(Configuration configuration, String id, SqlCommandType sqlCommandType, String parameterType, String resultType, String sql, Map<Integer, String> parameter) {
mappedStatement.configuration = configuration;
mappedStatement.id = id;
mappedStatement.sqlCommandType = sqlCommandType;
mappedStatement.parameterType = parameterType;
mappedStatement.resultType = resultType;
mappedStatement.sql = sql;
mappedStatement.parameter = parameter;
}
public MappedStatement build() {
assert mappedStatement.configuration != null;
assert mappedStatement.id != null;
return mappedStatement;
}
}
- 这个类保存着一个映射语句的信息,并且我们提供了一个
Builder
建造者来构建这个MappedStatement
。
MapperMethod和缓存
我们现在提供一个MapperMethod
来封装一个映射方法:
public class MapperMethod {
private final SqlCommand command;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration configuration) {
this.command = new SqlCommand(configuration, mapperInterface, method);
}
public Object execute(SqlSession sqlSession, Object[] args) {
return switch (command.getType()) {
case INSERT -> sqlSession.selectOne(command.getName(), args);
case DELETE -> sqlSession.selectOne(command.getName(), args);
case UPDATE -> sqlSession.selectOne(command.getName(), args);
case SELECT -> sqlSession.selectOne(command.getName(), args);
default -> throw new RuntimeException("Unknown execution method for: " + command.getName());
};
}
/**
* SQL 指令
*/
public static class SqlCommand {
private final String name;
private final SqlCommandType type;
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
String statementName = mapperInterface.getName() + "." + method.getName();
MappedStatement ms = configuration.getMappedStatement(statementName);
name = ms.getId();
type = ms.getSqlCommandType();
}
public String getName() {
return name;
}
public SqlCommandType getType() {
return type;
}
}
}
SqlCommand
包含两个信息,方法的全路径和sql的种类,创建MapperMethod
时会同步解析出SqlCommand
属性。excute
方法用于执行这个映射方法的,我们之后会将excute
执行过程委托给其它来执行,目前我们只是打印出sql
的一些信息。
总结
@Test
public void test_SqlSessionFactory() throws IOException {
// 1. 从SqlSessionFactory中获取SqlSession
Reader reader = Resources.getResourceAsReader("mybatis-config-datasource.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 2. 获取映射器对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 3. 测试验证
String res = userDao.queryUserInfoById("10001");
logger.info("测试结果:{}", res);
}
至此,我们已经可以通过上面的代码得到下面的执行结果:
方法:test_03.dao.IUserDao.queryUserInfoById
入参:[Ljava.lang.Object;@704921a5
待执行SQL:
SELECT id, userId, userHead, createTime
FROM user
where id = ?
现在我们框架的启动流程大致如下:
![image-20240129163427275](https://i-blog.csdnimg.cn/blog_migrate/3f2c4ad5443d7bb3637aaf46bff94350.png)
我们已经实现了这个框架启动的大致流程,我们下面的工作将会集中处理如何在数据库中调用sql语句并将结果返回。
r(IUserDao.class);
// 3. 测试验证
String res = userDao.queryUserInfoById("10001");
logger.info("测试结果:{}", res);
}
至此,我们已经可以通过上面的代码得到下面的执行结果:
```java
方法:test_03.dao.IUserDao.queryUserInfoById
入参:[Ljava.lang.Object;@704921a5
待执行SQL:
SELECT id, userId, userHead, createTime
FROM user
where id = ?
现在我们框架的启动流程大致如下:
![image-20240129163427275](https://i-blog.csdnimg.cn/blog_migrate/3f2c4ad5443d7bb3637aaf46bff94350.png)
我们已经实现了这个框架启动的大致流程,我们下面的工作将会集中处理如何在数据库中调用sql语句并将结果返回。