一、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組件的皮毛而已,由於編者水平有限,在很多觀點的闡述和代碼的處理方式還有存在着很大的爭議,望各位提出寶貴的意見和建議。
