logback重写DBAppenderBase连接达梦数据库实现自定义日志

一. 背景

        本来项目组使用的数据库是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(),如下图

看红框,我就是在这里栽倒了,这里需要你的日志表里面需要有主键,而我没有定义主键,但报错的话会报空指针异常,所以在数据库增加一个主键即可!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值