SpringBoot事件监听机制源码分析(上) SpringBoot源码(九)

简介: SpringBoot事件监听机制源码分析(上) SpringBoot源码(九)

SpringBoot中文注释项目Github地址:

https://github.com/yuanmabiji/spring-boot-2.1.0.RELEASE

本篇接 SpringApplication对象是如何构建的? SpringBoot源码(八)

1 温故而知新

温故而知新,我们来简单回顾一下上篇的内容,上一篇我们分析了SpringApplication对象的构建过程及SpringBoot自己实现的一套SPI机制,现将关键步骤再浓缩总结下:

  1. SpringApplication对象的构造过程其实就是给SpringApplication类的6个成员变量赋值;
  2. SpringBoot通过以下步骤实现自己的SPI机制:
  • 1)首先获取线程上下文类加载器;
  • 2)然后利用上下文类加载器从spring.factories配置文件中加载所有的SPI扩展实现类并放入缓存中;
  • 3)根据SPI接口从缓存中取出相应的SPI扩展实现类;
  • 4)实例化从缓存中取出的SPI扩展实现类并返回。

2 引言

在SpringBoot启动过程中,每个不同的启动阶段会分别广播不同的内置生命周期事件,然后相应的监听器会监听这些事件来执行一些初始化逻辑工作比如ConfigFileApplicationListener会监听onApplicationEnvironmentPreparedEvent事件来加载配置文件application.properties的环境变量等。

因此本篇内容将来分析下SpringBoot的事件监听机制的源码。

3 SpringBoot广播内置生命周期事件流程分析

为了探究SpringBoot广播内置生命周期事件流程,我们再来回顾一下SpringBoot的启动流程代码:

// SpringApplication.java

public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    configureHeadlessProperty();
    // 【0】新建一个SpringApplicationRunListeners对象用于发射SpringBoot启动过程中的生命周期事件
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // 【1】》》》》》发射【ApplicationStartingEvent】事件,标志SpringApplication开始启动
    listeners.starting();
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);
        // 【2】》》》》》发射【ApplicationEnvironmentPreparedEvent】事件,此时会去加载application.properties等配置文件的环境变量,同时也有标志环境变量已经准备好的意思
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
        configureIgnoreBeanInfo(environment);
        Banner printedBanner = printBanner(environment);
        context = createApplicationContext();
        exceptionReporters = getSpringFactoriesInstances(
                SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context); 
        // 【3】》》》》》发射【ApplicationContextInitializedEvent】事件,标志context容器被创建且已准备好
        // 【4】》》》》》发射【ApplicationPreparedEvent】事件,标志Context容器已经准备完成
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
        }
        // 【5】》》》》》发射【ApplicationStartedEvent】事件,标志spring容器已经刷新,此时所有的bean实例都已经加载完毕
        listeners.started(context);
        callRunners(context, applicationArguments);
    }
    // 【6】》》》》》发射【ApplicationFailedEvent】事件,标志SpringBoot启动失败
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }
    try {
        // 【7】》》》》》发射【ApplicationReadyEvent】事件,标志SpringApplication已经正在运行即已经成功启动,可以接收服务请求了。
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

可以看到SpringBoot在启动过程中首先会先新建一个SpringApplicationRunListeners对象用于发射SpringBoot启动过程中的各种生命周期事件,比如发射ApplicationStartingEvent,ApplicationEnvironmentPreparedEventApplicationContextInitializedEvent等事件,然后相应的监听器会执行一些SpringBoot启动过程中的初始化逻辑。那么,监听这些SpringBoot的生命周期事件的监听器们是何时被加载实例化的呢?还记得上篇文章在分析SpringApplication的构建过程吗?没错,这些执行初始化逻辑的监听器们正是在SpringApplication的构建过程中根据ApplicationListener接口去spring.factories配置文件中加载并实例化的。

3.1 为广播SpringBoot内置生命周期事件做前期准备

3.1.1 加载ApplicationListener监听器实现类

我们再来回顾下SpringApplication对象是如何构建的? SpringBoot源码(八)一文中讲到在构建SpringApplication对象时的setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));这句代码。

这句代码做的事情就是从spring.factories中加载出ApplicationListener事件监听接口的SPI扩展实现类然后添加到SpringApplication对象的listeners集合中,用于后续监听SpringBoot启动过程中的事件,来执行一些初始化逻辑工作。

SpringBoot启动时的具体监听器们都实现了ApplicationListener接口,其在spring.factories部分配置如下:

