自定义持久层(Mybatis)框架

本文详细介绍了从JDBC操作的问题出发,如何设计并实现一个简单的自定义Mybatis框架。通过分析JDBC的不足,提出了解决方案,包括使用数据库连接池、将SQL与参数抽取到XML配置文件,以及实体类封装。文章逐步讲解了配置文件的加载、解析,以及SqlSession的创建和执行过程,涉及Resources、Configuration、MappedStatement、SqlSessionFactoryBuilder等多个关键组件的实现。
摘要由CSDN通过智能技术生成

自定义框架

最近学习mybatis,为了提高阅读mybatis源码的效率,手撸了一个简单的mybatis框架,以下是搭建自定义框架中一些问题的解决方法。

此文可以算是笔者的一些笔记,但也同样希望能够为学习mybatis的小伙伴做一点微不足道的贡献。

从JDBC到持久层框架

犹记在上学时只学过用jdbc来连接数据库,当时每一次连接都要创建一个Connection,并且还不能忘记要将其关闭,实在烦不胜烦。

以下就要分析一遍jdbc存在的问题,我们自定义的框架也是围绕解决这些问题为中心实现的。

分析JDBC操作问题

首先来一段jdbc连接代码

    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, "zhangsan");
            // 向数据库发出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();
                }
            }
        }

    }

JDBC存在的问题:

  1. 数据库连接创建释放频繁,浪费系统资源,影响系统资源
  2. 连接参数,sql语句,传入参数存在硬编码,后续变化困难,不易维护
  3. 结果集需要进一步加工进集合返回,太复杂

解决JDBC问题思路:

  • 使用数据库线程池(C3P0)
  • 将连接参数,sql语句抽取到xml
  • 将实体类与表映射(与mybatis一样,传入sql参数用实体类封装,返回结果也用实体类封装)

自定义框架设计

首先明白框架的组成架构,就可以更好的去理解代码实现。框架的设计分为两端,使用端和框架端

使用端: 提供两个配置文件SqlMapConfig.xml和mapper.xml

  • SqlMapConfig.xml:存放数据源信息和引入mapper文件信息
  • mapper.xml:sql语句配置信息

框架端: 读取,解析配置文件,并且执行crud操作

  • 读取配置文件
    1. Resources:读取SqlMapConfig.xml字节文件加载成InputStream字节流
    2. Configuration:POJO类,利用DOM4J解析 InputStream字节流 获取到 数据源参数map<key,MapperStatement> 后,封装到 Configuration
    3. MappedStatement:封装解析mapper.xml后获得的sql语句statement类型输入参数java类型输出参数java类型
  • 解析配置文件
    1.SqlSessionFactoryBuilder 的 build() 方法
    用dom4j解析配置文件,将解析出来的内容封装到Configuration和MappedStatement中。
    创建SqlSessionFactory的实现类DefaultSqlSession。
    2.SqlSessionFactory 的 openSession() 方法
    生产DefaultSqlSession对象
    3.sqlSession接口及实现类的
    主要封装crud方法(底层由Executor执行)

自定义框架实现

先贴出执行sql语句的main方法,便于后续理解和对照

    @Test
    public void test() throws Exception {
    	//1.加载主配置文件SqlMapConfig.xml成字节流
        InputStream resourceAsSteam = Resources.getResourceAsSteam("sqlMapConfig.xml");
        //2.解析配置文件并生成SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsSteam);
        //3.得到SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //创建参数
        User user = new User();
        user.setId(1);
        user.setUsername("王五");
        //4.调用执行方法 5.封装结果都User
        User user2 = sqlSession.selectOne("user.findAll", user);
        System.out.println(user2);*/
}

需要的配置文件和实体类

按照自定义框架设计步骤,我会将使用端和框架端的代码贴出,请仔细查看

使用端SqlMapConfig.xml

<configuration>

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

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

</configuration>

使用端UserMapper.xml

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

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

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


</mapper>

框架端MappedStatement

public class MappedStatement {

