一、Log4j简介
在一个完整的J2EE项目开发中,日志是一个非常重要的功能组成部分。它可以记录下系统所产生的所有行为,并按照某种规范表达出来。我们可以通过日志信息为系统进行排错,优化系统的性能,或者根据这些信息调整系统等行为。Log4j是Apache针对于日志信息处理的一个开源项目,其最大特点是通过一个配置文件就可以灵活地控制日志信息的输出方式(控制台、文件和数据库等)、日志输出格式及日志信息打印级别等,而不需要修改应用的代码。
二、编写背景
作为一名程序猿在开发中总能遇到一些比较奇葩的需求,而这些需求对于身份低微的小编来说又不得不去尽力完成。在接触A公司项目之前,公司项目中使用到的日志信息基本都是写到对应文件中。而A公司客户觉得日志信息存在文件中不方便查看,需要把日志信息记录到数据库中,然后再做个界面供在页面上查询。说实话,日志写库这种低效率的事情我向来是不太赞同去做的。
三、编写目的
怕年纪大了就会忘了,给自己留个回忆。
四、Java日志信息存库详细解决方案
1.开发环境说明
Eclipse+Tomcat6+JDK1.6+Oracle+Log4j1.2
2.数据库表创建
表1.日志信息表(LOGGING_EVENT)
字段名 | 中文说明 | 类型 | 为空 |
TIMESTMP | 记录时间 | NUMBER(20) | N |
FORMATTED_MESSAGE | 格式化后的日志信息 | CLOB | N |
LOGGER_NAME | 执行记录请求的logger | VARCHAR2(256) | N |
LEVEL_STRING | 日志级别 | VARCHAR2(256) | N |
THREAD_NAME | 日志线程名 | VARCHAR2(256) | Y |
REFERENCE_FLAG | 包含标识:1-MDC或上下文属性;2-异常;3-均包含 | INTEGER | Y |
ARG0 | 参数1 | VARCHAR2(256) | Y |
ARG1 | 参数2 | VARCHAR2(256) | Y |
ARG2 | 参数3 | VARCHAR2(256) | Y |
ARG3 | 参数4 | VARCHAR2(256) | Y |
CALLER_FILENAME | 文件名 | VARCHAR2(256) | N |
CALLER_CLASS | 类 | VARCHAR2(256) | N |
CALLER_METHOD | 方法名 | VARCHAR2(256) | N |
CALLER_LINE | 行号 | VARCHAR2(256) | N |
EVENT_ID | 主键ID | NUMBER(10) | N |
注: 建这个表主要是因为项目中同时使用了logback和log4j两种组件记录日志信息,该表的表结构使用的是logback-classic-1.1.3.jar中提供的oracle.sql文件创建的。
3.实现方案
(1)直接配置log4j.properties文件
使用log4j原生态JDBCAppender最大的缺陷就是没法使用JNDI,还有比较棘手的就是没法把超过4000字符的日志信息插入到数据库表(即便使用CLOB类型来存储亦如此)
#配置将INFO级别及以上级别的日志存到数据库中 log4j.rootLogger=INFO,db #使用log4j默认的JDBCAppender将日志存到数据库 log4j.appender.db = org.apache.log4j.jdbc.JDBCAppender #配置产生多少条日志的时候再去插入到数据库,默认为1 log4j.appender.db.BufferSize=5 #配置数据库驱动 log4j.appender.db.driver=oracle.jdbc.driver.OracleDriver #配置数据库连接地址 log4j.appender.db.URL=jdbc:oracle:thin:@<ip>:<port>:<sid> #配置数据库连接用户名 log4j.appender.db.user=XXX #配置数据库连接密码 log4j.appender.db.password=XXX #配置日志存数库执行的sql,支持log4j格式化参数,LOGGING_EVENT_ID_SEQ是建立的索引,用于生成主键 log4j.appender.db.sql=insert into LOGGING_EVENT (timestmp,formatted_message,logger_name,level_string,thread_name,caller_filename,caller_class,caller_method,caller_line,event_id)
values((SYSDATE - TO_DATE('1970-1-1 8', 'YYYY-MM-DD HH24')) * 86400000 + TO_NUMBER(TO_CHAR(SYSTIMESTAMP(3), 'FF')),'%m','atsws','%p','%t','%F','%C','%M','%L',LOGGING_EVENT_ID_SEQ.nextval) #配置对应的layout log4j.appender.db.layout=org.apache.log4j.PatternLayout
(2)自定义JDBCAppender类
1)自定义Appender类(ATSDBAppender.java)
该ATSDBAppender是基于log4j-1.2.17.jar中原有的JDBCAppender改造而来,同时支持JDBC及JNDI连接数据库操作,具有比较好的扩展性,且很好的解决了日志信息超长无法存库的问题。
1 package com.hundsun.util.loggingevent; 2 3 import java.io.StringReader; 4 import java.sql.Connection; 5 import java.sql.DriverManager; 6 import java.sql.PreparedStatement; 7 import java.sql.SQLException; 8 import java.util.ArrayList; 9 import java.util.Iterator; 10 11 import javax.naming.InitialContext; 12 import javax.naming.NamingException; 13 import javax.sql.DataSource; 14 15 import org.apache.commons.lang3.StringUtils; 16 import org.apache.log4j.Appender; 17 import org.apache.log4j.AppenderSkeleton; 18 import org.apache.log4j.PatternLayout; 19 import org.apache.log4j.spi.ErrorCode; 20 import org.apache.log4j.spi.LocationInfo; 21 import org.apache.log4j.spi.LoggingEvent; 22 23 public class ATSDBAppender extends AppenderSkeleton implements Appender{ 24 protected String databaseURL; 25 protected String databaseUser; 26 protected String databasePassword; 27 protected Connection connection; 28 protected String sqlStatement; 29 protected int bufferSize = 1; 30 protected ArrayList<LoggingEvent> buffer; 31 protected ArrayList<LoggingEvent> removes; 32 private boolean locationInfo = false; 33 34 protected DataSource ds = null; 35 protected String jndiName;//JNDI名 36 37 public ATSDBAppender(){ 38 this.buffer = new ArrayList<LoggingEvent>(this.bufferSize); 39 this.removes = new ArrayList<LoggingEvent>(this.bufferSize); 40 } 41 @Override 42 public void close() { 43 flushBuffer(); 44 try{ 45 if((this.connection!=null)&&(!this.connection.isClosed())){ 46 this.connection.close(); 47 } 48 }catch(SQLException e){ 49 this.errorHandler.error("关闭连接失败",e,0); 50 } 51 this.closed=true; 52 } 53 public void flushBuffer(){ 54 this.removes.ensureCapacity(this.buffer.size()); 55 for(Iterator<LoggingEvent> i=this.buffer.iterator();i.hasNext();){ 56 LoggingEvent logEvent=(LoggingEvent)i.next(); 57 try{ 58 String sql=" insert into logging_event (timestmp,formatted_message,logger_name,level_string,thread_name,caller_filename,caller_class,caller_method,caller_line,event_id) values(?,?,?,?,?,?,?,?,?,LOGGING_EVENT_ID_SEQ.nextval) "; 59 execute(sql, logEvent); 60 }catch(SQLException e){ 61 this.errorHandler.error("执行sql出错", e, 2); 62 }finally{ 63 this.removes.add(logEvent); 64 } 65 } 66 this.buffer.removeAll(this.removes); 67 this.removes.clear(); 68 } 69 public void finalize(){ 70 close(); 71 } 72 @Override 73 public boolean requiresLayout() { 74 return true; 75 } 76 77 @Override 78 public synchronized void doAppend(LoggingEvent event) { 79 if(!StringUtils.isEmpty(name)&&"db".equals(name)&&closed){ 80 closed=false; 81 } 82 super.doAppend(event); 83 } 84 @Override 85 protected void append(LoggingEvent event) { 86 event.getTimeStamp(); 87 event.getThreadName(); 88 event.getMDCCopy(); 89 if(this.locationInfo){ 90 event.getLocationInformation(); 91 } 92 event.getRenderedMessage(); 93 event.getThrowableStrRep(); 94 this.buffer.add(event); 95 if(this.buffer.size()>=this.bufferSize) 96 flushBuffer(); 97 } 98 protected void execute(String sql,LoggingEvent logEvent) throws SQLException{ 99 Connection con=null; 100 PreparedStatement stmt=null; 101 try{ 102 con=getConnection(); 103 stmt=con.prepareStatement(sql); 104 stmt.setLong(1, logEvent.getTimeStamp()); 105 String largeText=logEvent.getRenderedMessage(); 106 StringReader reader=new StringReader(largeText); 107 stmt.setCharacterStream(2, reader,largeText==null?0:largeText.length()); 108 stmt.setString(3, "atsws"); 109 stmt.setString(4, logEvent.getLevel().toString()); 110 stmt.setString(5, logEvent.getThreadName()); 111 LocationInfo locationInfo = logEvent.getLocationInformation(); 112 stmt.setString(6, locationInfo.getFileName()); 113 stmt.setString(7, locationInfo.getClassName()); 114 stmt.setString(8, locationInfo.getMethodName()); 115 stmt.setString(9, locationInfo.getLineNumber()); 116 stmt.executeUpdate(); 117 }finally{ 118 if(stmt!=null){ 119 stmt.close(); 120 } 121 closeConnection(con); 122 } 123 } 124 protected void closeConnection(Connection con){ 125 try{ 126 if(connection!=null&&!connection.isClosed()) 127 connection.close(); 128 }catch(SQLException e){ 129 errorHandler.error("关闭连接失败!",e,ErrorCode.GENERIC_FAILURE); 130 } 131 } 132 protected Connection getConnection() throws SQLException{ 133 if(!DriverManager.getDrivers().hasMoreElements()){ 134 setDriver("oracle.jdbc.driver.OracleDriver"); 135 } 136 if(databaseURL!=null&&databaseUser!=null&&databasePassword!=null){ 137 if(this.connection==null){ 138 this.connection=DriverManager.getConnection(this.databaseURL, this.databaseUser, this.databasePassword); 139 } 140 }else{ 141 while(ds==null){ 142 try{ 143 InitialContext context=new InitialContext(); 144 ds=(DataSource)context.lookup(jndiName); 145 }catch(NamingException e){ 146 this.errorHandler.error(e.getMessage()); 147 } 148 } 149 this.connection=ds.getConnection(); 150 connection.setAutoCommit(true); 151 } 152 return this.connection; 153 } 154 public boolean isLocationInfo() { 155 return locationInfo; 156 } 157 public void setLocationInfo(boolean flag) { 158 this.locationInfo = flag; 159 } 160 public void setJndiName(String jndiName) { 161 this.jndiName = jndiName; 162 } 163 public void setSql(String s) 164 { 165 this.sqlStatement = s; 166 if (getLayout() == null) { 167 setLayout(new PatternLayout(s)); 168 }else{ 169 ((PatternLayout)getLayout()).setConversionPattern(s); 170 } 171 } 172 173 public String getSql() { 174 return this.sqlStatement; 175 } 176 177 public void setUser(String user) { 178 this.databaseUser = user; 179 } 180 181 public void setURL(String url) { 182 this.databaseURL = url; 183 } 184 185 public void setPassword(String password) { 186 this.databasePassword = password; 187 } 188 189 public void setBufferSize(int newBufferSize) { 190 this.bufferSize = newBufferSize; 191 this.buffer.ensureCapacity(this.bufferSize); 192 this.removes.ensureCapacity(this.bufferSize); 193 } 194 195 public String getUser() { 196 return this.databaseUser; 197 } 198 199 public String getURL() { 200 return this.databaseURL; 201 } 202 203 public String getPassword() { 204 return this.databasePassword; 205 } 206 207 public int getBufferSize() { 208 return this.bufferSize; 209 } 210 211 public void setDriver(String driverClass) { 212 try { 213 Class.forName(driverClass); 214 } catch (Exception e) { 215 this.errorHandler.error("加载数据库驱动失败", e, 0); 216 } 217 } 218 }
2)配置自定义ATSDBAppender,将日志信息存入Oracle数据库
Ⅰ.使用JDBC方式配置log4j.properties文件
#配置INFO级别的日志存入数据库 log4j.rootLogger=INFO,db #使用自定义的ATSDBAppender类来将日志信息存库 log4j.appender.db=com.hundsun.util.loggingevent.ATSDBAppender #设置有多少条日志数据时再进行存库操作,默认为1,即日志信息每产生一条就新增进数据库 log4j.appender.db.BufferSize=5 #配置数据库驱动 log4j.appender.db.driver=oracle.jdbc.driver.OracleDriver #配置数据库连接地址 log4j.appender.db.URL=jdbc:oracle:thin:@127.0.0.1:1521:orcl #配置数据库连接用户名 log4j.appender.db.user=tiger #配置数据库连接密码 log4j.appender.db.password=123456 #配置使用的Layout log4j.appender.db.layout=org.apache.log4j.PatternLayout
注:此处并没有配置sql语句,主要是因为在log4j-1.2.17版本中sql语句处理timestmp字段值使用时间戳方式比较繁琐,且日志信息超4000字符时会报字段超长错误。
Ⅱ.使用JNDI方式配置
A.Tomcat安装目录/config/server.xml文件配置JNDI
<Context debug="0" docBase="E:\prj_abic\src\trunk\fundats\ats-modules-webservice\target\ats-modules-webservice" path="/webservice" reloadable="true"> <Resource auth="Container" driverClassName="oracle.jdbc.driver.OracleDriver" maxActive="30" maxIdle="30" name="jdbc/logging" password="123456" type="javax.sql.DataSource" url="jdbc:oracle:thin:@127.0.0.1:1521:orcl" username="tiger"/> </Context>
注:该配置为Tomcat下配置JNDI连接比较常用的方式,若不太清楚这块的配置规则可去查阅相关书籍,此时定义的jndi名称为"jdbc/logging".
B.applicationContext.xml数据源配置
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>java:comp/env/jdbc/logging</value> </property>
</bean>
注:由于使用的是Tomcat服务器,所以在配置数据源的时候得加上前缀[java:comp/env/],weblogic服务器则无需添加前缀,此时jndi名称与前面server.xml文件配置的要保持一致。
C.log4j.properties文件配置
#配置将INFO级别的日志信息存储到数据库中
log4j.rootLogger=INFO,db
#使用自定义的Appender实现数据库的存库操作
log4j.appender.db=com.hundsun.util.loggingevent.ATSDBAppender
#设置一次性将多少条日志信息存入数据库,默认为1,但效率低
log4j.appender.db.BufferSize=5
#配置使用到的JNDI的名称,该值与Tomcat服务器配置的JNDI名称保持一致
log4j.appender.db.jndiName=java:comp/env/jdbc/logging
#配置日志使用的Layout
log4j.appender.db.layout=org.apache.log4j.PatternLayout
注:由于使用的是Tomcat服务器,所以jndiName的值需加上前缀[java:comp/env/],weblogic服务器则无需添加前缀,此时jndi名称与前面server.xml文件配置的要保持一致。
五、总结
文中提到的Log4j日志信息存库功能开发仅是Log4j组件的皮毛而已,由于编者水平有限,在很多观点的阐述和代码的处理方式还有存在着很大的争议,望各位提出宝贵的意见和建议。