不过在调试时,会从所有的spring.factories配置文件中加载监听器,最终加载了10个监听器。如下图:

3.1.2 加载SPI扩展类EventPublishingRunListener

前面讲到,在SpringBoot的启动过程中首先会先新建一个SpringApplicationRunListeners对象用于发射SpringBoot启动过程中的生命周期事件,即我们现在来看下SpringApplicationRunListeners listeners = getRunListeners(args);这句代码:

// SpringApplication.java

private SpringApplicationRunListeners getRunListeners(String[] args) {
    // 构造一个由SpringApplication.class和String[].class组成的types
    Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
    // 1) 根据SpringApplicationRunListener接口去spring.factories配置文件中加载其SPI扩展实现类
    // 2) 构建一个SpringApplicationRunListeners对象并返回
    return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
            SpringApplicationRunListener.class, types, this, args));
}

我们将重点放到`getSpringFactoriesInstances(

        SpringApplicationRunListener.class, types, this, args)`这句代码,`getSpringFactoriesInstances`这个方法我们已经很熟悉,在上一篇分析SpringBoot的SPI机制时已经详细分析过这个方法。可以看到SpringBoot此时又是根据`SpringApplicationRunListener`这个SPI接口去`spring.factories`中加载相应的SPI扩展实现类,我们直接去`spring.factories`中看看`SpringApplicationRunListener`有哪些SPI实现类:
        


由上图可以看到,SpringApplicationRunListener只有EventPublishingRunListener这个SPI实现类
EventPublishingRunListener这个哥们在SpringBoot的启动过程中尤其重要,由其在SpringBoot启动过程的不同阶段发射不同的SpringBoot的生命周期事件,SpringApplicationRunListeners对象没有承担广播事件的职责,而最终是委托EventPublishingRunListener这个哥们来广播事件的。

因为从spring.factories中加载EventPublishingRunListener类后还会实例化该类,那么我们再跟进EventPublishingRunListener的源码,看看其是如何承担发射SpringBoot生命周期事件这一职责的?

// EventPublishingRunListener.java

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {

    private final SpringApplication application;

    private final String[] args;
    /**
     * 拥有一个SimpleApplicationEventMulticaster事件广播器来广播事件
     */
    private final SimpleApplicationEventMulticaster initialMulticaster;

    public EventPublishingRunListener(SpringApplication application, String[] args) {
        this.application = application;
        this.args = args;
        // 新建一个事件广播器SimpleApplicationEventMulticaster对象
        this.initialMulticaster = new SimpleApplicationEventMulticaster();
        // 遍历在构造SpringApplication对象时从spring.factories配置文件中获取的事件监听器
        for (ApplicationListener<?> listener : application.getListeners()) {
            // 将从spring.factories配置文件中获取的事件监听器们添加到事件广播器initialMulticaster对象的相关集合中
            this.initialMulticaster.addApplicationListener(listener);
        }
    }

    @Override
    public int getOrder() {
        return 0;
    }
    // 》》》》》发射【ApplicationStartingEvent】事件
    @Override
    public void starting() {
        this.initialMulticaster.multicastEvent(
                new ApplicationStartingEvent(this.application, this.args));
    }
    // 》》》》》发射【ApplicationEnvironmentPreparedEvent】事件
    @Override
    public void environmentPrepared(ConfigurableEnvironment environment) {
        this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(
                this.application, this.args, environment));
    }
    // 》》》》》发射【ApplicationContextInitializedEvent】事件
    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        this.initialMulticaster.multicastEvent(new ApplicationContextInitializedEvent(
                this.application, this.args, context));
    }
    // 》》》》》发射【ApplicationPreparedEvent】事件
    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        for (ApplicationListener<?> listener : this.application.getListeners()) {
            if (listener instanceof ApplicationContextAware) {
                ((ApplicationContextAware) listener).setApplicationContext(context);
            }
            context.addApplicationListener(listener);
        }
        this.initialMulticaster.multicastEvent(
                new ApplicationPreparedEvent(this.application, this.args, context));
    }
    // 》》》》》发射【ApplicationStartedEvent】事件
    @Override
    public void started(ConfigurableApplicationContext context) {
        context.publishEvent(
                new ApplicationStartedEvent(this.application, this.args, context));
    }
    // 》》》》》发射【ApplicationReadyEvent】事件
    @Override
    public void running(ConfigurableApplicationContext context) {
        context.publishEvent(
                new ApplicationReadyEvent(this.application, this.args, context));
    }
    // 》》》》》发射【ApplicationFailedEvent】事件
    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
        ApplicationFailedEvent event = new ApplicationFailedEvent(this.application,
                this.args, context, exception);
        if (context != null && context.isActive()) {
            // Listeners have been registered to the application context so we should
            // use it at this point if we can
            context.publishEvent(event);
        }
        else {
            // An inactive context may not have a multicaster so we use our multicaster to
            // call all of the context's listeners instead
            if (context instanceof AbstractApplicationContext) {
                for (ApplicationListener<?> listener : ((AbstractApplicationContext) context)
                        .getApplicationListeners()) {
                    this.initialMulticaster.addApplicationListener(listener);
                }
            }
            this.initialMulticaster.setErrorHandler(new LoggingErrorHandler());
            this.initialMulticaster.multicastEvent(event);
        }
    }
    
    // ...省略非关键代码
}

