寫在前面: 我是 揚帆向海,這個暱稱來源於我的名字以及女朋友的名字。我熱愛技術、熱愛開源、熱愛編程。
技術是開源的、知識是共享的
。
這博客是對自己學習的一點點總結及記錄,如果您對 Java、算法 感興趣,可以關注我的動態,我們一起學習。
用知識改變命運,讓我們的家人過上更好的生活
。
相關文章:
【SpringBoot 系列】史上最全的springboot學習教程
本文通過剖析源碼,對Spring Boot(基於2.x版本)的啓動過程進行深入的理解 |
文章目錄
一、入口類及其源碼剖析
入口類
@SpringBootApplication
public class DevServiceApplication {
public static void main(String[] args) {
SpringApplication.run(DevServiceApplication.class,args);
}
}
首先從註解入手,進行分析:
@SpringBootApplication 註解
Spring Boot應用標註在某個類上說明這個類是SpringBoot的主配置類,SpringBoot就應該運行這個類的main方法來啓動SpringBoot應用
源碼剖析
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
從源碼可以看出,這個註解是@SpringBootConfiguration,@EnableAutoConfiguration以及@ComponentScan這三個註解的組合
① @SpringBootConfiguration
Spring Boot的配置類;標註在某個類上,表示一個類提供了Spring Boot應用程序
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
@Configuration:配置類上來標註這個註解;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
注意:
配置類相當於配置文件;配置類也是容器中的一個組件,它使用了@Component這個註解。
② @EnableAutoConfiguration
告訴SpringBoot開啓自動配置功能,這樣自動配置才能生效
藉助@import,掃描並實例化滿足條件的自動配置的bean,然後加載到IOC容器中
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};
/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
String[] excludeName() default {};
}
@AutoConfigurationPackage:自動配置包
@Import(EnableAutoConfigurationImportSelector.class):給容器中導入組件
使用@EnableAutoConfiguration
這個註解開啓自動掃描,然後使用select選擇挑選滿足條件的文件,並且使用SpringFactoriesLoader進行實例化。最後加載到IOC容器裏面,即ApplicationContext中。
③ @ComponentScan
@ComponentScan就是自動掃描並加載符合條件的組件(比如@Component和@Repository等)或者bean定義,最終將這些bean定義加載到IOC容器中去 。
二、實例化SpringApplication對象的源碼剖析
源碼剖析
/**
* Create a new {@link SpringApplication} instance. The application context will load
* beans from the specified primary sources (see {@link SpringApplication class-level}
* documentation for details. The instance can be customized before calling
* {@link #run(String...)}.
* @param resourceLoader the resource loader to use
* @param primarySources the primary bean sources
* @see #run(Class, String[])
* @see #setSources(Set)
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 初始化資源加載器
this.resourceLoader = resourceLoader;
// 資源加載類不能爲 null
Assert.notNull(primarySources, "PrimarySources must not be null");
// 初始化加載資源類集合並去重
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 推斷應用程序是不是web應用
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 設置初始化器(Initializer)
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 設置監聽器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 推斷出主應用入口類
this.mainApplicationClass = deduceMainApplicationClass();
}
其中,在推斷應用程序是不是web應用的時候調用了deduceFromClasspath() 方法
源碼剖析
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
// springboot2.0提出的響應式web應用
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
// 如果兩個包路徑都沒有的話,就是普通應用
if (!ClassUtils.isPresent(className, null)) {
// 普通的應用
return WebApplicationType.NONE;
}
}
// 其實最後返回的就是這個servlet,因爲是web應用
return WebApplicationType.SERVLET;
}
1. 設置初始化器(Initializer)
initializers 是 SpringApplication 中的一個實例屬性
源碼剖析
/**
* Sets the {@link ApplicationContextInitializer} that will be applied to the Spring
* {@link ApplicationContext}.
* @param initializers the initializers to set
*/
public void setInitializers(Collection<? extends ApplicationContextInitializer<?>> initializers) {
this.initializers = new ArrayList<>(initializers);
}
initailizer實現了ApplicationContextInitializer接口
源碼剖析
/**
* Callback interface for initializing a Spring {@link ConfigurableApplicationContext}
* prior to being {@linkplain ConfigurableApplicationContext#refresh() refreshed}.
*
* <p>Typically used within web applications that require some programmatic initialization
* of the application context. For example, registering property sources or activating
* profiles against the {@linkplain ConfigurableApplicationContext#getEnvironment()
* context's environment}. See {@code ContextLoader} and {@code FrameworkServlet} support
* for declaring a "contextInitializerClasses" context-param and init-param, respectively.
*
* <p>{@code ApplicationContextInitializer} processors are encouraged to detect
* whether Spring's {@link org.springframework.core.Ordered Ordered} interface has been
* implemented or if the @{@link org.springframework.core.annotation.Order Order}
* annotation is present and to sort instances accordingly if so prior to invocation.
*
* @author Chris Beams
* @since 3.1
* @param <C> the application context type
* @see org.springframework.web.context.ContextLoader#customizeContext
* @see org.springframework.web.context.ContextLoader#CONTEXT_INITIALIZER_CLASSES_PARAM
* @see org.springframework.web.servlet.FrameworkServlet#setContextInitializerClasses
* @see org.springframework.web.servlet.FrameworkServlet#applyInitializers
*/
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
/**
* Initialize the given application context.
* @param applicationContext the application to configure
*/
// 把初始化的ApplicationContextInitializer實現類加載到SpringApplication中
void initialize(C applicationContext);
}
總結:
- ApplicationContextInitializer接口的作用,在Spring上下文被刷新之前進行初始化的操作。典型地比如在Web應用中,註冊Property Sources或者是激活Profiles。Property Sources比較好理解,就是配置文件。Profiles是Spring爲了在不同環境下(如DEV,TEST,PRODUCTION等),加載不同的配置項而抽象出來的一個實體。
- 調用initialize()方法,把初始化的ApplicationContextInitializer實現加載到SpringApplication中
通過getSpringFactoriesInstances(
ApplicationContextInitializer.class)方法獲得實現類
源碼剖析
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return getSpringFactoriesInstances(type, new Class<?>[] {});
}
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
// 使用 Set保存names
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 根據names進行實例化
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
// 對實例進行排序
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
2. 設置監聽器
源碼剖析
/**
* Sets the {@link ApplicationListener}s that will be applied to the SpringApplication
* and registered with the {@link ApplicationContext}.
* @param listeners the listeners to set
*/
public void setListeners(Collection<? extends ApplicationListener<?>> listeners) {
this.listeners = new ArrayList<>(listeners);
}
繼承了ApplicationListener()接口
源碼剖析
/**
* Interface to be implemented by application event listeners.
*
* <p>Based on the standard {@code java.util.EventListener} interface
* for the Observer design pattern.
*
* <p>As of Spring 3.0, an {@code ApplicationListener} can generically declare
* the event type that it is interested in. When registered with a Spring
* {@code ApplicationContext}, events will be filtered accordingly, with the
* listener getting invoked for matching event objects only.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @param <E> the specific {@code ApplicationEvent} subclass to listen to
* @see org.springframework.context.ApplicationEvent
* @see org.springframework.context.event.ApplicationEventMulticaster
* @see org.springframework.context.event.EventListener
*/
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
/**
* Handle an application event.
* @param event the event to respond to
*/
void onApplicationEvent(E event);
}
總結:
在這裏使用到了觀察者模式,有一個被觀察者和許多觀察者,當被觀察者的狀態發生改變時,要通知所有的觀察者做一些操作。
3. 推斷主應用入口類
源碼剖析
private Class<?> deduceMainApplicationClass() {
try {
// 構造一個異常類
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
// 通過main的棧幀推斷出入口類的名字
if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
}
三、run() 方法源碼剖析
源碼剖析
/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
// 記時器,統計應用啓動的時間
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 初始化應用上下文和異常報告集合
ConfigurableApplicationContext context = null;
// SpringBootExceptionReporter 是異常處理器,啓動的時候通過它把異常信息展示出來
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 設置系統屬性java.awt.headless的值,默認爲true
configureHeadlessProperty();
// 監聽器,SpringApplicationRunListeners實際上是一個集合
SpringApplicationRunListeners listeners = getRunListeners(args);
// 回調所有的獲取SpringApplicationRunListener.starting()方法
listeners.starting();
try {
// 初始化默認參數
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 準備 Spring 環境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 創建環境完成後回調,配置bean
configureIgnoreBeanInfo(environment);
// 打印器,springboot啓動的時候會打印springboot的標誌以及對應的版本
Banner printedBanner = printBanner(environment);
// 創建Spring應用上下文,來決定創建web的ioc還是普通的ioc
context = createApplicationContext();
// 實例化異常報告器
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
//準備上下文環境
// Spring上下文前置處理
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// prepareContext運行完成以後回調所有的SpringApplicationRunListener的contextLoaded();
// Spring上下文刷新,表示刷新完成,進行後續的一些操作
refreshContext(context);
// Spring上下文後置處理
afterRefresh(context, applicationArguments);
// 停止計時器
stopWatch.stop();
// 輸出日誌記錄的類名、時間信息
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 發佈應用上下文啓動完成事件
listeners.started(context);
// 執行所有 Runner 運行器
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 發佈應用上下文就緒事件
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
// 返回應用上下文
return context;
}
1. 開啓計時器
開啓計時器,用來統計應用啓動的時間
public void start() throws IllegalStateException {
// 傳入一個空字符串作爲當前任務的名稱
this.start("");
}
public void start(String taskName) throws IllegalStateException {
if (this.currentTaskName != null) {
// 如果當前任務名字不爲空,拋出異常
throw new IllegalStateException("Can't start StopWatch: it's already running");
} else {
// 否則,記錄當前任務的開始時間
this.currentTaskName = taskName;
this.startTimeNanos = System.nanoTime();
}
}
- 首先,傳入一個空字符串作爲當前任務的名稱
- 其次,判斷當前任務名是否空,如果爲空,則記錄當前應用啓動的開始時間
2. 設置系統屬性的值
系統屬性的值默認是true,系統屬性的值來源於System.getProperty()。
private void configureHeadlessProperty() {
System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,
System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
}
3. 監聽器
private SpringApplicationRunListeners getRunListeners(String[] args) {
// 類加載對應的監聽器
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
// 創建SpringApplicationRunListener實例
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}
4. 初始化默認參數
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
5.創建 Spring 環境
private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 獲取環境。如果存在就直接返回,否則先創建一個再返回
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置環境
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 準備監聽器環境
listeners.environmentPrepared(environment);
// 將環境綁定到SpringApplication上面
bindToSpringApplication(environment);
// 如果不是web應用環境,將環境轉換成StandardEnvironment
if (this.webApplicationType == WebApplicationType.NONE) {
environment = new EnvironmentConverter(getClassLoader())
.convertToStandardEnvironmentIfNecessary(environment);
}
ConfigurationPropertySources.attach(environment);
// 返回環境
return environment;
}
總結:
- 獲取環境。如果存在就直接返回,否則先創建一個再返回
- 配置環境
- 準備監聽器環境
- 將環境綁定到SpringApplication上面
- 如果不是web應用環境,將環境轉換成StandardEnvironment
- 最後返回環境
6. 打印器
springboot啓動的時候會打印springboot的標誌以及對應的版本
private Banner printBanner(ConfigurableEnvironment environment) {
if (this.bannerMode == Banner.Mode.OFF) {
return null;
}
ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
: new DefaultResourceLoader(getClassLoader());
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
if (this.bannerMode == Mode.LOG) {
return bannerPrinter.print(environment, this.mainApplicationClass, logger);
}
return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
7. 創建Spring應用上下文
protected ConfigurableApplicationContext createApplicationContext() {
// 首先進行判斷有沒有指定的實現類
Class<?> contextClass = this.applicationContextClass;
// 如果沒有,則根據應用類型選擇
if (contextClass == null) {
try {
// 根據webApplicationType的類型去反射創建ConfigurableApplicationContext的具體實例
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
// 通過反射,得到創建的對象
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
總結:
- 首先進行判斷有沒有指定的實現類; 如果沒有,則根據應用類型選擇;
- 根據webApplicationType的類型去反射創建ConfigurableApplicationContext的具體實例;
- 最後通過反射,得到創建的對象
對於Web應用,上下文類型是DEFAULT_WEB_CONTEXT_CLASS。
8. 實例化異常報告器
用 getSpringFactoriesInstances() 方法,獲取配置的異常類名稱,並實例化所有的異常類。
源碼剖析
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
// 使用名稱並確保唯一,以防止重複
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
9. Spring上下文前置處理
源碼剖析
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 給IOC容器設置一些環境屬性
context.setEnvironment(environment);
// 給IOC容器註冊一些組件
postProcessApplicationContext(context);
// 調用初始化方法
applyInitializers(context);
// 監聽器,觸發contextPrepared 事件
listeners.contextPrepared(context);
// 記錄啓動過程中的日誌
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
// 添加特定的單例beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
// 加載所有資源
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// 加載啓動類,將啓動類注入到容器中去
load(context, sources.toArray(new Object[0]));
// 觸發contextLoaded 事件
listeners.contextLoaded(context);
}
10. Spring上下文刷新
刷新完成以後,會進行後續的一些操作
源碼剖析
private void refreshContext(ConfigurableApplicationContext context) {
// 調用父類的refresh操作
refresh(context);
if (this.registerShutdownHook) {
try {
// 註冊一個關閉容器時的鉤子函數,在JVM關機的時候關閉這個上下文。
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
調用了registerShutdownHook()方法
/**
* Register a shutdown hook {@linkplain Thread#getName() named}
* {@code SpringContextShutdownHook} with the JVM runtime, closing this
* context on JVM shutdown unless it has already been closed at that time.
* <p>Delegates to {@code doClose()} for the actual closing procedure.
* @see Runtime#addShutdownHook
* @see ConfigurableApplicationContext#SHUTDOWN_HOOK_THREAD_NAME
* @see #close()
* @see #doClose()
*/
@Override
public void registerShutdownHook() {
if (this.shutdownHook == null) {
// No shutdown hook registered yet.
this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
@Override
public void run() {
synchronized (startupShutdownMonitor) {
// 調用doClose方法,進行容器銷燬時的清理工作
doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
11. Spring上下文後置處理
在Spring容器刷新上下文後進行調用,依次調用註冊的Runners。
/**
* Called after the context has been refreshed.
* @param context the application context
* @param args the application arguments
*/
protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
}
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
// CommandLineRunner、ApplicationRunner 這兩個接口,是在容器啓動成功後的最後一步進行回調
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
CommandLineRunner、ApplicationRunner 這兩個接口,是在容器啓動成功後的最後一步進行回調
12. 停止計時器
做計時監聽器停止操作,並統計一些任務執行信息
public void stop() throws IllegalStateException {
if (this.currentTaskName == null) {
throw new IllegalStateException("Can't stop StopWatch: it's not running");
} else {
long lastTime = System.nanoTime() - this.startTimeNanos;
this.totalTimeNanos += lastTime;
this.lastTaskInfo = new StopWatch.TaskInfo(this.currentTaskName, lastTime);
if (this.keepTaskList) {
this.taskList.add(this.lastTaskInfo);
}
++this.taskCount;
this.currentTaskName = null;
}
}
13. 發佈Spring上下文啓動完成事件
void started(ConfigurableApplicationContext context) {
for (SpringApplicationRunListener listener : this.listeners) {
listener.started(context);
}
}
14. 執行所有 Runner 運行器
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
15. 發佈Spring上下文就緒事件
void running(ConfigurableApplicationContext context) {
for (SpringApplicationRunListener listener : this.listeners) {
listener.running(context);
}
}
觸發所有 SpringApplicationRunListener 監聽器的 running 事件的方法。
由於水平有限,本博客難免有不足,懇請各位大佬不吝賜教!