SpringBoot 自定义注解实现操作日志记录
文章目录
- 前言
- 正文
- 一、项目结构介绍
- 二、核心类
- 2.1 核心注解
- 2.1.1 CLog 日志注解
- 2.1.2 ProcessorBean 处理器bean
- 2.2 切面类
- 2.3 自定义线程池
- 2.4 工具类
- 2.4.1 管理者工具类
- 2.5 测试
- 2.5.1 订单创建处理器
- 2.5.2 订单管理者
- 2.5.3 订单控制器
- 2.5.4 测试报文
- 2.5.5 测试结果
- 附录
- 1、其他相关文章
前言
关于操作日志记录,在一个项目中是必要的。
本文基于 java8 和 SpringBoot 2.7 来实现此功能。
之前写过一个简单的接口报文日志打印的,和本文的起始思路相同,都是使用切面。但是本文功能更为强大,也更复杂。文章见本文附录《SpringBoot自定义starter之接口日志输出》。
本文代码仓库:https://gitee.com/fengsoshuai/custom-log2.git
正文
本文知识点如下:
自定义注解,SpringBoot使用切面,全局异常处理器,ThreadLocal的使用,MDC传递日志ID,登录拦截器,日志拦截器,自定义线程池,SPEL表达式解析,模版方法设计模式等。
一、项目结构介绍

其中 org.feng.clog 是核心代码区域。org.feng.test 是用于测试功能写的。
二、核心类

