仿照 Mybatis 手写 ROM 框架

一、问题描述

1. 代码片段

在这里插入图片描述

2. 问题清单:

如上图所示,传统的 JDBC 连接存在下列问题:

  1. 创建数据库连接存在硬编码问题
  2. 频繁的数据库连接与释放
  3. 组织查询 SQL 存在硬编码问题
  4. SQL 参数个数的变化会导致代码的修改,不易维护
  5. 封装结果集存在硬编码问题,POJO 属性的修改会带来结果集封装代码的变更,不易维护

3. 解决思路

  1. 将数据库配置信息及 SQL 语句转移至 xml 中(解决硬编码问题)
  2. 使用数据库连接池来获取数据库连接(解决频繁操作数据库连接问题)
  3. 通过反射、自省功能对结果集进行封装

二、代码

既然问题已经罗列出来了,那么接下来就通过自己手写的 ROM 框架将传统 JDBC 中存在的问题来解决一下。

1. 编写自定义数据库连接配置文件及 SQL 查询文件

1.1 数据库配置文件: sqlMapConfig.xml

<configuration><dataSource><property name="driverClass" value="com.mysql.jdbc.Driver">property><property name="jdbcUrl" value="jdbc:mysql://localhost:3306/stage_1_mybatis?characterEncoding=utf-8">property><property name="user" value="root">property><property name="password" value="root">property>dataSource><mapper resource="UserMapper.xml">mapper>
configuration>

1.2 SQL 配置文件: UserMapper.xml

<mapper nameSpace="com.idol.dao.IUserDao"><select id="findAll" resultType="pojo.User">select * from userselect><select id="findByCondition" paramterType="pojo.User" resultType="pojo.User">select * from user where id=#{id} and name=#{name}select>mapper>

2. 配置文件解析

2.1 xml 资源加载工具类: Resource

上面两个步骤已经将, JDBC 中硬编码部分提取成了 xml 文件,因此为了方便对 xml 配置文件进行加载,需要编写资源加载类。

import java.io.InputStream;/*** @author Supreme_Sir* @version 1.0* @className Resource* @description* @date 2020/9/24 22:39**/
public class Resource {/*** xml 资源加载器*/public static InputStream getResourceAsStream(String path) {InputStream resourceAsStream = Resource.class.getClassLoader().getResourceAsStream(path);return resourceAsStream;}
}

2.2 数据库配置文件: sqlMapConfig.xml 解析

