SpringBoot——監(jiān)聽(tīng)器解析

監(jiān)聽(tīng)器模式

監(jiān)聽(tīng)器模式有要素

  • 事件
  • 監(jiān)聽(tīng)器
  • 廣播器
  • 觸發(fā)機(jī)制

系統(tǒng)監(jiān)聽(tīng)器

監(jiān)聽(tīng)器 ApplicationListener

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

    /**
     * Handle an application event.
     * @param event the event to respond to
     */
    void onApplicationEvent(E event);

}

FunctionalInterface是jdk8新增的,表示ApplicationListener接口只有一個(gè)方法,如果大于一個(gè)方法,就不能使用這注解

接口中有個(gè)泛型<E extends ApplicationEvent>,繼承自ApplicationEvent。就代表這實(shí)現(xiàn)這個(gè)接口時(shí),可以聲明對(duì)哪些事件(如ApplicationEvent)感興趣,在觸發(fā)監(jiān)聽(tīng)器的時(shí)候,對(duì)感興趣的事件進(jìn)行過(guò)濾。

系統(tǒng)廣播器ApplicationEventMulticaster接口

public interface ApplicationEventMulticaster {

    void addApplicationListener(ApplicationListener<?> listener);

    void addApplicationListenerBean(String listenerBeanName);

    void removeApplicationListener(ApplicationListener<?> listener);

    void removeApplicationListenerBean(String listenerBeanName);

    void removeAllListeners();

    void multicastEvent(ApplicationEvent event);

    void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType);

}

ApplicationEventMulticaster接口主要有三類方法,增加監(jiān)聽(tīng)器,刪除監(jiān)聽(tīng)器,廣播方法

系統(tǒng)事件,SpringBoot框架事件

SpringBoot中的事件發(fā)送順序

注冊(cè)監(jiān)聽(tīng)器(Listener)

public class SpringApplication {    
    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        ......
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
        //設(shè)置監(jiān)聽(tīng)器
        this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = this.deduceMainApplicationClass();
    }
}

我們還是跟進(jìn)代碼看看getSpringFactoriesInstances

public class SpringApplication {   
    // 這里的入?yún)ype是:org.springframework.context.ApplicationListener.class
    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
        return this.getSpringFactoriesInstances(type, new Class[0]);
    }

    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
        ClassLoader classLoader = this.getClassLoader();
        Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }
}

可以發(fā)現(xiàn),這個(gè)加載相應(yīng)的類名,然后完成實(shí)例化的過(guò)程和上面在設(shè)置初始化器時(shí)如出一轍,同樣,還是以spring-boot-autoconfigure這個(gè)包中的spring.factories為例,看看相應(yīng)的Key-Value:

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

這10個(gè)監(jiān)聽(tīng)器會(huì)貫穿springBoot整個(gè)生命周期。至此,對(duì)于SpringApplication實(shí)例的初始化過(guò)程就結(jié)束了。

完成了SpringApplication實(shí)例化,下面開(kāi)始調(diào)用run方法:

public ConfigurableApplicationContext run(String... args) {
    // 計(jì)時(shí)工具
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
    this.configureHeadlessProperty();
    // 第一步:獲取并啟動(dòng)監(jiān)聽(tīng)器
    SpringApplicationRunListeners listeners = this.getRunListeners(args);
    listeners.starting();

    Collection exceptionReporters;
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // 第二步:根據(jù)SpringApplicationRunListeners以及參數(shù)來(lái)準(zhǔn)備環(huán)境
        ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
        this.configureIgnoreBeanInfo(environment);
        // 準(zhǔn)備Banner打印器 - 就是啟動(dòng)Spring Boot的時(shí)候打印在console上的ASCII藝術(shù)字體
        Banner printedBanner = this.printBanner(environment);
        // 第三步:創(chuàng)建Spring容器
        context = this.createApplicationContext();
        exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
        // 第四步:Spring容器前置處理
        this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        // 第五步:刷新容器
        this.refreshContext(context);
        // 第六步:Spring容器后置處理
        this.afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
        }
        // 第七步:發(fā)出結(jié)束執(zhí)行的事件
        listeners.started(context);
        this.callRunners(context, applicationArguments);
    } catch (Throwable var10) {
        this.handleRunFailure(context, var10, exceptionReporters, listeners);
        throw new IllegalStateException(var10);
    }

    try {
        // 第八步:執(zhí)行Runners
        listeners.running(context);
        // 返回容器
        return context;
    } catch (Throwable var9) {
        this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
        throw new IllegalStateException(var9);
    }
}
  • 第一步:獲取并啟動(dòng)監(jiān)聽(tīng)器
  • 第二步:根據(jù)SpringApplicationRunListeners以及參數(shù)來(lái)準(zhǔn)備環(huán)境
  • 第三步:創(chuàng)建Spring容器
  • 第四步:Spring容器前置處理
  • 第五步:刷新容器
  • 第六步:Spring容器后置處理
  • 第七步:發(fā)出結(jié)束執(zhí)行的事件
  • 第八步:執(zhí)行Runners

