从零开始手写-mybatis一MVP-版本

2次阅读

共计 8329 个字符,预计需要花费 21 分钟才能阅读完成。

什么是 MyBatis?

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

MyBatis 防止了简直所有的 JDBC 代码和手动设置参数以及获取后果集。

MyBatis 能够应用简略的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects, 一般的 Java 对象) 映射成数据库中的记录。(这是官网解释)

MyBatis 运行原理

当框架启动时,通过 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

正文完
 0