剖析slf4j原理并实现自己的日志框架

本文项目已开源

欢迎各位看官前来指出错误并优化。
日志系统

slf4j作用及其实现原理

  • slf4j是门面模式的典型应用,因此在讲slf4j前,我们先简单回顾一下门面模式

门面模式

  • 门面模式,其核心为外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用。用一张图来表示门面模式的结构为:

  • 门面模式的核心为Facade即门面对象,门面对象核心为几个点:
    • 知道所有子角色的功能和责任
    • 将客户端发来的请求委派到子系统中,没有实际业务逻辑
    • 不参与子系统内业务逻辑的实现

为什么要使用slf4j

假设我的一个系统使用了logback。
我的系统又用了A.jar, A.jar用了log4j。
我的系统又用了B.jar, B.jar用了slf4j-simple。

解决这个问题的方式就是引入一个适配层,由适配层决定使用哪一种日志系统,而调用端只需要做的事情就是打印日志而不需要关心如何打印日志,slf4j或者commons-logging就是这种适配层,slf4j是本文研究的对象。

slf4j只是一个日志标准,并不是日志系统的具体实现。理解这句话非常重要,slf4j只做两件事情:

  • 提供日志接口
  • 提供获取具体日志对象的方法

slf4j-simple、logback都是slf4j的具体实现,log4j并不直接实现slf4j,但是有专门的一层桥接slf4j-log4j12来实现slf4j。

测试

logback源码

logback-classsic-1.2.3.jar,其中可以看到包含了org.slf4j.impl包,这里边其实就是按着门面模式自己实现了一套逻辑。

logback-core-1.2.3.jar,其实就是logback的核心包,里边定义了自己的一些实现。在使用slf4j打日志时可以用到。

log4j源码分析

首先看因为log4j并不直接实现slf4j,但是有专门的一层桥接slf4j-log4j12来实现slf4j。
图中很明显的就看出来了。


slf4j-api和slf4j-simple分析

从源码截结构看出,api就是定义一个门面,让实现着自己去实现接口,然后slf4j有一个选择的过程。如果出现了多个实现,就会出现冲突。下面举例子可以看到。

举例分析

<dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.21</version>
        </dependency>
</dependencies>

可以看到如果有多个实现,会出现冲突,如果是maven间接依赖,去掉冲突的就可以了。

比如spring-boot-starter中依赖了logback日志框架(查看导入的maven依赖包就看到了),所以当我在实现自己的日志框架时起了冲突。

SpringBoot的spring-boot-starter有哪些(官方)

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-logging</artifactId>
</dependency>

<!--因为在项目中我用的是自己实现的日志框架,所以要把别的日志框架去除-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-amqp</artifactId>
	<exclusions>
		<exclusion>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-logging</artifactId>
		</exclusion>
	</exclusions>
</dependency>

从例子我们可以得出一个重要的结论,即slf4j的作用:只要所有代码都使用门面对象slf4j,我们就不需要关心其具体实现,最终所有地方使用一种具体实现即可,更换、维护都非常方便。

slf4j原理分析

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestA {

    private static final Logger logger = LoggerFactory.getLogger(TestA.class);

    public static void main(String[] args) {
        logger.info("aaaa");
    }
}
  1. 既然是通过工厂获取,那就进去看看。
public static Logger getLogger(Class<?> clazz) {
	最重要的在这,可以看到去另一个方法了。
	Logger logger = getLogger(clazz.getName());
	if (DETECT_LOGGER_NAME_MISMATCH) {
		Class<?> autoComputedCallingClass = Util.getCallingClass();
		if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
			Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
									  autoComputedCallingClass.getName()));
			Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
		}
	}
	return logger;
}

来到这个方法,传入类名。
首选获取到工厂。
然后从工厂里边获取日志类。
public static Logger getLogger(String name) {
	ILoggerFactory iLoggerFactory = getILoggerFactory();
	return iLoggerFactory.getLogger(name);
}

public static ILoggerFactory getILoggerFactory() {
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            synchronized (LoggerFactory.class) {
                if (INITIALIZATION_STATE == UNINITIALIZED) {
                    INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                    performInitialization();
                }
            }
        }
        switch (INITIALIZATION_STATE) {
        case SUCCESSFUL_INITIALIZATION:
            return StaticLoggerBinder.getSingleton().getLoggerFactory();
        case NOP_FALLBACK_INITIALIZATION:
            return NOP_FALLBACK_FACTORY;
        case FAILED_INITIALIZATION:
            throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
        case ONGOING_INITIALIZATION:
            // support re-entrant behavior.
            // See also http://jira.qos.ch/browse/SLF4J-97
            return SUBST_FACTORY;
        }
        throw new IllegalStateException("Unreachable code");
    }

debug可以看到来到performInitialization();方法。

private final static void performInitialization() {
	进入bind方法
	bind();
	if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
		versionSanityCheck();
	}
}

