日志框架(二)------log4j 扩展 JdbcAppender输出器

令狐阳秋
2023-12-01

 

 

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  

 

 

 类似资料: