[由零开始] 三、手写Mybatis-自定义持久层框架端的实现

[由零开始] 三、手写Mybatis-自定义持久层框架端的实现

  • 自定义持久层框架
    • 自定义持久层框架端实现
      • 自定义框架端分析
      • 自定义框架端的实现
        • 第一步 加载配置文件
        • 第二步 创建两个容器对象
        • 第三步 解析配置文件
        • 第四步 执行配置文件解析出的sql(解决替换占位符)

自定义持久层框架

上一期我们定义了使用端
这一期我们定义框架端

自定义持久层框架端实现

首先我们想要自定义框架供使用者使用 我们思考的问题就是要从使用者的角度出发

使用者(项目): 引入我们自定义的持久层框架的jar包(目的是利用框架对数据库进行操作)
   使用者需要提供两部分信息 1、数据库配置信息 2、sql配置信息(包括sql 、出入参)
      (1)自定义文件存放数据库配置信息(sqlMapConfig.xml)
      (2)自定义文件存放sql信息(mapper.xml)

自定义框架本身(完整工程): 本质上就是对JDBC代码进行封装
      (1) 加载配置文件 : 根据配置文件的路径, 加载配置文件成字节输入流, 存储在内存中
      (2) 创建两个javaBean (容器对象) :存放配置文件解析出来的内容
      (3) 利用dom4j 解析配置文件
      (4) 写相关代码执行JDBC代码

自定义框架端分析

之前我们分析了自定义框架的流程以及要做的事情
现在我们要想一下我们要的自定义框架到底是什么样的

首先 我们的框架一定是一个完整的工程 只有完整的工程在打成jar包时才可以被使用端所引用
第二 我们的使用端尽可能的用最少的代码完成对数据库的操作
明确以上两点 我们就可以开始动手了

(注: 这里的一些名称的定义都可以自定义 为了更好理解Mybatis的源码 我们尽量用到Mybatis的名称)

自定义框架端的实现

第一步 加载配置文件

我们要想办法完成可以加载配置文件的方法
我们创建类Resources 来完成这个操作
这个方法实际上就是用Class类的api 类加载器 把静态文件转成input流加载到内存
入参的path就是 我们要加载配置文件的路径
这个很好理解

package com.mrsoon.io;

import java.io.InputStream;

public class Resources {

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

第二步 创建两个容器对象

为了节省资源 我就不写get set方法了(实际上就是提供一种思路)
我们定义第一个容器用来装我们解析后的mapper里的具体方法
例如 id就是我们第二章提到的 namespace.id来组成 的 statementId 利用这个我们可以很轻松的定位到我们要执行的sql语句
剩下的就更好理解了 返回值我们用resultType来定义
参数值我们用paramterType 具体sql我们用sql来接

public class MappedStatement {

    //id标识
    private String id;
    //返回值类型
    private String resultType;
    //参数值类型
    private String paramterType;
    //sql语句
    private String sql;
    
    }

DataSource就是我们在sqlMapConfig.xml下的配置解析
下面就是我们解析出mapper的一个合集(因为一个mapper下有可能会对应多个sql)
我们用map来存封装好的MappedStatement对象
key statementId value: 封装好的MappedStatement对象

public class Configuration {

    private DataSource  dataSource;

    //key statementId value: 封装好的MappedStatement对象
    Map<String,MappedStatement> mappedStatementMap= new HashMap<>();
    
    }

第三步 解析配置文件

大致的流程是这样的
我们先创建一个方法 传入之前解析的input流 然后 利用dom4j解析出相关配置
然后把创建出的配置文件相关信息 传到c3p0连接池中(Mybatis自带的连接池配置是Pooled)
然后继续解析mapper 把解析好的全部信息封装到Configuration中返回上层
这时已经与数据库建立了连接

这些名称也是可以自己定义的 我也只是提供了一个思路和流程 方便理解Mybatis的源码

创建类SqlSessionFactoryBuilder以及builder方法


public class SqlSessionFactoryBuilder {

    public SqlSessionFactory builder(InputStream inputStream) throws DocumentException, PropertyVetoException {
        //第一 使用dom4j 解析配置文件, 将解析出来的内容封装到Configoration中

        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(inputStream);


        //第二 创建sqlSessionFactory对象 工厂模式 生产sqlSession
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);


