原创

MyBatis源码解析 - 日志模块

MyBatis源码解析 - 日志模块

前言

对应的包为:org.apache.ibatis.logging

良好的日志在一个软件中占了非常重要的地位,日志是开发与运维管理之间的桥梁。日志可以帮助运维人员和管理人员快速查找系统的故障和瓶颈,也可以帮助开发人员与运维人员沟通,更好地完成开发和运维任务。但日志的信息量会随着软件运行时间不断变多,所以需要定期汇总和清理,避免影响服务器的正常运行。

Java开发中常用的日志框架有Log4jLog4j2Apache Commons Logjava.utillogging、slf4j等,这些工具对外的接口不尽相同。为了统- -这些工具的接口,MyBatis定义了一套统一的日志接口供上层使用,并为上述常用的日志框架提供了相应的适配器。

适配器模式

首先,我们简单介绍设计模式中有六大原则。

  • 单一职责原则:不要存在多于一个导致类变更的原因,简单来说, -个类只负责唯一项职责。
  • 里氏替换原则:如果对每-一个类型为T1的对象tl,都有类型为T2的对象t2,使得以T1定义的所有程序P在所有的对象t1都代换成2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。遵守里氏替换原则,可以帮助我们设计出更为合理的继承体系。
  • 依赖倒置原则:系统的高层模块不应该依赖低层模块的具体实现,二者都应该依赖其抽象类或接口,抽象接口不应该依赖具体实现类,而具体实现类应该于依赖抽象。简单来说,我们要面向接口编程。当需求发生变化时对外接口不变,只要提供新的实现类即可。
  • 接口隔离原则:一个类对另一个类的依赖应该建立在最小的接口上。简单来说,我们在设计接口时,不要设计出庞大臃肿的接口,因为实现这种接口时需要实现很多不必要的方法。我们要尽量设计出功能单一的接口,这样也能保证实现类的职责单一。
  • 迪米特法则:一个对象应该对其他对象保持最少的了解。简单来说,就是要求我们减低类间耦合。
    开放-封闭原则:程序要对扩展开放,对修改关闭。简单来说,当需求发生变化时,我们可以通过添加新的模块满足新需求,而不是通过修改原来的实现代码来满足新需求。

在这六条原则中,开放封闭原则是最基础的原则,也是其他原则以及后文介绍的所有设计模式的最终目标。

下面回到适配器模式的介绍,适配器模式的主要目的是解决由于接口不能兼容而导致类无法使用的问题,适配器模式会将需要适配的类转换成调用者能够使用的目标接口。这里先介绍适配器模式中涉及的几个角色,如下所述。

  • 目标接口(Target):调用者能够直接使用的接口。
    需要适配的类(Adaptee):一般情况 下,Adaptee 类中有真正的业务逻辑,但是其接口不能被调用者直接使用。
  • 适配器(Adapter): Adapter 实现了Target 接口,并包装了-一个Adaptee对象。Adapter在实现Target接口中的方法时,会将调用委托给Adaptee对象的相关方法,由Adaptee完成具体的业务。
    下面来看适配器模式的类图,如图所示。

使用适配器模式的好处就是复用现有组件。应用程序需要复用现有的类,但接口不能被该应用程序兼容,则无法直接使用。这种场景下就适合使用适配器模式实现接口的适配,从而完成组件的复用。很明显,适配器模式通过提供Adapter 的方式完成接口适配,实现了程序复用Adaptee的需求,避免了修改Adaptee实现接口,这符合“开放-封闭”原则。当有新的Adaptee需要被复用时,只要添加新的Adapter即可,这也是符合“开放封闭“原则的。

MyBatis的日志模块中,就使用了适配器模式。MyBatis内部调用其日志模块时,使用了其内部接口(也就是后面要介绍的org.apacheibtsi1ogging.Log 接口)。但是Log4jLog4j2等第三方日志组件对外提供的接口各不相同,MyBatis 为了集成和复用这些第三方日志组件,在其日志模块中提供了多种Adapter,将这些第三方日志组件对外的接口适配成了org.apaceibatislogging Log接口,这样MyBatis内部就可以统一通过org.apace.iatislogging.Log接口调用第三方日志组件的功能了。
当程序中存在过多的适配器时,会让程序显得非常复杂(后续介绍的所有模式都会有该问题,但是与其带来的好处进行权衡后,这个问题可以忽略不计),增加了把握住核心业务逻辑的难度,例如,程序调用了接口A,却在又被适配成了接口B。如果程序中需要大量的适配器,则不再优先考虑使用适配器模式,而是考虑将系统进行重构,这就需要设计人员进行权衡。

日志适配器

前面描述的多种第三方日志组件都有各自的Log级别,且都有所不同,例如java.util.logging提供了AllFINESTFINERFINECONFIGINFOWARNING等9种级别,而Log4j2则只有tracedebuginfowarnerrorfatal 这6种日志级别。MyBatis统一提供了tracedebugwarnerror四个级别,这基本与主流日志框架的8志级别类似,可以满足绝大多数场景的日志需求。
MyBatis的日志模块位于org.apace.ibtis.logging包中,该模块中通过Log接口定义了日志.
模块的功能,当然日志适配器也会实现此接口。LogFactory工厂类负责创建对应的日志组件适配器,如图所示。

LogFactory类加载时会执行其静态代码块,其逻辑是按序加载并实例化对应日志组件的适配器,然后使用LogFactory.logConstructor这个静态字段,记录当前使用的第三方日志组件的适配器,具体代码如下所示。

//记录当前使用的第三方日志组件对应的适配器的构造方法
private static Constructor<? extends Log> logConstructor;

static {
  // 针对每种日志组件使用tryImplementation()方法进行尝试加载,具体调用顺序是:
  // useSlf4jLogging -> useCommonsLogging -> useLog4J2Logging -> useLog4JLogging
  // -> useJdkLogging -> useNoLogging
  tryImplementation(LogFactory::useSlf4jLogging);
  tryImplementation(LogFactory::useCommonsLogging);
  tryImplementation(LogFactory::useLog4J2Logging);
  tryImplementation(LogFactory::useLog4JLogging);
  tryImplementation(LogFactory::useJdkLogging);
  tryImplementation(LogFactory::useNoLogging);
}
private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {
      try {
        useJdkLogging();
      } catch (Throwable t) {
        // ignore
      }
    }
    //调用每个usse*()方法的代码与调用useJdkLogging()方法类似
 }

