Springboot日志监听功能

news2025/4/5 19:41:02

目录

  • 1. 概述
    • 1.1. 需求
    • 1.2. 思路
  • 2. 功能实现
    • 2.1 依赖选取
    • 2.2 编写logBack.xml
    • 2.3 日志拦截
    • 2.4 封装请求为HttpServletRequestWrapper
    • 2.5 AOP
    • 2.6 日志监听
  • 3. 后记

1. 概述

1.1. 需求

背景:拆分支付系统的日志,把每笔单子的日志单独拎出来存库。每笔单子会有多次前后端交互,以及多次后端与第三方的交互,我们需要把这笔单子的所有交互流程从日志中提炼出来,合到一块儿,然后存库。

需求:把单笔单子的明细展示、费用预结算、状态查询、回调通知的日志都提炼出来存库。

1.2. 思路

  1. 线程区分:因为前端会同时存在多次不同请求到后端(并发情况),所以要给日志打印中加上一个ThreadName来区分不同的线程;
  2. 请求区分:要区分不同的请求,需要给不同请求都单独配置一个特定的requestId;
  3. 固定requestId:因为一笔订单会有多个流程也就是多次请求,要保证每次请求的requestId都相同,为了不改库表结构,可以将requestId存入redis缓存,其key值可以取:固定前缀+订单号。因此,每次前端的请求都要传订单号作为参数;
  4. 内存暂存日志:使用currentHashMap暂时将日志写入内存,方便后续取出对应日志,并存表;
  5. 编写一个logBack.xml,要支持AppenderBase的日志监听;
  6. 编写一个拦截器:拦截请求,给每次请求都初始化requestId;
  7. 编写一个aop:打印方法调用的入参、返参以及其他自定义日志;
  8. 编写一个日志监听器:监听日志打印,从而截取拼接需要的日志。

2. 功能实现

2.1 依赖选取

我选择使用lombok搭配slf4j来打印日志,依赖如下:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

其他依赖:
Json操作

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.45</version>
</dependency>

Springbootweb:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2.2 编写logBack.xml

注意:

  1. 为了保证跨平台的一致性,建议使用 logback.xml 作为 Logback 的配置文件名。这样做可以避免在不同操作系统或不同环境中出现文件名识别的问题。
  2. 将 logback.xml 存放在 src/main/resources 目录下是一种标准且推荐的做法,这样可以确保 Logback 能够正确加载配置文件,并使项目结构更加清晰和一致。

案例:

