logging包
logging包负责完成 MyBatis操纵中的日志记录工作。
对于大多数系统而言,日志记录是必不可少的,它能够资助我们追踪系统的状态或者定位问题所在。MyBatis作为一个 ORM框架,运行过程中可能会在配置剖析、参数处理、数据查询、结果转化等各个环节中遇到错误,这时,MyBatis 输出的日志便成了定位错误的最好资料。
背景知识
适配器模式
适配器模式(Adapter Pattern)是一种结构型模式,基于该模式计划的类能够在两个或者多个不兼容的类之间起到沟通桥梁的作用。
转换插头就是一个适配器的典型例子。不同的转换插头能够适配不同国家的插座标准,从而使得一个电器能在各个国家使用。
适配器的思想在程序计划中非常常见,如代码10-1中就体现了这种思想。
【代码10-1】
上述代码中,方法三是核心方法,它必要四个输入参数。而在有些场景下,调用方只能提供三个或两个参数。为了使只有三个或两个参数的调用方能够正常地调用核心方法,方法一和方法二充当了方法适配器的作用。这两个适配器通过为未知参数设置默认值的方式,搭建起了调用方和核心方法之间的桥梁。
不过,通常我们提及适配器模式是指类适配器或者对象适配器。图10-1给出了类适配器类图。
图10-1 类适配器类图
在图10-1中,Target接口是 Client想调用的标准接口,而 Adaptee是提供服务但不符合标准接口的目的类。Adapter便是为了 Client能顺遂调用 Adaptee而创建的适配器类。如代码10-2所示,Adapter既实现了 Target接口又继承了 Adaptee类,从而使 Client能够与Adaptee适配。
【代码10-2】
而对象适配器 Adaptee不再继承目的类,而是直接持有一个目的类的对象。图10-2给出了对象适配器类图。
图10-2 对象适配器类图
代码10-3给出了使用对象适配器的示例。
【代码10-3】
如许,Adapter可以直接将 Client要求的操纵委托给目的类对象处理,也实现了 Client和 Adaptee 之间的适配。而且这种适配器更为灵活一些,因为要适配的目的对象是作为初始化参数传给 Adapter的。适配器模式能够使得原本不兼容的类可以一起工作。通常情况下,如果目的类是可以修改的,则不必要使用适配器模式,直接修改目的类即可。但如果目的类是不可以修改的(例如目的类由外部提供,或者目的类被众多其他类依赖必须保持不变),那么适配器模式则会非常有用。
日志框架与日志级别
日志框架是一种在目的对象发生变革时将相关信息记录进日志文件的框架。如许,当目的对象出现问题或必要核查目的对象变动汗青时,日志框架记录的日志文件便可以提供翔实的资料。
早先,Java的日志打印依靠软件开发者自行编辑输出语句将日志输出到文件流中。例如,通过“System.out.println”方法打印普通讯息,或通过“System.err.println”方法打印错误信息。
开发者自行编辑输出语句举行日志打印的方式非常烦琐,而且还会导致日志格式杂乱,不利于日志分析软件的进一步处理。为了解决这些问题,产生了大量的日志框架。
经过多年的发展,Java 领域的日志框架已经非常丰富,有 log4j、Logging、commons-logging、slf4j、logback等,它们为 Java的日志打印工作提供了极大的便利。
为了方便日志管理,日志框架大都对日志等级举行了划分。常见的日志等级划分方式如下。
· Fatal:致命等级的日志,指发生了严峻的会导致应用程序退出的事件;
· Error:错误等级的日志,指发生了错误,但是不影响系统运行;
· Warn:警告等级的日志,指发生了异常,可能是潜在的错误;· Info:信息等级的日志,指一些在粗粒度级别上必要夸大的应用程序运行信息;
· Debug:调试等级的日志,指一些细粒度的对于程序调试有资助的信息;
· Trace:跟踪等级的日志,指一些包罗程序运行详细过程的信息。
有了以上的日志划分后,在打印日志时我们就可以定义日志的级别。也可以根据日志等级举行输出,防止大量的日志信息混杂在一起。
目前,在很多集成开发环境中可以调节日志的显示级别,使只有一定级别以上的日志才会显示出来,如许能够根据不同的使用情况举行日志的筛选。图10-3 所示为划分了等级的日志在集成开发环境 IntelliJ IDEA Community Edition中的展示效果。
图10-3 不同级别日志的展示效果
基于反射的动态代理
在 9.1.3节介绍了静态代理,同时我们也提到,静态代理中代理对象和被代理对象是在程序中写死的,不够灵活。具体来说,要想建立某个对象的静态代理,必须为其建立一个代理类,而且所有被代理的方法均需在代理类中直接调用。这就使得代理类高度依赖被代理类,被代理类的任何变动都可能引发代理类的变动。
而动态代理则灵活很多,它能在代码运行时动态地为某个对象增加代理,并且能为代理对象动态地增加方法。
动态代理的实现方式有很多种,这一节我们介绍较为常用的一种:基于反射的动态代理。
在 Java中 java.lang.reflect包下提供了一个 Proxy类和一个 InvocationHandler接口,使用它们就可以实现动态代理。
接续 9.1.3节的示例,我们来展示基于反射的动态代理。在该示例中,接口和被代理类与之前划一,没有任何变革。
接口如代码10-4所示。
【代码10-4】
被代理类如代码10-5所示。
【代码10-5】
下面创建一个 ProxyHandler类继承 java.lang.reflect.InvocationHandler接口,并实现其中的 invoke方法。invoke方法中必要传入被代理对象、被代理方法及调用被代理方法所需的参数,如代码10-6所示。
【代码10-6】
接下来可以如代码10-7所示,使用动态代理。
【代码10-7】
程序运行结果如图10-4所示。
图10-4 程序运行结果
示例项目的类图如图10-5所示。
图10-5 示例项目的类图
动态代理类可以代理多个其他类。例如,在“ProxyHandler proxyHandler=new ProxyHandler(user)”中给 ProxyHandler 传入不同的被代理对象,然后就可以使用Proxy.newProxyInstance天生不同的代理对象。
本节示例中的动态代理是基于反射实现的。“Proxy.newProxyInstance”方法通过反射创建一个实现了“UserInterface”接口的对象,这个对象就是代理对象 userProxy。因此,对于基于反射的动态代理而言,有一个必需的条件:被代理的对象必须有一个父接口。
Log接口
logging包中最重要的就是 Log接口,它有 11个实现类,分布在 logging包的不同子包中。Log接口及其实现类的类图如图10-6所示。
图10-6 Log接口及其实现类的类图
下面先详细了解 Log 接口中的方法。Log 接口中定义了日志框架要实现的几个基本方法。
· error:打印 Error级别日志;
· warn:打印 Warn级别日志;
· debug:打印 Debug级别日志;
· trace:打印 Trace级别日志;
· isDebugEnabled:判断打印 Debug级别日志的功能是否开启;
· isTraceEnabled:判断打印 Trace级别日志的功能是否开启。
上述各个方法主要是实现不同级别日志的打印功能。然而,其中的isDebugEnabled方法和 isTraceEnabled方法略显突兀,我们将单独举行说明。
isDebugEnabled方法和 isTraceEnabled方法是从服从角度思量而计划的。
起首,Debug和 Trace是两个级别比力低的日志,越是级别低的日志越有如许的特点:
· 很少开启:因为它们级别很低,大多时候该级别的信息不必要展示;
· 输出频次高:低级别日志的触发门槛很低,这意味着一旦它们
开启,通常会以非常高的频率输出日志信息;
· 内容冗长:它们中通常包罗非常丰富和细致的信息,因此信息内容通常十分冗长。假如存在代码10-8 所示的日志打印操纵,在日志打印过程中调用了 trace 方法。以“org.apache.commons.logging.impl.SimpleLog”下的 trace 方法(可以通过 JakartaCommons LoggingImpl实现类中的trace方法追踪到该方法)为例,其具体实现如代码10-9所示。
【代码10-8】
【代码10-9】
低级别的日志很少开启,这意味着 this.isLevelEnabled(1)的返回值大概率是 false。因此代码10-8中所示的字符串拼接结果是无用的,会被直接丢弃。并且低级别日志输出频次高且内容冗长,这意味着这种无用的字符串拼接是频发的且资源斲丧很大。
要想避免上述无用的字符串操纵导致的大量系统资源斲丧,就必要使用 isDebugEnabled方法或 isTraceEnabled方法对低级别的日志输出举行前置判断,如代码10-10所示。
【代码10-10】
如许,借助 isTraceEnabled方法就避免了资源的浪费。
在阅读源码的过程中,读懂源码只是完成了浅层知识的学习。在读懂源码的同时思索源码为何这么计划将会使我们有更大的收获,也会使我们更容易读懂源码。
Log接口的实现类
在 Log接口的 11个实现类中,最简单的实现类就是 NoLoggingImpl类,因为它是一种不打印日志的实现类,内部几乎没有任何的操纵逻辑。StdOutImpl 实现类也非常简单,对于 Error 级别的日志调用 System.err.println 方法举行打印,而对于其他级别的日志则调用System.out.println方法举行打印。
其他的 9 个实现类中,Slf4jLocationAwareLoggerImpl 类和 Slf4jLoggerImpl 类是Slf4jImpl 类的装饰器,Log4j2AbstractLoggerImpl 类和 Log4j2LoggerImpl 类是 Log4j2Impl类的装饰器。这四个装饰器类结构非常简单,我们不再展开介绍。
接下来重点分析剩下的 5 个实现类,它们是 JakartaCommonsLoggingImpl、Jdk14LoggingImpl、Log4jImpl、Log4j2Impl 和 Slf4jImpl。我们以 commons 子包中的JakartaCommonsLoggingImpl为例,检察其具体实现。代码10-11是JakartaCommonsLoggingImpl类的部门源码。
【代码10-11】
可以看出,JakartaCommonsLoggingImpl 是一个典型的对象适配器。它的内部持有一个“org.apache.commons.logging.Log”对象,然后所有方法都将操纵委托给了该对象。
LogFactory
我们已经知道 Log接口有着众多的实现类,而 LogFactory就是制造实现类的工厂。最终,该工厂会给出一个可用的 Log实现类,由它来完成 MyBatis的日志打印工作。
Log 接口的实现类都是对象适配器(装饰器类除外),最终的实际工作要委托给被适配的目的对象来完成。因此是否存在一个可用的目的对象成了适配器可否正常工作的关键所在。于是 LogFactory的主要工作就是尝试天生各个目的对象。如果一个目的对象能够被天生,那该目的对象对应的适配器就是可用的。LogFactory天生目的对象的工作在静态代码块中被触发。代码10-12展示了 LogFactory的静态代码块。
【代码10-12】
起首检察代码10-13所示的 tryImplementation方法。
【代码10-13】
tryImplementation方法会在 logConstructor为 null的情况下调用 Runnable对象的 run方法。要留意一点,直接调用 Runnable对象的run方法并不会触发多线程,因此代码10-12中的多个 tryImplementation 方法是依次执行的。这也意味着 useNoLogging 方法中引用的NoLoggingImpl 实现类是最后的保底实现类,而且 NoLoggingImpl 不必要被适配对象的支持,一定能够成功。因此,最终的保底日志方案就是不输出日志。
以代码10-12中的“tryImplementation(LogFactory::useCommonsLogging)”为例继续追踪源码,该方法通过 useCommonsLogging方法调用 setImplementation方法。代码10-14给出了 setImplementation方法的带解释源码。
【代码10-14】
代码10-14显示 setImplementation方法会尝试获取参数中类的构造函数,并用这个构造函数创建一个日志记录器。如果这次创建是成功的,则意味着以后的创建也是成功的,即当前参数中的类是可用的。因此,把参数中类的构造方法赋给了 logConstructor属性。如许,当外部调用 getLog方法时,便可以由 logConstructor创建一个 Log类的实例。
在静态代码块中,我们发现 StdOutImpl类并没有参与设置 logConstructor属性的过程,这是因为它不在默认日志输出方式的备选列表中。不过这并不代表着它毫无用处,因为MyBatis允许我们自行指定日志实现类。例如,在配置文件的 settings节点下配置如下信息,则可以自定义 StdOutImpl类作为日志输出方式,使 MyBatis的日志输出到控制台上。
自行指定日志实现类是在 XML剖析阶段通过调用 LogFactory中的useCustomLogging方法实现的。它相比于静态代码块中的方法执行得更晚,会覆盖前面的操纵,因此具有更高的优先级。
JDBC日志打印
在前面几节的分析中,我们始终对 jdbc子包中的源码避而不谈。这是因为 jdbc子包中的源码和之前几节的实现逻辑完全不同。在这一节中,我们会对这些源码举行单独的分析。
MyBatis是 ORM框架,它负责数据库信息和 Java对象的相互映射操纵,而不负责具体的数据库读写操纵。具体的数据库读写操纵是由 JDBC举行的,这一点在后面的章节也会详细介绍。
既然 MyBatis不举行数据库的查询,那 MyBatis的日志中便不会包罗 JDBC的操纵日志。然而,很多时候 MyBatis的映射错误是由于 JDBC的错误引发的,例如 JDBC无法正确执行查询操纵或者查询得到的结果类型与预期的不划一等。因此,JDBC 的运行日志是分析 MyBatis框架报错的重要依据。然而,JDBC日志有自身的一套输出体系,JDBC日志和MyBatis日志是分开的,这会给我们的调试工作带来很多的困难。jdbc子包就是用来解决这个问题的。jdbc子包基于代理模式,让 MyBatis能够将 JDBC的操纵日志打印出来,极大地方便了我们的调试工作。接下来就介绍 jdbc子包是怎样实现这个操纵的。
图10-7给出 jdbc子包的类图。BaseJdbcLogger作为基类提供了一些子类会用到的基本功能,而其他几个实现类则为相应类提供日志打印本领。例如,ConnectionLogger 为“java.sql.Connection”类提供日志打印本领。
图10-7 jdbc子包类图
BaseJdbcLogger各个子类使用动态代理来实现日志的打印。以 ConnectionLogger为例,介绍 BaseJdbcLogger实现类的实现逻辑。
ConnectionLogger继承了InvocationHandler接口,从而成为一个代理类。在BaseExecutor的 getConnection方法中我们可以看到代码10-15所示的操纵,当 statementLog的 Debug功能开启时,getConnection 方法返回的不是一个原始的 Connection 对象,而是由“ConnectionLogger.newInstance”方法天生的一个代理对象。
【代码10-15】
如许,所有“java.sql.Connection”对象的方法调用都会进入 ConnectionLogger 中的invoke方法中。代码10-16给出了 ConnectionLogger中的 invoke方法。
【代码10-16】
上述 invoke方法主要完成了以下两个附加的操纵。
· 在 prepareStatement、prepareCall这两个方法执行之前增加了日志打印操纵。
· 在必要返回 PreparedStatement 对象、StatementLogger 对象的方法中,返回的是这些对象的具有日志打印功能的代理对象。如许,PreparedStatement 对象、StatementLogger对象中的方法也可以打印日志。
BaseJdbcLogger的其他几个实现类的逻辑与 ConnectionLogger的实现逻辑完全划一,留给各人自行分析。
本文给各人讲解的内容是通用源码阅读指导mybatis源码详解,底子功能包源码阅读: logging包
- 下篇文章给各人讲解的是通用源码阅读指导mybatis源码详解:底子功能包源码阅读:parsing包;
- 觉得文章不错的朋友可以转发此文关注小编;
- 感谢各人的支持!
|
|
|
|
|