深入Log4J源码之Appender

Appender负责定义日志输出的目的地,它可以是控制台(ConsoleAppender)、文件(FileAppender)、JMS服务器(JmsLogAppender)、以Email的形式发送出去(SMTPAppender)等。Appender是一个命名的实体,另外它还包含了对LayoutErrorHandlerFilter等引用:

 1 public interface Appender {
 2     void addFilter(Filter newFilter);
 3     public Filter getFilter();
 4     public void clearFilters();
 5     public void close();
 6     public void doAppend(LoggingEvent event);
 7     public String getName();
 8     public void setErrorHandler(ErrorHandler errorHandler);
 9     public ErrorHandler getErrorHandler();
10     public void setLayout(Layout layout);
11     public Layout getLayout();
12     public void setName(String name);
13     public boolean requiresLayout();
14 }

简单的,在配置文件中,Appender会注册到Logger中,Logger在写日志时,通过继承机制遍历所有注册到它本身和其父节点的Appender(在additivitytrue的情况下),调用doAppend()方法,实现日志的写入。在doAppend方法中,若当前Appender注册了Filter,则doAppend还会判断当前日志时候通过了Filter的过滤,通过了Filter的过滤后,如果当前Appender继承自SkeletonAppender,还会检查当前日志级别时候要比当前Appender本身的日志级别阀门要打,所有这些都通过后,才会将LoggingEvent实例传递给Layout实例以格式化成一行日志信息,最后写入相应的目的地,在这些操作中,任何出现的错误都由ErrorHandler字段来处理。

Log4J中的Appender类图结构:

深入Log4J源码之Appender

Log4J Core一小节中已经简单的介绍过了AppenderSkeletonWriterAppenderConsoleAppender以及 Filter,因小节将直接介绍具体的几个常用的Appender

FileAppender

FileAppender继承自WriterAppender,它将日志写入文件。主要的日志写入逻辑已经在WriterAppender中处理,FileAppender主要处理的逻辑主要在于将设置日志输出文件名,并通过设置的文件构建WriterAppender中的QuiteWriter字段实例。如果Log文件的目录没有创建,在setFile()方法中会先创建目录,再设置日志文件。另外,所有FileAppender字段在调用activateOptions()方法中生效。

 1     protected boolean fileAppend = true;
 2     protected String fileName = null;
 3     protected boolean bufferedIO = false;
 4     protected int bufferSize = 8 * 1024;
 5 
 6     public void activateOptions() {
 7         if (fileName != null) {
 8             try {
 9                 setFile(fileName, fileAppend, bufferedIO, bufferSize);
10             } catch (java.io.IOException e) {
11                 errorHandler.error("setFile(" + fileName + "," + fileAppend
12                         + ") call failed.", e, ErrorCode.FILE_OPEN_FAILURE);
13             }
14         } else {
15             LogLog.warn("File option not set for appender [" + name + "].");
16             LogLog.warn("Are you using FileAppender instead of ConsoleAppender?");
17         }
18     }
19 
20     public synchronized void setFile(String fileName, boolean append,
21             boolean bufferedIO, int bufferSize) throws IOException {
22         LogLog.debug("setFile called: " + fileName + "" + append);
23         if (bufferedIO) {
24             setImmediateFlush(false);
25         }
26         reset();
27         FileOutputStream ostream = null;
28         try {
29             ostream = new FileOutputStream(fileName, append);
30         } catch (FileNotFoundException ex) {
31             String parentName = new File(fileName).getParent();
32             if (parentName != null) {
33                 File parentDir = new File(parentName);
34                 if (!parentDir.exists() && parentDir.mkdirs()) {
35                     ostream = new FileOutputStream(fileName, append);
36                 } else {
37                     throw ex;
38                 }
39             } else {
40                 throw ex;
41             }
42         }
43         Writer fw = createWriter(ostream);
44         if (bufferedIO) {
45             fw = new BufferedWriter(fw, bufferSize);
46         }
47         this.setQWForFiles(fw);
48         this.fileName = fileName;
49         this.fileAppend = append;
50         this.bufferedIO = bufferedIO;
51         this.bufferSize = bufferSize;
52         writeHeader();
53         LogLog.debug("setFile ended");
54     }

 

DailyRollingFileAppender

DailyRollingFileAppender继承自FileAppender,不过这个名字感觉有点不靠谱,事实上,DailyRollingFileAppender会在每隔一段时间可以生成一个新的日志文件,不过这个时间间隔是可以设置的,不仅仅只是每隔一天。时间间隔通过setDatePattern()方法设置,datePattern必须遵循SimpleDateFormat中的格式。支持的时间间隔有:

1.       每天:’.’YYYY-MM-dd(默认)

2.       每星期:’.’YYYY-ww

3.       每月:’.’YYYY-MM

4.       每隔半天:’.’YYYY-MM-dd-a

5.       每小时:’.’YYYY-MM-dd-HH

6.       每分钟:’.’YYYY-MM-dd-HH-mm

