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