Log4j擴展使用--自定義輸出


  • 寫在前面的話

log4j支持自定義的輸出。全部的輸出都實現了自Appender接口。通常來講,自定義輸出值須要繼承AppenderSkeleton類,並實現幾個方法就能夠了。

寫這篇博客,我主要也是想說,框架之全部被成爲是一個框架,是在幫咱們完成大部分的通用代碼,這就有一個前提就是說它必需要有具備良好的擴張性。方便每個使用者來擴展,固然咱們也能夠根據本身的喜愛去改人家框架的源碼,可是最實在的也是最有效的去擴展人家開源框架,在擴展的時候咱們也能夠參照人家原來的默認實現,這樣子對於咱們的學習也是一大進步。

  • 一個自定義輸出的例子


OK,廢話不說了,如今咱們開始吧。先來看一個自定義輸出的例子,CountingConsoleAppender跟控制檯輸出相似,不一樣的是會統計日誌輸出的次數。當輸出次數超出預約的值時,會作相應的業務處理,這裏簡單的爲打印出一行提示信息,並中止輸出。代碼以下:
package org.linkinpark.commons.logtest;

import java.util.Objects;

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.spi.ErrorCode;
import org.apache.log4j.spi.LoggingEvent;

public class CountingConsoleAppender extends AppenderSkeleton
{
	protected int count = 0;
	protected int limit = 10;

	/**
	 * 關閉資源
	 */
	@Override
	public void close()
	{
		if (this.closed)
		{
			return;
		}
		this.closed = true;
	}

	/**
	 * 這裏須要使用格式化器
	 */
	@Override
	public boolean requiresLayout()
	{
		return true;
	}

	@Override
	protected void append(LoggingEvent event)
	{
		// 1,驗證,若是沒有格式化器,報錯,若是次數超過限制,報錯
		if (this.layout == null)
		{
			errorHandler.error("沒有設置[" + name + "]日誌格式化器。", null, ErrorCode.MISSING_LAYOUT);
			return;
		}
		if (count >= limit)
		{
			errorHandler.error("輸出次數[" + limit + "]達到了[" + getName() + "]的上限。", null, ErrorCode.WRITE_FAILURE);
			return;
		}
		// 控制檯打印日誌
		System.out.println(this.layout.format(event));
		// 若是配置的格式化器沒有處理異常,這裏打印異常棧信息
		if (layout.ignoresThrowable())
		{
			String[] throwableStrRep = event.getThrowableStrRep();
			if (Objects.nonNull(throwableStrRep))
			{
				for (String throwStr : throwableStrRep)
				{
					System.out.println(throwStr);
				}
			}
		}
		// 打印日誌結束,修改打印次數
		count++;
	}

	public int getCount()
	{
		return count;
	}

	public CountingConsoleAppender setCount(int count)
	{
		this.count = count;
		return this;
	}

	public int getLimit()
	{
		return limit;
	}

	public void setLimit(int limit)
	{
		this.limit = limit;
	}

}
配置文件以下:
#定義輸出等級和輸出appender
log4j.rootLogger=DEBUG,countingconsole
log4j.appender.countingconsole=org.linkinpark.commons.logtest.CountingConsoleAppender  
#設置輸出樣式
log4j.appender.countingconsole.layout=org.apache.log4j.PatternLayout
#日誌輸出信息格式爲
log4j.appender.countingconsole.layout.ConversionPattern=[%-d{yyyy-MM-dd HH:mm:ss}]-[%t-%5p]-[%C-%M(%L)]: %m%n 
#控制最大輸出次數
log4j.appender.countingconsole.limit=3
#打開4j自己的日誌輸出
log4j.debug=true
OK,如今咱們來運行下測試看下控制檯輸出狀況,測試代碼以下:
package org.linkinpark.commons.logtest;

import org.apache.log4j.Logger;
import org.junit.Test;

/**
 * @建立做者: LinkinPark
 * @建立時間: 2016年2月23日
 * @功能描述: 測試本身擴展的CountConsoleAppender
 */
public class Log4jTest
{

	public static Logger log = Logger.getLogger(Log4jTest.class);

