委派模式——从SLF4J说起

上一篇 / 下一篇  2023-01-12 15:16:25

  一、前言
  熟悉JAVA服务器开发的同学应该都使用过日志模块,并且大概率使用过"log4j-over-slf4j"和“slf4j-log4j”这两个包。那么这两个包的区别是什么?为什么会互相引用包含呢?这篇文章会解释下这几个概念的区别。
  首先说一下SLF4J。
  二、从SLF4J开始
  SLF4J全称"Simple Logging Facade for Java (SLF4J) ", 它诞生之初的目的,是为了针对不同的log解决方案,提供一套统一的接口适配标准,从而让业务代码无须关心使用到的第三方模块都使用了哪些log方案。
  举个例子, Apache Dubbo和RabbitMQ使用到的日志模块便不相同。从某种意义上而言,SLF4J只是一个facade,类似于当年的ODBC(针对不同的数据库厂商而制定的统一接口标准, 下文会涉及到)。而这个facade对应的包名,是 “slf4j-api-xxx.xxx.xxx.jar”。所以,当你应用了"slf4j-api-xxx.jar"的包时,其实只是引入了一个日志接口标准,而并没有引入日志具体实现。
  2.1、业内实现
  SLF4J标准在应用层的核心类,就是两个: org.slf4j.Logger 和 org.slf4j.LoggerFactory。其中,自版本1.6.0后,如果并没有具体的实现,slf4j-api会默认提供一个啥也不干的Logger实现(org.slf4j.helpers.NOPLogger)。
  在当前(本稿件于2022-03-01拟制)的市面上,既有的实现SLF4J的方案有以下几种:
  整体层次如下图:
  综上而言:以SLF4J-开头的jar包,一般指的是采用某种第三方框架实现的slf4j解决方案。
  2.2 工作机制
  那么整个SLF4J的工作机制是如何运作的呢,换句话说,系统是如何知道应该使用哪个实现方案的呢?
  对于那种不需要适配器的原生实现方式,直接引入对应的包即可。
  对于那种需要适配器的委托式实现方式,则需要通过另外的一个渠道来告知SLF4J应该使用哪个实现类: SPI机制。
  举个例子,我们看一下slf4j-log4j的包结构:
  我们先看pom文件,就包含两个依赖:
  <dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  </dependency>
  <dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  </dependency>
  slf4j-log4j同时引入了slf4j-api和log4j。那么slf4j-log4j本身的作用不言而喻:使用LOG4J的功能,实现SLF4J的接口标准。
  整体的接口/类关系路径如下图:
  但是这仍然没有解决本章节开始提出的问题(程序怎么知道应该用哪个Logger)。
  可以从源码入手:(slf4j/slf4j-log4j12 at master · qos-ch/slf4j · GitHub??),我们看到了以下关键的文件:
  也就是说:slf4j-log4j使用了java的SPI机制告知JVM在运行时调用具体哪一个实现类。由于SPI机制暂不属于本文章讨论范围,读者可以去官网获取信息。
  读者可以去??GitHub - qos-ch/slf4j: Simple Logging Facade for Java??看其他的实现方式的适配器是如何工作的。
  那么本章开始的问题答案便是:
  SLF4J制定一套日志打印流程,然后把核心类抽象出接口给外部去实现;
  适配器使用第三方日志组件实现了这些核心类接口,并采用SPI机制,让JAVA运行时意识到核心接口的具体实现类。
  而上述两点,构成了本文接下来要讲述的知识点:委派模式。
  三、委派模式
  从上文中,我们从SLF4J的案例,引出了"委派模式"这个概念,下面我们就重点讨论委派模式(delegation)。
  接下来我们按照认知流程,依次从三个问题,解释委派模式:
  ·为什么使用委派模式
  · 什么是委派模式
  · 如何使用委派模式
  然后会在下一章,用业内的典型案例,分析委派模式的使用情况。
  3.1 为什么采用委派模式?
  我们回到SLF4J。为什么它会用委派模式呢?因为日志打印功能存在各种不同的实现方式。对于应用开发者而言,最好需要一个标准的打印流程,其他第三方组件可以在某些地方有些不同,但是核心流程是最好不要变。对于标准制定者 而言,他无法控制每一个第三方组件的所有细节,所以只能暴露出有限的自定制能力。
  而我们放大到软件领域,或者在互联网开发领域,不同的开发者的协作模式,主要靠jar包应用:第三方开发一个工具包,放在中心仓库中(maven, gradle), 使用者从其他信息渠道(csdn, stackoverflow等等)根据问题定位到这个jar包,然后在代码工程中引用。理论上,如果这个第三方jar包很稳定(例如c3p0),那么该jar包的维护者就很少甚至几乎不会和使用者建立联系。如果某些中间件开发者觉得不满足自己公司/部门的需求,会根据该jar包再做一次自定义封装。
  纵观上述整个过程,不难发现两点:
  工具包开发者和使用者没有建立稳定的协同渠道
  工具包开发者对自己成品的发展掌控很薄弱
  那么如果有人想要建立一套标准呢?比如log标准,比如数据库连接标准,那么只能有几个大公司联盟,或者著名的开发团队联盟,制定一个标准,并实现其中核心链路部分。至于为什么不实现全部链路,原因也很简单:软件领域的协同本身就是弱中心化的 ,否则你不带别人玩,别人也不会采用你的标准(参考当年IBM推广的COBOL)。
  综上而言:委派模式是基于当前软件领域的协作特性,采取的较好的软件结构模式。
  所以啥时候采用委派模式呢?
  · 存在设定某个标准并由中心化团队负责的必要
  · 使用者有强烈的需求自定制某些局部实现
  这里就举一个硬件领域的反例:快充标准。在2018年甚至更早,消费者就需要一个快充的功能。但是快充需要定制很多硬件才能实现,所以此时就具备了条件一,但是当时并没有任何一个团队或者公司能够掌控安卓手机硬件整个生态,无法共同推出一个中心化团队去负责,从而导致各个手机厂商的快充功能百花齐放:A公司的快充线,无法给B公司的手机快充。
  3.2 什么是委派模式?
  基于上述的讨论,委派模式的核心构成就显而易见了:核心链路, 开放接口。
  核心链路指的是:为了达到某个目的,特定的一组构件,按照特定的顺序,特定的协同标准,共同执行计算的逻辑。
  开放接口指的是:给定特定的输入和输出,将实现细节交给外部的功能接口。
  举个比较现实的例子:传统汽车。
  几乎每一辆传统汽车,都按照三大件进行集成和协作:发动机,变速器,底盘。发动机做功, 通过变速器将动力传输给底盘(这么说并不标准,甚至在汽车工业的工人眼中,这种描述几乎是谬论,但是大致是这样)。也基于此,发动机的接口, 变速箱的接口,底盘的接口都已经固定,剩下的就各个厂商去实现了:三菱的发动机, 日产的发动机,爱信的变速箱,采埃孚的变速箱,伦福德的底盘,天合的底盘等等。甚至连轮胎的接口都制定好了:大陆的轮胎,普利司通的轮胎,固特异的轮胎。
  不同的汽车厂商,选择不同公司的组件,集成出某个汽车型号。当然也有公司自己去实现某个标准:比如大众自己生产EA888发动机,PSA自己生产并调教的底盘并引以为傲。
  如果大家觉得不够熟悉,那么可以举一个tomcat的例子。
  经历过00年代的软件开发者,应该知道当时开发一个web应用是多么的困难:如何监听socket, 如何编码解码,如何处理并发,如何管理进程等等。但是有一点是共通的:每一个Web开发者都想要一个框架去管理整个http服务的协议层和内核层。于是出现了JBoss, WebSphere, Tomcat(笑到了最后)。
  这些产品,都是指定了核心的链路:监听socket → 读数据包→ 封装成http报文 → 派发给处理池子 → 处理池的线程调用处理逻辑去处理 → 编码返回的报文 → 编组成tcp包 → 调用内核函数→ 发出数据。
  基于这个核心链路,制定标准:业务处理逻辑的输入是什么,输出是什么,如何让web框架识别到业务处理模块。
  Tomcat的方案就是web.xml。开发者只要遵从web.xml标准去实现servlet即可。也就是说,在整个http服务器链路中,Tomcat将特定的几个流程处理构件(listener, filter, interceptor, servlet)委派给了业务开发者去实现。
  3.3 如何使用委派模式
  在使用委派模式之前,先根据上文的模式匹配条件进行自我判断:
  · 存在设定某个标准并由中心化团队负责的必要
  · 使用者有强烈的需求自定制某些局部实现
  如果并不符合条件一,那么就不需要考虑使用委派模式;如果符合条件一但是不符合条件二,那就先预留好接口,采用依赖注入的方式,自己开发接口实现类并注入到主流程中。这个做法在很多的第三方依赖包中能够看到,比如spring的BeanFactory, BeanAware等等,还有各个公司开发SSO时预留的一些hook和filter等等。
  在确定使用委派模式后,第一件事就是“确定核心链路”,这一步最难,因为往往使用者都有某种期望,但是让他们具体描述出来,却又经常不够精准,甚至有时候后主次颠倒。笔者的建议是:直接让他们说出原始的需求/痛点,然后自己尝试给出方案,再对比他们的方案,进行沟通,并逐渐将两个方案统一。统一的过程也就是不断试探和确定的过程。
  上述的过程是笔者自己的经验,仅当借鉴。
  在确定核心流程后,再将流程中的一些需要自定制的功能抽象成接口暴露出去。接口的定义中,尽量减少对整个流程中其他类的调用依赖。
  所以整体的流程分为三步:确认使用该模式;提取核心流程;抽象开放接口。
  至于是采用SPI机制还是像TOMCAT一样使用XML配置识别,需要看具体情况,在此不做涉及。

TAG: 软件开发 Java java

 

评分:0

我来说两句

Open Toolbar