logback日志自定义占位符
前言
在大型系统运维中,很大程度上是需要依赖日志的。在java大型web工程中,一般都会使用slf4j+logback这一个组合来实现日志的管理。
logback中很多现成的占位符可以可以直接使用,比如线程号【%t】、时间【%d】、日志等级【%p】,更多详细的占位符可以参考后文的占位符总结。
实际应用中,仅仅使用这些已经存在的占位符可能不够,比如我想给我的日志加一个traceId,通过这个traceId我可以快速的定位到我这个请求的所有日志,方便日志追踪。
针对日志中自定义占位符,logback提供了两种解决方案。下面分别来介绍一下
自定义traceId占位符
使用MDC
mdc的全称是Mapped Diagnostic Context,是Logback日志框架中的一个功能,它允许你在同一个线程的执行路径上设置一系列的上下文信息(即键值对),这些上下文信息可以自动地添加到日志输出中
logback.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false"><contextName>${APP_NAME}</contextName><property name="APP_NAME" value="MouseDemo"/><property name="LOG_PATH" value="./system_log/MouseDemo"/><property name="CONSOLE_LOG_PATTERN"value="%d | %X{traceId}| %highlight(%-5level) | %boldYellow(%thread) | %boldGreen(%logger) | %msg%n"/><!--输出到控制台--><appender name="console" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>${CONSOLE_LOG_PATTERN}</pattern><charset>UTF-8</charset></encoder></appender><root level="info"><appender-ref ref="console"/></root></configuration>
在logback中,自定义的占位符是通过%X{配置的键}来获取的
设置MDC
在web工程中,设置traceId的时机很重要,一般是放在拦截器中。
自定义一个拦截器
package com.tml.mouseDemo.config;import cn.hutool.core.lang.UUID;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import static com.tml.mouseDemo.constants.CommonConstants.TRACE_ID;@Slf4j
public class TraceInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String traceId = UUID.fastUUID().toString(true);log.info("TraceInterceptor preHandle,generate traceId is:{}", traceId);MDC.put(TRACE_ID, traceId);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("TraceInterceptor afterCompletion,ready to clean traceId:{}", MDC.get(TRACE_ID));MDC.remove(TRACE_ID);}
}
注册拦截器
package com.tml.mouseDemo.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.nio.charset.Charset;
import java.util.List;@Configuration
public class WebAppConfig implements WebMvcConfigurer {@Beanpublic HttpMessageConverter<String> responseBodyConverter() {StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(Charset.forName("UTF-8"));return stringHttpMessageConverter;}@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {converters.add(responseBodyConverter());}@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new TraceInterceptor()).addPathPatterns("/**");registry.addInterceptor(new RequestInterceptor()).addPathPatterns("/**");}
}
演示案例
经过上面两步的配置,日志上就会额外多一个traceId了
先定义一个service类
package com.tml.mouseDemo.service;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;@Service
@Slf4j
public class TraceService {public void trace(String type) throws InterruptedException {log.info("trace start,type:{}", type);Thread.sleep(1000L);log.info("trace end");}
}
定义一个restful接口
package com.tml.mouseDemo.controller;import com.tml.mouseDemo.constants.CommonResponse;
import com.tml.mouseDemo.service.TraceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;@RestController
@Slf4j
public class TraceController {@Autowiredprivate TraceService traceService;@PostMapping("/trace")public CommonResponse<String> trace(String type) throws InterruptedException {log.info("trace type:{}", type);traceService.trace(type);return CommonResponse.success("success");}}
这里的案例是单线程,看下运行结果
日志中,确实是多了一个traceId,符合预期
不过MDC有其局限性,仅支持单线程的数据传递。因为其底层是基于ThreadLocal来实现的,如下图
局限性
下面来看一下再多线程环境下,使用MDC会有什么样的效果
package com.tml.mouseDemo.controller;import com.tml.mouseDemo.constants.CommonResponse;
import com.tml.mouseDemo.service.TraceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;@RestController
@Slf4j
public class TraceController {@Autowiredprivate ThreadPoolExecutor executor;@Autowiredprivate Executor ttlExecutor;@Autowiredprivate TraceService traceService;@PostMapping("/traceWithThread")public CommonResponse<String> traceWithThread(String type) throws InterruptedException {log.info("traceWithThread type:{}", type);traceService.trace(type);//模拟异步发短信new Thread(() -> {try {Thread.sleep(2000);String sendMsg = "hello world";log.info("sendMsg:{}", sendMsg);} catch (Exception e) {log.error("traceWithThread occur error", e);}}, "trace--001").start();return CommonResponse.success("success");}}
这里通过在主线程中new一个子线程来模拟多线程,运行一下看下结果

从图中可以看出,子线程的日志中是没有输出traceId的,这个就是MDC的局限性。看过前文阿里巴巴TransmittableThreadLocal使用指南的朋友们应该知道,这种new出来的线程不支持,那么使用线程池也肯定是不支持数据传递。
使用自定义Converter
相比MDC,使用自定义的Converter就会显得更加的灵活了,看下使用自定义的Converter的使用流程
定义traceId的存取
package com.tml.mouseDemo.config;import com.alibaba.ttl.TransmittableThreadLocal;public class TraceContext {private static final ThreadLocal<String> TRACE_CONTEXT = new TransmittableThreadLocal<>();public static String get() {return TRACE_CONTEXT.get();}public static void set(String age) {TRACE_CONTEXT.set(age);}public static void clean() {if (TRACE_CONTEXT.get() != null) {TRACE_CONTEXT.remove();}}
}
MDC使用的是ThreadLocalL来存储,我们这里自定义采用阿里巴巴的TransmittableThreadLocal,为什么要使用这个,详细的可以参考这篇博文阿里巴巴TransmittableThreadLocal使用指南
定义Converter
package com.tml.mouseDemo.core.log;import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.tml.mouseDemo.config.TraceContext;import java.util.Optional;public class TraceIdConverter extends ClassicConverter {@Overridepublic String convert(ILoggingEvent iLoggingEvent) {return Optional.ofNullable(TraceContext.get()).orElse("");}
}
拦截器调整
package com.tml.mouseDemo.config;import cn.hutool.core.lang.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@Slf4j
public class TraceInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String traceId = UUID.fastUUID().toString(true);log.info("TraceInterceptor preHandle,generate traceId is:{}", traceId);TraceContext.set(traceId);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("TraceInterceptor afterCompletion,ready to clean traceId:{}", TraceContext.get());TraceContext.clean();}
}
改成从TraceContext中获取traceId,拦截器的注册和上文的保持一致。
注册Converter
<conversionRule conversionWord="traceId" converterClass="com.tml.mouseDemo.core.log.TraceIdConverter"/>
logback.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false"><conversionRule conversionWord="traceId" converterClass="com.tml.mouseDemo.core.log.TraceIdConverter"/><contextName>${APP_NAME}</contextName><property name="APP_NAME" value="MouseDemo"/><property name="LOG_PATH" value="./system_log/MouseDemo"/><property name="CONSOLE_LOG_PATTERN"value="%d | %traceId| %highlight(%-5level) | %boldYellow(%thread) | %boldGreen(%logger) | %msg%n"/><!--输出到控制台--><appender name="console" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>${CONSOLE_LOG_PATTERN}</pattern><charset>UTF-8</charset></encoder></appender><root level="info"><appender-ref ref="console"/></root></configuration>
这里直接使用注册的Converter的traceId来作为占位符,而不是使用%X{键}来获取traceId
演示案例
直接new线程
代码案例和MDC中使用new创建线程一样,直接看下运行结果

