`
qinjingkai
  • 浏览: 259488 次
  • 性别: Icon_minigender_1
  • 来自: 广州
文章分类
社区版块
存档分类
最新评论

log4j之重写JDBCAppender

阅读更多

好久没更新博客了,嘿嘿!主要也是因为这段时间比较忙,一直都忙于需求的理解,编码,测试.....反正一系列很操蛋的事情!当然忙的收获就是在强迫的环境中让你学更多的知识,这个我会在后面的文章中提及,今天就先从重写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()方法的代码如下

写道
public void flushBuffer()
{
this.removes.ensureCapacity(this.buffer.size());
Iterator i = this.buffer.iterator(); if (i.hasNext());
try {
LoggingEvent logEvent = (LoggingEvent)i.next();
String sql = getLogStatement(logEvent);
execute(sql);
this.removes.add(logEvent);
}
catch (SQLException e) {
while (true) { this.errorHandler.error("Failed to excute sql", e, 2);
}

this.buffer.removeAll(this.removes);

this.removes.clear();
}
}

在自己重写的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方法,代码也很少,大体如下:

写道
public class BPSJDBCAppender extends JDBCAppender {

protected String getLogStatement(LoggingEvent event) {
String fqnOfCategoryClass=event.fqnOfCategoryClass;
Category logger=Category.getRoot();
Priority level=event.level;
Object message=event.getMessage();
Throwable throwable=null;
BPSLoggingEvent bEvent=new BPSLoggingEvent(fqnOfCategoryClass,logger,level,message,throwable);
return super.getLogStatement(bEvent);
}

到了这里只需要把log4j.xml中的jdbdappender换成我们刚刚写的类就可以看,也就是BPSJDBCAppender!最终问题得到解决!

分享到:
评论
4 楼 ynp 2015-07-09  
有没有考虑过用spring去管理日志,这样就不配置两套数据库的链接配置了,但因为加载顺序问题
	@Override
	protected Connection getConnection() throws SQLException
	{
		return DataSourceUtils.getConnection(jdbcDao.getDataSource());
	}


这么写 是有问题的,不知道博主研究过没有
3 楼 qinjingkai 2015-03-03  
jinbridge 写道
你好,我问一下,你的这个类继承以后怎么把它编译到原来的包中


不用编译到原来的包啊  改下log4j.xml文件就可以
把<appender name="DB_INFO" class="org.apache.log4j.jdbc.JDBCAppender"> 改成

<appender name="DB_INFO" class="org.apache.log4j.jdbc.BPSJDBCAppender">
2 楼 jinbridge 2015-01-16  
你好,我问一下,你的这个类继承以后怎么把它编译到原来的包中
1 楼 sunking_135 2013-03-13  
  • [img]
    [/img]

相关推荐

    log4j日志驱动包

    log4j.appender.DATABASE=org.apache.log4j.jdbc.JDBCAppender log4j.appender.DATABASE.URL=jdbc:mysql://localhost:3306/test log4j.appender.DATABASE.driver=com.mysql.jdbc.Driver log4j.appender.DATABASE....

    log4jToJDBCAppender.zip

    本工程用于研究log4j日志输出目的地org.apache.log4j.jdbc.JDBCAppender的使用方法 本工程编码方式:UTF-8 本工程开发工具:MyEclipse 本工程需要执行的SQL语句: CREATE DATABASE `test`; CREATE TABLE `...

    log4j jdbc 插件

    国外开源项目 用于控制log4j的 jdbc输出

    Java进阶课程日志框架视频教程

    -日志课程大纲.xlsx ...--16-Log4j的JDBCAppender配置.mp4 --17-Log4j的自定义logger.mp4 --18-JCL入门介绍.mp4 --19-JCL原理介绍.mp4 -- ---讲义 -日志技术(上).pdf - --day02 -day02资料.zip -

    自己封装的JDBC连接通过properties文件读取配置文件

    自己封装的JDBC连接通过properties文件读取配置文件

Global site tag (gtag.js) - Google Analytics