代碼分析與自動化重構

PS:根據過去編寫 Modernizing 相關的開源工具里,編寫的《代碼分析與自動化重構》指南。

遺留系統的現代化演進是一門藝術。在日常的軟件開發里,我們經常會遇到一系列的問題:

  • 如何解決人類智商不夠的問題?模式、原則和工具
  • 誰應該去解決代碼的問題?代碼
  • ……

應對于這些問題,其中的一個解決方案就是:自動化的工具,有些人喜歡稱之為。支撐這些工具的便是一系列的原則模式,將它們融入到工具之中。另外一個解決人成長的方案就是:元元(meta-meta),這是另外一個故事。應對于日常編碼而言,它便是代碼的分析,以及后續的自動化重構。

代碼分析與自動化重構流程:

Refactor Patterns

簡介

Why 開源 + 遺留系統現代化工具

遺留系統是常態。在大多數公司里,我們所遇到的系統里多數是是遺留系統,來到一個新項目時,可能就需要對他們快速的分析,以提供洞見 —— 寫 PPT 匯報。所以,在過去的幾年里,我們也沉淀了一系列的遺留系統分析和重構的工具,比如新哥的 Tequila、正在開源的架構分析和守護工具 ArchGuard 等等。除此,在有些重構項目里,還要編寫定制的工具來進行分析,諸如于先前我的同事覃宇和俊斌等所寫的「移動應用遺留系統重構」 系列。

技術熱情發電。對多數人而言,我們面臨的一個重要挑戰則是:拿自己的業余時間來完善工具。

既然要用自己的時間來開發,還和項目沒有關系,這種用愛發電的事情,用開源的方式最合適了。

我們需要怎樣的工具?

從對于使用工具的結果來看,我們需要這個現代化工具是:

  • 可視化驅動。快速生成項目的分析結果,并展示出來給開發人員了解現狀,還有編寫 PPT。
  • 必要的交互性。用于在重構的過程中,尋找合適的切入點。
  • 定制化開發
    • 特定壞味道。不同的開發團隊會有不同的壞味道,有些壞味道是無法由 SonarQube 這樣的工具識別的。
    • 自動化重構。基于已知的壞味道,對應的代碼位置信息,對代碼進行自動化重構。
  • 適當的語法精準度。更高的語法精準度,意味著更高的開發成本,需要有針對地平衡它們。
  • 多平臺。我們用的是 macOS,而多數時候,客戶使用的是 Windows。

如何開發這樣的工具?

這里定義的遺留系統現代化工具包含了這么幾部分:語法分析、結果及可視化、自動化重構、架構守護。

語法分析

對代碼進行語法分析,生成特定的語言的數據結構。常用的工具有:Antlr、Ctags、Tree-sitter、Doxygen、CodeQuery 等。一個大致的對比(拍腦袋訂的)如下表所示:

工具精確度量化開發難度跨語言學習成本添加新語言成本可自動化重構
語言編譯器完美-Yes
Antlr極高Yes
CtagsYes(成本高)
Tree-sitterYes(成本高,S)
DoxygenNo
CodeQuery極高Yes(成本高)

結果及可視化

通常來說,我們會出于以下的一些情況,來對遺留系統進行可視化:

  • 數值化。如針對于特定的 smell 進行自動化重構,類似于 SonarQube,常見的模式和原則源自于《重構》一書。在 Coca 里,還引入了在一些論文里看到了測試的 bad smell,諸如于沒有斷言的測試等。
  • 可視化依賴。如針對于代碼中的類、包等的依賴情況進行可視化,主要用于分析分層架構等。常用的工具有:PlantUML、Graphviz、D3.js、Echarts 等。
  • 代碼屬性可視化。如針對于文件的修改頻率、大小等屬性進行可視化,可以獲取諸如于單位時間內的文件變化頻率。一個文件經常修改,還大量被引用,那說明它是一個不穩定的類、文件,除了業務變化,最有可能就是設計不合理。
  • 其它
自動化重構

這一步是可選的,它取決于我們的場景。通常來說,編寫這樣的功能主要彌補是現代化的 IDE 無法完成的工作,諸如于:

  • 多代碼庫間的未使用類刪除。
  • 多代碼庫間的聚類。
  • 針對于 CSS 顏色的重構。
架構守護

編寫架構的守護規則,以對于系統的架構進行守護,用的工具有:ArchUnit、ArchGuard 等。在參考了 ArchUnit 的語法之后,我們也設計了一個多語言的架構守護工具:Guarding。

遺留系統現代化工具集

在?Modernizing?里,我們集合了先前開發的一系列工具。并創建了:awesome-modernization?用于對其它的一系列相關的工具進行收集。

在 Modernizing 里,針對于單個編程語言的工具有:

  • 針對于 Java 語言的系統重構、系統遷移和系統分析的工具:Coca,Go 語言,GitHub stars:691。Coca 是一個“全功能”的重構工具,基于 Antlr 進行語法分析的,除了常規的可視化、調用分析,還可以進行自動化重構。Coca 一名的由來是:對標新哥寫的?Tequila?—— 龍舌蘭酒 vs 快樂水。
  • 針對于 CSS/LESS/CSS 的分析和自動化重構工具:Lemonj,TypeScript 語言,GitHub stars:128。當時設計的主要目的是:用來對 CSS 中的顏色進行提取,基于 Antlr 的語法樹分析,可以用于進行自動化的重構。
  • 針對于 MySQL 代碼進行自動化分析,并從中構建中 UML,并生成其關系的:SQLing,Go 語言,使用 PingCap 的 SQL 解析器解析。當然了,還有一個初始化的針對于 PL/SQL 的版本:pling。
  • 適用于 Ant 轉 Maven 的半自動化工具:Merry,Go 語言 + Antlr。
  • 前端規范化改造工具:Clij,用于一鍵添加 eslint、husky、lint-staged 等,TypeScript 語言。

針對于多語言的工具,我們有:

  • 基于 Antlr 的多語言的語言模型分析工具:Chapi,Kotlin 語言。其設計的初衷是用于生成 Coca 相同的數據結構,以接入更多的可視化工具。在語法分析上,采用的是 Antlr 進行分析。
  • 基于 Doxygen 的多語言分析和可視化工具:Go mod 版本的新哥的 Tequila。其中,還有一系列的迷之代碼,需要重構掉。
  • 基于 Ctags 的多語言模型分析和可視化工具:Modeling,Rust 語言。分析源碼,并生成基于模型的可視化依賴。
  • 基于 Tree-sitter 的多語言架構守護工具:Guarding,Rust 語言。通過自制的 DSL,來對系統架構進行守護。

除此,還有一個在 Inherd 開源小組下開源的:Coco,它主要是通過代碼的物理屬性:修改頻率 + 目錄 + 行數來分析系統的工具。以及現在緊鑼密鼓開源中的 ArchGuard。

我們使用一系列不同的語言和工具來開發這些軟件,因為不同的場景之下,都會有不同的選擇。

自動化重構:代碼分析

代碼分析是我們編寫自動化重構、架構守護等一系列工具的第一步。而代碼分析的方式有多種不同的形態,最常見的是基于源碼以及基于編譯后的字節碼(常見于 Java 語言)的靜態程序分析。

通常來說,根據我們的目標獲取的信息是不同的,如:類/結構體、成員、函數(含參數、返回值、注解)、引用(import)、表達式等。因此,所選的工具也是不同的:

