Mybatis源码解析(一) -- 动态代理,拦截器及架构分析

567 阅读3分钟

使用jdbc连接数据库

在学习Mybatis之前我们需要先回顾以下如何使用jdbc连接数据库,下面是一个简单的例子。 例如我们这里有一张user表,表里面有两个字段。

字段名类型主键
idinttrue
namevarcharfalse
phonevarcharfalse

那么要当我们在数据库中查询出一条User记录时,我们要把它封装到一个User对象中,方便使用。

//省略get set方法
public class User {
    private int id;
    private String name;
    private String phone;
}

现在我们使用jdbc来对User表进行查询。

public class TestJdbc {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection connection = DriverManager.getConnection("");
        Statement statement  = connection.createStatement();
        ResultSet resultSet = statement.executeQuery("select * from user");
        List<User> list = new LinkedList<>();
        while (resultSet.next()){
            int id = resultSet.getInt("id");
            String name = resultSet.getString("name");
            String phone = resultSet.getString("phone");
            User user = new User(id,name,phone);
            list.add(user);
        }
        resultSet.close();
        statement.close();
        connection.close();
    }
}

这里大概有以下几个步骤。

  1. DriverManager注册驱动。
  2. 创建Connection 连接。
  3. 创建Statement(该对象用来执行sql语句并返回结果)。
  4. 执行sql并发到返回值。
  5. 对结果进行封装
  6. 关闭连接(按顺序)

可以看到,当我们使用jdbc来操作数据库,过程相当繁琐。当我们使用mybatis进行数据库操作

    String resource = "mybatis.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<User> = mapper.findList();
    sqlSession.close();

这里也有几个步骤

  1. 配置Mybatis
  2. 启动SqlSessionFactory sqlSessionFactory。
  3. 开启连接,拿到sqlSession
  4. 通过动态代理拿到mapper
  5. 关闭连接

可以看出Mybatis在使用起来更方便,对于语句分离和sql的执行,返回值的封装,都很方便。

这里面很重要的一步就是动态代理,通过mapper接口来执行具体的语句。我们可以自己写一个动态代理来模拟这个过程。

动态代理Demo

首先我们创建一个注解创建@Select,当然你也可以选择使用读取xml的方法,这里使用注解比较容易说明。

这个select用来传入一句sql

@Retention(value = RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
    String value();
}

接着我们创建一个Mapper

public interface UserMapper {
    @Select("select * from user")
    List<User> findList();
}

那么我们现在就需要使用动态代理让UserMapper来执行具体的查询语句了。

public class TestJdbc {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        UserMapper userMapper = (UserMapper) Proxy.newProxyInstance(TestJdbc.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return null;
            }
        });
    }
}

这里是jdk自带的一个使用动态代理的方法, Proxy.newProxyInstance,第一个参数传入类加载器,第二个是个class数组,第三个是个 InvocationHandler接口。这个接口里面有个invode方法,也就是说当我执行findList的时候,实际上就会调用invoke的方法。

那么接下来的事情就很清楚了,我们只需要补充invoke的逻辑,就能执行执行数据库操作了。可以分为以下几步。

  1. 通过注解(或者XML)拿到sql语句
//获取注解上的sql
 Select select = method.getAnnotation(Select.class);
 String sql = select.value();
  1. 执行sql语句,并拿到返回值。
  2. 将返回值封装程我们需要的List<User>然后返回。 当然实际的场景不可能这么简单,mybatis需要解析更加复杂的sql,并对#{},和${}分别进行解析。并对sql进行分类,例如是select的还是update的,如果是select类型还要从缓存中查询。等等

拦截器Demo

我们首先需要一个拦截器接口,这个拦截器传入一个sql语句

    interface Intercepter {
        String plugin(String sql);
    }

这个实现类将sql语句转成大写

  private static class ToUpperCaseIntercepter implements Intercepter{
      @Override
      public String plugin(String sql) {
          return sql.toUpperCase();
      }
  }

这个实现类会将#{id},转化为具体的值1.

  private static class ChangeValueIntercepter implements Intercepter{
      @Override
      public String plugin(String target) {
          return target.replace("#{id}","1");
      }
  }

之后就是我们的拦截器链了

    private static class IntercepterChain {
		//拦截器的list
        List<Intercepter> list = new LinkedList<>();
        //添加方法
        public void addIntercepter(Intercepter intercepter){
            list.add(intercepter);
        }
        //执行方法
        public String pluginAll(String sql){
            for (Intercepter intercepter : list) {
                sql = intercepter.plugin(sql);
            }
            return sql;
        }
    }

首先我们声明两个拦截器并添加到拦截器链中,然后执行pluginAll方法,这样我们的sql语句就会根据拦截器中的规则改变了。这两个拦截器的作用事将语句中#{id}替换成1,然后将语句转成大写。

在实际场景中可以对语句添加limit offset ,这样就能自动进行分页。

public class IntercepterDemo{
    public static void main(String[] args) {
        IntercepterChain intercepterChain = new IntercepterChain();
        intercepterChain.addIntercepter( new ChangeValueIntercepter());
        intercepterChain.addIntercepter( new ToUpperCaseIntercepter());
        String sql = intercepterChain.pluginAll("select * from user where id = #{id}");
        System.out.println(sql);
    }
}

Mybatis也是类似,只不过添加拦截器是根据xml中的配置文件进行注入的。且pluginAll的参数有所变化。在后文中回详细说明。

Mybatis的架构分析

我们根据上文sql的执行流程来逐步分析.

在启动mybatis之前,我们首先需要解析和保存它的xml配置文件,所以我们需要一个Configuration对象。Mybatis大部分的类中,都持有这个Configuration对象来方便获取配置文件的信息。

当解析完配置文件后,就能执行sql语句了,这时候就需要一个Executor执行器,用来调度增删改查这些操作,做一些前置操作如获取mapper的sql语句,查询缓存等等。当然Executor并不需要手动进行创建,在我们openSession的时候就会自动进行创建。

执行完前置操作之后,就需要到数据库中进行具体的查询了。与jdbc类似,这里也有一个名为StatementHandler的对象来具体执行sql。 当然在这之前我们我要对传入的参数进行处理,时#{}类型的使用占位符,${}类型的直接进行替换。所以我们要用到处理参数的ParameterHandler。 最后我们要将返回的结果封装成具体的对象,那就需要ResultSetHandler来生成并封装对象。

ParameterHandler和ResultSetHandler是StatementHandler中的成员,会在创建StatementHandler的时候同步进行创建。

在后文中,会根据这个流程来具体分析sql执行的过程。对象和字段同样也是使用User.