private final static void bind() {
        try {
            Set<URL> staticLoggerBinderPathSet = null;
            // skip check under android, see also
            // http://jira.qos.ch/browse/SLF4J-328
            if (!isAndroid()) {
		看到会获取可能的StaticLoggerBinder,进去看看是啥
                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            }
            // the next line does the binding
            StaticLoggerBinder.getSingleton();
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            reportActualBinding(staticLoggerBinderPathSet);
            fixSubstituteLoggers();
            replayEvents();
            // release all resources in SUBST_FACTORY
            SUBST_FACTORY.clear();
        } catch (NoClassDefFoundError ncde) {
            String msg = ncde.getMessage();
            if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
                Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
                Util.report("Defaulting to no-operation (NOP) logger implementation");
                Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
            } else {
                failedBinding(ncde);
                throw ncde;
            }
        } catch (java.lang.NoSuchMethodError nsme) {
            String msg = nsme.getMessage();
            if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
                INITIALIZATION_STATE = FAILED_INITIALIZATION;
                Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
                Util.report("Your binding is version 1.5.5 or earlier.");
                Util.report("Upgrade your binding to version 1.6.x.");
            }
            throw nsme;
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }

	说明了这个路径的作用
    // We need to use the name of the StaticLoggerBinder class, but we can't
    // reference
    // the class itself.
    private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

    static Set<URL> findPossibleStaticLoggerBinderPathSet() {
        // use Set instead of list in order to deal with bug #138
        // LinkedHashSet appropriate here because it preserves insertion order
        // during iteration
        Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
        try {
            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            Enumeration<URL> paths;
            if (loggerFactoryClassLoader == null) {
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
        这个地方用到了路径,说明了情况。
        即所有slf4j的实现,在提供的jar包路径下,一定是有"org/slf4j/impl/StaticLoggerBinder.class"存在的,上面的图中可以看到。
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }
            while (paths.hasMoreElements()) {
                URL path = paths.nextElement();
                staticLoggerBinderPathSet.add(path);
            }
        } catch (IOException ioe) {
            Util.report("Error getting resources from path", ioe);
        }
        return staticLoggerBinderPathSet;
    }

最后StaticLoggerBinder就比较简单了,不同的StaticLoggerBinder其getLoggerFactory实现不同,拿到ILoggerFactory之后调用一下getLogger即拿到了具体的Logger,可以使用Logger进行日志输出。

这个地方也即是实现自己的日志框架的根本所在。

/**
 * The <code>LoggerFactory</code> is a utility class producing Loggers for
 * various logging APIs, most notably for log4j, logback and JDK 1.4 logging.
 * Other implementations such as {@link org.slf4j.impl.NOPLogger NOPLogger} and
 * {@link org.slf4j.impl.SimpleLogger SimpleLogger} are also supported.
 */
public final class LoggerFactory {
	
    // private constructor prevents instantiation
    private LoggerFactory() {
    }

    private static boolean messageContainsOrgSlf4jImplStaticLoggerBinder(String msg) {
        if (msg == null)
            return false;
        if (msg.contains("org/slf4j/impl/StaticLoggerBinder"))
            return true;
        if (msg.contains("org.slf4j.impl.StaticLoggerBinder"))
            return true;
        return false;
    }

    
    是否存在冲突的实现,就是有多个实现
    private static boolean isAmbiguousStaticLoggerBinderPathSet(Set<URL> binderPathSet) {
        return binderPathSet.size() > 1;
    }

   多了就往控制台打印信息
    /**
     * Prints a warning message on the console if multiple bindings were found
     * on the class path. No reporting is done otherwise.
     */
    private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
        if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
            Util.report("Class path contains multiple SLF4J bindings.");
            for (URL path : binderPathSet) {
                Util.report("Found binding in [" + path + "]");
            }
            Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
        }
    }

     打印出实际绑定的日志工厂
    private static void reportActualBinding(Set<URL> binderPathSet) {
        // binderPathSet can be null under Android
        if (binderPathSet != null && isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
            Util.report("Actual binding is of type [" + StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr() + "]");
        }
    }

实现自己的日志框架

三大件

包名一定要注意,根据slf4j的查找规则,必须这样子定义。

  1. StaticLoggerBinder,这就是用来获取日志工厂的。
package org.slf4j.impl;

import org.slf4j.ILoggerFactory;
import org.slf4j.spi.LoggerFactoryBinder;


public class StaticLoggerBinder implements LoggerFactoryBinder {

    private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

    // to avoid constant folding by the compiler, this field must *not* be final
    // !final
    public static String REQUESTED_API_VERSION = "1.0";

    private static final String loggerFactoryClassStr = MyLoggerFactory.class.getName();

    private final ILoggerFactory loggerFactory;

    private StaticLoggerBinder() {
        loggerFactory = new MyLoggerFactory();
    }
    
    @Override
    public ILoggerFactory getLoggerFactory() {
        return loggerFactory;
    }

