天天看点

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

作者:AC编程

第一节 扫地僧并不简单

SLF4J对一个Java开发者来说,他就是一个不起眼的扫地僧,我们每天都能看到他,但几乎没有人去关注他,去多看他一眼,因为在我们眼里,他实在是太普通、太简单了,毫无亮点。但当我们静下心来,慢慢地靠近他、走进他之后,我们会被他那些所蕴含的丰富的编程思想所惊叹,原来是我们想太简单了。

第二节 SLF4J包含的编程思想与原则

SLF4J,即Java的简单日志门面( Simple Logging Facade for Java SLF4J),作为一个简单的门面或抽象,用来服务于各种各样的日志框架,比如java.util.logging、logback和log4j。SLF4J允许最终用户在部署时集成自己想要的日志框架(SPI机制)。简单来说,SLF4J是Java日志的一个标准或规范,logging、logback和log4j是对该规范的具体实现(日志框架)。

这不禁让我想到了JAP,同理,JPA也是一个标准,Hibernate、Spring data jpa、MyBaatis 是对该标准的具体实现(ORM框架)。

这种设计就体现了依赖倒置原则、面向抽象编程的思想、开闭原则、SPI机制等等。然而,这些概念和思想比较抽象,大部分人都很难理解,之前我们在单独学习这些概念时都是一知半解。今天,我将用一个具体的例子SLF4J日志框架,并尽可能详尽地来阐述这些概念和思想,希望我的这些浅见对您会有一些启发,如果有错误之处,望不吝指正。

第三节 重温SLF4J

我们先动手搭一个简单的项目,来重温一下我们平时是怎么用SLF4J的,等操作完后我们再来细讲SLF4J包含的编程思想与原则。

NO.3.1 用SLF4J输出日志信息

A、新建一个Maven项目

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

new maven project

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

maven project

B、编写并运行测试代码

导入slf4j依赖包

<dependencies>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.25</version>
    </dependency>
</dependencies>           

创建一个LogClient用来测试Log,注意Logger类用的是刚导入的org.slf4j.Logger,而不是其它包下的Logger,如下图:

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

org.slf4j.Logger

用LoggerFactory工厂获得一个Logger实例,并调用trace、info等方法打印日志信息,如下图:

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

打印日志

运行main函数,发现我们打印的日志信息并没有显示在控制台,但我们看到控制台提示了SLF4J的默认实现是不做任何操作的,no-operation (NOP) ,如下图:

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

运行测试代码

因此我们需要给SLF4J配置一个实现框架,我们先用Logback

NO.3.2 引入SLF4J的实现框架Logback输出日志

A、加入Logback

导入依赖包

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>           
Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

导入Logback

再次运行main函数,可以发现我们的日志信息已经在控制台打印出来了,如下图;

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

控制台显示日志信息

细心的同学可能已经发现了一个“小BUG”,我们要打印的trace怎么没有在控制台打印出来?因为这和log输出的level有关系。

B、添加配置文件logback.xml

现在我们来对logback进行相关配置,在src/main/resources目录下新建logback.xml,将root level设置为trace ,详细配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <Encoding>UTF-8</Encoding>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </layout>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <Encoding>UTF-8</Encoding>
        <!-- 指定日志文件的名称 -->
        <file>/Users/alanchen/temp/log/test.log</file>

        <!--
           日志输出格式:%d表示日期时间,%thread表示线程名,%-5level:级别从左显示5个字符宽度
           %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息,%n是换行符
        -->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern>
        </layout>

    </appender>

    <!--
    root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
    要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。
    -->
    <root level="trace">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>           

我们再次运行main函数,我们发现控制台已经打印出trace信息了,如下图:

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

控制台显示trace

另外,我们在配置文件中配置了输出文件,我们打开该输出文件查看一下输出信息

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

test.log

其实,slf4j的dependency配置可以去掉,因为logback的dependency已经包含了slf4j,我们注释slf4j后再次运行项目,项目依然能正常运行并输出日志信息,如下图:

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

正常运行并输出日志信息

NO.3.3 引入SLF4J的实现框架Log4j输出日志

我们将用Log4j来替代Logback,因此logback.xml配置文件以及Logback的dependency可以去掉了,我们加入log4j的依赖并运行项目,如下图:

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

Log4j

并没有在控制台打印日志信息,我们加上Log4j的配置文件log4j.properties,配置如下:

log4j.rootLogger=trace,stdout,file

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c] [%p] - %m%n

log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=/Users/alanchen/temp/log/test2.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c] [%p] - %m%n           

再运行项目,如下图:

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

log4j运行结果

到目前为止,日志框架的操作我们已经演示完了,接下来我们进入依赖倒置原则、面向抽象编程思想阐述部分。

第四节 依赖倒置原则、面向抽象编程、开闭原则、SPI机制

NO.4.1 依赖倒置原则定义

  • 上层模块不应该依赖底层模块,它们都应该依赖于抽象。
  • 抽象不应该依赖于细节,细节应该依赖于抽象。

High level modules should not depend upon low level modules. Both should depend upon abstractions.

Abstractions should not depend upon details. Details should depend upon abstractions.

NO.4.2 面向抽象编程

面向抽象编程,或者叫面向接口编程,也或者叫针对接口编程,不针对实现编程。

“针对接口编程”真正的意思是“针对超类型编程”,这样才能实现多态。超类型可以是父类、抽象类、接口。