DailyRollingFileAppender需要设置的两个属性:datePatternfileName。其中datePattern用于确定时间间隔以及当日志文件过了一个时间间隔后用于重命名之前的日志文件;fileName用于设置日志文件的初始名字。在实现过程中,datePattern用于实例化SimpleDateFormat,记录当前时间以及计算下一个时间间隔时间。在每次写日志操作之前先判断当前时间是否已经操作计算出的下一间隔时间,若是,则将之前的日志文件重命名(向日志文件名尾添加datePattern指定的时间信息),并创新的日志文件,同时重新设置当前时间以及下一次的时间间隔。

 1 public void activateOptions() {
 2     super.activateOptions();
 3     if (datePattern != null && fileName != null) {
 4         now.setTime(System.currentTimeMillis());
 5         sdf = new SimpleDateFormat(datePattern);
 6         int type = computeCheckPeriod();
 7         printPeriodicity(type);
 8         rc.setType(type);
 9         File file = new File(fileName);
10         scheduledFilename = fileName
11                 + sdf.format(new Date(file.lastModified()));
12 
13     } else {
14         LogLog.error("Either File or DatePattern options are not set for appender ["
15                 + name + "].");
16     }
17 }
18 void rollOver() throws IOException {
19     if (datePattern == null) {
20         errorHandler.error("Missing DatePattern option in rollOver().");
21         return;
22     }
23 
24     String datedFilename = fileName + sdf.format(now);
25     if (scheduledFilename.equals(datedFilename)) {
26         return;
27     }
28     this.closeFile();
29     File target = new File(scheduledFilename);
30     if (target.exists()) {
31         target.delete();
32     }
33     File file = new File(fileName);
34     boolean result = file.renameTo(target);
35     if (result) {
36         LogLog.debug(fileName + " -> " + scheduledFilename);
37     } else {
38         LogLog.error("Failed to rename [" + fileName + "] to ["
39                 + scheduledFilename + "].");
40     }
41     try {
42         this.setFile(fileName, truethis.bufferedIO, this.bufferSize);
43     } catch (IOException e) {
44         errorHandler.error("setFile(" + fileName + ", true) call failed.");
45     }
46     scheduledFilename = datedFilename;
47 }
48 protected void subAppend(LoggingEvent event) {
49     long n = System.currentTimeMillis();
50     if (n >= nextCheck) {
51         now.setTime(n);
52         nextCheck = rc.getNextCheckMillis(now);
53         try {
54             rollOver();
55         } catch (IOException ioe) {
56             if (ioe instanceof InterruptedIOException) {
57                 Thread.currentThread().interrupt();
58             }
59             LogLog.error("rollOver() failed.", ioe);
60         }
61     }
62     super.subAppend(event);
63 }

Log4J文档,DailyRollingFileAppender存在线程同步问题。不过本人木有找到哪里出问题了,望高人指点。

RollingFileAppender

RollingFileAppender继承自FileAppender,不同于DailyRollingFileAppender是基于时间作为阀值,RollingFileAppender则是基于文件大小作为阀值。当日志文件超过指定大小,日志文件会被重命名成日志文件名.1”,若此文件已经存在,则将此文件重命名成日志文件名.2”,一次类推。若文件数已经超过设置的可备份日志文件最大个数,则将最旧的日志文件删除。如果要设置不删除任何日志文件,可以将maxBackupIndex设置成Integer最大值,如果这样,这里rollover()方法的实现会引起一些性能问题,因为它要冲最大值开始遍历查找已经备份的日志文件。

 1 protected long maxFileSize = 10 * 1024 * 1024;
 2 protected int maxBackupIndex = 1;
 3 private long nextRollover = 0;
 4 
 5 public void rollOver() {
 6     File target;
 7     File file;
 8     if (qw != null) {
 9         long size = ((CountingQuietWriter) qw).getCount();
10         LogLog.debug("rolling over count=" + size);
11         // if operation fails, do not roll again until
12         // maxFileSize more bytes are written
13         nextRollover = size + maxFileSize;
14     }
15     LogLog.debug("maxBackupIndex=" + maxBackupIndex);
16 
17     boolean renameSucceeded = true;
18     // If maxBackups <= 0, then there is no file renaming to be done.
19     if (maxBackupIndex > 0) {
20         // Delete the oldest file, to keep Windows happy.
21         file = new File(fileName + '.' + maxBackupIndex);
22         if (file.exists())
23             renameSucceeded = file.delete();
24 
25         // Map {(maxBackupIndex - 1), 深入Log4J源码之Appender, 2, 1} to {maxBackupIndex, 深入Log4J源码之Appender, 3,
26         // 2}
27         for (int i = maxBackupIndex - 1; i >= 1 && renameSucceeded; i--) {
28             file = new File(fileName + "." + i);
29             if (file.exists()) {
30                 target = new File(fileName + '.' + (i + 1));
31                 LogLog.debug("Renaming file " + file + " to " + target);
32                 renameSucceeded = file.renameTo(target);
33             }
34         }
35 
36         if (renameSucceeded) {
37             // Rename fileName to fileName.1
38             target = new File(fileName + "." + 1);
39             this.closeFile(); // keep windows happy.
40             file = new File(fileName);
41             LogLog.debug("Renaming file " + file + " to " + target);
42             renameSucceeded = file.renameTo(target);
43             //
44             // if file rename failed, reopen file with append = true
45             //
46             if (!renameSucceeded) {
47                 try {
48                     this.setFile(fileName, true, bufferedIO, bufferSize);
49                 } catch (IOException e) {
50                     if (e instanceof InterruptedIOException) {
51                         Thread.currentThread().interrupt();
52                     }
53                     LogLog.error("setFile(" + fileName
54                             + ", true) call failed.", e);
55                 }
56             }
57         }
58     }
59 
60     //
61     // if all renames were successful, then
62     //
63     if (renameSucceeded) {
64         try {
65             this.setFile(fileName, false, bufferedIO, bufferSize);
66             nextRollover = 0;
67         } catch (IOException e) {
68             if (e instanceof InterruptedIOException) {
69                 Thread.currentThread().interrupt();
70             }
71             LogLog.error("setFile(" + fileName + ", false) call failed.", e);
72         }
73     }
74 }
75 
76 public synchronized void setFile(String fileName, boolean append,
77         boolean bufferedIO, int bufferSize) throws IOException {
78     super.setFile(fileName, append, this.bufferedIO, this.bufferSize);
79     if (append) {
80         File f = new File(fileName);
81         ((CountingQuietWriter) qw).setCount(f.length());
82     }
83 }
84 protected void setQWForFiles(Writer writer) {
85     this.qw = new CountingQuietWriter(writer, errorHandler);
86 }
87 protected void subAppend(LoggingEvent event) {
88     super.subAppend(event);
89     if (fileName != null && qw != null) {
90         long size = ((CountingQuietWriter) qw).getCount();
91         if (size >= maxFileSize && size >= nextRollover) {
92             rollOver();
93         }
94     }
95 }

