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

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

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

相关文章

【Java题】调整奇数位于偶数之前(超简单版)

题目&#xff1a; 调整数组顺序使得奇数位于偶数之前。调整之后&#xff0c;不关心大小顺序。 如数组&#xff1a;[1,2,3,4,5,6,7,8,9] 调整后可能是&#xff1a;[1, 9,3,7,5, 6, 4, 8, 2] 代码&#xff1a; import java.util.Arrays;public class Main {public static voi…

将python程序打包为exe格式

1. 安装pyinstaller winr打开命令窗口 输入&#xff1a; pip install pyinstaller输入命令后会自动安装pyinstaller 2. 打包 进入你的代码所在位置&#xff0c;输入cmd 在弹出的窗口中输入 pyinstaller --onefile your_script.pyyour_script.py修改为你需要打包的程序名字 …

ElasticSearch架构介绍及原理解析

ElasticSearch架构介绍及原理解析文章目录 一、Elasticsearch是什么&#xff1f;1.简介2.历史与发展3.有关概念1.cluster2.shards3.replicas4.recovery5.river6.gateway7.discovery.zen8.Transport 4.安装 二、ElasticSearch架构介绍及原理解析1.基本架构1.1 进程节点1.2 负载均…

简述操作系统内存管理

这篇可是真枯涩啊&#xff0c;哈哈&#xff0c;老早在学操作系统的时候整理的文章&#xff0c;没加润色&#xff0c;单纯从个人网站迁移过来。 操作系统内存管理的目的是将线性物理地址用抽象的逻辑地址空间&#xff0c;从而保护物理地址。此外&#xff0c;可以独立地址空间&am…

07-Linux部署Nginx

Linux部署Nginx 简介 NGINX是一款高性能的HTTP和反向代理服务器&#xff0c;也是一个IMAP/POP3/SMTP代理服务器。它的特点包括占用内存少、并发能力强&#xff0c;因此在处理高负载和高并发的场景时表现优秀。NGINX由俄罗斯的程序设计师Igor Sysoev开发&#xff0c;最初是为俄…

Windows Docker 部署 SQL Server

部署 SQL Server 打开 Docker Desktop&#xff0c;切换到 Linux 内核。然后在 PowerShell 执行下面命令&#xff0c;即可启动一个 SQL Server 服务&#xff0c;这里安装的是 2022 年版本 docker run -e "ACCEPT_EULAY" -e "MSSQL_SA_PASSWORDSQL123abcABC!&qu…

Spring 事务传播机制

事务传播机制&#xff1a;多个事务⽅法存在调⽤关系时, 事务是如何在这些⽅法间进⾏传播的。 ⽐如&#xff1a;有两个⽅法A&#xff0c;B都被 Transactional 修饰,&#xff0c;A⽅法调⽤B⽅法 A⽅法运⾏时, 会开启⼀个事务。当A调⽤B时&#xff0c; B⽅法本⾝也有事务&#xf…

59.仿简道云公式函数实战-文本函数-RMBCAP

1. RMBCAP函数 RMBCAP 函数可以将金额小写转换为人民币大写金额形式。 2. 函数用法 RMBCAP(数字) 3. 函数示例 如&#xff0c;在财务结算、报销管理、对公付款等场景中&#xff0c;可以利用 RMBCAP 函数将金额转换为大写&#xff0c;避免被篡改产生的负面影响 4. 代码实战…

Socket网络编程(一)——网络通信入门基本概念

目录 网络通信基本概念什么是网络&#xff1f;网络通信的基本架构什么是网络编程?7层网络模型-OSI模型什么是Socket&#xff1f;Socket的作用和组成Socket传输原理Socket与TCP、UDP的关系CS模型(Client-Server Application)报文段牛刀小试&#xff08;TCP消息发送与接收&#…

江科大stm32学习笔记——【5-2】对射式红外传感器计次旋转编码计次