    //id标识
    private String id;
    //返回值类型
    private String resultType;
    //参数值类型
    private String paramterType;
    //sql语句
    private String sql;
	......
	//省略getter和setter方法
}

框架端Configuration

public class Configuration {

    private DataSource dataSource;

    // key: statementid,value:封装好的mappedStatement对象,对应SqlMapConfig.xml的<mapper>
    Map<String,MappedStatement> mappedStatementMap = new HashMap<>();
	......
	//省略getter和setter方法
}

第一步 Resources怎么加载配置文件到字节流

框架端Resources
作用:加载配置文件到字节流

public class Resources {

    // 根据配置文件的路径,将配置文件加载成字节输入流,存储在内存中
    public static InputStream getResourceAsSteam(String path){
        InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
        return  resourceAsStream;
    }
}

第二步 如何解析字节流并封装到实体类

框架端XMLConfigBuilder
作用:使用dom4j解析字节流将SqlMapConfig配置文件数据内容封装到Configuration对象中

public class XMLConfigBuilder {

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

    /**
     * 该方法就是使用dom4j对配置文件进行解析,封装Configuration
     */
    public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {

        Document document = new SAXReader().read(inputStream);
        //从dom树中得到根元素<configuration>
        Element rootElement = document.getRootElement();
        //得到<configuration>中的所有<peoperty>
        List<Element> list = rootElement.selectNodes("//property");
        Properties properties = new Properties();
        //遍历<peoperty>元素集合,获取属性值,将其放入到Properties中
        for (Element element : list) {
            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
        configuration.setDataSource(comboPooledDataSource);

        //得到<mapper>元素集合
        List<Element> mapperList = rootElement.selectNodes("//mapper");
		//遍历集合
        for (Element element : mapperList) {
        	//逐个获得resource属性对应的mapper.xml的路径
            String mapperPath = element.attributeValue("resource");
            //将mapper.xml加载进字节流
            InputStream resourceAsSteam = Resources.getResourceAsSteam(mapperPath);

            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
            //解析mapper.xml字节流,将数据封装到mapperdStatement,再将mapperdStatement封装到configuration的map<namespace.id,mapperdStatement>集合
            xmlMapperBuilder.parse(resourceAsSteam);

        }

        return configuration;
    }

}

框架端XMLMapperBuilder
作用:使用dom4j解析字节流将mapper配置文件数据内容封装到MappedStatement对象,再将MappedStatement对象封到Configuration对象中

public class XMLMapperBuilder {
	//步骤大同小异,不做注释重复累赘
    private Configuration configuration;

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

    public void parse(InputStream inputStream) throws DocumentException {

        Document document = new SAXReader().read(inputStream);
        Element rootElement = document.getRootElement();

        String namespace = rootElement.attributeValue("namespace");

        List<Element> list = rootElement.selectNodes("//select");
        for (Element element : list) {
            String id = element.attributeValue("id");
            String resultType = element.attributeValue("resultType");
            String paramterType = element.attributeValue("paramterType");
            String sqlText = element.getTextTrim();
            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(id);
            mappedStatement.setResultType(resultType);
            mappedStatement.setParamterType(paramterType);
            mappedStatement.setSql(sqlText);
            //key值是namespace.id  ,  value值是mappedStatement
            String key = namespace+"."+id;
            configuration.getMappedStatementMap().put(key,mappedStatement);
        }
    }
}

第三步 Builder模式构建Configuration

框架端SqlSessionFactoryBuilder
作用:构建configuration,返回SqlSessionFactroy工厂对象

public class SqlSessionFactoryBuilder {

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


        // 第二:创建sqlSessionFactory对象:工厂类:生产sqlSession:会话对象
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);

        return defaultSqlSessionFactory;
    }

工厂模式和构建者模式多用于构建对象较为复杂的情况,但工厂模式下的对象往往都是一个完整的对象,而构建者模式下对象是可以一部分一部分去构建,可以得到不同的对象。

Mybatis中Configuration的构建比这复杂的多,在parseConfiguration()方法中有许多解析配置的方法,如下