LogFacory.trylmplementation()方法首先会检测logConstructor字段,若为空则调用Runnable.run()方法(注意,不是start(方法),如上述代码所示,其中会调用use*Logging()方法。这里以useJdkLogging()为例进行介绍,具体代码如下:

public static synchronized void useJdkLogging() {
  setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}
private static void setImplementation(Class<? extends Log> implClass) {
    try {
      //获取指定适配器的构造函数
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      Log log = candidate.newInstance(LogFactory.class.getName());
      //输出日志
      if (log.isDebugEnabled()) {
        log.debug("Logging initialized using '" + implClass + "' adapter.");
      }
      //初始化logConstructor
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
 }

Jdk14Logginglmpl实现了org.apache.ibatis.logging.Log接口,并封装了java.uti.logging.Logger对象,org.apache.ibatis.logging.Log接口的功能全部通过调用java.util.logging.Logger对象实现,这与前面介绍的适配器模式完全一致。 Jdk14LoggingImpl 的实现如下:

public class Jdk14LoggingImpl implements Log {

  //底层封装的java.util.logging.Logger对象
  private final Logger log;

  public Jdk14LoggingImpl(String clazz) {
    //初始化java.util.logging.Logger对象
    log = Logger.getLogger(clazz);
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isLoggable(Level.FINE);
  }

  @Override
  public boolean isTraceEnabled() {
    return log.isLoggable(Level.FINER);
  }

  @Override
  public void error(String s, Throwable e) {
    //讲请求全部委托给了java.util.logging.Logger对象的相应方法
    //其他级别雷同。。。
    log.log(Level.SEVERE, s, e);
  }
    // ...其他级别的省略...
}

实现了org.apache.ibatis.logging.Log接口的其他适配器与Jdk14LoggingImpl类似,这里就不再扯那么多。

代理模式与JDK动态代理

在下一小节要介绍的JDBC调试功能中会涉及代理模式与JDK动态代理的相关知识,所以在继续介绍日志模块中JDBC调试功能的实现之前,先来简单介绍一下代理模式以及JDK动态代理的实现和原理。
下面先来看代理模式,它的类图如图所示。

其中,Subject是程序中的业务逻辑接口,RealSubject是实现了Subject接口的真正业务类,Proxy是实现了Subject 接口的代理类,其中封装了RealSubject 对象。在程序中不会直接调动RealSubject对象的方法,而是使用Proxy对象实现相关功能。Proxy.operation()方法的实现会调用RealSubject对象的operation()方法执行真正的业务逻辑,但是处理完业务逻辑,Proxy.operation()会在Realsubjct.operation()方法 调用前后进行预处理和相关的后置处理。这就是所谓的“代理模式”。

使用代理模式可以控制程序对RealSubject对象的访问,或是在执行业务处理的前后进行相关的预处理和后置处理。代理模式还可以用于实现延迟加载的功能,我们知道查询数据库是一个耗时的操作,而有些时候查询到的数据也并没有真正被程序使用。延迟加载功能就可以有效地避免这种浪费,系统访问数据库时,首先可以得到-一个代理对象,此时并没有执行任何数据库查询操作,代理对象中自然也没有真正的数据,当系统真正需要使用数据时,再调用代理对象完成数据库查询并返回数据。MyBatis 中延迟加载功能的大致原理也是如此。另外,代理对象可以协调真正RealSubject对象与调用者之间的关系,在一-定程度上实现了解耦的效果。

上面介绍的这种代理模式也被称为“静态代理模式”,这是因为在编译阶段就要为每个RealSubject类创建创建一个 Proxy类,当需要代理的类很多时,这就会出现大量的Proxy类。熟悉Java编程的读者可能会说,我们可以使用JDK动态代理解决这个问题。JDK动态代理的核心是InvocationHandler接口。这里提供一个InvocationHandler 的示例实现,代码如下:

public class TestInvokerHandler implements InvocationHandler {
    private Object target; //真正的业务对象,也就是RealSubject对象

    public TestInvokerHandler(Object target) { // 构造方法
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        // ...在执行业务方法之前的预处理.
        Object result = method.invoke(target, args);
        // ...在执行业务方法之后的后置处理...
        return result;
    }
    public Object getProxy() {
        //创建代理对象
        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                target.getClass().getInterfaces(), this);
    }
    //由于篇幅限制,main()方法不再单独写在另一个类中
    public static void main (String[] args){
        Subject subject = new RealSubject();
        TestInvokerHandler invokerHandler = new TestInvokerHandler(subject);
        //获取代理对象.
        Subject proxy = (Subject) invokerHandler.getProxy();
        //调用代理对象的方法,它会调用TestInvokerHandler. invoke()方法
        proxy.operation();
    }
}

对于需要相同代理行为的业务类,只需要提供一个InvocationHandler 实现即可。在程序运行时,JDK会为每个RealSubject类动态生成代理类并加载到虚拟机中,之后创建对应的代理对象。
下面来分析JDK动态代理创建代理类的原理,笔者使用的JDK版本是1.8.0, 不同JDK版本的Proxy类实现可能有细微差别,但总体思路不变。JDK动态代理相关实现的入口是Proxy.newProxyInstance()这个静态方法,它的三个参数分别是加载动态生成的代理类的类加载器、业务类实现的接口、上面介绍的InvocationHandler 对象。Proxy.newProxyInstance()方法的具体实现如下:

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    Objects.requireNonNull(h);

    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
               //权限检查
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    /*
     * Look up or generate the designated proxy class.
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * Invoke its constructor with the designated invocation handler.
     */
    try {
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }
                //获取代理类的构造函数
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        //创建代理对象
        return cons.newInstance(new Object[]{h});
    } catch (Exception e) {
            // ... 省略部分try/catch ...
        throw new InternalError(e.toString(), e);
    }
}
private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        if (interfaces.length > 65535) {    //限制接口数量...
            throw new IllegalArgumentException("interface limit exceeded");
        }

        // If the proxy class defined by the given loader implementing
        // the given interfaces exists, this will simply return the cached copy;
        // otherwise, it will create the proxy class via the ProxyClassFactory
       //如果指定类的加载器中已经创建了实现指定接口的代理类,则查找缓存,否则通过ProxyClassFactory
       //创建指定接口的代理类
       return proxyClassCache.get(loader, interfaces);
}

proxyClassCache是定义在Proxy 类中的静态字段,主要用于缓存已经创建过的代理类,定义如下:

private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
    proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

WeakCache.get()方法会首先尝试从缓存中查找代理类,如果查找不到,则会创建Factory对象并调用其get()方法获取代理类。FactoryWeakCache中的内部类,Factory.get()方法 会调用ProxyClassFactory.apply()方法创建并加载代理类。
ProxyClassFactory.apply()方法首先会检测代理类需要实现的接口集合,然后确定代理类的名称,之后创建代理类并将其写入文件中,最后加载代理类,返回对应的Class对象用于后续的实例化代理类对象。该方法的具体实现如下:

