我的架构梦:(一)手写MyBatis框架

手写mybatis框架

    • 一、前言
    • 二、JDBC问题分析
    • 三、自定义框架实现
    • 四、自定义框架优化
    • 五、代码仓库

一、前言

在手写mybatis框架之前 ,我们先来思考一下这个问题:为啥要有mybatis框架存在?它是为了解决什么问题的?我们带着这两个问题来开始我们手写mybatis框架之旅。

我们刚开始搞java的时候,貌似都知道用jdbc去连接数据库,那我们来看一下jdbc连接数据库的代码:

public static void main(String[] args) {
    Connection conn = null;
    PreparedStatement ps = null;
    ResultSet rs = null;
    try {
        // 加载数据库驱动
        Class.forName("com.mysql.jdbc.Driver");
        // 通过驱动管理类获取数据库连接
        conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8",
                "root", "root");
        // 定义sql语句 ?标识占位符
        String sql = "select * from user where id = ? aand username = ?";
        // 获取预处理statement
        ps = conn.prepareStatement(sql);
        // 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为 设置的参数值
        ps.setString(1,  "riemann");
        // 向数据库发出sql执行查询,查询出结果集
        rs = ps.executeQuery();
        // 遍历结果集
        while (rs.next()) {
            int id = rs.getInt("id");
            String username = rs.getString("username");
            // 封装User
            user.setId(id);
            user.setUsername(username);
        }
        System.out.println(user);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 释放资源
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

相信有一定年限工作年限的小伙伴们,不难发现jdbc操作的问题。

二、JDBC问题分析

  1. 数据库连接创建、释放频繁造成系统资源浪费,从而影响性能。
  2. sql语句存在硬编码,造成代码不易维护。
  3. 使用preparedStatement向占有位符号传参数存在硬编码问题。
  4. 对结果解析存在硬编码(查询列名),sql变化导致解析代码变化。

知道了JDBC存在的一些问题,我们就来对症下药找解决方案。

1、问题解决方案

  • 数据库频繁创建连接以及释放资源:连接池
  • sql语句及参数存在硬编码:配置文件
  • 手动解析封装返回结果集:反射、内省

这也就解释了前面提的问题,为什么要有mybatis框架的存在?这是因为为了解决JDBC操作存在的这些问题。

好了,下面我们就来针对上面的解决方案来设计一个mybatis框架。

2、自定义框架设计

(1)、客户端:

提供核心配置文件

  • SqlMapConfig.xml: 存放数据源配置信息
  • Mapper.xml: sql语句的配置文件信息

(2)、框架端:

  1. 读取配置文件

    读取完以后以流的形式存在,我们不能讲读取到的配置信息以流的形式存放在内存中,不好操作,可以创建JavaBean来存储。

    Configuration:存放数据库基本信息和Map<唯一标识,Mapper> 唯一标识:namespace+"."+id
    MappedStatement:sql语句、statement类型、输入参数java类型、输出参数java类型

  2. 解析配置文件

    创建SqlSessionFactoryBuilder类:
    方法:SqlSessionFactory build();

    使用dom4j解析配置文件,将解析出来的内容封装到 ConfigurationMappedStatement
    创建SqlSessionFactory的实现类DefaultSqlSession

  3. 创建SqlSessionFactory

    方法:openSession():获取SqlSession接口的实现类实例对象

  4. 创建SqlSession接口及实现类:主要封装CRUD方法

    方法:
    selectList(String statementId, Object... params); // 查询所有
    selectOne(String statementId, Object... params); // 查询单个
    close(); 释放资源
    具体实现:封装JDBC完成对数据库表的查询操作

3、设计模式

构建者模式、工厂模式 、代理模式

三、自定义框架实现

在客户端项目中创建配置文件:

1、创建SqlMapConfig.xml

<configuration>

    
    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver">property>
        <property name="jdbcUrl" value="jdbc:mysql:///mybatis">property>
        <property name="username" value="root">property>
        <property name="password" value="root">property>
    dataSource>

    
    <mapper resource="UserMapper.xml">mapper>

configuration>

2、创建UserMapper.xml

<mapper namespace="user">

    
    <select id="selectList" resultType="com.riemann.pojo.User">
        select * from user
    select>

    
    <select id="selectOne" resultType="com.riemann.pojo.User" parameterType="com.riemann.pojo.User">
        select * from user where id = #{id} and username = #{username}
    select>

mapper>

接下来就来框架端编写相应的类:

3、User对象

@Data
public class User {

    private Integer id;

    private String username;

}

4、在父工程中引入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-testartifactId>
        <scope>testscope>
    dependency>
    <dependency>
        <groupId>junitgroupId>
        <artifactId>junitartifactId>
        <version>4.12version>
    dependency>
    <dependency>
        <groupId>com.google.guavagroupId>
        <artifactId>guavaartifactId>
        <version>28.2-jreversion>
    dependency>
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <version>1.18.12version>
        <scope>providedscope>
    dependency>
    <dependency>
        <groupId>org.apache.commonsgroupId>
        <artifactId>commons-lang3artifactId>
        <version>3.9version>
    dependency>
dependencies>

5、在子工程中mybatis-persistence引入依赖

<dependencies>
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <version>8.0.19version>
    dependency>
    <dependency>
        <groupId>c3p0groupId>
        <artifactId>c3p0artifactId>
        <version>0.9.1.2version>
    dependency>
    <dependency>
        <groupId>log4jgroupId>
        <artifactId>log4jartifactId>
        <version>1.2.17version>
    dependency>
    <dependency>
        <groupId>dom4jgroupId>
        <artifactId>dom4jartifactId>
        <version>1.6.1version>
    dependency>
    <dependency>
        <groupId>jaxengroupId>
        <artifactId>jaxenartifactId>
        <version>1.1.6version>
    dependency>
dependencies>

6、完成框架端中的读取配置文件

(1)、编写Configuration类

@Data
public class Configuration {

    private DataSource dataSource;

    /**
     * key:statementId
     * value:封装好的MappedStatement
     */
    Map<String, MappedStatement> mappedStatementMap = Maps.newHashMap();

}

(2)、编写MappedStatement类

@Data
public class MappedStatement {

    // id标识
    private String id;

    // 返回值类型
    private String resultType;

    // 参数值类型
    private String parameterType;

    // sql语句
    private String sql;

}

7、完成框架端中的解析配置文件

(1)、编写Resources类,读取客户端的xml文件

public class Resources {

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

}

(2)、编写SqlSessionFactoryBuilder类

  • 使用dom4j解析配置文件,将解析出来的内容封装到Configuration
  • 创建SqlSessionFactory对象:工厂类:生产SqlSession:会话对象
public class SqlSessionFactoryBuilder {

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

        // 2.创建SqlSessionFactory对象:工厂类:生产SqlSession:会话对象
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
        return defaultSqlSessionFactory;
    }

}

