使用SpringAOP解决日志记录问题+获取MyBatis执行的SQL语句(企业中常用的日志审计功能)

前言

需求是这样的:每个接口都有不同的数据库操作。想要将这些请求和数据库操作放到日志当中,方便管理员查看有哪些操作被执行了。这里排除查询操作,只在日志中记录 update、insert、delete 这三个操作。期望的日志表中应该有每次执行的 sql 语句,所以就要获取到SQL语句。

 一. 思路

首先我们不能变更原有的代码,并且我们的需求是在进行数据库操作的时候才进行记录,那么我们就想到可以使用Spring AOP,定义一个切面:在每次执行到 Dao 层(也就是数据库操作)的时候,我们在这个切面类中获取到当前的 SQL 语句和其他的一些参数,然后在切面类中将当前操作插入到日志表中。

二.具体代码

1.建表(UnifiedLog)

CREATE TABLE UnifiedLog (
    Id BIGINT PRIMARY KEY IDENTITY(1,1),
    LogType VARCHAR(10) NOT NULL,
    SqlStatement NVARCHAR(MAX),
    Parameters NVARCHAR(MAX),
    UpdateCount INT,
    RequestUri NVARCHAR(500),
    RequestMethod VARCHAR(20),
    RequestBody NVARCHAR(MAX),
    ResponseBody NVARCHAR(MAX),
    SpendTime BIGINT,
    CreatedAt DATETIME2 NOT NULL
);

对应的实体类 MyBatisLog.java

import java.sql.Timestamp;

@Data
public class MyBatisLog {
    private Long id;
    private String logType;
    private String sqlStatement;
    private String parameters;
    private String updateCount;
    private String requestUri;
    private String requestMethod;
    private String requestBody;
    private String responseBody;
    private Long spendTime;
    private Timestamp createdAt;
    
}

这样,我们就有了表和对应的Java实体类

2.编写插入Mapper 

这里的功能是给日志表中插入数据的

@Mapper
public interface MyBatisLogMapper {
    @Insert("INSERT INTO UnifiedLog (LogType, SqlStatement, Parameters, UpdateCount, RequestUri, RequestMethod, RequestBody, ResponseBody, SpendTime, CreatedAt) " +
            "VALUES (#{logType}, #{sqlStatement}, #{parameters}, #{updateCount}, #{requestUri}, #{requestMethod}, #{requestBody}, #{responseBody}, #{spendTime}, CURRENT_TIMESTAMP)")
    void insertMyBatisLog(MyBatisLog myBatisLog);
}

这个Mapper后面我们需要在 AOP 切面中调用,以达到插入数据库的效果。

3.引入SQLUtils工具包

这个工具包是借鉴的其他大佬的,能够有效提取出将要执行的 SQL 语句

详情请见:springboot对mapper切面,获取mybatis执行的sql_mapper层可以作切面么-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/sdzhangshulong/article/details/104393244代码如下:

通过调用这个类中的方法可以获取到sql语句,其中还有通过正则表达式判断一个 SQL 是不是查询语句。



import com.sun.deploy.util.ArrayUtil;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.text.DateFormat;
import java.util.*;

public class SqlUtils {

    /**
     * 获取aop中的SQL语句
     * @param pjp
     * @param sqlSessionFactory
     * @return
     * @throws IllegalAccessException
     */
    public static String getMybatisSql(ProceedingJoinPoint pjp, SqlSessionFactory sqlSessionFactory) throws IllegalAccessException {
        Map<String,Object> map = new HashMap<>();
        //1.获取namespace+methdoName
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        String namespace = method.getDeclaringClass().getName();
        String methodName = method.getName();
        //2.根据namespace+methdoName获取相对应的MappedStatement
        Configuration configuration = sqlSessionFactory.getConfiguration();
        MappedStatement mappedStatement = configuration.getMappedStatement(namespace+"."+methodName);
//        //3.获取方法参数列表名
//        Parameter[] parameters = method.getParameters();
        //4.形参和实参的映射
        Object[] objects = pjp.getArgs(); //获取实参
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (int i = 0;i<parameterAnnotations.length;i++){
            Object object = objects[i];
            if (parameterAnnotations[i].length == 0){ //说明该参数没有注解,此时该参数可能是实体类,也可能是Map,也可能只是单参数
                if (object.getClass().getClassLoader() == null && object instanceof Map){
                    map.putAll((Map<? extends String, ?>) object);
                    System.out.println("该对象为Map");
                }else{//形参为自定义实体类
                    map.putAll(objectToMap(object));
                    System.out.println("该对象为用户自定义的对象");
                }
            }else{//说明该参数有注解,且必须为@Param
                for (Annotation annotation : parameterAnnotations[i]){
                    if (annotation instanceof Param){
                        map.put(((Param) annotation).value(),object);
                    }
                }
            }
        }
        //5.获取boundSql
        BoundSql boundSql = mappedStatement.getBoundSql(map);
        return showSql(configuration,boundSql);
    }

