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

news2024/12/23 7:22:47

一. 背景

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/144689.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TCP/IP协议

作者&#xff1a;~小明学编程 文章专栏&#xff1a;JavaEE 格言&#xff1a;热爱编程的&#xff0c;终将被编程所厚爱。 目录 应用层 XML json ​编辑 protobuffer 传输层 UDP的数据报文格式 TCP报文格式 TCP的可靠传输 确认应答&#xff08;安全机制&#xff09; …

Java版本TransE代码的学习

参考资料 Anery/transE: transE算法 简单python实现 FB15k (github.com) Translating Embeddings for Modeling Multi-relational Data (nips.cc) 输入 1.数据集S 2.Entities集合E 3.Relations集合L 4.margin hyperparameter γ 5.每个向量的长度 k 初始化 为Entities集…

服务发现组件:Consul简易攻略

简介 Consul是一种分布式、高可用性、支持多数据中心的解决方案&#xff0c;可动态、分布式基础架构连接和配置的应用程序。 特性 『多数据中心』  Consul是为支持数据中心而构建的&#xff0c;可以支持任意数量的区域&#xff0c;而无需复杂的配置。『服务网格』  Consul服…

ROS移动机器人——32电机驱动

驱动电机&#xff0c;我们在这里使用TB6612来驱动电机&#xff0c;同时&#xff0c;电机的内部我们有编码器进行计次 输出比较 cnt&#xff0c;就是时基单元寄存器 ccr&#xff0c;在cnt下方&#xff0c;为捕获比较寄存器&#xff0c;cc是捕获比较的意思&#xff0c;r就是寄存…

内存溢出问题排查

一、问题背景下午突发服务器CPU频繁撑爆&#xff0c;服务启动后不久就挂掉。一周前系统有一次投产&#xff0c;之后再没有更新过系统。同时在日志中看到大量的dubbo服务调用失败。二、排查问题产生原因1.查看JVM崩溃日志hs_err_pid.logJVM崩溃时会生成hs_err_pid_xxx.log日志文…

面经分享|2022年秋招斩获7个规控算法工程师offer

该面经来自深蓝学院用户投稿&#xff0c;作者为东北大学自动化专业硕士。 作者2022年秋招情况&#xff1a;投递的岗位包括决策规划&#xff0c;规划控制和控制岗位。本人累计投递70家自动驾驶公司&#xff0c;最终收获10多家公司的笔试或面试反馈。最终拿到的offer包括主机厂、…

抖音短视频运营中的六大定位法

抖音六大定位法 1年龄反转法年龄反转法的秘诀就是指&#xff0c;所设定的人物、性格与实际的年龄并不是特别符合&#xff0c;从而让用户产生比较强烈的差异感。比如之前很火的北海爷爷&#xff0c;是一位有着七十多岁高龄的先生&#xff0c;但他依然很有精神&#xff0c;举止优…

【Mybatis-plus 入门教程】

&#x1f308;博客主页&#xff1a;屠一乐的博客 &#x1f4c5; 发文时间&#xff1a;2023.1.6 &#x1f388; 一定存在只有你才能做成的事 &#x1f339; 博主水平有限&#xff0c;如有错误&#xff0c;欢迎指正 欢迎各位&#x1f44d;收藏&#x1f48e;评论✉ MyBatisPlus …

舆情监测技术手段有哪些,网络舆情监测监测技术应用到哪些方面?

随着网络舆情的快速发展&#xff0c;舆情影响到方方面面&#xff0c;大多数企业将网络舆情监测纳入其日常工作。对于舆情监测上如何做到全面监测&#xff0c;那些技术上可以实现&#xff0c;接下来TOOM舆情监测小编带您简单了解舆情监测技术手段有哪些&#xff0c;网络舆情监测…

C语言 文件处理

文件操作 为什么使用文件 什么是文件 程序文件 数据文件 文件名 文件的打开和关闭 文件指针 只要没打开一个文件就会有一个文件信息区&#xff0c;只要一更改文件信息区也会跟着更改 **FILE *fopen( const char filename, const char mode ); filename 文件名 *char mode 打…

疑难杂症之vscode--During startup program exited with code 0xc0000139.--缺失重要文件(杂记)

