SpringBoot如何解析應用參數(shù)args
前言
前文深入解析了SpringBoot啟動的開始階段,包括獲取和啟動應用啟動監(jiān)聽器、事件與廣播機制,以及如何通過匹配監(jiān)聽器實現(xiàn)啟動過程各階段的自定義邏輯。接下來,我們將探討SpringBoot啟動類main函數(shù)中的參數(shù)args的作用及其解析過程
。
SpringBoot版本2.7.18
SpringApplication的run方法的執(zhí)行邏輯如下,本文將詳細介紹第3小節(jié):解析應用參數(shù)
// SpringApplication類方法 public ConfigurableApplicationContext run(String... args) { // 記錄應用啟動的開始時間 long startTime = System.nanoTime(); // 1.創(chuàng)建引導上下文,用于管理應用啟動時的依賴和資源 DefaultBootstrapContext bootstrapContext = createBootstrapContext(); ConfigurableApplicationContext context = null; // 配置無頭模式屬性,以支持在無圖形環(huán)境下運行 // 將系統(tǒng)屬性 java.awt.headless 設置為 true configureHeadlessProperty(); // 2.獲取Spring應用啟動監(jiān)聽器,用于在應用啟動的各個階段執(zhí)行自定義邏輯 SpringApplicationRunListeners listeners = getRunListeners(args); // 啟動開始方法(發(fā)布開始事件、通知應用監(jiān)聽器ApplicationListener) listeners.starting(bootstrapContext, this.mainApplicationClass); try { // 3.解析應用參數(shù) ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // 4.準備應用環(huán)境,包括讀取配置文件和設置環(huán)境變量 ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments); // 配置是否忽略 BeanInfo,以加快啟動速度 configureIgnoreBeanInfo(environment); // 5.打印啟動Banner Banner printedBanner = printBanner(environment); // 6.創(chuàng)建應用程序上下文 context = createApplicationContext(); // 設置應用啟動的上下文,用于監(jiān)控和管理啟動過程 context.setApplicationStartup(this.applicationStartup); // 7.準備應用上下文,包括加載配置、添加 Bean 等 prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); // 8.刷新上下文,完成 Bean 的加載和依賴注入 refreshContext(context); // 9.刷新后的一些操作,如事件發(fā)布等 afterRefresh(context, applicationArguments); // 計算啟動應用程序的時間,并記錄日志 Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup); } // 10.通知監(jiān)聽器應用啟動完成 listeners.started(context, timeTakenToStartup); // 11.調(diào)用應用程序中的 `CommandLineRunner` 或 `ApplicationRunner`,以便執(zhí)行自定義的啟動邏輯 callRunners(context, applicationArguments); } catch (Throwable ex) { // 12.處理啟動過程中發(fā)生的異常,并通知監(jiān)聽器 handleRunFailure(context, ex, listeners); throw new IllegalStateException(ex); } try { // 13.計算應用啟動完成至準備就緒的時間,并通知監(jiān)聽器 Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime); listeners.ready(context, timeTakenToReady); } catch (Throwable ex) { // 處理準備就緒過程中發(fā)生的異常 handleRunFailure(context, ex, null); throw new IllegalStateException(ex); } // 返回已啟動并準備就緒的應用上下文 return context; }
一、入口
將main方法的參數(shù)args封裝成一個對象DefaultApplicationArguments
,以便方便地解析和訪問啟動參數(shù)
// 3.解析應用參數(shù) ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
二、默認應用程序參數(shù)DefaultApplicationArguments
1、功能概述
DefaultApplicationArguments
是SpringBoot中的一個類,用于處理啟動時傳入的參數(shù)。它實現(xiàn)了ApplicationArguments
接口,并提供了一些便捷的方法來訪問傳入的命令行參數(shù)
和選項參數(shù)
。
// 3.解析應用參數(shù) ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
- 解析命令行參數(shù):將
main
方法中的args
參數(shù)解析成選項參數(shù)和非選項參數(shù),方便應用在啟動時讀取外部傳入的配置 - 訪問
選項參數(shù)
:支持以--key=value
格式的選項參數(shù),通過方法getOptionNames()
和getOptionValues(String name)
獲取特定的選項及其值 - 訪問
非選項參數(shù)
:對于不以--
開頭的參數(shù),可以通過getNonOptionArgs()
獲取它們的列表
2、使用示例
假設我們在命令行中運行應用,傳遞了一些參數(shù)
java -jar myapp.jar --server.port=8080 arg1 arg2
在代碼中,我們可以使用DefaultApplicationArguments
來解析這些參數(shù)
public static void main(String[] args) { DefaultApplicationArguments appArgs = new DefaultApplicationArguments(args); // 獲取所有選項參數(shù)名稱 System.out.println("選項參數(shù):" + appArgs.getOptionNames()); // 輸出: ["server.port"] // 獲取指定選項的值(所有以 `--` 開頭的選項參數(shù)名稱) System.out.println("server.port 值:" + appArgs.getOptionValues("server.port")); // 輸出: ["8080"] // 獲取非選項參數(shù)(所有不以 `--` 開頭的參數(shù),通常用于傳遞無標記的參數(shù)值) System.out.println("非選項參數(shù):" + appArgs.getNonOptionArgs()); // 輸出: ["arg1", "arg2"] }
3、接口ApplicationArguments
ApplicationArguments是DefaultApplicationArguments
類的父接口
// 提供對用于運行應用的參數(shù)的訪問。 public interface ApplicationArguments { // 返回傳遞給應用程序的原始未處理參數(shù) String[] getSourceArgs(); // 返回所有選項參數(shù)的名稱 Set<String> getOptionNames(); // 返回解析的選項參數(shù)集合中是否包含具有給定名稱的選項 boolean containsOption(String name); // 返回與給定名稱的選項參數(shù)關(guān)聯(lián)的值集合 List<String> getOptionValues(String name); // 返回解析的非選項參數(shù)集合 List<String> getNonOptionArgs(); }
getOptionNames()
:返回所有選項參數(shù)的名稱- 例如:參數(shù)是
"--foo=bar --debug"
,則返回["foo", "debug"]
- 例如:參數(shù)是
getOptionValues(String name)
:返回與給定名稱的選項參數(shù)關(guān)聯(lián)的值集合- 如果選項存在但沒有值(例如:
"--foo"
),返回一個空集合 - 如果選項存在且有單一值(例如:
"--foo=bar"
),返回一個包含一個元素的集合["bar"]
- 如果選項存在且有多個值(例如:
"--foo=bar --foo=baz"
),返回包含每個值的集合["bar", "baz"]
- 如果選項不存在,返回null
- 如果選項存在但沒有值(例如:
getNonOptionArgs()
:返回解析的非選項參數(shù)集合
4、實現(xiàn)類DefaultApplicationArguments
代碼很簡單,對外暴露使用DefaultApplicationArguments
,內(nèi)部實現(xiàn)都在Source
中
// ApplicationArguments的默認實現(xiàn)類,用于解析應用程序啟動時傳入的參數(shù)。 public class DefaultApplicationArguments implements ApplicationArguments { private final Source source; // 用于解析和存儲參數(shù)的內(nèi)部輔助類 private final String[] args; // 啟動時傳入的原始參數(shù) // 構(gòu)造函數(shù),使用傳入的參數(shù)數(shù)組初始化對象 public DefaultApplicationArguments(String... args) { Assert.notNull(args, "Args must not be null"); // 確保傳入?yún)?shù)不為 null this.source = new Source(args); // 使用內(nèi)部類 Source 解析參數(shù) this.args = args; // 保存原始參數(shù) } // 獲取原始未處理的參數(shù)數(shù)組 @Override public String[] getSourceArgs() { return this.args; } // 獲取所有選項參數(shù)的名稱集合 @Override public Set<String> getOptionNames() { String[] names = this.source.getPropertyNames(); // 該集合不能被修改(即添加、刪除元素等操作會拋出 UnsupportedOperationException 異常) return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(names))); } // 檢查是否包含指定名稱的選項參數(shù) @Override public boolean containsOption(String name) { return this.source.containsProperty(name); } // 獲取指定名稱的選項參數(shù)的值集合 @Override public List<String> getOptionValues(String name) { List<String> values = this.source.getOptionValues(name); return (values != null) ? Collections.unmodifiableList(values) : null; } // 獲取所有非選項參數(shù)(不以 "--" 開頭的參數(shù)) @Override public List<String> getNonOptionArgs() { return this.source.getNonOptionArgs(); } // 內(nèi)部類,用于處理和解析命令行參數(shù) // 繼承自 SimpleCommandLinePropertySource,可以獲取選項參數(shù)和非選項參數(shù) private static class Source extends SimpleCommandLinePropertySource { // 使用參數(shù)數(shù)組初始化 Source Source(String[] args) { super(args); } // 獲取所有非選項參數(shù)。 @Override public List<String> getNonOptionArgs() { return super.getNonOptionArgs(); } // 獲取指定名稱的選項參數(shù)的值列表 @Override public List<String> getOptionValues(String name) { return super.getOptionValues(name); } } }
三、Source
Source是DefaultApplicationArguments解析參數(shù)內(nèi)部的真正實現(xiàn)類,類圖如下,逐一分析。
1、屬性源PropertySource
PropertySource
是Spring框架中的一個核心抽象類,用于表示屬性(鍵值對)的來源
。通過將各種配置來源(如系統(tǒng)屬性
、環(huán)境變量
、配置文件
等)封裝為PropertySource對象,Spring可以提供統(tǒng)一的接口來讀取和管理這些配置數(shù)據(jù)。
屬性源名稱
:每個PropertySource實例都具有唯一的名稱
,用于區(qū)分不同的屬性源屬性源對象
:PropertySource<T>
是一個泛型類,其中T
代表具體的屬性源類型- getProperty(String name):用于在屬性源對象中檢索具體的屬性,name表示具體屬性的鍵,返回具體屬性的值
public abstract class PropertySource<T> { protected final Log logger = LogFactory.getLog(getClass()); protected final String name; // 屬性源的名稱 protected final T source; // 屬性源的數(shù)據(jù)源對象 // 使用給定的名稱和源對象創(chuàng)建屬性源 public PropertySource(String name, T source) { Assert.hasText(name, "Property source name must contain at least one character"); Assert.notNull(source, "Property source must not be null"); this.name = name; this.source = source; } // 使用給定的名稱和一個新的Object對象作為底層源創(chuàng)建屬性源 public PropertySource(String name) { this(name, (T) new Object()); } // 返回屬性源名稱 public String getName() { return this.name; } // 返回屬性源的底層源對象。 public T getSource() { return this.source; } // 判斷屬性源是否包含給定名稱的屬性(子類可以實現(xiàn)更高效的算法) // containsProperty和getProperty參數(shù)的name與上面定義的屬性源名稱的name不是一回事 public boolean containsProperty(String name) { return (getProperty(name) != null); } // 返回與給定名稱關(guān)聯(lián)的屬性值,如果找不到則返回null,子類實現(xiàn) @Nullable public abstract Object getProperty(String name); ... }
2、枚舉屬性源EnumerablePropertySource
EnumerablePropertySource
繼承自PropertySource,主要用于定義getPropertyNames()
方法,可以獲取屬性源對象中所有屬性鍵的名稱
。
public abstract class EnumerablePropertySource<T> extends PropertySource<T> { // 使用給定的名稱和源對象創(chuàng)建屬性源(調(diào)用父類PropertySource的構(gòu)造方法) public EnumerablePropertySource(String name, T source) { super(name, source); } // 也是調(diào)用父類構(gòu)造 protected EnumerablePropertySource(String name) { super(name); } // 判斷屬性源是否包含具有給定名稱的屬性(重新了PropertySource的此方法) @Override public boolean containsProperty(String name) { return ObjectUtils.containsElement(getPropertyNames(), name); } // 返回所有屬性的名稱 public abstract String[] getPropertyNames(); }
3、命令行屬性源CommandLinePropertySource
CommandLinePropertySource
是Spring框架中用于處理命令行參數(shù)
的PropertySource
實現(xiàn)。它可以將應用程序啟動時傳入的命令行參數(shù)解析成鍵值對
,便于在應用配置中使用。
- 命令行屬性源名稱默認為
commandLineArgs
getOptionValues(String name)
:通過命令行屬性源(即選項參數(shù)鍵值對
)的鍵獲取對應的值getNonOptionArgs()
:通過命令行屬性源(即鍵默認為nonOptionArgs的非選項參數(shù)鍵值對
)獲取對于的值- 例:java -jar your-app.jar --server.port=8081 --spring.profiles.active=prod arg1 arg2
- 選項參數(shù)會有
多個鍵值對
,key1為server.port,key2為spring.profiles.active - 非選項參數(shù)
永遠只有一個鍵值對
,所有key都是nonOptionArgs
- 選項參數(shù)會有
public abstract class CommandLinePropertySource<T> extends EnumerablePropertySource<T> { // CommandLinePropertySource實例的默認名稱 public static final String COMMAND_LINE_PROPERTY_SOURCE_NAME = "commandLineArgs"; // 表示非選項參數(shù)的屬性鍵的默認名稱 public static final String DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME = "nonOptionArgs"; private String nonOptionArgsPropertyName = DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME; // 創(chuàng)建一個新的命令行屬性源,使用默認名稱 public CommandLinePropertySource(T source) { super(COMMAND_LINE_PROPERTY_SOURCE_NAME, source); } // 創(chuàng)建一個新的命令行屬性源,具有給定名稱 public CommandLinePropertySource(String name, T source) { super(name, source); } // 可以通過set方法設置非選項參數(shù)的鍵的名稱 public void setNonOptionArgsPropertyName(String nonOptionArgsPropertyName) { this.nonOptionArgsPropertyName = nonOptionArgsPropertyName; } // 首先檢查指定的名稱是否是特殊的“非選項參數(shù)”屬性, // 如果是,則委托給抽象方法#getNonOptionArgs() // 否則,委托并返回抽象方法#containsOption(String) @Override public final boolean containsProperty(String name) { if (this.nonOptionArgsPropertyName.equals(name)) { return !getNonOptionArgs().isEmpty(); } return this.containsOption(name); } // 首先檢查指定的名稱是否是特殊的“非選項參數(shù)”屬性, // 如果是,則委托給抽象方法#getNonOptionArgs(),返回用逗號隔開字符串 // 否則,委托并返回抽象方法#getOptionValues(name),返回用逗號隔開字符串 @Override @Nullable public final String getProperty(String name) { if (this.nonOptionArgsPropertyName.equals(name)) { Collection<String> nonOptionArguments = getNonOptionArgs(); if (nonOptionArguments.isEmpty()) { return null; } else { return StringUtils.collectionToCommaDelimitedString(nonOptionArguments); } } Collection<String> optionValues = getOptionValues(name); if (optionValues == null) { return null; } else { return StringUtils.collectionToCommaDelimitedString(optionValues); } } // 返回從命令行解析的選項參數(shù)集合中是否包含具有給定名稱的選項 protected abstract boolean containsOption(String name); // 返回與給定名稱的選項參數(shù)關(guān)聯(lián)的值集合 @Nullable protected abstract List<String> getOptionValues(String name); // 返回從命令行解析的非選項參數(shù)集合,永不為null protected abstract List<String> getNonOptionArgs(); }
4、簡單命令行屬性源SimpleCommandLinePropertySource
SimpleCommandLinePropertySource
是Spring框架中的一個類,繼承自CommandLinePropertySource,用于解析和處理命令行參數(shù)
。它設計為簡單易用,通過接收一個字符串數(shù)組(即命令行參數(shù) args),將參數(shù)分為"選項參數(shù)"
和"非選項參數(shù)"
兩類。
- 命令行屬性源對象類型為
CommandLineArgs
,通過new SimpleCommandLineArgsParser().parse(args)
獲取
public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> { // 構(gòu)造函數(shù):創(chuàng)建一個使用默認名稱commandLineArgs的SimpleCommandLinePropertySource實例的命令行屬性源 public SimpleCommandLinePropertySource(String... args) { super(new SimpleCommandLineArgsParser().parse(args)); } // 創(chuàng)建指定名稱命令行屬性源 public SimpleCommandLinePropertySource(String name, String[] args) { super(name, new SimpleCommandLineArgsParser().parse(args)); } // 獲取所有選項參數(shù)的名稱 @Override public String[] getPropertyNames() { return StringUtils.toStringArray(this.source.getOptionNames()); } // 檢查是否包含指定名稱的選項 @Override protected boolean containsOption(String name) { return this.source.containsOption(name); } // 獲取指定選項名稱的值列表 @Override @Nullable protected List<String> getOptionValues(String name) { return this.source.getOptionValues(name); } // 獲取所有非選項參數(shù)的列表 @Override protected List<String> getNonOptionArgs() { return this.source.getNonOptionArgs(); } }
四、解析參數(shù)原理
在上一節(jié)中,我們了解了應用程序參數(shù)args
被解析后的結(jié)構(gòu)
和存儲方式
。接下來,我們回到文章開頭,詳細解析參數(shù)是如何被逐步解析出來的。
// 3.解析應用參數(shù) ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
根據(jù)new DefaultApplicationArguments(args)尋找解析arg的位置
解析完arg調(diào)用父類CommandLinePropertySource的構(gòu)造方法
1、解析方法
SimpleCommandLineArgsParser通過遍歷傳入的命令行參數(shù)數(shù)組,根據(jù)參數(shù)的格式,將參數(shù)解析并分為選項參數(shù)和非選項參數(shù)。
- 選項參數(shù)解析規(guī)則
- 選項參數(shù)
必須以--前綴開頭
,例如 --name=John 或 --debug - 如果包含等號 =,= 左邊的部分是選項名稱,右邊的部分是選項值
- 如果沒有等號,則視為不帶值的選項
- 如果獲取不到選項名稱(例如傳入 --=value或–),拋出異常,表示參數(shù)格式無效
- 選項參數(shù)
- 非選項參數(shù)解析規(guī)則
- 所有
不以--開頭
的參數(shù)被視為非選項參數(shù)
- 所有
class SimpleCommandLineArgsParser { public CommandLineArgs parse(String... args) { // 創(chuàng)建 CommandLineArgs 實例,用于存儲解析結(jié)果 CommandLineArgs commandLineArgs = new CommandLineArgs(); for (String arg : args) { // 遍歷每個命令行參數(shù) if (arg.startsWith("--")) { // 如果參數(shù)以 "--" 開頭,則視為選項參數(shù) String optionText = arg.substring(2); // 去掉 "--" 前綴 String optionName; // 選項名稱 String optionValue = null; // 選項值,默認為 null int indexOfEqualsSign = optionText.indexOf('='); // 查找等號的位置 if (indexOfEqualsSign > -1) { // 如果找到了等號 optionName = optionText.substring(0, indexOfEqualsSign); // 等號前的部分為選項名稱 optionValue = optionText.substring(indexOfEqualsSign + 1); // 等號后的部分為選項值 } else { optionName = optionText; // 如果沒有等號,整個文本為選項名稱,值為 null } // 如果選項名稱為空,拋出異常,例如,只輸入了 "--=" 或 "--" if (optionName.isEmpty()) { throw new IllegalArgumentException("Invalid argument syntax: " + arg); } // 將解析出的選項名稱和值添加到 CommandLineArgs 對象中 commandLineArgs.addOptionArg(optionName, optionValue); } else { // 如果參數(shù)不是選項參數(shù),直接作為非選項參數(shù)添加到 CommandLineArgs 對象中 commandLineArgs.addNonOptionArg(arg); } } return commandLineArgs; // 返回解析結(jié)果 } }
屬性源對象類型CommandLineArgs
// 命令行參數(shù)的簡單表示形式,分為“帶選項參數(shù)”和“無選項參數(shù)”。 class CommandLineArgs { // 存儲帶選項的參數(shù),每個選項可以有一個或多個值 private final Map<String, List<String>> optionArgs = new HashMap<>(); // 存儲無選項的參數(shù) private final List<String> nonOptionArgs = new ArrayList<>(); // 為指定的選項名稱添加一個選項參數(shù),并將給定的值添加到與此選項關(guān)聯(lián)的值列表中(可能有零個或多個) public void addOptionArg(String optionName, @Nullable String optionValue) { if (!this.optionArgs.containsKey(optionName)) { this.optionArgs.put(optionName, new ArrayList<>()); } if (optionValue != null) { this.optionArgs.get(optionName).add(optionValue); } } // 返回命令行中所有帶選項的參數(shù)名稱集合 public Set<String> getOptionNames() { return Collections.unmodifiableSet(this.optionArgs.keySet()); } // 判斷命令行中是否包含指定名稱的選項。 public boolean containsOption(String optionName) { return this.optionArgs.containsKey(optionName); } // 返回與給定選項關(guān)聯(lián)的值列表。 // 表示null表示該選項不存在;空列表表示該選項沒有關(guān)聯(lián)值。 @Nullable public List<String> getOptionValues(String optionName) { return this.optionArgs.get(optionName); } // 將給定的值添加到無選項參數(shù)列表中 public void addNonOptionArg(String value) { this.nonOptionArgs.add(value); } // 返回命令行中指定的無選項參數(shù)列表 public List<String> getNonOptionArgs() { return Collections.unmodifiableList(this.nonOptionArgs); } }
2、解析參數(shù)的存儲和訪問
解析方法很簡單,所有內(nèi)容都在SimpleCommandLineArgsParser的parse方法中完成。相比之下,存儲
和訪問
方式更為復雜。
存儲位置位于屬性源對象PropertySource
中。從代碼可知,args表示命令行參數(shù),因此屬性源名稱
為命令行屬性源默認名稱commandLineArgs
,屬性源對象
為解析args后的鍵值對
。
訪問查詢方式的底層實現(xiàn)就是操作CommandLineArgs中的optionArgs(選項參數(shù))
和nonOptionArgs(非選項參數(shù))
兩個集合,但此過程經(jīng)過多次跳轉(zhuǎn),最終依次通過 DefaultApplicationArguments -> DefaultApplicationArguments#Source -> SimpleCommandLinePropertySource -> CommandLineArgs
獲取,其中CommandLineArgs就是是命令行屬性源對象。這種設計主要是為了提供更靈活、安全的訪問方式,避免直接暴露內(nèi)部數(shù)據(jù)結(jié)構(gòu)帶來的潛在風險。
3、實際應用
之前在SpringBoot基礎(二):配置文件詳解文章中有介紹過配置文件設置臨時屬性,這次回過頭再來看,就很清晰明了了。
總結(jié)
- 在SpringBoot啟動時,啟動類main函數(shù)中的
args
參數(shù)被解析為兩類選項參數(shù)
(如 --server.port=8080)非選項參數(shù)
(如 arg1、arg2)
- 對外暴露應用參數(shù)對象
ApplicationArguments
提供查詢方法getOptionValues(String name)
方法可以獲取選項參數(shù)getNonOptionArgs()
方法則用于獲取非選項參數(shù)- 這些參數(shù)在啟動過程的后續(xù)階段可供使用
到此這篇關(guān)于SpringBoot源碼解析(之如何解析應用參數(shù)args的文章就介紹到這了,更多相關(guān)SpringBoot解析應用參數(shù)args內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot 指標監(jiān)控actuator的專題
未來每一個微服務在云上部署以后,我們都需要對其進行監(jiān)控、追蹤、審計、控制等。SpringBoot就抽取了Actuator場景,使得我們每個微服務快速引用即可獲得生產(chǎn)級別的應用監(jiān)控、審計等功能,通讀本篇對大家的學習或工作具有一定的價值,需要的朋友可以參考下2021-11-11Mybatis之通用Mapper動態(tài)表名及其原理分析
這篇文章主要介紹了Mybatis之通用Mapper動態(tài)表名及其原理分析,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08