小架构step系列08:logback.xml的配置

1 概述

logback.xml配置文件的详细配置,很多地方都说得比较细,本文主要从几个重点来看一下原理,了解原理能够帮助确定哪些应该配置,以及如何配置。

logback.xml是为打印日志服务的,打印的内容一般打印到控制台(Console)和文件(file)里,在生产环境中主要是打印到文件里,然后用扫描工具汇总到某个地方方便查询(如ELK)。打印的内容要符合一定的格式,提供足够的信息,方便进行日志查询和分析;如果所有日志都打印到一个文件里,就有可能文件过大而难以查看,还可能大到一个磁盘装不下,也很难把早期的日志删除掉仅保留一定期限内的日志,所以需要对日志文件进行拆分,每个文件确定在一定大小之内,并控制总体仅保留一定量的日志,避免日志总量过多把磁盘占满了宕机等。日志配置文件也不是一成不变的,当修改了配置文件,希望能够不需要重启Java进程而能够生效,比如修改日志级别。

2 原理

2.1 打印内容的格式

<appender name="console" class="ch.qos.logback.core.ConsoleAppender"><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%thread] %logger{50} - %msg%n</Pattern><charset>UTF-8</charset></encoder>
</appender>

上面就是一个<appender>的配置,里面的<Pattern>就是日志内容的格式,还可以设置高亮颜色。如果要详细看logback支持的格式,那么可以参考官方文档:https://logback.qos.ch/manual/layouts.html

如果想了解原理也容易,上面就指明了对应的类ch.qos.logback.classic.encoder.PatternLayoutEncoder,这也代表着甚至可以通过它来自定义。

 