AsyncAppender

AsyncAppender顾名思义,就是异步的调用Appender中的doAppend()方法。有多种方法实现这样的功能,比如每当调用doAppend()方法时,doAppend()方法内部启动一个线程来处理这一次调用的逻辑,这个线程可以是新建的线程也可以是线程池,然而我们知道线程是一个比较耗资源的实体,为每一次的操作都创建一个新的线程,而这个线程在这一次调用结束后就不再使用,这种模式是非常不划算的,性能低下;而且即使在这里使用线程池,也会导致在非常多请求同时过来时引起消耗大量的线程池中的线程或者因为线程池已满而阻塞请求。因而这种直接使用线程去处理每一次的请求是不可取的。

另一种常用的方案可以使用生产者和消费中的模式来实现类似的逻辑。即每一次请求做为一个生产者,将请求放到一个Queue中,而由另外一个或多个消费者读取Queue中的内容以处理真正的逻辑。

在最新的Java版本中,我们可以使用BlockingQueue类简单的实现类似的需求,然而由于Log4J的存在远早于BlockingQueue的创建,因而为了实现对以前版本的兼容,它还是自己实现了这样一套生产者消费者模型。

AsyncAppender并不会在每一次的doAppend()调用中都直接将消息输出,而是使用了buffer,即只有等到bufferLoggingEvent实例到达bufferSize个的时候才真正的处理这些消息,当然我们也可以讲bufferSize设置成1,从而实现每一个LoggingEvent实例的请求都会直接执行。如果bufferSize设置过大,在应用程序异常终止时可能会丢失部分日志。

1 public static final int DEFAULT_BUFFER_SIZE = 128;
2 private final List buffer = new ArrayList();
3 private final Map discardMap = new HashMap();
4 private int bufferSize = DEFAULT_BUFFER_SIZE;
5 private final Thread dispatcher;
6 private boolean locationInfo = false;
7 private boolean blocking = true;