這里主要分析監(jiān)聽(tīng)器相關(guān)的步驟

第一步:獲取并啟動(dòng)監(jiān)聽(tīng)器

獲取監(jiān)聽(tīng)器
跟進(jìn)getRunListeners方法:

private SpringApplicationRunListeners getRunListeners(String[] args) {
    Class<?>[] types = new Class[]{SpringApplication.class, String[].class};
    return new SpringApplicationRunListeners(logger, this.getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

這里仍然利用了getSpringFactoriesInstances方法來(lái)獲取實(shí)例,大家可以看看前面的這個(gè)方法分析,從META-INF/spring.factories中讀取Key為org.springframework.boot.SpringApplicationRunListener的Values:

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

getSpringFactoriesInstances中反射獲取實(shí)例時(shí)會(huì)觸發(fā)EventPublishingRunListener的構(gòu)造函數(shù),我們來(lái)看看EventPublishingRunListener的構(gòu)造函數(shù):

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
    private final SpringApplication application;
    private final String[] args;
    //廣播器
    private final SimpleApplicationEventMulticaster initialMulticaster;

    public EventPublishingRunListener(SpringApplication application, String[] args) {
        this.application = application;
        this.args = args;
        this.initialMulticaster = new SimpleApplicationEventMulticaster();
        Iterator var3 = application.getListeners().iterator();

        while(var3.hasNext()) {
            ApplicationListener<?> listener = (ApplicationListener)var3.next();
            //將上面設(shè)置到SpringApplication的十一個(gè)監(jiān)聽(tīng)器全部添加到SimpleApplicationEventMulticaster這個(gè)廣播器中
            this.initialMulticaster.addApplicationListener(listener);
        }
    }
    ......
}

我們看到EventPublishingRunListener里面有一個(gè)廣播器,EventPublishingRunListener 的構(gòu)造方法將SpringApplication的十一個(gè)監(jiān)聽(tīng)器全部添加到SimpleApplicationEventMulticaster這個(gè)廣播器中,我們來(lái)看看是如何添加到廣播器:

public abstract class AbstractApplicationEventMulticaster
        implements ApplicationEventMulticaster, BeanClassLoaderAware, BeanFactoryAware {
    //廣播器的父類中存放保存監(jiān)聽(tīng)器的內(nèi)部?jī)?nèi)
    private final ListenerRetriever defaultRetriever = new ListenerRetriever(false);

    ......

    @Override
    public void addApplicationListener(ApplicationListener<?> listener) {
        synchronized (this.retrievalMutex) {
            // Explicitly remove target for a proxy, if registered already,
            // in order to avoid double invocations of the same listener.
            Object singletonTarget = AopProxyUtils.getSingletonTarget(listener);
            if (singletonTarget instanceof ApplicationListener) {
                this.defaultRetriever.applicationListeners.remove(singletonTarget);
            }
            //內(nèi)部類對(duì)象
            this.defaultRetriever.applicationListeners.add(listener);
            this.retrieverCache.clear();
        }
    }

    private class ListenerRetriever {
        //保存所有的監(jiān)聽(tīng)器
        public final Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<>();

        public final Set<String> applicationListenerBeans = new LinkedHashSet<>();

        private final boolean preFiltered;

        public ListenerRetriever(boolean preFiltered) {
            this.preFiltered = preFiltered;
        }

        public Collection<ApplicationListener<?>> getApplicationListeners() {
            List<ApplicationListener<?>> allListeners = new ArrayList<>(
                    this.applicationListeners.size() + this.applicationListenerBeans.size());
            allListeners.addAll(this.applicationListeners);
            if (!this.applicationListenerBeans.isEmpty()) {
                BeanFactory beanFactory = getBeanFactory();
                for (String listenerBeanName : this.applicationListenerBeans) {
                    try {
                        ApplicationListener<?> listener = beanFactory.getBean(listenerBeanName, ApplicationListener.class);
                        if (this.preFiltered || !allListeners.contains(listener)) {
                            allListeners.add(listener);
                        }
                    }
                    catch (NoSuchBeanDefinitionException ex) {
                        // Singleton listener instance (without backing bean definition) disappeared -
                        // probably in the middle of the destruction phase
                    }
                }
            }
            if (!this.preFiltered || !this.applicationListenerBeans.isEmpty()) {
                AnnotationAwareOrderComparator.sort(allListeners);
            }
            return allListeners;
        }
    }
}

上述方法定義在SimpleApplicationEventMulticaster父類AbstractApplicationEventMulticaster中。關(guān)鍵代碼為this.defaultRetriever.applicationListeners.add(listener);,這是一個(gè)內(nèi)部類,用來(lái)保存所有的監(jiān)聽(tīng)器。也就是在這一步,將spring.factories中的監(jiān)聽(tīng)器傳遞到SimpleApplicationEventMulticaster中。我們現(xiàn)在知道EventPublishingRunListener中有一個(gè)廣播器SimpleApplicationEventMulticaster,SimpleApplicationEventMulticaster廣播器中又存放所有的監(jiān)聽(tīng)器。

啟動(dòng)監(jiān)聽(tīng)器

我們上面一步通過(guò)getRunListeners方法獲取的監(jiān)聽(tīng)器為EventPublishingRunListener,從名字可以看出是啟動(dòng)事件發(fā)布監(jiān)聽(tīng)器,主要用來(lái)發(fā)布啟動(dòng)事件。

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
    private final SpringApplication application;
    private final String[] args;
    private final SimpleApplicationEventMulticaster initialMulticaster;

我們先來(lái)看看SpringApplicationRunListener這個(gè)接口

public interface SpringApplicationRunListener {
    // 在run()方法開(kāi)始執(zhí)行時(shí),該方法就立即被調(diào)用,可用于在初始化最早期時(shí)做一些工作
    void starting();

    // 當(dāng)environment構(gòu)建完成,ApplicationContext創(chuàng)建之前,該方法被調(diào)用
    void environmentPrepared(ConfigurableEnvironment environment);

    // 當(dāng)ApplicationContext構(gòu)建完成時(shí),該方法被調(diào)用
    void contextPrepared(ConfigurableApplicationContext context);

    // 在ApplicationContext完成加載,但沒(méi)有被刷新前,該方法被調(diào)用
    void contextLoaded(ConfigurableApplicationContext context);

    // 在ApplicationContext刷新并啟動(dòng)后,CommandLineRunners和ApplicationRunner未被調(diào)用前,該方法被調(diào)用
    void started(ConfigurableApplicationContext context);

    // 在run()方法執(zhí)行完成前該方法被調(diào)用
    void running(ConfigurableApplicationContext context);

    // 當(dāng)應(yīng)用運(yùn)行出錯(cuò)時(shí)該方法被調(diào)用
    void failed(ConfigurableApplicationContext context, Throwable exception);
}

SpringApplicationRunListener接口在Spring Boot 啟動(dòng)初始化的過(guò)程中各種狀態(tài)時(shí)執(zhí)行,我們也可以添加自己的監(jiān)聽(tīng)器,在SpringBoot初始化時(shí)監(jiān)聽(tīng)事件執(zhí)行自定義邏輯,我們先來(lái)看看SpringBoot啟動(dòng)時(shí)第一個(gè)啟動(dòng)事件listeners.starting():

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
    public void starting() {
        //關(guān)鍵代碼,先創(chuàng)建application啟動(dòng)事件`ApplicationStartingEvent`
        this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
    }
}

