Mybatis(九) - 手写Mybatis简单版本 - V1

91 阅读3分钟

手写Mybatis简单版本

本人已参与「新人创作礼」活动,一起开启掘金创作之路。

本篇文章是通过看视频学习总结的内容, 如有错误的地方请谅解,并联系博主及时修改,谢谢您的阅读.

前言: 根据之前八篇博客的讲述,现在搞明白了 MyBatis 一系列的工作原理,那么就可以参照 MyBatis 自己写一个 半 ORM 框架,接下来先做分析,看看需要哪些对象、配置文件等等。

源码地址:github 简单版本源码

开始前的准备

  • 项目依赖
  • statementId 和 sql 的映射关系(mybatis 中是 mapper.xml),这里使用properties代替
  • 实体类
  • 全局配置对象 Configuration
  • 执行器 Executor
  • sql 会话 SqlSession 对象
  • mapper接口的代理对象 MapperProxy

begin

  • 1 项目依赖
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.6</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.20</version>
</dependency>
  • 2 sql.properties
#  statementId = sql
com.example.ibatis.mapper.BlogMapper.selectById=select * from blog where bid = %d
  • 3 Blog.java 实体类
@Setter
@Getter
@ToString
public class Blog implements Serializable{

    /** 文章ID **/
    private Integer id;
    
    /** 文章标题 **/
    private String name;
    
    /** 文章作者ID **/
    private Integer authorId;
}
  • 4 Configuration.java
package com.example.ibatis.v1;

import java.lang.reflect.Proxy;
import java.util.ResourceBundle;

/**
 * <p>
 * 全局配置文件,目前主要是通过 properties 文件加载sql语句的
 * </p>
 *
 * @author zyred
 * @createTime 2020/9/16 16:13
 **/
public class Configuration {

    public static final ResourceBundle sqlMapping;

    static {
        // 读取sql.properties文件,类被初始化的时候,会读取文件中的内容
        sqlMapping = ResourceBundle.getBundle("sql");
    }

    /**
     * 通过class动态代理mapper接口
     *
     * @param clazz      mapper接口
     * @param sqlSession sqlSession
     * @param <T>        被代理的对象
     * @return
     */
    public <T> T getMapper(Class<?> clazz, SqlSession sqlSession) {
        return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(),
                new Class[]{clazz},
                new MapperProxy(sqlSession));
    }
}
  • 5 Executor 执行器
package com.example.ibatis.v1;

import com.example.ibatis.doman.Blog;

import java.sql.*;

/**
 * <p>
 *      sql执行器
 * </p>
 *
 * @author zyred
 * @createTime 2020/9/16 16:18
 **/
public class Executor {

    public <T> T query(String sql, Object paramater) {
        Connection conn = null;
        Statement stmt = null;
        Blog blog = new Blog();

        try {
            // 注册 JDBC 驱动
            Class.forName("com.mysql.cj.jdbc.Driver");

            // 打开连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT%2B8", "root", "root");

            // 执行查询
            stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(String.format(sql, paramater));

            // 获取结果集
            while (rs.next()) {
                Integer bid = rs.getInt("id");
                String name = rs.getString("name");
                Integer authorId = rs.getInt("author_id");
                blog.setAuthorId(authorId);
                blog.setBid(bid);
                blog.setName(name);
            }
            System.out.println(blog);

            rs.close();
            stmt.close();
            conn.close();
        } catch (SQLException se) {
            se.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (stmt != null) {
                    stmt.close();
                }
            } catch (SQLException se2) {
            }
            try {
                if (conn != null) {
                    conn.close();
                }
            } catch (SQLException se) {
                se.printStackTrace();
            }
        }
        return (T)blog;
    }
}
  • 6 SqlSession
package com.example.ibatis.v1;

/**
 * <p>
 *      sql会话
 * </p>
 *
 * @author zyred
 * @createTime 2020/9/16 16:17
 **/
public class SqlSession {
    Configuration configuration;
    Executor executor;

    public SqlSession(Configuration configuration, Executor executor){
        this.configuration = configuration;
        this.executor = executor;
    }

    /**
     * 单条查询
     * @param statementId
     * @param param
     * @param <T>
     * @return
     */
    public <T> T selectOne (String statementId, Object param){
        // 拿到sql语句
        String sql = Configuration.sqlMapping.getString(statementId);
        return this.executor.query(sql, param);
    }

    /**
     * 通过动态代理去执行方法,在invoke方法中执行查询
     * @param clazz     mapper接口
     * @param <T>       动态代理的类
     * @return
     */
    public <T> T getMapper(Class<?> clazz){
        return this.configuration.getMapper(clazz, this);
    }
}
  • 7MapperProxy
/**
 * <p>
 *     代理mapper接口的对象
 * </p>
 *
 * @author zyred
 * @createTime 2020/9/16 16:25
 **/
public class MapperProxy implements InvocationHandler {
    private final static String DOT = ".";
    SqlSession sqlSession;

    public MapperProxy(SqlSession sqlSession) {
        this.sqlSession = sqlSession;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String clazzName = method.getDeclaringClass().getName();
        String methodName = method.getName();

        return this.sqlSession.selectOne(clazzName.concat(DOT).concat(methodName), args[0]);
    }
}
  • 8 main 方法测试
public class MyBatisTest {

    public static void main(String[] args) {
        SqlSession sqlSession = new SqlSession(new Configuration(), new Executor());
        BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
        // Blog(id=1, name=mybatis深入理解, authorId=1)
        mapper.selectById(1);
    }
}

流程分析

  • main 方法入手
  1. SqlSession sqlSession = new SqlSession(new Configuration(), new Executor()); 这句代码:先 new 出了 Configuration 对象,该对象初始化的时候,完成了从 sql.properties 加载,并且放入到 ResourceBundle 内部,这个容器模拟了 mybatisMappedStatement 对象,然后创建 Executor 对象(Executor 是跟随 SqlSession 的,所以在这里需要这个对象去执行查询)

  2. BlogMapper mapper = sqlSession.getMapper(BlogMapper.class); 这里不就是通过 JDK 动态代理完成接口的实现类代理吗?

  3. mapper.selectById(1); 这句代码其实就是执行了动态代理后的类方法,然后调用 MapperProxy 类中 invocke(),而 invoke () 调用了 session#selectOne()session 这里又调用了执行器的 query(),最后执行 executor#query() 查询数据库

难点分析

  • JDK 动态代理必须要实现 InvocationHandler 接口,生成出来的代理类的方法一旦被调用,那么 InvocationHandler 接口的 invoke() 就会被执行,所以后续的逻辑可以放入到 invoke() 来处理

执行流程

在这里插入图片描述