	@Test
	public void logTest()
	{
		log.debug("debug級別的日誌輸出");
		log.debug("debug級別的日誌輸出1");
		log.debug("debug級別的日誌輸出2");
		log.debug("debug級別的日誌輸出3");
	}


}
測試綠條,控制檯輸出以下:
log4j: Parsing for [root] with value=[DEBUG,countingconsole].
log4j: Level token is [DEBUG].
log4j: Category root set to DEBUG
log4j: Parsing appender named "countingconsole".
log4j: Parsing layout options for "countingconsole".
log4j: Setting property [conversionPattern] to [[%-d{yyyy-MM-dd HH:mm:ss}]-[%t-%5p]-[%C-%M(%L)]: %m%n ].
log4j: End of parsing for "countingconsole".
log4j: Setting property [limit] to [3].
log4j: Parsed "countingconsole" options.
log4j: Finished configuring.
[2016-02-25 23:42:16]-[main-DEBUG]-[org.linkinpark.commons.logtest.Log4jTest-logTest(19)]: debug級別的日誌輸出
 
[2016-02-25 23:42:16]-[main-DEBUG]-[org.linkinpark.commons.logtest.Log4jTest-logTest(20)]: debug級別的日誌輸出1
 
[2016-02-25 23:42:16]-[main-DEBUG]-[org.linkinpark.commons.logtest.Log4jTest-logTest(21)]: debug級別的日誌輸出2
 
log4j:ERROR 輸出次數[3]達到了[countingconsole]的上限。

  • 關於例子的解釋

1,在擴展這個appender的時候,我有參照consoleAppender的實現。核心就是說實現append方法,固然咱們直接繼承自 AppenderSkeleton類來進行的擴展,因此能夠直接拿到裏面的一些屬性,好比layput,好比erroHandler等等
2,剛開始的寫這個類的時候,我直接定義了一個limit屬性,用來控制日誌輸出次數,直接是在代碼中賦的初始值,爲了方便,因此我就想寫進配置文件中,可是怎麼都注入不進去,控制檯一直報下面這個error:
log4j:WARN Failed to set property [limit] to value "3". 
沒辦法,我只要打開log4j自己的日誌,配置文件中設值log4j.debug=true就OK。後來終於發現個人set方法有問題,這個方法這裏必須是void返回類型的,而我通常的set方法都是返回自身this,因此這裏沒有注入。關於log4j處理set注入我下面一節會整理到。
3,固然咱們在擴展的時候直接繼承 ConsoleAppender自這個類也是能夠的,這樣子的話只須要重寫append方法就夠了,其餘的都不須要了。我本身試了一下測試經過,代碼相似,這裏不作贅述了。

  • 關於反射set值的另外一種方式

咱們常常編碼,可是其實寫反射的代碼並非不少,通常的在IOC框架中都是讀取配置文件或者說掃描註解來獲取相關key-value,返回跑下set方法的反射,就能夠設值到一個對象裏面去了,這樣子的話就能夠把一些屬性的設值放入到配置文件中,實現解耦。
在之前咱們是這樣子編碼的:
// 取出須要設置Field值的目標對象
				Object target = getObject(objAndProp[0]);
				// 該Field對應的setter方法名:set + "屬性的首字母大寫" + 剩下部分
				String mtdName = "set" + objAndProp[1].substring(0 , 1).toUpperCase() + objAndProp[1].substring(1);
				// 經過target的getClass()獲取它實現類所對應的Class對象
				Class<?> targetClass = target.getClass();
				// 獲取該屬性對應的setter方法,下面這一行道出了springIOC的精髓,爲何實現XML咱們每次都要提供get和set方法,除了註解的哦
				Method mtd = targetClass.getMethod(mtdName , String.class);
				// 經過Method的invoke方法執行setter方法,將config.getProperty(name)的屬性值做爲調用setter的方法的實參
				mtd.invoke(target , config.getProperty(name));

看過了log4j的源碼之後,咱們多了一種選擇,就是使用JDK自帶的PropertyDescriptor類,這個類就是按照javabean規範寫的一個存儲器。
該類裏面有2個方法能夠直接獲取咱們的get和set方法: setReadMethod,getWriteMethod。之後這也是一種嘗試,必要的時候能夠參照log4j來用這個方式跑反射。OK,我這裏貼出log4j中該類的源碼:
package org.apache.log4j.config;

import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.Priority;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.spi.OptionHandler;
import org.apache.log4j.spi.ErrorHandler;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.InterruptedIOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.Properties;

/**
 * General purpose Object property setter. Clients repeatedly invokes
 * {@link #setProperty setProperty(name,value)} in order to invoke setters
 * on the Object specified in the constructor. This class relies on the
 * JavaBeans {@link Introspector} to analyze the given Object Class using
 * reflection.
 * 
 * <p>
 * Usage:
 * 
 * <pre>
 * PropertySetter ps = new PropertySetter(anObject);
 * ps.set("name", "Joe");
 * ps.set("age", "32");
 * ps.set("isMale", "true");
 * </pre>
 * 
 * will cause the invocations anObject.setName("Joe"), anObject.setAge(32),
 * and setMale(true) if such methods exist with those signatures.
 * Otherwise an {@link IntrospectionException} are thrown.
 * 
 * @author Anders Kristensen
 * @since 1.1
 */