這里先創(chuàng)建了一個(gè)啟動(dòng)事件ApplicationStartingEvent,我們繼續(xù)跟進(jìn)SimpleApplicationEventMulticaster,有個(gè)核心方法:

public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
    @Override
    public void multicastEvent(ApplicationEvent event) {
        multicastEvent(event, resolveDefaultEventType(event));
    }

    @Override
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
        //獲取線程池,如果為空則同步處理。這里線程池為空,還未沒(méi)初始化。
        Executor executor = getTaskExecutor();
        //通過(guò)事件類型ApplicationStartingEvent獲取對(duì)應(yīng)的監(jiān)聽(tīng)器
        for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
            if (executor != null) {
                //異步發(fā)送事件
                executor.execute(() -> invokeListener(listener, event));
            }
            else {
                //同步發(fā)送事件
                invokeListener(listener, event);
            }
        }
    }
}

這里會(huì)根據(jù)事件類型ApplicationStartingEvent獲取對(duì)應(yīng)的監(jiān)聽(tīng)器,在容器啟動(dòng)之后執(zhí)行響應(yīng)的動(dòng)作,有如下4種監(jiān)聽(tīng)器:


public class LoggingApplicationListener implements GenericApplicationListener {
    public void onApplicationEvent(ApplicationEvent event) {
        //在springboot啟動(dòng)的時(shí)候
        if (event instanceof ApplicationStartingEvent) {
            this.onApplicationStartingEvent((ApplicationStartingEvent)event);

            //springboot的Environment環(huán)境準(zhǔn)備完成的時(shí)候
        } else if (event instanceof ApplicationEnvironmentPreparedEvent) {
            this.onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent)event);