    /**
     * 解析BoundSql,生成不含占位符的SQL语句
     * @param configuration
     * @param boundSql
     * @return
     */
    private  static String showSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    String[] s =  metaObject.getObjectWrapper().getGetterNames();
                    s.toString();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    }
                }
            }
        }
        return sql;
    }

    /**
     * 若为字符串或者日期类型,则在参数两边添加''
     * @param obj
     * @return
     */
    private static String getParameterValue(Object obj) {
        String value = null;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(new Date()) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }
        }
        return value;
    }

    /**
     * 获取利用反射获取类里面的值和名称
     *
     * @param obj
     * @return
     * @throws IllegalAccessException
     */
    private static Map<String, Object> objectToMap(Object obj) throws IllegalAccessException {
        Map<String, Object> map = new HashMap<>();
        Class<?> clazz = obj.getClass();
        System.out.println(clazz);
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            String fieldName = field.getName();
            Object value = field.get(obj);
            map.put(fieldName, value);
        }
        return map;
    }

    /**
     * 正则表达式 判断一个sql语句是不是select语句
     *
     */
    public static boolean isSelectStatement(String sql) {
        String selectRegex = "^\\s*(select|SELECT)\\s+.*";
        return sql.matches(selectRegex);
    }
}

4. 正式编写 AOP 切面类


@Aspect
@Component
@Slf4j
public class WebLogAspect  {

    @Autowired
    private MyBatisLogMapper myBatisLogMapper;

    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    @Pointcut("execution(* com.joysonsafety.joysonoperationplatform.dao.mapper..*(..))")
    public void saveLog() {
    }

    @Around("com.joysonsafety.joysonoperationplatform.aspect.WebLogAspect.saveLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {

    }

}

我们首先要做好的是正确的创建出切面类,那么我们就要将切点自定义到mapper层,我的Mapper层如下:

 @Pointcut("execution(* com.joysonsafety.joysonoperationplatform.dao.mapper..*(..))")
    public void saveLog() {
    } 
这个代码就做到了将切面定义到了整个mapper中,这样每次执行到这些数据库操作,我们就能进入到切面类中执行我们想要的操作了

4. 完整代码


import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.joysonsafety.joysonoperationplatform.dao.mapper.MyBatisLogMapper;
import com.joysonsafety.joysonoperationplatform.entity.MybatisLog.MyBatisLog;

import com.joysonsafety.joysonoperationplatform.util.SqlUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.session.SqlSessionFactory;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.Properties;

@Aspect
@Component
@Slf4j
public class WebLogAspect  {

    @Autowired
    private MyBatisLogMapper myBatisLogMapper;
    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    @Pointcut("execution(* com.joysonsafety.joysonoperationplatform.dao.mapper..*(..))")
    public void saveLog() {
    }

    private static final ThreadLocal < Boolean > isInsideLogMethod = new ThreadLocal < > ();


    @Around("com.joysonsafety.joysonoperationplatform.aspect.WebLogAspect.saveLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        ServletRequestAttributes sra =  (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (!Objects.isNull(sra)){
            HttpServletRequest request = sra.getRequest();
            System.out.println("url: " + request.getRequestURI());
            System.out.println("method: "+request.getMethod());      //post or get? or ?
            System.out.println("class.method: " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
            System.out.println("args: "+joinPoint.getArgs());
        }
        //Object proceed = joinPoint.proceed();
        //3.获取SQL
        String sql = SqlUtils.getMybatisSql(joinPoint, sqlSessionFactory);
        

        long startTime = System.currentTimeMillis();
        //获取当前请求对象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Object result = joinPoint.proceed();

        try {
            //生成web日志
            generatorWebLog(joinPoint, startTime, request, result,sql);
        }catch (Exception e){
            log.error("web请求日志异常",e);
        }
        return result;

    }



