log4j中提供了将日志记录到数据库中的输出器: org.apache.log4j.jdbc.JDBCAppender。通过以下配置即可生效
log4j.logger.dbLog=ERROR,dbLogAppender log4j.appender.dbLogAppender=org.apache.log4j.jdbc.JDBCAppender log4j.appender.dbLogAppender.URL=jdbc:mysql://localhost:3306/test log4j.appender.dbLogAppender.driver=com.mysql.jdbc.Driver log4j.appender.dbLogAppender.user=root log4j.appender.dbLogAppender.password=xiaodi log4j.appender.dbLogAppender.sql=insert into t_log4j_info(priority,message,catalog,time) values('%p','%m','%c','%d{yyyy-MM-dd HH:mm:ss}') log4j.appender.dbLogAppender.bufferSize=10 log4j.appender.dbLogAppender.layout=org.apache.log4j.PatternLayout
扩展类 PrepareStatementJdbcAppender 和 JndiDataSourceAppender
JdbcAppender使用JDBC 的Statement提交sql,本例基于PreparedStatement重新实现数据库输出器(PrepareStatementJdbcAppender),
并以此为父类实现调用JNDI数据源的输出器(JndiDataSourceAppender)。
PrepareStatementJdbcAppender类定义
/** * 支持预编译sql的jdbc输出器<br> * 设置参数:<br> * URL、driver、user、password、sql、sqlParams 、[bufferSize]<br> * 不需要设置layout * * @author houfeng-win7 * */ public class PrepareStatementJdbcAppender extends org.apache.log4j.AppenderSkeleton implements org.apache.log4j.Appender { protected Connection connection = null; protected String driverClass; protected String databaseURL = "jdbc:mysql://localhost:3306/test"; protected String databaseUser = "root"; protected String databasePassword = "xiaodi"; /** * 预编译sql //insert into tab values (?,?) */ protected String sqlStatement = ""; /** * sql参数 有序字符串 <br> * 不要有任何多余字符。 * 格式范例:%p%m%c%d{yyyy-MM-dd HH:mm:ss} */ protected String sqlParams = ""; /** * 存储当前sql参数值 */ protected ArrayList sqlParamValues ; protected int bufferSize = 1; protected ArrayList buffer; protected ArrayList removes; /** * sqlParams模式转换器 */ protected PatternConverter patternConverter; public PrepareStatementJdbcAppender() { this.buffer = new ArrayList(this.bufferSize); this.removes = new ArrayList(this.bufferSize); } public void append(LoggingEvent event) { if (!checkEntryConditions()) return; this.buffer.add(event); if (this.buffer.size() >= this.bufferSize) flushBuffer(); } /** * 检测必需参数是否设置 * @return */ protected boolean checkEntryConditions() { if(this.driverClass == null){ this.errorHandler.error("No 'driver' set for the appender named [" + name + "]"); return false; } if(this.databaseURL == null){ this.errorHandler.error("No 'URL' set for the appender named [" + name + "]"); return false; } if(this.databaseUser == null){ this.errorHandler.error("No 'user' set for the appender named [" + name + "]"); return false; } if(this.databasePassword == null){ this.errorHandler.error("No 'password' set for the appender named [" + name + "]"); return false; } if(this.sqlStatement == null){ this.errorHandler.error("No 'sql' set for the appender named [" + name + "]"); return false; } if (this.sqlParams == null || this.patternConverter == null) { this.errorHandler.error("No 'sqlParams' set for the appender named [" + name + "]"); return false; } return true; } protected void execute() { String sql = this.sqlStatement; ArrayList list = this.sqlParamValues; Connection con = null; PreparedStatement stmt = null; try { try { con = getConnection(); stmt = con.prepareStatement(sql); for(int i=0;i<list.size();i++){ stmt.setString(i+1, list.get(i).toString()); } stmt.executeUpdate(); } finally{ stmt.close(); closeConnection(con); } } catch (SQLException e) { errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE); } } protected void closeConnection(Connection con) { // 不立即关闭,保持jdbc连接而不立即释放,保证执行效率,在log销毁时通过close方法释放。 // 该方法在数据源连接的时候可增加关闭连接的代码。 } protected Connection getConnection() throws SQLException { if (this.connection == null||this.connection.isClosed()) { if (!(DriverManager.getDrivers().hasMoreElements())) { setDriver("sun.jdbc.odbc.JdbcOdbcDriver"); } this.connection = DriverManager.getConnection(this.databaseURL, this.databaseUser, this.databasePassword); } return this.connection; } public void close() { flushBuffer(); try { if ((this.connection != null) && (!(this.connection.isClosed()))) this.connection.close(); } catch (SQLException e) { this.errorHandler.error("Error closing connection", e, 0); } this.closed = true; } public void flushBuffer() { this.removes.ensureCapacity(this.buffer.size()); for (Iterator i = this.buffer.iterator(); i.hasNext();) { LoggingEvent logEvent = (LoggingEvent) i.next(); StringBuffer tempBuf = new StringBuffer(); for(PatternConverter c = this.patternConverter;c!=null;c=c.next){ c.format(tempBuf, logEvent); this.sqlParamValues.add(tempBuf.toString()); tempBuf.setLength(0); } execute(); this.sqlParamValues.clear(); this.removes.add(logEvent); } this.buffer.removeAll(this.removes); this.removes.clear(); } public boolean requiresLayout() { return false;// 不需要Layout } public void setSql(String s) { this.sqlStatement = s; //if (super.getLayout() == null) super.setLayout(new PatternLayout(s)); //else ((PatternLayout) super.getLayout()).setConversionPattern(s); } public String getSql() { return this.sqlStatement; } public void setSqlParams(String s) { this.sqlParams = s.trim(); this.patternConverter = new PatternParser(this.sqlParams).parse(); this.sqlParamValues = new ArrayList(); } public String getSqlParams() { return this.sqlParams; } public void setUser(String user) { this.databaseUser = user; } public void setURL(String url) { this.databaseURL = url; } public void setPassword(String password) { this.databasePassword = password; } public void setBufferSize(int newBufferSize) { this.bufferSize = newBufferSize; this.buffer.ensureCapacity(this.bufferSize); this.removes.ensureCapacity(this.bufferSize); } public String getUser() { return this.databaseUser; } public String getURL() { return this.databaseURL; } public String getPassword() { return this.databasePassword; } public int getBufferSize() { return this.bufferSize; } public void setDriver(String driverClass) { this.driverClass = driverClass; try { Class.forName(this.driverClass); } catch (Exception e) { this.errorHandler.error("Failed to load driver", e, 0); } } }
JndiDataSourceAppender类定义:
/** * 使用JNDI数据源的 输出器<br> * 由于JNDI数据源在log4j初始化前已经可用,所以可以通过配置的方式实现; * 或者在log4j中定义一个独立于程序和容器的数据源,也可以在配置中实现,但这样不利于数据源的统一维护。<br> * 相比,通过程序生成的数据源(包括spring配置的数据源)在log4j初始化的时候很可能还未创建,所以不能通过配置的方式定义这类 * 数据源输出器,而只能在程序中创建Appender。<br> * 需要配置的参数:<br> * jndiName、 sql、sqlParams 、[bufferSize]<br> * @author houfeng-win7 * */ public class JndiDataSourceAppender extends PrepareStatementJdbcAppender { protected DataSource ds = null; protected String jndiName = "java:comp/env/jdbc/mysql_log"; public JndiDataSourceAppender(){ } public void setJndiName(String name){ this.jndiName = name; } @Override protected boolean checkEntryConditions() { if(this.jndiName == null){ this.errorHandler.error("No 'jndiName' set for the appender named [" + name + "]"); return false; } if(this.sqlStatement == null){ this.errorHandler.error("No 'sql' set for the appender named [" + name + "]"); return false; } if (this.sqlParams == null || this.patternConverter == null) { this.errorHandler.error("No 'sqlParams' set for the appender named [" + name + "]"); return false; } return true; } protected synchronized void init(){ if(ds!=null) return; try { InitialContext context = new InitialContext(); ds = (DataSource)context.lookup(jndiName); } catch (NamingException e) { this.errorHandler.error(e.getMessage()); } } @Override protected Connection getConnection() throws SQLException { while(ds==null) init(); Connection conn = ds.getConnection(); conn.setAutoCommit(true); return conn; } @Override protected void closeConnection(Connection conn) { try { if(conn!=null&&!conn.isClosed()) conn.close(); } catch (SQLException e) { this.errorHandler.error(e.getMessage()); } } }
配置文件:
## -----自定义 logger2 prepareSqlDbLog -------------## log4j.logger.prepareSqlDbLog=ERROR,prepareStatementDbLogAppender log4j.appender.prepareStatementDbLogAppender=cn.log4j.datasource.PrepareStatementJdbcAppender log4j.appender.prepareStatementDbLogAppender.URL=jdbc:mysql://localhost:3306/test log4j.appender.prepareStatementDbLogAppender.driver=com.mysql.jdbc.Driver log4j.appender.prepareStatementDbLogAppender.user=root log4j.appender.prepareStatementDbLogAppender.password=xiaodi log4j.appender.prepareStatementDbLogAppender.sql=insert into t_log4j_info(priority,message,catalog,time) values(?,?,?,?) log4j.appender.prepareStatementDbLogAppender.sqlParams=%p%m%c%d{yyyy-MM-dd HH:mm:ss} log4j.appender.prepareStatementDbLogAppender.bufferSize=10 ##--------------db ------------------## log4j.appender.db=cn.log4j.datasource.JndiDataSourceAppender log4j.appender.db.jndiName=java:comp/env/jdbc/mysql_log log4j.appender.db.sql=insert into t_log4j_info(priority,message,catalog,time) values(?,?,?,?) log4j.appender.db.sqlParams=%p%m%c%d{yyyy-MM-dd HH:mm:ss} log4j.appender.db.bufferSize=5
另外,通过分析源码了解下log4j的JdbcAppender是如何保证线程安全的:
JdbcAppender继承AppenderSkeleton抽象基类,AppenderSkeleton通过synchronized的doAppend方法调用抽象的append方法,所以子类JdbcAppender实现的append方法也会是线程顺序进入,
从而保证了append中调用的日志缓冲区(ArrayList)及数据库连接对象没有线程并发访问问题。
同时,在log4j运行期间,同一名称的logger存在唯一实例;声明的同一名称的appender也存在唯一实例。
补充:发现一个更强大的 JdbcAppender :http://www.mannhaupt.com/danko/projects/index.html