(3)、编写XMLConfigBuilder类

public class XMLConfigBuilder {

    private Configuration configuration;

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

    /**
     * 该方法就是使用dom4j对配置文件进行解析,封装成Configuration对象
     * @param in 字节输入流
     * @return   Configuration
     */
    public Configuration parseConfig(InputStream in) throws DocumentException, PropertyVetoException {
        Document document = new SAXReader().read(in);
        // 
        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);
        }

        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);

        // mapper.xml解析:拿到路径--字节输入流--dom4j进行解析
        List<Element> mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            String mapperPath = element.attributeValue("resource");
            InputStream resourceAsStream = Resources.getResourceAsStream(mapperPath);
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
            xmlMapperBuilder.parse(resourceAsStream);
        }

        return configuration;
    }
}    

(4)、编写XMLMapperBuilder类

public class XMLMapperBuilder {

    private Configuration configuration;

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

    public void parse(InputStream in) throws DocumentException {
        Document document = new SAXReader().read(in);
        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 parameterType = element.attributeValue("parameterType");
            String sqlText = element.getTextTrim();
            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(id);
            mappedStatement.setResultType(resultType);
            mappedStatement.setParameterType(parameterType);
            mappedStatement.setSql(sqlText);

            String key = namespace + "." + id;
            configuration.getMappedStatementMap().put(key, mappedStatement);
        }
    }

}

(5)、SqlSessionFactory接口以及DefaultSqlSessionFactory实现类

public interface SqlSessionFactory {

    SqlSession openSession();

}
public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

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

    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(configuration);
    }

}

(6)、SqlSession接口以及DefaultSqlSession实现类

public interface SqlSession {

    /**
     * 查询所有
     * @param statementId sql唯一id
     * @param params      sql有可能十四模糊查询,传可变参数
     * @param          泛型
     * @return            List集合
     */
    <E> List<E> selectList(String statementId, Object... params) throws Exception;

    /**
     * 根据条件查询单个
     * @param statementId sql唯一id
     * @param params      sql有可能十四模糊查询,传可变参数
     * @param          泛型
     * @return            某一对象
     */
    <T> T selectOne(String statementId, Object... params) throws Exception;

}
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("查询结果为空或者结果过多!");
        }
    }

}