    private void generatorWebLog(ProceedingJoinPoint joinPoint, long startTime, HttpServletRequest request, Object result,String sql) throws IllegalAccessException {
       if(!SqlUtils.isSelectStatement(sql)) {
           //检查当前线程是否已经在进行日志插入操作
           if(Boolean.TRUE.equals(isInsideLogMethod.get())){
               return;
           }
           try {
               //进入这个代码块中,先将当前线程设置为TRUE 这样第二个方法来的时候不会执行下面的代码
               isInsideLogMethod.set(true);
               MyBatisLog myBatisLog = new MyBatisLog();
               long endTime = System.currentTimeMillis();
               String urlStr = request.getRequestURL().toString();
               myBatisLog.setLogType("Web");
               myBatisLog.setSqlStatement(sql);
               myBatisLog.setUpdateCount(String.valueOf((Integer) result));
               myBatisLog.setRequestUri(request.getRequestURI());
               myBatisLog.setRequestMethod(request.getMethod());
               myBatisLog.setRequestBody(JSONUtil.toJsonPrettyStr(getParameter(joinPoint))); // Assumes this method extracts parameters
               String ret = "本次成功更新的条数:" + result.toString();
               myBatisLog.setResponseBody(ret);
               myBatisLog.setSpendTime(endTime - startTime);
//           myBatisLogMapper.insertMyBatisLog(new MyBatisLog("web",sql,null,ret,request.getRequestURI(),request.getMethod(),JSONUtil.toJsonPrettyStr(getParameter(joinPoint)),ret
//           ,endTime - startTime);
               myBatisLogMapper.insertMyBatisLog(myBatisLog);
               log.info("web请求日志:{}", JSONUtil.parse(myBatisLog));
           } finally {
               //清除标记
               isInsideLogMethod.remove();
           }

       }


    }

    /**
     * 根据方法和传入的参数获取请求参数
     */
    private Object getParameter(ProceedingJoinPoint joinPoint) {
        //获取方法签名
        MethodSignature signature =(MethodSignature) joinPoint.getSignature();
        //获取参数名称
        String[] parameterNames = signature.getParameterNames();
        //获取所有参数
        Object[] args = joinPoint.getArgs();
        //请求参数封装
        JSONObject jsonObject = new JSONObject();
        if(parameterNames !=null && parameterNames.length > 0){
            for(int i=0; i<parameterNames.length;i++){
                jsonObject.put(parameterNames[i],args[i]);
            }
        }
        return jsonObject;
    }

    /**
     * 获取方法描述
     */
    private String getDescription(ProceedingJoinPoint joinPoint) {
        //获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //获取方法
        Method method = signature.getMethod();
        //获取注解对象
        //ApiOperation annotation = method.getAnnotation(ApiOperation.class);
//        if (Objects.isNull(annotation)) {
//            return "";
//        }
//        return annotation.value();
        return "";
    }


}

上面的代码书写完毕,程序已经可以正常运行了!下面记录的是笔者遇到的问题和总结

        
        需要特别注意的是:我们插入日志表的操作也会被进入到 AOP 切面中被执行,所以就造成了无限套娃的场景。 比如:generatorWebLog 方法中,第一次进来的可能是更新语句,然后将更新语句保存到日志表(Insert操作)中,也会触发AOP此操作。这样执行了一次插入操作,然后一次又称一次的执行,这样就无限循环了!!!
        所以笔者在这里爬了很久的坑,最终帅气的同事给到了一个解决方案,那就是使用 ThreadLocal !

        new ThreadLocal<>(): 这行代码创建了一个新的 ThreadLocal实例。ThreadLocal 是 Java 中的一个特殊的类,它可以为每个线程提供一个独立的变量副本。

        它是全局共享的,每个线程都可以独立地访问和修改它的值,而不会相互干扰。这种设计通常用于需要保持线程独立状态的场景,比如日志记录、事务管理等。
      那么我们是如何使用 ThreadLocal 保证每次只执行一次语句呢?

 5.遇到的坑

解决套娃问题:确保 AOP 方法在当前线程中只执行一次。