可以看到EventPublishingRunListener类实现了SpringApplicationRunListener接口,SpringApplicationRunListener接口定义了SpringBoot启动时发射生命周期事件的接口方法,而EventPublishingRunListener类正是通过实现SpringApplicationRunListener接口的starting,environmentPreparedcontextPrepared等方法来广播SpringBoot不同的生命周期事件,我们直接看下SpringApplicationRunListener接口源码好了:

// SpringApplicationRunListener.java

public interface SpringApplicationRunListener {

    void starting();

    void environmentPrepared(ConfigurableEnvironment environment);

    void contextPrepared(ConfigurableApplicationContext context);

    void contextLoaded(ConfigurableApplicationContext context);

    void started(ConfigurableApplicationContext context);

    void running(ConfigurableApplicationContext context);

    void failed(ConfigurableApplicationContext context, Throwable exception);
}

我们再接着分析EventPublishingRunListener这个类,可以看到其有一个重要的成员属性initialMulticaster,该成员属性是SimpleApplicationEventMulticaster类对象,该类正是承担了广播SpringBoot启动时生命周期事件的职责,EventPublishingRunListener对象没有承担广播事件的职责,而最终是委托SimpleApplicationEventMulticaster这个哥们来广播事件的。EventPublishingRunListener的源码中也可以看到在starting,environmentPreparedcontextPrepared等方法中也正是通过调用SimpleApplicationEventMulticaster类对象的multicastEvent方法来广播事件的。

思考 SpringBoot启动过程中发射事件时事件广播者是层层委托职责的,起初由SpringApplicationRunListeners对象承担,然后SpringApplicationRunListeners对象将广播事件职责委托给EventPublishingRunListener对象,最终EventPublishingRunListener对象将广播事件的职责委托给SimpleApplicationEventMulticaster对象。为什么要层层委托这么做呢? 这个值得大家思考。

前面讲到从spring.factories中加载出EventPublishingRunListener类后会实例化,而实例化必然会通过EventPublishingRunListener的构造函数来进行实例化,因此我们接下来分析下EventPublishingRunListener的构造函数源码:

// EventPublishingRunListener.java

public EventPublishingRunListener(SpringApplication application, String[] args) {
    this.application = application;
    this.args = args;
    // 新建一个事件广播器SimpleApplicationEventMulticaster对象
    this.initialMulticaster = new SimpleApplicationEventMulticaster();
    // 遍历在构造SpringApplication对象时从spring.factories配置文件中获取的事件监听器
    for (ApplicationListener<?> listener : application.getListeners()) {
        // 将从spring.factories配置文件中获取的事件监听器们添加到事件广播器initialMulticaster对象的相关集合中
        this.initialMulticaster.addApplicationListener(listener);
    }
}

可以看到在EventPublishingRunListener的构造函数中有一个for循环会遍历之前从spring.factories中加载的监听器们,然后添加到集合中缓存起来,用于以后广播各种事件时直接从这个集合中取出来即可,而不用再去spring.factories中加载,提高效率。

3.2 广播SpringBoot的内置生命周期事件

spring.factories配置文件中加载并实例化EventPublishingRunListener对象后,那么在在SpringBoot的启动过程中会发射一系列SpringBoot内置的生命周期事件,我们再来回顾下SpringBoot启动过程中的源码:

// SpringApplication.java

