Mybatis(一)

这是我在这个专题下的第一篇blog,这个专题主要讲解对Mybatis的源码和实际运用相关知识,相信有一部分人都使用过JDBC来连接数据库,大致的过程可以是注册驱动、获取连接、编写一个sql、然后执行sql并获取返回值。如果每次都需要按上述的步骤来编写代码访问数据库,是十分麻烦的。所以我们就使用Mybatis这个半自动的ORM框架,他有三大要素:SQL、映射规则和POJO。

首先我们对Mybatis的日志模块进行分析:

1、Mybatis并没有提供日志的实现类,需要接入第三方的日志组件,但是每个第三方日志组件存在差异,它们有着各自的Log级别,且各不相同,而Mybatis统一提供了trace、debug、warn、error四个级别。那Mybatis应该如何去加载它们?

2、Mybatis当有多种日志实现的时候,如何先择的呢?

3、日志功能是怎么为我们提供服务的,何时加入到我们的代码中?

上面的三个问题,是我们分析日志模块需要弄懂的主要问题,那他们分别是如何解决的呢?我们接下来一个一个的去分析。

首先是Mybatis对这么多种不同的日志实现,该如何去实现对他们的调用呢?

我们可以发现,每种第三方日志实现并不是继承与同一个接口的,而我们想要用Mybatis提供的Log接口去统一调用它们,Mybatis是如何实现的呢?其实用到了一种设计模式–适配器模式。这种设计模式是为本身两种不兼容的接口添加一个适配器类,这个类起到了桥梁的作用。打个比方,现在有的手机并没有耳机孔,那我们有线耳机圆形的孔,该怎么与手机连接呢?我们会通过一个转接头,一头插入手机,然后再将耳机连接到转接头上,这样就完成了与手机的连接,那么这个手机和耳机就相当于我们适配器模式中原本互不兼容的接口,而转接头就是这个适配器类。接下来我们看看源码中是怎样的。

mybatis springboot 日志配置 mybatis的日志_mysql


mybatis springboot 日志配置 mybatis的日志_spring boot_02

这里以log4j2的适配器为例(随机选了个,其他适配器也一样的),可以看的出来它继承了Mybatis统一的接口Log,里面调用的是实际的日志实现类来完成日志任务。

mybatis springboot 日志配置 mybatis的日志_mysql_03

从这里我们可以看到适配器模式的几个好处:日志实现组件,适配器还有统一的接口(Log)他们的定义都十分清晰明确符合单一职责原则。我们使用时只需要面向Log接口编程,不需要关心底层的日志实现,符合依赖倒置原则。如果我们需要添加新的第三方日志框架,我们不需要修改原来的代码,只需要扩展新的模块,符合开闭原则。

接下来我们看看有这么多种日志实现,Mybatis是如何选择日志组件的?

首先我们要从LogFactory这个类出发,因为我们获取日志实现组件是从这个类的getLog()方法获取的,下面看看这个类先:

mybatis springboot 日志配置 mybatis的日志_java_04

上面是LogFactory类,在类中有个属性是logConstructor构造器,这个构造器是日志实现类的适配器构造器,当我们调用getLog()方法的时候,会通过这个构造器创建一个日志实现类的适配器给我们。那么现在的重点就是这个构造器是怎么来的?下面我们来看看整个加载的过程:首先是从静态代码块开始。

mybatis springboot 日志配置 mybatis的日志_mybatis_05


mybatis springboot 日志配置 mybatis的日志_spring boot_06

这个类在创建的时候logConstructor肯定为null,所以肯定会执行useSlf4Logging方法,而这个方法从前面的整个类代码中可以看到调用的是setImplementation(Slf4jImpl.class)方法,那么我们来看看这个方法里做了什么。

mybatis springboot 日志配置 mybatis的日志_mybatis_07

注意:这个时候可能有人发现了,如果在setImplementation方法中出现异常会不会导致后面的日志实现类加载都不执行呢?可以看到,在上面的runnable.run()中用try/catch块捕获了但是catch中直接不做任何处理,也就是说如果一个日志加载中途出现异常,则直接跳入到下一个日志的加载,不会影响到后续日志的加载。