如果第一次执行到了 update 语句,然后在将 update 语句保存到日志表之前,我们将当前线程的 ThreadLocal 设置标志为 True,然后我们执行到 插入日志表操作的 sql 时,会先进行判断,如果当前 ThreadLocal 已经是 True 了,那么说明当前有线程在使用 AOP ,那么此次就直接返回,然后他就去跑自己的正规业务了。

举例:update 语句被触发了 AOP,在保存日志表的时候,又一次触发了AOP的操作,此时这个AOP不生效,然后正确的执行了保存日志表操作。最后 update 语句执行完 AOP 后,正确的执行了自己的 update 操作。

三.总结

这个日志功能中,拿到SQL语句是我们的重中之重,大家可以借鉴上述代码进行业务的编写。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于电鸿(电力鸿蒙)的边缘计算网关,支持定制

1 产品信息 边缘计算网关基于平头哥 TH1520 芯片&#xff0c;支持 OpenHarmony 小型系统&#xff0c;是 连接物联网设备和云平台的重要枢纽&#xff0c;可应用于城市基础设施&#xff0c;智能工厂&#xff0c;智能建筑&#xff0c;营业网点&#xff0c;运营 服务中心相关场…

RK3568笔记四十一:DHT11驱动开发测试

若该文为原创文章&#xff0c;转载请注明原文出处。 记录开发单总线&#xff0c;读取DHT11温湿度 一、DHT11介绍 DHT11是串行接口&#xff08;单线双向&#xff09;DATA 用于微处理器与 DHT11之间的通讯和同步&#xff0c;采用单总线数据格式&#xff0c;一次通讯时间4ms左右…

好用的AI搜索引擎

1. 360AI 搜索 访问 360AI 搜索: https://www.huntagi.com/sites/1706642948656.html 360AI 搜索介绍&#xff1a; 360AI 搜索&#xff0c;新一代智能答案引擎&#xff0c;值得信赖的智能搜索伙伴&#xff0c;为复杂搜索提供专业支持&#xff0c;解锁更相关、更全面的答案。AI…

视频汇聚,GB28181,rtsp,rtmp,sip,webrtc,视频点播等多元异构视频融合,视频通话,视频会议交互方案

现在视频汇聚&#xff0c;视频融合和视频互动&#xff0c;是视频技术的应用方向&#xff0c;目前客户一般有很多视频的业务系统&#xff0c;如已有GB28181的监控&#xff08;GB现在是国内主流&#xff0c;大量开源接入和商用方案&#xff09;&#xff0c;rtsp设备&#xff0c;音…

建筑集团工程地产类公司网站源码系统 带完整的安装代码包以及搭建部署教程

系统概述 在数字化浪潮的推动下&#xff0c;建筑行业正经历着前所未有的变革。为了提升企业形象&#xff0c;优化客户体验&#xff0c;加强项目管理&#xff0c;建筑集团工程地产类公司急需一套高效、易用的网站源码系统。小编给大家分享一款专为建筑行业量身定制的网站源码系…

C语言 ——— 在控制台上打印动态变化的菱形

目录 代码要求 代码实现 代码要求 输入 整数line &#xff0c;菱形的上半部分的长度就为line&#xff08;动态变化的菱形&#xff09; 菱形由 "*" 号构成 代码实现 #include<stdio.h> int main() {// 上半长int line 0;scanf("%d", &line)…

MYSQL——库表操作

MYSQL——库表操作 1.1 SQL语句基础1.1.1. SQL简介1.1.2. SQL语句分类1.1.3. SQL语句的书写规范 1.2 数据库的操作1.2.1 数据库的登录及退出1.2.2 查看数据库1.2.3 创建数据库1.2.4 切换数据库1.2.5 查看当前用户1.2.6 删除数据库 1.3 MySQL字符集1.3.1. 字符集1.3.2. 字符序1.…

LeetCode刷题记录(第三天)55. 跳跃游戏

题目&#xff1a; 55. 跳跃游戏 标签&#xff1a;贪心 数组 动态规划 题目信息&#xff1a; 思路一&#xff1a;动态规划 确定dp数组含义&#xff1a; dp[i] 第[i]个位置能否达到确定递推公式&#xff1a; dp[i] 能不能达到&#xff0c;取决于前面d[i-j]&#xff0c;d[i-j…

7月18日学习打卡,数据结构堆

