自定义持久层框架 -- Mybatis阉割版

一、分析JDBC操作问题

public class Test {
    public static void main(String[] args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            // 加载数据库驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 通过驱动管理类获取数据库链接
            connection =
                    DriverManager.getConnection(
                            "jdbc:mysql://localhost:3306/mybatis? characterEncoding = utf - 8", " root", " root");
            // 定义sql语句?表示占位符
            String sql = "select * from user where username = ?";
            // 获取预处理statement
            preparedStatement = connection.prepareStatement(sql);
            // 设置参数,第⼀个参数为sql语句中参数的序号(从1开始),第⼆个参数为设置的参数值
            preparedStatement.setString(1, "tom");
            // 向数据库发出sql执⾏查询,查询出结果集
            resultSet = preparedStatement.executeQuery();
            // 遍历查询结果集
            while (resultSet.next()) {
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                // 封装User
                user.setId(id);
                user.setUsername(username);
            }
            System.out.println(user);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            if (resultSet != null) {
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (preparedStatement != null) {
                try {
                    preparedStatement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • 数据库配置信息存在硬编码问题
    解决方案:利用配置文件
  • 频繁创建释放数据库链接
    解决方案:连接池
  • sql语句,设置参数,获取结果集参数 均存在硬编码问题
    解决方案:利用配置文件,但是由于这几项信息是频繁改动的内容,所以不应该和数据库配置信息放在同一份配置文件中,应该分开存放
  • 手动封装返回结果集,较为繁琐
    解决方案:反射、内省

二、自定义框架设计思路分析

  • 使用端(项目)
    1. 引入自定义持久层框架的jar包
    2. 提供两部分配置信息
      • sqlMapConfig.xml : 存放数据源信息
      • Mapper.xml : sql 语句的配置文件信息
  • 自定义持久层框架本身(工程)
    1. 加载配置文件
      根据配置文件的路劲,加载配置文件成字节输入流,存入内存中
      创建 Resources 类,方法:InputSteam getResourceAsSteam(String path)
    2. 创建两个javaBean(容器对象)
      存放对配置文件解析出来的内容
      Configuration(核心配置类) : 存放 sqlMapConfig.xml 解析出来的内容
      MappedStatement(映射配置类):存放 Mapper.xml 解析出来的内容
    3. 使用dom4j解析配置文件
      创建 sqlSessionFactoryBuilder 类,方法:build(InputSteam in)
      创建 SqlSessionFactory 对象,生产 SqlSession
    4. 创建 SqlSessionFactory 接口及实现类 DefaultSqlSessionFactory
      方法:openSession生产SqlSession
    5. 创建SqlSession接口及实现类DefaultSession
      定义对数据库的CRUD操作
    6. 创建Executor接口及实现类SimpleExecutor
      方法:query(Configuration c, MappedStatement m, Object...params)执行JDBC代码

三、使用端代码编写

sqlMapConfig.xml 定义数据源信息

<configuration>

    <!--数据库配置信息-->
    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql:///wujun_test"></property>
        <property name="username" value="wujun"></property>
        <property name="password" value="wujun@2020"></property>
    </dataSource>

    <!--存放mapper.xml的全路径-->
    <mapper resource="UserMapper.xml"></mapper>

</configuration>

UserMapper.xml 定义 sql 信息
SQL唯一标识(StatementId)由 namespace.id 来组成
例如:com.wujun.dao.IUserDao.findAll

<mapper namespace="com.wujun.dao.IUserDao">

    <select id="findAll" resultType="com.wujun.pojo.User">
        select * from user
    </select>

    <select id="findByCondition" resultType="com.wujun.pojo.User" paramterType="com.wujun.pojo.User">
        select * from user where id = #{id} and username = #{username}
    </select>
</mapper>

四、自定义持久层框架代码编写

  1. 创建 Resources 类,加载配置文件
package com.wujun.io;

import java.io.InputStream;

public class Resources {

    // 借助类加载器,将指定路劲的配置文件加载成字节输入流,存储在内存中
    public static InputStream getResourceAsSteam(String path) {
       return Resources.class.getClassLoader().getResourceAsStream(path);
    }
}
  1. 创建两个 javaBean(容器对象)用来存放配置文件解析出来的信息
/**
 * 存放解析出来的数据源对象 以及 封装 MappedStatement
 * 以便于后续作为参数层层传递,最后进行JDBC操作
 */
@Data
public class Configuration {

    private DataSource dataSource;

    /**
     * key:StatementId(由 namespace.id 组成)
     */
    private Map<String, MappedStatement> mappedStatementMap = new HashMap<>();
}

/**
 * 针对SQL语句配置文件解析之后,用来存放的对象
 * 通俗的来说,有几个select标签,就会生成几个MappedStatement对象
 */
@Data
public class MappedStatement {

    /**
     * id标识
     */
    private String id;

    /**
     * 返回值类型
     */
    private String resultType;

    /**
     * 参数值类型
     */
    private String parameterType;

    /**
     * SQL语句
     */
    private String sql;
}
  1. 使用 dom4j 解析配置文件
/**
 * XML解析
 */
public class XMLConfigBuilder {

    private Configuration configuration;

    public XMLConfigBuilder() {
        this.configuration = new Configuration();
    }

    /**
     * 使用dom4j对配置文件进行解析,封装成 Configuration 对象
     */
    public Configuration parseConfig(InputStream in) throws DocumentException, PropertyVetoException {
        Document document = new SAXReader().read(in);

        // 拿到配置文件的根对象 <configuration>
        Element rootElement = document.getRootElement();
        // 查找所有的 property 标签
        List<Element> elementList = rootElement.selectNodes("//property");
        // 遍历 elementList
        Properties properties = new Properties();
        for (Element element : elementList) {
            String name = element.attributeValue("name");
            String value = element.attributeValue("value");
            properties.setProperty(name, value);
        }
        // 使用c3p0数据库连接池存放数据源信息
        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);

        // 解析sql配置文件信息,存入 configuration 中
        List<Element> mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            String mapperPath = element.attributeValue("resource");
            InputStream mapperResource = Resources.getResourceAsSteam(mapperPath);
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
            xmlMapperBuilder.parse(mapperResource);
        }

        return configuration;
    }
}
/**
 * Mapper XML解析
 */
public class XMLMapperBuilder {

    private Configuration configuration;

    public XMLMapperBuilder(Configuration configuration) {
        this.configuration = configuration;
    }

    /**
     * 使用dom4j对 mapper.xml 配置文件进行解析,放入 Configuration 对象
     */
    public void parse(InputStream in) throws DocumentException, PropertyVetoException {
        Document document = new SAXReader().read(in);

        // 拿到配置文件的根对象 <mapper>
        Element rootElement = document.getRootElement();
        // 获取 namespace
        String namespace = rootElement.attributeValue("namespace");
        // 查找所有的 select 标签
        List<Element> elementList = rootElement.selectNodes("//select");
        // 遍历 elementList
        for (Element element : elementList) {
            String selectId = element.attributeValue("id");
            String resultType = element.attributeValue("resultType");
            String parameterType = element.attributeValue("parameterType");
            String sql = element.getTextTrim();
            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(selectId);
            mappedStatement.setParameterType(parameterType);
            mappedStatement.setResultType(resultType);
            mappedStatement.setSql(sql);
            configuration.getMappedStatementMap().put(namespace + "." + selectId, mappedStatement);
        }
    }
}
  1. 创建SqlSession
public class SqlSessionFactoryBuild {

    /**
     * 解析配置文件,创建 SqlSession
     */
    public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException {
        // 1、使用dom4j解析配置文件,将解析出来的内容封装到 Configuration 中
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(in);

        // 2、创建SqlFactory对象
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
        return defaultSqlSessionFactory;

    }
}
  1. 定义对数据库的CRUD操作
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 {
        SimpleExecutor simpleExecutor = new SimpleExecutor(configuration);
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        List<Object> list = simpleExecutor.query(configuration, mappedStatement, params);
        return (List<E>) list;
    }

    @Override
    public <T> T selectOne(String statementId, Object... params) throws Exception {
        List<Object> list = selectList(statementId, params);
        if (list != null && list.size() == 1) {
            return (T) list.get(0);
        } else {
            throw new RuntimeException("查询得到的结果为空或者查询得到多个结果");
        }
    }
}
  1. 在SimpleExecutor 中编写JDBC代码
public class SimpleExecutor implements Executor {

    private Configuration configuration;

    public SimpleExecutor(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {

        // 1、注册驱动,获取数据库连接
        Connection connection = configuration.getDataSource().getConnection();

        /*
         * 2、获取sql语句 : select * from user where id = #{id} and username = #{username}
         * 转换sql语句: select * from user where id = ? and username = ? ,转换的过程中,还需要对#{}里面的值进行解析存储
         */
        String sql = mappedStatement.getSql();
        BoundSql boundSql = getBoundSql(sql);

        // 3、获取预处理对象 preparedStatement
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSql());

        // 4、设置参数
        String parameterType = mappedStatement.getParameterType();
        Class<?> parameterTypeClass = getClassType(parameterType);
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String content = parameterMapping.getContent();
            Field declaredField = parameterTypeClass.getDeclaredField(content);
            declaredField.setAccessible(true);
            Object o = declaredField.get(params[0]);
            preparedStatement.setObject(i + 1, o);
        }

        // 5. 执行sql
        ResultSet resultSet = preparedStatement.executeQuery();
        String resultType = mappedStatement.getResultType();
        Class<?> resultTypeClass = getClassType(resultType);

        ArrayList<Object> objects = new ArrayList<>();

        // 6. 封装返回结果集
        while (resultSet.next()) {
            Object o = resultTypeClass.getDeclaredConstructor().newInstance();
            ResultSetMetaData metaData = resultSet.getMetaData();
            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                String columnName = metaData.getColumnName(i);
                Object value = resultSet.getObject(columnName);
                //使用内省,根据数据库表和实体的对应关系,完成封装
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
                Method writeMethod = propertyDescriptor.getWriteMethod();
                writeMethod.invoke(o, value);
            }
            objects.add(o);
        }
        return (List<E>) objects;
    }