<configuration>

    <!-- 定义日志文件的存储路径 -->
    <property name="LOG_PATH" value="./logs"/>

    <!-- 定义日志文件的最大大小和保留时间 -->
    <property name="MAX_FILE_SIZE" value="10MB"/>
    <property name="MAX_HISTORY" value="30"/>
    <property name="TOTAL_SIZE_CAP" value="3GB"/>

    <!-- 彩色日志渲染 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%highlight(%X{requestId})] [%highlight(%thread)] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 滚动文件日志渲染 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 日志文件存储位置 -->
        <file>${LOG_PATH}/myPay.log</file>
        
        <!-- 定义滚动策略 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 按时间和大小滚动日志文件,输出文件名包括日期和索引 -->
            <fileNamePattern>${LOG_PATH}/myPay.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
            <maxHistory>${MAX_HISTORY}</maxHistory>
            <totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
        </rollingPolicy>

        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{requestId}] [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 根Logger配置 -->
    <root level="INFO">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </root>

    <!-- 添加对 LoggingEventListener 的支持 -->
    <appender name="CUSTOM_LISTENER" class="com.yourpackage.LoggingEventListener"/>
    <logger name="ROOT" level="INFO" additivity="false">
        <appender-ref ref="CUSTOM_LISTENER"/>
    </logger>

</configuration>

说明:

  1. %thread:Logback 的一个内置转换词,它会自动获取当前执行日志记录时的线程名。你不需要在代码中额外配置任何东西,SLF4J 会通过 Logback 自动捕获并记录线程名。
  2. %X{}:Logback 日志配置中的一个占位符,用于在日志输出时插入一个来自 Mapped Diagnostic Context (MDC) 的值。

    注意:MDC(Mapped Diagnostic Context)是 SLF4J 和 Logback 提供的一种机制,用于在日志记录中添加和管理上下文信息。线程私有的数据。

  3. appender:负责将日志事件写入特定目标或介质的组件。每个 appender 可以配置为将日志发送到不同的输出位置,比如控制台、文件、数据库、远程服务器等。

    注意:只要在 logback.xml 中正确配置了 appender 标签,并将自定义的 AppenderBase 实现(如 LoggingEventListener)配置为一个 appender,我们就可以使用 AppenderBase 的 append 方法来处理和监听日志事件。

按照上面的logback.xml配置,我的日志输出会长下面这样:

2024-08-14 12:34:56.789 [requestId] [thread] INFO  com.mypackage.ClassName - This is a log message

2.3 日志拦截

拦截器实现:

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.UUID;

// 自定义个key值固定前缀
import static xxx.IConst.KEY_PRE;
// 自定义的日志节点常量接口
import xxx.PMCLogNode;
// 自定义的redis工具类
import xxx.RedisClient;

/**
 * Description: 日志拦截器
 */
@Slf4j
@Component
public class LogInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String requestId;
        /* 
            每笔订单的第一个流程都是一样的,都是调用这个接口,
            所以在调用这个接口的时候给日志生成一个requestId,并存入MDC
            然后获取该日志的订单号medOrgOrd,拼接成redis的key
            然后将requestId存入缓存,再将requestId存入MDC
        */
        if (PMCLogNode.START.equals(request.getRequestURI())) {
            requestId = String.valueOf(UUID.randomUUID());
            String medOrgOrd = getMedOrgOrd(request);
            RedisClient.setString(KEY_PRE + medOrgOrd, requestId, 2 * 60 * 60);
        } else {
            /*
                非初始请求,改从缓存获取requestId
            */
            String medOrgOrd = getMedOrgOrd(request);
            requestId = RedisClient.getString(KEY_PRE + medOrgOrd);
        }
        MDC.put("requestId", requestId);
        log.info("请求路径为==>{}", request.getRequestURI());
        return true;
    }

    /*
        获取请求参数中的订单号medOrgOrd的方法
    */
    private String getMedOrgOrd(HttpServletRequest request) {
        BufferedReader reader = null;
        try {
            reader = request.getReader();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        StringBuilder jsonBuilder = new StringBuilder();
        String line;

        while (true) {
            try {
                if ((line = reader.readLine()) == null) break;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            jsonBuilder.append(line);
        }

        String json = jsonBuilder.toString();

        JSONObject jsonObject = JSONObject.parseObject(json);

        return jsonObject.getString("medOrgOrd");
    }
}

注册拦截器:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LogInterceptor logInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 LogInterceptor,并拦截所有请求路径
        registry.addInterceptor(logInterceptor).addPathPatterns("/**");
    }
}

2.4 封装请求为HttpServletRequestWrapper

虽然我上面的拦截器里面只调用了request.getRequestURI()获取请求url,并没去使用request.getInputStream()、request.getReader(),按理说我后续还是可以通过@RequestBody等注解来接收参数的,但经过测试,后续还是会报错:java.lang.IllegalStateException: getReader() has already been called for this request。
因此,为了以防万一,还是写个filter将HttpServletRequest封装为HttpServletRequestWrapper,这样就可以多次调用request.getInputStream()、request.getReader()了。

  1. CustomHttpServletRequestWrapper继承HttpServletRequestWrapper,并覆盖一些必要的方法。

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    
    public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
    
        // 对比String类型,byte[]更通用,适合处理任何类型的数据,特别是非文本数据。
        private byte[] body;
    
        public CustomHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
            super(request);
            this.body = request.getInputStream().readAllBytes(); // 读取请求体为字节数组
        }
    
        @Override
        public InputStream getInputStream() throws IOException {
            return new ByteArrayInputStream(body);
        }
    
        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(getInputStream(), this.getCharacterEncoding()));
        }
    
        public byte[] getBody() {
            return this.body;
        }
    }
    
  2. 过滤器,拦截所有请求,将HttpServletRequest封装为HttpServletRequestWrapper。

    import XXX.CustomHttpServletRequestWrapper;
    import lombok.SneakyThrows;
    import lombok.extern.slf4j.Slf4j;
    
    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * description:
     */
    @WebFilter(filterName = "requestFilter", urlPatterns = {"/*"})
    @Slf4j
    public class RequestFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) {
    
        }
    
        @Override
        @SneakyThrows
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
            ServletRequest requestWrapper = null;
            if (request instanceof HttpServletRequest) {
                requestWrapper = new CustomHttpServletRequestWrapper((HttpServletRequest) request);
            }
            if (requestWrapper == null) {
                chain.doFilter(request, response);
            } else {
                chain.doFilter(requestWrapper, response);
            }
    
        }
    
        @Override
        public void destroy() {
    
        }
    }
    

    @SneakyThrows的作用:Lombok 提供的一个注解,用于简化异常处理。它可以让你在方法中抛出受检异常(checked exception),而无需显式地使用 try-catch 块或者在方法签名中声明 throws。