hello大家好呀&#xff0c;本博客目的在于记录暑假学习打卡&#xff0c;后续会整理成一个专栏&#xff0c;主要打算在暑假学习完数据结构&#xff0c;因此会发一些相关的数据结构实现的博客和一些刷的题&#xff0c;个人学习使用&#xff0c;也希望大家多多支持&#xff0c;有不…

SourceCodester v1.0 SQL 注入漏洞(CVE-2023-2130)

前言 CVE-2023-2130是一个影响SourceCodester Purchase Order Management System v1.0的SQL注入漏洞。此漏洞的存在是由于应用程序未能正确过滤和验证用户输入&#xff0c;使得攻击者可以通过SQL注入来执行任意SQL命令&#xff0c;从而对数据库进行未授权的访问和操作。 在利…

java学习--类方法和类方法

类变量 类中static变量会在类加载的时候创建&#xff0c;生存的地方在jdk版本8.0以后都是在堆中&#xff0c;之前都是在方法区&#xff0c;生成的空间不会随着新创建得对象一样变多&#xff0c;同一类的该static变量只会生成一个&#xff0c;每个新创建得对象可共享该变量 类方…

docker compose 容器 编排分组

遇到问题&#xff1a;执行docker compose up -d 后docker compose 创建的容器们 在desktop-docker 中都在docker下一堆 搜索想着能不能把这个docker名字改一下&#xff0c;但是都没有找到这样的一个方案&#xff1b; 最后发现&#xff0c;我执行docker compose up -d 命令所在…

我在百科荣创企业实践——简易函数信号发生器(5)

对于高职教师来说,必不可少的一个任务就是参加企业实践。这个暑假,本人也没闲着,报名参加了上海市电子信息类教师企业实践。7月8日到13日,有幸来到美丽的泉城济南,远离了上海的酷暑,走进了百科荣创科技发展有限公司。在这短短的一周时间里,我结合自己的教学经验和企业的…

前端三大主流框架Vue React Angular有何不同?

前端主流框架&#xff0c;Vue React Angular&#xff0c;大家可能都经常在使用&#xff0c;Vue React&#xff0c;国内用的较多&#xff0c;Angualr相对用的少一点。但是大家有思考过这三大框架的不同吗&#xff1f; 一、项目的选型上 中小型项目&#xff1a;Vue2、React居多…

昇思25天学习打卡营第23天 | 基于MindSpore的红酒分类实验

学习心得&#xff1a;基于MindSpore的红酒分类实验 在机器学习的学习路径中&#xff0c;理解和实践经典算法是非常重要的一步。最近我进行了一个有趣的实验&#xff0c;使用MindSpore框架实现了K近邻&#xff08;KNN&#xff09;算法进行红酒分类。这个实验不仅加深了我对KNN算…

layui 让table里的下拉框不被遮挡

记录&#xff1a;layui 让table里的下拉框不被遮挡 /* 这个是让table里的下拉框不被遮挡 */ .goods_table .layui-select-title,.goods_table .layui-select-title input{line-height: 28px;height: 28px; }.goods_table .layui-table-cell {overflow: visible !important; }.…

用DrissionPage过某里滑块分析

最近我又在找工作了&#xff0c;悲哀啊~&#xff0c;面试官给了一道题&#xff0c;要求如下&#xff1a; 爬虫机试&#xff1a;https://detail.1688.com/offer/643272204627.html 过该链接的滑动验证码&#xff0c;拿到正确的商品信息页html&#xff0c;提取出商品维度的信息&a…

详解SVN与Git相比存在的不足

原文全文详见个人博客&#xff1a; 详解SVN与Git相比存在的不足截至目前&#xff0c;我们已既从整理梳理的SVN和Git在设计理念上的差异&#xff0c;也重点对二者的存储原理和分支管理理念的差异进行深入分析。这些差异也直接造成了SVN和Git在分支合并、冲突解决、历史记录管理…

数据结构之二元查找树转有序双向链表详解与示例(C/C++)

文章目录 1. 二元查找树&#xff08;BST&#xff09;简介2. 有序双向链表&#xff08;DLL&#xff09;简介3. 二元查找树的实现4. 转换为有序双向链表的步骤5. C实现代码6. C实现代码7. 效率与空间复杂度比较8. 结论 在数据结构与算法中&#xff0c;树和链表都是非常重要的数据…