public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

        //... 对interface集合进行一系列检测 (略)
        //... 选择代理类的报名(略)

        //代理类的包名是通过包名、代理类名称前缀以及编号这三项组成
        long num = nextUniqueNumber.getAndIncrement();
        String proxyName = proxyPkg + proxyClassNamePrefix + num;

           //生成代理类,并写入文件
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
        try {
                //加载代理类,并返回Class对象
            return defineClass0(loader, proxyName,
                                proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            /*
             * A ClassFormatError here means that (barring bugs in the
             * proxy class generation code) there was some other
             * invalid aspect of the arguments supplied to the proxy
             * class creation (such as virtual machine limitations
             * exceeded).
             */
            throw new IllegalArgumentException(e.toString());
        }
    }
}

ProxyGenerator. generateProxyClass()方法会按照指定的名称和接口集合生成代理类的字节码,并根据条件决定是否保存到磁盘上。该方法的具体代码如下:

public static byte[] generateProxyClass(final String name, Class<?>[] interfaces, int var2) {
    ProxyGenerator gen = new ProxyGenerator(name, interfaces, var2);
    //动态生成代理类的字节码
    final byte[] classFile = gen.generateClassFile();
    //如果saveGeneratedFiles为true,会生成的代理类的字节码保存到文件中
    if (saveGeneratedFiles) {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                try {
                    int interfaces = name.lastIndexOf(46);
                    Path var2;
                    if (interfaces > 0) {
                        Path gen = Paths.get(name.substring(0, interfaces).replace('.', File.separatorChar));
                        Files.createDirectories(gen);
                        var2 = gen.resolve(name.substring(interfaces + 1, name.length()) + ".class");
                    } else {
                        var2 = Paths.get(name + ".class");
                    }

                    Files.write(var2, classFile, new OpenOption[0]);
                    return null;
                } catch (IOException classFilex) {
                    throw new InternalError("I/O exception saving generated file: " + classFilex);
                }
            }
        });
    }

    return classFile;
}

通过本小节的介绍我们可以知道,JDK动态代理的实现原理是动态创建代理类并通过指定类加载器加载,然后在创建代理对象时将InvokerHandler对象作为构造参数传入。当调用代理对象时,会调用InvokerHandler.invoke()方法,并最终调用真正业务对象的相应方法。JDK动态代理不仅在MyBatis的多个模块中都有所涉及,在很多开源框架中也能看到其身影。

JDBC调试

MyBatis的日志模块中有一个Jdbc包,它并不是将日志信息通过JDBC保存到数据库中,而是通过JDK动态代理的方式,将JDBC操作通过指定的日志框架打印出来。这个功能通常在开发阶段使用,它可以输出SQL语句、用户传入的绑定参数、SQL语句影响行数等等信息,对调试程序来说是非常重要的。BaseJdbcLogger是-一个抽象类,它是Jdbc包下其他Logger类的父类,继承关系如图所示。

BaseJdbcLogger中定义了SET_METHODSEXECUTE_METHODS两个Set<String>
型的集合,用于记录绑定SQL参数相关的set*()方法名称以及执行SQL语句相关的方法名称,
其定义以及相关静态代码块如下:

//记录 PreparedStatement 接口中定义的常用的set*()方法
protected static final Set<String> SET_METHODS;
//记录了Statement接口和PreparedStatement接口中与执行sql语句相关的方法
protected static final Set<String> EXECUTE_METHODS = new HashSet<>();

static {
  SET_METHODS = Arrays.stream(PreparedStatement.class.getDeclaredMethods())
          .filter(method -> method.getName().startsWith("set"))
          .filter(method -> method.getParameterCount() > 1)
          .map(Method::getName)
          .collect(Collectors.toSet());

  EXECUTE_METHODS.add("execute");
  EXECUTE_METHODS.add("executeUpdate");
  EXECUTE_METHODS.add("executeQuery");
  EXECUTE_METHODS.add("addBatch");
}

BaseJdbcLogger中核心字段的含义如下:

//记录了PreparedStatement.set*()方法设置的键值对
private final Map<Object, Object> columnMap = new HashMap<>();
//记录了PreparedStatement.set*()方法设置的key值
private final List<Object> columnNames = new ArrayList<>();

