Java 日志框架

什么是日志?日志有什么作用?

日志是开发者为了方便找错,或着为了更高的开发效率而做的一个日志输出!

最简单的输出就是

System.out.println("hello word");

当然在实际开发过程中,日志不可能会这样简单的,真正的生产环境下日志如下 :

2020-09-09 10:37:45 - [INFO]-[com.pangugle.modules.boot.all.Bootstrap] - Starting Bootstrap v1.0.0 on prod with PID 1 (/srv/websites/code/lib/myjar.jar started by root in /)

它的组成部分是

[时间] - [日志输出等级] - [日志输出所有的类] - [日志输出信息]

其中日志级别有以下几种
1. debug 一般用于调试
2. info  仅是一些基本的信息说明而已
3. warn 警示的信息,可能有问题,但是还不至於影响到系统
4. error 一些重大的错误信息, 可能会影响某个服务或系统

Java 日志有哪些

Java 日志框架有很多, 主要有以下几个

  • SLF4j (推荐)
  • JDK 日志
  • commons-loggin
  • log4j

在这么多的日志框架里,SLF4j 是最常用的,性能还是不错的,故而推荐使用此日志框架!

但是在实际开发过程中,每个人选择的日志框架都不一样,所以很容易造成日志框架无法使用等 一系列的情况,盘古歌技术在开始接触的过程也是遇到的好多问题,后面选择了 SLF4j! 并利用 适配器模式 封装了日志框架!

引入jar包

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>empty-delete</version>
    <scope>system</scope>
  <systemPath>${lib.dir}/local/empty.jar</systemPath>
</dependency>

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <scope>system</scope>
  <systemPath>${lib.dir}/local/empty.jar</systemPath>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-nop</artifactId>
    <scope>system</scope>
  <systemPath>${lib.dir}/local/empty.jar</systemPath>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <scope>system</scope>
  <systemPath>${lib.dir}/local/empty.jar</systemPath>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <scope>system</scope>
  <systemPath>${lib.dir}/local/empty.jar</systemPath>
</dependency>

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>empty-delete</version>
    <scope>system</scope>
  <systemPath>${lib.dir}/local/empty.jar</systemPath>
</dependency>

注意上面所有的emtpy.jar 都是为了替换其它的日志jar,防止包冲突异常, empty.jar 就是一个空的,什么都没有,你们自己搞一个!

  • 定义日志接口
public interface Log{

    public void trace(String msg);
    public void trace(String msg, Throwable e);

	public void debug(String msg);
	public void debug(String msg, Throwable e);

	public void info(String msg);
	public void info(String msg, Throwable e);

	public void warn(String msg);
	public void warn(String msg, Throwable e);

	public void error(String msg);
	public void error(String msg, Throwable e);

	/**
	 * error + notice by email
	 * @param msg
	 */
	public void alarm(String msg);
	public void alarm(String msg, Throwable e);

}
  • 定义日志创建适配器
public interface LogAdapter {
	/**
	 * 获取日志输出器
	 *
	 * @param key 分类键
	 * @return 日志输出器, 后验条件: 不返回null.
	 */
	Log getLog(Class<?> key);

	/**
	 * 获取日志输出器
	 *
	 * @param key 分类键
	 * @return 日志输出器, 后验条件: 不返回null.
	 */
	Log getLog(String key);
}
  • SLF4j 日志实现
public class Slf4jLog implements Log, java.io.Serializable{

	private static final long serialVersionUID = 1L;
	private final org.slf4j.Logger logger;

	public Slf4jLog(org.slf4j.Logger logger) {
		this.logger = logger;
	}

	public void trace(String msg) {
		logger.trace(msg);
	}

	public void trace(String msg, Throwable e) {
		logger.trace(msg, e);
	}

	public void debug(String msg) {
		logger.debug(msg);
	}


	public void debug(String msg, Throwable e) {
		logger.debug(msg, e);
	}

	public void info(String msg) {
		logger.info(msg);
	}

	public void info(String msg, Throwable e) {
		logger.info(msg, e);
	}

	public void warn(String msg) {
		logger.warn(msg);
	}

	public void warn(String msg, Throwable e) {
		logger.warn(msg, e);
	}