(7)、Executor接口以及SimpleExecutor实现类

public interface Executor {

    <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IntrospectionException, InstantiationException, Exception;

}
public class SimpleExecutor implements Executor {

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

        // 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);
            // 暴力访问,防止访问的字段是private修饰
            declaredField.setAccessible(true);
            Object obj = declaredField.get(params[0]);
            preparedStatement.setObject(i + 1, obj);
        }

        // 5.执行sql
        ResultSet resultSet = preparedStatement.executeQuery();
        String resultType = mappedStatement.getResultType();
        Class<?> resultTypeClass = getClassType(resultType);
        Object o = resultTypeClass.newInstance();
        List<Object> objects = Lists.newArrayList();

        // 6.封装返回结果集
        while (resultSet.next()) {
            // 元数据
            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 parameterType) throws ClassNotFoundException {
        if (parameterType != null) {
            Class<?> clazz = Class.forName(parameterType);
            return clazz;
        }
        return null;
    }

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

}

四、自定义框架优化

前面我们提到的问题都解决完了,但是我们继续分析刚刚完成的自定义框架代码,还没有什么问题?

问题如下:

  • dao的实现类中存在重复代码,整个 操作的过程模板重复(创建SqlSession、调用SqlSession方法、关闭SqlSession )。
  • dao的实现类存在硬编码,调用SqlSession的方法时,参数statementId硬编码。

解决思路:

使用代理模式生成dao层接口的代理对象!!!

我的架构梦:(一)手写MyBatis框架_第1张图片
SqlSession 接口中增加getMapper方法

/**
 * 为Dao层接口生成代理实现类
 * @param mapperClass 字节码
 * @param          泛型
 * @return            某一对象
 */
<T> T getMapper(Class<?> mapperClass) throws Exception;

DefaultSqlSession 实现类

@Override
public <T> T getMapper(Class<?> mapperClass) throws Exception {
    // 使用JDK动态代理来为Dao层接口生成代理对象,并返回。
    Object proxyInstance = Proxy.newProxyInstance(mapperClass.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            /**
             * 底层都还是去执行JDBC代码
             * 根据不同情况来调用findAll或者findByCondition方法
             * 准备参数:
             * 1.statementId: sql语句的唯一标识 nnamespace.id = 接口全限定名.方法名
             */
            // 方法名
            String methodNme = method.getName();
            String className = method.getDeclaringClass().getName();

            String   statementId  = className + "." + methodNme;

            // 准备参数 2.params:args
            // 获取被调用方法的返回值类型
            Type genericReturnType = method.getGenericReturnType();
            // 判断是否进行了泛型类型参数化
            if (genericReturnType instanceof ParameterizedType) {
                List<Object> objects = selectList(statementId, args);
                return objects;
            }
            return selectOne(statementId, args);
        }
    });
    return (T) proxyInstance;
}

代理模式的过程如下图:
我的架构梦:(一)手写MyBatis框架_第2张图片

invoke()方法中的三个参数:

  • Object proxy 当前代理对象的引用
  • Method method 当前被调用方法的的引用
  • Object[] args 传递的参数

最后我们来测试一下优化后的结果:

public class PersistenceTest {

    @Test
    public void test() throws Exception {
        InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 调用
        User user = new User();
        user.setId(1);
        user.setUsername("riemann");

//      User user2 = sqlSession.selectOne("user.selectOne", user);
//      System.out.println(user2);

        UserDao userDao = sqlSession.getMapper(UserDao.class);
        
        User user2 = userDao.findByCondition(user);
        System.out.println(user2);
        
    }

}

日志打印如下:

User(id=1, username=riemann)

单个对象的OK,我们再来测一下多个的对象的。

List all = userDao.findAll();
System.out.println(all);

日志打印如下:

[User(id=3, username=edgar), User(id=3, username=edgar), User(id=3, username=edgar)]

什么鬼?多个对象的结果输出显示都是数据库中最后的那条记录。

分析问题:

断点跟踪发现,原来是SimpleExecutor这个类的query方法中有点小问题:我的架构梦:(一)手写MyBatis框架_第3张图片

解决完上面以后,测试结果如下:

[User(id=1, username=riemann), User(id=2, username=andy), User(id=3, username=edgar)]

结果正确!!!这下我们就优化好了使用代理模式来生成dao层接口的代理对象,解决了dao的实现类中存在重复代码以及dao的实现类存在硬编码。

五、代码仓库

https://github.com/riemannChow/perseverance/tree/master/handwriting-framework/mybatis

你可能感兴趣的:(我的架构梦)