springboot在feign和线程池中使用TraceId日志链路追踪(最终版)-2

文章目录

    • 简述
    • 问题
    • feign调用时给head加入traceId
      • FeignConfig配置
      • FeignConfig 局部生效
      • feign拦截器和配置合并为一个文件(最终版)
      • feign异步调用拦截器配置[不常用]
    • 使用TTL自定义线程池
      • 为什么需要TransmittableThreadLocal?
    • 总结
    • 参考和拓展阅读

简述

书接前文:SpringBoot使用TraceId日志链路追踪-1

在上文中我们使用了springboot+MDC+Log4j2支持链路ID打印,但是如果我们使用feigin调用其他服务,线程池如何获取链路id呢;这里我们要用到阿里的TTL,在多线程面试中也会常用问到一个问题那就是线程传递问题。刚好这里就实践以下具体用法,并且这也是一个很好的案例。

问题

Spring 默认的日志框架 Logback 中提供的 LogbackMDCAdapter 内部使用的是ThreadLocal,只有本线程才有效,子线程和下游的服务 MDC 里的值会丢失。

主要的难点是解决值传递问题,主要包括以下几个部分:
异步情况下(线程池)如何传递 MDC 中的 TraceId 到子线程
API Gateway 网关中如何传递 MDC 中的 TraceId
微服务之间互相远程调用时如何传递 MDC 中的 TraceId

阿里的TTL组件解决了线程池场景下的上下文传递问题。通过装饰线程池,TTL在任务提交时自动拷贝父线程的上下文到子线程,并在任务结束后清理副本,确保多级线程池调用链路完整。

feign调用时给head加入traceId

1.需要拦截feign的requst请求,并加入自定义的报文头信息
2.需要把这个拦截器配置到feign的配置项中

在application.yml配置文件里面,可以添加feign相关的配置信息,常见的配置信息有如下这些:

loggerLevel:日志级别,四个取值:

  • NONE 不打印日志;
  • BASIC:只打印请求方法、URL、响应状态码、执行时间。
  • HEADERS:打印请求头、响应头的日志信息。
  • FULL:打印所有日志。

连接配置参数

  • connectTimeout:连接超时时间,单位毫秒:ms。
  • readTimeout:读取超时时间,单位毫秒:ms。
  • retryer:重试策略。
  • requestInterceptors:自定义的拦截器,可以多个,是一个List集合。
  • defaultRequestHeaders:默认的请求头信息。
  • defaultQueryParameters:默认的查询参数信息。
  • followRedirects: 是否允许重定向。

这里我们开启HEADERS级别的日志,方便本地服务调用时打印报文头我们可以查看是否TraceId信息;


import feign.RequestInterceptor;
import feign.RequestTemplate;
import gyqx.spd.common.constants.GlobalConstants;
import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.jboss.logging.MDC;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import java.util.Optional;/*** @version 1.0.0* @Date: 2025/3/15 8:49* @Description: 自定义feigin请求拦截器加入traceId,甚至我们可以把前端携带的token信息和用户id等报文头也放进来,方便转发给下一个被调用的服务*/
@Slf4j
public class CustomFeignInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate template) {// TODO 在这里可以实现一些自定义的逻辑,例如:用户认证log.info("Feign执行拦截器....");Optional.ofNullable(RequestContextHolder.getRequestAttributes()).map(it -> ((ServletRequestAttributes) it).getRequest()).ifPresent(it -> {String traceId = it.getHeader(GlobalConstants.GLOBAL_X_TRACE_ID);if (StringUtil.isEmpty(traceId)) {traceId = MDC.get(GlobalConstants.GLOBAL_X_TRACE_ID) == null ? null : MDC.get(GlobalConstants.GLOBAL_X_TRACE_ID).toString();}if (StringUtil.isNotBlank(traceId)) {MDC.put(GlobalConstants.GLOBAL_X_TRACE_ID, traceId);template.header(GlobalConstants.GLOBAL_X_TRACE_ID, traceId);}log.info("Feign执行拦截器traceId={}", traceId);});}
}

FeignConfig配置

一般都是全局配置,每个服务引入依赖自动配置生效,当然如果只测试可以在某个feigin上自己加上