public class PropertySetter
{
	protected Object obj;
	protected PropertyDescriptor[] props;

	/**
	 * Create a new PropertySetter for the specified Object. This is done
	 * in prepartion for invoking {@link #setProperty} one or more times.
	 * 
	 * @param obj
	 *            the object for which to set properties
	 */
	public PropertySetter(Object obj)
	{
		this.obj = obj;
	}

	/**
	 * Uses JavaBeans {@link Introspector} to computer setters of object to be
	 * configured.
	 */
	protected void introspect()
	{
		try
		{
			BeanInfo bi = Introspector.getBeanInfo(obj.getClass());
			props = bi.getPropertyDescriptors();
		}
		catch (IntrospectionException ex)
		{
			LogLog.error("Failed to introspect " + obj + ": " + ex.getMessage());
			props = new PropertyDescriptor[0];
		}
	}

	/**
	 * Set the properties of an object passed as a parameter in one
	 * go. The <code>properties</code> are parsed relative to a
	 * <code>prefix</code>.
	 * 
	 * @param obj
	 *            The object to configure.
	 * @param properties
	 *            A java.util.Properties containing keys and values.
	 * @param prefix
	 *            Only keys having the specified prefix will be set.
	 */
	public static void setProperties(Object obj, Properties properties, String prefix)
	{
		new PropertySetter(obj).setProperties(properties, prefix);
	}

	/**
	 * Set the properites for the object that match the
	 * <code>prefix</code> passed as parameter.
	 * 
	 * 
	 */
	public void setProperties(Properties properties, String prefix)
	{
		int len = prefix.length();

		for (Enumeration e = properties.propertyNames(); e.hasMoreElements();)
		{
			String key = (String) e.nextElement();

			// handle only properties that start with the desired frefix.
			if (key.startsWith(prefix))
			{

				// ignore key if it contains dots after the prefix
				if (key.indexOf('.', len + 1) > 0)
				{
					// System.err.println("----------Ignoring---["+key
					// +"], prefix=["+prefix+"].");
					continue;
				}

				String value = OptionConverter.findAndSubst(key, properties);
				key = key.substring(len);
				if (("layout".equals(key) || "errorhandler".equals(key)) && obj instanceof Appender)
				{
					continue;
				}
				//
				// if the property type is an OptionHandler
				// (for example, triggeringPolicy of org.apache.log4j.rolling.RollingFileAppender)
				PropertyDescriptor prop = getPropertyDescriptor(Introspector.decapitalize(key));
				if (prop != null && OptionHandler.class.isAssignableFrom(prop.getPropertyType()) && prop.getWriteMethod() != null)
				{
					OptionHandler opt = (OptionHandler) OptionConverter.instantiateByKey(properties, prefix + key, prop.getPropertyType(), null);
					PropertySetter setter = new PropertySetter(opt);
					setter.setProperties(properties, prefix + key + ".");
					try
					{
						Method writeMethod = prop.getWriteMethod();
						System.out.println("woqu=" + writeMethod);
						prop.getWriteMethod().invoke(this.obj, new Object[] { opt });
					}
					catch (IllegalAccessException ex)
					{
						LogLog.warn("Failed to set property [" + key + "] to value \"" + value + "\". ", ex);
					}
					catch (InvocationTargetException ex)
					{
						if (ex.getTargetException() instanceof InterruptedException || ex.getTargetException() instanceof InterruptedIOException)
						{
							Thread.currentThread().interrupt();
						}
						LogLog.warn("Failed to set property [" + key + "] to value \"" + value + "\". ", ex);
					}
					catch (RuntimeException ex)
					{
						LogLog.warn("Failed to set property [" + key + "] to value \"" + value + "\". ", ex);
					}
					continue;
				}

				setProperty(key, value);
			}
		}
		activate();
	}

