MyBatis源码(1)—— MyBatis架构梗概

目录

一、 我是如何看源码的

二、MyBastis架构

1. 预先分析一下MyBatis的骨架

2. 根据这个架构写出一个MyBatis的1.0版本Demo

3. 看1.0版本有哪些不足,该添加些什么,该怎样升级

4. 开始看真实的MyBatis源码, 理出其骨架

5. 查看MyBatis源码中的各模块代码,查看相互联系和代码实现


一、 我是如何看源码的

   

     看框架原码是提高代码设计能力的一个重要的途径。通过学习大牛的优秀代码和设计思想,我们既能够更深地理解框架的底层原理,又能够强化代码的架构能力。

    但是开源框架通常都是比较大的项目,盲目地一头扎到代码中去逐行看,可能会摸不着头脑,没有成就感,而一点点地耗尽热情,最终收效甚微。

    我的建议是—— 先抓梗概,后看模块,带着问题看代码

 

    我通常的做法是

    1. 在看源码之前,设想如果自己来设计这个框架的话会怎么写

         根据框架的功能和暴露的接口,写一个小Demo, 可以写的很简单,接口里甚至只打印一句话都行,关键是思考着对框架进行分层和功能模块划分,只实现最核心的逻辑。  

         这样能让自己提起好奇心,以便看代码时不会倦怠。同时这是自己设计能力的一个答卷,后面将对照着标准答案(真实的框架代码设计情况)进行评判,找到自己的不足和更好的优化方式。

    2. 先看框架梗概,不陷入具体细节,列出整个框架的骨架图和各模块之间的关系;之后再按功能看各个模块的代码实现

         过早地陷入代码细节,会导致只见树木不见森林;先搭好骨架再去填充细节要相对容易得多。

 

    3. 看完代码之后按真实的框架再写一个demo,前面写的demo哪个地方不好?该怎样优化? 

         不懂的地方可以猜测再debug一下。

         多问些关键问题,找关键类联系起来。

 

二、MyBastis架构

    我们以MyBastis为例来看一下框架源码。

1. 预先分析一下MyBatis的骨架

      我们先看一下MyBatis的常用例子:

SqlSession session = null;
try{
      //创建SessionFactory对象,读取配置信息
      SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(
                                      Resources.getResourceAsStream("mybatis-config.xml"));

      //创建一个session
      session = factory.openSession();
      
      PersonMapper personMapper = session.getMapper(PersonMapper.class);

      //Mybatis通过mapper代理直接执行查询方法
      Person person= personMapper.selectOne(1001);
      System.out.println(person);
}
catch(Exception e){
    e.printStackTrace();
}
finally{
    if(null != session){
        session.close();
    }
}

    从这些应用信息来看,我们能知道MyBatis:

    (1)要有一个Configuration类,负责解析xml配置文件, 将相应的配置读进来并存储到Map中,包括DB相应的配置、包名.类名.方法名 与SQL语句之间的转换配置、以及其他的配置等。

    (2)Session类, 要建立与DB的连接,根据调用的情况执行相应的SQL语句并返回相应的结果;如果Session类的功能太多会导致耦合,不妨将与DB打交道的功能单独封装到一个Executor类中(至于叫什么无所谓)。

    (3)Session类需要提供一个getMapper() 方法, 根据类名生成相应的代理来调用想要的方法,肯定要用到动态代理,再封装一个动态代理类MapperProxy。

      至于SessionFactory和SessionFactoryBuilder不属于核心功能,后续再去添加。

    这些是最基本的骨架,我们根据这些基本的功能来搭建MyBatis的1.0版本的Demo。

2. 根据这个架构写出一个MyBatis的1.0版本Demo

      Session类:

package mybatisdemo;

import java.lang.reflect.Proxy;

public class MSession {
    private String xmlPath;

    public MSession(String xmlPath) {
        this.xmlPath = xmlPath;
    }

    public <T> T getMapper(Class clazz){
        return (T) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
                new Class[]{clazz},
                new MProxyHandler(this.xmlPath));
    }
}

ProxyHandler类:

package mybatisdemo;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MProxyHandler implements InvocationHandler {
    private MConfiguration config;
    private MExecutor executor;

    public MProxyHandler(String xmlPath) {
        config = new MConfiguration(xmlPath);
        executor = new MExecutor(config);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (this.config.getNameSpace().equals(method.getDeclaringClass().getName())){
            //这里应该要根据所调用的方法选择不同的调用分支,先写个最简单版本,之后再丰富
            return executor.selectOne(method.getName(), args);
        }
        //不属于配置的方法,则去调用Object原有的方法
        return method.invoke(this, args);
    }
}

Configuration类

package mybatisdemo;

import java.util.HashMap;

public class MConfiguration{
    //存储数据库配置信息
    private HashMap<String, String> DBMap;
    //存储类名.方法名 与SQL之间的映射
    private HashMap<String, String> SQLMap;
    //包名.类名,本应读xml配置,这里模拟直接写死
    private String nameSpace;

