借鉴Mybatis框架手写一个ORM框架。mybatis整体架构中的整体思路是,首先解析一下配置文件,一个是框架的全局配置文件,一个是mapper配置文件,定义格式如下
<configuration>
</configuration>
<mapper>
</mapper>
其中,全局配置文件中包含数据源配置信息和mapper配置文件所在位置,如下
<configuration>
<!-- 数据源配置 -->
<dataSource>
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
<!-- 记录mapper.xml的路径 -->
<!--<mapper resource="UserMapper"/>-->
<mapper resource="com/***/UserMapper.xml"/>
</configuration>
对该配置文件解析后,可以将这些数据封装成一个java实体,该实体包含了所有的配置信息,全局配置文件中可能包含多个mapper文件的配置,可以将其封装成一个map集合,如下
Map<String, MapperStatement>
其中,集合的key为String类型,value为MapperStatement类型,MapperStatement是对mapper配置文件的一个封装,如下
<mapper namespace="com.***.UserMapper">
<select id="selectList" resultType="com.***.User">
select * from user
</select>
</mapper>
框架会将整个工程中的每个mapper配置文件都封装成一个个MapperStatement并保存到map中,这就需要对每个MapperStatement进行区分,区分的关键就是mapper配置文件中的namespace和id,将其拼接起来作为statementId。
然后提供对应的curd方法,方法的作用是对sql语句进行解析并调用jdbc查询数据库,最后封装结果集。
具体实现如下:
- 解析配置文件
新建一个类Resources,该类负责将一个文件转换成输入流,如下
public class Resources {
/**
* 根据配置文件的路径将配置文件加载成字节输入流
*
* @param path
* @return
*/
public static InputStream getResourceAsStream(String path) {
return Resources.class.getClassLoader().getResourceAsStream(path);
}
}
接下来新建一个SqlSessionFactoryBuilder类,该类提供一个build方法来生成SqlSessionFactory,如下
public class SqlSessionFactoryBuilder {
/**
* 构建
*
* @param inputStream 文件配置流
* @return
* @throws DocumentException
* @throws PropertyVetoException
*/
public SqlSessionFactory build(InputStream inputStream) throws DocumentException, PropertyVetoException {
// 使用dom4j解析配置文件,将解析出来的内容封装到Configuration中
XmlConfigBuilder builder = new XmlConfigBuilder();
Configuration configuration = builder.parseConfig(inputStream);
// 创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
return sqlSessionFactory;
}
}
其中SqlSessionFactory是一个接口,需要创建它的默认实现类DefaultSqlSessionFactory,该类需要传入一个Configuration类型对象,这个Configuration就是对全局配置的一个封装,如下
public class Configuration {
private DataSource dataSource;
/**
* key:statementId,namespace + id
* value:封装好的MapperStatement对象
*/
private Map<String, MapperStatement> mapperStatementMap = new HashMap<>();
}
那么现在关键就是对全局配置文件进行解析了,需提供一个类XmlConfigBuilder,该类提供parseConfig方法来将输入流转换为Configuration对象,如下
public class XmlConfigBuilder {
private Configuration configuration;
public XmlConfigBuilder() {
this.configuration = new Configuration();
}
public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
Document document = new SAXReader().read(inputStream);
// 全局查找<configuration>标签
Element rootElement = document.getRootElement();
// 全局查找<property>标签
List<Node> list = rootElement.selectNodes("//property");
List<Element> propertyList = list.stream().map(node -> (Element) node).collect(Collectors.toList());
Properties properties = new Properties();
propertyList.forEach(element -> {
// 获取到标签中的name和value属性
String name = element.attributeValue("name");
String value = element.attributeValue("value");
properties.setProperty(name, value);
});
// 创建数据源
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
comboPooledDataSource.setUser(properties.getProperty("username"));
comboPooledDataSource.setPassword(properties.getProperty("password"));
configuration.setDataSource(comboPooledDataSource);
// 解析mapper.xml文件
XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
List<Node> list2 = rootElement.selectNodes("//mapper");
List<Element> mapperList = list2.stream().map(node -> (Element) node).collect(Collectors.toList());
for (Element element : mapperList) {
String mapperPath = element.attributeValue("resource");
InputStream mapperAsStream = Resources.getResourceAsStream(mapperPath);
//XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
xmlMapperBuilder.parse(mapperAsStream);
}
return configuration;
}
}
借助dom4j可以很容易的实现解析,将每个标签中的属性和属性值读取处理进行对应的封装即可,对应mapper配置文件的解析也是如此,通过resource属性可以得到mapper文件位置,然后将其转为输入流并解析,如下
public class XmlMapperBuilder {
private Configuration configuration;
public XmlMapperBuilder(Configuration configuration) {
this.configuration = configuration;
}
/**
* mapper配置文件解析成MappedStatement对象
*
* @param inputStream
* @throws DocumentException
*/
public void parse(InputStream inputStream) throws DocumentException {
Document document = new SAXReader().read(inputStream);
// 得到<mapper>根标签
Element rootElement = document.getRootElement();
String namespace = rootElement.attributeValue("namespace");
// 得到<select>标签
List<Node> list = rootElement.selectNodes("//select");
List<Element> selectList = list.stream().map(node -> (Element) node).collect(Collectors.toList());
handlerMapperStatement(namespace, selectList);
// 得到<insert>标签
List<Node> list2 = rootElement.selectNodes("//insert");
List<Element> insertList = list2.stream().map(node -> (Element) node).collect(Collectors.toList());
handlerMapperStatement(namespace,insertList);
}
/**
* 封装MapperStatement对象
* @param namespace 命名空间
* @param selectNodes 操作节点
*/
private void handlerMapperStatement(String namespace, List<Element> selectNodes) {
if (selectNodes == null || selectNodes.size() == 0) {
return;
}
selectNodes.forEach(element -> {
String id = element.attributeValue("id");
String resultType = element.attributeValue("resultType");
String parameterType = element.attributeValue("parameterType");
String sql = element.getTextTrim();
// 封装MapperStatement对象
MapperStatement mapperStatement = new MapperStatement();
mapperStatement.setId(id);
mapperStatement.setResultType(resultType);
mapperStatement.setParameterType(parameterType);
mapperStatement.setSql(sql);
//key:namespace+id,用于标识哪个命名空间下的哪个方法,不然不同文件可能有重名方法
String key = namespace + "." + id;
// 将MapperStatement对象保存到Configuration中
configuration.getMapperStatementMap().put(key, mapperStatement);
});
}
}
同样读取每个配置的属性名和属性值,对应MappedStatementMap的封装,其map的key为namespace+id。
- 执行查询
读取完配置文件之后,就得到了一个SqlSessionFactory接口,其实现类为DefaultSqlSessionFactory对象,该对象提供一个openSession方法来获得SqlSession对象,如下
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
其中,返回的SqlSession接口的默认实现DefaultSqlSession,如下
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
@Override
public <E> List<E> selectList(String statementId, Object... params) throws Exception {
Executor executor = new SimpleExecutor();
MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
List<Object> objects = executor.query(configuration, mapperStatement, params);
return (List<E>) objects;
}
@Override
public <T> T selectOne(String statementId, Object... params) throws Exception {
List<Object> objects = this.selectList(statementId, params);
if (objects != null && objects.size() == 1) {
return (T) objects.get(0);
} else {
throw new RuntimeException("返回结果为空或返回结果过多");
}
}
@Override
public Integer insertOne(String statementId, Object... params) throws Exception {
Executor executor = new SimpleExecutor();
MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
return executor.save(configuration, mapperStatement, params);
}
@Override
public <T> T getMapper(Class<?> mapperClass) {
//通过jdk动态代理获取对象
Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//方法名(这也是为什么xml中id要和接口方法名一致)
String methodName = method.getName();
//类全路径名(这也是为什么xml中namespace要和接口全路径一致)
String className = method.getDeclaringClass().getName();
String statementId = className + "." + methodName;
Type genericReturnType = method.getGenericReturnType();
if (genericReturnType instanceof ParameterizedType) {
return selectList(statementId, args);
}
if (genericReturnType.getTypeName().contains("Integer")) {
return insertOne(statementId, args);
}
return selectOne(statementId, args);
}
});
return (T) proxyInstance;
}
}
在该对象中,实现查询操作时需要借助一个SimpleExecutor类来实现具体的查询,那执行查询操作需要那些参数,包括configuration、MapperStatement、查询参数,其中configuration里面是数据源和MapperStatement对象,这样就可以实现查询了。
- 实现查询
执行查询需要Executor来完成,其实现类如下:
public class SimpleExecutor implements Executor {
@Override
public <T> List<T> query(Configuration configuration, MapperStatement mapperStatement, Object... params) throws Exception {
Connection connection = configuration.getDataSource().getConnection();
// select * from e_user where id = #{id} and name = #{name}
String sql = mapperStatement.getSql();
// 将sql中的 #{} 替换为 ?
SqlMapping sqlMapping = getSqlMapping(sql);
PreparedStatement preparedStatement = connection.prepareStatement(sqlMapping.getParseSql());
// 获取到参数的全限定类名
String parameterType = mapperStatement.getParameterType();
Class<?> parameterClass = getClassType(parameterType);
// 设置参数
List<ParameterMapping> parameterMappings = sqlMapping.getParameterMappings();
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
String fieldName = parameterMapping.getParam();
// 反射设置值
Field field = parameterClass.getDeclaredField(fieldName);
field.setAccessible(true);
Object param = field.get(params[0]);
preparedStatement.setObject(i + 1, param);
}
// 执行sql
ResultSet resultSet = preparedStatement.executeQuery();
String resultType = mapperStatement.getResultType();
Class<?> resultTypeClass = getClassType(resultType);
List<Object> list = new ArrayList<>();
// 封装返回结果集
while (resultSet.next()) {
// 获取实体实例
Object instance = resultTypeClass.newInstance();
// 获取元数据
ResultSetMetaData metaData = resultSet.getMetaData();
for (int i = 1; i <= metaData.getColumnCount(); ++i) {
// 获取字段名
String columnName = metaData.getColumnName(i);
// 获取字段值
Object columnValue = resultSet.getObject(columnName);
// 内省设置值,映射表和实体的关系
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
Method writeMethod = propertyDescriptor.getWriteMethod();
writeMethod.invoke(instance, columnValue);
}
list.add(instance);
}
return (List<T>) list;
}
@Override
public Integer save(Configuration configuration, MapperStatement mappedStatement, Object... params) throws Exception {
Connection connection = configuration.getDataSource().getConnection();
SqlMapping sqlMapping = getSqlMapping(mappedStatement.getSql());
PreparedStatement preparedStatement = connection.prepareStatement(sqlMapping.getParseSql());
List<ParameterMapping> parameterMappings = sqlMapping.getParameterMappings();
String parameterType = mappedStatement.getParameterType();
Class<?> parameterTypeClass = getClassType(parameterType);
for (int i = 0; i < parameterMappings.size(); i++) {
String fieldName = parameterMappings.get(i).getParam();
Field field = parameterTypeClass.getDeclaredField(fieldName);
field.setAccessible(true);
Object param = field.get(params[0]);
preparedStatement.setObject(i + 1, param);
}
return preparedStatement.executeUpdate();
}
private Class<?> getClassType(String parameterType) throws ClassNotFoundException {
if (parameterType != null) {
return Class.forName(parameterType);
}
return null;
}
/**
* 解析sql
*
* @param sql
* @return
*/
private SqlMapping getSqlMapping(String sql) {
// 配合标记解析器完成占位符的解析
ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", tokenHandler);
// sql解析
String parseSql = genericTokenParser.parse(sql);
// 解析#{}中的参数名称
List<ParameterMapping> parameterMappings = tokenHandler.getParameterMappings();
return new SqlMapping(parseSql, parameterMappings);
}
}
整个框架的核心部分就是这个SimpleExecutor类,我们知道,JDBC中preparedStatement类执行的sql是以?作为占位符的,所以我们把#{}替换成?,并将#{id}里面的属性名取出来,这就是查询的一些参数信息,将参数类型和返回类型均通过反射内省技术进行值的封装后,即可得到查询结果。
其整体的工程结构如下:
- 工程应用
新建一个java工程,引入手写的ORM框架,如下
<dependencies>
<dependency>
<groupId>com.***</groupId>
<artifactId>mybatis_***</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
注意其前提是将手写的ORM工程打包到maven的本地仓库,这样就可以通过pom进行引入了。
全局配置文件,如下
<configuration>
<!-- 数据源配置 -->
<dataSource>
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
<!-- 记录mapper.xml的路径 -->
<!--<mapper resource="UserMapper"/>-->
<mapper resource="com/***/UserMapper.xml"/>
</configuration>
mapper配置文件,如下
<mapper namespace="com.ldc.mapper.UserMapper">
<select id="selectList" resultType="com.ldc.model.User">
select * from user
</select>
<select id="selectOne" parameterType="com.ldc.model.User" resultType="com.ldc.model.User">
select * from user where id = #{id} and name = #{name}
</select>
<insert id="insertOne" parameterType="com.ldc.model.User" resultType="Integer">
insert into user(name) values(#{name})
</insert>
</mapper>
测试,如下
@Test
public void test() throws Exception {
InputStream inputStream = Resources.getResourceAsStream("Mybatis-Config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = new User();
user.setId(2);
user.setName("caocao");
List<User> userList = sqlSession.selectList("com.ldc.mapper.UserMapper.selectList");
System.out.println(userList);
User user2 = sqlSession.selectOne("com.ldc.mapper.UserMapper.selectOne", user);
System.out.println(user2);
// Integer integer = sqlSession.insertOne("com.ldc.mapper.UserMapper.insertOne", user);
// System.out.println(integer);
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList2 = userMapper.selectList();
System.out.println(userList2);
User user3 = userMapper.selectOne(user);
System.out.println(user3);
// Integer integer2 = userMapper.insertOne(user);
// System.out.println(integer2);
}
测试结果,如下
继续深入理解ORM框架底层,做到知其然,知其所以然。