import com.idol.io.Resource;
import com.idol.pojo.Configeration;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;import java.beans.PropertyVetoException;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;/*** 数据库配置文件解析类** @author Supreme_Sir* @version 1.0* @className XMLConfigBuilder* @description* @date 2020/9/26 15:22**/
public class XMLConfigBuilder {private Configeration configeration;public XMLConfigBuilder() {this.configeration = new Configeration();}/*** 该方法用于将 xml 解析成配置文件对象* @param inputStream sqlMapConfig.xml 配置文件的 InputStream 对象*/public Configeration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {// 解析 sqlMapConfig.xmlDocument document = new SAXReader().read(inputStream);Element rootElement = document.getRootElement();List<Element> propertyList = rootElement.selectNodes("//property");Properties dataSourceProperties = new Properties();for (Element element : propertyList) {String name = element.attributeValue("name");String value = element.attributeValue("value");dataSourceProperties.put(name, value);}// 创建 c3p0 数据库连接池ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();comboPooledDataSource.setDriverClass(dataSourceProperties.getProperty("driverClass"));comboPooledDataSource.setJdbcUrl(dataSourceProperties.getProperty("jdbcUrl"));comboPooledDataSource.setUser(dataSourceProperties.getProperty("user"));comboPooledDataSource.setPassword(dataSourceProperties.getProperty("password"));this.configeration.setDataSource(comboPooledDataSource);// 解析 Mapper.xmlList<Element> mapperList = rootElement.selectNodes("//mapper");for (Element element : mapperList) {String mapperPath = element.attributeValue("resource");InputStream mapperStream = Resource.getResourceAsStream(mapperPath);XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(this.configeration);xmlMapperBuilder.parse(mapperStream);}return this.configeration;}
}

2.3 SQL 配置文件:UserMapper.xml 解析

import com.idol.pojo.Configeration;
import com.idol.pojo.MappedStatement;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;import java.io.InputStream;
import java.util.List;/*** SQL 配置文件解析类** @author Supreme_Sir* @version 1.0* @className XMLMapperBuilder* @description* @date 2020/9/26 15:45**/
public class XMLMapperBuilder {private Configeration configeration;public XMLMapperBuilder(Configeration configeration) {this.configeration = configeration;}public void parse(InputStream inputStream) throws DocumentException {Document document = new SAXReader().read(inputStream);Element rootElement = document.getRootElement();String nameSpace = rootElement.attributeValue("nameSpace");// 获取根节点下所有一级子节点List<Element> sqlElement = rootElement.elements();for (Element element : sqlElement) {String id = element.attributeValue("id");String resultType = element.attributeValue("resultType");String paramterType = element.attributeValue("paramterType");String sqlText = element.getTextTrim();MappedStatement mappedStatement = new MappedStatement(id, paramterType, resultType, sqlText);String mappedStatementName = nameSpace + "." + id;this.configeration.putMappedStatement(mappedStatementName, mappedStatement);}}
}

3. 创建 SQL 执行函数

3.1 创建执行 SQL 的统一方法

import com.idol.config.BondSql;
import com.idol.pojo.Configeration;
import com.idol.pojo.MappedStatement;
import com.idol.utils.GenericTokenParser;
import com.idol.utils.ParameterMapping;
import com.idol.utils.ParameterMappingTokenHandler;import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;/*** 创建数据库连接并执行 SQL 语句** @author Supreme_Sir* @version 1.0* @className SimpleExecutor* @description* @date 2020/9/26 16:48**/
public class SimpleExecutor implements Executor {/*** 执行 SQL** @param configeration 解析出来的数据库配置文件 DO 对象* @param mappedStatement 解析出来的 SQL 配置文件 DO 对象* @param params 条件查询参数* @param * @return* @throws Exception*/@Overridepublic <E> List<E> query(Configeration configeration, MappedStatement mappedStatement, Object... params) throws Exception {// 创建数据库连接Connection connection = configeration.getDataSource().getConnection();String sql = mappedStatement.getSql();// 将 SQL 配置文件中的 SQL 解析为可供 PreparedStatement 使用的 SQL 语句BondSql bondSql = getBondSql(sql);PreparedStatement preparedStatement = connection.prepareStatement(bondSql.getSqlText());// 将条件查询参数配置到 SQL 语句中String parameterType = mappedStatement.getParameterType();Class<?> parameterTypeClass = getClassType(parameterType);List<ParameterMapping> parameterMappingList = bondSql.getParameterMappingList();for (int i = 0; i < parameterMappingList.size(); i++) {ParameterMapping parameterMapping = parameterMappingList.get(i);String name = parameterMapping.getContent();Field field = parameterTypeClass.getDeclaredField(name);// 暴力访问对象属性field.setAccessible(true);// 获取参数中指定属性的值Object o = field.get(params[0]);preparedStatement.setObject(i + 1, o);}// 执行 SQL 查询语句ResultSet resultSet = preparedStatement.executeQuery();// 开始封装查询结果String resultType = mappedStatement.getResultType();Class<?> resultTypeClass = getClassType(resultType);List result = 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 val = resultSet.getObject(columnName);// 为查询结果实例赋值PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);Method writeMethod = propertyDescriptor.getWriteMethod();writeMethod.invoke(instance, val);}result.add(instance);}return result;}/*** 根据 SQL 配置文件中的类路径获取该对象的 Class** @param classPath* @return* @throws ClassNotFoundException*/private Class<?> getClassType(String classPath) throws ClassNotFoundException {if (classPath != null) {return Class.forName(classPath);}return null;}/*** 将包含 #{} 的 SQL 解析为可供 PreparedStatement 使用的 SQL** @param sql* @return*/private BondSql getBondSql(String sql) {// 参数标识处理类ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();// 解析 xml 文件中的 SQLGenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);String parsedSql = genericTokenParser.parse(sql);// 获取 SQL 中的参数名称List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();return new BondSql(parsedSql, parameterMappings);}
}

3.2 创建具体的 SQL 查询方法

import com.idol.pojo.Configeration;import java.lang.reflect.*;
import java.util.List;/*** 封装具体的 SQL 查询方法** @author Supreme_Sir* @version 1.0* @className DefaultSqlSession* @description* @date 2020/9/26 16:34**/
public class DefaultSqlSession implements SqlSession {private Configeration configeration;@Overridepublic <E> List<E> selectAll(String statementID, Object... params) throws Exception {SimpleExecutor simpleExecutor = new SimpleExecutor();return simpleExecutor.query(this.configeration, this.configeration.getMappedStatement(statementID), params);}@Overridepublic <T> T selectOne(String statementID, Object... params)  throws Exception {List<T> objectList = selectAll(statementID, params);if (objectList.size() == 1) {return objectList.get(0);} else {throw new RuntimeException("查询结果为空或查询结果不唯一!!!");}}/*** 通过 Java 动态代理执行 SQL** @param mapperClass* @param * @return*/@Overridepublic <T> T getMapper(Class<?> mapperClass) {Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 由于代理对象无法获取 SQL 配置文件中的相关信息,因此通过类名和方法名的拼接,完成成对应 id SQL 的调用String methodName = method.getName();String className = method.getDeclaringClass().getName();String statementID = className + "." + methodName;// 判断返回结果是否存在泛型,如果存在泛型则表示改结果为集合,则调用 selectAll 方法Type returnType = method.getGenericReturnType();if (returnType instanceof ParameterizedType) {return selectAll(statementID, args);}return selectOne(statementID, args);}});return (T) proxyInstance;}public DefaultSqlSession(Configeration configeration) {this.configeration = configeration;}
}

4. 方法测试

4.1 创建数据表: user.sql

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (`id` int(10) NOT NULL,`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, '张三');
INSERT INTO `user` VALUES (2, '李四');
INSERT INTO `user` VALUES (3, '王五');
INSERT INTO `user` VALUES (4, '赵六');
INSERT INTO `user` VALUES (5, '冯七');SET FOREIGN_KEY_CHECKS = 1;

4.2 编写测试方法

import com.idol.dao.IUserDao;
import com.idol.io.Resource;
import com.idol.sql.session.SqlSession;
import com.idol.sql.session.SqlSessionFactory;
import com.idol.sql.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import pojo.User;import java.io.InputStream;
import java.util.List;/*** @author Supreme_Sir* @version 1.0* @className MyFrameworkTest* @description* @date 2020/9/24 22:44**/
public class MyFrameworkTest {@Testpublic void getResourceAsStramTest() throws Exception {InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");SqlSessionFactory sqlsessionFactory = new SqlSessionFactoryBuilder().builder(resourceAsStream);SqlSession sqlSession = sqlsessionFactory.openSession();IUserDao userDao = sqlSession.getMapper(IUserDao.class);List<User> userList = userDao.findAll();System.out.println("SelectAll~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");for (User user : userList) {System.out.println(user);}System.out.println("\r\nSelectOne~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");User user = userDao.findByCondition(new User(1, "张三"));System.out.println(user);}
}

4.3 执行结果

SelectAll~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
User{id=1, name='张三'}
User{id=2, name='李四'}
User{id=3, name='王五'}
User{id=4, name='赵六'}
User{id=5, name='冯七'}SelectOne~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
User{id=1, name='张三'}

4.4 源码

下载源码

-------------------- 你在朋友圈看到的华丽照片,P掉了多少生活中的苦难 --------------------


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部