public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    configureHeadlessProperty();
    // 【0】新建一个SpringApplicationRunListeners对象用于发射SpringBoot启动过程中的生命周期事件
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // 【1】》》》》》发射【ApplicationStartingEvent】事件,标志SpringApplication开始启动
    listeners.starting();
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);
        // 【2】》》》》》发射【ApplicationEnvironmentPreparedEvent】事件,此时会去加载application.properties等配置文件的环境变量,同时也有标志环境变量已经准备好的意思
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
        configureIgnoreBeanInfo(environment);
        Banner printedBanner = printBanner(environment);
        context = createApplicationContext();
        exceptionReporters = getSpringFactoriesInstances(
                SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context); 
        // 【3】》》》》》发射【ApplicationContextInitializedEvent】事件,标志context容器被创建且已准备好
        // 【4】》》》》》发射【ApplicationPreparedEvent】事件,标志Context容器已经准备完成
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
        }
        // 【5】》》》》》发射【ApplicationStartedEvent】事件,标志spring容器已经刷新,此时所有的bean实例都已经加载完毕
        listeners.started(context);
        callRunners(context, applicationArguments);
    }
    // 【6】》》》》》发射【ApplicationFailedEvent】事件,标志SpringBoot启动失败
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }
    try {
        // 【7】》》》》》发射【ApplicationReadyEvent】事件,标志SpringApplication已经正在运行即已经成功启动,可以接收服务请求了。
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

可以看到在SpringBoot的启动过程中总共会发射7种不同类型的生命周期事件,来标志SpringBoot的不同启动阶段,同时,这些生命周期事件的监听器们也会执行一些启动过程中的初始化逻辑,关于这些监听器的初始化逻辑将在下一篇内容中会分析。以下是SpringBoot启动过程中要发射的事件类型,其中ApplicationFailedEvent在SpringBoot启动过程中遇到异常才会发射:

  1. ApplicationStartingEvent
  2. ApplicationEnvironmentPreparedEvent
  3. ApplicationContextInitializedEvent
  4. ApplicationPreparedEvent
  5. ApplicationStartedEvent
  6. ApplicationFailedEvent
  7. ApplicationReadyEvent

我们以listeners.starting();这句代码为例,看看EventPublishingRunListener对象发射事件的源码:

// SpringApplicationRunListeners.java

public void starting() {
    // 遍历listeners集合,这里实质取出的就是刚才从spring.factories中取出的SPI实现类EventPublishingRunListener
    // 而EventPublishingRunListener对象承担了SpringBoot启动过程中负责广播不同的生命周期事件
    for (SpringApplicationRunListener listener : this.listeners) {
            // 调用EventPublishingRunListener的starting方法来广播ApplicationStartingEvent事件
        listener.starting();
    }
}

继续跟进listener.starting();的源码:

EventPublishingRunListener.java

// 》》》》》发射【ApplicationStartingEvent】事件
public void starting() {
    // EventPublishingRunListener对象将发布ApplicationStartingEvent这件事情委托给了initialMulticaster对象
    // 调用initialMulticaster的multicastEvent方法来发射ApplicationStartingEvent事件
    this.initialMulticaster.multicastEvent(
            new ApplicationStartingEvent(this.application, this.args));
}

可以看到,EventPublishingRunListener对象将发布ApplicationStartingEvent这件事情委托给了SimpleApplicationEventMulticaster对象initialMulticaster,
,而initialMulticaster对象最终会调用其multicastEvent方法来发射ApplicationStartingEvent事件。关于SimpleApplicationEventMulticaster类如何广播事件,笔者已经在Spring是如何实现事件监听机制的? Spring源码(二)这篇文章已经详细分析,这里不再赘述。

关于SpringBoot启动过程中发射其他生命周期事件的源码这里不再分析

4 SpringBoot的内置生命周期事件总结

好了,前面已经分析了SpringBoot启动过程中要发射的各种生命周期事件,下面列一个表格总结下:

5 小结

SpringBoot启动过程中广播生命周期事件的源码分析就到此结束了,下一篇会继续介绍监听这些生命周期事件的监听器们。我们再回顾本篇内容总结下关键点:

SpringBoot启动过程中会发射7种类型的生命周期事件,标志不同的启动阶段,然后相应的监听器会监听这些事件来执行一些初始化逻辑工作;

【源码笔记】Github源码分析项目上线啦!!!下面是笔记的Github地址:

https://github.com/yuanmabiji/Java-SourceCode-Blogs

点赞和转发是对笔者最大的激励哦!

相关文章
|
前端开发 Java 关系型数据库
基于Java+Springboot+Vue开发的鲜花商城管理系统源码+运行
基于Java+Springboot+Vue开发的鲜花商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的鲜花商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。技术学习共同进步
904 7
|
JavaScript 前端开发 Java
制造业ERP源码,工厂ERP管理系统,前端框架:Vue,后端框架:SpringBoot
这是一套基于SpringBoot+Vue技术栈开发的ERP企业管理系统,采用Java语言与vscode工具。系统涵盖采购/销售、出入库、生产、品质管理等功能,整合客户与供应商数据,支持在线协同和业务全流程管控。同时提供主数据管理、权限控制、工作流审批、报表自定义及打印、在线报表开发和自定义表单功能,助力企业实现高效自动化管理,并通过UniAPP实现移动端支持,满足多场景应用需求。
1318 1
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
789 70
|
供应链 JavaScript BI
ERP系统源码,基于SpringBoot+Vue+ElementUI+UniAPP开发
这是一款专为小微企业打造的 SaaS ERP 管理系统,基于 SpringBoot+Vue+ElementUI+UniAPP 技术栈开发,帮助企业轻松上云。系统覆盖进销存、采购、销售、生产、财务、品质、OA 办公及 CRM 等核心功能,业务流程清晰且操作简便。支持二次开发与商用,提供自定义界面、审批流配置及灵活报表设计,助力企业高效管理与数字化转型。
997 2
ERP系统源码,基于SpringBoot+Vue+ElementUI+UniAPP开发
|
机器学习/深度学习 数据采集 人机交互
springboot+redis互联网医院智能导诊系统源码,基于医疗大模型、知识图谱、人机交互方式实现
智能导诊系统基于医疗大模型、知识图谱与人机交互技术,解决患者“知症不知病”“挂错号”等问题。通过多模态交互(语音、文字、图片等)收集病情信息,结合医学知识图谱和深度推理,实现精准的科室推荐和分级诊疗引导。系统支持基于规则模板和数据模型两种开发原理:前者依赖人工设定症状-科室规则,后者通过机器学习或深度学习分析问诊数据。其特点包括快速病情收集、智能病症关联推理、最佳就医推荐、分级导流以及与院内平台联动,提升患者就诊效率和服务体验。技术架构采用 SpringBoot+Redis+MyBatis Plus+MySQL+RocketMQ,确保高效稳定运行。
977 0
|
小程序 Java 关系型数据库
weixin117新闻资讯系统设计+springboot(文档+源码)_kaic
本文介绍了一款基于微信小程序的新闻资讯系统,涵盖其开发全过程。该系统采用Java的SSM框架进行后台管理开发,使用MySQL作为本地数据库,并借助微信开发者工具确保稳定性。管理员可通过个人中心、用户管理等功能模块实现高效管理,而用户则能注册登录并查看新闻与视频内容。系统设计注重可行性分析(技术、经济、操作),强调安全性与数据完整性,界面简洁易用,功能全面,极大提升了信息管理效率及用户体验。关键词包括基于微信小程序的新闻资讯系统、SSM框架和MYSQL数据库。
|
小程序 JavaScript Java
基于SpringBoot的智慧停车场微信小程序源码分享
智慧停车场微信小程序主要包含管理端和小程序端。管理端包括停车场管理,公告信息管理,用户信息管理,预定信息管理,用户反馈管理等功能。小程序端包括登录注册,预约停车位,停车导航,停车缴费,用户信息,车辆信息,钱包充值,意见反馈等功能。
1205 5
基于SpringBoot的智慧停车场微信小程序源码分享
|
JavaScript Java 测试技术
基于SpringBoot+Vue实现的留守儿童爱心网站设计与实现(计算机毕设项目实战+源码+文档)
博主是一位全网粉丝超过100万的CSDN特邀作者、博客专家,专注于Java、Python、PHP等技术领域。提供SpringBoot、Vue、HTML、Uniapp、PHP、Python、NodeJS、爬虫、数据可视化等技术服务,涵盖免费选题、功能设计、开题报告、论文辅导、答辩PPT等。系统采用SpringBoot后端框架和Vue前端框架,确保高效开发与良好用户体验。所有代码由博主亲自开发,并提供全程录音录屏讲解服务,保障学习效果。欢迎点赞、收藏、关注、评论,获取更多精品案例源码。
|
JavaScript Java 测试技术
基于SpringBoot+Vue实现的家政服务管理平台设计与实现(计算机毕设项目实战+源码+文档)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
|
JavaScript 搜索推荐 Java
基于SpringBoot+Vue实现的家乡特色推荐系统设计与实现(源码+文档+部署)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!