import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @version 1.0.0* @Date: 2024/3/15 8:53* @Description: OpenFeign 配置类全局生效(直接放入核心配置,其他服务引入该依赖项即可)*/
@Configuration
public class FeignConfig {/*** 注入自定义的拦截器*/@Beanpublic RequestInterceptor requestInterceptor() {return new CustomFeignInterceptor();}}

FeignConfig 局部生效

以下方式皆可

  • 把上面的配置文件FeignConfig只放入某个服务中生效(本文就是这种)
  • yam中配置只对某个服务生效时配置
# feign 配置
feign:client:config:# 这里写微服务的服务名称,例如:我这里写的是 service-provider 服务名称# 针对 service-provider 微服务的请求,都将执行这些配置信息service-provider:loggerlevel: full# 配置请求拦截器,可以多个requestInterceptors:- com.gitee.code.interceptor.CustomFeignInterceptor

feign拦截器和配置合并为一个文件(最终版)

上面的写法是拦截器和配置文件分开了,当然我们可以缩写直接合并为一个文件,全局生效即可;


import feign.RequestInterceptor;
import feign.RequestTemplate;
import gyqx.spd.common.constants.GlobalConstants;
import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.jboss.logging.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import java.util.Optional;/*** @version 1.0.0* @Date: 2024/3/15 8:53* @Description: OpenFeign 配置类+拦截器*/
@Slf4j
@Configuration
public class FeignConfig {/*** 注入自定义的拦截器*/@Beanpublic RequestInterceptor requestInterceptor() {return new RequestInterceptor(){@Overridepublic void apply(RequestTemplate template) {// TODO 在这里可以实现一些自定义的逻辑,例如:用户认证log.info("Feign执行拦截器....");Optional.ofNullable(RequestContextHolder.getRequestAttributes()).map(it -> ((ServletRequestAttributes) it).getRequest()).ifPresent(it -> {String traceId = it.getHeader(GlobalConstants.GLOBAL_X_TRACE_ID);if (StringUtil.isEmpty(traceId)) {traceId = MDC.get(GlobalConstants.GLOBAL_X_TRACE_ID) == null ? null : MDC.get(GlobalConstants.GLOBAL_X_TRACE_ID).toString();}log.info("Feign执行拦截器token:traceId={}", traceId);if (StringUtil.isNotBlank(traceId)) {MDC.put(GlobalConstants.GLOBAL_X_TRACE_ID, traceId);traceId= traceId.substring(traceId.indexOf(":") + 1);//我这里去掉了用户的token信息,不影响使用template.header(GlobalConstants.GLOBAL_X_TRACE_ID, traceId);}log.info("Feign执行拦截器traceId={}", traceId);});}};}@BeanLogger.Level feignLoggerLevel() {return Logger.Level.FULL;//日志级别全打印-方便调试,后续可以修改级别}
}

开启报文打印后,可以看到报文头里面已经有了traceId
在这里插入图片描述

在服务提供方的日志中也可以看到拦截到了
在这里插入图片描述

feign异步调用拦截器配置[不常用]

对于上面的配置对异步阻塞调用,异步非阻塞调用的情况就不再适用,需要单独把请求头获取后放入本地ThreadLocal变量中,也就是有个备份,然后放入异步线程的请求头里面。

详细请参考下面的blog,这种异步的异步项目中很少用到,但是放入本地ThreadLocal变量中的方式倒是很常用,比如我们把当前登录用户的session信息放入当前线程的上下文环境中,方便在业务中获取当前用户的ID,部门,权限等数据。

而这里把请求头的数据用拦截器拿到后放入本地ThreadLocal变量中只是为了方便调用其他feign异步接口时获取到当前线程携带过来的数据防止被覆盖和清空,为给另一个异步操作参数做准备工作。
https://blog.csdn.net/LatiaoCanCode/article/details/144358227

原理比较简单,上文中的TraceId我们也是在拦截器中获取并存入了MDC的ThreadLocal变量,这里其实也是自定义了一个ThreadLocal变量用户存放请求头信息,甚至可以拓展这个ThreadLocal变量为当前线程上下文环境变量CurrentThreadContex专门存放前端调用携带过来的上文数据作为缓存在当前线程中,方便后续操作使用。

对于线程部分的配置则无需参考,没有什么实际意义直接参考下面的自定义线程池使用TraceId

使用TTL自定义线程池

异步请求丢失上文的问题,这些问题追根究底都是ThreadLocal惹得祸。
由于ThreadLocal只能保存当前线程的信息,不能实现父子线程的继承。
说到这,很多人想到了InheritableThreadLocal,确实InheritableThreadLocal能够实现父子线程间传递本地变量,但是你的程序如果采用线程池,则存在着线程复用的情况,这时就不一定能够实现父子线程间传递了,因为在线程在线程池中的存在不是每次使用都会进行创建,InheritableThreadlocal是在线程初始化时intertableThreadLocals=true才会进行拷贝传递

失败样例

@Test
public void test() throws Exception {//单一线程池ExecutorService executorService = Executors.newSingleThreadExecutor();//InheritableThreadLocal存储InheritableThreadLocal<String> username = new InheritableThreadLocal<>();for (int i = 0; i < 10; i++) {username.set("公众号:牧竹子—"+i);Thread.sleep(3000);CompletableFuture.runAsync(()-> System.out.println(username.get()),executorService);}
}//打印如下
-----------------------
公众号:牧竹子—0
公众号:牧竹子—0
公众号:牧竹子—0
公众号:牧竹子—0

所以若使用的子线程是已经被池化的线程,从线程池中取出线下进行使用,是没有经过初始化的过程,也就不会进行父子线程的本地变量拷贝。
由于在日常应用场景中,绝大多数都是会采用线程池的方式进行资源的有效管理。

为什么需要TransmittableThreadLocal?

在Spring框架中,默认情况下,线程池的任务执行是通过java.util.concurrent包中的ThreadPoolExecutor实现的。然而,如果你想要在使用Spring框架的同时,利用阿里巴巴的TransmittableThreadLocal(TTL)来传递线程局部变量(ThreadLocal)的值到异步任务中,你可以通过自定义线程池配置来实现这一点。

线程中JUC默认父子线程传递的InheritableThreadlocal是在线程初始化时intertableThreadLocals=true才会进行拷贝传递。但是在线程中已经是初始化之后的线程无法获取新的拷贝所以总是初始值。

TransmittableThreadLocal是阿里巴巴开源的库,主要用于解决在使用线程池时,线程局部变量(ThreadLocal)不能正确地传递到异步任务中的问题。这在微服务架构或者使用Spring Boot进行异步处理时尤其重要

样例

@Test
public void test() throws Exception {//单一线程池ExecutorService executorService = Executors.newSingleThreadExecutor();//需要使用TtlExecutors对线程池包装一下executorService=TtlExecutors.getTtlExecutorService(executorService);//TransmittableThreadLocal创建TransmittableThreadLocal<String> username = new TransmittableThreadLocal<>();for (int i = 0; i < 10; i++) {username.set("公众号:牧竹子—"+i);Thread.sleep(3000);CompletableFuture.runAsync(()-> System.out.println(username.get()),executorService);}
}//打印如下
-----------------------
公众号:牧竹子—0
公众号:牧竹子—1
公众号:牧竹子—2

可以看到已经能够实现了线程池中的父子线程的数据传递。
在每次调用任务的时,都会将当前的主线程的TTL数据copy到子线程里面,执行完成后,再清除掉。同时子线程里面的修改回到主线程时其实并没有生效。这样可以保证每次任务执行的时候都是互不干涉。

替换 Spring 默认线程池 使用 alibaba 的 TtlRunnable进行替换。

