天天看點

深入Log4J源碼之Log4J CoreLogger類Level類LoggerRepository類LoggingEvent類Layout類Appender接口總結

畢業又趕上大學的同學會,還去騎車環了趟崇明島,六月貌似就沒消停過,不過終于這些事情基本上都結束了,我也可以好好的看些書、讀些源碼、寫點部落格了。

log4j将寫日志功能抽象成七個核心類/接口:logger、loggerrepository、level、loggingevent、appender、layout、objectrender。其類圖如下:

深入Log4J源碼之Log4J CoreLogger類Level類LoggerRepository類LoggingEvent類Layout類Appender接口總結

更詳細的,實作log4j主要功能相關的類圖:

深入Log4J源碼之Log4J CoreLogger類Level類LoggerRepository類LoggingEvent類Layout類Appender接口總結

其實log4j最核心的也就5個類:logger用于對日志記錄行為的抽象,提供記錄不同級别日志的接口;level對日志級别的抽象;appender是對記錄日志形式的抽象;layout是對日志行格式的抽象;而loggingevent是對一次日志記錄過程中所能取到資訊的抽象。另外兩個loggerrepository是logger執行個體的容器,而objectrender是對日志執行個體的解析接口,它們主要提供了一種擴充支援。

簡單的一次記錄日志過程的序列圖如下:

深入Log4J源碼之Log4J CoreLogger類Level類LoggerRepository類LoggingEvent類Layout類Appender接口總結

即擷取logger執行個體->判斷logger執行個體對應的日志記錄級别是否要比請求的級别低->若是調用forcelog記錄日志->建立loggingevent執行個體->将loggingevent執行個體傳遞給appender->appender調用layout執行個體格式化日志消息->appender将格式化後的日志資訊寫入該appender對應的日志輸出中。

包含log4j其他子產品類的更詳細序列圖如下:

深入Log4J源碼之Log4J CoreLogger類Level類LoggerRepository類LoggingEvent類Layout類Appender接口總結

在簡單的介紹了log4j各個子產品類的作用後,以下将詳細的介紹各個子產品的具體作用以及代碼實作。

logger是對記錄日志動作的抽象,它提供了記錄不同級别日志的接口,日志資訊可以包含異常資訊也可以不包含:

 1 public void debug(object message) {

 2     if(islevelenabled(level.debug)) {

 3         forcelog(fqcn, level.debug, message, null);

 4     }

 5 }

 6 public void debug(object message, throwable cause) {

 7     if(islevelenabled(level.debug)) {

 8         forcelog(fqcn, level.debug, message, cause);

 9     }

10 }

11 protected void forcelog(string fqcn, level level, object message, throwable t) {

12     callappenders(new loggingevent(fqcn, this, level, message, t));

13 }

logger類包含level資訊 ,如果目前logger未設定level值,它也可以中父節點中繼承下來,該值可以用來控制該logger可以記錄的日志級别:

 1 protected level level;

 2 public level geteffectivelevel() {

 3     for(logger logger = this; logger != null; logger = logger.parent) {

 4         if(logger.level != null) {

 5             return logger.level;

 6         }

 7     }

 8     return null;

 9 }

10 public boolean islevelenabled(level level) {

11     return level.isgreaterorequal(this.geteffectivelevel());

12 }

13 public boolean isdebugenabled() {

14     return islevelenabled(level.debug);

15 }

logger是一個命名的實體,其名字一般用”.”分割以展現不同logger的層次關系,其中level和appender資訊可以從父節點中擷取,因而logger類中還具有name和parent屬性。

1 private string name;

2 protected logger parent;

在某些情況下,我們希望某些logger隻将日志記錄到特定的appender中,而不想記錄在父節點中的appender中,log4j為這種需求提供了additivity屬性,即對目前logger節點,如果其additivity屬性設定為false,則該logger不會繼承父節點的appender資訊,但是其子節點依然會繼承該logger的appender資訊,除非子節點的additivity屬性也設定成了false。

 1 private boolean additive = true;

 2 public void callappenders(loggingevent event) {

 3     int writes = 0;

 4     

 5     for(logger logger = this; logger != null; logger = logger.parent) {

 6         synchronized(logger) {

 7             if(logger.appenders != null) {

 8                 writes += logger.appenders.appendlooponappenders(event);

 9             }

10             if(!logger.additive) {

11                 break;

12             }

13         }

14     }

15     

16     if(writes == 0) {

17         system.err.println("no appender is configed.");

18     }

19 }

