小架構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
英文地址,請注明出處:http://en.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&…