        <dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId><version>2.14.4</version></dependency>

自定义线程池


import com.alibaba.ttl.TtlRunnable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.ThreadPoolExecutor;@Configuration
public class TaskThreadPoolConfig {@Autowiredprivate TaskThreadPoolProperties config;@Bean(name = "taskExecutor")public ThreadPoolTaskExecutor threadPoolTaskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();//核心线程池大小executor.setCorePoolSize(config.getCorePoolSize());//最大线程数executor.setMaxPoolSize(config.getMaxPoolSize());//队列容量executor.setQueueCapacity(config.getQueueCapacity());//活跃时间executor.setKeepAliveSeconds(config.getKeepAliveSeconds());//线程名字前缀executor.setThreadNamePrefix("MyExecutor-");// setRejectedExecutionHandler:当pool已经达到max size的时候,如何处理新任务// CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());executor.setWaitForTasksToCompleteOnShutdown(true);executor.setAwaitTerminationSeconds(120); //等待任务执行时间,如果超过这个时间还没有销毁就 强制销executor.setTaskDecorator(getTraceContextDecorator());//使用TTL包装executor.initialize();return executor;}private TaskDecorator getTraceContextDecorator() {return runnable -> TtlRunnable.get(() -> {try {//把父级值放入当前线程的MDC本地ThreadLocal中,log4j打印时使用MDC.put("traceId", GlobTraceContext.getTraceId());runnable.run();} finally {MDC.clear();}});}}

因为MDC默认使用的ThreadLocal缓存当前线程的值,因此这里不能在用MDC默认的方式,得使用TTL改写为TransmittableThreadLocal类型放线程变量;

GlobTraceContext


import cn.hutool.core.util.IdUtil;
import com.alibaba.ttl.TransmittableThreadLocal;/*** 基于TransmittableThreadLocal实现线程池安全的TraceID传递** @author wnhyang* @date 2025/3/3**/
public class GlobTraceContext {private static final TransmittableThreadLocal<String> TRACE_ID = new TransmittableThreadLocal<>();/*** 设置TraceID,并同步到Log4j2的MDC*/public static void setTraceId(String traceId) {TRACE_ID.set(traceId);}public static String getTraceId() {return TRACE_ID.get();}public static void clear() {TRACE_ID.remove();}public static String generateTraceId() {return IdUtil.simpleUUID();}
}

AsyncService


import gyqx.spd.outside.conf.GlobTraceContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;/*** 异步调用service* @author summer*/
@Service
@Slf4j
public class AsyncService {/*** 使用 @Async 注解 实现异步调用* taskExecutor为自定义线程池,指定自定义线程池* @return* @throws InterruptedException*/@Async("taskExecutor")public void async(){log.info("async异步任务开始: " + Thread.currentThread().getName());try {log.info("子线程traceId:: " + GlobTraceContext.getTraceId());// 模拟耗时操作(实际工作中,此处写业务逻辑处理)Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}log.info("async异步任务完成");}
}

AsyncService

@RequestMapping("test")
public class TestController {@Autowiredprivate AsyncService asyncService;@Resourceprivate ThreadPoolTaskExecutor taskExecutor;//使用我们自定义的线程池@GetMapping("/async")public AjaxResult<String> async() {String traceId = UUID.randomUUID().toString();System.out.println("父亲线程traceId:" +traceId);GlobTraceContext.setTraceId(traceId);//异步执行CompletableFuture.runAsync(() -> asyncService.async(), taskExecutor);//        asyncService.async();log.info("async异步任务调用成功");return AjaxResult.ok("async异步任务调用成功");}}

打印结果如下

父亲线程traceId:db9a27e4-549a-4a41-ba7d-25297d875dca
controller.HrpStockController  : async异步任务调用成功
controller.AsyncService     : async异步任务开始: MyExecutor-1
controller.AsyncService     : 子线程traceId:: db9a27e4-549a-4a41-ba7d-25297d875dca
controller.AsyncService     : async异步任务完成

总结

要实现线程池支持的全局链路追踪需要上一章节和本章节feign拦截器中都要使用TransmittableThreadLocal作为替换ThreadLocal类型MDC作为存放链路ID,同样在父子线程中想存放其他值也需要使用TransmittableThreadLocal类型。

这里我们需要把上一章,和本章的LogInterceptor,FeignConfig中的MDC.put前加上GlobTraceContext.setTraceId即可,在线程池配置中我们在包装TTL的时候执行任务前把获取到的,getTraceId()放入当前线程的MDC变量中,这样log4j打印日志即可拿到当前线程的traceId值

在这里插入图片描述

参考和拓展阅读

SpringBoot使用TraceId日志链路追踪-1
https://blog.csdn.net/zjcjava/article/details/146237248

微服务中使用阿里开源的TTL,优雅的实现身份信息的线程间复用
https://developer.aliyun.com/article/12012001056715.html

阿里开源支持缓存线程池的ThreadLocal Transmittable ThreadLocal(TTL)
https://www.cnblogs.com/xiaopotian/p/11056715.html

配置spring/springboot默认的异步线程池
https://www.cnblogs.com/duanxz/p/6084494.html?ivk_sa=1024320u

ThreadLocal父子线程数据传递
https://blog.csdn.net/zjcjava/article/details/125601123

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL数据库单表与多表查询

一.单表查询 1.创建用于数据查询的数据库表 CREATE TABLE worker (部门号 int(11) NOT NULL,职工号 int(11) NOT NULL,工作时间 date NOT NULL,工资 float(8,2) NOT NULL,政治面貌 varchar(10) NOT NULL DEFAULT 群众,姓名 varchar(20) NOT NULL,出生日期 date NOT NULL,PRIM…

海外紧固件市场格局与发展趋势研究报

一、引言 紧固件作为各类机械装备、建筑结构以及电子设备中不可或缺的基础性零部件&#xff0c;在国民经济的各个领域都有着广泛应用。其市场动态与全球经济发展态势以及各行业的兴衰紧密相连。在全球化进程不断加速、产业分工日益精细的大背景下&#xff0c;深入研究海外紧固…

【多学科稳定EI会议大合集】计算机应用、通信信号、电气能源工程、社科经管教育、光学光电、遥感测绘、生物医学等多学科征稿!

在当今科技高速发展的时代&#xff0c;多学科领域的学术交流与融合显得尤为重要。以下是稳定EI会议合集&#xff0c;涵盖计算机、信息通信、电气能源、社科经管教育、光学遥感、生物医学等多个学科领域。 会议皆已通过国际知名出版社出版审核&#xff0c;EI检索稳定&#xff0…

【深度学习新浪潮】展平RVQ技术详解

展平 RVQ(Flattened Residual Vector Quantization)是一种基于矢量量化(Vector Quantization, VQ)的技术,主要用于高效地表示和压缩数据(例如图像、音频或文本嵌入)。它结合了**残差矢量量化(Residual Vector Quantization, RVQ)**的思想与“展平”操作,从而进一步优…

【第23节】windows网络编程模型(WSAEventSelect模型)

目录 引言 一、WSAEventSelect模型概述 二、 WSAEventSelect模型的实现流程 2.1 创建一个事件对象&#xff0c;注册网络事件 2.2 等待网络事件发生 2.3 获取网络事件 2.4 手动设置信号量和释放资源 三、 WSAEventSelect模型伪代码示例 四、完整实践示例代码 引言 在网…

LlamaFactory部署及模型微调【win10环境】

1.Llama-Factory简介 LLaMA-Factory&#xff0c;全称 Large Language Model Factory&#xff0c;旨在简化大模型的微调过程&#xff0c;帮助开发者快速适应特定任务需求&#xff0c;提升模型表现。它支持多种预训练模型和微调算法&#xff0c;适用于智能客服、语音识别、机器翻…

Jmeter简介、学习目标及安装启动

1. 简介 JMeter 是 Apache 组织使用 Java 开发的一款测试工具&#xff1a;可以用于对服务器、网络或对象模拟巨大的负载&#xff1b;通过创建带有断言的脚本来验证程序是否能返回期望的结果。 1&#xff09;优点&#xff1a;开源、免费&#xff1b;跨平台&#xff1b;支持多协…

无参数读文件和RCE

什么是无参数&#xff1f; 无参数&#xff08;No-Argument&#xff09;的概念&#xff0c;顾名思义&#xff0c;就是在PHP中调用函数时&#xff0c;不传递任何参数。我们需要利用仅靠函数本身的返回值或嵌套无参数函数的方式&#xff0c;达到读取文件或远程命令执行&#xff0…

细胞内与细胞间网络整合分析!神经网络+细胞通讯,这个单细胞分析工具一箭双雕了(scTenifoldXct)

生信碱移 细胞间-细胞内通讯网络分析 scTenifoldXct&#xff0c;一种结合了细胞内和细胞间基因网络的计算工具&#xff0c;利用 scRNA-seq 数据检测细胞间相互作用。 单细胞 RNA 测序&#xff08;scRNA-seq&#xff09;能够以稳健且可重复的方式同时收集数万个细胞的转录组信息…

怎么处理 Vue 项目中的错误的?

一、错误类型 任何一个框架,对于错误的处理都是一种必备的能力 在Vue 中,则是定义了一套对应的错误处理规则给到使用者,且在源代码级别,对部分必要的过程做了一定的错误处理。 主要的错误来源包括: 后端接口错误代码中本身逻辑错误二、如何处理 后端接口错误 通过axi…

05.AI搭建preparationの(transformers01)BertTokenizer实现分词编码

一、下载 bert-base-chinese镜像下载 二、简介作用&#xff1a; 模型每个参数占用的字节大小模型大小模型大小层数头数GPT-14 个字节的 FP32 精度浮点数117M446MB1212GPT-22 个字节的 FP161.5亿到1.75亿0.5GB到1.5GB4816GPT-32 个字节的 FP161.75万亿&#xff08;17500亿&a…

工业4G路由器赋能智慧停车场高效管理

工业4G路由器作为智慧停车场管理系统通信核心&#xff0c;将停车场内的各个子系统连接起来&#xff0c;包括车牌识别系统、道闸控制系统、车位检测系统、收费系统以及监控系统等。通过4G网络&#xff0c;将这些系统采集到的数据传输到云端服务器或管理中心&#xff0c;实现信息…

git 基础操作

1. git 的安装 与 卸载 1.1. git 的安装 判断是否安装 git git --version 安装 git: centos: sudo yum -y install git ubuntu: sudo apt-get install git -y windows: 3.安装git和图形化界面工具_哔哩哔哩_bilibili 1.2. git 的卸载 判断是否安装 git git --version…

【计算机网络】计算机网络协议、接口与服务全面解析——结合生活化案例与图文详解

协议、接口与服务 导读一、协议1.1 定义1.2 组成 二、接口三、服务3.1 定义3.2 服务与协议的区别3.3 分类3.3.1 面向连接服务于无连接服务3.3.2 可靠服务和不可靠服务3.3.3 有应答服务和无应答服务 结语 导读 大家好&#xff0c;很高兴又和大家见面啦&#xff01;&#xff01;…

Vue.js 完全指南:从入门到精通

1. Vue.js 简介 1.1 什么是 Vue.js? Vue.js(通常简称为 Vue)是一个用于构建用户界面的渐进式 JavaScript 框架。所谓"渐进式",意味着 Vue 的设计是由浅入深的,你可以根据自己的需求选择使用它的一部分或全部功能。 Vue 最初由尤雨溪(Evan You)在 2014 年创…

qt QOffscreenSurface详解

1、概述 QOffscreenSurface 是 Qt 中用于离屏渲染的一个类。它允许在不直接与屏幕交互的情况下进行 OpenGL 渲染操作&#xff0c;常用于生成纹理、预渲染场景等。通过 QOffscreenSurface&#xff0c;可以在后台创建一个渲染表面&#xff0c;进行绘制操作&#xff0c;并将结果捕…

如何使用VS中的Android Game Development Extension (AGDE) 来查看安卓 Logcat 日志

一、首先按照以下 指引 中的 第1、2步骤&#xff0c;安装一下 AGDE &#xff0c;AGDE 的安装包可以在官网上找到。 UE4 使用AndroidGameDevelopmentExtension&#xff08;AGDE&#xff09;对安卓客户端做“断点调试”与“代码热更”-CSDN博客 在执行第二步骤前&#xff0c;记得…

NodeJs之fs模块

一、定义&#xff1a; fs 模块可以实现与硬盘的交互。例如&#xff1a;文件的创建、删除、重命名、移动&#xff1b;文件内容的写入、读取&#xff1b;文件夹的操作。 二、引入 fs 模块&#xff1a; const fs require(fs)三、文件写入&#xff1a; 1、异步写入&#xff1a;w…

Android14 Settings应用添加有线网开关条目实现

Android14 Settings应用添加有线网开关条目 文章目录 Android14 Settings应用添加有线网开关条目一、前言二、适配修改1、network_provider_settings.xml2、NetworkProviderSettings.java3、TurnOnOffEthernetNetworkController.java4、去除有线网提示条目。5、效果UI&#xff…

微信小程序如何接入直播功能

一、小程序直播开通背景 1.政府资质要求 政府的要求&#xff0c;小程序开通直播需要注册主体具备互联网直播的资质&#xff0c;普通企业需要《信息网络传播视听节目许可证》&#xff0c;表演性质的直播需要《网络文化经营许可证》&#xff0c;政府主体需要《社会信用代码》及…