年前没有什么心情开发正常的需求,最近一直在做保监会存量数据提取,很头大,打算研究点新东西换换心情。
我工作的公司服务器日志必须跟DBA要,一直麻烦人家不太好,而且他如果有事,过了很长时间才给我日志,会影响我问题排查的效率。我就想把日志存储到数据库中,这样十分完美。
最开始想自己写一套Appender来实现,后来Google一下,发现Log4j默认就提供JDBCAppender来实现日志存储数据库的功能。我参考了 log4j日志存储到数据库这篇博客,但是有个问题没有解决,所以才有我这篇博客的诞生,即我优化的之处。
create table XXX_LOG
(
loglevel VARCHAR2(5) not null,--日志登记
maketime VARCHAR2(25) not null,--打印时间
location VARCHAR2(300) not null,--输出位置
message VARCHAR2(4000) not null--日志内容
);
由于输出位置我是用的是%l,即输出 类全名.函数名(文件名:行号),所以输出位置长度需要比较长。
由于项目中用的是log4j.properties,我主要对properties格式的配置文件做说明。
#使用下面定义的appender.DB做完日志的输出位置
log4j.rootLogger=DB
#创建名字为DB的Appender,用来输出日志到数据库
log4j.appender.DB=org.apache.log4j.jdbc.JDBCAppender
#数据库的驱动
log4j.appender.DB.driver=oracle.jdbc.driver.OracleDriver
#数据库的路径
log4j.appender.DB.URL=jdbc:oracle:thin:@IP:1521:dbname
#数据库的用户名
log4j.appender.DB.user=xxx
#数据库的密码
log4j.appender.DB.password=xxx
#有多少条日志信息后才存入数据库,如果数量过小是会影响生产的效率
log4j.appender.DB.BufferSize=30
#插入数据库使用的SQL语句,用的跟PatternLayout是一样的格式化参数
log4j.appender.DB.sql=INSERT INTO Prip_Log VALUES('%p','%d','%l','%m')
#格式化参数使用的类,也是我跟我参考的那篇博客不一样的地方,下面具体说明
log4j.appender.DB.layout=xxx.DBPatternLayout
如果直接按照我参考博客干,会出一个问题,打印信息中包含”’”(单引号),系统会报“缺少逗号”的错误。为什么会出现这个问题能,如果你把相应内容直接替换到插入SQL中就会发现问题。例如下面的SQL:
insert into xxx_log (LOGLEVEL, MAKETIME, LOCATION, MESSAGE)
values ('INFO', '2017-01-23 09:13:54,092', 'xxx.Configs.(Configs.java:37)' , ''测试单引号'');
你会发现打印信息的单引号跟SQL中的字符串边界的单引号匹配了,“测试单引号”这句话就没有被单引号包裹,数据库就不认为它是字符串。才产生这个问题。
那么如何解决的这个问题呢,用转义字符,将单引号转义就行了,根据具体数据库有不同的转义字符,oracle是用单引号作为转义,例如如下改写后的SQL:
insert into xxx_log (LOGLEVEL, MAKETIME, LOCATION, MESSAGE)
values ('INFO', '2017-01-23 09:13:54,092', 'xxx.Configs.(Configs.java:37)' , '''测试单引号''');
三个单引号,第一个是字符串边界,第二个是转义字符,第三个才是要打印的单引号。
或者是MySql转义字符用的是”\”,如下SQL:
insert into xxx_log (LOGLEVEL, MAKETIME, LOCATION, MESSAGE)
values ('INFO', '2017-01-23 09:13:54,092', 'xxx.Configs.(Configs.java:37)' , '\'测试单引号\'');
上面只是说明了一下问题的原因,及解决思路,具体的实施方法没有说明。Log4j既然是个开源库,肯定会有各种的扩展方法,我就开始研究JDBCAppender的实现方式。想怎么样可以将日志内容中的“’”替换成“””。这个就得研究源代码了。
1. 方案1
先说一个不是十分完美的方案。我发现%m是获取org.apache.log4j.spi.LoggingEvent.getRenderedMessage(),我最开始的想法是写个子类继承LoggingEvent,重写getRenderedMessage方法,然后将子类对象植入log4j的过程中。
public class DBLoggingEvent extends LoggingEvent
{
private LoggingEvent mEvent = null;
public DBLoggingEvent(LoggingEvent tEvent)
{
super(tEvent.getFQNOfLoggerClass(), tEvent.getLogger(), tEvent.getLevel(), tEvent.getMessage(), null);
mEvent = tEvent;
}
……
@Override
public String getRenderedMessage()
{
// 将字符串中的“'”替换成“''”
return mEvent.getRenderedMessage().replace("'", "''");
}
}
public class DBPatternLayout extends PatternLayout
{
@Override
public String format(LoggingEvent event)
{
// 将新的LoggingEvent对象植入log4j的过程中
DBLoggingEvent tEvent = new DBLoggingEvent(event);
return super.format(tEvent);
}
}
在log4j.properties的配置文件中使用自定义的解析类,即
log4j.appender.DB.layout=xxx.DBPatternLayout
但是这个方案有三个问题,1、LoggingEvent 没有默认的无参构造函数,必须使用有参构造函数,如果之后构造函数发送变化了,这个类也得跟着改,有耦合问题。就算修改LoggingEvent 的源代码也会有切换log4j版本产生的耦合问题。2、每次都得多建一个对象,消耗系统资源。3、违反log4j的设计模式,即LoggingEvent 只是个javaBean,用来传输数据的,不要增加多余的业务处理。
2. 方案2
那我继续研究代码,发现log4j是用PatternParser来获取解析格式化参数的解释器的,那我写个子类继承PatternParser,增加对单引号的处理,更能符合log4j的体系,即我第二种的方案的思路。
DBPatternParser相当于PatternParser的代理,增加了对保存到数据库时,打印信息中有单引号这个情况的特殊处理,其他情况还是调用父类的实现,我自己感觉用if能简便一些,但是我看父类代码,感觉switch更好一些,为了保持对将来的扩展能力,最后还是模仿父类的架构。
public class DBPatternParser extends PatternParser
{
public DBPatternParser(String pattern)
{
super(pattern);
}
@Override
protected void finalizeConverter(char c)
{
switch (c)
{
case 'm':
// 如果是打印信息,调用DBPatternConverter来替换一个单引号成两个单引号
addConverter(new DBPatternConverter(formattingInfo, c));
currentLiteral.setLength(0);
break;
default:
// 其他情况调用父类的实现
super.finalizeConverter(c);
break;
}
}
private static class DBPatternConverter extends PatternConverter
{
char type;
DBPatternConverter(FormattingInfo formattingInfo, char type)
{
super(formattingInfo);
this.type = type;
}
public String convert(LoggingEvent event)
{
switch (type)
{
case 'm':
return event.getRenderedMessage().replace("'", "''");
default:
return null;
}
}
}
}
将新写的PatternParser子类植入log4j的体系中
public class DBPatternLayout extends PatternLayout
{
@Override
protected PatternParser createPatternParser(String pattern)
{
return new DBPatternParser(pattern);
}
}
在log4j.properties的配置文件中使用自定义的解析类,跟方案1一样
log4j.appender.DB.layout=xxx.DBPatternLayout
在这谈一下对zhangchu_63的感谢,即我参考的那篇博客的作者
由于
#有多少条日志信息后才存入数据库,如果数量过小是会影响生产的效率
log4j.appender.DB.BufferSize=30
这个配置对性能的优化,往往会出现这个问题,如果这个你一直监控着系统,发现有问题了,马上去查看数据库的日志存储,发现最后的日志还没有刷新到数据库中,这个时候就比较尴尬了,我原先只能手动的多点几下,多输出点日志,然后让我想看到的日志刷新到数据库中。
那么有什么更好的方法去实现呢?还是得研究代码,我发现在JDBCAppender中调用flushBuffer方法来实现将缓存区中的日志刷新到数据库中的。
if (buffer.size() >= bufferSize)
flushBuffer();
这个问题就变成获取JDBCAppender,然后调用flushBuffer。
如何获取呢?从代码中发现Category类中有
synchronized
public
Appender getAppender(String name) {
if(aai == null || name == null)
return null;
return aai.getAppender(name);
}
这个函数能实现我想要的功能。
理所当然的问题就变成了如何获取Category,然后调用getAppender。
我一看这个Category的子类,看着好面熟啊,Logger这个不就是我们输出日志的时候使用的类吗,如果我直接使用程序中获取的Logger能获取到Appender,我感觉不太可能啊,经过实际测试,也是不太可行的。那怎么办,继续看代码,终于我眼前一亮,我看到了如下的代码:
Logger root = Logger.getRootLogger();
root.addAppender(appender);
然后就有了如下类的诞生。由于Appender在log4j中是单例模式,那我也使用单例模式来获取它
public class FlushLogBuffer
{
JDBCAppender mAppender = null;
private static FlushLogBuffer BUFFER = null;
private FlushLogBuffer()
{
Logger logger = Logger.getRootLogger();
// 获取名字为DB的Appender
mAppender = (JDBCAppender) logger.getAppender("DB");
}
public static FlushLogBuffer getInstance()
{
if (BUFFER == null)
{
synchronized (FlushLogBuffer.class)
{
if (BUFFER == null)
{
BUFFER = new FlushLogBuffer();
}
}
}
return BUFFER;
}
public void flushBuffer()
{
mAppender.flushBuffer();
}
}
只要写个页面,然后后台调用,就能将缓存区的日志刷新到数据库中
FlushLogBuffer tFlushLogBuffer = FlushLogBuffer.getInstance();
tFlushLogBuffer.flushBuffer();
至此,log4j日志存储到数据库的优化已经全部完毕了