好久没更新博客了,嘿嘿!主要也是因为这段时间比较忙,一直都忙于需求的理解,编码,测试.....反正一系列很操蛋的事情!当然忙的收获就是在强迫的环境中让你学更多的知识,这个我会在后面的文章中提及,今天就先从重写DBCAppender说起!先说下这个问题的背景,现在的项目中要求把日志信息写到文件的同时也把其写入数据库中,以便日后的备查,从而改变了日志文件只能通过简单的文件搜索命令(如在linux中的less命令等)来查找的状况,好了,可以说最高目的是很好的。在看看实现的方式,只要在log4j.xml文件中加入如下配置:
<appender name="DB_INFO" class="org.apache.log4j.jdbc.JDBCAppender"> <param name="Threshold" value="INFO"/> <param name="BufferSize" value="1"/> <!-- 本地 --> <param name="URL" value="jdbc:oracle:thin:@192.168.100.231:1522:mpptest"/> <param name="driver" value="oracle.jdbc.driver.OracleDriver"/> <param name="user" value="gmcc"/> <param name="password" value="skywin"/> <!-- 生产机 <param name="URL" value="jdbc:oracle:thin:@192.168.101.4:1521:gmcctes"/> <param name="driver" value="oracle.jdbc.driver.OracleDriver"/> <param name="user" value="gmcc"/> <param name="password" value="gmcc"/> --> <param name="sql" value="INSERT INTO RE_GLOBAL_LOG(currtime,currthread,currlevel,currcode,currmsg) VALUES ('%d{yyyy-MM-dd HH:mm:ss}', '%t', '%p', '%l', '%m')"/> <filter class="org.apache.log4j.varia.LevelRangeFilter"> <param name="levelMin" value="INFO" /> <param name="levelMax" value="INFO" /> <param name="AcceptOnMatch" value="true" /> </filter> </appender>
然后在代码中调用log.info("abcd");等就可以把信息写入数据库了;需要说明的是这样的配置在本地运行(如一般的写个main方法后直接运行)和在tomcat服务器中运行是可以的,是没有问题的,但是当其放到weblogic服务器上的时候,问题出现了,运行了半天日志的信息就是写不进去,而其他功能完全正常,那么问题出现在哪里呢?初步的猜想是执行的sql语句出了问题,没办法只好看看jdbcAppender的源代码,这里首先看看 execute(String sql)方法,在这里建议如果不熟悉的话,直接自己写一个类继承JDBCAppender,尽可能的重写它里面的所有方法即可,这里的重写直接调用父类的对应方法就可以了,不用写那么复杂。在重写的execute(String sql)中把要执行的sql语句打印出来,看了下,果然是这样,要执行的sql语句里面包含了单引号,而我们知道要插入数据库中单引号是要进行转义处理的,在这里出现单引号的是%t参数,也就是获得线程名的时候。问题到了这里,如果说就想为了实现功能,可以直接将得到的sql进行替换,即对所有的单引号进行转义就可以了!但是出于好奇心,我还是往下看下源代码,首先发现上述说的execute(String sql)是在一个flushBuffer()方法里面被执行的,flushBuffer()方法的代码如下
在自己重写的flushBuffer方法中观察到sql语句(就是很无耻的在各个为位置打印出jdbcAppender的getsql方法)在 getLogStatement(logEvent)的前后发生了变化,到了这里问题已经很明显了 ,就是getLogStatement(logEvent)方法对sql进行了处理,那么到底进行了怎么样的处理呢,接着看这个方法的源代码,代码如下:
protected String getLogStatement(LoggingEvent event) { return super.getLayout().format(event); }
代码在简单不过了,就一句话,就调用了一个logout的format方法,而现在的关键就是这个layout是哪里来的,通过追踪发现在jdbcAppender的setSql方法中对logout进行了赋值,代码如下:
public void setSql(String s) { this.sqlStatement = s; if (super.getLayout() == null) { super.setLayout(new PatternLayout(s)); } else ((PatternLayout)super.getLayout()).setConversionPattern(s); }
在这里意思就是说如果在log4j.xml文件中没有为jdbcAppender配置patterLayout那么会自动扔一个PatternLayout给jdbdAppender,好了,到了这里不用说接下来就看PatternLayout的format方法了,代码如下:
public String format(LoggingEvent event) { if (this.sbuf.capacity() > 1024) this.sbuf = new StringBuffer(256); else { this.sbuf.setLength(0); } PatternConverter c = this.head; while (c != null) { c.format(this.sbuf, event); c = c.next; } return this.sbuf.toString(); }
代码也不难,最核心的地方就是一个while循环,然后不断调用一个c.format方法就完事了,这下就要关注这个c是啥东西了,首先它就是this.head,而搜索下jdbdAppend的源代码,终于发现this.head是在哪里被赋值了,其实也是粗心了一点,如果刚刚在看setsql方法的时候细心的往下看就会发现在
super.setLayout(new PatternLayout(s));
中PatterLayout的构造方法是带参数的,而现在看下这个带参数的方法做了怎么?看下代码
public PatternLayout(String pattern) { this.BUF_SIZE = 256; this.MAX_CAPACITY = 1024; this.sbuf = new StringBuffer(256); this.pattern = pattern; this.head = createPatternParser(pattern).parse(); }
呵呵,终于发现给this.head赋值的地方了,不用说 直接看createPatternParser(pattern).parse()方法,代码很长,如下:
public PatternConverter parse() { char c; this.i = 0; while (true) { while (true) { if (this.i >= this.patternLength) break label572; c = this.pattern.charAt(this.i++); switch (this.state) { case 0: if (this.i != this.patternLength) break; this.currentLiteral.append(c); case 1: case 4: case 3: case 5: case 2: } } if (c == '%') { switch (this.pattern.charAt(this.i)) { case '%': this.currentLiteral.append(c); this.i += 1; break; case 'n': this.currentLiteral.append(Layout.LINE_SEP); this.i += 1; break; default: if (this.currentLiteral.length() != 0) { addToList(new LiteralPatternConverter(this.currentLiteral.toString())); } this.currentLiteral.setLength(0); this.currentLiteral.append(c); this.state = 1; this.formattingInfo.reset(); continue; this.currentLiteral.append(c); continue; this.currentLiteral.append(c); switch (c) { case '-': this.formattingInfo.leftAlign = true; break; case '.': this.state = 3; break; default: if ((c >= '0') && (c <= '9')) { this.formattingInfo.min = (c - '0'); this.state = 4; } else { finalizeConverter(c); continue; this.currentLiteral.append(c); if ((c >= '0') && (c <= '9')) { this.formattingInfo.min = (this.formattingInfo.min * 10 + c - '0'); } else if (c == '.') { this.state = 3; } else { finalizeConverter(c); continue; this.currentLiteral.append(c); if ((c >= '0') && (c <= '9')) { this.formattingInfo.max = (c - '0'); this.state = 5; } else { LogLog.error("Error occured in position " + this.i + ".\n Was expecting digit, instead got char \"" + c + "\"."); this.state = 0; continue; this.currentLiteral.append(c); if ((c >= '0') && (c <= '9')) { this.formattingInfo.max = (this.formattingInfo.max * 10 + c - '0'); } else { finalizeConverter(c); this.state = 0; } } } } } } } } if (this.currentLiteral.length() != 0) { label572: addToList(new LiteralPatternConverter(this.currentLiteral.toString())); } return this.head; }
上面的代码是比较长,但是功能其实不难,大概的意思就是把log4j.xml中写到的sql语句,即"INSERT INTO RE_GLOBAL_LOG(currtime,currthread,currlevel,currcode,currmsg) VALUES ('%d{yyyy-MM-dd HH:mm:ss}', '%t', '%p', '%l', '%m')"中的t,p,l,m等等都搞成一个PatternConverter,并返回第一个PatternConverter,也就是说到底就是一个链表,而this.head是这个链表的头元素,哈哈,到了这里终于明白数据结构等那些基础知识的重要性了!到了这里,主要就是关注各个PatternConverter的format方法了 ,很显然是这些PatternConverter的format方法里面出现了单引号等特殊字符,最后发现当遇到sql中%t的时候被解成了BasicPatternConverter,代码如下
case 't': pc = new BasicPatternConverter(this.formattingInfo, 2001); this.currentLiteral.setLength(0); break;
再看2001到底干了啥,看下代码
case 2001: return event.getThreadName();
很简单把,就是获得线程的名字,我的本意是在return event.getThreadName()返回前对单引号进行替换,但是发现BasicPatternConverter这个是私有的内部类(private),看来还是挺难搞的,看来没得搞了,只好重写event的getThreadName方法,就是自己写一个类扩展LoggingEvent,重写它的getThreadName方法,代码也不难了,在这里就简单的写下把:
public class BPSLoggingEvent extends LoggingEvent { private static final long serialVersionUID = -1405129465403337629L; public BPSLoggingEvent(String fqnOfCategoryClass, Category logger, Priority level, Object message, Throwable throwable) { super(fqnOfCategoryClass, logger, level, message, throwable); // TODO Auto-generated constructor stub } public String getThreadName() { // TODO Auto-generated method stub String thrdName=super.getThreadName(); if(thrdName.indexOf("'")!=-1){ thrdName=thrdName.replaceAll("'", "''"); } return thrdName; } public String getRenderedMessage() { String msg=super.getRenderedMessage(); if(msg.indexOf("'")!=-1){ msg=msg.replaceAll("'", "''"); } return msg; } }
好了,到这里大功告成了,就只剩下一小步了,那就是刚刚重写的方法如何被调用,因为按照类jdbdAppend的流程是不会执行我重写的getThreadName的?呵呵,在多写一个类,让它按照执行就是了,这里写的类就是要扩展JDBCAPPend了,覆盖里面的getLogStatement方法,代码也很少,大体如下:
到了这里只需要把log4j.xml中的jdbdappender换成我们刚刚写的类就可以看,也就是BPSJDBCAppender!最终问题得到解决!