那我们继续往下走,如果这个slf4j加载没有成功,那么就会进入到下一个,过程和前面的过程一样,就不重复讲解了。那么如果这个slf4j加载成功了,后面的加载过程又是如何呢?其实在前两个图中已经说明了,如果前面有构造器加载成功了,那么直接跳过本次加载过程,并不会继续尝试加载,这也保证了当有多个日志实现时使用优先级最高的日志实现,后面的并不会被加载成功。以上就是日志的按优先级来加载的的实现。

接下来就是日志怎么为我们提供服务,何时被加入到代码中的?

这个问题解决办法设计到了动态代理的知识点。因为这个知识点比较复杂,在这里我就简单的描述下,之后也会专门写一篇关于动态代理的blog。有兴趣的读者可以进行学习下,有什么疑问随时欢迎提出大家一起讨论解决。

首先要说说静态,代理模式就是你不直接调用目标对象,而是调用目标对象的代理对象,由代理对象来调用目标对象,并且可以再不改变目标对象的情况下进行一些其他的操作。这也要求代理类和目标类实现同一个接口,并且代理类包含了目标类对象。

动态代理其实就是在运行期间,动态的生成代理类,并不是我们在编码阶段写好这个代理类,那他是怎么实现的呢?
借助Proxy类和InvocationHandler接口就可以实现动态代理。
Proxy就是我们代理类的基类,代理类除了要实现这个基类外还需要目标类实现的所有接口,然后我们通过调用代理类的方法,实际上就会调用InvocationHandler的Invoke方法。而我们实现InvocationHandler时需要实现它的唯一抽象方法invoke(),这个方法里面会通过反射调用目标对象的相对应方法,并且加入其他的逻辑。

了解了动态代理后(这里只是简单说明,可以去看看上面两个类的源码),我们需要继续解答上面的疑问,日志是怎么为我们提供服务的,何时被加入到我们的代码中来的?这个问题的答案就在MybatisLogging.jdbc包下。下面用一个图来解释下这个包里面的类结构。

mybatis springboot 日志配置 mybatis的日志_java_08

完成我们日志模块功能的代理类增强类就是ConnectionLogger、PreparedStatementLogger、ResultSetLogger这几个类,他们继承了BaseJdbcLogger,实现了InvocationHandler。

我们就从BaseJdbcLogger出发来讲解日志的实现过程。
public abstract class BaseJdbcLogger {
    protected static final Set<String> SET_METHODS = (Set)Arrays.stream(PreparedStatement.class.getDeclaredMethods()).filter((method) -> {
        return method.getName().startsWith("set");
    }).filter((method) -> {
        return method.getParameterCount() > 1;
    }).map(Method::getName).collect(Collectors.toSet());
    protected static final Set<String> EXECUTE_METHODS = new HashSet();
    private final Map<Object, Object> columnMap = new HashMap();
    private final List<Object> columnNames = new ArrayList();
    private final List<Object> columnValues = new ArrayList();
    protected final Log statementLog;
    protected final int queryStack;

    //由于类的内容较多,这里只展示了一部分。
    
    static {
        EXECUTE_METHODS.add("execute");
        EXECUTE_METHODS.add("executeUpdate");
        EXECUTE_METHODS.add("executeQuery");
        EXECUTE_METHODS.add("addBatch");
    }
}

在BaseJdbcLogger类中,我们可以发现有五个容器:

Set SET_METHODS:用来记录’set‘开头的方法名的

Set EXECUTE_METHODS:用来记住与Execute有关的方法名

Map<Object, Object> columnMap:用来记录sql语句参数的key-value键值对的

List columnNames:用来记录参数的key(也可以说是参数name)

List columnValues:用来记录参数的value