private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
//解析<properties />标签
propertiesElement(root.evalNode("properties"));
// 解析 <settings /> 标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
//加载自定义的VFS实现类
loadCustomVfs(settings);
// 解析 <typeAliases /> 标签
typeAliasesElement(root.evalNode("typeAliases"));
//解析<plugins />标签
pluginElement(root.evalNode("plugins"));
// 解析 <objectFactory /> 标签
objectFactoryElement(root.evaINode("obj ectFactory"));
// 解析 <objectWrapper Factory /> 标签
obj ectWrappe rFacto ryElement(root.evalNode("objectWrapperFactory"));
// 解析 <reflectorFactory /> 标签
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 赋值 <settings /> 到 Configuration 属性
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析 <environments /> 标签
environmentsElement(root.evalNode("environments"));
// 解析 <databaseIdProvider /> 标签
databaseldProviderElement(root.evalNode("databaseldProvider"));
}

第三步 工厂模式生产SqlSession

框架端SqlSessionFactory接口及实现类DefaultSqlSessionFactory
作用:传递configuration到SqlSession,并且生产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执行语句

框架端SqlSession接口及实现类DefaultSqlSession
作用:传递封装的参数和sql给Executor并接收返回的数据集合

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里的query方法的调用
        simpleExecutor simpleExecutor = new simpleExecutor();
        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> objects = selectList(statementid, params);
        if(objects.size()==1){
            return (T) objects.get(0);
        }else {
            throw new RuntimeException("查询结果为空或者返回结果过多");
        }
    }
}

第四步 真正封装JDBC操作的Executor

框架端Executor接口及实现类SimpleExecutor
作用:执行jdbc操作

public class simpleExecutor implements  Executor {


    @Override                                                                                //user
    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.getSqlText());

        // 4. 设置参数
            //获取到了参数的全路径
         String paramterType = mappedStatement.getParamterType();
         Class<?> paramtertypeClass = getClassType(paramterType);

        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();

        //遍历BoundSql中的存放的数据(#{**}中的**)
        //使用filed.get(ParameterMap)方法获取对应**在ParameterMap(传入到sql的变量)的值
        //再用preparedStatement.setObject设置为sql的参数
        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String content = parameterMapping.getContent();

            //反射
            Field declaredField = paramtertypeClass.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.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;

    }

    private Class<?> getClassType(String paramterType) throws ClassNotFoundException {
        if(paramterType!=null){
            Class<?> aClass = Class.forName(paramterType);
            return aClass;
        }
         return null;

    }


    /**
     * 完成对#{}的解析工作:1.将#{}使用?进行代替,2.解析出#{}里面的值进行存储
     * @param sql
     * @return
     */
    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;

    }


}

框架端BoundSql和ParameterMapping
作用:存放解析出#{}的sql语句和被“?”替换的输入参数名

public class BoundSql {

    private String sqlText; //解析过后的sql

    private List<ParameterMapping> parameterMappingList = new ArrayList<>();

    public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
        this.sqlText = sqlText;
        this.parameterMappingList = parameterMappingList;
    }

    public String getSqlText() {
        return sqlText;
    }

    public void setSqlText(String sqlText) {
        this.sqlText = sqlText;
    }

    public List<ParameterMapping> getParameterMappingList() {
        return parameterMappingList;
    }

    public void setParameterMappingList(List<ParameterMapping> parameterMappingList) {
        this.parameterMappingList = parameterMappingList;
    }
}



public class ParameterMapping {
    private String content;
    public ParameterMapping(String content) {this.content = content; }
    public String getContent() {return content; }
    public void setContent(String content) { this.content = content; }
}

第五步 用于解析sql语句的工具类

用于解析sql语句,将#{}替换为?的工具类,从mybatis源码中copy。
分别是:

  1. GenericTokenParser
  2. TokenHandler接口和它的实现类ParameterMappingTokenHandler
    因为代码太多就不贴出来啦,大家可以去源码中找~~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值