            //在springboot容器的環(huán)境設(shè)置完成以后
        } else if (event instanceof ApplicationPreparedEvent) {
            this.onApplicationPreparedEvent((ApplicationPreparedEvent)event);

            //容器關(guān)閉的時(shí)候
        } else if (event instanceof ContextClosedEvent && ((ContextClosedEvent)event).getApplicationContext().getParent() == null) {
            this.onContextClosedEvent();

            //容器啟動(dòng)失敗的時(shí)候
        } else if (event instanceof ApplicationFailedEvent) {
            this.onApplicationFailedEvent();
        }

    }
}

因?yàn)槲覀兊氖录愋蜑锳pplicationEvent,所以會(huì)執(zhí)行onApplicationStartedEvent((ApplicationStartedEvent) event);。springBoot會(huì)在運(yùn)行過(guò)程中的不同階段,發(fā)送各種事件,來(lái)執(zhí)行對(duì)應(yīng)監(jiān)聽(tīng)器的對(duì)應(yīng)方法。

第二步:環(huán)境構(gòu)建

ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);

跟進(jìn)去該方法:

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
    //獲取對(duì)應(yīng)的ConfigurableEnvironment
    ConfigurableEnvironment environment = this.getOrCreateEnvironment();
    //配置
    this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs());
    ConfigurationPropertySources.attach((Environment)environment);
    //發(fā)布環(huán)境已準(zhǔn)備事件,這是第二次發(fā)布事件
    listeners.environmentPrepared((ConfigurableEnvironment)environment);
    this.bindToSpringApplication((ConfigurableEnvironment)environment);
    if (!this.isCustomEnvironment) {
        environment = (new EnvironmentConverter(this.getClassLoader())).convertEnvironmentIfNecessary((ConfigurableEnvironment)environment, this.deduceEnvironmentClass());
    }

    ConfigurationPropertySources.attach((Environment)environment);
    return (ConfigurableEnvironment)environment;
}

來(lái)看一下getOrCreateEnvironment()方法,前面已經(jīng)提到,environment已經(jīng)被設(shè)置了servlet類型,所以這里創(chuàng)建的是環(huán)境對(duì)象是StandardServletEnvironment。

