2017-08-04-log4j-自定义日志器

我想念泉州的夏天了

自定义日志插件

  • 日志是为了什么?
    • 调试时可以使用,便于开发。生产时可以关闭。
    • 定位错误,记录日志,便于出错时找错。
    • 可以记录用户做了哪些操作,用户的执行路线。追溯错误原因。
    • … 当然还有其它作用
  • Log4j这么复杂?为什么呢?他提供了哪些功能
    • 将日志输出到不同的终端
    • 日志可以有指定的格式
    • 可配置日志参数

为什么我们不能简单一点?如果让我们从简单开始开发一个日志插件呢?

测试1

MyLog1


package mylog;

public class MyLog1 {

	public static void debug(String msg) {
		System.out.println("[debug]" + msg);
	}

	public static void info(String msg) {
		System.out.println("[info]" + msg);
	}

	public static void warn(String msg) {
		System.out.println("[warn]" + msg);
	}

	public static void error(String msg) {
		System.out.println("[error]" + msg);
	}

	public static void fatal(String msg) {
		System.out.println("[fatal]" + msg);
	}

}

测试

测试类


package mylog.logTest;

import mylog.MyLog1;

public class MyLogTest {

	public static void main(String[] args) {
		method1();
		method2();
	}

	public static void method1() {
		MyLog1.debug("method1开始");
		// ... 代码块
		MyLog1.info("method1执行了...操作");
		// ... 代码块
		MyLog1.debug("method1结束");
	}

	public static void method2() {
		MyLog1.debug("method2开始");
		// ... 代码块
		MyLog1.info("method2执行了...操作");
		// ... 代码块
		MyLog1.debug("method2结束");
	}
}

测试结果

[debug]method1开始
[info]method1执行了...操作
[debug]method1结束
[debug]method2开始
[info]method2执行了...操作
[debug]method2结束

思考了

这个代码有啥问题?

  • 这个日志只能向控制台输出,这肯定是不合理的。
    • 在调试的时候,我们可以向控制台输出日志,但是当产品投入实际应用,哪里来的控制台让你输出,而且用户发现出错了,你是根据用户提供的信息到日志文件中去找错,所以可能需要保存到文件中或者数据库中。
  • 不能控制等级/控制输出范围
    • 有时候只想看error(错误)的信息,而不想看debug(调试)的信息。程序中,输出debug信息通常多于info,info多余warn,以此类推。这样信息量太大不好查找。
    • 在调试一个大型的实际项目中,我想看看某一操作通过ibatis后真正执行的sql是什么,所以在log4j日志中设置了debug,结果,控制台打印日志根本停不下来,唰唰唰的过去,根本看不到自己想要的信息,该大型项目日志打太多了。该情况下我仅想看到debug信息,info,warn等信息对我就是多余的了。但其实debug信息,也太多了,因为其他开发人员所调用的sql也会打印出来,我又想从其中过滤出属于我自己的日志,比如我的日志我都会在日志开头加上lxylog,这样我就能过滤自己的日志了 。
    • 记得在调试手机端H5页面的js的时候,用自己写的js日志器,将日志输出内容使用alert()展示,查看变量的值,但是,一旦开启了alert()的方式,那么所有的日志都会通过alert()的方式来展示,而我又只想只在某个模块产生这种效果,其它模块依然不要使用alert()的方式,希望实现这种局部控制。
  • 日志格式
    • 其实这个问题也还好,如果你对日志的格式固定,那么就不算个问题,一开始写好就行了。

测试2

针对上面问题,我们进行解决

  • 问题:只能向控制台输出
    • 可能需要同时像控制台输出,向文件中输出,数据库中…
  • 问题:日志的等级问题
    • 设置哪几个等级,等级是否是枚举类?还是普通类,它其中是否需要什么方法? 暂时不知,就弄为枚举类
  • 问题: 日志格式
    • 也做一个可配置的日志

所以要写一个,能更向控制台和文件两个地方输出,拥有日志等级,可配置的日志插件

结构如下:

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log2packet.png

类图:

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log2classdiagram.png

  • 1个Layout接口
  • 1个Layout接口的实现类 SimpleLayout
  • 1个日志等级枚举类
  • MyLogTest是测试类
  • 1个接口Output定义了输出方法
  • 2个Output接口的实现类,
    • OutputToConsole向控制台输出,
    • OutputToFile向指定文件输出
  • 1个异常类,OutputException用于抛异常
  • MyLog2 是日志类。

Layout接口


package mylog2.layout;