针对接口编程,可以隔离掉以后系统可能发生的一大堆改变。如果代码是针对接口而写,那么通过多态,它可以与任何新类实现该接口。但是,当代码使用大量的具体类时,等于是自找麻烦,因为一旦加入新的具体类,就必须改变代码,打破开闭原则。

NO.4.3 开闭原则定义

一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。

Software entities like classes, modules and functions should be open for extension but closed for modification

NO.4.4 SPI机制

A、什么是SPI机制

SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制,可以轻松实现面向服务的注册与发现,完成服务提供与使用的解耦,并且可以实现动态加载。

引入服务提供者就是引入了SPI接口的实现者,通过本地的注册发现获取到具体的实现类,轻松可插拔,SPI实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载,为某个接口寻找服务实现的机制。

我的理解就是上层提供接口,我们需要去实现,并且上层只需要根据我们的配置文件即可拿到我们的实现类(反射获取)。

B、Java SPI的具体约定为

当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。JDK提供服务实现查找的一个工具类:java.util.ServiceLoader。

第五节 SLF4J如何体现了依赖倒置原则、面向抽象编程、开闭原则、SPI机制?

NO5.1 依赖倒置原则

怎么理解依赖倒置?倒置的是什么?其实从依赖倒置这个名词中就能找到答案,倒置的是依赖,具体来说倒置的是依赖关系。从依赖倒置这个名词中,我们隐约能感受到这是非正常的逆向思维的,有依赖倒置,就有依赖正置。

  • 依赖正置:即正常思维,依赖的是具体/实现。
  • 依赖倒置:即逆向思维,依赖的是抽象。

其实依赖倒置核心也就是面向接口编程/面向抽象编程。

首先我们来谈依赖倒置原则的上半句[上层模块不应该依赖底层模块,它们都应该依赖于抽象],上层模块是日志框架的使用者,在上面例子中就是LogClient这个类,显然,底层模块(或叫下层模块)就是日志框架了。在例子中LogClient用的是org.slf4j.Logger这个接口,所以上层模块依赖的是抽象,同时,Logback、log4j都对该接口进行了实现,所以底层模块依赖的也是抽象。

想象一下,如果上层模块依赖底层模块,即LogClient直接用的是Logback的实现类,再想把Logback换成了Log4j,我们就需要去改LogClient,目前是只有一个Client,真实项目中就是n多个Client。

再来,我们继续来谈谈依赖倒置原则的下半句[抽象不应该依赖于细节,细节应该依赖于抽象],其实说的还是面向接口编程。我们再拆开来说:

【抽象不应该依赖于细节】,即我们的依赖要用接口或抽象类(抽象),而不是具体实现类(细节)。【细节应该依赖于抽象】,说的是我们的类要实现接口,或继承抽象类,如:

public abstract class Pizza {
}

public class CheesePizza extends Pizza{
}

public interface IPizzaStroeService {

    // 抽象依赖抽象,没有依赖细节
    void order(Pizza pizza);

    // 抽象依赖了细节
    void order(CheesePizza pizza);
}           

NO5.2 面向抽象编程

上层和底层都依赖抽象有什么好处?好处显然易见,LogClient我一行代码没改,就把Logback换成了Log4j,这就是面向抽象编程的好处,面向抽象编程使我们的程序更具扩展性。我们常说的面向抽象编程也叫面向接口编程。

NO5.3 开闭原则

我们再来看,这有没有体现开闭原则?当然有体现,没有改LogClient一行代码,就把Logback换成了Log4j,对修改进行了关闭。如果我们对Logback、Log4j都不满意,自己写一个日志框架LogAC也去实现SLF4J,然后项目中换成自己的日志框架LogAC,这就是对扩展开放。

NO5.4 SPI机制

SLF4J是Java日志的一个标准或规范,logging、logback和log4j是对该规范的具体实现,并可以无缝切换,通过查看源码我们可以发现SPI的身影。

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

logback的jar包

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

LogbackServletContainerInitializer

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

ServletContainerInitializer

第六节 SPI机制扩展说明

NO.6.1 SPI的另一应用场景 JDBC驱动

通常各大厂商(如MySQL、Oracle)会根据一个统一的规范(java.sql.Driver)开发各自的驱动实现逻辑。客户端使用JDBC时不需要去改变代码,直接引入不同的SPI接口服务即可。Mysql的则是com.mysql.jdbc.Drive,Oracle则是oracle.jdbc.driver.OracleDriver

Java扫地僧SLF4J(依赖倒置原则、面向抽象编程、开闭原则、SPI)

MySql的jar包

NO.6.2 SpringBoot中的类SPI扩展机制

在SpringBoot的自动装配过程中,最终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。(例如:数据库的自动配置功能)。

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// spring.factories文件的格式为:key=value1,value2,value3
// 从所有的jar包中找到META-INF/spring.factories文件
// 然后从文件中解析出key=factoryClass类名称的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    // 取得资源文件的URL
    Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    List<String> result = new ArrayList<String>();
    // 遍历所有的URL
    while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        // 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
        Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
        String factoryClassNames = properties.getProperty(factoryClassName);
        // 组装数据,并返回
        result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
    }
    return result;
}           

可以看到,它并没有采用JDK中的SPI机制来加载这些类,不过原理差不多。都是通过一个配置文件,加载并解析文件内容,然后通过反射创建实例。

假如你希望参与到SpringBoot初始化的过程中,现在我们又多了一种方式。我们也创建一个spring.factories文件,自定义一个初始化器。

继续阅读