BaseJdbcLogger所有日志增强的抽象基类,用于记录 JDBC 那些方法需要增强,保存运行期间 sql 参数信息。在后面的三个类中进行日志增强时会用到上面容器中的内容,并且BaseJdbcLogger类中还有个静态代码块,这个代码块初始化了EXECUTE_METHODS容器,将四个方法名放进了容器中。

看完BaseJdbcLogger,我们继续看看ConnectionLogger。

mybatis springboot 日志配置 mybatis的日志_spring boot_09

ConnectionLogger:负责打印连接信息和 SQL 语句。通过动态代理,对 connection 进行增强,如果是调用 prepareStatement、prepareCall、createStatement 的方法,打印要执行的 sql 语句并返回 prepareStatement 的代理对象(PreparedStatementLogger),让prepareStatement 也具备日志能力,打印参数。(整个流程在上图的invoke方法中有详细的分析)

ConnectionLogger中会返回了增强的代理类对象PreparedStatementLogger,那么我们就进入到PreparedStatementLogger的源码中看看。

mybatis springboot 日志配置 mybatis的日志_spring boot_10

我们主要看invoke这个方法里的增强逻辑,其他方法跟ConnectionLogger大体上相同。在invoke方法里面,主要增强分了三块:首先是在调用SET_METHODS容器中方法的时候,将参数存储到columnMap、columnNames、columnValues三个容器中,为打印参数做好准备.其次是增强了EXECUTE_METHODS容器中的方法,当执行这些方法时,打印出参数容器中的参数信息,并返回动态代理增强的ResultSet(ResultSetLogger)。最后如果是getResultSet方法,则返回动态代理增强的ResultSet(ResultSetLogger),如果是更新操作,则打印影响的行数。整个流程在上图的invoke方法中有详细的分析)

那么PreparedStatementLogger中一样返回了增强的代理类对象ResultSetLogger,接下来我们就进入到ResultSetLogger这个类的源码看看。

mybatis springboot 日志配置 mybatis的日志_加载_11

可以看到,ResultSetLogger在invoke()方法中主要是增强了在最后将总记录数通过日志打印出来的功能。

可能还有读者有疑问,有了这些增强的动态代理类后,它们是几时被创建并加入到主体功能的?由于Mybatis与访问数据库的功能是在执行器Executor这个组件中实现的,所以日志功能是在Executor中嵌入进去的。关于Executor这个组件后面会有专门的小节进行讲解

mybatis springboot 日志配置 mybatis的日志_mysql_12


mybatis springboot 日志配置 mybatis的日志_mysql_13

在上面prepareStatement方法的参数中,我们会发现传入了一个Log类型的StatementLog,这个参数其实是日志实现类的实例对象,之前我们讲到了在LogFactory加载的时候会选择一个优先级较高的日志实现类并将相对应的适配器构造器赋值给变量construct属性,我们只需要调用getLog方法,这个构造器就会创建出相对应的日志实现类适配器对象。下面来看看这个日志实现类适配器对象从生成到传入增强代理类的过程。

mybatis springboot 日志配置 mybatis的日志_java_14

上面是LogFactory类的getLog方法

mybatis springboot 日志配置 mybatis的日志_mybatis_15

上面是MappedStatement中内部类Builder中的方法,MappedStatement这个类是记录Mapper.xml中某一个节点(select/insert/update/delete)信息的。

mybatis springboot 日志配置 mybatis的日志_mybatis_16

上面是SimpleExecutor中的方法,看完这些步骤,跟前面的步骤也就可以连接在一起了。首先Factory类会加载优先级较高的日志实现类所对应的适配器构造器,然后我们调用getLog方法可以通过这个构造器拿到日志实现类适配器的对象statementLog,如果打开了日志功能则会在创建增强的ConnectionLogger时传入日志实现类适配器对象statementLog,然后返回增强的ConnectionLogger对象,这个对象调用相应的方法时又会返回增强的PreparedStatementLogger对象,然后PreparedStatementLogger对象在调用相应方法时又会返回增强的ResultSetLogger对象。这大概就是整个Mybatis日志增强的过程。