//记录了PreparedStatement.set*()方法设置的value值
private final List<Object> columnValues = new ArrayList<>();

//用于输出日志的Log对象
protected final Log statementLog;
//记录了SQL的层数,用于格式化输出SQL
protected final int queryStack;

BaseJdbcLogger中提供了填充上述集合的方法以及一些简单的工具方法,后面用到时会进行分析。
ConnectionLogger继承了BaseJdbcLogger抽象类,其中封装了Connection对象并同时实现了InvocationHandler接口。ConnectionLogger.newInstance()方法为会为其封装的Connection对象创建相应的代理对象,具体代码如下:

public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
  //使用jdk动态代理的方式创建代理对象
  InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
  ClassLoader cl = Connection.class.getClassLoader();
  return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
}

ConnectionLogger.invoke()方法是代理对象的核心方法,它为prepareStatement()prepareCall()createStatement()等方法提供了代理,具体实现如下:

public Object invoke(Object proxy, Method method, Object[] params)
    throws Throwable {
  try {
    //如果调用的类是从Object继承的方法,则直接调用,不做任何处理
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, params);
    }
    //如果是prepareStatement()、prepareCall()、createStatement()、createStatement()方法
    //则在创建Statement对象后,为其创建代理对象并返回代理对象
    if ("prepareStatement".equals(method.getName())) {
      if (isDebugEnabled()) {
        //日志输出
        debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
      }
      //调用底层封装的Connection对象的prepareStatement()方法,得到PreparedStatement对象
      PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
      //为该PreparedStatement对象创建代理对象
      stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
      return stmt;
    } else if ("prepareCall".equals(method.getName())) {
      if (isDebugEnabled()) {
        //日志输出
        debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
      }
      PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
      stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
      return stmt;
    } else if ("createStatement".equals(method.getName())) {
      Statement stmt = (Statement) method.invoke(connection, params);
      stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
      return stmt;
    } else {
      //其他方法则直接调用底层Connection对象的相应的方法
      return method.invoke(connection, params);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

StatementLogger的实现与PreparedStatementLogger类似,不再赘述,请小伙伴参考源码学习。ResultSetLogger中封装了ResultSet对象,也继承了BaseJdbcLogger抽象类并实现了InvocationHandler接口。ResultSetLogger 中定义的字段如下:

//记录超大长度的类型
private static final Set<Integer> BLOB_TYPES = new HashSet<>();
//是否是ResultSet结果集的第一行
private boolean first = true;
//统计行数
private int rows;
//真正的ResultSet对象
private final ResultSet rs;
//记录了超大长度的列编号
private final Set<Integer> blobColumns = new HashSet<>();

static {
  //添加BINARY、BLOB、CLOB等类型
  BLOB_TYPES.add(Types.BINARY);
  BLOB_TYPES.add(Types.BLOB);
  BLOB_TYPES.add(Types.CLOB);
  // ...
}

ResultSetLogger.newInstance()方法的实现与ConnectionLogger.newInstance()类似, 它会为ResultSet创建代理对象,不再赘述。ResultSetLogger.invoke()方 法的实现会针对ResultSet.next()方法的调用进行一系列后置操作,通过这些后置操作会将ResultSet数据集中的记录全部输出到日志中。具体实现如下:

public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
  try {
    //如果调用的类是从Object继承的方法,则直接调用,不做任何处理
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, params);
    }
    Object o = method.invoke(rs, params);
    if ("next".equals(method.getName())) {
      if ((Boolean) o) {  //是否还存在下一行数据
        rows++;
        if (isTraceEnabled()) {
          ResultSetMetaData rsmd = rs.getMetaData();
          final int columnCount = rsmd.getColumnCount();  //获取数据集的列数
          if (first) {  //如果是第一列 则输出表头
            first = false;
            //除了输出表头 还会填充blobColumns集合 记录超大类型的列
            printColumnHeaders(rsmd, columnCount);
          }
          // 输出该行记录,注意会过滤调blobColumns中记录的列,这些列的数据较大,不会输出到日志中
          printColumnValues(columnCount);
        }
      } else {
        //遍历完ResultSet之后,会输出总函数
        debug("     Total: " + rows, false);
      }
    }
    //清空BaseJdbcLogger中的column*集合
    clearColumnInfo();
    return o;
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}
正文到此结束
该篇文章的评论功能已被站长关闭