    public MConfiguration(String xml) {
        //直接在这里赋值模拟读xml文件
        DBMap = new HashMap<>();
        DBMap.put("user", "root");
        DBMap.put("password", "passwd");
        DBMap.put("driver", "com.mysql.jdbc.Driver");
        DBMap.put("url", "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8");

        //SQLMap的内部也应该是读xml进来的,这里直接赋值模拟
        SQLMap = new HashMap<>();
        SQLMap.put("selectOne", "select * from person where id = ?");

        this.nameSpace = "mybatisdemo.PersonMapper";
    }

    public HashMap<String, String> getDBMap() {
        return DBMap;
    }

    public HashMap<String, String> getSQLMap() {
        return SQLMap;
    }

    public String getNameSpace() {
        return nameSpace;
    }
}

Executor类:

package mybatisdemo;

import java.sql.*;

public class MExecutor {
    private static MConfiguration config;
    private Connection connection = null;
    private PreparedStatement statement = null;

    public MExecutor(final MConfiguration config) {
        this.config = config;
    }

    public void init(){
        //获取连接
        try {
            Class.forName(config.getDBMap().get("driver"));
            if (null == this.connection){
                String url = config.getDBMap().get("url");
                String user = config.getDBMap().get("user");
                String passwd = config.getDBMap().get("password");
                connection = DriverManager.getConnection(url, user, passwd);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public <T> T selectOne(String methodName, Object[] args){
        String sql = config.getSQLMap().get(methodName);
        if (null != sql && 0 != sql.length()){
            return query(sql, args);
        }
        return null;
    }

    public <T> T query(String sql, Object[] args){
        //这里也是用的硬编码,后续需要改进
        PersonInfo person = new PersonInfo();
        try {
            init();
            statement = connection.prepareStatement(sql);
            statement.setLong(1, (Long) args[0]);
            ResultSet rs = statement.executeQuery();
            //这里结果集处理用的硬编码,需改进
            while (rs.next()){
                person.setId(rs.getLong(1));
                person.setName(rs.getString(2));
                person.setAge(rs.getInt(3));
                person.setGender( rs.getByte("gender"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            try {
                connection.close();
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return (T) person;
    }
}

PersonInfo类: 一个信息封装类

package mybatisdemo;

public class PersonInfo {
    private long id;
    private String name;
    private int age;
    private byte gender;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public byte getGender() {
        return gender;
    }

    public void setGender(byte gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "PersonInfo{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", gender=" + gender +
                '}';
    }
}

PersonMapper interface: 一个用来测试的接口,不做implements, 

package mybatisdemo;

import java.util.List;

public interface PersonMapper {
    PersonInfo selectOne(Long id);
    List<PersonInfo> selectAll();
    int insert(PersonInfo info);
    int delete(Long id);
    int update(PersonInfo info);
}

Main函数, 直接session调用PersonMapper.class 生成动态代理,不用写实现直接根据文件就能查出结果

package mybatisdemo;

public class TestMain {
    public static void main(String[] args) {
        MSession session = new MSession("mybatis.xml");
        PersonMapper mapper = session.getMapper(PersonMapper.class);
        PersonInfo info = mapper.selectOne(1L);
        System.out.println(info);
    }
}

运行结果:

 

3. 看1.0版本有哪些不足,该添加些什么,该怎样升级

     1.0版本能够运行了,已经能够像MyBatis那样,不用写实现、直接通过配置文件就能够在调用方法时输出结果。

     但这个简陋的版本,也存在着不少的问题,还需要继续优化。我们看看存在哪些问题:

    (1)读取xml部分还没有写

    (2)ProxyHandler 的invoke() 里只写了一个方法,这里需要根据不同的调用情况选择不同的调用分支

    (3)结果集处理写的是硬编码,这里需要由xml配置给出resultSet或 resultMap,并且加上ORM 进行自动处理

    (4)Session加上工厂类SessionFactory、 SessionFactoryBuilder

    (5)加上一级缓存、二级缓存,提高查询性能, Cache类及Cache过期策略, LRU/FIFO, 以及增/删/改时Cache失效

    (6)连接管理、事务管理

    (7)日志,插件,注解,异常处理,以及其他的扩展功能

 

 

 

4. 开始看真实的MyBatis源码, 理出其骨架

    别急着深入看各个类,先粗看整理出枝节,不要陷入细节

    这是一个MyBatis的层级架构和功能模块图

 

5. 查看MyBatis源码中的各模块代码,查看相互联系和代码实现

    

    根据功能模块,查看各代码。 看细节代码时,时刻记得整体的架构,自己写的与它们的差距在哪里,怎样写的更好。并注意各模块之间的联系。

     后面博文,将分别解读各核心模块, 共包括 Configuration、Plugin、  MapperProxy、 SqlSession、 Executor 几个部分。

    谢谢!

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章