一.对射式红外传感器计次 1.原理 2.硬件连接 3.程序 CountSensor.c: #include "stm32f10x.h" // Device header #include "Delay.h"uint16_t CountSensor_Count;void CountSensor_Init(void) {//配置RCC时钟&#xff1a;RCC_APB2Perip…

前端架构: 脚手架之包管理工具的案例对比及workspaces特性的使用与发布过程

npm的workspaces 特性 1 &#xff09;使用或不使用包管理工具的对比 vue-cli 这个脚手架使用 Lerna 管理&#xff0c;它的项目显得非常清晰在 vue-cli 中包含很多 package 点开进去&#xff0c;每一个包都有package.json它里面有很多项目&#xff0c;再没有 Lerna 之前去维护和…

SpringCloudNacos注册中心服务分级存储模型

文章目录 服务分级存储模型概述配置集群同集群优先的负载均衡 权重配置总结 之前对 Nacos注册中心入门 已经做了演示. 这篇文章对 Nacos 的服务分级存储模型做理论与实践. 服务分级存储模型概述 一个服务可以有多个实例&#xff0c;例如我们的 user-server&#xff0c;可以有:…

C#使用iText7给PDF文档添加书签

上一篇文章将SqlSugar官网文档中每个链接对应的网页生成独立PDF文档再合并为单个PDF文档&#xff0c;但是没有书签&#xff0c;八百多页的内容查找和跳转都不方便&#xff0c;本文学习和使用iText7给PDF文档添加多级书签。   添加多级书签分为两大步骤&#xff1a;1&#xff…

VR全景HDR拍摄教程

什么是HDR&#xff1f; HDR可以用在哪里&#xff1f; 书面解释&#xff1a;HDR&#xff08;高动态范围 High Dynamic Range&#xff09;摄影&#xff0c;是摄影领域广泛使用的一种技术。 是不是有点懵&#xff1f; 我们来看一个实际的拍摄现场环境&#xff0c;你就懂了 我们…

使用 Gradle 版本目录进行依赖管理 - Android

/ 前言 / 在软件开发中&#xff0c;依赖管理是一个至关重要的方面。合理的依赖版本控制有助于确保项目的稳定性、安全性和可维护性。 Gradle版本目录&#xff08;Version Catalogs&#xff09;是 Gradle 构建工具的一个强大功能&#xff0c;它为项目提供了一种集中管理依赖…

定时任务框架

定时任务的框架有哪些 ● Timer&#xff0c;JDK自带的&#xff0c;比较简单&#xff0c;使用的时候&#xff0c;定义一个TimerTask&#xff0c;实现run方法&#xff0c;然后定义一个Timer类&#xff0c;调用timer.schedule(timerTask,1000,3000); ○ 缺点&#xff1a;单线程、…

附加Numpy数组

参考&#xff1a;Append Numpy Array 引言 在数据科学和机器学习领域&#xff0c;处理大规模数据集是一项重要且常见的任务。为了高效地处理数据&#xff0c;numpy是一个非常强大的Python库。本文将详细介绍numpy中的一个重要操作&#xff0c;即如何附加&#xff08;append&a…

LeetCode:2867. 统计树中的合法路径数目(筛质数+ DFS Java)

目录 2867. 统计树中的合法路径数目 题目描述&#xff1a; 实现代码与思路&#xff1a; 筛质数 DFS 原理思路&#xff1a; 2867. 统计树中的合法路径数目 题目描述&#xff1a; 给你一棵 n 个节点的无向树&#xff0c;节点编号为 1 到 n 。给你一个整数 n 和一个长度为 …

西软云XMS operate XXE漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…

Jquery操作DOM对象

文章目录 目录 文章目录 本章目标 一.DOM操作分类 二.JQuery中的DOM操作 内容操作 属性值操作 节点操作 节点属性操作 节点遍历 总结 本章目标 使用Jquery操作网页元素使用JQuery操作文本与属性值内容使用JQuery操作DOM节点使用Jquery遍历DOM节点使用JQuery操作CSS-DOM 一…