- 写在前面的话
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=trueOK,现在我们来运行下测试看下控制台输出情况,测试代码如下:
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,先这样子吧。
风流子弟曾少年,多少老死江湖前。。。