	public void error(String msg) {
		logger.error(msg);
	}

	public void error(String msg, Throwable e) {
		logger.error(msg, e);
	}

	public void alarm(String msg)
	{
		error(msg);
	}
	public void alarm(String msg, Throwable e)
	{
		error(msg, e);
	}
}

/**
* 适配器
*/
public class Slf4jAdapter implements LogAdapter{
	public Log getLog(Class<?> key) {
		return new Slf4jLog(org.slf4j.LoggerFactory.getLogger(key));
	}

	public Log getLog(String key) {
		return new Slf4jLog(org.slf4j.LoggerFactory.getLogger(key));
	}
}
  • JDK 日志实现
public class JdkLog implements Log,java.io.Serializable {

	private static final long serialVersionUID = 1L;
	private final java.util.logging.Logger logger;

	public JdkLog(java.util.logging.Logger logger) {
		this.logger = logger;
	}

	public void trace(String msg) {
		logger.log(Level.FINER, msg);
	}

	public void trace(Throwable e) {
		logger.log(Level.FINER, e.getMessage(), e);
	}

	public void trace(String msg, Throwable e) {
		logger.log(Level.FINER, msg, e);
	}

	public void debug(String msg) {
		logger.log(Level.FINE, msg);
	}

	public void debug(Throwable e) {
		logger.log(Level.FINE, e.getMessage(), e);
	}

	public void debug(String msg, Throwable e) {
		logger.log(Level.FINE, msg, e);
	}

	public void info(String msg) {
		logger.log(Level.INFO, msg);
	}

	public void info(String msg, Throwable e) {
		logger.log(Level.INFO, msg, e);
	}

	public void warn(String msg) {
		logger.log(Level.WARNING, msg);
	}

	public void warn(String msg, Throwable e) {
		logger.log(Level.WARNING, msg, e);
	}

	public void error(String msg) {
		logger.log(Level.SEVERE, msg);
	}

	public void error(String msg, Throwable e) {
		logger.log(Level.SEVERE, msg, e);
	}

	public void error(Throwable e) {
		logger.log(Level.SEVERE, e.getMessage(), e);
	}

	public void info(Throwable e) {
		logger.log(Level.INFO, e.getMessage(), e);
	}

	public void alarm(String msg)
	{
		error(msg);
	}
	public void alarm(String msg, Throwable e)
	{
		error(msg, e);
	}
}


/**
 适配器
*/
public class JdkAdapter implements LogAdapter{
	public Log getLog(Class<?> key) {
		return new JdkLog(java.util.logging.Logger.getLogger(key == null ? "" : key.getName()));
	}

	public Log getLog(String key) {
		return new JdkLog(java.util.logging.Logger.getLogger(key));
	}
}
  • JCL 日志实现
public class JclLog implements Log, java.io.Serializable{

	private static final long serialVersionUID = 1L;

	private final org.apache.commons.logging.Log logger;

	public JclLog(org.apache.commons.logging.Log logger) {
		this.logger = logger;
	}

    public void trace(String msg) {
        logger.trace(msg);
    }

    public void trace(String msg, Throwable e) {
        logger.trace(msg, e);
    }

	public void debug(String msg) {
		logger.debug(msg);
	}

	public void debug(String msg, Throwable e) {
		logger.debug(msg, e);
	}

	public void info(String msg) {
		logger.info(msg);
	}

	public void info(String msg, Throwable e) {
		logger.info(msg, e);
	}

	public void warn(String msg) {
		logger.warn(msg);
	}

	public void warn(String msg, Throwable e) {
		logger.warn(msg, e);
	}

	public void error(String msg) {
		logger.error(msg);
	}

	public void error(String msg, Throwable e) {
		logger.error(msg, e);
	}

	public void alarm(String msg)
	{
		error(msg);
	}
	public void alarm(String msg, Throwable e)
	{
		error(msg, e);
	}

}


/**
 适配器
*/
public class JclAdapter implements LogAdapter{
	public Log getLog(String key) {
		return new JclLog(LogFactory.getLog(key));
	}
    public Log getLog(Class<?> key) {
        return new JclLog(LogFactory.getLog(key));
    }
}