目標語法信息級別可選 工具
HTTP API@注解、參數、類、方法語法分析器(語言自帶、三方、Antlr)
領域模型類/結構體、成員等根據不同精度,可以考慮 Ctags、Tree-sitter等
包、類依賴關系引用、函數調用等。Doxygen、 語法分析器等
調用鏈全部信息語法分析器(語言自帶、三方、Antlr)

根據我們的不同需求,我們還需要記錄語法的位置信息。比如,同樣是 HTTP API 的情況下,我們想獲取:

  • API URI 列表。只需要解析注解即可。
  • API 的輸入和輸出參數。注解 + 解析函數簽名。
  • API 輸入到數據庫。注解 + 解析函數簽名 + 調用鏈。

因此,是不是使用語言自帶的語法分析器,生成一個完整的模型就行了,如 Java 使用?Javaparser。事情并不是這么簡單,如今是微服務時代,每個服務都可能使用不同語言,一個二三十人的研發團隊,可能使用 7~8 種語言 —— 為每個服務挑選合適的語言,老系統 C#、新系統 Java、大數據 Scala、AI 用 Python 等。除此,為某個語言寫一個成本也是頗高的,并且用處可能還不大。

所以 ,在不斷平衡之間,我們有了一系列的工具選型。

編譯器前端

編譯器粗略分為詞法分析,語法分析,類型檢查,中間代碼生成,代碼優化,目標代碼生成,目標代碼優化。

基于語法分析器(parser)

從實現的層面來看,使用官方的 parser 是最準確的 —— 前提是它提供了便利的接口,像 Java 語言好像就沒有這樣的接口。

  • 官方支持。如?Coca?早期在解析 Golang 時,使用的是 Go 的?parser?包。
  • 三方。在?SQLing?中,我們使用的 TiDB 的?parser,它宣稱與 MySQL 完全兼容,并盡可能兼容 MySQL 的語法。

使用這一類 parser 比較麻煩的是在于跨語言的支持,每實現一個新的語言,就需要實現一套,不能復用。

自制 parser

為了實現更好的跨平臺,以及更好玩,選用一個合適的解析器生成器就更“科學” 了。在這一方面,除了傳統的 Flex 和 Bison,Antlr 也是一個不錯的選擇 —— 多語言支持:JavaScript、Golang、Java、Rust 等。

Antlr 社區維護了一個語法庫:https://github.com/antlr/grammars-v4/,內置了幾十種編程語言的 Antlr 語法文件。雖然,部份語法可能不太準確,需要我們手動進行修改,但是依舊可以大大減少我們的編寫成本 —— 除了學習 Antlr 是個成本。Antlr 之類工具的迷人之處在于:你可以重溫一下《編譯原理》,又或者是《計算機程序的構造和解釋(SICP)》,畢竟它是編譯器的前端部分。你再掌握一下 LLVM 的 API,就可以開發個語言了。它的挑戰之處在于,你需要知道語言的各類語法細節,所以也是一個不錯的學習新語言語法的機會。

不過,諸如 Java、C++ 等支持在編譯時進行代碼生成的語言,也會遇到一系列的挫折。諸如于:

  • 引用推斷。最難受的?junit.*需要做一些推斷
  • 生成工具推斷。如 lombok 等

所以,我們需要通過編譯過程中的中間表示,來做一些額外的處理。

基于中間表示(IR)

IR-Intermediate Representation(中間表示)是程序編譯過程中,源代碼與目標代碼之間翻譯的中介。

為了提升語法分析的精準度,就需要應對編譯其的代碼生成,于是,就需要分析 IR。如:Java 里的 ASM。能對?.class?文件進行分析。只是,IR 處理了一些信息,所以如 class 文件里有些內容(如 annotation)好像并不會被記錄行號信息,詳見:LineNumberTable Attribute。

Java Flow

Java、Android 在編譯過程中對于 Annotation 的操作,又或者是在編譯后的騷優化,也是 666。

不過,它能完成大部分我們所需要的工作。

編輯器語法樹

編輯器在做語法高亮的時候,也在做類似的事情。正好,我先前在某 spike 過編輯器 / IDE 的架構和實現。

  • Atom/VSCode。主要由 JSON/PList 格式的 TMLanguage(源自 TextMate) + 正則表達式實現,即?VSCode TextMate?和?Oniguruma?共同構成了 VSCode 的一部分語法高亮功能。吐槽一句,非常難以維護。
  • Eclipse。需要手寫解析器,FAQ How do I write an editor for my own language?。
  • Intellij IDEA。可以通過 BNF 來添加相應的功能:Custom Language Support Tutorial?。
  • Vim。由自帶的 Vim 腳本 + 正則表達式(類似)來實現,示例:Rust.vim
  • Emacs。由 Emacs Lisp 語言 + 正則表達式(類似)來實現,示例:rust-mode

只是呢,上述的工具,在離開了編輯器之后,這個 API 嘛,就有些難用了。于是,有一些獨立的工具出現了。

基于語言服務器(LSP)

雖然,我還沒有嘗試過使用 LSP 來實現語法分析,但是我嘗試構建過一個語言及其 LSP。因此,從理論來說,LSP 也能達成此目的。并且與 Antlr 類似,Microsoft 也維護了一個 LSP 的目錄:Language Servers。

麻煩的是,不同語言的 LSP 可能由不同的語言來實現,在系統的集成上會比較困難。其所需要的語言運行環境比較多,比如 Java 的就需要一個 JDK/SDK,在編寫分析工具時,自動化測試環境搭建起來也比較麻煩。

Ctags:有限的解析

Ctags 可以快速實現對類、成員的解析,所以它經常被用在 Vim 的語法高亮上。只是呢,使用 Ctags 難以實現支持:某個函數調用了哪些函數、哪些函數被某個函數調用。從流程上,先用 ctags 生成 tags 文件,然后解析這個 tags 文件即可。如下是一個 tags 文件(部分):

