仿照 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>
    <!--添加 SQLMapper 文件信息,方便资源查找-->
    <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 user
    </select>

    <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.xml
        Document 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.xml
        List<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 <E>
     * @return
     * @throws Exception
     */
    @Override
    public <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 文件中的 SQL
        GenericTokenParser 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;

    @Override
    public <E> List<E> selectAll(String statementID, Object... params) throws Exception {
    
    
        SimpleExecutor simpleExecutor = new SimpleExecutor();
        return simpleExecutor.query(this.configeration, this.configeration.getMappedStatement(statementID), params);
    }

    @Override
    public <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 <T>
     * @return
     */
    @Override
    public <T> T getMapper(Class<?> mapperClass) {
    
    
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{
    
    mapperClass}, new InvocationHandler() {
    
    
            @Override
            public 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 {
    
    
    @Test
    public 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掉了多少生活中的苦难 --------------------

猜你喜欢

转载自blog.csdn.net/Supreme_Sir/article/details/108819127
今日推荐