日志接口和实现类我们都实现好了,但是为了安全起见,也就是说在输出的时候发生错误,给 它 try-catch 下, 相当于一个包装器,它也是实现了Log 接口

public class FailsafeLogger implements Log{

	private Log logger;

	public FailsafeLogger(Log logger) {
		this.logger = logger;
	}

	private String appendContextMessage(String msg) {
	    return msg;
//	    return "[bootstrap=" + MyAppContext.getLogPrefix() + "] " + msg;
	}

	@Override
	public void trace(String msg) {
		try {
			logger.trace(msg);
		} catch (Throwable e) {
		}
	}

    public void trace(String msg, Throwable e) {
        try {
            logger.trace(appendContextMessage(msg), e);
        } catch (Throwable t) {
        }
    }

	public void debug(String msg, Throwable e) {
		try {
			logger.debug(appendContextMessage(msg), e);
		} catch (Throwable t) {
		}
	}


	public void debug(String msg) {
		try {
			logger.debug(appendContextMessage(msg));
		} catch (Throwable t) {
		}
	}

	public void info(String msg, Throwable e) {
		try {
			logger.info(appendContextMessage(msg), e);
		} catch (Throwable t) {
		}
	}

	public void info(String msg) {
		try {
			logger.info(appendContextMessage(msg));
		} catch (Throwable t) {
		}
	}

	public void warn(String msg, Throwable e) {
		try {
			logger.warn(appendContextMessage(msg), e);
		} catch (Throwable t) {
		}
	}

	public void warn(String msg) {
		try {
			logger.warn(appendContextMessage(msg));
		} catch (Throwable t) {
		}
	}

	public void error(String msg, Throwable e) {
		try {
			logger.error(appendContextMessage(msg), e);
		} catch (Throwable t) {
		}
	}

	public void error(String msg) {
		try {
			logger.error(appendContextMessage(msg));
		} catch (Throwable t) {
		}
	}

	public void alarm(String msg)
	{
		error(msg);
	}
	public void alarm(String msg, Throwable e)
	{
		error(msg, e);
	}
}

还有一个日志工厂类

public final class LogFactory {

	private LogFactory() {
	}

	private static volatile LogAdapter mLogAdapter;

	private static final ConcurrentMap<String, Log> mLogs = new ConcurrentHashMap<String, Log>();

	// 查找常用的日志框架
	static {
	    try {
	    	init();
			String logger = System.getProperty("pangugle.application.log", "slf4j");
			if ("slf4j".equals(logger)) {
				setLogAdapter(new Slf4jAdapter());
			} else if ("jcl".equals(logger)) {
				setLogAdapter(new JclAdapter());
			}
//			else if ("log4j".equals(logger)) {
//				setLogAdapter(new Log4jAdapter());
//			}
			else if ("jdk".equals(logger)) {
				setLogAdapter(new JdkAdapter());
			}
		} catch (Exception e) {
			try {
    			setLogAdapter(new Slf4jAdapter());
            } catch (Throwable e1) {
                try {
                	setLogAdapter(new JclAdapter());
                } catch (Throwable e2) {
//                    try {
//                    	setLogAdapter(new Log4jAdapter());
//                    } catch (Throwable e3) {
                    	setLogAdapter(new JdkAdapter());
//                    }
                }
            }
		}
	}

	public static void setLogAdapter(LogAdapter adapter) {
		mLogAdapter = adapter;
	}

	/**
	 * 获取日志输出器
	 *
	 * @param key
	 *            分类键
	 * @return 日志输出器, 后验条件: 不返回null.
	 */
	public static Log getLog(Class<?> key) {
		Log log = mLogs.get(key.getName());
		if (log == null) {
			log = new FailsafeLogger(mLogAdapter.getLog(key));
			mLogs.putIfAbsent(key.getName(), log);
		}
		return log;
	}

	private static String getEnv()
	{
		String env = System.getProperty("env");
		return StringUtils.isEmpty(env) ? "dev" : env;
	}