MethodInfo	src/coco_struct.rs	/^pub struct MethodInfo {$/;\"	struct	line:21	language:Rust
name	src/coco_struct.rs	/^    pub name: String,$/;\"	field	line:22	language:Rust	struct:MethodInf

然后,再寫幾個正則表達式 match 一下:

        Regex::new(r"(?x)/\^([\s]*)
([A-Za-z0-9_.]+)
(,(\s|\t)*([A-Za-z0-9_.]+))*(\s|\t)*
(?P<datatype>[A-Za-z0-9_.<>\[\]]+)").unwrap();

因此,在不考慮正則表達式難寫和代碼精準度的情況下,使用 Ctags 還會存在一些小問題:

  1. 版本沖突,如 macOS 環境自帶了一個 ctags,需要 override,或者自定義路徑。
  2. 下載 ctags。特別是如果客戶是在內網環境時,又會比較麻煩。

所以,Tree-sitter 成了一個更好的選擇:平衡。

Tree-sitter

Tree-sitter 是一個解析器生成工具和增量解析庫。 它可以為源文件構建具體的語法樹,并在編輯源文件時有效地更新語法樹。這個工具最初是為 Atom 編輯器設計的。Tree-sitter 內置了一個?S 表達式,可以快速構建出我們想要的模型。如下是一個 C# 代碼:

using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;[ApiController]
public class SharpingClassVisitor { }

對應的 S 表達式如下:

(using_directive(qualified_name) @import-name)(class_declaration(attribute_list (attribute name: (identifier) @annotation.name))?name: (identifier) @class-name
)

我們在 Guarding 中使用了 Tree-sitter 來實現,示例:[Guarding Ident](https://github.com/modernizing/guarding/tree/master/guarding_ident/src/identify),與 Ctags 相比,沒有這個環境依賴,會比較清爽。

其在線 Playground:https://tree-sitter.github.io/tree-sitter/playground?。

其它生成工具

除了上述的幾類,還有一些可選的工具。

文檔生成器:Doxygen

Doxygen 是一個適用于 C++、C、Java、Objective-C、Python、IDL、Fortran、VHDL、PHP、C# 和 D 語言的文檔生成器。為了生成代碼的文檔,它需要能支持對于代碼進行語法分析。所以,它也內置了有限的語法分析功能。

在?Tequila?中,是通過分析 Doxygen 生成的文檔結果,從而構建出內部的依賴關系。如下是一個 Doxygen 生成的 Graphviz 文件:

digraph "Domain::AggregateRootB"
{// LATEX_PDF_SIZEedge [fontname="Helvetica",fontsize="10",labelfontname="Helvetica",labelfontsize="10"];node [fontname="Helvetica",fontsize="10",shape=record];Node1 [label="Domain::AggregateRootB",height=0.2,width=0.4,color="black", fillcolor="grey75", style="filled", fontcolor="black",tooltip=" "];Node2 -> Node1 [dir="back",color="midnightblue",fontsize="10",style="solid",fontname="Helvetica"];Node2 [label="Domain::AggregateRoot",height=0.2,width=0.4,color="black", fillcolor="white", style="filled",URL="$class_domain_1_1_aggregate_root.html",tooltip=" "];Node3 -> Node2 [dir="back",color="midnightblue",fontsize="10",style="solid",fontname="Helvetica"];Node3 [label="Domain::Entity",height=0.2,width=0.4,color="black", fillcolor="white", style="filled",URL="$class_domain_1_1_entity.html",tooltip=" "];
}

解析這個?dot?文件,從而生成項目的類與類之間的依賴信息。

索引工具:CodeQuery

CodeQuery?是由 GitHub 推出的索引和查詢工具,它主要結合了 Ctags 和 Cscope,cscope 可以實現部分語言的表達式(expression)的支持。它試圖結合 cscope 和 ctags 提供的功能,提供比 cscope 更快的數據庫訪問(因為它使用 sqlite)。雖然,我還沒有試過,但是應該也是可以玩一玩的。架構如下所示:

CodeQuery workflow

它結合了 starscope、pyscope、cscope 等多個工具,來實現對于代碼的解析。

自動化重構:為代碼再建個代碼模型

為代碼建模并不是一件很難的事情,畢竟每個編譯器都在重復做同樣的事情。

從代碼到模型

現在,回憶一下你大學學的編譯原理 —— 雖然有些人可能和我一樣沒上過對應的課。

class GFG {public static void main(String[] args){System.out.println("Hello World!");}
}

解析成 AST 后,可以用如下的形式來表示(可能沒有對照 JVM 里的實現):

CLASS_DEF -> CLASS_DEF [1:0]
|--MODIFIERS -> MODIFIERS [1:0]
|   `--LITERAL_PUBLIC -> public [1:0]
|--LITERAL_CLASS -> class [1:7]
|--IDENT -> GFG [1:13]
`--OBJBLOCK -> OBJBLOCK [1:17]|--LCURLY -> { [1:17]|--METHOD_DEF -> METHOD_DEF [2:4]|   |--MODIFIERS -> MODIFIERS [2:4]|   |   |--LITERAL_PUBLIC -> public [2:4]|   |   `--LITERAL_STATIC -> static [2:11]|   |--TYPE -> TYPE [2:18]|   |   `--LITERAL_VOID -> void [2:18]|   |--IDENT -> main [2:23]|   |--LPAREN -> ( [2:27]|   |--PARAMETERS -> PARAMETERS [2:34]|   |   `--PARAMETER_DEF -> PARAMETER_DEF [2:34]|   |       |--MODIFIERS -> MODIFIERS [2:34]|   |       |--TYPE -> TYPE [2:34]|   |       |   `--ARRAY_DECLARATOR -> [ [2:34]|   |       |       |--IDENT -> String [2:28]|   |       |       `--RBRACK -> ] [2:35]|   |       `--IDENT -> args [2:37]|   |--RPAREN -> ) [2:41]|   `--SLIST -> { [2:43]|       |--EXPR -> EXPR [3:26]|       |   `--METHOD_CALL -> ( [3:26]|       |       |--DOT -> . [3:18]|       |       |   |--DOT -> . [3:14]|       |       |   |   |--IDENT -> System [3:8]|       |       |   |   `--IDENT -> out [3:15]|       |       |   `--IDENT -> println [3:19]|       |       |--ELIST -> ELIST [3:27]|       |       |   `--EXPR -> EXPR [3:27]|       |       |       `--STRING_LITERAL -> "Hello World!" [3:27]|       |       `--RPAREN -> ) [3:41]|       |--SEMI -> ; [3:42]|       `--RCURLY -> } [4:4]`--RCURLY -> } [5:0]

對于代碼分析來說,我們就是:

  1. 構建?AST 模型。通過代碼分析工具,得到一個類似上述內容的結果,不同的工具得到的詳盡程度不同。
  2. 基于標準的 AST 構建分析模型。如我們只取類、函數的信息,就需要解析?CLASS_DEF?里的?IDENT?,以及其 children 中的?METHOD_DEF?里的?IDENT,遍歷-取值,就這么簡單。

所以,要構建出一個完善的 AST 及其模型,基本上就是寫一個語言的編譯器前端。在現代的編程語言里,Rust 能提供一個非常不錯的參考,如 Rust 的編譯過程是 AST → HIR → MIR → LIR,其官方在引入 MIR 的時候寫了一篇博客《Introducing MIR》

Rust Flow

在 Rust 編譯器里, HIR 相當于是 Rust 的 AST,它在源碼的基礎上進行解析、宏擴展和名稱解析之后生成。如下是 Rust 的 hello, world! 生成的 HIR 表示:

#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
fn main() {{::std::io::_print(::core::fmt::Arguments::new_v1(&["hello, world!\n"],&[]));};}

基于不同階段的編構建模型,得到的模型結果是不完全一樣的。如果我們想分析編程語言調用系統的庫,或者是三方的庫,那么從這里得到的才是更精確的 —— 對比于 Java 的 Bytecode。Java 編程過程中對于 Annotation 的處理,也在側面反應兩者的差異之處。所以,想獲取準備的代碼模型,可以在這的基礎上,進一步探索下編程語言的構建。

如此一來,你也就是一個真正的代碼專家了。

0.1 初始化版本:面向 Java

在和新哥設計第一個 MVP 版本的時候,只是想比 Doxygen/Tequila 更準確地記錄 Java 代碼的調用鏈。所以,設計的模型也相當的簡單:

type JClassNode struct {Package     stringClass       stringType        stringPath        stringFields      []JAppFieldMethods     []JMethodMethodCalls []JMethodCall
}

如上的歷史代碼所示,在面向 Java 語言設計,只記錄一個類(Class)的名稱、包名、類型、路徑、成員變量(包含了依賴的類型)、函數/方法、函數調用關系。因為最初只是為依賴設計,所以調用關系只保存在 ClassNode 里。基于 Antlr 這樣的解析器生成器之后,其對應的解析代碼(Listener 模式)也頗為簡單(java_full_listener.go):

func (s *JavaCallListener) EnterClassDeclaration(ctx *ClassDeclarationContext) {currentType = "Class"currentClz = ctx.IDENTIFIER().GetText()if ctx.EXTENDS() != nil {currentClzExtends = ctx.TypeType().GetText()}
}

從 ClassDef/ClassDecl 中獲取 ident 就是類名,如果有 extends 關系的話,再獲取 extends 關系。這是一個初始化的版本,所以沒有考慮到非常復雜的場景,比如多重繼承、泛型等等。

但是,它也讓我重新理解了一下,為什么有的語言的語法設計得有點詭異 —— 解析器不好寫。

1.0 版本:更多的工具,更多的模型

在發布了 Coca 之后,從 GitHub 幾百的 stars 和對應的遷移指南?Migration?2.8k 的 stars 來看,這個領域的需求還是相當的旺盛。

所以,我們開發了更多的功能,也一步步陷入了「人類創造的三個系統」的陷阱中。

適用于重構的模型

而后,為了生成實現不適用在 IDE 用的重構功能(多代碼庫引用檢測、類移動等),我們又構建了一個新的模型,因為我們就只需要這么多信息:

type JFullMethod struct {Name              stringStartLine         intStartLinePosition intStopLine          intStopLinePosition  int
}type JField struct {Name   stringSource stringStartLine         intStopLine          int
}type JPkgInfo struct {Name   stringStartLine         intStopLine          int
}

這個時候要實現的功能,還是比較簡單的,所以并不是那么復雜

適用于測試代碼壞味道的模型

除了重構之后,在 Coca 中,還有一個非常有意思的特性:測試代碼壞味道。測試代碼壞味道,是指單元測試代碼中的不良編程實踐(例如,測試用例的組織方式,實現方式以及彼此之間的交互方式),它們表明測試源代碼中潛在的設計問題。簡單來說,就是看測試是否有斷言?ignore 的測試數量等等。需求不復雜,所以構建的模型也比較簡單:

type BSDataStruct struct {core_domain.CodeDataStructFunctions    []BSFunctionDataStructBS ClassBadSmellInfo
}type BSFunction struct {core_domain.CodeFunctionFunctionBody stringFunctionBS   FunctionBSInfo
}

當然了,細節都是魔鬼,比如?FunctionBSInfo?長什么樣的?

2.0 AST 集合:一個臃腫而緩慢的系統

隨后,我們試圖構建一個更理想的系統,于是就有了「第二個系統」,一個經過精心設計的系統。

兼容更多的語言

隨著 Coca/Chapi 的演進,陸陸續續想支持 Golang、Java、Python 等語言。于是,一個平凡的 ClassNode 已經變成了 DataStruct:

@Serializable
open class CodeDataStruct(var NodeName: String = "",var Type: DataStructType = DataStructType.EMPTY,var Package: String = "",var FilePath: String = "",var Fields: Array<CodeField> = arrayOf(),var MultipleExtend: Array<String> = arrayOf(),var Implements: Array<String> = arrayOf(),var Extend: String = "",var Functions: Array<CodeFunction> = arrayOf(),var InnerStructures: Array<CodeDataStruct> = arrayOf(),var Annotations: Array<CodeAnnotation> = arrayOf(),var FunctionCalls: Array<CodeCall> = arrayOf(),@Deprecated(message = "looking for constructor method for SCALA")var Parameters: Array<CodeProperty> = arrayOf(), // for Scalavar Imports: Array<CodeImport> = arrayOf<CodeImport>(),    // todo: select node useonly importsvar Extension: JsonElement = JsonObject(HashMap())
) {  ...
}

一味地進行了兼容設計,導致它變得異常復雜。而和多數系統一樣,這種兼容設計并非是最理想的,沒有進一步做一些抽象,比如函數的屬性,參數、返回類型等,是否能構建?Type Signature?雖然這是一個技術項目,但是也掉入了同樣的業務模型的常見問題中。

最后,我嘗試將非 Java 語言分離成插件,但是因為 Golang 當時的版本并不支持插件化架構。所以,從形態拆分為了 Java + 其它語言 CLI,并轉向了 Rust 語言。

更多的模型

既然,原來的模型可能太重了,那么是不是會有新模型。所以,陸陸續續又構建了一系列的模型。如,在設計 Guarding/Modeling 的時候,我們也構建了一個簡化的版本:

#[repr(C)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CodeClass {pub name: String,pub package: String,pub extends: Vec<String>,pub implements: Vec<String>,pub constant: Vec<ClassConstant>,pub functions: Vec<CodeFunction>,pub start: CodePoint,pub end: CodePoint
}#[repr(C)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CodeFunction {pub name: String,// todo: thinking in modifierpub vars: Vec<String>,pub start: CodePoint,pub end: CodePoint
}

在這個版本里,使用的是 Tree-sitter 沒有函數調用,所以顯得非常簡單 —— 只是記錄基本的類、函數信息等,又是一個非常簡單的初始化版本。

3.0 進行中的向下抽象:MIR <=> AST

既然,從 AST 做合集太復雜,那么是不是往下鉆,尋找更 common 的元素,就能獲得更通用的結果,畢竟最后運行在機器上的是一樣的。

所以,這就成為了 2022 年的一個潛在的業余樂趣,如果你有興趣,歡迎到 GitHub 上討論:https://github.com/modernizing/kernel

潛在的路:MIR

在這一方面,Rust 編譯器的 MIR 就是一個不錯的參考,它基于控制流圖、也沒有嵌套表達式,并且所有類型都是完全顯式的 —— 更多的細節可以查看官方的文檔:Rust MIR。除此,你可以在?Rust Playground?里,查看 Rust 在 HIR、MIR、LLVM IR 不同階段的形式,當然直接詩源碼是最簡單的。如下是一段 Rust 的代碼(本來應該用 Hello, world!,但是它更復雜)。

fn main() {let mut vec = Vec::new();vec.push(1);vec.push(2);
}

生成的 CFG 示例

    ...bb0: {_1 = Vec::<i32>::new() -> bb1;}bb1: {_3 = &mut _1; _2 = Vec::<i32>::push(move _3, const 1_i32) -> [return: bb2, unwind: bb5];}bb2: {_5 = &mut _1;                    _4 = Vec::<i32>::push(move _5, const 2_i32) -> [return: bb3, unwind: bb5];}...

在這個階段,MIR 比 AST 添加了更完整的細節 —— 我們能知道?push?方法是從哪里來的,不需要自己做一些推斷。

與之相類似的,還有一個名為?MIR Project?的項目更有意思,它嘗試建立多語言的抽象。只是從形式上來看,它接近于 LIR:

hello_m:  moduleimport printf
hello:    func i64local i64:r # local variable has to be i64
# prototype of printf
p_printf: proto i32, p:fmt
format:   string "hello, world\n"call p_printf, printf, r, formatret rendfuncendmodule

不過,在代碼模型上,還是接近于 MIR 的:

/* Function definition */
typedef struct MIR_func {const char *name;MIR_item_t func_item;size_t original_vars_num;DLIST (MIR_insn_t) insns, original_insns;uint32_t nres, nargs, last_temp_num, n_inlines;MIR_type_t *res_types;char vararg_p;           /* flag of variable number of arguments */char expr_p;             /* flag of that the func can be used as a linker expression */VARR (MIR_var_t) * vars; /* args and locals but temps */void *machine_code;      /* address of generated machine code or NULL */void *call_addr;         /* address to call the function, it can be the same as machine_code */void *internal;          /* internal data structure */
} * MIR_func_t;

它讓我重新思考起,我如何去組件 Struct/Class 和 Function 的關系?從 AST 的層面來說,這個不好解決,但是從 MIR/LIR 的話,這個問題就變得異常簡單了 —— 在底層沒有繼承。

所以,我們應該如何去設計這樣一個模型呢?

還有 CLR 和 Graal IR ?

在先前設計 Chapi 的期間,鵬飛推薦了一本書《CLR via C#》,在設計 Chapi 的時候,參考了一部分。簡單來說,就是 Microsoft .NET Framework 里構建了一個公共語言運行時(Common Language Runtime,CLR)。其核心功能(如內存管理、程序集加載、安全性、異常處理和線程同步)可面向 CLR 的所有語言使用。我并不關心 CLR 怎么實現,我關心的是其中的 “通用類型系統”(Common Type System)。

另外一個有意思的項目就是 Graal VM,它是一個生態系統和共享運行時,不僅提供基于 JVM 的語言(如Java,Scala,Groovy和Kotlin)的性能優勢,還提供其他編程語言(如JavaScript,Ruby,Python和R)的性能優勢。其中的 Graal IR 便是 Graal 的核心構建塊之一:GraalVM Compiler,這個可以作為下一個階段的研究的樂趣。

這部分足夠讓我們重新思考一下:公共語言模型是怎樣的?

其它

我并非編譯器方面的專家,更細節的內容可以自己去讀代碼或者比編譯原理相關的書籍。除了傳統的龍書、虎書、鯨書,在編譯器前端上,Antlr 作者編寫的《編程語言實現模型》和后續的《ANTLR 4權威指南》能更快速地幫你入門語法解析。

其它常見問題:

  • 沒有類型怎么辦?諸如于 JavaScript 這一類動態語言,就需要自己嘗試性地做一些類型推斷。
  • 在底層 MIR 真的能做融合嗎?不確定,但是可以試試,畢竟有上述的 MIR 大佬說可以。

自動化重構:代碼可視化代碼

有了模型,有了分析代碼,便可以將代碼序列化為一個個的數據。接著,剩下的就是對數據進行操作的過程了。

根據不同的可視化需求,我使用過一系列的可視化形式,如下表:

工具編碼成本可定制性代碼可維護性UI 美觀性額外工具主要使用場景
Graphviz低(形式的DSL)是(打包到工具中,需要考慮跨平臺)快速構建 PoC,諸如于依賴可視化
PlantUML低(標準化的格式)面向模型建模 + IDE的可視化支持
Web 之 D3.js取決于設計不需要,可以打包到工具中可交互性要求高,如依賴、調用分析
Web 之 Echats/AntV不需要,可以打包到工具中可交互性要求高,如依賴、調用分析
Web 3D 之 Three.js取決于設計不需要,可以打包到工具中3D 交互、 VR 世界

總的來說,我們依舊是在各種的平衡,每個工具都有自身的特點和優勢。在代碼模型不一致的時候,我們需要一層 adapter,用于從代碼模型轉換到可視化所需要的模型,進而將代碼可視化。在這個時候,就能體現出會寫前端代碼的好處。從模式上來說,主要是分為兩類:

  1. 利用已有工具的靜態可視化。比較常見的有,諸如于使用 Dot 語言描述的 Graphviz,使用 UML 描述的 PlantUML。
  2. 開發新工具的交互性可視化。常見的有 Web 技術開發的工具,如 D3.js 等。

相似的,和代碼分析一樣,也需要一個成本的考慮。從無到有,優先考慮已有的工具;從 1 到 100,便是考慮自己做個可視化工具。

靜態的代碼可視化

這里主要以我使用過的 Graphviz、PlantUML 作為示例。

神器 Graphviz:依賴可視化

Graphviz 是自 1991 年開發的,歷史悠久,比較從使用頻率來看,它應該是用得最多的一類工具。參見?Graphviz 的 wiki,諸如于 Doxygen、Rust、Sphinx 等大量的工具都會用它來生成文檔中的圖形,而像 OmniGraffle 這一類工具,則使用它來生成自動化布局。從場景上來看,主要就是利用它便利的 Dot 語言描述,結合圖形算法,來自動生成依賴關系。

Graphviz 中的 Dot 語言非常便利,只需要使用??這樣的語法,就可以生成調用關系。如下是 Coca 中生成調用鏈的 dot 文件示例:

digraph G { "POST /books" -> "com.phodal.pholedge.book.BookController.createBook";"com.phodal.pholedge.book.BookController.createBook" -> "com.phodal.pholedge.book.BookService.createBook";...
}

對應轉換后的圖形如下所示(因為是測試代碼中有多個相同的 Controller,所以是雙份箭頭):

對于代碼量較大的工程來說,生成的 SVG 就會比較大,以致于可能會在瀏覽器上渲染許久。為此,常見的一種解決方案就是:添加大量的 filter 函數、參數,以有選擇性的過濾。這也造成了另外一個問題,工具的學習成本和試命令的成本比較高。有一個很好的例子就是,雖然我是 Coca 的作者,但是很多功能,我現在已經不記得了。

PlantUML:模型可視化

和 Graphviz 相比,UML 更為人所知,是個建模的好工具。PlantUML?是一個開源工具,能讓你通過純文本的方式來生成 UML 圖(Unified Model Language 統一建模語言)。在 Modernizing 的幾個工具里,主要是用它來對模型進行可視化,諸如于:

  • Modeling,結合 Ctags 對代碼庫中的模型(如 repository)進行分析,結合 id 等,構建出簡單的依賴關系。
  • SQLing,結合 MySQL parser 對數據庫的 Schema 進行分析,結合外鍵關系,構建出表的依賴關系,進而幫助我們推導出模型的關系。

以 SQLing 為例,如下是一個網上找的 SQL 代碼:


CREATE TABLE human(...
)
CREATE TABLE car(id VARCHAR(12) PRIMARY KEY,mark VARCHAR(24),price NUMERIC(6,2),hid VARCHAR(12),CONSTRAINT fk_human FOREIGN KEY(hid) REFERENCES human(id)
)

通過 SQLing,可以轉換為如下的結果(UML):

@startuml
class Human {...
}
class Car {- Id: String- Mark: String- Price: BigDecimal- Hid: String
}
Car --> Human
@enduml

這樣一來,就可以配合 IDEA 的 PlantUML 插件進行可視化了:

Modeling 的依賴構建會比 SQLing 復雜一些,在構建模型的時候,還要從?xxId?中嘗試分析出是否存在這樣的類,以構建出對應的依賴關系 —— 當然,這種是基于編碼模式的分析,有些人的代碼寫的是?id?沒有前綴,這就分析不出來了。

交互的代碼可視化

在基于微服務、代碼庫小的場景下,上述的 Graphviz、PlantUML 基本上可以完成大部分的工作。而對于遺留系統來說,它巨大的代碼量,就意味著我們需要更強的交互工具。所以,我找了個周末寫了個工具:Merry。

從 Graphviz 到 D3.js:OSGi 的天坑

我嘗試構建的第一個場景是一個 OSGi 系統的 Ant 轉移到 Maven 方案上,我們的目標是告訴客戶:你還不如重寫。不過,你需要有強壯的證據,還有可估算的成本證明。采用 OSGi 框架,就意味著系統可能有幾十、幾百個 bundle,可以理解為模塊,而這些模塊又可以相互依賴,妥妥的一個大泥球。與此同時,采用 Ant 又意味著系統的依賴是放在某個目錄里管理的,具體的版本什么的,也不定會在文件名中體現。所以,我們所要做的就是:

  1. 解析?build.xml,從中獲取?classpath?中的 jar 路徑。
  2. 解析 jar 包中的 Manifest.MF、pom.properties,從中解析出包名、版本號、Export、Import 等一系列的信息。
  3. 自動生成一個 pom.xml 文件。(PS:需要對一些依賴進行人工校驗,所以是半自動的。可以通過配置 map 文件,在后續變成全自動化。)

其中,最過于坑人的,要數 Manifest.MF 存在多個不同的版本的問題。在使用正則無力的情況下,最后只能用 Antlr 來寫解析器了。有意思的是,OSGi 生成的 Manifest.MF 里,必須有?Import-Package?和?Export-Package,便可以從中生成項目的依賴信息。就這么找了 Apache 的 OSGi 項目,run 了一下,寫了個 demo,it works:

然后,來到客戶現場,一試,嘿,傻眼了,客戶有幾百個 bundle。怎么看清包之間的關系,怎么看清哪個 bundle 被依賴最多?所以,讓 D3 來干活吧。

依賴圖

在有了依賴關系之后,只需要生成一個 JSON 文件,就可以給 D3.js 使用了。剩下要做的就是打包 Web 應用,以便于在客戶的 Windows 電腦上運行 —— 這就體現出了 Golang 的跨平臺優勢。在采用有了 GitHub Action 的多平臺構建之后,Rust 也可以實現同樣的效果。接著,迅速實現了個 demo,然后拿 Eclipse 的 OSGi 框架 Equinox 跑了一下,這圖估計也 hold 不住,幾百個 bundle:

于是,又從 D3.js 的 Gallery 里繼續拿個圖了測試一下:

效果比上面好一點,但是依舊不理想。然后,我就一如即往的棄坑了 —— 在 OSGi 技術越來越難見到的時代,投精力開發工具,顯得非常不值得。和 D3.js 的簡單 demo 相比,我們在 ArchGuard 設計的、基于 AntV G6 的可視化來說,它顯得更加的好用。

Merry 可視化的最后 demo 見:Merry Dependencies Analyser

可交互的變化

上面的可交互性僅限于當前時期,但是歷史上的變化有時候往往更重要。于是,在設計效能分析工具?Coco?時,我們做提分析 Git 的提交歷史,從中發現歷史上的高頻變更。如下是?Nginx?的示例,可以播放,然后查看變化:

對于本身就是增量變更的 Git 來說,分析 Git 的日志,就能得到上面的結果。但是,對于代碼來說,要分析模型上的增量變更,還是稍微有一點麻煩。如果有哪個小伙伴有空,可以去構建這樣的功能。

面向風口的可視化

幾年前,在閱讀《Your Code as a Crime Scene》一書之后,我便一直想構建一個 Code City,只是我一直看不到有效的使用場景。在設計 Coco 和 Coca 的時候,雖然圖形是 2D 的,表現力是有限的,但是多數時候是夠用的 —— 受客戶開發機的性能影響。所以,去年在元世界又開始火了之后,結合了幾年前在 Thoughtworks 國內構建的第一個 VR 機器人,并寫了 Code City 的 demo:https://github.com/modernizing/codecity。

當然,這還只是一個玩具。只要一打開 Oculus Quest 2,我就沉迷在 Beat Saber 中But the way,我構建了我一直想構建的 Code City demo。

開發工具就是這樣的,在業余的時候,需要先搭建個架子,等到使用的時候,就可以改吧改吧上線了。而不是用的時候,發現沒有架子,然后就不做了。

讓代碼修改代碼

程序員嘛,重復的事情都應該盡可能自動化。所以,在我們呈現完問題,就要一一去解決問題。

以機器的角度來考慮,對于重構來說,就是發現 bad smell 的模式,尋找解決方案,編程以自動化重構。諸如于 Intellij IDEA 這類的 IDE,以及各類 Lint 工具,便也是類似于此。不過,在已經有了大量的現有工具的情況下,我們編寫的工具能做點什么?

  • 規模化修改。比起一個個在 IDE 中敲入?Alt?+?Enter?來得更有效率 —— 對于大型的工程來說。
  • IDE 難以完成的工作。跨多個工程的代碼重構,一來是性能問題,二來是不支持。
  • 其它不常見的 bad smell 模式

好的習慣不容易學習,但是不好的、便利的習慣,往往非常容易上手。先來看一個簡單的 CSS 重構案例。

前端:自動化的顏色重構

在諸多的前端項目中,在早期如果沒有構建好項目模板,又或者是后期沒有按規范捃,那么項目中的顏色中就會分散在各個 CSS 和各類 CSS 預處理器。這個時候,當我們來一個主題類的需求,比如過年的大紅色。那么,就需要一個個的 debug。因此,一個比較簡單的方式,就是識別代碼中的 CSS 中的顏色,提取出來,統一管理。于是,在 2020 年的時候,我和劉宇構建了一個簡單的 CSS 重構工具:Lemonj。

思路上也頗為簡單:

  1. 識別代碼中的各類顏色。記錄每一個顏色的文件信息,位置信息等。
  2. 生成顏色的 mapping 文件。
  3. 修改生成的 mapping 文件。通過記錄的信息,將顏色值,修改成對應的變量
  4. 執行重構。將顏色變量修改到文件中。

從技術實現上,就是使用 Antlr 構建一個跨 CSS 預處理器的顏色解析,主要是針對于 LESS。其中,比較麻煩的一個點在于 CSS 里的顏色,除了?color?屬性,在?box-shadowborder?等一系列的屬性中都會出現:

    switch (propertyKey) {case 'color':case 'background-color':case 'border-color':case 'background':...case 'border':case 'border-right':case 'border-left':case 'border-bottom':case 'border-top':case 'border-right-color':case 'border-left-color':case 'border-bottom-color':case 'border-top-color':case 'box-shadow':case '-webkit-box-shadow':case '-moz-box-shadow':...}

主要分析代碼見:RefactorAnalysisListener.ts。隨后,生成一個 Mapping 文件:

// _fixtures/less/color/border.less
@color1: #ddd;
// _fixtures/less/color/border.less
@color2: green;
// _fixtures/less/color/rgba.less
@color3: rgba(255, 0, 0, 0.3);
// _fixtures/less/color/sample.less

其中的注釋信息主要是用于人為的 debug。當然,它還不是全自動化的,后續還需要一系列小的代碼修改。但是,大體上已經大大減少了工作量了。隨后,我們在這基礎上構建了一個簡單的 CSS 的 bad smell 識別,用于證明 Antlr 語法的可用性。如下是一個 bad smell 示例:

Code Smell:  {colors: 24,importants: 4,issues: 8,mediaQueries: 1,absolute: 0,oddWidth: 1
}

這個項目還有一系列的 Todo 要做,有興趣的小伙伴可以基于此來構建自己的 CSS 重構工具,又或者是接手、完善?Lemonj。

模式上依舊是:識別 bad smell 模式,尋找解析方案,編寫自動化重構代碼。

后端:批量性 Java 代碼重構

回到先前說到的 Coca 支持的 Java 代碼重構上。同樣的,也是識別代碼味道的模式,然后重構。場景上是:客戶有一個?common?的?common?包,簡單來說,就是上百人的團隊,最后維護出一個非常大的?common?包,JVM 啟動慢 blabla。有些團隊離開了這個包,有些團隊還在使用,所以需要分析哪些不被使用了。于是,基于 Coca 的分析功能,我們開始構建的第一個例子里,刪除未使用的?import?—— 客戶寫的代碼太爛了。歷史有點悠久,當時似乎好像是在 IDEA 中,只要?import?的,但是未使用的,也會被視為依賴?。另外一個原因是,代碼量較大,一個個過濾成本高。

在有了 AST 的基礎上,分析代碼就非常簡單了:

func BuildErrorLines(node models2.JFullIdentifier) []int {var fields = node.GetFields()var imports = node.GetImports()var errorLines []intfor index := range imports {imp := imports[index]ss := strings.Split(imp.Name, ".")lastField := ss[len(ss)-1]var isOk = falsefor _, field := range fields {if field.Name == lastField || lastField == "*" {isOk = true}}if !isOk {errorLines = append(errorLines, imp.StartLine)}}return errorLines
}

從上述代碼,其實有一個雷那就是?lastField == "*"?此坑嘛,沒有填上。然后,就是重構 —— 隨機刪除代碼了:

func (j *RemoveUnusedImportApp) Refactoring(resultNodes []models2.JFullIdentifier) {for _, node := range resultNodes {if node.Name != "" {errorLines := BuildErrorLines(node)removeImportByLines(currentFile, errorLines)}}
}

只要編譯通過了,就說明我們的重構是好的。第一次寫 Goland 寫了 Coca,所以代碼寫得比較一般了,不過測試覆蓋率有 90%,也算是方便大家對這個代碼庫重構了。

后續,我用這個項目來向客戶證明,嘿,我們的代碼都是有測試的,你不需要 100%,只需要 90% 即可(手動狗頭)。

Coca 還有比較簡單的批量移動 + 重命名功能。速度比 IDEA 高效 + 快速,至少放在當時,客戶的機器 + 他們的代碼量,IDEA 就未響應了。通過如下的配置形式,以支持重命名 + 移動:

move.a.ImportForB -> move.b.ImportForB
move.c.ImportForB -> move.d.ImportForB

簡單易懂,還相當的靠譜(我覺得),下班回去后一兩個小時就能寫完 —— billable 時間寫不了。

當然,在 IDEA 支撐得住,代碼量小的情況下,還是告訴客戶你們手動移動吧,然后自己回去想想怎么自動化。

讓重構消失:構建前置的架構守護

重構,從理論上來說,是一種事后補救的方式。我們應該盡量避免 bad smell 的出現,從 CI 上的 SonarQube,到 Git Hooks 的 pre check,再到 IDE 里的 Checkstyle,我們無一不是在構建架構適應度函數,以讓系統的架構逐步演進到合適的狀態。

在我們有了代碼模型,又有了語法分析能力之后,我們就能構建出一個跨越任何語言的架構守護工具,類似于?ArchUnit。好的架構模式、設計模式,只有變成代碼,可測試、可度量,它才有發揮的空間。通過前面的一系列 Antlr 的語法分析基礎,很容易就能具備編寫一套新的 DSL,再配上老馬的《領域特定語言》作為指導思想,《ANTLR 4權威指南》作為實踐手冊,我們就是一個“代碼專家”。

于是呢,我按照這個想法,開了一個坑:Guarding?一個用于 Java、JavaScript、Rust、Golang 等語言的架構守護工具。結合 Tree-sitter 進行目標代碼的模型構建,借助于易于理解的 DSL,來編寫守護規則。在設計上參考了 ArchUnit 的語法,采用了 Rust 里的 pest 作為解析器 —— 主要是一年前 Rust 的 Antlr 支持不好(完整的語法:guarding.pest):

normal_rule = {rule_level ~ ("(" ~ scope ~ ")")? ~ (use_symbol ~ expression)? ~ should? ~ only? ~ operator ~ assert ~ ";"?
}rule_level = {"package" |"class" |"struct" |"function" |"file"
}use_symbol = {"::" |"->"
}

rule_level 對應 ArchUnit 里的 CodeUnits,后面的?operator?和?assert便是核心的計算邏輯所在。最后的規則示例:


// class
class(implementation "BaseParser")::name should endsWith "Parser";
class("java.util.Map") only accessed(["com.phodal.pepper.refactor.staticclass"]);
class(implementation "BaseParser")::name should not contains "Lexer";// naming
class("..myapp..")::function.name should contains("Model");// 簡單的值計算
package(".")::file.len should < 200;
package(".")::file.len should > 50;

代碼中的?::?可以換成??表示,都是在?use_symbol?中聲明的,自己寫的語法嘛,怎么開心就這么寫。最后,代碼是可以 work 的,也沒有枉費我看了許久的 ArchUnit 源碼。

順帶一提,先前提到的 Tree-sitter 的 S 表達式還挺好玩的,有空應該實現一個:

(using_directive(qualified_name) @import-name)

上述的代碼可以用于識別 C# 里的?using聲明。不過,我在 Guarding 中實現的解析倒是不太好。

其它

重構是一件有技巧、有難度的手工活。但是,作為一個工程實踐上的專家,我們應該讓重構消失。

回到開始,成為一個代碼方面的專家非常有意思。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/910551.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/910551.shtml
英文地址,請注明出處:http://en.pswp.cn/news/910551.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【android bluetooth 框架分析 04】【bt-framework 層詳解 8】【DeviceProperties介紹】

前面我們提到了 藍牙協議棧中的 Properties &#xff0c; 這篇文章是 他的補充。 【android bluetooth 框架分析 04】【bt-framework 層詳解 6】【Properties介紹】 1. 設計初衷與核心問題 1. 為什么要設計 DeviceProperties&#xff1f; 在 Android 藍牙實際使用中&#x…

華為OD-2024年E卷-字母組合[200分] -- python

問題描述&#xff1a; 每個數字對應多個字母&#xff0c;對應關系如下&#xff1a; 0&#xff1a;a,b,c 1&#xff1a;d,e,f 2&#xff1a;g,h,i 3&#xff1a;j,k,l 4&#xff1a;m,n,o 5&#xff1a;p,q,r 6&#xff1a;s,t 7&#xff1a;u,v 8&#xff1a;w,x 9&#xff1…

機器學習競賽中的“A榜”與“B榜”:機制解析與設計深意

在Kaggle、天池等主流機器學習競賽平臺上&#xff0c;“A榜”&#xff08;Public Leaderboard&#xff09;和“B榜”&#xff08;Private Leaderboard&#xff09;是選手們最關注的指標。但很多新人對兩者的區別和設計意圖感到困惑。本文將深入解析其差異及背后的邏輯。 &#…

云徙科技 OMS:讓訂單管理變得輕松又高效

在如今這個線上線下購物融合得越來越緊密的時代&#xff0c;企業要是想在競爭激烈的市場里站穩腳跟&#xff0c;訂單管理這一塊可得好好下功夫。云徙科技的 OMS&#xff08;訂單管理系統&#xff09;就像是給企業量身打造的一把“金鑰匙”&#xff0c;能幫企業把訂單管理得井井…

qt常用控件--02

文章目錄 qt常用控件--02toolTip屬性focusPolicy屬性styleSheet屬性補充知識點按鈕類控件QPushButton 結語 很高興和大家見面&#xff0c;給生活加點impetus&#xff01;&#xff01;開啟今天的編程之路&#xff01;&#xff01; 今天我們進一步c11中常見的新增表達 作者&…

P3258 [JLOI2014] 松鼠的新家

題目描述 松鼠的新家是一棵樹&#xff0c;前幾天剛剛裝修了新家&#xff0c;新家有 n n n 個房間&#xff0c;并且有 n ? 1 n-1 n?1 根樹枝連接&#xff0c;每個房間都可以相互到達&#xff0c;且倆個房間之間的路線都是唯一的。天哪&#xff0c;他居然真的住在“樹”上。 …

基于openfeign攔截器RequestInterceptor實現的微服務之間的夾帶轉發

需求&#xff1a; trade服務需要在下單后清空購物車 分析&#xff1a; 顯然&#xff0c;清空購物車需要調用cart服務&#xff0c;也就是這個功能的實現涉及到了微服務之間的轉發。 其次&#xff0c;清空購車還需要userId&#xff0c;所以需要使用RequestInterceptor來實現夾…

w~深度學習~合集9

我自己的原文哦~ https://blog.51cto.com/whaosoft/14010384 #UPSCALE 這里設計了一個通用算法UPSCALE&#xff0c;可以剪枝具有任意剪枝模式的模型。通過消除約束&#xff0c;UPSCALE將ImageNet精度提高2.1個點。 paper地址&#xff1a;https://arxiv.org/pdf/2307.08…

python如何刪除xml中的w:ascii屬性

可以使用Python的xml.etree.ElementTree模塊通過以下步驟刪除XML中的w:ascii屬性&#xff1a; import xml.etree.ElementTree as ET# 原始XML片段&#xff08;需包含命名空間聲明&#xff09; xml_str <w:rPr xmlns:w"http://schemas.openxmlformats.org/wordproces…

【React】React CSS 樣式設置全攻略

在 React 中設置 CSS 樣式主要有以下幾種方式&#xff0c;各有適用場景&#xff1a; 1. 內聯樣式 (Inline Styles) 直接在 JSX 元素中使用 style 屬性&#xff0c;值為 JavaScript 對象&#xff08;使用駝峰命名法&#xff09; function Component() {return (<div style…

JS紅寶書筆記 8.2 創建對象

雖然使用Object構造函數或對象字面量可以方便地創建對象&#xff0c;但這些方式有明顯不足&#xff1a;創建具有同樣接口的多個對象需要重復編寫很多代碼 工廠模式可以用不同的參數多次調用函數&#xff0c;每次都會返回一個新對象&#xff0c;這種模式雖然可以解決創建多個類…

高通camx hal進程dump日志分析三:Pipeline DumpDebugInfo原理分析

【關注我,后續持續新增專題博文,謝謝!!!】 上一篇我們講了: 這一篇我們開始講: 目錄 一、問題背景 二、DumpDebugInfo原理 2.1:我們分析下代碼 2.2 :Pipeline Dump debug info 2.3 :dump Metadata Pending Node信息 2.4 :Dump Metadata Pool Debug信息 2.5 :No…

【數據結構】_二叉樹基礎OJ

目錄 1. 單值二叉樹 1.1 題目鏈接與描述 1.2 解題思路 1.3 程序 2. 相同的樹 2.1 題目鏈接與描述 2.2 解題思路 2.3 程序 3. 對稱二叉樹 3.1 題目鏈接與描述 3.2 解題思路 3.3 程序 1. 單值二叉樹 1.1 題目鏈接與描述 題目鏈接&#xff1a; 965. 單值二叉樹 - 力…

軟件工程畫圖題

目錄 1.大綱 2.數據流圖 3.程序流圖 4.流圖 5.ER圖 6.層次圖 7.結構圖 8.盒圖 9.狀態轉換圖 10.類圖 11.用例圖 12.活動圖 13.判定表和判定樹 14.基本路徑測試過程(白盒測試) 15.等價類劃分(黑盒測試) 1.大綱 (1).數據流圖 (2).程序流圖 (3).流圖 (4).ER圖…

H7-TOOL自制Flash讀寫保護算法系列,為華大電子CIU32F003制作使能和解除算法,支持在線燒錄和脫機燒錄使用2025-06-20

說明&#xff1a; 很多IC廠家僅發布了內部Flash算法文件&#xff0c;并沒有提供讀寫保護算法文件&#xff0c;也就是選項字節算法文件&#xff0c;需要我們制作。 實際上當前已經發布的TOOL版本&#xff0c;已經自制很多了&#xff0c;比如已經支持的兆易創新大部分型號&…

go channel用法

介紹 channel 在 Go 中是一種專門用來在 goroutine 之間傳遞數據的類型安全的管道。 你可以把它理解成&#xff1a; 多個 goroutine 之間的**“傳話筒”**&#xff0c;誰往通道里塞東西&#xff0c;另一個 goroutine 就能接收到。 Go 語言采用 CSP&#xff08;Communicatin…

openLayers切換基于高德、天地圖切換矢量、影像、地形圖層

1、需要先加載好地圖&#xff0c;具體點此鏈接 openLayers添加天地圖WMTS、XYZ瓦片服務圖層、高德地圖XYZ瓦片服務圖層-CSDN博客文章瀏覽閱讀31次。本文介紹了基于OpenLayers的地圖交互功能實現&#xff0c;主要包括以下內容&#xff1a; 地圖初始化&#xff1a;支持天地圖XYZ…

springMVC-15 異常處理

異常處理-基本介紹 基本介紹 1.Spring MVC通過HandlerExceptionResolver處理程序的異常&#xff0c;包括Handler映射、數據綁定以及目標方法執行時發生的異常。 2.主要處理Handler中用ExceptionHandler注解定義的方法。 3.ExceptionHandlerMethodResolver內部若找不到Excepti…

視頻匯聚EasyCVR平臺v3.7.2發布:新增全局搜索、播放器默認解碼方式等4大功能

EasyCVR視頻匯聚平臺帶著全新的v3.7.2版本重磅登場&#xff01;此次升級&#xff0c;絕非簡單的功能堆砌&#xff0c;而是從用戶體驗、操作效率以及系統性能等多維度進行的深度優化與革新&#xff0c;旨在為大家帶來更加強大、穩定且高效的視頻監控管理體驗。 一、全局功能搜索…

三、kubectl使用詳解

三、kubectl使用詳解 文章目錄 三、kubectl使用詳解1、常用基礎命令1.1 Kubectl命令格式1.2 查詢一個資源1.3 創建一個資源1.4 修改一個資源1.5 刪除一個資源1.6 其他 2、K8s隔離機制Namespace&#xff08;命名空間作用及使用&#xff09;2.1 什么是命名空間2.2 命名空間主要作…