private ConfigurableEnvironment getOrCreateEnvironment() {
    if (this.environment != null) {
        return this.environment;
    } else {
        switch(this.webApplicationType) {
        case SERVLET:
            return new StandardServletEnvironment();
        case REACTIVE:
            return new StandardReactiveWebEnvironment();
        default:
            return new StandardEnvironment();
        }
    }
}

接下來(lái)看一下listeners.environmentPrepared(environment);,上面已經(jīng)提到了,這里是第二次發(fā)布事件。什么事件呢?來(lái)看一下根據(jù)事件類型獲取到的監(jiān)聽(tīng)器:



主要來(lái)看一下ConfigFileApplicationListener,該監(jiān)聽(tīng)器非常核心,主要用來(lái)處理項(xiàng)目配置。項(xiàng)目中的 properties 和yml文件都是其內(nèi)部類所加載。具體來(lái)看一下:



首先還是會(huì)去讀spring.factories 文件,List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();獲取的處理類有以下四種:
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor

在執(zhí)行完上述三個(gè)監(jiān)聽(tīng)器流程后,ConfigFileApplicationListener會(huì)執(zhí)行該類本身的邏輯。由其內(nèi)部類Loader加載項(xiàng)目制定路徑下的配置文件:

private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";

至此,項(xiàng)目的變量配置已全部加載完畢,來(lái)一起看一下:



這里一共7個(gè)配置文件,取值順序由上到下。也就是說(shuō)前面的配置變量會(huì)覆蓋后面同名的配置變量。項(xiàng)目配置變量的時(shí)候需要注意這點(diǎn)。

監(jiān)聽(tīng)事件觸發(fā)機(jī)制

獲取監(jiān)聽(tīng)器列表

通用觸發(fā)條件

自定義監(jiān)聽(tīng)器

實(shí)現(xiàn)方式一

  • 1、實(shí)現(xiàn)ApplicationListener接口
@Order(1)
public class Listener1 implements ApplicationListener<ApplicationStartedEvent> {

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        System.out.println("hello Listener1");
    }
}
  • 2、利用SPI機(jī)制在META-INF/spring.factories中添加配置項(xiàng)進(jìn)行注冊(cè)
org.springframework.context.ApplicationListener=com.yibo.source.code.listener.Listener1

實(shí)現(xiàn)方式二

  • 1、實(shí)現(xiàn)ApplicationListener接口
@Order(2)
public class Listener2 implements ApplicationListener<ApplicationStartedEvent> {

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        System.out.println("hello Listener2");
    }
}
  • 2、SpringApplication初始化后設(shè)置進(jìn)去
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(Application.class);
        springApplication.addListeners(new Listener2());
        springApplication.run();
    }
}

實(shí)現(xiàn)方式三

  • 1、實(shí)現(xiàn)ApplicationListener接口
@Order(3)
public class Listener3 implements ApplicationListener<ApplicationStartedEvent> {

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        System.out.println("hello Listener3");
    }
}
  • 2、appplication.properties內(nèi)填寫接口實(shí)現(xiàn)
context.listener.classes=com.yibo.source.code.listener.Listener3

實(shí)現(xiàn)方式四

  • 1、實(shí)現(xiàn)SmartApplicationListener接口
@Order(4)
public class Listener4 implements SmartApplicationListener{

    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        return ApplicationStartedEvent.class.isAssignableFrom(eventType) ||
                ApplicationPreparedEvent.class.isAssignableFrom(eventType);
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        System.out.println("hello Listener4");
    }
}
  • 2、需要重寫supportsEventType方法
  • 3、使用前三種方式注入框架

總結(jié)

  • 1、實(shí)現(xiàn)ApplicationListener接口針對(duì)單一事件監(jiān)聽(tīng)
  • 2、實(shí)現(xiàn)SmartApplicationListener接口針對(duì)多種事件監(jiān)聽(tīng)
  • 3、Order值越小越先執(zhí)行
  • 4、application.properties中定義的優(yōu)于其他方式

參考:
https://www.cnblogs.com/linlf03/p/12273052.html

https://www.cnblogs.com/java-chen-hao/p/11829344.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容