// 当加载logback.xml文件时,解析到<encoder>节点的时候,会调PatternLayoutEncoder.start()
// 源码位置:ch.qos.logback.classic.encoder.PatternLayoutEncoder
public class PatternLayoutEncoder extends PatternLayoutEncoderBase<ILoggingEvent> {public PatternLayoutEncoder() {}public void start() {// 1. 初始化PatternLayout,里面会初始化一些ConverterPatternLayout patternLayout = new PatternLayout();patternLayout.setContext(this.context);patternLayout.setPattern(this.getPattern());patternLayout.setOutputPatternAsHeader(this.outputPatternAsHeader);patternLayout.start();this.layout = patternLayout;super.start();}
}// 源码位置:ch.qos.logback.classic.PatternLayout
// 2. 静态代码块初始化converter,它们决定了<Pattern>节点里能够配置的变量,用%来指示变量,比如%d表示时间
public static final Map<String, String> DEFAULT_CONVERTER_MAP = new HashMap<String, String>();
static {DEFAULT_CONVERTER_MAP.putAll(Parser.DEFAULT_COMPOSITE_CONVERTER_MAP);DEFAULT_CONVERTER_MAP.put("d", DateConverter.class.getName());DEFAULT_CONVERTER_MAP.put("date", DateConverter.class.getName());DEFAULT_CONVERTER_MAP.put("r", RelativeTimeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("relative", RelativeTimeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("level", LevelConverter.class.getName());DEFAULT_CONVERTER_MAP.put("le", LevelConverter.class.getName());DEFAULT_CONVERTER_MAP.put("p", LevelConverter.class.getName());DEFAULT_CONVERTER_MAP.put("t", ThreadConverter.class.getName());DEFAULT_CONVERTER_MAP.put("thread", ThreadConverter.class.getName());DEFAULT_CONVERTER_MAP.put("lo", LoggerConverter.class.getName());DEFAULT_CONVERTER_MAP.put("logger", LoggerConverter.class.getName());DEFAULT_CONVERTER_MAP.put("c", LoggerConverter.class.getName());DEFAULT_CONVERTER_MAP.put("m", MessageConverter.class.getName());DEFAULT_CONVERTER_MAP.put("msg", MessageConverter.class.getName());DEFAULT_CONVERTER_MAP.put("message", MessageConverter.class.getName());DEFAULT_CONVERTER_MAP.put("C", ClassOfCallerConverter.class.getName());DEFAULT_CONVERTER_MAP.put("class", ClassOfCallerConverter.class.getName());DEFAULT_CONVERTER_MAP.put("M", MethodOfCallerConverter.class.getName());DEFAULT_CONVERTER_MAP.put("method", MethodOfCallerConverter.class.getName());DEFAULT_CONVERTER_MAP.put("L", LineOfCallerConverter.class.getName());DEFAULT_CONVERTER_MAP.put("line", LineOfCallerConverter.class.getName());DEFAULT_CONVERTER_MAP.put("F", FileOfCallerConverter.class.getName());DEFAULT_CONVERTER_MAP.put("file", FileOfCallerConverter.class.getName());DEFAULT_CONVERTER_MAP.put("X", MDCConverter.class.getName());DEFAULT_CONVERTER_MAP.put("mdc", MDCConverter.class.getName());DEFAULT_CONVERTER_MAP.put("ex", ThrowableProxyConverter.class.getName());DEFAULT_CONVERTER_MAP.put("exception", ThrowableProxyConverter.class.getName());DEFAULT_CONVERTER_MAP.put("rEx", RootCauseFirstThrowableProxyConverter.class.getName());DEFAULT_CONVERTER_MAP.put("rootException", RootCauseFirstThrowableProxyConverter.class.getName());DEFAULT_CONVERTER_MAP.put("throwable", ThrowableProxyConverter.class.getName());DEFAULT_CONVERTER_MAP.put("xEx", ExtendedThrowableProxyConverter.class.getName());DEFAULT_CONVERTER_MAP.put("xException", ExtendedThrowableProxyConverter.class.getName());DEFAULT_CONVERTER_MAP.put("xThrowable", ExtendedThrowableProxyConverter.class.getName());DEFAULT_CONVERTER_MAP.put("nopex", NopThrowableInformationConverter.class.getName());DEFAULT_CONVERTER_MAP.put("nopexception", NopThrowableInformationConverter.class.getName());DEFAULT_CONVERTER_MAP.put("cn", ContextNameConverter.class.getName());DEFAULT_CONVERTER_MAP.put("contextName", ContextNameConverter.class.getName());DEFAULT_CONVERTER_MAP.put("caller", CallerDataConverter.class.getName());DEFAULT_CONVERTER_MAP.put("marker", MarkerConverter.class.getName());DEFAULT_CONVERTER_MAP.put("property", PropertyConverter.class.getName());DEFAULT_CONVERTER_MAP.put("n", LineSeparatorConverter.class.getName());DEFAULT_CONVERTER_MAP.put("black", BlackCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("red", RedCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("green", GreenCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("yellow", YellowCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("blue", BlueCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("magenta", MagentaCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("cyan", CyanCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("white", WhiteCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("gray", GrayCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("boldRed", BoldRedCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("boldGreen", BoldGreenCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("boldYellow", BoldYellowCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("boldBlue", BoldBlueCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("boldMagenta", BoldMagentaCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("boldCyan", BoldCyanCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("boldWhite", BoldWhiteCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("highlight", HighlightingCompositeConverter.class.getName());DEFAULT_CONVERTER_MAP.put("lsn", LocalSequenceNumberConverter.class.getName());DEFAULT_CONVERTER_MAP.put("prefix", PrefixCompositeConverter.class.getName());    
}
public Map<String, String> getDefaultConverterMap() {return DEFAULT_CONVERTER_MAP;
}// 回到PatternLayoutEncoder,继续初始化
// 源码位置:ch.qos.logback.classic.encoder.PatternLayoutEncoder
public class PatternLayoutEncoder extends PatternLayoutEncoderBase<ILoggingEvent> {public PatternLayoutEncoder() {}public void start() {// 1. 初始化PatternLayout,里面会初始化一些Converter,细节看1.1PatternLayout patternLayout = new PatternLayout();patternLayout.setContext(this.context);patternLayout.setPattern(this.getPattern());patternLayout.setOutputPatternAsHeader(this.outputPatternAsHeader);// 3. 开始解析pattern,实际调用的是PatternLayout父类PatternLayoutBase的start()方法patternLayout.start();this.layout = patternLayout;super.start();}
}// 继承关系:PatternLayoutEncoder < PatternLayoutBase
// 源码位置:ch.qos.logback.core.pattern.PatternLayoutBase
public void start() {if (pattern == null || pattern.length() == 0) {addError("Empty or null pattern.");return;}try {Parser<E> p = new Parser<E>(pattern);if (getContext() != null) {p.setContext(getContext());}Node t = p.parse();// 4. 解析pattern,getEffectiveConverterMap()里面调了步骤2的getDefaultConverterMap()得到了初始化的converterthis.head = p.compile(t, getEffectiveConverterMap());if (postCompileProcessor != null) {postCompileProcessor.process(context, head);}ConverterUtil.setContextForConverters(getContext(), head);ConverterUtil.startConverters(this.head);super.start();} catch (ScanException sce) {StatusManager sm = getContext().getStatusManager();sm.add(new ErrorStatus("Failed to parse pattern \"" + getPattern() + "\".", this, sce));}
}// 源码位置:ch.qos.logback.core.pattern.parser.Parser
public Converter<E> compile(final Node top, Map converterMap) {Compiler<E> compiler = new Compiler<E>(top, converterMap);compiler.setContext(context);// compiler.setStatusManager(statusManager);// 5. 实际解析patternreturn compiler.compile();
}// 源码位置:ch.qos.logback.core.pattern.parser.Compiler
// 实际解析pattern,整个pattern已经被分成一个个Node,Node的格式如下:
// 以pattern=%d{yyyy-MM-dd HH:mm:ss.SSS}a b c%highlight(%-5level) [%thread] %logger{50} - %msg%n 为例:
// 1) 以%作为开始字符、以})和空格等作为结束字符作为一段进行分割成多段,开始字符和结束字符之间的字符作为一个Keyword Node,段之间如果还有非空字符则作为Literal Node;
//    上例中%d{yyyy-MM-dd HH:mm:ss.SSS}作为Keyword Node,a b c为Literal Node;
// 2) 如果只有一个关键字的则是Simple Keyword Node,如%d{yyyy-MM-dd HH:mm:ss.SSS}
//    如果有多个关键字嵌套的则是Composite Keyword Node,如%highlight(%-5levelx)
// 3) %后面的关键字要能够从converterMap取到,通过它能够从里面找到对应的Converter(参考步骤2),如果找不到就报错了;
// 4) 关键字前面的是format信息,后面的是option信息,
//    比如:%d{yyyy-MM-dd HH:mm:ss.SSS}中,yyyy-MM-dd HH:mm:ss.SSS是option信息,无format信息;
//    比如:%-5level中,5是format信息,表示最短5字符,无option信息;
// 5) 嵌套结构用childNode表示,如%highlight(%-5level)中,%-5level是一个childNode,格式和Node一样;
// 6) 每个Node都有个next,指向下一个节点,即链式结构,通过一层层的next可以遍历整条链;
// 下面最终都是把Converter放到Node里,实际上pattern里的信息都已经分解到Converter里了,打印日志的时候可以直接使用
Converter<E> compile() {head = tail = null;// 当n = n.next为null时,链条结束for (Node n = top; n != null; n = n.next) {switch (n.type) {case Node.LITERAL: // Literal NodeaddToList(new LiteralConverter<E>((String) n.getValue()));break;case Node.COMPOSITE_KEYWORD: // Composite Keyword NodeCompositeNode cn = (CompositeNode) n;// 根据keyword找到Converter,并进行实例化CompositeConverter<E> compositeConverter = createCompositeConverter(cn);if (compositeConverter == null) {addError("Failed to create converter for [%" + cn.getValue() + "] keyword");addToList(new LiteralConverter<E>("%PARSER_ERROR[" + cn.getValue() + "]"));break;}// 把信息设置到Converter里compositeConverter.setFormattingInfo(cn.getFormatInfo());compositeConverter.setOptionList(cn.getOptions());// 再处理childNode的ConverterCompiler<E> childCompiler = new Compiler<E>(cn.getChildNode(), converterMap);childCompiler.setContext(context);Converter<E> childConverter = childCompiler.compile();compositeConverter.setChildConverter(childConverter); // 记录childNode的ConverteraddToList(compositeConverter); // 记录当前node的Converterbreak;case Node.SIMPLE_KEYWORD: // Simple Keyword NodeSimpleKeywordNode kn = (SimpleKeywordNode) n;// 根据keyword找到Converter,并进行实例化,主要是Convert的具体类不一样,所以需要多写一个DynamicConverter<E> dynaConverter = createConverter(kn);if (dynaConverter != null) {// 把信息设置到Converter里dynaConverter.setFormattingInfo(kn.getFormatInfo());dynaConverter.setOptionList(kn.getOptions());addToList(dynaConverter); // 记录当前node的Converter} else {// 没有对应的Converter则报错Converter<E> errConveter = new LiteralConverter<E>("%PARSER_ERROR[" + kn.getValue() + "]");addStatus(new ErrorStatus("[" + kn.getValue() + "] is not a valid conversion word", this));addToList(errConveter); // 记录当前node的Converter}}}return head;
}
CompositeConverter<E> createCompositeConverter(CompositeNode cn) {String keyword = (String) cn.getValue();// 只是从Map里取到Converter的类名,然后实例化String converterClassStr = (String) converterMap.get(keyword);if (converterClassStr != null) {try {// 实例化converter对象return (CompositeConverter) OptionHelper.instantiateByClassName(converterClassStr, CompositeConverter.class, context);} catch (Exception e) {addError("Failed to instantiate converter class [" + converterClassStr + "] as a composite converter for keyword [" + keyword + "]", e);return null;}} else {addError("There is no conversion class registered for composite conversion word [" + keyword + "]");return null;}
}
DynamicConverter<E> createConverter(SimpleKeywordNode kn) {String keyword = (String) kn.getValue();// 只是从Map里取到Converter的类名,然后实例化String converterClassStr = (String) converterMap.get(keyword);if (converterClassStr != null) {try {return (DynamicConverter) OptionHelper.instantiateByClassName(converterClassStr, DynamicConverter.class, context);} catch (Exception e) {addError("Failed to instantiate converter class [" + converterClassStr + "] for keyword [" + keyword + "]", e);return null;}} else {addError("There is no conversion class registered for conversion word [" + keyword + "]");return null;}
}

3 架构一小步

3.1 pattern选择

logback提供了这么多关键字,在实际中也不可能都配上,到了生产环境有些也不太实用,比如上面用的%highlight,选择关键字的标准:
  • 日志的主要目的是为了定位问题,所以应该选择一些对定位问题比较有帮助的。从日志看出问题后,最好是能够定位到代码的位置,也就是哪个类的哪个方法,甚至是哪一行代码。
  • 打印日志不能影响业务的正常运行,比如打印日志不能消耗掉很多性能。
关键字
说明
备注
%d{yyyy-MM-dd HH:mm:ss.SSS}
打印日志的时间,要到毫秒
%-5level
日志级别,主要有TRACE、DEBUG、INFO、WARN、ERROR,最多5个字符,为了对齐则定为最少5个字符
%thread
线程标识,用于表示代码执行在哪个线程里
%logger{50}
logger名称,等同于节点的name属性。一般获取logger的时候使用的是含包名的类名,所以logger的名称实际上指的是包名+类名,可能很长,需要限制长度,比如50字符。也可以使用%class{50}来代替。
带包名的类名一般比较长,所以它和%class一般只用一个。用logger的好处是能兼容类名和一些特殊的logger名。
%method
方法名,打印代码所在的方法
%line
行号,打印代码的行号位置
这个配置对定位问题是比较有用的,但其比较耗费性能,一般不配置,除非性能不是问题。
%msg
日志内容
最好在一个方法内能够唯一指示是哪个日志内容
%n
换行符
示例:
Pattern=%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{50}#%method - %msg%n202x-xx-xx xx:xx:xx.xxx DEBUG [http-nio-8080-exec-2] com.qqian.stepfmk.srvpro.hello.HelloController#say - Access saying hello, message=zhangsan
202x-xx-xx xx:xx:xx.xxx INFO  [http-nio-8080-exec-2] com.qqian.stepfmk.srvpro.hello.HelloController#say - Access saying hello, message=zhangsan
202x-xx-xx xx:xx:xx.xxx WARN  [http-nio-8080-exec-2] com.qqian.stepfmk.srvpro.hello.HelloController#say - Access saying hello, message=zhangsan
202x-xx-xx xx:xx:xx.xxx ERROR [http-nio-8080-exec-2] com.qqian.stepfmk.srvpro.hello.HelloController#say - Access saying hello, message=zhangsan

3.2 日志内容规范

相对于上面常用的信息,日志内容更加重要,但实际打印的时候,很多日志只是随意写了一句话,这句话大多是方法名的拼凑,这种日志只起到一个识别的作用,也就是帮助找到打印日志的位置,但是日志常用信息已经有此功能,所以这种日志除了知道代码有执行到这里之外,没有其它用,这就对定位问题几乎没有帮助。因此,有必要对日志内容定一些规范。
  • 日志内容必要用一句简短的话说明日志出现的结果,这句话最好唯一。
    • 强调结果是为了帮忙定位问题,而不是说明代码做了啥。代码做了啥是可以通过看代码了解到的。
    • 实际日志一般打印不了行号,这句话若能够唯一,就能够快速定位到具体的代码,相当于有了代码行号。
    • 比如判断一个中间变量为不合法l时打印日志,这句话应该说明该变量哪里不合法。通过这个说明就大致能够判断问题的所在,即使不能直接判断问题,也应该尽可能提供最多的信息量。
  • 日志内容要包含上下文,没有上下文的日志不要打印。
    • 上下文一般是指出现日志所指示结果的一些相关变量。
    • 这些变量可能是从前面传过来的,也可能是中间产生的。
    • 打印哪些变量衡量的标准是:当出现日志内容指示的结果时,需要哪些变量才能确定问题。比如一个从数据库读取数据的结果为null时,需要知道组装查询SQL的重点变量。
  • 对于异常信息必须打印堆栈。
    • 使用logger.error("xxxx", e)打印异常时,注意不要给e提供占位符,有占位符只打印了e.getMessage(),没有打印堆栈。
    • 有堆栈才能详细看到是哪段代码发生异常,也就是帮助定位到具体的代码。

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

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

相关文章

STM32中SPI协议详解

前言 在嵌入式系统中&#xff0c;设备间的数据传输协议多种多样&#xff0c;SPI&#xff08;Serial Peripheral Interface&#xff0c;串行外设接口&#xff09;凭借其高速、全双工、易用性等特点&#xff0c;成为连接STM32与高速外设&#xff08;如OLED屏、Flash芯片、AD转换器…

TypeScript 接口全解析:从基础到高级应用

TypeScript 接口全解析&#xff1a;从基础到高级应用在 TypeScript 中&#xff0c;接口是定义数据结构和行为规范的强大工具&#xff0c;它能够显著提升代码的可读性、可维护性和类型安全性。本文将全面讲解 TypeScript 接口的相关知识点&#xff0c;从基础语法到高级特性&…

主存(DRAM)是什么?

主存&#xff08;DRAM&#xff09;是什么&#xff1f; 主存&#xff08;DRAM&#xff09;详解 主存&#xff08;Main Memory&#xff09; 通常由 DRAM&#xff08;Dynamic Random Access Memory&#xff0c;动态随机存取存储器&#xff09; 构成&#xff0c;是计算机系统中用于…

Python 机器学习核心入门与实战进阶 Day 6 - 模型保存与加载(joblib / pickle)

✅ 今日目标 掌握如何将训练好的模型持久化保存到文件熟悉两种主流保存方式&#xff1a;joblib 和 pickle加载模型并应用于新数据预测实现完整的“训练 → 保存 → 加载 → 预测”流程为后续部署做准备&#xff08;如 Flask、FastAPI&#xff09;&#x1f9f0; 一、模型保存工具…

【SigNoz部署安装】Ubuntu环境部署SigNoz:Docker容器化监控的全流程指南

文章目录前言1.关于SigNoz2.本地部署SigNoz3.SigNoz简单使用4. 安装内网穿透5.配置SigNoz公网地址6. 配置固定公网地址前言 在分布式架构主导的现代运维体系中&#xff0c;系统性能监控正面临范式变革的关键转折。当微服务架构遭遇服务雪崩、无服务器架构出现冷启动延迟等复杂…

NV298NV312美光固态闪存NW639NW640

美光固态闪存技术全景解析&#xff1a;从NV298到NW640的深度探索近年来&#xff0c;美光科技凭借其在3D NAND闪存技术上的持续突破&#xff0c;推出了多款备受市场关注的固态硬盘产品。本文将从技术评测、产品对比、市场趋势、用户反馈及应用场景等多个维度&#xff0c;深入剖析…

2025.07.04【服务器】|使用万兆网卡提升服务器间互联速度,实现快速数据传输

文章目录1. **万兆网卡概述**2. **为什么选择万兆网卡**3. **万兆网卡配置与安装**3.1 **安装网卡**3.2 **安装驱动程序**3.3 **检查网卡状态**4. **配置网络接口**4.1 **Linux 系统配置**4.2 **Windows 系统配置**5. **优化性能**5.1 **使用多线程传输**5.2 **开启 TCP/UDP 窗…

光伏发电量精准估算,提升投资效益

在光伏产业规模化发展进程中&#xff0c;准确估算光伏发电量是提升项目投资效益的关键环节。科学的发电量预测不仅能为项目可行性研究提供依据&#xff0c;更能在电站全生命周期内优化运营策略&#xff0c;实现投资回报最大化。基于多维度数据整合与智能算法构建的精准预测体系…

Linux的互斥锁、Linux的POSIX信号量(二值、计数)、RTOS的二值信号量

锁和信号量最大的区别就是:锁严格要求 “谁占用谁释放”,而信号量允许 “一个任务 / 线程释放,另一个任务 / 线程获取”。 特性互斥锁(Mutex)POSIX 信号量(Semaphore)初始状态初始为 “锁定”(PTHREAD_MUTEX_INITIALIZER),需显式获取(pthread_mutex_lock)。初始值可…

基于Java+SpringBoot 协同过滤算法私人诊所管理系统

源码编号&#xff1a;S607源码名称&#xff1a;基于SpringBoot5的协同过滤算法的私人诊所管理系统用户类型&#xff1a;双角色&#xff0c;患者、医生、管理员数据库表数量&#xff1a;15 张表主要技术&#xff1a;Java、Vue、ElementUl 、SpringBoot、Maven运行环境&#xff1…

什么是DINO?

DINO 是一个由 Meta AI (当时的 Facebook AI) 在 2021 年提出的自监督学习框架&#xff0c;其全称是 “self-DIstillation with NO labels”&#xff0c;直译为“无标签的自我蒸馏”。这个名字精准地概括了它的核心思想。 DINO 的出现是一个里程碑&#xff0c;因为它首次有力地…

如何在 Android Framework层面控制高通(Qualcomm)芯片的 CPU 和 GPU。

如何在 Android Framework层面控制高通&#xff08;Qualcomm&#xff09;芯片的 CPU 和 GPU。 参考&#xff1a;https://blog.csdn.net/YoungHong1992/article/details/117047839?utm_source%20%20uc_fansmsg 作为一名 Framework 开发者&#xff0c;您拥有系统级的权限&#…

程序员在线接单

十年Java全栈工程师在线接单Java程序代做&#xff0c;兼职接单&#xff0c;系统代做&#xff0c;二次开发&#xff0c;网站开发部署&#xff0c;项目合作&#xff0c;商业项目承包 全栈开发&#xff0c;支持定制各种管理系统、小程序 商用或个人使用等项目都接 服务二: Java调试…

Python 异步爬虫(aiohttp)高效抓取新闻数据

一、异步爬虫的优势 在传统的同步爬虫中&#xff0c;爬虫在发送请求后会阻塞等待服务器响应&#xff0c;直到收到响应后才会继续执行后续操作。这种模式在面对大量请求时&#xff0c;会导致大量的时间浪费在等待响应上&#xff0c;爬取效率较低。而异步爬虫则等待可以在服务器…

Jenkins Pipeline(二)

1.Pipeline 变量 在 Jenkins 管道&#xff08;Pipeline&#xff09;中&#xff0c;变量是一种非常有用的功能&#xff0c;它们可以帮助你在构建过程中存储和传递数据。Jenkins 管道支持多种方式来定义和使用变量&#xff0c;包括环境变量、脚本变量以及全局变量。 1.2 脚本变…

springsecurity02

提前打开Redis1&#xff09;通过内置的用户名和密码登录spring-boot-starter-security.jar2&#xff09;使用自定义用户名和密码登录UserDetailService自定义类实现UserDetailService接口&#xff0c;重写loadUserByUsername方法class UserDetailServiceImpl implements UserDe…

Apache组件遭大规模攻击:Tomcat与Camel高危RCE漏洞引发数千次利用尝试

漏洞态势分析帕洛阿尔托网络公司Unit 42团队最新研究报告显示&#xff0c;针对Apache Tomcat和Apache Camel关键漏洞的网络攻击正在全球激增。2025年3月披露的这三个远程代码执行&#xff08;RCE, Remote Code Execution&#xff09;漏洞——CVE-2025-24813&#xff08;Tomcat&…

Odoo 中国特色高级工作流审批模块研发

本文旨在为基于Odoo 18平台开发一款符合中国用户习惯的、功能强大的通用工作流审批模块提供一份全面的技术实现与产品设计方案。该模块的核心特性包括&#xff1a;为最终用户设计的图形化流程设计器、对任意Odoo模型的普适性、复杂的审批节点逻辑&#xff08;如会签、条件分支、…

unplugin-vue-components 最佳实践手册

&#x1f3a8; unplugin-vue-components 最佳实践手册 整理不易&#xff0c;收藏、点赞、关注支持下&#xff01;本文详细介绍了 unplugin-vue-components 插件的作用、配置方法、常用场景及与 unplugin-auto-import 配合使用的实战技巧&#xff0c;特别适合 Vue 3 Vite 项目。…

⿻ Java 学习日志 01

Java 运行机制&#xff1a; 原文件>编译器>字节码&#xff08;class后缀&#xff09;>JVM虚拟机>操作系统既有编译的过程也有解释的过程。JVM&#xff1a;Java Virture Machine/执行字节码的虚拟机&#xff0c;是实现跨平台——Java核心机制的核心。 JRE&…