2.5 AOP

import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
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.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.util.UUID;

// 自定义的 resultBean
import XXX.ResultBean;

/**
 * description:日志生成idAop
 *
 * @author Li Jianqiao
 * @since 2020/11/2 9:13
 */
@Aspect
@Configuration
@Slf4j
public class LogAop {
    /**
     * 定义切点Pointcut
     * 第一个*号:表示返回类型, *号表示所有的类型
     * 第二个*号:表示类名,*号表示所有的类
     * 第三个*号:表示方法名,*号表示所有的方法
     * 后面括弧里面表示方法的参数,两个句点表示任何参数
     */
    @Pointcut("execution(* com.mypackage..*(..)) " +
            "|| execution(* otherpackage.*.*(..)))
    public void executionService() {
        // 记录日志方法切点
    }

    @Around("executionService()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object[] args = proceedingJoinPoint.getArgs();
        log.info("====>@Before:请求方法为:[{}.{}]", proceedingJoinPoint.getSignature().getDeclaringTypeName(), proceedingJoinPoint.getSignature().getName());
        log.info("====>@Before:请求参数:");
        for (Object arg : args) {
            if (arg instanceof ServletRequest || arg instanceof ServletResponse || arg instanceof MultipartFile) {
                //ServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
                //ServletResponse不能序列化 从入参里排除,否则报异常:java.lang.IllegalStateException: getOutputStream() has already been called for this response
                continue;
            }
            try {
                log.info("==>{}", JSONObject.toJSONString(arg));
            } catch (JSONException e) {
                log.info("==>{}", arg);
            }
        }
        // 开始计时
        long begin = System.currentTimeMillis();
        // 执行方法
        Object result = proceedingJoinPoint.proceed();
        // 结束计时
        long end = System.currentTimeMillis();
        // 耗时
        long spendTime = end - begin;
        String resultStr = "";
        if (result instanceof ResultBean) {
            resultStr = ((ResultBean<?>) result).toStringJSON();
        } else {
            resultStr = JSONObject.toJSONString(result);
        }
        log.info("<====@After:方法[{}]耗时:{} ms", proceedingJoinPoint.getSignature().getName(), spendTime);
        log.info("<====@After:响应参数:");
        log.info("<=={}", resultStr);
        return result;
    }

}

2.6 日志监听

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

// 节点
import XXX.PMCLogNode;
// 用来存日志的表的dto
import XXX.entity.PaymentLog;
// 日志记录表的service
import XXX.PaymentLogService;

/**
 * @desc
 */

@Slf4j
@Component
public class LoggingEventListener extends AppenderBase<ILoggingEvent> {

    // 初始化一个线程池
    private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(100);
    // 采用 ConcurrentHashMap(线程安全) 来存储日志到内存,以便后续读取
    private static final ConcurrentMap<String, StringBuilder> logRecord = new ConcurrentHashMap<>();

    @Resource
    private PaymentLogService paymentLogService;

    private static final Map<String, Integer> pmcLogNodes = new HashMap<>();