    @Override
    public String getLoggerFactoryClassStr() {
        return loggerFactoryClassStr;
    }

    public static StaticLoggerBinder getSingleton() {
        return SINGLETON;
    }
}



public interface LoggerFactoryBinder {
    /**
     * Return the instance of {@link ILoggerFactory} that 
     */
    public ILoggerFactory getLoggerFactory();

    /**
     * The String form of the {@link ILoggerFactory} object that this 
     * <code>LoggerFactoryBinder</code> instance is <em>intended</em> to return. 
     */
    public String getLoggerFactoryClassStr();
}

2. 实现自己的LoggerFactory

package org.slf4j.impl;

import org.slf4j.ILoggerFactory;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class MyLoggerFactory implements ILoggerFactory {

    ConcurrentMap<String, Logger> loggerMap;

    public MyLoggerFactory() {
        loggerMap = new ConcurrentHashMap<String, Logger>();
    }

    /**
     * Return an Logger
     */
    @Override
    public Logger getLogger(String name) {
        Logger logger = loggerMap.get(name);
        if (logger != null) {
            return logger;
        } else {
            Logger newInstance = new Logger(name);
            Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
            return oldInstance == null ? newInstance : oldInstance;
        }
    }

    /**
     * Clear the internal logger cache.
     */
    void reset() {
        loggerMap.clear();
    }
}

3. 实现自己的Logger

package org.slf4j.impl;

import org.slf4j.LoggerFactory;
import org.slf4j.Marker;

import java.io.ObjectStreamException;
import java.io.Serializable;

public final class Logger implements org.slf4j.Logger, Serializable {

    private static final long serialVersionUID = 1L;


    private LoggerUtil loggerUtil;

    /**
     * The name of this logger
     */
    private String name;

    public Logger(String name) {
        this.name = name;
        this.loggerUtil = new LoggerUtil();
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public boolean isTraceEnabled() {
        return false;
    }

    @Override
    public void info(String msg) {
        System.out.println("haha --- > " + msg);
    }

  /**
  * 反序列化时,返回同一个实例
  */
    protected Object readResolve() throws ObjectStreamException {
        return LoggerFactory.getLogger(getName());
    }
}

至此,项目结束,主要就是利用门面模式,搭建自己的日志框架。

  1. 我的LoggerUtil类
import com.alibaba.fastjson.JSON;
import com.softlab.logsystem.core.model.LogVo;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

public final class LoggerUtil {
    /**
     * Java8日期格式化,线程安全
     */
    private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    /**
     * 日志发送接口
     */
    public static final String URI = "保密";

    public void info(String msg) {
        send(new Exception().getStackTrace()[1].getClassName() + " : " + new Exception().getStackTrace()[1].getMethodName(), "info", msg);
    }

    public void warn(String msg) {
        send(new Exception().getStackTrace()[1].getClassName() + " : " + new Exception().getStackTrace()[1].getMethodName(),"warn", msg);
    }

    public void error(String msg) {
        send(new Exception().getStackTrace()[1].getClassName() + " : " + new Exception().getStackTrace()[1].getMethodName(),"error", msg);
    }

    public void debug(String msg) {
        send(new Exception().getStackTrace()[1].getClassName() + " : " + new Exception().getStackTrace()[1].getMethodName(),"debug", msg);
    }

    public void send(String className, String level, String content) {
        //synchronized(LoggerUtil.class) {
            LogVo logVo = new LogVo();
            logVo.setLevel(level);
            // 设置自己的项目名称
            //logVo.setApplication(APPLICATION_NAME);
            logVo.setContent(content);
            logVo.setTag(className);
            logVo.setTimestamp(LocalDateTime.now().format(dtf));

            String baseUrl = "保密";
            WebClient webClient = WebClient.create(baseUrl);
            Mono<RestData> mono = webClient.post().uri("/log/send").bodyValue(logVo).retrieve().bodyToMono(RestData.class);
            RestData res = mono.block();
            if (res.getCode() == 0) {
                System.out.println("发送成功,code :" + res.getCode() + " , data : " + res.getData());
            } else {
                System.err.println("发送失败,code :" + res.getCode() + " , message : " + res.getMessage());
            }
    }

}

Logger类中的LoggerUtil属于我自己之前写的日志工具类,利用RabbitMQ接收日志,写到数据库,然后可以实时查看。目前只是一个初级版本。
项目已经开源,欢迎各位看官及时指出错误,地址:https://gitee.com/z520_w/LogSystem/tree/master

如何不改变原先代码引入自己的日志框架?

  1. 假设之前用的就是slf4j,那么就可以使用maven去除logback等那些之前使用的依赖,然后导入自己的jar包即可,并且可以在自己的日志框架里使用logback,实现他们的功能。

在Spring中的处理?

可以写一个切面拦截自己的日志实现类的方法,当然,这个类需要被Spring管理,并且不能定义为null。在SpringBoot2.x中,默认使用cglib实现代理功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值