子线程的日志中有traceId,并且是和父线程的traceId保持一致,达到预期
使用线程池
为了达到演示效果,我这里定义了两个线程池
package com.tml.mouseDemo.config;import com.alibaba.ttl.threadpool.TtlExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.concurrent.*;@Slf4j
@Configuration
public class CommonConfig {Thread.UncaughtExceptionHandler exceptionHandler = (Thread t, Throwable e) -> {log.info("current thread occurs error!", e);};@Beanpublic ThreadPoolExecutor executor() {ThreadFactory threadFactory = new ThreadFactoryBuilder().setUncaughtExceptionHandler(exceptionHandler).setNameFormat("mouse-worker-%d").build();int processors = Runtime.getRuntime().availableProcessors();log.info("processors:{}", processors);ThreadPoolExecutor executor = new ThreadPoolExecutor(1,processors * 2,0L,TimeUnit.MILLISECONDS,new LinkedBlockingDeque<>(1000),threadFactory,new ThreadPoolExecutor.AbortPolicy());return executor;}/*** 使用阿里的 TransmittableThreadLocal 装饰线程池* @return*/@Beanpublic Executor ttlExecutor() {ThreadFactory threadFactory = new ThreadFactoryBuilder().setUncaughtExceptionHandler(exceptionHandler).setNameFormat("mouse-worker-ttl-%d").build();int processors = Runtime.getRuntime().availableProcessors();log.info("processors:{}", processors);ThreadPoolExecutor executor = new ThreadPoolExecutor(1,processors * 2,0L,TimeUnit.MILLISECONDS,new LinkedBlockingDeque<>(1000),threadFactory,new ThreadPoolExecutor.AbortPolicy());Executor ttlExecutor = TtlExecutors.getTtlExecutor(executor);return ttlExecutor;}}
这里线程池的核心线程数设置为1,是为了方便测试,和这篇文章的思路一致web项目国际化指南
测试代码
package com.tml.mouseDemo.controller;import com.tml.mouseDemo.constants.CommonResponse;
import com.tml.mouseDemo.service.TraceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;@RestController
@Slf4j
public class TraceController {@Autowiredprivate ThreadPoolExecutor executor;@Autowiredprivate Executor ttlExecutor;@Autowiredprivate TraceService traceService;@PostMapping("/traceWithThreadPool")public CommonResponse<String> traceWithThreadPool(String type) throws InterruptedException {log.info("traceWithThreadPool type:{}", type);traceService.trace(type);//模拟异步发短信executor.execute(() -> {try {Thread.sleep(2000);String sendMsg = "hello world";log.info("sendMsg:{}", sendMsg);} catch (Exception e) {log.error("traceWithThreadPool occur error", e);}});//模拟异步发短信ttlExecutor.execute(() -> {try {Thread.sleep(2000);String sendMsg = "hello world";log.info("sendMsg:{}", sendMsg);} catch (Exception e) {log.error("traceWithThreadPool occur error", e);}});return CommonResponse.success("success");}
}
运行两次,看下效果
可以看到,使用ttl装饰的线程池的日志是正常的,使用普通的线程池的日志的traceId错乱了,这个也是尤其需要注意的点。
总结
在logback中,可以使用多种方式来自定义占位符,通常一般的做法主要是上面两种。使用MDC的方式就是简单,不过他的缺陷也是显而易见的,就是他不支持多线程的数据传递。
所以,如果你的项目中想要自定义占位符,强烈建议使用自定义Convertor的方式。详细的代码已上传至github,欢迎前来围观我的github
常见的日志占位符总结
这里总结下logback中常见的占位符,欢迎补充
| 占位符名称 | 占位符含义 | 备注 |
| %d | 日期和时间 | %d{HH:mm:ss.SSS} %d{yyyy-MM-dd HH:mm:ss} 可以自定义时间的格式 |
| %t | 线程名称 | |
| %p | 日志的优先级 | |
| %c | 日志记录器名称 | 一般表示类名,可以%c{20},可以通过 这样来设置类名的最大长度 |
| %m | 日志消息 | |
| %n | 换行符 | |
| %L | 日志行号 | |
| %X{key} | 代表 MDC中存储的 key 对应的信息 | 不建议使用,不支持多线程 |
相关文章:
logback日志自定义占位符
前言 在大型系统运维中,很大程度上是需要依赖日志的。在java大型web工程中,一般都会使用slf4jlogback这一个组合来实现日志的管理。 logback中很多现成的占位符可以可以直接使用,比如线程号【%t】、时间【%d】、日志等级【%p】,…...
Vue平台开发三——项目管理页面
前言 对于多个项目的使用,可能需要进行项目切换管理,所以这里创建一个项目管理页面,登录成功后跳转这个页面,进行选择项目,再进入Home页面展示对应项目的内容。 一、实现效果图预览 二、页面内容 功能1、项目列表展…...
用于牙科的多任务视频增强
Multi-task Video Enhancement for Dental Interventions 2022 miccai Abstract 微型照相机牢牢地固定在牙科手机上,这样牙医就可以持续地监测保守牙科手术的进展情况。但视频辅助牙科干预中的视频增强减轻了低光、噪音、模糊和相机握手等降低视觉舒适度的问题。…...
【Node.js]
一、概述 Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境 ,使用了一个事件驱动、非阻塞式I/O模型, 让JavaScript 运行在服务端的开发平台,它让JavaScript成为与PHP、Python、Perl、Ruby等服务端语言平起平坐的脚本语言。 官网地…...
【Elasticsearch】腾讯云安装Elasticsearch
Elasticsearch 认识Elasticsearch安装Elasticsearch安装Kibana安装IK分词器分词器的作用是什么?IK分词器有几种模式?IK分词器如何拓展词条?如何停用词条? 认识Elasticsearch Elasticsearch的官方网站如下 Elasticsearch官网 Ela…...
【网络协议】ACL(访问控制列表)第一部分
概述 网络安全在网络中的重要性不言而喻。本文(即第一部分)将介绍ACL的基本概念以及标准ACL的配置。第二部分将重点讨论扩展ACL、其他相关概念以及ACL的故障排除。 文章目录 概述ACL定义数据包过滤ACLACL配置指导原则配置ACL的三条规则ACL功能ACL工作原…...
2025.1.20——一、[RCTF2015]EasySQL1 二次注入|报错注入|代码审计
题目来源:buuctf [RCTF2015]EasySQL1 目录 一、打开靶机,整理信息 二、解题思路 step 1:初步思路为二次注入,在页面进行操作 step 2:尝试二次注入 step 3:已知双引号类型的字符型注入,构造…...
Spring Boot 整合 Knife4j:打造更优雅的 API 文档
在现代 Web 应用开发中,API 文档的重要性不言而喻。清晰、准确、易用的 API 文档不仅可以方便开发者理解和使用 API,还能提高团队协作效率。Knife4j 是一个基于 Swagger 的增强型 API 文档工具,它可以为 Spring Boot 项目生成美观、易于交互的…...
Kafka 源码分析(一) 日志段
首先我们的 kafka 的消息本身是存储在日志段中的, 对应的源码是下面这段代码: class LogSegment private[log] (val log: FileRecords,val lazyOffsetIndex: LazyIndex[OffsetIndex],val lazyTimeIndex: LazyIndex[TimeIndex],val txnIndex: TransactionIndex,val baseOffset:…...
javaEE初阶————多线程初阶(2)
今天给大家带来第二期啦,保证给大家讲懂嗷; 1,线程状态 NEW安排了工作还未开始行动RUNNABLE可工作的,或者即将工作,正在工作BLOCKED排队等待WAITING排队等待其他事TIMED_WAITING排队等待其他事TERMINATED工作完成了 …...
Redis学习笔记1【数据类型和常用命令】
Redis学习笔记 基础语法 1.数据类型 String: 最基本的类型,可以存储任何数据,例如文本或数字。示例值为 hello world。Hash: 用于存储键值对,适合存储对象或结构体。示例值为 {"name": "Jack", "age": 21}。…...
JavaWeb项目——查询角色列表到页面中——转发模式
一、知识点 1、req.getRequestDispatch与resp.sendRedirect跳转方式的比较 一、实现原理 1、req.getRequestDispatcher: 属于服务器端跳转,在服务器内部将请求转发给另一个资源(如另一个 Servlet 或 JSP 页面)。调用 getReques…...
feign调用跳过HTTPS的SSL证书校验配置详解
一、问题抛出 如果不配置跳过SSL证书校验,当Feign客户端尝试连接到一个使用自签名证书的服务器时,可能会抛出类似以下的异常: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building faile…...
今天也是记录小程序进展的一天(破晓时8)
嗨嗨嗨朋友们,今天又来记录一下小程序的进展啦!真是太激动了,项目又迈出了重要的一步,231啦!感觉每一步的努力都在积累,功能逐渐完善,离最终上线的目标越来越近了。大家一直支持着这个项目&…...
SQL-leetcode—1084. 销售分析 III
1084. 销售分析 III 表: Product --------------------- | Column Name | Type | --------------------- | product_id | int | | product_name | varchar | | unit_price | int | --------------------- product_id 是该表的主键(具有唯一值的列&…...
Linux C\C++编程-文件位置指针与读写文件数据块
【图书推荐】《Linux C与C一线开发实践(第2版)》_linux c与c一线开发实践pdf-CSDN博客 《Linux C与C一线开发实践(第2版)(Linux技术丛书)》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 Linu…...
Flask简介与安装以及实现一个糕点店的简单流程
目录 1. Flask简介 1.1 Flask的核心特点 1.2 Flask的基本结构 1.3 Flask的常见用法 1.3.1 创建Flask应用 1.3.2 路由和视图函数 1.3.3 动态URL参数 1.3.4 使用模板 1.4 Flask的优点 1.5 总结 2. Flask 环境创建 2.1 创建虚拟环境 2.2 激活虚拟环境 1.3 安装Flask…...
【自动化测试】—— Appium使用保姆教程
目录 一. 连接手机 1. 授权 2. 调试 3. 获取参数 二. 启动APP 1. 启动Appium服务 2. 启动Appium Inspector 3. 配置Appium Inspector 三. 功能说明 1. 主菜单功能 2. 快照视图菜单 3. 元素视图菜单 四. 常见问题 1. appPackage有多个设备时 一. 连接手机 1. 授权 首先将手机的开…...
西门子【Library of General Functions (LGF) for SIMATIC S7-1200 / S7-1500】
文章目录 概要整体架构流程技术名词解释技术细节小结 概要 通用函数库 (LGF) 扩展了 TIA Portal 中用于 PLC 编程的 STEP 7 指令(数学函数、时间、计数器 等)。该库可以不受限制地使用,并包含 FIFO 、搜索功能、矩阵计算、 astro 计…...
IntelliJ IDEA 2023.3 中配置 Spring Boot 项目的热加载
IntelliJ IDEA 2023.3 中配置 Spring Boot 项目的热加载 在 IntelliJ IDEA 2023.3 中配置 Spring Boot 项目的热加载,可以让你在不重启应用的情况下看到代码修改的效果。以下是详细的配置步骤: 添加 spring-boot-devtools 依赖 在 pom.xml 文件中添加 …...
潮州东方轻奢风全屋高定找哪家
开篇引言根据《2026年中国全屋定制行业发展报告》,潮州市全屋定制市场规模同比增长38%,其中全屋高端定制细分市场同比增长52%。目前,潮州市家庭全屋定制需求占比72%,高端定制需求占比45%。为了帮助潮州市消费者选择合规、靠谱、差…...
别再死记硬背了!用5个生活化比喻彻底搞懂Linux进程的fork、exec和wait
别再死记硬背了!用5个生活化比喻彻底搞懂Linux进程的fork、exec和wait想象你正在厨房准备一顿大餐。菜谱上写着"切菜"、"炒菜"、"装盘"等步骤,但突然发现需要同时处理多道菜品——这时候,你会本能地让家人分工…...
用图神经网络做缺陷定位,准确率比传统方法高出30%
在现代软件工程的复杂迷宫中,缺陷定位始终是测试团队面临的核心挑战。想象这样一个场景:一个电商系统在特定压力条件下偶发订单丢失,日志中只留下泛泛的超时错误,问题可能深藏在上百个微服务的调用链、分布式事务的竞态条件或某个…...
万星easy-vibe:描述需求即发布 零基础无需学语法
开源Easy-Vibe是一套开源AI编程学习方案,把学习顺序从先学语法再做项目翻转为直接做项目。文章拆解了项目驱动、提示词编写、AI编辑器和多Agent协作的完整流程,解释了为什么想法比语法更重要。 github上datawhalechina/easy-vibe:它在GitHub…...
5A智慧景区建设|对标一流!巨有科技打造数智化标杆景区
5A级景区是中国旅游的最高标准,代表着服务与管理的顶尖水平。随着5A评审标准日益严苛,“智慧化”已成为核心硬性指标。然而,不少景区的智慧化建设陷入“重硬件、轻整合”的误区,系统林立、数据孤岛,投入巨大却效果不佳…...
基于PGA2311的树莓派Hi-Fi模拟音量控制器设计与实现
1. 项目概述:为树莓派DAC打造的高品质模拟音量控制器玩过树莓派音频播放器的朋友都知道,用上像PCM1794A这类高性能DAC芯片后,音质确实能上一个台阶,但有个不大不小的麻烦:这类芯片本身不带音量控制。软件调音量&#x…...
对比不同模型在创意生成任务中的效果与token消耗差异
🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 对比不同模型在创意生成任务中的效果与token消耗差异 在为一场创意大赛准备素材时,我们面临一个常见的选择:…...
用ESP32-C3的PWM做个RGB呼吸灯吧:从配置结构体到色彩渐变(乐鑫ESP-IDF实战)
ESP32-C3 RGB呼吸灯实战:从PWM配置到色彩渐变算法 当智能家居的灯光不再只是简单的开关控制,而是能像呼吸般自然渐变时,整个空间的氛围立刻变得生动起来。ESP32-C3凭借其出色的LED PWM控制器(LEDC)外设,为开…...
3步零基础掌握星露谷物语SMAPI模组加载器:高效管理你的模组世界
3步零基础掌握星露谷物语SMAPI模组加载器:高效管理你的模组世界 【免费下载链接】SMAPI The modding API for Stardew Valley. 项目地址: https://gitcode.com/gh_mirrors/smap/SMAPI SMAPI(Stardew Valley Modding API)是星露谷物语官…...
AI 应用原型开发阶段利用 Taotoken 快速进行多模型效果对比
🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 AI 应用原型开发阶段利用 Taotoken 快速进行多模型效果对比 在构建一个 AI 应用的原型时,开发者常常面临一个核心问题&…...