    // 不同的日志节点,执行时间可能不同,因此要使用不同的等待时间
    static {
        pmcLogNodes.put(PMCLogNode.GETPAYURL, 1);
        pmcLogNodes.put(PMCLogNode.GETMEDICALCONTENTA, 3);
        pmcLogNodes.put(PMCLogNode.GETFRONTCONFIG, 1);
        pmcLogNodes.put(PMCLogNode.AUTHQUERY, 15);
        pmcLogNodes.put(PMCLogNode.UNIFIEDORDER, 15);
        pmcLogNodes.put(PMCLogNode.TOPAY, 15);
        pmcLogNodes.put(PMCLogNode.QUERYNOTIFYSTATUS, 20);
        pmcLogNodes.put(PMCLogNode.NOTIFY, 30);
    }

    @PostConstruct
    public void init() {
        ((ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory.getLogger("ROOT"))
                .addAppender(this);
        start();
    }

    @Override
    protected void append(ILoggingEvent eventObject) {
        // 获取当前行日志的内容
        String message = eventObject.getFormattedMessage();
        // 获取线程名
        String threadName = eventObject.getThreadName();
        // 从MDC获取requestId
        String requestId = eventObject.getMDCPropertyMap().get("requestId");
        // 用于获取触发日志事件的 Logger 的名称
        String loggerName = eventObject.getLoggerName();
        String key = requestId + threadName;

        logRecord.merge(key, new StringBuilder(loggerName).append("\t -- \t").append(message),
                (oldValue, newValue) -> oldValue.append("\n").append(loggerName).append("\t -- \t").append(message));

        if (pmcLogNodes.containsKey(message)) {
            startLog(threadName, requestId, message);
        }
    }

    private void startLog(String threadName, String requestId, String node) {
        Future<?> future = executorService.submit(() -> {
            try {
                processLogs(threadName, requestId, node);
            } catch (Exception ex) {
                log.error("处理日志时发生异常: {}", ex.getMessage());
            }
        });
    }

    private void processLogs(String threadName, String requestId, String node) {
        while (!Thread.currentThread().isInterrupted()) {
            // 根据node来判断大概要用多少时间
            try {
                TimeUnit.SECONDS.sleep(pmcLogNodes.get(node));
                String currentKey = requestId + threadName;
                String logTxt;

                logTxt = Objects.requireNonNull(logRecord.computeIfPresent(currentKey, (k, v) -> {
                    // 取完数据,就删掉,防止存不下
                    logRecord.remove(currentKey);
                    return v;
                })).toString();

                if (PMCLogNode.GETPAYURL.equals(node)) {
                    PaymentLog paymentLog = new PaymentLog()
                            .setLogTxt(logTxt)
                            .setOutTradeNo(extractValue(logTxt, "medOrgOrd"))
                            .setAppId(extractValue(logTxt, "appId"))
                            .setRequestId(requestId)
                            .setPTime(LocalDateTime.now());
                    paymentLogService.save(paymentLog);
                    break;
                } else {
                    List<PaymentLog> paymentLogs = paymentLogService.getPaymentLogsByOutTradeNo(extractValue(logTxt, "medOrgOrd"), requestId);
                    if (paymentLogs.size() != 1) {
                        log.error("日志记录异常: 数据库异常");
                        break;
                    }
                    PaymentLog paymentLog = paymentLogs.get(0);
                    paymentLog.setLogTxt(paymentLog.getLogTxt() + "\n" + logTxt);
                    paymentLogService.updateById(paymentLog);
                    break;
                }

            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            } catch (Exception ex) {
                log.error("日志处理过程中发生错误:{}", ex.getMessage());
                break;
            }
        }
    }

    private static String extractValue(String sb, String key) {
        String regex = String.format("\"%s\"\\s*:\\s*\"([^\"]+)\"", key);
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(sb);

        if (matcher.find()) {
            return matcher.group(1);
        }
        return null;
    }

    public void shutdown() {
        executorService.shutdown();
    }
}

3. 后记

如果是非外部请求接口打印的日志,会丢失requestId,导致上面的日志监听类无法获取到key,从而存库,这时候就需要在相应的位置根据订单号从缓存获取requestId了,然后MDC.put(“requestId”, requestId),接着log.info(一个固定的值),最终在pmcLogNodes.put(一个固定的值, 30);来匹配了。

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

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

相关文章

如何将高清图片修复?3个方法一键还原图片

如何将高清图片修复&#xff1f;高清图片修复是一个涉及图像处理技术的复杂过程&#xff0c;是对图片进行简单的调整或优化。这个过程旨在最大程度地恢复和提升图片的清晰度、细节和整体视觉效果&#xff0c;使其更加逼真、生动。通过高清图片的修复&#xff0c;我们可以让老旧…

稀疏注意力:时间序列预测的局部性和Transformer的存储瓶颈

时间序列预测是许多领域的重要问题&#xff0c;包括对太阳能发电厂发电量、电力消耗和交通拥堵情况的预测。在本文中&#xff0c;提出用Transformer来解决这类预测问题。虽然在我们的初步研究中对其性能印象深刻&#xff0c;但发现了它的两个主要缺点:(1)位置不可知性:规范Tran…

C++_2_ inline内联函数 宏函数(2/3)

C推出了inline关键字&#xff0c;其目的是为了替代C语言中的宏函数。 我们先来回顾宏函数&#xff1a; 宏函数 现有个需求&#xff1a;要求你写一个Add(x,y)的宏函数。 正确的写法有一种&#xff0c;错误的写法倒是五花八门&#xff0c;我们先来“见不贤而自省也。” // …

windows下部署redis3.2

一、下载redis3.2的包 6.2.6的包也有&#xff0c;但无法安装为Windows服务&#xff0c;暂时舍弃。 直接运行&#xff1a; redis-server redis.windows.conf 修改密码, 对应 redis.windows.conf 中的 requirepass 节点&#xff0c;注意去掉前面的# 修改端口&#xff0c;对应…

缺陷检测AI 重要参数解释

一、参数介绍 基本参数 True Positives (TP) True Positives (TP) 是一个用于评估模型性能的术语。它指的是模型正确预测为正例&#xff08;Positive&#xff09;的样本数量&#xff0c;即实际为正例且被正确分类为正例的样本数量。 False Positives (FP) FP (False Posit…

Python 文件目录操作,以及json.dump() 和 json.load()

import os 是用来引入 Python 标准库中的 os 模块的&#xff0c;这个模块提供了与操作系统交互的功能。这个模块常用于文件和目录操作&#xff0c;比如获取文件的目录路径、创建目录等。 如果你在代码中需要使用与操作系统相关的功能&#xff08;例如获取目录名、检查文件是否…

qt-11基本对话框(消息框)

基本对话框--消息框 msgboxdlg.hmsgboxdlg.cppmain.cpp运行图QustionMsgInFormationMsgWarningMsgCriticalMsgAboutMsgAboutAtMsg自定义 msgboxdlg.h #ifndef MSGBOXDLG_H #define MSGBOXDLG_H#include <QDialog> #include <QLabel> #include <QPushButton>…

Cesium模型制作,解决Cesium加载glb/GLTF显示太黑不在中心等问题

Cesium模型制作&#xff0c;解决Cesium加载glb/GLTF显示太黑不在中心等问题 QQ可以联系这里&#xff0c;谢谢

电商搜索新纪元:大模型引领购物体验革新

随着电商行业的蓬勃发展&#xff0c;搜索技术作为连接用户与商品的桥梁&#xff0c;其重要性日益凸显。在技术不断革新的今天&#xff0c;电商搜索技术经历了哪些阶段&#xff1f;面对大模型的飞速发展&#xff0c;企业又将如何把握趋势&#xff0c;应对挑战&#xff1f;为了深…

openinstall支持抖音游戏小手柄监测,助力游戏联运生态高效增长

近来&#xff0c;抖音“小手柄”功能风靡游戏广告生态&#xff0c;通过新颖的联运形式成功将游戏广告触达到抖音整个流量池&#xff0c;由于受众较广&#xff0c;小手柄也是目前直播场数、点赞数最高的形式。 为了帮助广告主快速捕捉流量红利&#xff0c;打通抖音小手柄的数据…

【选型指南】大流量停车场和高端停车场如何选择停车场管理系统?

在当今快节奏的城市生活中&#xff0c;大型停车场和高端车场的运营者面临着一系列挑战&#xff0c;尤其是在车辆流量大和客户期望高的情况下。选择一个合适的停车场管理系统&#xff0c;不仅关系到日常运营的效率&#xff0c;更关系到客户的满意度和车场的整体形象。 捷顺科技认…

螺纹钢生产线中测径仪对基圆和负公差的测量和影响

螺纹钢生产线中测径仪的作用 在螺纹钢生产线中&#xff0c;测径仪是一种关键的检测设备&#xff0c;它负责对螺纹钢的基圆直径、横肋和纵肋等尺寸进行实时测量。测径仪的数据对于监控和控制螺纹钢的生产质量至关重要&#xff0c;尤其是在进行负公差轧制时&#xff0c;它能够提供…

K8S中使用英伟达GPU —— 筑梦之路

前提条件 根据不同的操作系统&#xff0c;安装好显卡驱动&#xff0c;并能正常识别出来显卡&#xff0c;比如如下截图&#xff1a; GPU容器创建流程 containerd --> containerd-shim--> nvidia-container-runtime --> nvidia-container-runtime-hook --> libnvid…

【Spring Boot - 注解】@ResponseBody 注解:处理 JSON 响应

文章目录 一、ResponseBody 注解概述1. 注解的功能2. 主要功能 二、ResponseBody 的工作原理1. 接口定义2. 消息转换器3. 自动配置与默认行为 三、ResponseBody 的应用场景1. RESTful API 的实现2. 返回复杂数据结构3. 错误处理和异常处理 四、ResponseBody 的配置和自定义1. 自…

Rabbit的学习——从安装到集群

一、MQ概念 1.1、异步通讯和同步通讯 1.2、同步调用和异步调用 1.2.1、同步调用 1.2.2、异步调用 1.3、消息队列的作用 1.3.1、流量削峰/限流 1.3.2、 应用解耦 1.3.3、异步处理 1.4、消息队列的两种模式 1.4.1、点对点模式 1.4.2、发布/订阅模式 二、RabbitMQ 2.1…

MyBatis Plus批量写入慢?

1. 数据库连接配置 在使用 MyBatis Plus 进行批量插入之前&#xff0c;首先需要配置数据库连接。在连接 URL 中添加 &rewriteBatchedStatementstrue&#xff0c;以提高批量插入的性能。以下是一个示例&#xff1a; spring.datasource.urljdbc:mysql://localhost:3306/your…

路径规划 | 基于改进蝙蝠算法的多无人机路径规划(Matlab)

目录 效果一览基本介绍程序设计参考文献 效果一览 基本介绍 路径规划 | 基于改进蝙蝠算法的多无人机路径规划&#xff08;Matlab&#xff09; 蝙蝠算法&#xff08;Bat Algorithm&#xff09;是一种基于自然界蝙蝠群体行为的启发式优化算法。该算法模拟了蝙蝠在寻找食物时的行为…

Linux 内核源码分析---内核ICMP协议分析

因特网控制报文协议ICMP&#xff08;Internet Control Message Protocol&#xff09; 是一个差错报告机制&#xff0c;是TCP/IP协议簇中的一个重要子协议&#xff0c;通常被IP层或更高层协议&#xff08;TCP或UDP&#xff09;使用&#xff0c;属于网络层协议&#xff0c;主要用…

论文阅读-Transformer Layers as Painters

1. 摘要 尽管大语言模型现在已经被广泛的应用于各种任务&#xff0c;但是目前对其并没有一个很好的认知。为了弄清楚删除和重组预训练模型不同层的影响&#xff0c;本文设计了一系列的实验。通过实验表明&#xff0c;预训练语言模型中的lower和final layers与中间层分布不一致…

四路一体行车记录仪,语音提示注意行人,保障车辆行驶安全

在叉车、货车、客车等行业中&#xff0c;随着运输业务量的不断增加&#xff0c;行车安全问题已经成为了一大难题。经常会发生车祸、司乘人身安全无保障、货物损失等意外情况&#xff0c;这些事件不仅会给企业带来经济损失&#xff0c;也会影响对应行业的整体形象。 如何提高运…