问题展示在vscode中&#xff0c;只要用了STL容器&#xff0c;就会出现这样的提示发现自己的 vscode 不能运行带有部分 stl 库的程序&#xff0c;编译不会报错&#xff0c;运行也不会报错但是也没有结果&#xff0c;调试的话会有下图中报错。拿一个以前的程序做测试&#xff0c;…

Java 集合系列:Vector源码深入解析

概论 学完ArrayList和LinkedList之后&#xff0c;我们接着学习Vector。学习方式还是和之前一样&#xff0c;先对Vector有个整体认识&#xff0c;然后再学习它的源码&#xff1b;最后再通过实例来学会使用它。 第1部分 Vector介绍 Vector简介 Vector 是矢量队列&#xff0c;…

java服装商城购物商场项目源码

简介 Java基于ssm开发的服装商城&#xff0c;用户可以浏览商品和特价商品&#xff0c;加入购物车&#xff0c;直接下单支付&#xff0c;在我的个人中心里可以管理自己的订单&#xff0c;收货地址&#xff0c;编辑资料等。管理员可以发布商品&#xff0c;上下架商品&#xff0c…

Neo4j详细介绍及使用教程

文章目录一、Neo4j介绍1.Neo4j简介2.图数据库简介3.Neo4j的优缺点4.Neo4j的常见应用场景二、使用教程1.下载安装2.数据插入和查询(1)基本概念(2)基本语法Ⅰ.CREATE操作——创建Ⅱ.MERGE——创建或更新Ⅲ.Match操作——查找指定的图数据Ⅳ.DELETE操作——删除节点3.JAVA实战一、…

FPGA之VGA/LCD数字时钟显示

文章目录前言一、LCD显示控制1.LCD显示一个字符2.LCD显示多个字符二、数字时钟输出1.数字时钟2.十进制数据拆分BCD码三、按键检测及LCD驱动1.按键检测2.LCD驱动四、总结前言 软件实现了在4.3寸LCD左上角显示一个数字时钟&#xff0c;效果如下图所示。本文针对VGA/LCD控制时许有…

leetcode:2103. 环和杆(python3解法)

难度&#xff1a;简单 总计有 n 个环&#xff0c;环的颜色可以是红、绿、蓝中的一种。这些环分布穿在 10 根编号为 0 到 9 的杆上。 给你一个长度为 2n 的字符串 rings &#xff0c;表示这 n 个环在杆上的分布。rings 中每两个字符形成一个 颜色位置对 &#xff0c;用于描述每个…

makefile 入门

make常用选项 # make 默认在当前目录中寻找GUNmakefile,makefile,Makefile的文件作为make的输入文件 # -f 可以指定默认的输入文件名,如: -f MyMakefile # -v 显示make版本号 # -n 只输出命令,但不执行,一般用于测试 # -s 只执行命令,但不显示具体命令,与在命令中使用作用一样…

第四十四讲:神州防火墙双机热备配置

两台防火墙硬件型号和软件版本都完全相同&#xff0c;为了避免防火墙不堪重负而宕机引起网络中断&#xff0c;可以考虑应用双机热备&#xff08;HA&#xff09;解决方案。双机热备能够把两台防火墙构成一个工作组&#xff0c;一主一备&#xff0c;保证数据通信畅通&#xff0c;…

【实际开发01】- 单元测试 ( 追求正确性 )

目录 0. 单元测试 概念 / 解析 1. 为什么要进行单元测试 1. JUnit ~ Test 2. IDEA 中使用 junit 单元测试 , 不能使用 Scanner 的解决方法 3. Junit 测试 Tutorial 1. daiding 4. Test 修饰的方法必须 public 1. validatePublicVoidNoArgMethods(Test.class, false, er…

功率二极管的损耗分析和选型原则

功率二极管的损耗分析和选型原则 tip&#xff1a;参考网上资料&#xff0c;学习为主 1.二极管的分类 2.二极管的损耗组成 3.二级管的损耗分析 4.应用实例1.Flyback电源电路二极管损耗计算 5.实例应用2.BOOST电路二极管损耗计算 6.实例应用3.大功率整流桥二极管参数计算 7.选型…