一、前言熟悉JAVA服务器开发的同学应该都用过日志模块,大概率用过“log4j-over-slf4j”和“slf4j-log4j”这两个包。那么这两个包有什么区别呢?他们为什么互相称呼?本文将解释这些概念之间的区别。先说SLF4J。2.从SLF4J开始SLF4J的全称是“SimpleLoggingFacadeforJava(SLF4J)”。它诞生的目的是为不同的日志方案提供一套统一的接口适配标准,让业务代码无需关心所使用的第三方模块使用的是哪种日志方案。比如ApacheDubbo和RabbitMQ使用的日志模块是不一样的。从某种意义上说,SLF4J只是一个门面,类似于当年的ODBC(针对不同数据库厂商制定的统一接口标准,下文会介绍)。而这个门面对应的包名是“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年3月1日),现有的实现SLF4J的方案有以下几种:什么是一些第三方框架实现的slf4j方案。2.2工作机制那么整个SLF4J的工作机制是如何工作的呢?换句话说,系统如何知道要使用哪个实现?对于不需要适配器的原生实现,直接导入相应的包即可。对于需要适配器的委托实现,需要通过另一个渠道:SPI机制告诉SLF4J使用哪个实现类。比如我们看slf4j-log4j的包结构:先看pom文件,里面包含两个依赖:org.slf4jslf4j-apilog4jlog4jslf4j-log4j同时引入了slf4j-api和log4j。那么slf4j-log4j本身的作用就不言而喻了:利用LOG4J的功能实现SLF4J的接口标准。整体的接口/类关系路径如下图所示:但这仍然没有解决本章开头提出的问题(程序如何知道使用哪个Logger)。大家可以从源码入手:(slf4j/slf4j-log4j12atmasterqos-ch/slf4jGitHub),我们看到了以下几个关键文件:也就是说:slf4j-log4j利用java的SPI机制来通知JVM在运行时调用了哪个具体的实现类。由于SPI机制不属于本文讨论范围,读者可以去官网获取资料。读者可以前往GitHub-qos-ch/slf4j:SimpleLoggingFacadeforJava查看适配器的其他实现是如何工作的。那么本章开头问题的答案是:SLF4J制定了一套日志打印流程,然后将核心类抽象出接口,对外实现;适配器使用第三方日志组件来实现这些核心类接口,并使用SPI机制让JAVA在运行时感知到核心接口的具体实现类。以上两点构成了本文接下来要讲述的知识点:委托模型。3.委托模式综上所述,我们从SLF4J的案例中引入了“委托模式”的概念。接下来,我们将重点介绍委托模式(delegation)。接下来,我们将按照认知过程,从三个问题来解释委托模型:为什么要使用委托模型,什么是委托模型,如何使用委托模型,然后在下一章中,我们将通过典型案例行业分析委托模型的使用。3.1为什么要使用委托模型?我们回到SLF4J。为什么使用委托模型?因为日志打印功能有多种实现方式。对于应用程序开发人员来说,最好需要一个标准的打印流程。其他第三方组件可能有些地方不一样,但是核心流程最好不要改动。对于标准制定者来说,他无法控制每一个第三方组件的所有细节,只能对外开放有限的定制能力。而我们放大到软件领域,或者互联网开发领域,不同开发者的协作方式主要依赖jar包应用:第三方开发一个工具包,放到中央仓库(maven,gradle),用户通过其他信息渠道(csdn、stackoverflow等)根据问题定位到这个jar包,然后在代码项目中引用。理论上,如果第三方jar包非常稳定(比如c3p0),那么jar包的维护者很少甚至几乎不会与用户建立联系。如果有的中间件开发人员觉得不符合公司/部门的需求,就会在jar包的基础上另行定制打包。纵观上面的整个过程,不难发现两点:Toolkit开发者和用户并没有建立起稳定的协作渠道。工具包开发人员对其成品的开发几乎没有控制权。那么如果有人想建立一套标准呢?比如日志标准,比如数据库连接标准,那么只有少数大公司联盟,或者知名的开发团队联盟才能制定标准,实现核心链接部分。至于为什么所有环节都没有实现,原因也很简单:软件领域的协作本身就是弱中心化的,否则你不带别人来玩,别人也不会采用你的标准(参考IBM推的COBOL当时)。综上所述:委托模型是根据当前软件领域的协同特点提出的一种较好的软件结构模型。那么什么时候使用委托模型呢?需要制定一定的标准,并由一个集中的团队负责。用户有强烈的定制化部分实现的需求。这里有一个硬件领域的反例:快充标准。2018年甚至更早,消费者将需要快充功能。但是快充需要定制很多硬件来实现,所以这个时候第一个条件就满足了,但是那个时候还没有一个团队或者公司能够掌控整个安卓手机硬件生态,而且它不可能联合发起一个中心化的团队来负责,导致各种手机。厂商快充功能全面开花:A公司的快充线无法为B公司的手机快充。3.2什么是委托模式?基于以上讨论,委托模型的核心构成显而易见:核心环节和开放接口。核心环节是指:为了达到某种目的,一组特定的组件,按照特定的顺序、特定的协同标准,共同执行计算逻辑。开放接口是指在给定特定输入和输出的情况下,将实现细节交给外部的功能接口。举一个更现实的例子:传统汽车。几乎每一辆传统汽车都是按照发动机、变速器、底盘三大部分进行集成和协调的。发动机做功,动力通过变速器传给底盘(这么说不标准,甚至在汽车行业的工人看来,这种描述几乎是谬论,但大致相同).也是基于此,发动机接口、变速箱接口、底盘接口都固定下来了,剩下的就是各厂家实现了:三菱的发动机、日产的发动机、爱信的变速箱、采埃孚的变速箱、伦福德的底盘、天合的底盘等等.连轮胎的接缝都搞定了:马牌轮胎、普利司通轮胎、固特异轮胎。不同的汽车制造商选择来自不同公司的组件来集成某种车型。当然,也有自己达到一定标准的企业:比如大众汽车自己生产??EA888发动机,PSA以自己生产调校的底盘为荣。如果觉得自己不够熟悉,可以举个tomcat的例子。经历过2000年代的软件开发者应该都知道,当时开发一个Web应用程序有多么困难:如何监控socket,如何编解码,如何处理并发,如何管理进程等等。但有一点很普遍:每个Web开发人员都希望有一个框架来管理整个http服务的协议层和内核层。然后出现了JBoss、WebSphere、Tomcat(笑到最后)。这些产品都规定了核心环节:监听socket→读取数据包→封装成http报文→派发到处理池→处理池中线程调用处理逻辑处理→对返回报文进行编码→将其编组为tcp数据包→调用内核函数→发送数据。基于这个核心环节,制定标准:业务处理逻辑的输入是什么,输出是什么,如何让web框架识别业务处理模块。Tomcat的解决方案是web.xml。开发者只需要遵循web.xml标准就可以实现servlets。也就是说,在整个http服务器环节中,Tomcat将几个具体的流程处理组件(listener、filter、interceptor、servlet)委托给了业务开发人员去实现。3.3如何使用委托模式在使用委托模式之前,根据以上模式匹配条件做一个自我判断:有一个必要的用户,他设定了一定的标准,负责中心化的团队,有强烈的需求自定义一些部分实现。如果不满足条件1,则不需要考虑使用委托模式;如果满足条件1不满足条件2,那么先保留接口,使用依赖注入,开发自己的接口实现类注入到主进程中。这种方式在很多第三方依赖包中都能看到,比如spring的BeanFactory、BeanAware等,还有各个公司在开发SSO时预留的一些hooks和filters。决定采用委托模式后,首先要做的就是“确定核心环节”。这一步是最难的,因为用户往往有一定的期望,但要求他们详细描述往往不够准确,有时甚至会出现主次颠倒的情况。作者的建议是:直接让他们说出原来的需求/痛点,然后尝试自己给出解决方案,然后比较他们的解决方案,进行交流,逐步统一两种解决方案。统一的过程也是不断检验和确定的过程。以上过程是笔者亲身经历,仅供参考。核心流程确定后,将流程中一些需要定制的功能抽象成接口暴露出来。在接口的定义上,尽量减少整个过程中对其他类的调用依赖。因此,整体流程分为三步:确认使用该模式;提取核心流程;抽象开放接口。至于是使用SPI机制还是像TOMCAT一样使用XML配置标识,要看具体情况,这里不涉及。4.行业案例4.1JDBCJDBC的诞生很大程度上是基于ODBC的思想,为JAVA设计了专门的数据库连接规范JDBC(JAVADatabaseConnectivity)。JDBC的预期目标是让Java开发人员在编写数据库应用程序时有一个统一的接口,而不需要依赖特定的数据库API,从而实现“一次开发,适用于所有数据库”。尽管在实际开发中,由于使用了数据库特定的语法、数据类型或函数,往往无法达到目标,但JDBC标准大大简化了开发工作。总体来说,JDBC的访问结构大致如下:但其实在JDBC诞生之初,市面上响应SUN的厂商并不多(当时SUN还没有被Oracle收购),所以SUN是采用了本文介绍的桥接模式,如下图:也就是说,在形式上,出现了初步委托的结构形式。下面将只分析单个委托的JDBC层面。综上所述,每一个委托结构都必须具备两个要素:核心路径和开放接口。我们从这两个维度开始分析JDBC。JDBC的核心路径分为六步,包括委托机制需要的两步(引入包、声明委托继承者),共八步,分别为:引入JDBC实现包、注册JDBCDriver和与数据库建立连接,并发起事务(如果需要),创建语句,执行语句并读取返回,插入ResultSet处理ResultSet,关闭ResultSet,关闭Statement,关闭Connection。整个过程中,核心参与者有:Driver、Connection、Statement、ResultSet。Transaction实际上是基于Connection的三个方法(setAutoCommit、commit、rollback)包裹起来的session层,理论上不属于标准层。以mysql-connector-java为例,JDBC接口的具体实现如下:通过Java自带的重写机制,只要使用com.mysql.jdbc.Driver,其他组件的实现类就会由应用程序直接实现。细节不讨论。那么mysql-connector-java是如何告诉JVM应该使用com.mysql.jdbc.Driver的呢?两种模式纯文本模式——在业务代码中以纯文本方式使用Class.forName("com.mysql.jdbc.Driver")SPA机制其实以上两种方式的核心都是初始化com.mysql.jdbc。驱动并执行以下类的初始化逻辑。try{DriverManager.registerDriver(newDriver());}catch(SQLExceptionvar1){thrownewRuntimeException("Can'tregisterdriver!");}也就是说,JDBC通过DriveManager来维护委托继承者的信息。如果读者有兴趣查看DriverManager的源码,就会另辟蹊径实现JDBC类发现。但考虑到篇幅,笔者在此不再赘述。4.2ApacheDubboDubbo的核心路径大致如下(不考虑服务管理集合):消费者调用→参数序列化→网络请求→接收请求→参数反序列化→提供者计算返回→结果序列化→网络返回→消费者接收→结果反序列化(斜体代表consumer端的dubbo职责,下划线代表provider端的dubbo职责)。Dubbo有很多可定制的接口,整体上采用了大量的“类SPI”机制来提供整个RPC过程中的很多环节。自定义注入机制。与传统的JavaSPI相比,DubboSPI在封装和实现类发现等方面做了很多扩展和定制。DubboSPI的整体实现机制和工作机制不在本文讨论范围之内,但为了写作方便,这里做了一些必要的说明。整体的DubboSPI机制可以分为三部分:@SPI注解——声明当前接口类为可扩展接口。@Adaptive注解——声明当前接口类(或当前接口类的当前方法)可以根据特定条件(注解中的值)动态调用具体实现类的实现方法。@Activate注解——声明当前类/方法实现了一个可扩展接口(或可扩展接口的特定方法的实现),并指明了激活的条件,以及所有被激活的实现类中的排序信息。我们以Dubbo-Auth(dubbo/dubbo-plugin/dubbo-authat3.0·apache/dubbo·GitHub)为例,从核心路径和开放接口两个维度进行分析。Dubbo-Auth的实现逻辑是基于Dubbo-filter的原理,也就是说:Dubbo-Auth本身就是Dubbo整体流程中某个环节的委托实现者。Dubbo-Auth的核心入口(也就是核心路径的起点)是ProviderAuthFilter,它是org.apache.dubbo.auth.filter的具体实现,也就是说:org.apache.dubbo.auth.filter是dubbo核心链路上暴露的一个开发接口(类定义上标注了@SPI)。ProviderAuthFilter实现了dubbo核心链接中暴露的开发接口Filter(ProviderAuthFilter实现类定义标有@Activate)。ProviderAuthFilter的核心路径比较简单:获取Authenticator对象,使用Authenticator对象进行auth验证。具体代码如下:@Activate(group=CommonConstants.PROVIDER,order=-10000)publicclassProviderAuthFilterimplementsFilter{@OverridepublicResultinvoke(Invoker>invoker,Invocationinvocation)throwsRpcException{URLurl=invoker.获取网址();booleanshouldAuth=url.getParameter(Constants.SERVICE_AUTH,false);if(shouldAuth){Authenticatorauthenticator=ExtensionLoader.getExtensionLoader(Authenticator.class).getExtension(url.getParameter(Constants.AUTHENTICATOR,Constants.DEFAULT_AUTHENTICATOR){authenticator.authenticate(invocation,url);}catch(Exceptione){返回AsyncRpcResult.newDefaultAsyncResult(e,invocation);}}returninvoker.invoke(invocation);}}注意,上面代码中,url.getParameter(Constants.AUTHENTICATOR,Constants.DEFAULT_AUTHENTICATOR)是dubbospi的Adaptive机制中的选择条件.读者可自行深究,此处略过。既然核心路径中包含了Authenticator,那么Authenticator自然很可能是一个对外暴露的开发接口。也就是说Authenticator的声明类必须使用@SPI注解。@SPI("accessKey")publicinterfaceAuthenticator{/***给请求一个标志**@paraminvocation*@paramurl*/voidsign(Invocationinvocation,URLurl);/***验证请求的签名是否有效*@paraminvocation*@paramurl*@throwsRpcAuthenticationException当验证当前调用失败时*/voidauthenticate(Invocationinvocation,URLurl)throwsRpcAuthenticationException;}上面的代码证明了作者的猜想。在Dubbo-Auth中,提供了一个默认的Authenticator:AccessKeyAuthenticator。在这个实现类中,重新指定了核心路径:getaccessKeyPai;使用accessKeyPair计算签名;比较请求中的签名是否与计算出的签名相同。在本核心路径中,由于引入了accessKeyPair的概念,引入了一个链接:如何获取accessKeyPair。为此,dubbo-auth定义了一个开放接口:AccessKeyStorage。@SPIpublicinterfaceAccessKeyStorage{/***getAccessKeyPairofthisrequest**@paramurl*@paraminvocation*@return*/AccessKeyPairgetAccessKey(URLurl,Invocationinvocation);}4.3LOG4J最后一个案例,我们回到日志组件,而之所以引入LOG4J,是因为它使用了一种非常规的“反向委托”机制。LOG4J借鉴了SLF4J的思想(还是先用LOG4J?SLF4J借鉴了LOG4J?),同样采用了接口标准+适配器+第三方方案的思想来实现委托。所以很明显,这里有一个问题:SLF4J确认了它的核心路径,然后暴露了要实现的接口。SLF4J-LOG4J在尝试实现SLF4J要实现的接口时,使用委托机制将相关路径细节外包。出来,从而形成一个环。所以,如果我同时引入“log4j-over-slf4j”和“slf4j-log4j”,就会引起stackoverflow。这个问题很典型,google一下可能会看到很多案例,比如log4j-over-slf4j和slf4j-log4j12并存的stackoverflow异常分析-actorsfit等。官方也给出了警告(SLF4JErrorCodes).由于本文的重点是委托模型,因此不会详细讨论这个问题。这个案例的重点是说明一件事:委托模型的缺点是开放接口的实现逻辑是不可控的。如果第三方实施存在重大隐患,将导致整个核心流程出现问题。五、总结概括起来,委托模式的使用场景是:需要制定一定的标准,由一个中心化的团队负责;用户有强烈的定制一些部分实现的需求。委托模式的核心点:核心路径、开放接口。委托模式的隐藏机制:实现的注册/发现。参考资料:SLF4J手册将log4j2与slf4j结合使用:java.lang.StackOverflowError-StackOverflow创建可扩展应用程序(Java?教程>扩展机制>创建和使用扩展)(oracle.com)slf4j/slf4j-log4j12atmaster·qos-ch/slf4j·GitHub委托模式-维基百科什么是JDBC驱动程序?-IBM文档课程:JDBC基础知识(Java?教程>JDBC数据库访问)(oracle.com)GitHub-apache/dubbo:ApacheDubbo是一种高性能、基于java的开源软件RPC框架。