一. 背景
本来项目组使用的数据库是Oracle,如今要切换到达梦。。。早期的时候就讨论过数据库日志迁移的问题,本来以为是一个小问题可以随便搞定,没想到是踩坑了,记录下:
二. 如果已经实现Oracle自定义日志
简单来说,如果你现在已经实现了Oracle的自定义日志,数据库迁移到达梦后用同一套代码报错,那么就是达梦没有创建主键,只需要创建一个主键。并在自己实现的 DBAppender 里面重写一下这个方法即可:
@Override
protected Method getGeneratedKeysMethod() {
try {
return PreparedStatement.class.getMethod("getGeneratedKeys", (Class[]) null);
} catch (Exception ex) {
return null;
}
}
但如果啥都没实现,那么就继续看下去吧。
三. 快速实现
lomback.xml主要配置信息(只展示数据库连接部分配置)
<!-- 输出日志到数据库-->
<appender name="DB" class="org.cloud.common.config.MyDBAppender">
<connectionSource class="org.cloud.common.config.LogDBRewrite">
</connectionSource>
</appender>
<!-- * 通配符 设置log打印级别 对所有类有效TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF-->
<!--将上面的appender添加到root-->
<root level="INFO">
<!--日志输出到数据库 -->
<appender-ref ref="DB"/>
</root>
MyDBAppender.java
import ch.qos.logback.classic.spi.CallerData;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.db.DBAppenderBase;
import dm.jdbc.driver.DmdbPreparedStatement;
import dm.jdbc.driver.DmdbStatement;
import org.slf4j.MDC;
import java.lang.reflect.Method;
import java.sql.*;
/**
* @author hhh
* @Description lomback日志连接数据库配置
* @create 2021-01-06
*/
public class MyDBAppender extends DBAppenderBase<ILoggingEvent> {
private String insertSQL;
private static Method GET_GENERATED_KEYS_METHOD;
private static final int LOGID_INDEX = 1;
private static final int USERID_INDEX = 1;
private static final int CLASS_INDEX = 2;
private static final int MOTHOD_INDEX = 3;
private static final int OPERATIONTIME_INDEX = 4;
private static final int LOGLEVEL_INDEX = 5;
private static final int MSG_INDEX = 6;
private static final int IP_INDEX = 7;
private static final StackTraceElement EMPTY_CALLER_DATA = CallerData.naInstance();
static {
// PreparedStatement.getGeneratedKeys() method was added in JDK 1.4
Method getGeneratedKeysMethod;
try {
getGeneratedKeysMethod = PreparedStatement.class.getMethod("getGeneratedKeys", (Class[]) null);
} catch (Exception ex) {
getGeneratedKeysMethod = null;
}
GET_GENERATED_KEYS_METHOD = getGeneratedKeysMethod;
}
@Override
public void start() {
insertSQL = buildInsertSQL();
super.start();
}
private static String buildInsertSQL() {
//我用的是达梦数据库,test_sequence.nextval是一个自增长的值
return "INSERT INTO test " +
"(event_id, USERID, CLASS, MOTHOD, OPERATIONTIME, LOGLEVEL, MSG, IP)"+
"VALUES (test_sequence.nextval, ?, ? ,?, ?, ?, ?, ?)";
}
private void bindLoggingEventWithInsertStatement(PreparedStatement stmt, ILoggingEvent event) throws SQLException {
stmt.setString(USERID_INDEX, event.getMDCPropertyMap().get("username"));
stmt.setString(IP_INDEX, event.getMDCPropertyMap().get("ip"));
stmt.setTimestamp(OPERATIONTIME_INDEX, new Timestamp(event.getTimeStamp()));
stmt.setString(MSG_INDEX, event.getFormattedMessage());
stmt.setString(LOGLEVEL_INDEX, event.getLevel().toString());
stmt.setString(CLASS_INDEX, event.getLoggerName());
stmt.setString(MOTHOD_INDEX, event.getThreadName());
}
private void bindCallerDataWithPreparedStatement(PreparedStatement stmt, StackTraceElement[] callerDataArray) throws SQLException {
}
@Override
protected void subAppend(ILoggingEvent event, Connection connection, PreparedStatement insertStatement) throws Throwable {
bindLoggingEventWithInsertStatement(insertStatement, event);
// This is expensive... should we do it every time?
bindCallerDataWithPreparedStatement(insertStatement, event.getCallerData());
int updateCount = insertStatement.executeUpdate();
if (updateCount != 1) {
addWarn("Failed to insert loggingEvent");
}
}
private StackTraceElement extractFirstCaller(StackTraceElement[] callerDataArray) {
StackTraceElement caller = EMPTY_CALLER_DATA;
if (hasAtLeastOneNonNullElement(callerDataArray))
caller = callerDataArray[0];
return caller;
}
private boolean hasAtLeastOneNonNullElement(StackTraceElement[] callerDataArray) {
return callerDataArray != null && callerDataArray.length > 0 && callerDataArray[0] != null;
}
@Override
protected Method getGeneratedKeysMethod() {
return GET_GENERATED_KEYS_METHOD;
}
@Override
protected String getInsertSQL() {
return insertSQL;
}
protected void secondarySubAppend(ILoggingEvent event, Connection connection, long eventId){
}
}
LogDBRewrite.java
import ch.qos.logback.core.db.DataSourceConnectionSource;
import org.cloud.bdma.common.util.CryptUtil;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
/**
* @author zjt
* @description logback日志数据库配置
* @updateTime 2023/1/3 14:51
*/
public class LogDBRewrite extends DataSourceConnectionSource {
private static String url;
private static String username;
private static String password;
private static String driverClassName = dm.jdbc.driver.DmDriver;
@Override
public void start() {
try {
if (driverClassName != null) {
Class.forName(driverClassName);
discoverConnectionProperties();
} else {
addError("WARNING: No JDBC driver specified for logback DriverManagerConnectionSource.");
}
} catch (final ClassNotFoundException cnfe) {
addError("Could not load JDBC driver class: " + driverClassName, cnfe);
}
}
@Override
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}
}
数据库建表语句:(必须有一个主键)
CREATE TABLE test(
"event_id" int PRIMARY KEY,
"userid" VARCHAR2(12),
"class" VARCHAR2(255),
"mothod" VARCHAR2(255),
"operationtime" timestamp,
"loglevel" VARCHAR2(255),
"msg" VARCHAR2(555),
"ip" VARCHAR2(50)
);
CREATE SEQUENCE test_sequence INCREMENT BY 1;
测试类:
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.slf4j.MDC;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName MyDBAppenderTest
* @authtr zjt
* @Description TODO
* @createTime 2023年01月06日 16:03:00
*/
@SpringBootTest
@Slf4j
public class MyDBAppenderTest {
@Test
public void testDB(){
//自定义插入数据库的数据,在 MyDBAppender#bindLoggingEventWithInsertStatement 方法中的event.getMDCPropertyMap().get("ip")和event.getMDCPropertyMap().get("username") 中可以获取到这些值
Map context = new HashMap();
context.put("username", "test");
context.put("ip", "127.0.0.1");
MDC.setContextMap(context);
log.info("测试");
}
}
执行后查询数据库,即可看到测试数据结果
四. 原理
4.1 为何在达梦执行就需要加上主键
去看 DBAppenderBase 的启动类,可以看到这行代码
表示如果不支持主键又没有方言的数据库, 那么就不支持连接到数据库写日志,而达梦lomback不支持达梦的方言,所以只能走主键这条路了。
为何需要重写 getGeneratedKeysMethod
如上图,因为达梦没有方言,所以如果 cnxSupportsGetGeneratedKeys 也为false,那么就会直接报错导致无法启动,所以需要让这个值为true,那么肯定要让这个条件为true
if (this.getGeneratedKeysMethod() != null)
所以重写这个方法,让这段代码成功执行,这样项目就可以启动啦 !
至于这个方法干嘛的,后面也有介绍,如果不深究原理就不用看下去啦。
4.2 执行流程(建议自己打断点看,图为参考)
继承了 DBAppenderBase 这个类,让我们可以重写logback写入数据库的规则:
1.LOGID_INDEX = 1 这些是为了方便修改定义出来的,可以随意修改,主要对应的是 buildInsertSQL()方法中的占位符(例如第一个?,就代表1)
2. bindLoggingEventWithInsertStatement(PreparedStatement stmt, ILoggingEvent event) 这个方法是绑定sql中的 ? 对应的参数的
3. bindCallerDataWithPreparedStatement(PreparedStatement stmt, StackTraceElement[] callerDataArray) :与存储过程相关,可以不实现
4.subAppend(ILoggingEvent event, Connection connection, PreparedStatement insertStatement) :调用 2和3 然后对数据库进行插入(insertStatement.executeUpdate())
这里插入成功后会继续往下跑:
执行完 subAppend 后,还会继续执行 selectEventId(PreparedStatement insertStatement, Connection connection) 这个方法,继续深究:
看红框中的代码,这是反射的调用,insertStatement这个参数打个断点就能知道它的真正类型是 DmdbPreparedStatement,回到 我们定义的 MyDBAppender:getGeneratedKeysMethod() 中可以看到返回了这个Method对象 GET_GENERATED_KEYS_METHOD ,而在静态代码块中我们对它进行了初始化:
PreparedStatement.class.getMethod("getGeneratedKeys", (Class[]) null)
所以这里的rs是调用了这个方法: DmdbPreparedStatement:getGeneratedKeys,再点进去可以看到实际调用的是这个方法: do_getGenerateKeys(),如下图
看红框,我就是在这里栽倒了,这里需要你的日志表里面需要有主键,而我没有定义主键,但报错的话会报空指针异常,所以在数据库增加一个主键即可!!!