/**
 * 布局接口,仅定义一个方法,传入日志等级以及日志内容
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public interface Layout {

	String parse(String message, String logLevel);
}

SimpleLayout


package mylog2.layout;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 简单的日志格式过滤,替换指定的字符
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public class SimpleLayout implements Layout {

	private String format;

	public String getFormat() {
		return format;
	}

	public void setFormat(String format) {
		this.format = format;
	}

	// -d- : 替换为日期
	// -t- : 替换为时间
	// -l- : 替换为logLevel
	// -m- : 替换为日志内容
	@Override
	public String parse(String msg, String logLevel) {
		Date date = new Date();
		String filterMsg = this.format;
		if (format.contains("-d-")) {
			SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
			filterMsg = filterMsg.replaceAll("-d-", "[" + simpleDateFormat.format(date) + "] ");
		}
		if (format.contains("-t-")) {
			SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
			filterMsg = filterMsg.replaceAll("-t-", "[" + simpleDateFormat.format(date) + "] ");
		}
		if (format.contains("-l-")) {
			filterMsg = filterMsg.replaceAll("-l-", "[" + logLevel + "] ");
		}
		if (format.contains("-m-")) {
			filterMsg = filterMsg.replaceAll("-m-", msg + " ");
		}

		return filterMsg;
	}
}


LogLevel


package mylog2.level;

/**
 * 枚举类,有哪些日志等级
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public enum LogLevel {
	debug, info, warn, error, fatal
}

Output接口


package mylog2.output;

/**
 * 日志器输出接口,具体实现输出到不同的终端。
 *  
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public interface Output {
	void ouptput(String msg) throws OutputException;
}

OutputToConsole


package mylog2.output;

/**
 * 实现Output接口,输出到控制台
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public class OutputToConsole implements Output {

	/**
	 * 输出到控制台
	 */
	@Override
	public void ouptput(String msg) {
		System.out.println(msg);
	}
}

OutputToFile 向文件输出


package mylog2.output;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

/**
 * 实现Output接口,输出到指定文件中。
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public class OutputToFile implements Output {

	private File outputFile;
	private FileWriter fw;

	/**
	 * 初始化
	 *
	 * @param pathName 日志输出到文件的文件名。
	 * @throws OutputException 创建过程中出现错误,则抛出
	 */
	public OutputToFile(String pathName) throws OutputException {
		outputFile = new File(pathName);
		try {
			if (!outputFile.exists()) {
				outputFile.createNewFile();
			}
			fw = new FileWriter(outputFile);
		} catch (IOException e) {
			throw new OutputException(e.getMessage());
		}
	}

	/**
	 * 输出到日志文件中。
	 *
	 * @throws OutputException 如果日志文件不存在或者写入文件异常。
	 */
	@Override
	public void ouptput(String msg) throws OutputException {
		if (null == fw) {
			throw new OutputException("日记记录文件不存在!");
		}
		try {
			fw.write(msg + "\n");
			fw.flush();
		} catch (IOException e) {
			new OutputException(e.getMessage());
		}
	}
}

异常类


package mylog2.output;
/**
 * 输出异常
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public class OutputException extends Exception {

	public OutputException() {
		super();
	}

	public OutputException(String message, Throwable cause) {
		super(message, cause);
	}

	public OutputException(String message) {
		super(message);
	}

	public OutputException(Throwable cause) {
		super(cause);
	}

}

MyLog2 日志类

package mylog2;

import java.util.ArrayList;
import java.util.List;

import mylog2.layout.Layout;
import mylog2.level.LogLevel;
import mylog2.output.Output;

/**
 * 日志器
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public class MyLog2 {

	// 默认日志等级为debug
	private static LogLevel logLevel = LogLevel.debug;

	// 日志输出终端,因为可能多个
	private static List<Output> outputs = new ArrayList<Output>();

	// 日志样式
	private static Layout layout;

	/**
	 * 设置日志样式
	 *
	 * @param layout
	 */
	public static void setLayout(Layout layout) {
		MyLog2.layout = layout;
	}

	/**
	 * 添加输入终端
	 *
	 * @param output
	 */
	public static void addOutput(Output output) {
		outputs.add(output);
	}

	/**
	 * 设置日志等级
	 *
	 * @param logLevel
	 */
	public static void setLogLevel(LogLevel logLevel) {
		MyLog2.logLevel = logLevel;
	}

	/**
	 * 根据用户设置的日志等级以及该日志的等级,判断该日志是否需要被记录。
	 *
	 * @param level
	 * @return
	 */
	private static boolean canLog(LogLevel level) {
		if (logLevel.equals(LogLevel.debug)) {
			return true;
		} else if (logLevel.equals(LogLevel.info)) {
			if (level.equals(LogLevel.info)
				|| level.equals(LogLevel.warn)
				|| level.equals(LogLevel.error)
				|| level.equals(LogLevel.fatal)) {
				return true;
			}
		} else if (logLevel.equals(LogLevel.warn)) {
			if (level.equals(LogLevel.warn)
				|| level.equals(LogLevel.error)
				|| level.equals(LogLevel.fatal)) {
				return true;
			}
		} else if (logLevel.equals(LogLevel.error)) {
			if (level.equals(LogLevel.error)
				|| level.equals(LogLevel.fatal)) {
				return true;
			}
		} else if (logLevel.equals(LogLevel.fatal)) {
			if (level.equals(LogLevel.fatal)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * 遍历终端,进行输出
	 *
	 * @param msg
	 * @throws Exception
	 */
	private static void toOutput(String msg) throws Exception {
		for (Output o : outputs) {
			o.ouptput(msg);
		}
	}

	public static void debug(String msg) throws Exception {
		if (canLog(LogLevel.debug)) {
			toOutput(layout.parse(msg, "debug"));
		}
	}

	public static void info(String msg) throws Exception {
		if (canLog(LogLevel.info)) {
			toOutput(layout.parse(msg, "info"));
		}
	}

	public static void warn(String msg) throws Exception {
		if (canLog(LogLevel.warn)) {
			toOutput(layout.parse(msg, "warn"));
		}
	}

	public static void error(String msg) throws Exception {
		if (canLog(LogLevel.error)) {
			toOutput(layout.parse(msg, "error"));
		}
	}

	public static void fatal(String msg) throws Exception {
		if (canLog(LogLevel.fatal)) {
			toOutput(layout.parse(msg, "fatal"));
		}
	}
}

测试

测试类


package mylog2.logtset;

import mylog2.MyLog2;
import mylog2.layout.SimpleLayout;
import mylog2.level.LogLevel;
import mylog2.output.OutputToConsole;
import mylog2.output.OutputToFile;

public class MyLogTest {

	public static void main(String[] args) throws Exception {
		// 日志等级为info,说明debug的日志不会被打印出来
		MyLog2.setLogLevel(LogLevel.info);

		// 输出目标有控制台和一个本地文本
		MyLog2.addOutput(new OutputToConsole());
		MyLog2.addOutput(new OutputToFile("D:\\logs\\logtset2.txt"));

		// 设置格式
		SimpleLayout layout = new SimpleLayout();
		layout.setFormat("-d- -t- -l- -m-");
		MyLog2.setLayout(layout);

		method1();
		method2();
	}

	public static void method1() throws Exception {
		MyLog2.debug("a");
		MyLog2.info("b");
		MyLog2.warn("c");
		MyLog2.error("d");
		MyLog2.fatal("e");
	}

	public static void method2() throws Exception {
		MyLog2.debug("1");
		MyLog2.info("2");
		MyLog2.warn("3");
		MyLog2.error("4");
		MyLog2.fatal("5");
	}
}

测试结果

控制台的输出:

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log2result1.png

文本中的输出

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log2result2.png

思考了

当前MyLog2也比较简单。

  • 问题:只能向控制台输出

可能需要同时像控制台输出,向文件中输出,数据库中…

这个问题算是解决了,如果需要输出到别的地方,只需新增那个类即可。

  • 问题:日志的等级问题

这个问题, 日志等级暂时够用了。

  • 问题: 日志格式

暂时只有一个简单的日志器输出格式,看不出啥问题。

感觉又不妙了。

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log2classdiagram.png

我们现在只有一个日志测试类。如果我们有A,B两个类。

  • A类用SimpleLayout输出info以上的日志到控制台
  • B类用其它新建的Laout输出warn以上的日志到文本中

那么当前就不能满足这个需求了。需要再进行改造。将MyLog2变成可实例化的类即可。

改造成MyLog2V2

类图结构

类图

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log2v2classdiagram.png

结构

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log2v2diagram.png

新增的XMlLayout


package mylog2.layout;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 简单的XMLLayout类。
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-11-29
 */
public class XMLLayout implements Layout {

	// -d- : 替换为日期
	// -t- : 替换为时间
	// -l- : 替换为logLevel
	// -m- : 替换为日志内容
	@Override
	public String parse(String message, String logLevel) {

		StringBuilder sb = new StringBuilder();
		sb.append("<log>");
		sb.append("<date>-d-</date>");
		sb.append("<time>-t-</time>");
		sb.append("<logLevel>-l-</logLevel>");
		sb.append("<content>-m-</content>");
		sb.append("</log>");

		Date date = new Date();
		String result = sb.toString();
		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
		result = result.replaceAll("-d-", simpleDateFormat.format(date));
		simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
		result = result.replaceAll("-t-", simpleDateFormat.format(date));
		result = result.replaceAll("-l-", logLevel);
		result = result.replaceAll("-m-", message);
		return result;
	}
}


MyLog2V2

将其中的静态属性方法都改造成非静态,从而可以实例化多个日志器,多个日志器可以拥有不同的输出格式和输出终端。


package mylog2;

import java.util.ArrayList;
import java.util.List;

import mylog2.layout.Layout;
import mylog2.level.LogLevel;
import mylog2.output.Output;

/**
 * 日志器
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public class MyLog2V2 {

	// 默认日志等级为debug
	private LogLevel logLevel = LogLevel.debug;

	// 日志输出终端,因为可能多个
	private List<Output> outputs = new ArrayList<Output>();

	// 日志样式
	private Layout layout;

	/**
	 * 设置日志样式
	 *
	 * @param layout
	 */
	public void setLayout(Layout layout) {
		this.layout = layout;
	}

	/**
	 * 添加输入终端
	 *
	 * @param output
	 */
	public void addOutput(Output output) {
		outputs.add(output);
	}

	/**
	 * 设置日志等级
	 *
	 * @param logLevel
	 */
	public void setLogLevel(LogLevel logLevel) {
		this.logLevel = logLevel;
	}

	/**
	 * 根据用户设置的日志等级以及该日志的等级,判断该日志是否需要被记录。
	 *
	 * @param level
	 * @return
	 */
	private boolean canLog(LogLevel level) {
		if (logLevel.equals(LogLevel.debug)) {
			return true;
		} else if (logLevel.equals(LogLevel.info)) {
			if (level.equals(LogLevel.info)
				|| level.equals(LogLevel.warn)
				|| level.equals(LogLevel.error)
				|| level.equals(LogLevel.fatal)) {
				return true;
			}
		} else if (logLevel.equals(LogLevel.warn)) {
			if (level.equals(LogLevel.warn)
				|| level.equals(LogLevel.error)
				|| level.equals(LogLevel.fatal)) {
				return true;
			}
		} else if (logLevel.equals(LogLevel.error)) {
			if (level.equals(LogLevel.error)
				|| level.equals(LogLevel.fatal)) {
				return true;
			}
		} else if (logLevel.equals(LogLevel.fatal)) {
			if (level.equals(LogLevel.fatal)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * 遍历终端,进行输出
	 *
	 * @param msg
	 * @throws Exception
	 */
	private void toOutput(String msg) throws Exception {
		for (Output o : outputs) {
			o.ouptput(msg);
		}
	}

	public void debug(String msg) throws Exception {
		if (canLog(LogLevel.debug)) {
			toOutput(layout.parse(msg, "debug"));
		}
	}

	public void info(String msg) throws Exception {
		if (canLog(LogLevel.info)) {
			toOutput(layout.parse(msg, "info"));
		}
	}

	public void warn(String msg) throws Exception {
		if (canLog(LogLevel.warn)) {
			toOutput(layout.parse(msg, "warn"));
		}
	}

	public void error(String msg) throws Exception {
		if (canLog(LogLevel.error)) {
			toOutput(layout.parse(msg, "error"));
		}
	}

	public void fatal(String msg) throws Exception {
		if (canLog(LogLevel.fatal)) {
			toOutput(layout.parse(msg, "fatal"));
		}
	}
}


测试

测试类


package mylog2.logtset;

import mylog2.MyLog2V2;
import mylog2.layout.SimpleLayout;
import mylog2.layout.XMLLayout;
import mylog2.level.LogLevel;
import mylog2.output.OutputToConsole;
import mylog2.output.OutputToFile;

/**
 * 这个测试类,新建log1和log2两个对象,模拟不同类中拥有不同的日志器。
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-11-29
 */
public class MyLogTestV2 {

	public static void main(String[] args) throws Exception {
		MyLog2V2 log1 = new MyLog2V2();
		MyLog2V2 log2 = new MyLog2V2();

		// A类用SimpleLayout输出info以上的日志到控制台
		SimpleLayout layout1 = new SimpleLayout();
		layout1.setFormat("-d- -t- -l- -m-");
		log1.setLayout(layout1);
		log1.setLogLevel(LogLevel.info);
		log1.addOutput(new OutputToConsole());

		// B类用其它新建的Laout输出warn以上的日志到文本中
		XMLLayout layout2 = new XMLLayout();
		log2.setLayout(layout2);
		log2.setLogLevel(LogLevel.warn);
		log2.addOutput(new OutputToFile("d:/logs/a.xml"));


		log1.debug("a");
		log1.info("b");
		log1.warn("c");
		log1.error("d");
		log1.fatal("e");

		log2.debug("1");
		log2.info("2");
		log2.warn("3");
		log2.error("4");
		log2.fatal("5");
	}
}


测试结果

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log2v2result1.png

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log2v2result2.png

思考了

通过把MyLog2改造成MyLog2V2,将其中的静态属性/方法改成非静态的,然后实例多个MyLog2V2,现在能够满足下面的需求了。

我们现在只有一个日志测试类。如果我们有A,B两个类。

  • A类用SimpleLayout输出info以上的日志到控制台
  • B类用其它新建的Laout输出warn以上的日志到文本中

那如果现在又有了这样的问题。

  • A类用SimpleLayout输出warn以上的日志到控制台–控制台中不需要看太多debug的信息,我需要看warn,error,fatal这些对我的系统有影响有用的东西
  • A类用SimpleLayout输出debug以上的日志到文本中–文本中,我需要记录详细信息,用于查看时清楚每一步都做了什么。

看了这个问题,感觉和前面那个问题很像,总的来说,我们的问题就在于(什么)类用(什么)Layout输出(什么)日志等级到(什么)终端

这个问题,我们发现同一个类中,需要用同样的格式,输出不同的日志等级到不同的终端。是否需要在一个类中新建两个日志器?那如果A类需要输出多个不同等级的日志到不同的终端,那不是也需要新建多个日志器了?

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log2v2diagram.png

根据现在的结构,一个日志器Mylog2V2,只能有一个Layout,所以如果A类需要输出多个不同等级的日志到不同的终端,势必要新建多个日志器。但一个类中,新建那么多个日志器,意味着什么?

伪代码,意味着一大堆的日志代码= =。。谁会用这么麻烦的日志器,


	class A {
		MyLog2V2 logToConsole;
		MyLog2V2 logToFile;
		MyLog2V2 logToOther;
		... 再多来几个终端,就会有更多的日志器。。

		public void method1() {
			logToConsole.debug("method1 begin");
			logToFile.debug("method1 begin");
			logToOther.debug("method1 begin");

			...

			logToConsole.debug("do something");
			logToFile.debug("do something");
			logToOther.debug("do something");

			...

			logToOther.debug("method1 end");
			logToFile.debug("method1 end");
			logToConsole.debug("method1 end");
		}
	}

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log2v2diagram.png

再看看类图,我们是否有改造之处, 一个日志器,有一个LogLevel,一个Layout,多个Output。这个局限就在,一个日志器,虽然有多个Output,但是只能用同一种Layout输出同一个logLevel以上的日志。 那么我们需要改造,让每个Output拥有自己的Layout和自己的LogLevel。这样我们就能只声明一个日志器,该日志器中的每个Output进行单独的控制。

测试3

类图 结构

结构

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log3.png

MyLog3做了一些变动。

  • 新建了一个LogEvent,用来封装日志内容以及日志等级。
  • 修改Output中的 output(String msg) 为 output(LogEvent event)
  • 新增了抽象类AbstractOutput,定义了日志格式和日志等级。

类图

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log3classdiagram.png

LogEvent


package mylog3.event;

import mylog3.level.LogLevel;

/**
 * 用于传播日志的日志事件
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-11-29
 */
public class LogEvent {

	private String message;
	private LogLevel level;

	public LogEvent() {
		super();
	}
	public LogEvent(String message, LogLevel level) {
		super();
		this.message = message;
		this.level = level;
	}

	public String getMessage() {
		return message;
	}
	public void setMessage(String message) {
		this.message = message;
	}
	public LogLevel getLevel() {
		return level;
	}
	public void setLevel(LogLevel level) {
		this.level = level;
	}
}

LogLevel

新增了一个获取枚举类型对应值的方法。


package mylog3.level;


/**
 * 枚举类,有哪些日志等级
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public enum LogLevel {
	debug, info, warn, error, fatal;

	/**
	 * 该静态方法用户获取枚举类型对应的数值,用于比对大小。
	 * debug=1
	 * info=2
	 * ...
	 * fatal = 5
	 * @param level
	 * @return
	 */
	public static int getValue(LogLevel level) {
		int value = 1;
		switch (level) {
			case info :
				value = 2;
				break;
			case warn :
				value = 3;
				break;
			case error :
				value = 4;
				break;
			case fatal :
				value = 5;
				break;
			default :
				value = -1;
		}
		return value;
	}
}


Output

修改 output(String msg) 为 output(LogEvent event)


package mylog3.output;

import mylog3.event.LogEvent;

/**
 * 日志器输出接口,具体实现输出到不同的终端。
 *  
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public interface Output {
	void ouptput(LogEvent event) throws OutputException;
}


AbstractOutput

定义了抽象的Outupt类,定义了Layout和LogLevel,还有一个用于根据该日志器设置等输出日志等级和传入日志的日志等级判断是否需要打印的方法。


package mylog3.output;

import mylog3.layout.Layout;
import mylog3.level.LogLevel;

/**
 * 抽象Output,定义了LayoutLogLevel
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-11-29
 */
public abstract class AbstractOutput implements Output {

	private LogLevel logLevel;
	private Layout layout;

	public LogLevel getLogLevel() {
		return logLevel;
	}

	public void setLogLevel(LogLevel logLevel) {
		this.logLevel = logLevel;
	}

	public Layout getLayout() {
		return layout;
	}

	public void setLayout(Layout layout) {
		this.layout = layout;
	}

	/**
	 * 根据用户设置的日志等级以及该日志的等级,判断该日志是否需要被记录。
	 *
	 * @param level
	 * @return
	 */
	public boolean canLog(LogLevel level) {
		final int logLevelValue = LogLevel.getValue(level);
		final int OutputLogLevelValue = LogLevel.getValue(getLogLevel());
		if (OutputLogLevelValue <= logLevelValue) {
			return true;
		} else {
			return false;
		}
	}
}


OutputToConsole

修改了Output方法


package mylog3.output;

import mylog3.event.LogEvent;

/**
 * 实现Output接口,输出到控制台
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public class OutputToConsole extends AbstractOutput {

	/**
	 * 输出到控制台
	 */
	@Override
	public void ouptput(LogEvent event) {
		if (canLog(event.getLevel())) {
			System.out.println(getLayout().parse(event.getMessage(), event.getLevel().toString()));
		}
	}
}


OutputToFile

修改了Output方法


package mylog3.output;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import mylog3.event.LogEvent;

/**
 * 实现Output接口,输出到指定文件中。
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public class OutputToFile extends AbstractOutput {

	private File outputFile;
	private FileWriter fw;

	/**
	 * 初始化
	 *
	 * @param pathName 日志输出到文件的文件名。
	 * @throws OutputException 创建过程中出现错误,则抛出
	 */
	public OutputToFile(String pathName) throws OutputException {
		outputFile = new File(pathName);
		try {
			if (!outputFile.exists()) {
				outputFile.createNewFile();
			}
			fw = new FileWriter(outputFile);
		} catch (IOException e) {
			throw new OutputException(e.getMessage());
		}
	}

	/**
	 * 输出到日志文件中。
	 *
	 * @throws OutputException 如果日志文件不存在或者写入文件异常。
	 */
	@Override
	public void ouptput(LogEvent event) throws OutputException {
		if (canLog(event.getLevel())) {
			if (null == fw) {
				throw new OutputException("日记记录文件不存在!");
			}
			try {
				fw.write(getLayout().parse(event.getMessage(), event.getLevel().toString()) + "\n");
				fw.flush();
			} catch (IOException e) {
				new OutputException(e.getMessage());
			}
		}

	}
}



MyLog3

将Layout和LogLevel移至Output中


package mylog3;

import java.util.ArrayList;
import java.util.List;

import mylog3.event.LogEvent;
import mylog3.output.Output;
import mylog3.output.OutputException;

/**
 * 将Layout和LogLevel移至Output中
 *
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-11-29
 */
public class MyLog3 {

	private List<Output> outputs = new ArrayList<Output>();

	/**
	 * 添加输入终端
	 *
	 * @param output
	 */
	public void addOutput(Output output) {
		outputs.add(output);
	}

	/**
	 * 遍历终端,进行输出
	 *
	 * @param msg
	 * @throws Exception
	 */
	private void toOutput(LogEvent event) {

		for (Output o : outputs) {
			try {
				o.ouptput(event);
			} catch (OutputException e) {
				System.out.println("打印日志错误" + e);
			}
		}
	}

	public void debug(String msg) {
		toOutput(new LogEvent(msg, mylog3.level.LogLevel.debug));
	}

	public void info(String msg) {
		toOutput(new LogEvent(msg, mylog3.level.LogLevel.info));
	}

	public void warn(String msg) {
		toOutput(new LogEvent(msg, mylog3.level.LogLevel.warn));
	}

	public void error(String msg) {
		toOutput(new LogEvent(msg, mylog3.level.LogLevel.error));
	}

	public void fatal(String msg) {
		toOutput(new LogEvent(msg, mylog3.level.LogLevel.fatal));
	}
}


测试

测试类

一个日志类,用同一个Layout,输入不同等级的日志到不同终端。


package mylog3.logtest;

import mylog3.MyLog3;
import mylog3.layout.SimpleLayout;
import mylog3.level.LogLevel;
import mylog3.output.OutputException;
import mylog3.output.OutputToConsole;
import mylog3.output.OutputToFile;

public class MyLog3Test {

	public static void main(String[] args) throws OutputException {
		MyLog3 log = new MyLog3();

		SimpleLayout layout = new SimpleLayout();
		layout.setFormat("-d- -t- -l- -m-");

		OutputToConsole output1 = new OutputToConsole();
		output1.setLogLevel(LogLevel.warn);
		output1.setLayout(layout);

		OutputToFile output2 = new OutputToFile("d:/logs/log3.txt");
		output2.setLogLevel(LogLevel.debug);
		output2.setLayout(layout);

		log.addOutput(output1);
		log.addOutput(output2);

		log.debug("aaaa");
		log.info("bbbb");
		log.warn("cccc");
		log.error("dddd");
		log.fatal("eeeee");
	}
}

测试结果

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log3result.png

思考了

现在这个日志类已经能够满足这个需求了

  • A类用SimpleLayout输出warn以上的日志到控制台–控制台中不需要看太多debug的信息,我需要看warn,error,fatal这些对我的系统有影响有用的东西
  • A类用SimpleLayout输出debug以上的日志到文本中–文本中,我需要记录详细信息,用于查看时清楚每一步都做了什么。

但又来一些新问题:

  • 复用日志器,A类申明了这个日志器,刚好B类也是同样的输出,B类如何复用A类这个日志器?

对于这个问题,我们可以把日志器从A类中抽出,放入一个日志器集合中,例如一个 Map<logId, logger>,那我们就可以根据日志器id获取对应的日志器。 这样,A类把自己创建的日志器放到这个Map中,B类发现自己也需要使用同样的日志器,那么就可以根据A日志器存入时的logId取出那个日志器来用。

  • 假如B类复用了A类创建的日志器,但是A类输出debug及以上的日志,B类想要输出warn及以上的内容。或者A类输出warn及以上的日志,B类想要输出debug及以上的内容。那又如何?

如果按照前面一个问题的解决思路,创建一个 Map<logId, logger> 来复用同一个log,想要做到这一点,是不可能的,看类图, Output是依赖于LogLevel的,一个Output只能有一个LogLevel。

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log3classdiagram.png

因为总不能每次打印之前设置一下level吧,这样也不安全。


    class A {
        public void method() {
            log.setLevel(LogLevel.debug);
            
            log.debug("....");
        }
    }
    class B {
        public void method() {
            log.setLevel(LogLevel.warn);
            
            log.debug("....");
        }
    }
    

那么问题就在于, 复用Log,就得复用其Output,Layout和LogLevel,不同类想复用同一个日志器,但又存在Output,Layout和LogLevel中某个地方有点小不同,这样就导致不能复用。

如果把LogLevel和Layout放回Log中,每次要输出日志的时候,由Log自己判断是否应该打印,若要打印,使用Layout解析该字符串,然后把要打印的日志串直接交给相应的Output。

在这里,我们复用的Output,发现,Layout似乎也可以复用,因为一般输出的样式,没有搞那么多花样吧。 通常都是一种,所以没有必要每个日志器都创建一种Layout,可以复用。

那么,根据这个来构建MyLog4。

测试4

类图 结构

结构,新增 LayoutRepository和OutputRepository来存放相应的 Layout和Output,使得提高它们重用性。

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log4struct.png

类图

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log4diagram.png

LogEvent

在MyLog4中,新增了layout属性


package mylog4.event;

import mylog4.layout.Layout;
import mylog4.level.LogLevel;

/**
 * 用于传播日志的日志事件
 * 在MyLog4中,新增了layout属性
 * 
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-11-29
 */
public class LogEvent {

	private String message;
	private LogLevel level;
	private Layout layout;
	
	public LogEvent() {
		super();
	}
	public LogEvent(String message, LogLevel level) {
		super();
		this.message = message;
		this.level = level;
	}

	public LogEvent(String message, LogLevel level, Layout layout) {
		super();
		this.message = message;
		this.level = level;
		this.layout = layout;
	}
	public String getMessage() {
		return message;
	}
	public void setMessage(String message) {
		this.message = message;
	}
	public LogLevel getLevel() {
		return level;
	}
	public void setLevel(LogLevel level) {
		this.level = level;
	}
	public Layout getLayout() {
		return layout;
	}
	public void setLayout(Layout layout) {
		this.layout = layout;
	}
	
}


LayoutRepository

新增的Layout仓库,用于存放Layout。


package mylog4.layout;

import java.util.HashMap;
import java.util.Map;

/**
 * Layout仓库,用于存放Layout。
 *
 * @author linxingyang(linxingy@foxmail.com)
 * @date 2017-12-01
 */
public class LayoutRepository {
	private static Map<String, Layout> layouts = new HashMap<String, Layout>();
	
	public static Layout getLayoutByKey(String key) {
		return layouts.get(key);
	}
	
	public static void addLayout(String key, Layout layout) throws LayoutNameAlreadyUsedException {
		if (null == key || "".equals(key.trim())) {
			throw new NullPointerException("Layout的Key不能为空!");
		}
		if (layouts.containsKey(key)) {
			throw new LayoutNameAlreadyUsedException("该Layout名称已被使用:" + key);
		}
		layouts.put(key, layout);
	}
}


OutputRepository

新增的Output仓库,用于存放Output


package mylog4.output;

import java.util.HashMap;
import java.util.Map;


public class OutputRepository {
	
	// private List<Output> outputs = new ArrayList<Output>();
	private static Map<String, Output> outputs = new HashMap<String, Output>();
	
	public static  Output getOutputByKey(String key) {
		return outputs.get(key);
	}
	
	/**
	 * 
	 * @param output
	 * @throws OutputAlreadyExistsException
	 */
	public static void addOutput(String key, Output output) throws OutputAlreadyExistsException {
		if (null == key || "".equals(key.trim())) {
			throw new NullPointerException("Output的Key不能为空!");
		}
		if (outputs.containsKey(key)) {
			throw new OutputAlreadyExistsException("该日志器已经存在!");
		}
		outputs.put(key, output);
	}
	
	
	
}


OutputToConsole

做了部分修改,删除了AbstractOutput。

Layout通过LogEvent传入


System.out.println(event.getLayout().parse(event.getMessage(), event.getLevel().toString()));


package mylog4.output;

import mylog4.event.LogEvent;

/**
 * 实现Output接口,输出到控制台
 * 
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-08-18
 */
public class OutputToConsole implements Output {

	/**
	 * 输出到控制台
	 */
	@Override
	public void ouptput(LogEvent event) {
		System.out.println(event.getLayout().parse(event.getMessage(), event.getLevel().toString()));
	}
}


OutputToFile

也做了同样修改



	/**
	 * 输出到日志文件中。
	 * 
	 * @throws OutputException 如果日志文件不存在或者写入文件异常。
	 */
	@Override
	public void ouptput(LogEvent event) throws OutputException {
		if (null == fw) {
			throw new OutputException("日记记录文件不存在!");
		}
		try {
			fw.write(event.getLayout().parse(event.getMessage(), event.getLevel().toString()) + "\n");
			fw.flush();
		} catch (IOException e) {
			new OutputException(e.getMessage());
		}

MyLog4

由MyLog4来判断是否需要打印。


package mylog4;

import java.util.ArrayList;
import java.util.List;

import mylog4.event.LogEvent;
import mylog4.layout.Layout;
import mylog4.level.LogLevel;
import mylog4.output.Output;
import mylog4.output.OutputException;

/**
 * 将Layout和LogLevel移至Output中
 * 
 * @author linxingyang(linxingyang@foxmail.com)
 * @date 2017-11-29
 */
public class MyLog4 {
	
	private LogLevel logLevel;
	private Layout layout;
	private List<Output> outputs = new ArrayList<Output>();
	
	public LogLevel getLogLevel() {
		return logLevel;
	}

	public void setLogLevel(LogLevel logLevel) {
		this.logLevel = logLevel;
	}

	public Layout getLayout() {
		return layout;
	}

	public void setLayout(Layout layout) {
		this.layout = layout;
	}
	
	/**
	 * 添加输入终端
	 * 
	 * @param output
	 */
	public void addOutput(Output output) {
		outputs.add(output);
	}
	
	/**
	 * 遍历终端,进行输出
	 * 
	 * @param msg
	 * @throws Exception
	 */
	private void toOutput(LogEvent event) {
		
		for (Output o : outputs) {
			try {
				o.ouptput(event);
			} catch (OutputException e) {
				System.out.println("打印日志错误" + e);
			}
		}
	}

	/**
	 * 根据用户设置的日志等级以及该日志的等级,判断该日志是否需要被记录。
	 * 
	 * @param level
	 * @return
	 */
	public boolean canLog(LogLevel level) {
		final int logLevelValue = LogLevel.getValue(level);
		final int OutputLogLevelValue = LogLevel.getValue(getLogLevel());
		if (OutputLogLevelValue <= logLevelValue) {
			return true;
		} else {
			return false;
		}
	}
	
	public void debug(String msg) {
		if (canLog(LogLevel.debug)) {
			toOutput(new LogEvent(msg, LogLevel.debug, layout));
		}
	}

	public void info(String msg) {
		if (canLog(LogLevel.info)) {
			toOutput(new LogEvent(msg, LogLevel.info, layout));
		}
	}

	public void warn(String msg) {
		if (canLog(LogLevel.warn)) {
			toOutput(new LogEvent(msg, LogLevel.warn, layout));
		}
	}

	public void error(String msg) {
		if (canLog(LogLevel.error)) {
			toOutput(new LogEvent(msg, LogLevel.error, layout));
		}
	}

	public void fatal(String msg) {
		if (canLog(LogLevel.fatal)) {
			toOutput(new LogEvent(msg, LogLevel.fatal, layout));
		}
	}
}

测试

测试类


package mylog4.logtest;

import mylog4.MyLog4;
import mylog4.layout.LayoutNameAlreadyUsedException;
import mylog4.layout.LayoutRepository;
import mylog4.layout.SimpleLayout;
import mylog4.layout.XMLLayout;
import mylog4.level.LogLevel;
import mylog4.output.Output;
import mylog4.output.OutputAlreadyExistsException;
import mylog4.output.OutputException;
import mylog4.output.OutputRepository;
import mylog4.output.OutputToConsole;
import mylog4.output.OutputToFile;

public class MyLog4Test {

	public static void main(String[] args) throws OutputAlreadyExistsException, OutputException {
		// 创建Output
		// 输出到控制台
		Output console = new OutputToConsole();
		// 输出到文件1
		Output file1 = new OutputToFile("d:/logs/mylog4-1.txt");
		// 输出到文件2
		Output file2 = new OutputToFile("d:/logs/mylog4-2.txt");
		// 添加到Output仓库中
		OutputRepository.addOutput("console", console);
		OutputRepository.addOutput("file1", file1);
		OutputRepository.addOutput("file2", file2);
		
		// 创建Layout
		// 样式1
		SimpleLayout layout1 = new SimpleLayout() ;
		layout1.setFormat("我的日志样式1  -d- -t- -l- -m-");
		
		// 样式2
		SimpleLayout layout2 = new SimpleLayout() ;
		layout2.setFormat("我的日志样式2  -d- -m-");
		
		// 样式3
		XMLLayout layout3 = new XMLLayout();
		
		// 添加到Layout仓库中
		try {
			LayoutRepository.addLayout("style1", layout1);
			LayoutRepository.addLayout("style2", layout2);
			LayoutRepository.addLayout("style3", layout3);
		} catch (LayoutNameAlreadyUsedException e) {
			e.printStackTrace();
		}
		
		// 模拟,A类日志器需要console使用样式1输出  warn以上的日志到控制台console
		MyLog4 a = new MyLog4();
		a.setLogLevel(LogLevel.warn);;
		a.addOutput(OutputRepository.getOutputByKey("console")); 
		a.setLayout(LayoutRepository.getLayoutByKey("style1"));
		
		// 模拟,B类日志器需要file使用样式2输出 debug以上的日志到file1和file2
		MyLog4 b = new MyLog4();
		b.setLogLevel(LogLevel.debug);;
		b.addOutput(OutputRepository.getOutputByKey("file1"));
		b.addOutput(OutputRepository.getOutputByKey("file2"));
		b.setLayout(LayoutRepository.getLayoutByKey("style2"));
		
		// 模拟,C类日志器需要console使用样式3输出info以上的日志到(console)和file(file1)
		MyLog4 c = new MyLog4();
		c.setLogLevel(LogLevel.info);;
		c.addOutput(OutputRepository.getOutputByKey("console"));
		c.addOutput(OutputRepository.getOutputByKey("file1"));
		c.setLayout(LayoutRepository.getLayoutByKey("style3"));
		
		
		a.debug("a----debug");
		a.info("a----info");
		a.warn("a----warn");
		a.error("a----error");
		a.fatal("a----fatal");
		
		b.debug("b----debug");
		b.info("b----info");
		b.warn("b----warn");
		b.error("b----error");
		b.fatal("b----fatal");
		
		c.debug("c----debug");
		c.info("c----info");
		c.warn("c----warn");
		c.error("c----error");
		c.fatal("c----fatal");
	}
}


测试结果

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log4result1.png

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log4result2.png

http://image.linxingyang.net/image/L-log4j/image/2017-08-04/log4result3.png

思考了

改造到这里,


// 模拟,A类日志器需要console使用样式1输出  warn以上的日志到控制台console
		MyLog4 a = new MyLog4();
		a.setLogLevel(LogLevel.warn);;
		a.addOutput(OutputRepository.getOutputByKey("console")); 
		a.setLayout(LayoutRepository.getLayoutByKey("style1"));
		
		// 模拟,B类日志器需要file使用样式2输出 debug以上的日志到file1和file2
		MyLog4 b = new MyLog4();
		b.setLogLevel(LogLevel.debug);;
		b.addOutput(OutputRepository.getOutputByKey("file1"));
		b.addOutput(OutputRepository.getOutputByKey("file2"));
		b.setLayout(LayoutRepository.getLayoutByKey("style2"));
		
		// 模拟,C类日志器需要console使用样式3输出info以上的日志到(console)和file(file1)
		MyLog4 c = new MyLog4();
		c.setLogLevel(LogLevel.info);;
		c.addOutput(OutputRepository.getOutputByKey("console"));
		c.addOutput(OutputRepository.getOutputByKey("file1"));
		c.setLayout(LayoutRepository.getLayoutByKey("style3"));
		
		

现在一个Log中有多个Output,一个Layout,一个LogLevel,也就是这个Log所有的Output都是固定的Layout和LogLevel。

如果把 Layout放在Output中,那么这个Output的Layout就固定了。(Log4j是这种做法)。这样一个Log的输出Layout就可以有多种了。

这个日志器的功能还是不太够,比如不能过滤日志,没有总开关等。 但可以继续往上堆积功能。就不接着改造这些功能点了。

后面再整理下笔记,来看log4j的源码吧。