    /**
     * 完成对#{}的解析工作:1.将#{}使用?进行代替,2.解析出#{}里面的值进行存储
     * 具体工具类都是从 Mybatis 中直接拿来使用的
     */
    private BoundSql getBoundSql(String sql) {
        // 标记处理类:配置标记解析器来完成对占位符的解析处理工作
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        // 解析出来的sql
        String parseSql = genericTokenParser.parse(sql);
        // #{}里面解析出来的参数名称
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
        BoundSql boundSql = new BoundSql(parseSql, parameterMappings);
        return boundSql;
    }

    private Class<?> getClassType(String type) throws ClassNotFoundException {
        if (type != null) {
            Class<?> aClass = Class.forName(type);
            return aClass;
        }
        return null;
    }
}
  1. 使用端编写测试类
package com.wujun.test;

import com.alibaba.fastjson.JSON;
import com.wujun.io.Resources;
import com.wujun.pojo.User;
import com.wujun.sqlSession.SqlSession;
import com.wujun.sqlSession.SqlSessionFactory;
import com.wujun.sqlSession.SqlSessionFactoryBuild;
import org.junit.Test;

import java.io.InputStream;

public class IPersistenceTest {

    @Test
    public void test() throws Exception {
        InputStream resourceAsSteam = Resources.getResourceAsSteam("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuild().build(resourceAsSteam);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        User user = new User();
        user.setId(1L);
        user.setUsername("吴俊1");
        User sqlResult = sqlSession.selectOne("com.wujun.dao.IUserDao.findByCondition", user);
        System.out.println(JSON.toJSONString(sqlResult));
    }


}
  1. 测试返回结果
Connected to the target VM, address: '127.0.0.1:60755', transport: 'socket'
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.dom4j.io.SAXContentHandler (file:/D:/repository/dom4j/dom4j/1.6.1/dom4j-1.6.1.jar) to method com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser$LocatorProxy.getEncoding()
WARNING: Please consider reporting this to the maintainers of org.dom4j.io.SAXContentHandler
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
log4j:WARN No appenders could be found for logger (com.mchange.v2.log.MLog).
log4j:WARN Please initialize the log4j system properly.
{"id":1,"username":"吴俊1"}
Disconnected from the target VM, address: '127.0.0.1:60755', transport: 'socket'

Process finished with exit code 0
  1. 数据库截图


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容