在项目启动时,会把AbstractProcessorTemplate 的子类放入Spring容器。同时会执行注册处理器的方法,其定义如下:
package org.feng.clog;import lombok.extern.slf4j.Slf4j;
import org.feng.clog.annotation.ProcessorBean;
import org.feng.clog.utils.SpringBeanUtils;import javax.annotation.PostConstruct;/*** 处理器模板** @author feng*/
@Slf4j
public abstract class AbstractProcessorTemplate<T, R> implements Processor<T, R> {protected void init(ProcessorContext<T> context) {}protected void after(ProcessorContext<T> context, R result) {}public R start(ProcessorContext<T> context) {init(context);// 直接调用handle会导致aop失效// R result = handle(context);AbstractProcessorTemplate<T, R> template = SpringBeanUtils.getByClass(this.getClass());R result = template.handle(context);after(context, result);return result;}@PostConstructprivate void registerProcessor() {if (this.getClass().isAnnotationPresent(ProcessorBean.class)) {ProcessorBean processorBean = this.getClass().getDeclaredAnnotation(ProcessorBean.class);log.info("ProcessorBean Register, action is {}, processor is {}", processorBean.action(), this.getClass().getName());ProcessorFactory.register(processorBean.action(), this);}}
}
2.1 核心注解
2.1.1 CLog 日志注解
package org.feng.clog.annotation;import org.feng.clog.enums.ActionTypeEnum;
import org.feng.clog.enums.ModuleEnum;import java.lang.annotation.*;/*** 日志注解</br>* <pre>* <ul>使用示例:* <li>@CLog(template = "这是简单模版,无参数",actionType = ActionTypeEnum.UPDATE,actionIdEl = "{#userReq.id}",moduleEl = "1")</li>* <li>@CLog(template = "带参数模版,学生名称:{#userReq.name},班级名称:{#userReq.classReq.name}",actionTypeStr = "这是操作",actionIdEl = "{#userReq.id}")</li>* <li>@CLog(template = "带参数计算模版,{#userReq.classReq.number > 20?'大班':'小班'}",actionTypeStr = "这是操作",actionIdEl = "{#userReq.id}")</li>* <li>@CLog(template = "复杂模版,{#userReq.classReq.number > 20?'大班':('这是名称:').concat(#userReq.name).concat(',这是年龄:').concat(#userReq.age)}",actionTypeStr = "这是操作",actionIdEl = "{#userReq.id}")</li>* <li>@CLog(template = "自定义表达式处理,{SfObjectUtil.isEmpty(#userReq.id)?'id为0或者为空':'id不为0或者为空'}",actionTypeStr = "这是操作",actionIdEl = "{#userReq.id}")</li>* <li>@CLog(template = "自定义处理,{logDesc}",actionTypeStr = "这是操作",actionIdEl = "{id}")</li>* </ul>* </pre>** @author feng*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CLog {/*** 日志模版*/String template();/*** 模块*/ModuleEnum module() default ModuleEnum.DEFAULT;/*** 所属模块名*/String moduleStr() default "";/*** 所属模块名</br>* 变量/表达式获取*/String moduleEl() default "";/*** 操作类型*/ActionTypeEnum actionType() default ActionTypeEnum.DEFAULT;/*** 操作类型,优先级高于枚举;不为空时强制读取此值*/String actionTypeStr() default "";/*** 操作类型</br>* 变量/表达式获取*/String actionTypeEl() default "";/*** 业务操作唯一值</br>* 变量/表达式获取*/String actionIdEl() default "";/*** 业务操作唯一值,多值*/String actionIds() default "";/*** 扩展字段*/String ext() default "";
}
2.1.2 ProcessorBean 处理器bean
package org.feng.clog.annotation;import org.feng.clog.enums.ActionTypeEnum;import java.lang.annotation.*;/*** 处理器bean** @author feng*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ProcessorBean {ActionTypeEnum action();
}
2.2 切面类
package org.feng.clog.aspect;import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.feng.clog.LogId;
import org.feng.clog.LogRecordContext;
import org.feng.clog.annotation.CLog;
import org.feng.clog.config.LogCustomerConfig;
import org.feng.clog.enums.ActionTypeEnum;
import org.feng.clog.enums.ModuleEnum;
import org.feng.clog.utils.SpELParserUtils;
import org.feng.clog.utils.StringUtil;
import org.feng.clog.utils.UserUtil;
import org.feng.clog.vo.UserVo;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;/*** 日志切面** @author feng*/
@Aspect
@Component
@Slf4j
public class LogAspect {private static final Pattern BRACES_PATTERN = Pattern.compile("\\{.*?}");@Resource(name = "logThreadPoolTaskExecutor")private Executor executor;@Pointcut("@annotation(org.feng.clog.annotation.CLog)")private void pointCut() {}@AfterReturning(value = "pointCut()")public void after(JoinPoint joinPoint) {try {addLog(joinPoint);} finally {LogRecordContext.clean();}}public void addLog(JoinPoint joinPoint) {String logId = LogId.get();UserVo userVo = UserUtil.get();Map<String, String> logRecordMap = LogRecordContext.get();executor.execute(() -> {try {// 传递logId到异步线程LogId.put(logId);// 获取方法+入参MethodSignature signature = (MethodSignature) joinPoint.getSignature();Object[] args = joinPoint.getArgs();// 获取注解CLog cLog = signature.getMethod().getDeclaredAnnotation(CLog.class);// 获取模版中的参数(如果存在参数),并拼接List<String> templateParameters = getTemplateParameters(cLog.template());buildTemplateData(templateParameters, signature, args, logRecordMap);String template = cLog.template();for (String templateParameter : templateParameters) {template = template.replace(templateParameter, logRecordMap.get(templateParameter));}// 获取moduleString module = getModule(cLog, signature, args, logRecordMap);// 获取actionTypeString actionType = getActionType(cLog, signature, args, logRecordMap);// 获取actionIdList<String> actionIds = getActionId(cLog, signature, args, logRecordMap);// 获取扩展字段JSONObject ext = getExt(cLog, signature, args, logRecordMap);if (StringUtil.isNotBlank(template)) {for (String actionId : actionIds) {log.info("记录日志,user={}, template={}, module={}, actionType={}, actionId={}, ext={}", userVo, template, module, actionType, actionId, ext);// todo 日志落库}} else {log.info("设置日志数据失败:不满足注解条件");}} catch (Exception e) {log.warn("设置日志异常:", e);}});}private List<String> getTemplateParameters(String template) {List<String> parameters = new ArrayList<>();Matcher matcher = BRACES_PATTERN.matcher(template);while (matcher.find()) {parameters.add(matcher.group());}return parameters;}private void buildTemplateData(List<String> parameters, MethodSignature signature, Object[] args, Map<String, String> map) {for (String el : parameters) {// 如果EL表达式为空,则直接下一个if (!StringUtil.isNotBlank(el)) {continue;}String spEl = el;// 兼容自定义数据spEl = getEl(spEl);if (map.containsKey(spEl)) {map.put("{" + spEl + "}", map.get(spEl));continue;}// 自定义类处理spEl = parseCustomerMethodEl(spEl);// El执行if (spEl.contains("#")) {String value = SpELParserUtils.parse(signature.getMethod(), args, spEl, String.class);map.put(el, value);} else {map.put(el, "");}}}private String getModule(CLog cLog, MethodSignature signature, Object[] args, Map<String, String> map) {// 设置了module枚举时,优先获取枚举对应的描述if (!ModuleEnum.DEFAULT.equals(cLog.module())) {return cLog.module().getDesc();}// 设置了moduleStr时if (StringUtil.isNotBlank(cLog.moduleStr())) {return cLog.moduleStr();}// 设置了moduleEl时if (StringUtil.isNotBlank(cLog.moduleEl())) {try {String el = cLog.moduleEl();el = getEl(el);// 处理自定义的elif (map.containsKey(el)) {return map.get(el);}// 处理自定义方法elel = parseCustomerMethodEl(el);// 执行elreturn SpELParserUtils.parse(signature.getMethod(), args, el, String.class);} catch (Exception e) {log.error("日志切面获取module错误", e);}}return null;}private String getActionType(CLog cLog, MethodSignature signature, Object[] args, Map<String, String> map) {// 设置了actionType枚举时,优先获取枚举对应的描述if (!ActionTypeEnum.DEFAULT.equals(cLog.actionType())) {return cLog.actionType().getDesc();}// 设置了actionTypeStr时if (StringUtil.isNotBlank(cLog.actionTypeStr())) {return cLog.actionTypeStr();}// 设置了actionTypeEl时if (StringUtil.isNotBlank(cLog.actionTypeEl())) {String el = cLog.actionTypeEl();el = getEl(el);// 处理自定义的elif (map.containsKey(el)) {return map.get(el);}// 处理自定义方法elel = parseCustomerMethodEl(el);// 执行elreturn SpELParserUtils.parse(signature.getMethod(), args, el, String.class);}return null;}private List<String> getActionId(CLog cLog, MethodSignature signature, Object[] args, Map<String, String> map) {// 设置了actionIdEl时if (StringUtil.isNotBlank(cLog.actionIdEl())) {if (map.containsKey(cLog.actionIdEl())) {return Collections.singletonList(map.get(cLog.actionIdEl()));}String el = cLog.actionIdEl();el = getEl(el);// 处理自定义elif (map.containsKey(el)) {return Collections.singletonList(map.get(el));}// 执行elreturn Collections.singletonList(SpELParserUtils.parse(signature.getMethod(), args, el, String.class));}// 设置了actionIds时if (StringUtil.isNotBlank(cLog.actionIds())) {String el = getEl(cLog.actionIds());if (map.containsKey(el)) {return Arrays.asList(map.get(el).split(","));}}return Collections.singletonList(System.currentTimeMillis() * 10 + new Random().nextInt(10000) + "");}private JSONObject getExt(CLog cLog, MethodSignature signature, Object[] args, Map<String, String> map) {// 如果EL表达式为空,则直接结束if (!StringUtil.isNotBlank(cLog.ext())) {return null;}String spEl = cLog.ext();//兼容自定义数据spEl = getEl(spEl);if (map.containsKey(spEl)) {String value = map.get(spEl);if (StringUtil.isNotBlank(value)) {try {return JSONObject.parseObject(value);} catch (Exception e) {log.info("JSON转换失败:{},{}", value, e.getMessage());return null;}}return null;}// 自定义类处理spEl = parseCustomerMethodEl(spEl);// El执行if (spEl.contains("#")) {String value = SpELParserUtils.parse(signature.getMethod(), args, spEl, String.class);if (StringUtil.isNotBlank(value)) {try {return JSONObject.parseObject(value);} catch (Exception e) {log.info("JSON转换失败:{},{}", value, e.getMessage());return null;}}return null;}return null;}private String parseCustomerMethodEl(String el) {for (String key : LogCustomerConfig.getCustomerMethod().keySet()) {if (el.contains(key)) {String className = key.split("\\.")[0];el = el.replace(className, "T(" + LogCustomerConfig.getCustomerMethod().get(key) + ")");}}return el;}private String getEl(String str) {str = str.replaceAll("\\{", "");str = str.replaceAll("}", "");return str;}}
2.3 自定义线程池
package org.feng.clog.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;/*** 线程池配置** @author feng*/
@Configuration
@EnableAsync
public class ThreadPoolConfig {@Bean(name = "logThreadPoolTaskExecutor")public Executor initLogCpuExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() + 1);executor.setMaxPoolSize(150);executor.setQueueCapacity(50);executor.setKeepAliveSeconds(60);executor.setThreadNamePrefix("log-thread-pool-");executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());executor.initialize();executor.setTaskDecorator(runnable -> runnable);return executor;}
}
2.4 工具类
2.4.1 管理者工具类
package org.feng.clog.utils;import org.feng.clog.AbstractProcessorTemplate;
import org.feng.clog.ProcessorContext;
import org.feng.clog.ProcessorFactory;/*** 管理工具** @author feng*/
public class ManagerUtil {public static <R, T> R handle(ProcessorContext<T> context) {AbstractProcessorTemplate<T, R> processor = ProcessorFactory.getProcessor(context.getAction());if (processor == null) {throw new RuntimeException("未找到 " + context.getAction() + "对应的处理器");}return processor.start(context);}
}
2.5 测试
2.5.1 订单创建处理器
package org.feng.test;import lombok.extern.slf4j.Slf4j;
import org.feng.clog.AbstractProcessorTemplate;
import org.feng.clog.LogRecordContext;
import org.feng.clog.ProcessorContext;
import org.feng.clog.annotation.CLog;
import org.feng.clog.annotation.ProcessorBean;
import org.feng.clog.enums.ActionTypeEnum;
import org.feng.clog.enums.ModuleEnum;
import org.feng.clog.utils.StringUtil;
import org.springframework.stereotype.Service;/*** 创建订单处理器** @author feng*/
@Slf4j
@Service
@ProcessorBean(action = ActionTypeEnum.ORDER_CREATE)
public class OrderCreateProcessor extends AbstractProcessorTemplate<OrderCreateReq, Boolean> {@Overrideprotected void init(ProcessorContext<OrderCreateReq> context) {preHandleReq(context.getData());}@Override@CLog(template = "测试日志记录,{testK1}", module = ModuleEnum.ORDER, actionType = ActionTypeEnum.ORDER_CREATE,actionIdEl = "{#context.data.orderNum}", ext = "{JacksonUtil.toJSONString(#context.data)}")public Boolean handle(ProcessorContext<OrderCreateReq> context) {LogRecordContext.put("testK1", "3wewd2");OrderCreateReq orderCreateReq = context.getData();log.info("处理--创建订单{}", orderCreateReq.getOrderNum());return true;}@Overrideprotected void after(ProcessorContext<OrderCreateReq> context, Boolean result) {// todo 后置操作}private void preHandleReq(OrderCreateReq req) {// todo 参数校验// 例如校验参数if (StringUtil.isBlank(req.getOrderNum())) {throw new IllegalArgumentException("订单号不能为空");}}
}
2.5.2 订单管理者
package org.feng.test;import org.feng.clog.ProcessorContext;
import org.feng.clog.enums.ActionTypeEnum;
import org.feng.clog.utils.ManagerUtil;
import org.springframework.stereotype.Component;/*** 订单管理** @author feng*/
@Component
public class OrderManager {/*** 创建订单*/public Boolean createOrder(OrderCreateReq req) {ProcessorContext<OrderCreateReq> processorContext = new ProcessorContext<>();processorContext.setAction(ActionTypeEnum.ORDER_CREATE);processorContext.setData(req);return ManagerUtil.handle(processorContext);}
}
2.5.3 订单控制器
package org.feng.test;import org.feng.clog.utils.ResultUtil;
import org.feng.clog.vo.ResultVo;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;/*** 控制器** @author feng*/
@RestController
@RequestMapping("order")
public class OrderController {@Resourceprivate OrderManager orderManager;// @WithoutLogin@PostMapping("/test1")public ResultVo<String> test1(@RequestBody OrderCreateReq req) {// 创建Boolean started = orderManager.createOrder(req);return ResultUtil.success("success " + started);}
}
2.5.4 测试报文
{"orderNum": "1001","type": 1,"senderName": "","likes": ["1", "2", "3"]
}
2.5.5 测试结果
控制台日志输出:
2024-02-28 11:48:40.102 INFO 92309 --- [log-thread-pool-1] org.feng.clog.aspect.LogAspect.lambda$addLog$0(LogAspect.java:95) : [logId=d3b0dc267ce64dfa8a987e8eb6aad4ba] 记录日志,user=UserVo(id=1001, username=feng123, phone=18143431243, email=null), template=测试日志记录,3wewd2, module=订单, actionType=订单创建, actionId=1001, ext={"senderName":"","orderNum":"1001","type":1,"likes":["1","2","3"]}
可以看到,日志中记录了logId,以及日志注解对应的信息。
附录
1、其他相关文章
- SpringBoot自定义starter之接口日志输出
- SpringBoot使用线程池之ThreadPoolTaskExecutor和ThreadPoolExecutor
相关文章:
SpringBoot 自定义注解实现操作日志记录
文章目录 前言正文一、项目结构介绍二、核心类2.1 核心注解2.1.1 CLog 日志注解2.1.2 ProcessorBean 处理器bean 2.2 切面类2.3 自定义线程池2.4 工具类2.4.1 管理者工具类 2.5 测试2.5.1 订单创建处理器2.5.2 订单管理者2.5.3 订单控制器2.5.4 测试报文2.5.5 测试结果 附录1、…...
ubuntu常见配置
ubuntu各个版本的安装过程大差小不差,可以参考,ubuntu20.04 其它版本换一下镜像版本即可 安装之后需要配置基本的环境,我的话大概就以下内容,后续可能有所删改 sudo apt-get update sudo apt-get install gcc sudo apt-get inst…...
electron+vue3全家桶+vite项目搭建【27】封装窗口工具类【1】雏形
文章目录 引入思路抽出公共声明文件抽出全局通用数据类型和方法主进程模块1.抽离基础常量2.封装窗口工具类 渲染进程模块测试结果 引入 demo项目地址 可以看到我们之前在主进程中的逻辑全部都塞到index.ts文件中,包括窗口的一些事件处理,handle监听&am…...
从模型到复合AI系统的转变
2023年,大型语言模型(LLM)吸引了所有人的注意力,它可以通过提示来执行通用任务,例如翻译或编码。这自然导致人们将模型作为AI应用开发的主要成分而密切关注,所有人都在想新的LLM将带来什么能力。然而,随着越来越多的开发者开始使用LLM构建,我们认为这种关注正在迅速改变:最先进…...
将仓库A中的部分提交迁移到仓库B中
结论: 使用git format-patchgit am即可实现 使用场景: 例如仓库A这里有5个提交记录,commitid1, commitid2, commitid3, commitid4,commitid5 仓库B想用仓库A中提交的代码,手动改比较慢,当改动较多的时候…...
信息安全技术基础
本博客地址:https://security.blog.csdn.net/article/details/136331705 一、信息安全基础 1、信息安全的基本要素有机密性、完整性、可用性、可控性与可审查性。信息安全的范围包括设备安全、数据安全、内容安全和行为安全。其中数据安全即采取措施确保数据免受未…...
flask知识--01
flask介绍 # python 界的web框架: Django:大而全,使用率较高 :https://github.com/django/django -FastAPI:新项目选择使用它:https://github.com/tiangolo/fastapi -flask:公司一些…...
软考52-上午题-【数据库】-关系模式2
一、关系模式的回顾 见:软考38-上午题-【数据库】-关系模式 二、关系模式 2-1、关系模式的定义 示例: 念法:A——>B A决定B,或者,B依赖于A。 2-2、函数依赖 1、非平凡的函数依赖 如果X——>Y,&a…...
devc++跑酷小游戏3.5.0
本来想搞存档的,失败了,要再学学文件操作的函数。还有一个打印地图的函数,更失败,彻底放弃。最近开学了,游戏不会经常更新,要写作业。昨天写到10点T_T #include<bits/stdc.h> #include<windows.h…...
Redisson限流算法
引入依赖 <dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.12.3</version> </dependency>建议版本使用3.15.5以上 使用 这边写了一个demo示例,定…...
GPT与MBR:硬盘分区表格式的革新与区别
概述 在计算机存储领域,硬盘分区是管理数据和操作系统部署的基础。两种广泛使用的分区表格式——MBR(Master Boot Record)和GPT(GUID Partition Table),各自代表了不同的技术阶段和发展需求。本文将详细介…...
机器学习-1
文章目录 前言机器学习基本定义 练习题 前言 在本片开始将为大家介绍机器学习相关的知识点。 机器学习基本定义 夏天,我们通常会去水果店里买西瓜,我们看到一个根蒂蜷缩、敲起来声音浑浊的青绿色的西瓜,我们提着西瓜就去结账了,…...
Stream流详解
当我们对一个集合中的元素进行多次过滤应该怎样做? 下面看一个案例 按照下面的要求完成集合的创建和遍历 创建一个集合,存储多个字符串元素 把集合中所有以"张"开头的元素存储到一个新的集合 把"张"开头的集合中的长度为3的元素存储到一个新…...
javaweb学习(day05-TomCat)
一、介绍 1 官方文档 地址: https://tomcat.apache.org/tomcat-8.0-doc/ 2 WEB 开发介绍 2.1 WEB 在英语中 web 表示网/网络资源(页面,图片,css,js)意思,它用于表示 WEB 服务器(主机)供浏览器访问的资源 2.2 Web 资源 WEB 服务器 ( 主机 ) 上供外界访问的 …...
【Unity】构建简单实用的年份选择器(简单原理示范)
在许多应用程序和游戏中,年份选择是一个常见的需求。无论是在日历应用程序中查看事件,还是在历史类游戏中选择时间段,年份选择器都是用户体验的重要组成部分,下面实现一个简易的年份选择器。 一、效果预览: 目录 一、…...
LeetCode 2120.执行所有后缀指令
现有一个 n x n 大小的网格,左上角单元格坐标 (0, 0) ,右下角单元格坐标 (n - 1, n - 1) 。给你整数 n 和一个整数数组 startPos ,其中 startPos [startrow, startcol] 表示机器人最开始在坐标为 (startrow, startcol) 的单元格上。 另给你…...
租赁小程序|租赁系统|租赁软件开发带来高效运营
随着社会的不断发展和科技的不断进步,越来越多的企业开始关注设备租赁业务。设备租赁作为一种短期使用设备的方式,为企业提供了灵活和成本节约的优势。针对设备租赁业务的管理和提升企业竞争力的需求,很多企业选择定制开发设备租赁系统。本文…...
大数据集群管理软件 CDH、Ambari、DataSophon 对比
文章目录 引言工具介绍CDHAmbariDataSophon 对比分析 引言 大数据集群管理方式分为手工方式和工具方式,手工方式一般指的是手动维护平台各个组件,工具方式是靠大数据集群管理软件对集群进行管理维护。本文针对于常见的方法和工具进行比较,帮助…...
插值、逼近、拟合、光顺
插值 插值(Interpolation)是数学和计算科学中的一个重要概念,它指的是通过已知的一系列数据点,构造一个函数或曲线,并据此估计未知数据点的值。这个过程通常发生在已知数据点之间,用于预测或估算在这些已知…...
Java单元测试 - mock静态方法
文章目录 1. mock 静态方法2. 升级 maven 依赖3. 示例 1. mock 静态方法 mockito 在 3.4.0 版本之后,开始支持 mock static method。 2. 升级 maven 依赖 <dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artif…...
收藏!SaaS小白必看:AI大模型落地实战路线图,从功能堆砌到价值创造
本文分析了SaaS公司在整合AI大模型时应避免“功能堆砌”陷阱,并介绍了三大AI技术路线:Prompt/RAG/微调的特点及适用场景。文章强调SaaSAI产品的成功关键在于技术路线与客户价值的适配,提出了分阶段组合策略,即初创期以提示词为主&…...
告别图片重复困扰:AntiDupl.NET 图片去重工具完整使用指南
告别图片重复困扰:AntiDupl.NET 图片去重工具完整使用指南 【免费下载链接】AntiDupl A program to search similar and defect pictures on the disk 项目地址: https://gitcode.com/gh_mirrors/an/AntiDupl 你是否曾为电脑中大量重复图片占用宝贵存储空间而…...
如何5分钟完成DOL游戏汉化美化:终极整合包使用指南
如何5分钟完成DOL游戏汉化美化:终极整合包使用指南 【免费下载链接】DOL-CHS-MODS Degrees of Lewdity 整合 项目地址: https://gitcode.com/gh_mirrors/do/DOL-CHS-MODS 想要体验完整汉化的Degrees of Lewdity游戏,但面对复杂的mod安装感到困惑&…...
2026年数字IC设计紫光展锐笔试带答案解析
文章目录 一、试卷结构 二、单选题(共20题,每题2分,共40分) 三、多选题(共15题,每题2分,共30分。多选、少选、错选均不得分) 四、简答题(共3题,每题10分,共30分) 一、试卷结构 项目 说明 笔试岗位 数字IC设计工程师 / 芯片验证工程师 题型分布 单选题20题(每题2分…...
ODrive 0.5.6源码编译实战:从环境配置到烧录调试(STM32F4平台)
ODrive 0.5.6源码编译实战:从环境配置到烧录调试(STM32F4平台) 在嵌入式开发领域,ODrive因其出色的FOC(磁场定向控制)算法实现和开源特性,已成为高性能电机控制的热门选择。本文将手把手带你完成…...
还在用Excel手动贴标签?试试用C#调用CodeSoft 6的ActiveX组件,5分钟搞定批量打印
告别Excel手工标签:用C#与CodeSoft 6打造智能打印系统 仓库管理员小李每天上班第一件事,就是打开Excel表格核对上百个资产编号,然后逐个复制粘贴到标签模板中打印。上周因为手误把两个贵重设备的标签贴反,导致盘点时花了整整两天时…...
从零到一:基于STM32与PWM的超声波雾化片驱动全解析
1. 超声波雾化片驱动原理揭秘 第一次接触超声波雾化片时,我盯着那个直径不到3cm的金属圆片看了半天——这玩意儿怎么就能把水变成雾气呢?后来拆解了几个报废的加湿器才明白,原来核心秘密在于压电效应。当给雾化片施加特定频率的交流电时&…...
告别驱动烦恼:一劳永逸的QT5.14+MySQL8数据库连接配置方案(Windows平台)
告别驱动烦恼:一劳永逸的QT5.14MySQL8数据库连接配置方案(Windows平台) 每次新建QT项目都要重新编译MySQL驱动?在不同电脑上部署环境总遇到动态库缺失?这套工程化解决方案将彻底改变你的开发体验。我们将从实战角度出发…...
不只是降噪:聊聊声加ENC算法在TWS耳机通话中的AEC与ANC联动
不只是降噪:声加ENC算法在TWS耳机中的系统级协同设计 当你在嘈杂的地铁里用TWS耳机通话时,是否想过这背后隐藏着一场精密的算法交响乐?ANC(主动降噪)、AEC(回声消除)和ENC(环境噪声消…...
大麦网抢票终极指南:Python自动化脚本助你告别手速烦恼
大麦网抢票终极指南:Python自动化脚本助你告别手速烦恼 【免费下载链接】DamaiHelper 大麦网演唱会演出抢票脚本。 项目地址: https://gitcode.com/gh_mirrors/dama/DamaiHelper 还在为抢不到热门演唱会门票而焦虑吗?面对秒光的热门演出、昂贵的黄…...