最後,為了支援國際化,log4j還提供了兩個l7dlog()方法,通過指定的key,以從資源檔案中擷取消息内容。為了使用這兩個方法,需要設定資源檔案。同樣,資源檔案也是可以從父節點中繼承的。

 1 private resourcebundle resourcebundle;

 2 public void l7dlog(level level, string key, throwable cause) {

 3     if(islevelenabled(level)) {

 4         string message = getresourcebundlestring(key);

 5         if(message == null) {

 6             message = key;

 7         }

 8         forcelog(fqcn, level, message, cause);

11 

12 public void l7dlog(level level, string key, object[] params, throwable cause) {

13     

深入Log4J源碼之Log4J CoreLogger類Level類LoggerRepository類LoggingEvent類Layout類Appender接口總結

14         if(pattern == null) {

15             message = key;

16         } else {

17             message = messageformat.format(pattern, params);

18         }

19     

深入Log4J源碼之Log4J CoreLogger類Level類LoggerRepository類LoggingEvent類Layout類Appender接口總結

20 }

21 

22 protected string getresourcebundlestring(string key) {

23     resourcebundle rb = getresourcebundle();

24     

深入Log4J源碼之Log4J CoreLogger類Level類LoggerRepository類LoggingEvent類Layout類Appender接口總結

25     return rb.getstring(key);

26 } 

27 public resourcebundle getresourcebundle() {

28     for(logger logger = this; logger != null; logger = logger.parent) {

29         if(logger.resourcebundle != null) {

30             return logger.resourcebundle;

31         }

32     }

33     return null;    

34 }

另外,在實際開發中經常會遇到要把日志資訊同時寫到不同地方,如同時寫入檔案和控制台,因而一個logger執行個體中可以包含多個appender,為了管理多個appender,log4j抽象出了appenderattachable接口,它定義了幾個用于管理多個appender執行個體的方法,這些方法由appenderattachableimpl類實作,而logger會執行個體化appenderattachableimpl,并将這些方法代理給該執行個體:

 1 public interface appenderattachable {

 2     public void addappender(appender newappender);

 3     public enumeration getallappenders();

 4     public appender getappender(string name);

 5     public boolean isattached(appender appender);

 6     void removeallappenders();

 7     void removeappender(appender appender);

 8     void removeappender(string name);

在log4j中,所有logger執行個體組成一個單根的樹狀結構,由于logger執行個體的根節點有一點特殊:它的名字為“root”,它沒有父節點,它的level字段必須設值以防止其他logger執行個體都沒有設定level值的情況。基于這些考慮,log4j通過繼承logger類實作了rootlogger類,它用于表達所有logger執行個體的根節點:

 1 public final class rootlogger extends logger {

 2     public rootlogger(level level) {

 3         super("root");

 4         setlevel(level);

 5     }

 6     public final level getchainedlevel() {

 7         return level;

 8     }

 9     public final void setlevel(level level) {

10         if (level == null) {

11             loglog.error("you have tried to set a null level to root.",

12                     new throwable());

13         } else {

14             this.level = level;

15         }

16     }

17 }

有時候,為了測試等其他需求,我們希望logger本身不做什麼事情,log4j為這種需求提供了noplogger類,它繼承自logger,但是基本上的方法都為空。

level是對日志級别的抽象,目前log4j支援的級别有fatal、error、warn、info、debug、trace,從頭到尾一次級别遞減,另外log4j還支援兩種特殊的級别:all和off,它們分别表示打開和關閉日志功能。

 1 public static final int off_int = integer.max_value;

 2 public static final int fatal_int = 50000;

 3 public static final int error_int = 40000;

 4 public static final int warn_int  = 30000;

 5 public static final int info_int  = 20000;

 6 public static final int debug_int = 10000;

 7 public static final int trace_int = 5000;

 8 public static final int all_int = integer.min_value;

 9 

10 public static final level off = new level(off_int, "off", 0);

11 public static final level fatal = new level(fatal_int, "fatal", 0);

12 public static final level error = new level(error_int, "error", 3);

13 public static final level warn = new level(warn_int, "warn", 4);

14 public static final level info = new level(info_int, "info", 6);

15 public static final level debug = new level(debug_int, "debug", 7);

16 public static final level trace = new level(trace_int, "trace", 7);

17 public static final level all = new level(all_int, "all", 7);

每個level執行個體包含了該level代表的int值(一般是從級别低到級别高一次增大)、該level的string表達、該level和系統level的對應值。

1 protected transient int level;

2 protected transient string levelstr;

3 protected transient int syslogequivalent;

4 protected level(int level, string levelstr, int syslogequivalent) {

5     this.level = level;

6     this.levelstr = levelstr;

7     this.syslogequivalent = syslogequivalent;

8 }

level類主要提供了判斷哪個level級别更高的方法isgreaterorequal()以及将int值或string值轉換成level執行個體的tolevel()方法:

 1 public boolean isgreaterorequal(level level) {

 2     return this.level >= level.level;

 3 }

 4 public static level tolevel(int level) {

 5     return tolevel(level, debug);

 6 }

 7 public static level tolevel(int level, level defaultlevel) {

 8     switch(level) {

 9         case off_int: return off;

10         case fatal_int: return fatal;

11         case error_int: return error;

12         case warn_int: return warn;

13         case info_int: return info;

14         case debug_int: return debug;

15         case trace_int: return trace;

16         case all_int: return all;

17     }

18     return defaultlevel;

另外,由于對相同級别的level執行個體來說,它必須是單例的,因而log4j對序列化和反序列化做了一些處理。即它的三個成員都是transient,真正序列化和反序列化的代碼自己寫,并且加入readresolve()方法的支援,以保證反序列化出來的相同級别的level執行個體是相同的執行個體。

 1 private void readobject(final objectinputstream input) throws ioexception, classnotfoundexception {

 2     input.defaultreadobject();

 3     level = input.readint();

 4     syslogequivalent = input.readint();

 5     levelstr = input.readutf();

 6     if(levelstr == null) {

 7         levelstr = "";

10 private void writeobject(final objectoutputstream output) throws ioexception {

11     output.defaultwriteobject();

12     output.writeint(level);

13     output.writeint(syslogequivalent);

14     output.writeutf(levelstr);

16 private object readresolve() throws objectstreamexception {

17     if(this.getclass() == level.class) {

18         return tolevel(level);

19     }

20     return this;

21 }

如果要實作自己的level類,可以繼承自level,并且實作相應的靜态tolevel()方法即可。關于如何實作自己的level類将會在配置檔案相關小節中詳細讨論。

loggerrepository從概念以及字面上來說它就是一個logger執行個體的容器:一方面相同名字的logger執行個體隻需要建立一次,在後面的使用中,隻需要從這個容器中取即可;另一方面,logger容器可以存放從配置檔案中解析出來的資訊,進而使配置資訊可以無縫的應用到log4j内部系統中;最後logger容器還為維護logger的樹狀層次結構提供了方面,每個logger隻維護父節點的資訊,有了logger容器的存在則可以很容易的找到一個新的logger執行個體的父節點;關于logger容器将在下一節中詳細講解。

loggingevent個人感覺用loggingcontext更合适一些,它是對一次日志記錄時哪能擷取到的資料的封裝。它包含了以下資訊以提供layout在format()方法中使用:

1.       fqnofcategoryclass:日志記錄接口(預設為logger)的類全名,該資訊主要用于計算日志記錄點的源檔案、調用方法以及行号等位置資訊。

2.       locationinfo:通過fqnofcategoryclass計算位置資訊,位置資訊的計算由locationinfo類實作,這些資訊可以提供給layout使用。

3.       logger:目前來看主要是通過logger執行個體取得logrepository執行個體,并通過logrepository取得注冊的objectrender執行個體,如果有的話。

4.       loggername:目前日志記錄的logger名稱,提供給layout使用。

5.       threadname:目前線程名,提供給layout使用。

6.       level:目前日志的級别,提供給layout使用。

7.       message:目前日志類,一般是string類型,但是也可以通過注冊objectrender,然後傳入相應的其他對象類型。

8.       renderedmessage:經過objectrender處理後的日志資訊,提供給layout使用。

9.       throwableinfo:異常資訊,如果存在的話,提供給layout使用。

10.   timestamp:建立loggingevent執行個體的時間,提供給layout使用。

11.   其他相對不常用的資訊将會在後面小節中講解。

loggingevent隻是一個簡單的資料對象(do),因而其實作還是比較簡單的,即在建立執行個體時将資料提供給它,在其他類(layout等)使用它時通過getxxx()方法取資料。不過還是有幾個方法可以簡單的講解一下。

locationinfo所指的位置資訊主要包括記錄日志所在的源檔案名、類名、方法名、所在源檔案的行号。

1     transient string linenumber;

2     transient string filename;

3     transient string classname;

4     transient string methodname;

5     //fully.qualified.classname.of.caller.methodname(filename.java:line)

6     public string fullinfo;

我們知道在異常棧中每一條記錄都包含了方法調用對應的這些資訊,log4j的這些資訊正是利用了這個原理,即通過建構一個throwable執行個體,而後在該throwable的棧資訊中解析出來的:

1 public locationinfo getlocationinformation() {

2     if (locationinfo == null) {

3         locationinfo = new locationinfo(new throwable(), 

4 fqnofcategoryclass);

5     }

6     return locationinfo;

7 }

以上throwable一般會産生如下異常棧:

1 java.lang.throwable

深入Log4J源碼之Log4J CoreLogger類Level類LoggerRepository類LoggingEvent類Layout類Appender接口總結

3 at org.apache.log4j.patternlayout.format(patternlayout.java:413)

4 at org.apache.log4j.fileappender.doappend(fileappender.java:183)

5 at org.apache.log4j.category.callappenders(category.java:131)

6 at org.apache.log4j.category.log(category.java:512)

7 at callers.fully.qualified.classname.methodname(filename.java:74)

深入Log4J源碼之Log4J CoreLogger類Level類LoggerRepository類LoggingEvent類Layout類Appender接口總結

因而我們就可以通過callers.fully.qualified.classname資訊來找到改行資訊,這個classname資訊即是傳入的fqnofcategoryclass。

如果目前jdk版本是1.4以上,我們就可以通過jdk提供的一些方法來查找:

 1 getstacktracemethod = throwable.class.getmethod("getstacktrace",

 2         noargs);

 3 class stacktraceelementclass = class

 4         .forname("java.lang.stacktraceelement");

 5 getclassnamemethod = stacktraceelementclass.getmethod(

 6         "getclassname", noargs);

 7 getmethodnamemethod = stacktraceelementclass.getmethod(

 8         "getmethodname", noargs);

 9 getfilenamemethod = stacktraceelementclass.getmethod("getfilename",

10         noargs);

11 getlinenumbermethod = stacktraceelementclass.getmethod(

12         "getlinenumber", noargs);

13 

14 object[] noargs = null;

15 object[] elements = (object[]) getstacktracemethod.invoke(t,

16         noargs);

17 string prevclass = na;

18 for (int i = elements.length - 1; i >= 0; i--) {

19     string thisclass = (string) getclassnamemethod.invoke(

20             elements[i], noargs);

21     if (fqnofcallingclass.equals(thisclass)) {

22         int caller = i + 1;

23         if (caller < elements.length) {

24             classname = prevclass;

25             methodname = (string) getmethodnamemethod.invoke(

26                     elements[caller], noargs);

27             filename = (string) getfilenamemethod.invoke(

28                     elements[caller], noargs);

29             if (filename == null) {

30                 filename = na;

31             }

32             int line = ((integer) getlinenumbermethod.invoke(

33                     elements[caller], noargs)).intvalue();

34             if (line < 0) {

35                 linenumber = na;

36             } else {

37                 linenumber = string.valueof(line);

38             }

39             stringbuffer buf = new stringbuffer();

40             buf.append(classname);

41             buf.append(".");

42             buf.append(methodname);

43             buf.append("(");

44             buf.append(filename);

45             buf.append(":");

46             buf.append(linenumber);

47             buf.append(")");

48             this.fullinfo = buf.tostring();

49         }

50         return;

51     }

52     prevclass = thisclass;

53 }

否則,則需要我們通過字元串查找的方式來查找:

 1 string s;

 2 // protect against multiple access to sw.

 3 synchronized (sw) {

 4     t.printstacktrace(pw);

 5     s = sw.tostring();

 6     sw.getbuffer().setlength(0);

 7 }

 8 int ibegin, iend;

 9 ibegin = s.lastindexof(fqnofcallingclass);

10 if (ibegin == -1)

11     return;

12 // see bug 44888.

13 if (ibegin + fqnofcallingclass.length() < s.length()

14         && s.charat(ibegin + fqnofcallingclass.length()) != '.') {

15     int i = s.lastindexof(fqnofcallingclass + ".");

16     if (i != -1) {

17         ibegin = i;

20 

21 ibegin = s.indexof(layout.line_sep, ibegin);

22 if (ibegin == -1)

23     return;

24 ibegin += layout.line_sep_len;

25 

26 // determine end of line

27 iend = s.indexof(layout.line_sep, ibegin);

28 if (iend == -1)

29     return;

30 

31 // va has a different stack trace format which doesn't

32 // need to skip the inital 'at'

33 if (!invisualage) {

34     // back up to first blank character

35     ibegin = s.lastindexof("at ", iend);

36     if (ibegin == -1)

37         return;

38     // add 3 to skip "at ";

39     ibegin += 3;

40 }

41 // everything between is the requested stack item

42 this.fullinfo = s.substring(ibegin, iend);

對于通過字元串查找到的fullinfo值,在擷取其他單個值時還需要做相應的字元串解析:

classname:

 1 // starting the search from '(' is safer because there is

 2 // potentially a dot between the parentheses.

 3 int iend = fullinfo.lastindexof('(');

 4 if (iend == -1)

 5     classname = na;

 6 else {

 7     iend = fullinfo.lastindexof('.', iend);

 8 

 9     // this is because a stack trace in visualage looks like:

10 

11     // java.lang.runtimeexception

12     // java.lang.throwable()

13     // java.lang.exception()

14     // java.lang.runtimeexception()

15     // void test.test.b.print()

16     // void test.test.a.printindirect()

17     // void test.test.run.main(java.lang.string [])

18     int ibegin = 0;

19     if (invisualage) {

20         ibegin = fullinfo.lastindexof(' ', iend) + 1;

21     }

22 

23     if (iend == -1)

24         classname = na;

25     else

26         classname = this.fullinfo.substring(ibegin, iend);

filename:

2 int iend = fullinfo.lastindexof(':');

3 if (iend == -1)

4     filename = na;

5 else {

6     int ibegin = fullinfo.lastindexof('(', iend - 1);

7     filename = this.fullinfo.substring(ibegin + 1, iend);

linenumber:

1 int iend = fullinfo.lastindexof(')');

2 int ibegin = fullinfo.lastindexof(':', iend - 1);

3 if (ibegin == -1)

4     linenumber = na;

5 else

6     linenumber = this.fullinfo.substring(ibegin + 1, iend);

methodname:

1 int iend = fullinfo.lastindexof('(');

2 int ibegin = fullinfo.lastindexof('.', iend);

4     methodname = na;

6     methodname = this.fullinfo.substring(ibegin + 1, iend);

log4j中,對傳入的message執行個體,如果是非string類型,會先使用注冊的objectrender(在logrepository中查找注冊的objectrender資訊)處理成string後傳回,若沒有找到相應的objectrender,則使用預設的objectrender,它隻是調用該消息執行個體的tostring()方法。

 1 public object getmessage() {

 2     if (message != null) {

 3         return message;

 4     } else {

 5         return getrenderedmessage();

 6     }

 8 public string getrenderedmessage() {

 9     if (renderedmessage == null && message != null) {

10         if (message instanceof string)

11             renderedmessage = (string) message;

12         else {

13             loggerrepository repository = logger.getloggerrepository();

14 

15             if (repository instanceof renderersupport) {

16                 renderersupport rs = (renderersupport) repository;

17                 renderedmessage = rs.getrenderermap()

18                         .findandrender(message);

19             } else {

20                 renderedmessage = message.tostring();

21             }

22         }

23     }

24     return renderedmessage;

25 }

throwableinformation類用以處理異常棧資訊,即通過throwable執行個體擷取異常棧字元串數組。同時還支援自定義的throwablerender(在logrepository中設定),預設的throwablerender通過系統printstacktrace()方法來擷取資訊:

 1 if (throwable != null) {

 2     this.throwableinfo = new throwableinformation(throwable, logger);

 4 throwablerenderer renderer = null;

 5 if (category != null) {

 6     loggerrepository repo = category.getloggerrepository();

 7     if (repo instanceof throwablerenderersupport) {

 8         renderer = ((throwablerenderersupport) repo)

 9                 .getthrowablerenderer();

10     }

11 }

12 if (renderer == null) {

13     rep = defaultthrowablerenderer.render(throwable);

14 } else {

15     rep = renderer.dorender(throwable);

16 }

17 public static string[] render(final throwable throwable) {

18     stringwriter sw = new stringwriter();

19     printwriter pw = new printwriter(sw);

20     try {

21         throwable.printstacktrace(pw);

22     } catch (runtimeexception ex) {

24     pw.flush();

25     linenumberreader reader = new linenumberreader(new stringreader(

26             sw.tostring()));

27     arraylist lines = new arraylist();

28     try {

29         string line = reader.readline();

30         while (line != null) {

31             lines.add(line);

32             line = reader.readline();

33         }

34     } catch (ioexception ex) {

35         if (ex instanceof interruptedioexception) {

36             thread.currentthread().interrupt();

37         }

38         lines.add(ex.tostring());

39     }

40     string[] temprep = new string[lines.size()];

41     lines.toarray(temprep);

42     return temprep;

43 }

layout負責将loggingevent中的資訊格式化成一行日志資訊。對不同格式的日志可能還需要提供頭和尾等資訊。另外有些layout不會處理異常資訊,此時ignoresthrowable()方法傳回false,并且異常資訊需要appender來處理,如patternlayout。

 1 public abstract class layout implements optionhandler {

 2     public final static string line_sep = system.getproperty("line.separator");

 3     public final static int line_sep_len = line_sep.length();

 4     abstract public string format(loggingevent event);

 5     public string getcontenttype() {

 6         return "text/plain";

 8     public string getheader() {

 9         return null;

11     public string getfooter() {

12         return null;

13     }

14     abstract public boolean ignoresthrowable();

layout的實作比較簡單,如simplelayout對一行日志資訊隻是列印日志級别資訊以及日志資訊。

 1 public class simplelayout extends layout {

 2     stringbuffer sbuf = new stringbuffer(128);

 3     public simplelayout() {

 5     public void activateoptions() {

 7     public string format(loggingevent event) {

 8         sbuf.setlength(0);

 9         sbuf.append(event.getlevel().tostring());

10         sbuf.append(" - ");

11         sbuf.append(event.getrenderedmessage());

12         sbuf.append(line_sep);

13         return sbuf.tostring();

15     public boolean ignoresthrowable() {

16         return true;

18 }

關于layout更詳細的資訊将會在以後小節中介紹。

appender負責定義日志輸出的目的地,它可以是控制台(consoleappender)、檔案(fileappender)、jms伺服器(jmslogappender)、以email的形式發送出去(smtpappender)等。appender是一個命名的實體,另外它還包含了對layout、errorhandler、filter等引用:

 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(在additivity為true的情況下),調用doappend()方法,實作日志的寫入。在doappend方法中,若目前appender注冊了filter,則doappend還會判斷目前日志時候通過了filter的過濾,通過了filter的過濾後,如果目前appender繼承自skeletonappender,還會檢查目前日志級别時候要比目前appender本身的日志級别閥門要打,所有這些都通過後,才會将loggingevent執行個體傳遞給layout執行個體以格式化成一行日志資訊,最後寫入相應的目的地,在這些操作中,任何出現的錯誤都由errorhandler字段來處理。

目前log4j實作的appender都繼承自skeletonappender類,該類對appender接口提供了最基本的實作,并且引入了threshold的概念,即所有的比目前appender定義的日志級别閥指要大的日志才會記錄下來。

 1 public abstract class appenderskeleton implements appender, optionhandler {

 2     protected layout layout;

 3     protected string name;

 4     protected priority threshold;

 5     protected errorhandler errorhandler = new onlyonceerrorhandler();

 6     protected filter headfilter;

 7     protected filter tailfilter;

 8     protected boolean closed = false;

 9     public appenderskeleton() {

10         super();

11     }

12     public void activateoptions() {

14     abstract protected void append(loggingevent event);

15     public boolean isassevereasthreshold(priority priority) {

16         return ((threshold == null) || priority.isgreaterorequal(threshold));

18     public synchronized void doappend(loggingevent event) {

19         if (closed) {

20             loglog.error("attempted to append to closed appender named ["

21                     + name + "].");

22             return;

23         }

24         if (!isassevereasthreshold(event.getlevel())) {

25             return;

26         }

27         filter f = this.headfilter;

28         filter_loop: while (f != null) {

29             switch (f.decide(event)) {

30             case filter.deny:

31                 return;

32             case filter.accept:

33                 break filter_loop;

34             case filter.neutral:

35                 f = f.getnext();

36             }

38         this.append(event);

40 public void finalize() {

41         if (this.closed)

42             return;

43         loglog.debug("finalizing appender named [" + name + "].");

44         close();

45     }

46 }

skeletonappender實作了doappend()方法,它首先檢查日志級别是否要比threshold要大;然後如果注冊了filter,則使用filter對loggingevent執行個體進行過濾,如果filter傳回filter.deny則doappend()退出,否則執行append()方法,該方法由子類實作。

在log4j中,filter組成一條鍊,它定了以decide()方法,由子類實作,若傳回deny則日志不會被記錄、neutral則繼續檢查下一個filter執行個體、accept則filter通過,繼續執行後面的寫日志操作。使用filter可以為appender加入一些出了threshold以外的其他邏輯,由于它本身是鍊狀的,而且它的執行是橫跨在appender的doappend方法中,因而這也是一個典型的aop的概念。filter的實作将會在下一小節中講解。

skeletonappender還重寫了finalize()方法,這是因為log4j本身作為一個元件,它可能還是通過其他元件如commons-logging或slf4j元件間接的引入,因而使用它的程式不應該對它存在依賴的,然而在程式退出之前所有的appender需要調用close()方法以釋放它所占據的資源,為了不在使用log4j的程式手動的close()的方法,以減少log4j代碼的侵入性,因而log4j将close()的方法調用加入到finalize()方法中,即在垃圾回收器回收appender執行個體時就會調用它的close()方法。

writerappender将日志寫入java io中,它繼承自skeletonappender類。它引入了三個字段:immediateflush,指定沒寫完一條日志後,即将日志内容重新整理到裝置中,雖然這麼做會有一點性能上的損失,但是如果不怎麼做,則會出現在程式異常終止的時候無法看到部分日志資訊,而經常這些丢失的日志資訊要用于分析為什麼會出現異常終止的情況,因而一般推薦将該值設定為true,即預設值;econding用于定義日志文本的編碼方式;qw定義寫日志的writer,它可以是檔案或是控制台等java io支援的流。

在寫日志文本前,writerappender還會做其他檢查,如該appender不能已經closed、qw和layout必須有值等,而後才可以将layout格式化後的日志行寫入裝置中。若layout本身不處理異常問題,則有appender處理異常問題。最後如果每行日志需要重新整理,則調用重新整理操作。

 1 public class writerappender extends appenderskeleton {

 2     protected boolean immediateflush = true;

 3     protected string encoding;

 4     protected quietwriter qw;

 5     public writerappender() {

 7     public writerappender(layout layout, outputstream os) {

 8         this(layout, new outputstreamwriter(os));

10     public writerappender(layout layout, writer writer) {

11         this.layout = layout;

12         this.setwriter(writer);

14     public void append(loggingevent event) {

15         if (!checkentryconditions()) {

16             return;

17         }

18         subappend(event);

20     protected boolean checkentryconditions() {

21         if (this.closed) {

22             loglog.warn("not allowed to write to a closed appender.");

23             return false;

24         }

25         if (this.qw == null) {

26             errorhandler

27                     .error("no output stream or file set for the appender named ["

28                             + name + "].");

29             return false;

30         }

31         if (this.layout == null) {

32             errorhandler.error("no layout set for the appender named [" + name

33                     + "].");

34             return false;

35         }

36         return true;

37     }

38     protected void subappend(loggingevent event) {

39         this.qw.write(this.layout.format(event));

40         if (layout.ignoresthrowable()) {

41             string[] s = event.getthrowablestrrep();

42             if (s != null) {

43                 int len = s.length;

44                 for (int i = 0; i < len; i++) {

45                     this.qw.write(s[i]);

46                     this.qw.write(layout.line_sep);

47                 }

48             }

50         if (shouldflush(event)) {

51             this.qw.flush();

52         }

53     }

54     public boolean requireslayout() {

55         return true;

56     }

57 }

consoleappender繼承自writerappender,它隻是簡單的将system.out或system.err執行個體傳遞給writerappender以建構相應的writer,最後實作将日志寫入到控制台中。

在log4j中,filter組成一條鍊,它定了以decide()方法,由子類實作,若傳回deny則日志不會被記錄、neutral則繼續檢查下一個filter執行個體、accept則filter通過,繼續執行後面的寫日志操作。使用filter可以為appender加入一些出了threshold以外的其他邏輯,由于它本身是鍊狀的,而且它的執行是橫跨在appender的doappend方法中,因而這也是一個典型的aop的概念。

 1 public abstract class filter implements optionhandler {

 2     public filter next;

 3     public static final int deny = -1;

 4     public static final int neutral = 0;

 5     public static final int accept = 1;

 6     public void activateoptions() {

 8     abstract public int decide(loggingevent event);

 9     public void setnext(filter next) {

10         this.next = next;

12     public filter getnext() {

13         return next;

log4j本身提供了四個filter:denyallfilter、levelmatchfilter、levelrangefilter、stringmatchfilter。

denyallfilter隻是簡單的在decide()中傳回deny值,可以将其應用在filter鍊尾,實作如果之前的filter都沒有通過,則該loggingevent沒有通過,類似或的操作:

1 public class denyallfilter extends filter {

2     public int decide(loggingevent event) {

3         return filter.deny;

4     }

5 }

stringmatchfilter通過日志消息中的字元串來判斷filter後的狀态:

 1 public class stringmatchfilter extends filter {

 2     boolean acceptonmatch = true;

 3     string stringtomatch;

 4     public int decide(loggingevent event) {

 5         string msg = event.getrenderedmessage();

 6         if (msg == null || stringtomatch == null)

 7             return filter.neutral;

 8         if (msg.indexof(stringtomatch) == -1) {

 9             return filter.neutral;

10         } else { // we've got a match

11             if (acceptonmatch) {

12                 return filter.accept;

13             } else {

14                 return filter.deny;

15             }

16         }

levelmatchfilter判斷日志級别是否和設定的級别比對以決定filter後的狀态:

 1 public class levelmatchfilter extends filter {

 2     boolean acceptonmatch = true;    

 3 level leveltomatch;

 5         if (this.leveltomatch == null) {

 6             return filter.neutral;

 8         boolean matchoccured = false;

 9         if (this.leveltomatch.equals(event.getlevel())) {

10             matchoccured = true;

11         }

12         if (matchoccured) {

13             if (this.acceptonmatch)

14                 return filter.accept;

15             else

16                 return filter.deny;

17         } else {

18             return filter.neutral;

19         }

20     }

levelrangefilter判斷日志級别是否在設定的級别範圍内以決定filter後的狀态:

 1 public class levelrangefilter extends filter {

 2     boolean acceptonmatch = false;

 3     level levelmin;

 4     level levelmax;

 5     public int decide(loggingevent event) {

 6         if (this.levelmin != null) {

 7             if (event.getlevel().isgreaterorequal(levelmin) == false) {

 8                 return filter.deny;

10         }

11         if (this.levelmax != null) {

12             if (event.getlevel().toint() > levelmax.toint()) {

13                 return filter.deny;

14             }

16         if (acceptonmatch) {

17             return filter.accept;

18         } else {

19             return filter.neutral;

20         }

22 }

這一系列終于是結束了。本文主要介紹了log4j核心類的實作和他們之間的互動關系。涉及到各個子產品本身的其他詳細資訊将會在接下來的小節中詳細介紹,如logrepository與配置資訊、appender類結構的詳細資訊、layout類結構的詳細資訊以及部分loggingevent提供的進階功能。而像level、logger本身,由于内容不多,已經在這一小節中全部介紹完了。