	/**
	 * Set a property on this PropertySetter's Object. If successful, this
	 * method will invoke a setter method on the underlying Object. The
	 * setter is the one for the specified property name and the value is
	 * determined partly from the setter argument type and partly from the
	 * value specified in the call to this method.
	 * 
	 * <p>
	 * If the setter expects a String no conversion is necessary.
	 * If it expects an int, then an attempt is made to convert 'value'
	 * to an int using new Integer(value). If the setter expects a boolean,
	 * the conversion is by new Boolean(value).
	 * 
	 * @param name
	 *            name of the property
	 * @param value
	 *            String value of the property
	 */
	public void setProperty(String name, String value)
	{
		if (value == null)
		{
			return;
		}

		name = Introspector.decapitalize(name);
		PropertyDescriptor prop = getPropertyDescriptor(name);

		// LogLog.debug("---------Key: "+name+", type="+prop.getPropertyType());

		if (prop == null)
		{
			LogLog.warn("No such property [" + name + "] in " + obj.getClass().getName() + ".");
		}
		else
		{
			try
			{
				setProperty(prop, name, value);
			}
			catch (PropertySetterException ex)
			{
				LogLog.warn("Failed to set property [" + name + "] to value \"" + value + "\". ", ex.rootCause);
			}
		}
	}

	/**
	 * Set the named property given a {@link PropertyDescriptor}.
	 * 
	 * @param prop
	 *            A PropertyDescriptor describing the characteristics
	 *            of the property to set.
	 * @param name
	 *            The named of the property to set.
	 * @param value
	 *            The value of the property.
	 */
	public void setProperty(PropertyDescriptor prop, String name, String value) throws PropertySetterException
	{
		Method setter = prop.getWriteMethod();
		if (setter == null)
		{
			throw new PropertySetterException("No setter for property [" + name + "].");
		}
		Class[] paramTypes = setter.getParameterTypes();
		if (paramTypes.length != 1)
		{
			throw new PropertySetterException("#params for setter != 1");
		}

		Object arg;
		try
		{
			arg = convertArg(value, paramTypes[0]);
		}
		catch (Throwable t)
		{
			throw new PropertySetterException("Conversion to type [" + paramTypes[0] + "] failed. Reason: " + t);
		}
		if (arg == null)
		{
			throw new PropertySetterException("Conversion to type [" + paramTypes[0] + "] failed.");
		}
		LogLog.debug("Setting property [" + name + "] to [" + arg + "].");
		try
		{
			setter.invoke(obj, new Object[] { arg });
		}
		catch (IllegalAccessException ex)
		{
			throw new PropertySetterException(ex);
		}
		catch (InvocationTargetException ex)
		{
			if (ex.getTargetException() instanceof InterruptedException || ex.getTargetException() instanceof InterruptedIOException)
			{
				Thread.currentThread().interrupt();
			}
			throw new PropertySetterException(ex);
		}
		catch (RuntimeException ex)
		{
			throw new PropertySetterException(ex);
		}
	}

	/**
	 * Convert <code>val</code> a String parameter to an object of a
	 * given type.
	 */
	protected Object convertArg(String val, Class type)
	{
		if (val == null)
		{
			return null;
		}

		String v = val.trim();
		if (String.class.isAssignableFrom(type))
		{
			return val;
		}
		else if (Integer.TYPE.isAssignableFrom(type))
		{
			return new Integer(v);
		}
		else if (Long.TYPE.isAssignableFrom(type))
		{
			return new Long(v);
		}
		else if (Boolean.TYPE.isAssignableFrom(type))
		{
			if ("true".equalsIgnoreCase(v))
			{
				return Boolean.TRUE;
			}
			else if ("false".equalsIgnoreCase(v))
			{
				return Boolean.FALSE;
			}
		}
		else if (Priority.class.isAssignableFrom(type))
		{
			return OptionConverter.toLevel(v, Level.DEBUG);
		}
		else if (ErrorHandler.class.isAssignableFrom(type))
		{
			return OptionConverter.instantiateByClassName(v, ErrorHandler.class, null);
		}
		return null;
	}

	protected PropertyDescriptor getPropertyDescriptor(String name)
	{
		if (props == null)
		{
			introspect();
		}

		for (int i = 0; i < props.length; i++)
		{
			if (name.equals(props[i].getName()))
			{
				return props[i];
			}
		}
		return null;
	}

	public void activate()
	{
		if (obj instanceof OptionHandler)
		{
			((OptionHandler) obj).activateOptions();
		}
	}
}

  • 總結

Log4j源碼仍是寫的不錯的,特別是一些小巧的設計,好比hashtable的性能提高,好比layout引入瞭解釋器模式等等,都是值得咱們借鑑的,在擴展性方面也寫的挺好。
若是有必要的咱們能夠本身重寫一個appender來實現咱們本身的特定功能,OK,先這樣子吧。
相關文章
相關標籤/搜索