        return defaultSqlSessionFactory;

    }
}

将配置文件解析 并封装 利用dom4j 然后传递出去


public class XMLConfigBuilder {

    private Configuration configuration;

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

    /**
     * 该方法就是将配置文件解析 并封装 利用dom4j
     * @param inputStream
     * @return
     */
    public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {

        Document document = new SAXReader().read(inputStream);
        //
        Element rootElement = document.getRootElement();
        List<Element> list = rootElement.selectNodes("//property");
        Properties properties = new Properties();
        for (Element element : list) {
            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("jdbcUrl");
        comboPooledDataSource.setUser("username");
        comboPooledDataSource.setPassword("password");

        configuration.setDataSource(comboPooledDataSource);

        //mapper.xml 解析  拿到路径--加载成字节输入流---dom4j字节流
        List<Element> mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            String mapperPath = element.attributeValue("resoirces");
            InputStream resourceAsSteam = Resources.getResourceAsSteam(mapperPath);
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
            xmlMapperBuilder.parse(resourceAsSteam);
        }
        return configuration;
    }
}

我们创建SqlSession接口 以及实现类DefaultSqlSession
这里主要是sql的实现方法 具体的sql执行实际上就是jdbc的执行
用到的方法就是 simpleExecutor.query 中


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 SQLException, IllegalAccessException, IntrospectionException, InstantiationException, NoSuchFieldException, InvocationTargetException, ClassNotFoundException {
        simpleExecutor simpleExecutor = new simpleExecutor();
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        List<Object> query = simpleExecutor.query(configuration, mappedStatement, params);
        return (List<E>) query;
    }

    @Override
    public <T> T selectOne(String statementId, Object... params) throws SQLException, IllegalAccessException, IntrospectionException, InstantiationException, ClassNotFoundException, InvocationTargetException, NoSuchFieldException {
        List<Object> objects = selectList(statementId, params);
        if (objects.size()==1){
            return (T)objects.get(0);
        }else{
            throw new RuntimeException("查询结果为空或者返回结果过多");
        }
    }
}

第四步 执行配置文件解析出的sql(解决替换占位符)

我们创建query方法 然后执行jdbc代码
本质上我们对数据库的操作就是jdbc的实现
我们创建Executor接口 以及它的实现类simpleExecutor

这里用到了反射的方法



public class simpleExecutor implements Executor {
    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IntrospectionException, InstantiationException, InvocationTargetException {

        //执行JDBC代码
        //1。注册驱动 获取连接
        Connection connection = configuration.getDataSource().getConnection();
        //2。获取sql语句 替换出入参
        String sql = mappedStatement.getSql();
        BoundSql boundSql= getBoundSql(sql);
        //3.获取预处理对象
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
        //4.设置参数
        String paramterType = mappedStatement.getParamterType();
        //获取入参数class
        Class<?> paramterTypeClass=getClassType(paramterType);
        String resultType = mappedStatement.getResultType();
        //获取出参数class
        Class<?> resultTypeClass=getClassType(resultType);


        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
        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 object = declaredField.get(params[0]);
            preparedStatement.setObject(i+1,object);
        }
        //5。执行sql
        ResultSet resultSet = preparedStatement.executeQuery();
        List<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;
        }else{
            return null;
        }
    }


    //1。将#{}使用? 代替
    //2. 替换相关值
    private BoundSql getBoundSql(String sql) {
        //标记处理类 配置标记解析器来完成对占位符的解析处理工作
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        //解析出来的sql
        String parse = genericTokenParser.parse(sql);
        //#{} 里面解析出来的参数名称
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
        BoundSql boundSql = new BoundSql(parse,parameterMappings);
        return boundSql;
    }
}

替换占位符用的mybatis源码中的标记处理器
TokenHandler 这个实现很简单也很好理解 当然也可以自己写
主要是替换和映射的问题
这样我们就完成了自定义持久层框架
也是一个小型mybatis的一个雏形
是可以用的了
一些mybatis的动态标签还有自动化映射问题我们还没有解决
下一章我们将优化我们的自定义框架

你可能感兴趣的:(由零开始)