• 从零开始手写 mybatis(一)MVP 版本


    什么是 MyBatis ?

    MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。

    MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

    MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。(这是官网解释)

    MyBatis 运行原理

    mybatis.png

    当框架启动时,通过configuration解析config.xml配置文件和mapper.xml映射文件,映射文件可以使用xml方式或者注解方式,然后由configuration获得sqlsessionfactory对象,再由sqlsessionfactory获得sqlsession数据库访问会话对象,通过会话对象获得对应DAO层的mapper对象,通过调用mapper对象相应方法,框架就会自动执行SQL语句从而获得结果。

    手写 mybatis

    其实整体流程就是这么简单,我们来一起实现一个简单版本的 mybatis。

    创作目的

    (1)深入学习 mybatis 的原理

    一千个读者就有一千个哈姆雷特,一千个作者就有一千个莎士比亚。——老马

    (2)实现属于自己的 mybatis 工具。

    数据库的种类实际上有几百种,比如工作中就用到过 GreenPlum 这种相对小众的数据库,这时候 mybatis 可能就不能使用了。

    感觉大可不必,符合 SQL 标准都应该统一支持下,这样更加方便实用。

    实现方式

    本系列目前共计 17 个迭代版本,基本完成了 mybatis 的核心特性。

    耗时大概十天左右,相对实现的方式比较简单。

    采用 mvp 的开发策略,逐渐添加新的特性。

    本系列将对核心代码进行讲解,完整代码已经全部开源

    https://github.com/houbb/mybatis

    快速体验

    mysql 安装

    不是本系列重点,请自行找资料。

    版本:使用的是 v5.7 版本,v8.0 之后依赖的驱动包会有所不同。

    sql 执行

    -- auto-generated definition
    create table user
    (
      id   int auto_increment
        primary key,
      name varchar(100) not null,
      password varchar(100) not null
    );
    
    
    insert into user (name, password) value ('ryo', '123456');
    

    maven 引入

    <dependency>
        <groupId>com.github.houbb</groupId>
        <artifactId>mybatis</artifactId>
        <version>0.0.1</version>
    </dependency>
    

    配置文件

    • mybatis-config-5-7.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    
        <dataSource>
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/test"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </dataSource>
    
        <mappers>
            <mapper resource="mapper/UserMapper.xml"/>
        </mappers>
    
    </configuration>
    

    测试代码

    Config config = new XmlConfig("mybatis-config-5-7.xml");
    
    SqlSession sqlSession = new DefaultSessionFactory(config).openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    User user = userMapper.selectById(1L);
    System.out.println(user);
    

    输出结果:

    User{id=1, name='ryo', password='123456'}
    

    是不是有种 mybatis 初恋般的感觉呢?

    到这里都是引子,下面我们来讲述下一些核心实现。

    代码实现

    maven 依赖

    这里我们需要访问 mysql,也需要解析 xml。

    需要引入如下的依赖:

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.29</version>
    </dependency>
    <dependency>
        <groupId>dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>1.6.1</version>
    </dependency>
    

    接口定义

    上述的测试代码中,我们演示用到的几个核心接口如下:

    • Config.java

    配置接口

    /**
     * 配置信息
     * @author binbin.hou
     * @since 0.0.1
     */
    public interface Config {
    
        /**
         * 获取数据源信息
         * @return 数据源配置
         * @since 0.0.1
         */
        DataSource getDataSource();
    
        /**
         * 获取映射类信息
         * @param clazz 类信息
         * @return 结果
         * @since 0.0.1
         */
        MapperClass getMapperData(final Class clazz);
    
        /**
         * 获取映射类信息
         * @param clazz 类信息
         * @param methodName 方法名称
         * @return 结果
         * @since 0.0.1
         */
        MapperMethod getMapperMethod(final Class clazz,
                                     final String methodName);
    
        /**
         * 数据库连接信息
         * @return 连接信息
         * @since 0.0.1
         */
        Connection getConnection();
    }
    
    • SqlSession.java
    public interface SqlSession {
    
        /**
         * 查询单个
         * @param mapperMethod 方法
         * @param args 参数
         * @param <T> 泛型
         * @return 结果
         * @since 0.0.1
         */
        <T> T selectOne(final MapperMethod mapperMethod, Object[] args);
    
        /**
         * Retrieves a mapper.
         * @param <T> the mapper type
         * @param type Mapper interface class
         * @return a mapper bound to this SqlSession
         * @since 0.0.1
         */
        <T> T getMapper(Class<T> type);
    
        /**
         * 获取配置信息
         * @return 配置
         * @since 0.0.1
         */
        Config getConfig();
    
    }
    
    • UserMapper.java

    UserMapper 就是我们经常定义的 mapper

    public interface UserMapper {
    
        User selectById(final long id);
    
    }
    

    下面我们来看看对应的几个比较重要的实现。

    xml 的配置初始化

    我们的很多配置放在 config.xml 文件中,肯定是通过解析 xml 实现的。

    基础属性

    public class XmlConfig extends ConfigAdaptor {
    
        /**
         * 文件配置路径
         *
         * @since 0.0.1
         */
        private final String configPath;
    
        /**
         * 配置文件信息
         *
         * @since 0.0.1
         */
        private Element root;
    
        /**
         * 数据源信息
         *
         * @since 0.0.1
         */
        private DataSource dataSource;
    
        /**
         * mapper 注册类
         *
         * @since 0.0.1
         */
        private final MapperRegister mapperRegister = new MapperRegister();
    
        public XmlConfig(String configPath) {
            this.configPath = configPath;
    
            // 配置初始化
            initProperties();
    
            // 初始化数据连接信息
            initDataSource();
    
            // mapper 信息
            initMapper();
        }
    
        @Override
        public DataSource getDataSource() {
            return this.dataSource;
        }
    
        @Override
        public Connection getConnection() {
            try {
                Class.forName(dataSource.driver());
                return DriverManager.getConnection(dataSource.url(), dataSource.username(), dataSource.password());
            } catch (ClassNotFoundException | SQLException e) {
                throw new MybatisException(e);
            }
        }
    
        @Override
        public MapperMethod getMapperMethod(Class clazz, String methodName) {
            return this.mapperRegister.getMapperMethod(clazz, methodName);
        }
    }
    

    配置初始化

    这里就是解析 xml 文件的 root 节点,便于后续使用:

    root 节点的初始化如下:

    /**
     * 获取根节点
     * @param path 配置路径
     * @return 元素
     * @since 0.0.1
     */
    public static Element getRoot(final String path) {
        try {
            // 初始化数据库连接信息
            InputStream inputStream = StreamUtil.getInputStream(path);
            SAXReader reader = new SAXReader();
            Document document = reader.read(inputStream);
            return document.getRootElement();
        } catch (DocumentException e) {
            throw new MybatisException(e);
        }
    }
    

    初始化数据连接信息

    这就是解析 xml 中对于 dataSource 的配置信息:

    /**
     * 初始化数据源
     *
     * @since 0.0.1
     */
    private void initDataSource() {
        // 根据配置初始化连接信息
        this.dataSource = new DataSource();
        Element dsElem = root.element("dataSource");
        Map<String, String> map = new HashMap<>(4);
        for (Object property : dsElem.elements("property")) {
            Element element = (Element) property;
            String name = element.attributeValue("name");
            String value = element.attributeValue("value");
            map.put("jdbc." + name, value);
        }
        dataSource.username(map.get(DataSourceConst.USERNAME))
                .password(map.get(DataSourceConst.PASSWORD))
                .driver(map.get(DataSourceConst.DRIVER))
                .url(map.get(DataSourceConst.URL));
    }
    

    初始化 mapper

    解析 xml 中的 mapper 配置。

    /**
     * 初始化 mapper 信息
     *
     * @since 0.0.1
     */
    private void initMapper() {
        Element mappers = root.element("mappers");
        // 遍历所有需要初始化的 mapper 文件路径
        for (Object item : mappers.elements("mapper")) {
            Element mapper = (Element) item;
            String path = mapper.attributeValue("resource");
            mapperRegister.addMapper(path);
        }
    }
    

    mapperRegister 就是对方法的元数据进行一些构建,比如出参,入参的类型,等等,便于后期使用。

    比如我们的 UserMapper.xml 方法内容如下:

    <select id = "selectById" paramType="java.lang.Long" resultType = "com.github.houbb.mybatis.domain.User">
            select * from user where id = ?
    </select>
    

    sql 就是:select * from user where id = ?

    方法标识:selectById

    入参:Long

    出参:User

    创建 session

    如何创建

    SqlSession sqlSession = new DefaultSessionFactory(config).openSession();
    

    这句话实际执行的是:

    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(config, new SimpleExecutor());
    }
    

    获取 mapper 实现

    UserMapper userMapper = sqlSession.getMapper(UserMapper.class)
    

    这里获取 mapper,实际获取的是什么呢?

    实际上获取到的是一个代理。

    mybatis 将我们的接口,和实际 xml 中的 sql 二者通过动态代理结合,让我们调用 xml 中的 sql 和使用接口方法一样自然。

    获取代理

    getMapper 实际上是一个动态代理。

    @Override
    @SuppressWarnings("all")
    public <T> T getMapper(Class<T> clazz) {
        MapperProxy proxy = new MapperProxy(clazz, this);
        return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, proxy);
    }
    

    动态代理的实现

    MapperProxy 的实现如下:

    public class MapperProxy implements InvocationHandler {
    
        /**
         * 类信息
         *
         * @since 0.0.1
         */
        private final Class clazz;
    
        /**
         * sql session
         *
         * @since 0.0.1
         */
        private final SqlSession sqlSession;
    
        public MapperProxy(Class clazz, SqlSession sqlSession) {
            this.clazz = clazz;
            this.sqlSession = sqlSession;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            MapperMethod mapperMethod = this.sqlSession.getConfig()
                    .getMapperMethod(clazz, method.getName());
            if (mapperMethod != null) {
                return this.sqlSession.selectOne(mapperMethod, args);
            }
            return method.invoke(proxy, args);
        }
    
    }
    

    代理了什么?

    当我们执行 userMapper.selectById(1L) 时,实际执行的是什么?

    实际执行的是 sqlSession.selectOne(mapperMethod, args)

    代理实现

    selectOne 是比较核心的内容了。

    整体实现

    整体如下

    public <T> T query(final Config config,
                       MapperMethod method, Object[] args) {
        try(Connection connection = config.getConnection();
            PreparedStatement preparedStatement = connection.prepareStatement(method.getSql());) {
            // 2. 处理参数
            parameterHandle(preparedStatement, args);
            // 3. 执行方法
            preparedStatement.execute();
            // 4. 处理结果
            final Class resultType = method.getResultType();
            ResultSet resultSet = preparedStatement.getResultSet();
            ResultHandler resultHandler = new ResultHandler(resultType);
            Object result = resultHandler.buildResult(resultSet);
            return (T) result;
        } catch (SQLException ex) {
            throw new MybatisException(ex);
        }
    }
    

    我们获取到 xml 中的 sql,然后构建 jdbc 中大家比较熟悉的 PreparedStatement。

    然后对出参和入参进行处理,最后返回结果。

    入参设置

    public void setParams(final Object[] objects) {
        try {
            for(int i = 0; i < objects.length; i++) {
                Object value = objects[i];
                // 目标类型,这个后期可以根据 jdbcType 获取
                // jdbc 下标从1开始
                statement.setObject(i+1, value);
            }
        } catch (SQLException throwables) {
            throw new MybatisException(throwables);
        }
    }
    

    针对我们非常简单的例子:

    select * from user where id = ?
    

    那就是直接把入参中的 1L 设置到占位符 ? 即可。

    出参处理

    这里主要用到反射,将查询结果和 javaBean 做一一映射。

    /**
     * 构建结果
     * @param resultSet 结果集合
     * @return 结果
     * @since 0.0.1
     */
    public Object buildResult(final ResultSet resultSet) {
        try {
            // 基本类型,非 java 对象,直接返回即可。
            // 可以进行抽象
            Object instance = resultType.newInstance();
            // 结果大小的判断
            // 为空直接返回,大于1则报错
            if(resultSet.next()) {
                List<Field> fieldList = ClassUtil.getAllFieldList(resultType);
                for(Field field : fieldList) {
                    Object value = getResult(field, resultSet);
                    ReflectFieldUtil.setValue(field, instance, value);
                }
                // 返回设置值后的结果
                return instance;
            }
            return null;
        } catch (InstantiationException | IllegalAccessException | SQLException e) {
            throw new MybatisException(e);
        }
    }
    

    到这里,一个简易版的 myabtis 就可以跑起来了。

    当然这里还有很多的不足之处,我们后续都会一一优化。

    完整代码地址

    为了便于学习,完整版本代码以开源:

    https://github.com/houbb/mybatis

    image

  • 相关阅读:
    [CentOS_7.4]Linux编译安装mono环境
    [CentOS_7.4]Linux安装与网络配置
    div框选中状态,倒三角样式
    photoswipe图片滑动插件使用
    微信应用号(小程序)开发教程二
    微信应用号(小程序)开发教程一
    开发者必去的10大国内网站推荐
    愤怒的小鸟 高清完整版下载
    魔兽 高清完整版下载
    分歧者3 高清完整版下载
  • 原文地址:https://www.cnblogs.com/houbbBlogs/p/13296442.html
Copyright © 2020-2023  润新知