对其他字段,discardMap用于存放当当前LoggingEvent请求数已经超过bufferSize或当前线程被中断的情况下能继续保留这些日志信息;locationInfo用于设置是否需要保留位置信息;blocking用于设置在消费者正在处理时,是否需要生产者“暂停”下来,默认为true;而dispatcher即是消费者线程,它在构建AsyncAppender是启动,每次监听buffer这个list,如果发现buffer中存在LoggingEvent实例,则将所有bufferdiscardMap中的LoggingEvent实例拷贝到数组中,清空bufferdiscardMap,并调用AsyncAppender内部注册的Appender实例打印日志。

 1 public void run() {
 2     boolean isActive = true;
 3     try {
 4         while (isActive) {
 5             LoggingEvent[] events = null;
 6             synchronized (buffer) {
 7                 int bufferSize = buffer.size();
 8                 isActive = !parent.closed;
 9 
10                 while ((bufferSize == 0&& isActive) {
11                     buffer.wait();
12                     bufferSize = buffer.size();
13                     isActive = !parent.closed;
14                 }
15                 if (bufferSize > 0) {
16                     events = new LoggingEvent[bufferSize
17                             + discardMap.size()];
18                     buffer.toArray(events);
19                     int index = bufferSize;
20 
21                     for (Iterator iter = discardMap.values().iterator(); iter
22                             .hasNext();) {
23                         events[index++= ((DiscardSummary) iter.next())
24                                 .createEvent();
25                     }
26                     buffer.clear();
27                     discardMap.clear();
28                     buffer.notifyAll();
29                 }
30             }
31             if (events != null) {
32                 for (int i = 0; i < events.length; i++) {
33                     synchronized (appenders) {
34                         appenders.appendLoopOnAppenders(events[i]);
35                     }
36                 }
37             }
38         }
39     } catch (InterruptedException ex) {
40         Thread.currentThread().interrupt();
41     }
42 }

这里其实有一个bug,即当程序停止时只剩下discardMap中有日志信息,而buffer中没有日志信息,由于Dispatcher线程不检查discardMap中的日志信息,因而此时会导致discardMap中的日志信息丢失。即使在生成者中当buffer为空时,它也会激活buffer锁,然而即使激活后buffer本身大小还是为0,因而不会处理之后的逻辑,因而这个逻辑也处理不了该bug

对于生产者,它首先处理当消费者线程出现异常而不活动时,此时将同步的输出日志;而后根据配置获取LoggingEvent中的数据;再获得buffer的对象锁,如果buffer还没满,则直接将LoggingEvent实例添加到buffer中,否则如果blocking设置为true,即生产者会等消费者处理完后再继续下一次接收数据。如果blocking设置为fasle或者消费者线程被打断,那么当前的LoggingEvent实例则会保存在discardMap中,因为此时buffer已满。

 1 public void append(final LoggingEvent event) {
 2     if ((dispatcher == null|| !dispatcher.isAlive() || (bufferSize <= 0)) {
 3         synchronized (appenders) {
 4             appenders.appendLoopOnAppenders(event);
 5         }
 6         return;
 7     }
 8     event.getNDC();
 9     event.getThreadName();
10     event.getMDCCopy();
11     if (locationInfo) {
12         event.getLocationInformation();
13     }
14     event.getRenderedMessage();
15     event.getThrowableStrRep();
16     synchronized (buffer) {
17         while (true) {
18             int previousSize = buffer.size();
19             if (previousSize < bufferSize) {
20                 buffer.add(event);
21                 if (previousSize == 0) {
22                     buffer.notifyAll();
23                 }
24                 break;
25             }
26             boolean discard = true;
27             if (blocking && !Thread.interrupted()
28                     && Thread.currentThread() != dispatcher) {
29                 try {
30                     buffer.wait();
31                     discard = false;
32                 } catch (InterruptedException e) {
33                     Thread.currentThread().interrupt();
34                 }
35             }
36             if (discard) {
37                 String loggerName = event.getLoggerName();
38                 DiscardSummary summary = (DiscardSummary) discardMap
39                         .get(loggerName);
40 
41                 if (summary == null) {
42                     summary = new DiscardSummary(event);
43                     discardMap.put(loggerName, summary);
44                 } else {
45                     summary.add(event);
46                 }
47                 break;
48             }
49         }
50     }
51 }

最后,AsyncAppenderAppender的一个容器,它实现了AppenderAttachable接口,改接口的实现主要将实现逻辑代理给AppenderAttachableImpl类。

测试代码如下:

 1 @Test
 2 public void testAsyncAppender() throws Exception {
 3     AsyncAppender appender = new AsyncAppender();
 4     appender.addAppender(new ConsoleAppender(new TTCCLayout()));
 5     appender.setBufferSize(1);
 6     appender.setLocationInfo(true);
 7     appender.activateOptions();
 8     configAppender(appender);
 9     
10     logTest();
11 }

JDBCAppender

JDBCAppender将日志保存到数据库的表中,由于数据库保存操作是一个比较费时的操作,因而JDBCAppender默认使用缓存机制,当然你也可以设置缓存大小为1实现实时向数据库插入日志。JDBCAppender中的Layout默认只支持PatternLayout,用户可以通过设置自己的PatternLayout,其中ConversionPattern设置成插入数据库的SQL语句或通过setSql()方法设置SQL语句,JDBCAppender内部会创建相应的PatternLayout,如可以设置SQL语句为:

insert into LogTable(Thread, Class, Message) values(“%t”, “%c”, “%m”)

doAppend()方法中,JDBCAppender通过layout获取SQL语句,将LoggingEvent实例插入到数据库中。

 1 protected String databaseURL = "jdbc:odbc:myDB";
 2 protected String databaseUser = "me";
 3 protected String databasePassword = "mypassword";
 4 protected Connection connection = null;
 5 protected String sqlStatement = "";
 6 protected int bufferSize = 1;
 7 protected ArrayList buffer;
 8 protected ArrayList removes;
 9 private boolean locationInfo = false;
10 
11 public void append(LoggingEvent event) {
12     event.getNDC();
13     event.getThreadName();
14     event.getMDCCopy();
15     if (locationInfo) {
16         event.getLocationInformation();
17     }
18     event.getRenderedMessage();
19     event.getThrowableStrRep();
20     buffer.add(event);
21     if (buffer.size() >= bufferSize)
22         flushBuffer();
23 }
24 public void flushBuffer() {
25     removes.ensureCapacity(buffer.size());
26     for (Iterator i = buffer.iterator(); i.hasNext();) {
27         try {
28             LoggingEvent logEvent = (LoggingEvent) i.next();
29             String sql = getLogStatement(logEvent);
30             execute(sql);
31             removes.add(logEvent);
32         } catch (SQLException e) {
33             errorHandler.error("Failed to excute sql", e,
34                     ErrorCode.FLUSH_FAILURE);
35         }
36     }
37     buffer.removeAll(removes);
38     removes.clear();
39 }
40 protected String getLogStatement(LoggingEvent event) {
41     return getLayout().format(event);
42 }
43 protected void execute(String sql) throws SQLException {
44     Connection con = null;
45     Statement stmt = null;
46     try {
47         con = getConnection();
48         stmt = con.createStatement();
49         stmt.executeUpdate(sql);
50     } catch (SQLException e) {
51         if (stmt != null)
52             stmt.close();
53         throw e;
54     }
55     stmt.close();
56     closeConnection(con);
57 }
58 protected Connection getConnection() throws SQLException {
59     if (!DriverManager.getDrivers().hasMoreElements())
60         setDriver("sun.jdbc.odbc.JdbcOdbcDriver");
61     if (connection == null) {
62         connection = DriverManager.getConnection(databaseURL, databaseUser,
63                 databasePassword);
64     }
65     return connection;
66 }
67 protected void closeConnection(Connection con) {
68 }

用户可以编写自己的JDBCAppender,继承自JDBCAppender,重写getConnection()closeConnection(),可以实现从数据库连接池中获取connection,在每次将JDBCAppender缓存中的LoggingEvent列表插入数据库时从连接池中获取缓存,而在该操作完成后将获得的连接释放回连接池。用户也可以重写getLogstatement()以自定义插入LoggingEventSQL语句。

JMSAppender

JMSAppender类将LoggingEvent实例序列化成ObjectMessage,并将其发送到JMS Server的一个指定Topic中。它的实现比较简单,设置相应的connectionFactoryNametopicNameproviderURLuserNamepasswordJMS相应的信息,在activateOptions()方法中创建相应的JMS链接,在doAppend()方法中将LoggingEvent序列化成ObjectMessage发送到JMS Server中,它也可以通过locationInfo字段是否需要计算位置信息。不过这里的实现感觉有一些bug:在序列化LoggingEvent实例之前没有先缓存必要的信息,如threadName,因为这些信息默认是不设置的,具体可以参考JDBCAppenderAsyncAppender等。

  1 String securityPrincipalName;
  2 String securityCredentials;
  3 String initialContextFactoryName;
  4 String urlPkgPrefixes;
  5 String providerURL;
  6 String topicBindingName;
  7 String tcfBindingName;
  8 String userName;
  9 String password;
 10 boolean locationInfo;
 11 
 12 TopicConnection topicConnection;
 13 TopicSession topicSession;
 14 TopicPublisher topicPublisher;
 15 
 16 public void activateOptions() {
 17     TopicConnectionFactory topicConnectionFactory;
 18     try {
 19         Context jndi;
 20         LogLog.debug("Getting initial context.");
 21         if (initialContextFactoryName != null) {
 22             Properties env = new Properties();
 23             env.put(Context.INITIAL_CONTEXT_FACTORY,
 24                     initialContextFactoryName);
 25             if (providerURL != null) {
 26                 env.put(Context.PROVIDER_URL, providerURL);
 27             } else {
 28                 LogLog.warn("You have set InitialContextFactoryName option but not the "
 29                         + "ProviderURL. This is likely to cause problems.");
 30             }
 31             if (urlPkgPrefixes != null) {
 32                 env.put(Context.URL_PKG_PREFIXES, urlPkgPrefixes);
 33             }
 34             if (securityPrincipalName != null) {
 35                 env.put(Context.SECURITY_PRINCIPAL, securityPrincipalName);
 36                 if (securityCredentials != null) {
 37                     env.put(Context.SECURITY_CREDENTIALS,
 38                             securityCredentials);
 39                 } else {
 40                     LogLog.warn("You have set SecurityPrincipalName option but not the "
 41                             + "SecurityCredentials. This is likely to cause problems.");
 42                 }
 43             }
 44             jndi = new InitialContext(env);
 45         } else {
 46             jndi = new InitialContext();
 47         }
 48         LogLog.debug("Looking up [" + tcfBindingName + "]");
 49         topicConnectionFactory = (TopicConnectionFactory) lookup(jndi,
 50                 tcfBindingName);
 51         LogLog.debug("About to create TopicConnection.");
 52         if (userName != null) {
 53             topicConnection = topicConnectionFactory.createTopicConnection(
 54                     userName, password);
 55         } else {
 56             topicConnection = topicConnectionFactory
 57                     .createTopicConnection();
 58         }
 59         LogLog.debug("Creating TopicSession, non-transactional, "
 60                 + "in AUTO_ACKNOWLEDGE mode.");
 61         topicSession = topicConnection.createTopicSession(false,
 62                 Session.AUTO_ACKNOWLEDGE);
 63         LogLog.debug("Looking up topic name [" + topicBindingName + "].");
 64         Topic topic = (Topic) lookup(jndi, topicBindingName);
 65         LogLog.debug("Creating TopicPublisher.");
 66         topicPublisher = topicSession.createPublisher(topic);
 67         LogLog.debug("Starting TopicConnection.");
 68         topicConnection.start();
 69         jndi.close();
 70     } catch (JMSException e) {
 71         errorHandler.error(
 72                 "Error while activating options for appender named ["
 73                         + name + "].", e, ErrorCode.GENERIC_FAILURE);
 74     } catch (NamingException e) {
 75         errorHandler.error(
 76                 "Error while activating options for appender named ["
 77                         + name + "].", e, ErrorCode.GENERIC_FAILURE);
 78     } catch (RuntimeException e) {
 79         errorHandler.error(
 80                 "Error while activating options for appender named ["
 81                         + name + "].", e, ErrorCode.GENERIC_FAILURE);
 82     }
 83 }
 84 
 85 public void append(LoggingEvent event) {
 86     if (!checkEntryConditions()) {
 87         return;
 88     }
 89     try {
 90         ObjectMessage msg = topicSession.createObjectMessage();
 91         if (locationInfo) {
 92             event.getLocationInformation();
 93         }
 94         msg.setObject(event);
 95         topicPublisher.publish(msg);
 96     } catch (JMSException e) {
 97         errorHandler.error("Could not publish message in JMSAppender ["
 98                 + name + "].", e, ErrorCode.GENERIC_FAILURE);
 99     } catch (RuntimeException e) {
100         errorHandler.error("Could not publish message in JMSAppender ["
101                 + name + "].", e, ErrorCode.GENERIC_FAILURE);
102     }
103 }

TelnetAppender

TelnetAppender类将日志消息发送到指定的Socket端口(默认为23),用户可以使用telnet连接以获取日志信息。这里的实现貌似没有考虑到telnet客户端如何退出的问题。另外,在windows中可能默认没有telnet支持,此时只需要到控制面板”->”程序和功能”->”打开或关闭windows功能中大概Telnet服务即可。TelnetAppender使用内部类SocketHandler封装发送日志消息到客户端,如果没有Telnet客户端连接,则日志消息将会直接被抛弃。

 1 private SocketHandler sh;
 2 private int port = 23;
 3 
 4 public void activateOptions() {
 5     try {
 6         sh = new SocketHandler(port);
 7         sh.start();
 8     } catch (InterruptedIOException e) {
 9         Thread.currentThread().interrupt();
10         e.printStackTrace();
11     } catch (IOException e) {
12         e.printStackTrace();
13     } catch (RuntimeException e) {
14         e.printStackTrace();
15     }
16     super.activateOptions();
17 }
18 protected void append(LoggingEvent event) {
19     if (sh != null) {
20         sh.send(layout.format(event));
21         if (layout.ignoresThrowable()) {
22             String[] s = event.getThrowableStrRep();
23             if (s != null) {
24                 StringBuffer buf = new StringBuffer();
25                 for (int i = 0; i < s.length; i++) {
26                     buf.append(s[i]);
27                     buf.append("\r\n");
28                 }
29                 sh.send(buf.toString());
30             }
31         }
32     }
33 }

SocketHandler中,创建一个新的线程以监听指定的端口,如果有Telnet客户端连接过来,则将其加入到connections集合中。这样在send()方法中就可以遍历connections集合,并将日志信息发送到每个连接的Telnet客户端。

 1 private Vector writers = new Vector();
 2 private Vector connections = new Vector();
 3 private ServerSocket serverSocket;
 4 private int MAX_CONNECTIONS = 20;
 5 
 6 public synchronized void send(final String message) {
 7     Iterator ce = connections.iterator();
 8     for (Iterator e = writers.iterator(); e.hasNext();) {
 9         ce.next();
10         PrintWriter writer = (PrintWriter) e.next();
11         writer.print(message);
12         if (writer.checkError()) {
13             ce.remove();
14             e.remove();
15         }
16     }
17 }
18 public void run() {
19     while (!serverSocket.isClosed()) {
20         try {
21             Socket newClient = serverSocket.accept();
22             PrintWriter pw = new PrintWriter(
23                     newClient.getOutputStream());
24             if (connections.size() < MAX_CONNECTIONS) {
25                 synchronized (this) {
26                     connections.addElement(newClient);
27                     writers.addElement(pw);
28                     pw.print("TelnetAppender v1.0 ("
29                             + connections.size()
30                             + " active connections)\r\n\r\n");
31                     pw.flush();
32                 }
33             } else {
34                 pw.print("Too many connections.\r\n");
35                 pw.flush();
36                 newClient.close();
37             }
38         深入Log4J源码之Appender
39     }
40     深入Log4J源码之Appender
41 }

SMTPAppender

SMTPAppender将日志消息以邮件的形式发送出来,默认实现,它会先缓存日志信息,只有当遇到日志级别是ERRORERROR以上的日志消息时才通过邮件的形式发送出来,如果在遇到触发发送的日志发生之前缓存中的日志信息已满,则最早的日志信息会被覆盖。用户可以通过setEvaluatorClass()方法改变触发发送日志的条件。

 1 public void append(LoggingEvent event) {
 2     if (!checkEntryConditions()) {
 3         return;
 4     }
 5     event.getThreadName();
 6     event.getNDC();
 7     event.getMDCCopy();
 8     if (locationInfo) {
 9         event.getLocationInformation();
10     }
11     event.getRenderedMessage();
12     event.getThrowableStrRep();
13     cb.add(event);
14     if (evaluator.isTriggeringEvent(event)) {
15         sendBuffer();
16     }
17 }
18 protected void sendBuffer() {
19     try {
20         String s = formatBody();
21         boolean allAscii = true;
22         for (int i = 0; i < s.length() && allAscii; i++) {
23             allAscii = s.charAt(i) <= 0x7F;
24         }
25         MimeBodyPart part;
26         if (allAscii) {
27             part = new MimeBodyPart();
28             part.setContent(s, layout.getContentType());
29         } else {
30             try {
31                 ByteArrayOutputStream os = new ByteArrayOutputStream();
32                 Writer writer = new OutputStreamWriter(MimeUtility.encode(
33                         os, "quoted-printable"), "UTF-8");
34                 writer.write(s);
35                 writer.close();
36                 InternetHeaders headers = new InternetHeaders();
37                 headers.setHeader("Content-Type", layout.getContentType()
38                         + "; charset=UTF-8");
39                 headers.setHeader("Content-Transfer-Encoding",
40                         "quoted-printable");
41                 part = new MimeBodyPart(headers, os.toByteArray());
42             } catch (Exception ex) {
43                 StringBuffer sbuf = new StringBuffer(s);
44                 for (int i = 0; i < sbuf.length(); i++) {
45                     if (sbuf.charAt(i) >= 0x80) {
46                         sbuf.setCharAt(i, '?');
47                     }
48                 }
49                 part = new MimeBodyPart();
50                 part.setContent(sbuf.toString(), layout.getContentType());
51             }
52         }
53 
54         Multipart mp = new MimeMultipart();
55         mp.addBodyPart(part);
56         msg.setContent(mp);
57 
58         msg.setSentDate(new Date());
59         Transport.send(msg);
60     } catch (MessagingException e) {
61         LogLog.error("Error occured while sending e-mail notification.", e);
62     } catch (RuntimeException e) {
63         LogLog.error("Error occured while sending e-mail notification.", e);
64     }
65 }
66 protected String formatBody() {
67     StringBuffer sbuf = new StringBuffer();
68     String t = layout.getHeader();
69     if (t != null)
70         sbuf.append(t);
71     int len = cb.length();
72     for (int i = 0; i < len; i++) {
73         LoggingEvent event = cb.get();
74         sbuf.append(layout.format(event));
75         if (layout.ignoresThrowable()) {
76             String[] s = event.getThrowableStrRep();
77             if (s != null) {
78                 for (int j = 0; j < s.length; j++) {
79                     sbuf.append(s[j]);
80                     sbuf.append(Layout.LINE_SEP);
81                 }
82             }
83         }
84     }
85     t = layout.getFooter();
86     if (t != null) {
87         sbuf.append(t);
88     }
89     return sbuf.toString();
90 }

SocketAppender

SocketAppender将日志消息(LoggingEvent序列化实例)发送到指定Hostport端口。在创建SocketAppender时,SocketAppender会根据设置的Host和端口建立和远程服务器的链接,并创建ObjectOutputStream实例。

 1 void connect(InetAddress address, int port) {
 2     if (this.address == null)
 3         return;
 4     try {
 5         cleanUp();
 6         oos = new ObjectOutputStream(
 7                 new Socket(address, port).getOutputStream());
 8     } catch (IOException e) {
 9         if (e instanceof InterruptedIOException) {
10             Thread.currentThread().interrupt();
11         }
12         String msg = "Could not connect to remote log4j server at ["
13                 + address.getHostName() + "].";
14         if (reconnectionDelay > 0) {
15             msg += " We will try again later.";
16             fireConnector(); // fire the connector thread
17         } else {
18             msg += " We are not retrying.";
19             errorHandler.error(msg, e, ErrorCode.GENERIC_FAILURE);
20         }
21         LogLog.error(msg);
22     }
23 }

如果创建失败,调用fireConnector()方法,创建一个Connector线程,在每间隔reconnectionDelay(默认值为30000ms,若将其设置为0表示在链接出问题时不创建新的线程检测)的时间里不断重试链接。当链接重新建立后,Connector线程退出并将connector实例置为null以在下一次链接出现问题时创建的Connector线程检测。

 1 实例置为null以在下一次链接出现问题时创建的Connector线程检测。
 2 void fireConnector() {
 3     if (connector == null) {
 4         LogLog.debug("Starting a new connector thread.");
 5         connector = new Connector();
 6         connector.setDaemon(true);
 7         connector.setPriority(Thread.MIN_PRIORITY);
 8         connector.start();
 9     }
10 }
11 
12 class Connector extends Thread {
13     boolean interrupted = false;
14     public void run() {
15         Socket socket;
16         while (!interrupted) {
17             try {
18                 sleep(reconnectionDelay);
19                 LogLog.debug("Attempting connection to "
20                         + address.getHostName());
21                 socket = new Socket(address, port);
22                 synchronized (this) {
23                     oos = new ObjectOutputStream(socket.getOutputStream());
24                     connector = null;
25                     LogLog.debug("Connection established. Exiting connector thread.");
26                     break;
27                 }
28             } catch (InterruptedException e) {
29                 LogLog.debug("Connector interrupted. Leaving loop.");
30                 return;
31             } catch (java.net.ConnectException e) {
32                 LogLog.debug("Remote host " + address.getHostName()
33                         + " refused connection.");
34             } catch (IOException e) {
35                 if (e instanceof InterruptedIOException) {
36                     Thread.currentThread().interrupt();
37                 }
38                 LogLog.debug("Could not connect to "
39                         + address.getHostName() + ". Exception is " + e);
40             }
41         }
42     }
43 }

而后,在每一次日志记录请求时只需将LoggingEvent实例序列化到之前创建的ObjectOutputStream中即可,若该操作失败,则会重新建立Connector线程以隔时检测远程日志服务器可以重新链接。

 1 public void append(LoggingEvent event) {
 2     if (event == null)
 3         return;
 4     if (address == null) {
 5         errorHandler
 6                 .error("No remote host is set for SocketAppender named \""
 7                         + this.name + "\".");
 8         return;
 9     }
10     if (oos != null) {
11         try {
12             if (locationInfo) {
13                 event.getLocationInformation();
14             }
15             if (application != null) {
16                 event.setProperty("application", application);
17             }
18             event.getNDC();
19             event.getThreadName();
20             event.getMDCCopy();
21             event.getRenderedMessage();
22             event.getThrowableStrRep();
23             oos.writeObject(event);
24             oos.flush();
25             if (++counter >= RESET_FREQUENCY) {
26                 counter = 0;
27                 // Failing to reset the object output stream every now and
28                 // then creates a serious memory leak.
29                 // System.err.println("Doing oos.reset()");
30                 oos.reset();
31             }
32         } catch (IOException e) {
33             if (e instanceof InterruptedIOException) {
34                 Thread.currentThread().interrupt();
35             }
36             oos = null;
37             LogLog.warn("Detected problem with connection: " + e);
38             if (reconnectionDelay > 0) {
39                 fireConnector();
40             } else {
41                 errorHandler
42                         .error("Detected problem with connection, not reconnecting.",
43                                 e, ErrorCode.GENERIC_FAILURE);
44             }
45         }
46     }
47 }

SocketNode

Log4J为日志服务器的实现提供了SocketNode类,它接收客户端的链接,并根据配置打印到相关的Appender中。

 1 public class SocketNode implements Runnable {
 2     Socket socket;
 3     LoggerRepository hierarchy;
 4     ObjectInputStream ois;
 5 
 6     public SocketNode(Socket socket, LoggerRepository hierarchy) {
 7         this.socket = socket;
 8         this.hierarchy = hierarchy;
 9         try {
10             ois = new ObjectInputStream(new BufferedInputStream(
11                     socket.getInputStream()));
12         } catch (深入Log4J源码之Appender) {
13             深入Log4J源码之Appender
14         }
15     }
16 
17     public void run() {
18         LoggingEvent event;
19         Logger remoteLogger;
20         try {
21             if (ois != null) {
22                 while (true) {
23                     event = (LoggingEvent) ois.readObject();
24                     remoteLogger = hierarchy.getLogger(event.getLoggerName());
25                     if (event.getLevel().isGreaterOrEqual(
26                             remoteLogger.getEffectiveLevel())) {
27                         remoteLogger.callAppenders(event);
28                     }
29                 }
30             }
31         } catch (深入Log4J源码之Appender) {
32             深入Log4J源码之Appender
33         } finally {
34             if (ois != null) {
35                 try {
36                     ois.close();
37                 } catch (Exception e) {
38                     logger.info("Could not close connection.", e);
39                 }
40             }
41             if (socket != null) {
42                 try {
43                     socket.close();
44                 } catch (InterruptedIOException e) {
45                     Thread.currentThread().interrupt();
46                 } catch (IOException ex) {
47                 }
48             }
49         }
50     }
51 }

事实上,Log4J提供了两个日志服务器的实现类:SimpleSocketServerSocketServer。他们都会接收客户端的连接,为每个客户端链接创建一个SocketNode实例,并根据指定的配置文件打印日志消息。它们的不同在于SimpleSocketServer同时支持xmlproperties配置文件,而SocketServer只支持properties配置文件;另外,SocketServer支持不同客户端使用不同的配置文件(以客户端主机名作为选择配置文件的方式),而SimpleSocketServer不支持。

最后,使用SocketAppender时,在应用程序退出时,最好显示的调用LoggerManager.shutdown()方法,不然如果是通过垃圾回收器来隐式的关闭(finalize()方法)SocketAppender,在Windows平台中可能会存在TCP管道中未传输的数据丢失的情况。另外,在网络连接不可用时,SocketAppender可能会阻塞应用程序,当网络可用,但是远程日志服务器不可用时,相应的日志会被丢失。如果日志传输给远程日志服务器的速度要慢于日志产生速度,此时会影响应用程序性能。这些问题在下一小节的SocketHubAppender中同样存在。

测试代码

可以使用一下代码测试SocketAppenderSocketNode

 1 @Test
 2 public void testSocketAppender() throws Exception {
 3     SocketAppender appender = new SocketAppender(
 4             InetAddress.getLocalHost(), 8000);
 5     appender.setLocationInfo(true);
 6     appender.setApplication("AppenderTest");
 7     appender.activateOptions();
 8     configAppender(appender);
 9     
10     Logger log = Logger.getLogger("levin.log4j.test.TestBasic");
11     for(int i = 0;i < 100; i++) {
12         Thread.sleep(10000);
13         if(i % 2 == 0) {
14             log.info("Normal test深入Log4J源码之Appender.");    
15         } else {
16             log.info("Exception test深入Log4J源码之Appender"new Exception());
17         }
18     }
19 }
20 
21 @Test
22 public void testSimpleSocketServer() throws Exception {
23     ConsoleAppender appender = new ConsoleAppender(new TTCCLayout());
24     appender.activateOptions();
25     configAppender(appender);
26     
27     ServerSocket serverSocket = new ServerSocket(8000);
28     while(true) {
29         Socket socket = serverSocket.accept();
30         new Thread(new SocketNode(socket,
31                 LogManager.getLoggerRepository()),
32                 "SimpleSocketServer-" + 8000).start();
33     }
34 }

SocketHubAppender

SocketHubAppender类似SocketAppender,它也将日志信息(序列化后的LoggingEvent实例)发送到指定的日志服务器,该日志服务器可以是SocketNode支持的服务器。不同的是,SocketAppender指定日志服务器的地址和端口号,而SocketHubAppender并不直接指定日志服务器的地址和端口号,它自己启动一个指定端口的服务器,由日志服务器注册到到该服务器,从而产生一个连接参数,SocketHubAppender根据这些参数发送日志信息到注册的日志服务器,因而SocketHubAppender支持同时发送相同的日志信息到多个日志服务器。

另外,SocketHubAppender还会缓存部分LoggingEvent实力,从而支持在新注册一个日志服务器时,它会先将那些缓存下来的LoggingEvent发送给新注册服务器,然后接受新的LoggingEvent日志打印请求。

具体实现以及注意事项参考SocketAppender。最好补充一点,SocketHubAppender可以和chainsaw一起使用,它好像使用了zeroconf协议,和SocketHubAppender以及SocketAppender中的ZeroConfSupport类相关,不怎么了解这个协议,也没有时间细看了。

LF5Appender

将日志显示在Swing窗口中。对Swing不熟,没怎么看代码,不过可以使用一下测试用例简单的做一些测试,提供一些感觉。

1 @Test
2 public void testLF5Appender() throws Exception {
3     LF5Appender appender = new LF5Appender();
4     appender.setLayout(new TTCCLayout());
5     appender.activateOptions();
6     configAppender(appender);
7     
8     logTest();
9 }

其他Appender

ExternallyRolledFileAppender类继承自RollingFileAppender,因而它最基本的也是基于日志文件大小来备份日志文件。然而它同时还支持外界通过Socket发送“RollOver”给它以实现在特定情况下手动备份日志文件。Log4J提供Roller类来实现这样的功能,其使用方法是:

java -cp log4j-1.2.16.jar org.apache.log4j.varia.Roller <hostname> <port>

但是由于任何可以和应用程序运行的服务器连接的代码都能像该服务器发送“RollOver”消息,因而这种方式并不适合production环境在,在production中最好能加入一些限制信息,比如安全验证等信息。

NTEventLogAppender将日志打印到NT事件日志系统中(NT event log system)。顾名思义,它只能用在Windows中,而且需要NTEventLogAppender.dllNTEventLogAppender.amd64.dllNTEventLogAppender.ia64.dllNTEventLogAppender.x86.dll动态链接库在Windows PATH路径中。在Windows 7中可以通过控制面板->管理工具->查看事件日志中查看相应的日志信息。

SyslogAppender将日志打印到syslog中,我对syslog不怎么了解,通过查看网上信息的结果,看起来syslog只是存在于LinuxUnix中。SyslogAppender额外的需要配置FacilitySyslogHost字段。具体可以查看:http://www.cnblogs.com/frankliiu-java/articles/1754835.htmlhttp://sjsky.iteye.com/blog/962870

NullAppender作为一个Null Object模式存在,不会输出任何打印日志消息。


上一篇:C#对象转json字符串和json字符串转对象


下一篇:【No.7 C++对象的构造与析构时间】