	public static void init()
	{
		try {
			String env = getEnv();
			String filename = "logback-" + env + ".xml";

			ResourceLoader resourceLoader = new DefaultResourceLoader();
			InputStream is = resourceLoader.getResource("classpath:config/" + filename).getInputStream();

			loadLogback(is);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private static void loadLogback(InputStream inputStream)
	{
		try {
			if(inputStream == null)
			{
				throw new RuntimeException("logback config is not exist for ");
			} else
			{
				LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
				JoranConfigurator configurator = new JoranConfigurator();
				configurator.setContext(context);
				context.reset();
				configurator.doConfigure(inputStream);
				StatusPrinter.printInCaseOfErrorsOrWarnings(context);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

由于我使用是slf4j,所以就只引入slf4j的配置

开发环境配置文件 :logback-dev.xml

<?xml version="1.0" encoding="UTF-8"?>

<configuration scan="false">

    <property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss} - [%level] - [%logger] - %msg%n"/>

    <!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
    <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
        <encoder><pattern>${pattern}</pattern></encoder>
    </appender>

    <logger name="springfox" level="error" additivity="false"/>
    <logger name="org" level="error" additivity="false"/>
    <logger name="freemarker.cache" level="error" additivity="false"/>
    <logger name="reactor" level="error" additivity="false"/>
    <logger name="ch" level="error" additivity="false"/>
    <logger name="us.codecraft.webmagic" level="error" additivity="false"/>
    <logger name="io.netty" level="error" additivity="false"/>
    <logger name="com.corundumstudio" level="error" additivity="false"/>
    <logger name="io.undertow" level="error" additivity="false"/>
    <logger name="org.apache" level="debug" additivity="false"/>
    <logger name="com.alibaba" level="error" additivity="false"/>
    <logger name="com.aliyun" level="error" additivity="false"/>
    <logger name="com.chenlb" level="error" additivity="false"/>
    <logger name="com.atomikos" level="error" additivity="false"/>

    <root level="debug">
        <appender-ref ref="consoleAppender"/>
    </root>
</configuration>

生产环境配置文件 : logback-prod.xml

<?xml version="1.0" encoding="UTF-8"?>

<!--
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->

<configuration scan="false">
    <!-- 定义日志的根目录 -->
    <property name="LOG_HOME" value="/var/log/websites" />
    <!-- 定义日志文件名称 -->
    <property name="appName" value="log"/>

    <property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss} - [%level]-[%logger] - %msg%n"/>

    <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->  
    <appender name="fileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
    	<file>${LOG_HOME}/${appName}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/${appName}.log-%d{yyyy-MM-dd}-%i</fileNamePattern>
            <MaxHistory>10</MaxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder>
	    	<pattern>${pattern}</pattern>
	  	</encoder>
    </appender>

	<logger name="springfox" level="error" additivity="false"/>
	<logger name="reactor" level="error" additivity="false"/>
   	<logger name="org" level="error" additivity="false"/>
   	<logger name="ch" level="error" additivity="false"/>
   	<logger name="us.codecraft.webmagic" level="error" additivity="false"/>
   	<logger name="io.netty" level="error" additivity="false"/>
   	<logger name="com.corundumstudio" level="error" additivity="false"/>
   	<logger name="io.undertow" level="error" additivity="false"/>
   	<logger name="org.apache" level="error" additivity="false"/>
    <logger name="com.alibaba" level="warn" additivity="false"/>
    <logger name="com.chenlb" level="error" additivity="false"/>
    <logger name="com.atomikos" level="error" additivity="false"/>

    <root level="INFO">
        <appender-ref ref="fileAppender"/>
    </root>
</configuration>

最后就是使用了

public class Test
{

  public static Log LOG = LogFactory.getLog(Test.class);

  public void testLog()
  {
    LOG.info("===============");

		try {
			// =================
			int i = 3 / 0;
		} catch(Exception e)
		{
			LOG.error("error:", e);
		}
  }

}

运行结果为

2020-09-09 19:32:19 - [INFO] - [com.pangugle.framework.reflect.Test] - ===============
2020-09-09 19:32:19 - [ERROR] - [com.pangugle.framework.reflect.Test] - error:
java.lang.ArithmeticException: / by zero
	at com.pangugle.framework.reflect.Test.test(Test.java:38)
	at com.pangugle.framework.reflect.Test.main(Test.java:48)