多SpringBoot项目同进程下统一启动

news2024/12/24 7:42:33

1.背景

使用SpringBoot技术栈进行REST HTTP接口开发服务时,一般来说如果模块较多或者涉及多人协作开发,大家会不自觉的将每个模块独立成一个单独的项目进行开发,部署时则将每个服务进行单独部署和运行。服务间的调用则通过FeignClients,服务的接入、负载、路由则是在前面摆个SpringCloud Gateway,同时服务注册/发现、配置则使用一个统一的Nacos。这样做好处显而易见,例如:开发时的代码冲突及分支合并、运行时系统资源分配及性能优化等都不打架、对某个服务的扩缩容也方便、K8S容器化也方便。这些对于公有云来说确实就应该这样搞,但是假如哪天要私有化售卖和交付的话,这样又问题颇多:一个服务一个K8S容器势必导致服务器数量的增加、私有自动化部署成本大等。那么面对这样的情况,最好的方式无非是:单独部署和统一集成部署双支持。想要的结果现在已经很明确了,可是咋搞呢?现在已经这样了,难道把所有分散的子项目合并到一个项目下,然后用一个SpringApplication.run()去启动?那么问题来了:之前是N个服务,现在是一个服务,人肉合并代码工作量大,Nacos中的一堆配置要改要整合等;那么如果能让nacos中的配置保持不变,各个服务代码不变或者很少变动则一种比较合适的方案。

2. 实现方案

基于上面的背景和前提,这里给出一个具体的实例,出于时间问题,我就不写一个完成的Demo了。方法和套路懂了,自行就能写出测试验证Demo。

2.1 Nacos服务注册说明

统一启动服务通过使用 SpringApplicationBuilder 启动了多个独立的 Spring Boot 应用实例,但由于它们共享了同一个 JVM 环境,而Nacos 客户端使用全局配置单例,这样会导致多个服务实例在同一进程中运行时,它们共享相同的 Nacos 客户端,后面的服务实例在注册时会覆盖前面的注册信息,从而导致只注册了一个服务。为了解决这个问题需要在 SpringApplicationBuilder 中为每个实例设置不同的 Nacos 配置前缀来隔离它们的注册信息,这样它们在 Nacos 中会被识别为独立的服务实例,从而避免了注册信息的覆盖问题。同样的子服务中的代码如果使用了@Value(“${spring.application.name}”)的方式获取服务名,统一聚合启动时同样有覆盖问题。

2.2 子服务约束

  1. 为避免统一集成部署时jar包冲突问题,要求所有子服务相关依赖的版本必须使用根POM中统一定义的版本(当前我们各个子服务就是这样做的);// 之前各个子服务独立运行肯定不会有问题,当所集成到一个进程下运行之后,如果某些依赖的版本不一致,那么就会出现jar包冲突问题。
  2. 所有子服务的根包名相对统一,例如,都是com.china.xxx(这一点一般来说都满足,毕竟依赖管理中的groupId是重要标识);所有子服务的SpringBootApplication启动类增加一个自定义注解用于进行子服务启动类的发现。 // 这条是可选的,只我不想在统一启动服务中去做一个配置,于是选择了用反射扫描的方式去进行子服务启动类发现的方式。
  3. 所有子服务必须打原包,不能使用SpringBoot 的"FAT JAR"方式打包(skip掉spring-boot-maven-plugin的repackage即可;);spring-boot-maven-plugin 是 Spring Boot 提供的一个插件,用于简化 Maven 项目中的构建、打包和运行过程。它默认会执行一个名为 repackage 的任务,将项目的 JAR 重新打包成一个包含所有依赖的可执行 JAR(也称为 Fat JAR 或 Uber JAR)。Fat JAR中的SpringBootApplication启动类不太好直接拿到; // 反正我们的子项目都用了,因此要增加这条约束。
  4. 代码中禁止通过@Value(“${spring.application.name}”)获取服务名(解决方案:服务名都是固定的,定义一个常量即可;);

2.3 统一启动服务实现思路

  1. 引用所有子服务的JAR包;
  2. 使用Reflections.getTypesAnnotatedWith的扫描方式获取所有子服务的SpringBootApplication启动类;同时使用VM参数支持子服务的In和Out的配置。
  3. 使用SpringApplicationBuilder分别启动各个子服务,同时为每个子服务创建的一个新的 ConfigurableApplicationContext 实例,以确保每个服务都在独立的上下文中运行;
  4. 统一启动服务通过使用 SpringApplicationBuilder 启动了多个独立的 Spring Boot 应用实例,但由于它们共享了同一个 JVM 环境,而Nacos 客户端使用全局配置单例,这样会导致多个服务实例在同一进程中运行时,它们共享相同的 Nacos 客户端,后面的服务实例在注册时会覆盖前面的注册信息,从而导致只注册了一个服务。为了解决这个问题需要在 SpringApplicationBuilder 中为每个实例设置不同的 Nacos 配置前缀来隔离它们的注册信息,这样它们在 Nacos 中会被识别为独立的服务实例,从而避免了注册信息的覆盖问题。
  5. 增加Shutdown钩子以确保创建的ApplicationContext 实例可以被优雅的关闭;

2. 主要实现

2.1 项目结构

项目结构大致如下,一共3个项目:bw-server-all是统一启动服务项目,bw-job和bw-ai-app是2个独立的子服务项目;

--统一启动服务:bw-server-all 
├── /bw-server-all 
│   ├── /src
│   │   ├── /main
│   │   │   ├── /java
│   │   │   │   └── /com
│   │   │   │       └── beam
│   │   │   │           └── work
│   │   │   │               └── server
│   │   │   │                   └── all
│   │   │   │                       └── boot
│   │   │   │                           └── Bootstrap.java  // 统一启动类
│   │   │   └── /resources
│   │   │       └── application.yml
│   │   └── /test
│   │       └── ...
│   └── pom.xml

--子服务1:bw-job
├── /bw-job
│   ├── /src
│   │   ├── /main
│   │   │   └── /java
│   │   │       └── /com
│   │   │           └── beam
│   │   │               └── job
│   │   │                   └── provider
│   │   │                       └── JobApplicationRun.java  // 启动类
│   │   └── /resources
│   │       └── application.yml
│   └── pom.xml

--子服务2:bw-ai-app
└── /bw-ai-app
    ├── /src
    │   ├── /main
    │   │   └── /java
    │   │       └── /com
    │   │           └── beam
    │   │               └── ai
    │   │                   └── app
    │   │                       └── provider
    │   │                           └── ApplicationBootstrap.java  // 启动类
    │   └── /resources
    │       └── application.yml
    └── pom.xml 

2.2 启动类扫描自定义注解

/**
 * BwApplication
 *
 * @author chenx
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BwApplication {

    /**
     * service name
     */
    String name() default "";

    /**
     * contextId:default: name + "-context"
     */
    String contextId() default "";
}

2.3 子服务启动类示例

在这里插入图片描述

2.4 子服务打包示例

在这里插入图片描述

2.4 统一启动服务实现

2.4.1 引用所有子服务的JAR包

在这里插入图片描述

2.4.2 BootstrapHelper实现

package com.beam.work.server.all.boot;

import com.umbrella.work.common.annotation.BwApplication;
import com.umbrella.work.common.exception.BeemRuntimeException;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.reflections.Reflections;
import org.reflections.scanners.Scanners;
import org.reflections.util.ConfigurationBuilder;
import org.slf4j.Logger;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.core.env.StandardEnvironment;

import java.io.PrintStream;
import java.util.*;

/**
 * BootstrapHelper
 *
 * @author chenx
 */
public class BootstrapHelper {

    private static final String[] SCAN_BASE_PACKAGES = {"com.beam", "com.umbrella.work", "com.beem"};

    private BootstrapHelper() {
        // do nothing
    }

    /**
     * getApplications
     *
     * @param appIn
     * @param appOut
     * @param nacosGroup
     * @return
     */
    public static Map<String, SpringApplicationBuilder> getApplications(String appIn, String appOut, String nacosGroup) {
        if (StringUtils.isEmpty(nacosGroup)) {
            throw new BeemRuntimeException("nacosGroup is empty!");
        }

        Set<String> in = getAppSet(appIn);
        Set<String> out = getAppSet(appOut);

        Reflections reflections = new Reflections(new ConfigurationBuilder()
                .forPackages(SCAN_BASE_PACKAGES)
                .addScanners(Scanners.TypesAnnotated));
        Set<Class<?>> annotatedClasses = reflections.getTypesAnnotatedWith(BwApplication.class);

        Map<String, SpringApplicationBuilder> map = new HashMap<>(annotatedClasses.size());
        for (Class<?> clazz : annotatedClasses) {
            BwApplication annotation = clazz.getAnnotation(BwApplication.class);
            String appName = annotation.name().toLowerCase();
            if (!isLoadApplication(appName, in, out)) {
                continue;
            }

            String contextId = StringUtils.isEmpty(annotation.contextId()) ? appName + "-context" : annotation.contextId();
            SpringApplicationBuilder builder = new SpringApplicationBuilder();
            builder.sources(clazz)
                    .environment(new StandardEnvironment())
                    .properties("spring.application.name=" + appName
                            , "spring.main.application-context-id=" + contextId
                            , "spring.main.allow-bean-definition-overriding=true"
                            , "spring.cloud.nacos.discovery.group=" + nacosGroup
                            , "spring.cloud.nacos.discovery.service=" + appName
                    )
                    .web(WebApplicationType.SERVLET);

            map.putIfAbsent(appName, builder);
        }

        return map;
    }

    /**
     * printSeparatedLog
     *
     * @param logger
     * @param info
     */
    public static void printSeparatedLog(Logger logger, String info) {
        if (Objects.isNull(logger)) {
            return;
        }

        String separator = getSeparator(info);
        logger.info(separator);
        logger.info(info);
        logger.info(separator);
    }

    /**
     * getSeparator
     *
     * @param info
     * @return
     */
    private static String getSeparator(String info) {
        if (StringUtils.isEmpty(info)) {
            return "";
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < info.length(); i++) {
            sb.append("=");
        }

        return sb.toString();
    }

    /**
     * getAppSet
     */
    private static Set<String> getAppSet(String apps) {
        if (StringUtils.isEmpty(apps)) {
            return Collections.emptySet();
        }

        Set<String> set = new HashSet<>();
        String[] array = apps.split(",");
        for (String entry : array) {
            String appName = entry.toLowerCase();
            if (StringUtils.isEmpty(appName) || set.contains(appName)) {
                continue;
            }

            set.add(appName);
        }

        return set;
    }

    /**
     * isLoadApplication
     */
    private static boolean isLoadApplication(String appName, Set<String> in, Set<String> out) {
        if (StringUtils.isEmpty(appName)) {
            return false;
        }

        if (CollectionUtils.isEmpty(in) && CollectionUtils.isEmpty(out)) {
            return true;
        }

        // APP_IN优先
        if (!CollectionUtils.isEmpty(in)) {
            return in.contains(appName.toLowerCase());
        }

        if (!CollectionUtils.isEmpty(out)) {
            return !out.contains(appName.toLowerCase());
        }

        return true;
    }

    /**
     * BootstrapBanners
     */
    public enum BootstrapBanners {

        START(new String[]{
                " ######  ########    ###    ########  ######## ",
                "##    ##    ##      ## ##   ##     ##    ##    ",
                "##          ##     ##   ##  ##     ##    ##    ",
                " ######     ##    ##     ## ########     ##    ",
                "      ##    ##    ######### ##   ##      ##    ",
                "##    ##    ##    ##     ## ##    ##     ##    ",
                " ######     ##    ##     ## ##     ##    ##    "}),
        FAIL(new String[]{
                " _______    ___       __   __      ",
                "|   ____|  /   \\     |  | |  |     ",
                "|  |__    /  ^  \\    |  | |  |     ",
                "|   __|  /  /_\\  \\   |  | |  |     ",
                "|  |    /  _____  \\  |  | |  `----.",
                "|__|   /__/     \\__\\ |__| |_______|"}),
        ;

        private final String[] banner;

        BootstrapBanners(String[] banner) {
            this.banner = banner;
        }

        /**
         * printBanner
         *
         * @param logger
         */
        public void printBanner(Logger logger) {
            if (this.banner != null) {
                for (String line : this.banner) {
                    logger.warn(line);
                }
            }
        }

        /**
         * printBanner
         *
         * @param out
         */
        public void printBanner(PrintStream out) {
            if (this.banner != null) {
                for (String line : this.banner) {
                    out.println(line);
                }
            }
        }
    }
}

2.4.3 Bootstrap实现

package com.beam.work.server.all.boot;

import com.umbrella.work.common.exception.BeemRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.HashMap;
import java.util.Map;

/**
 * Bootstrap
 *
 * @author chenx
 */
@Slf4j
public class Bootstrap {

    private static final String PROPERTY_KEY_APP_IN = "APP_IN";
    private static final String PROPERTY_KEY_APP_OUT = "APP_OUT";
    private static final String PROPERTY_KEY_NACOS_GROUP = "NACOS_GROUP";

    /**
     * VM参数说明:
     * 1. APP_IN(可选): 要启动的服务(多个服务逗号分割);
     * 2. APP_OUT(可选): 不要启动的服务(多个服务逗号分割);
     * 3. NACOS_GROUP(必须):Nacos组名,对应根POM中profile.nacos.group(由于统一启动服务不是Springboot项目因此这里走VM参数配置);
     * 4. 某个服务同时存在于APP_IN和APP_OUT时APP_IN优先;
     * 5. 示例:-DAPP_IN=bw-job,bw-ai-app -DNACOS_GROUP=bw-dev
     * <p>
     * 服务发现机制:
     * 1. 扫SCAN_BASE_PACKAGES下所有含有@BwApplication注解的Springboot启动类;
     * 2. BwApplication.name():服务名称(为空则忽略该服务启动类);
     * 3. BwApplication.contextId():每个子服务的启动都使用 SpringApplicationBuilder 创建的一个新的 ConfigurableApplicationContext 实例,以确保每个服务都在独立的上下文中运行;
     * <p>
     * Nacos服务注册说明:
     * 统一启动服务通过使用 SpringApplicationBuilder 启动了多个独立的 Spring Boot 应用实例,但由于它们共享了同一个 JVM 环境,
     * 而Nacos 客户端使用全局配置单例,这样会导致多个服务实例在同一进程中运行时,它们共享相同的 Nacos 客户端,后面的服务实例在注册时会覆盖前面的注册信息,从而导致只注册了一个服务。
     * 为了解决这个问题需要在 SpringApplicationBuilder 中为每个实例设置不同的 Nacos 配置前缀来隔离它们的注册信息,这样它们在 Nacos 中会被识别为独立的服务实例,从而避免了注册信息的覆盖问题。
     *
     * <p>
     * 支持聚合启动子服务开发规范:
     * 1. SpringBootApplication启动类增加@BwApplication注解;
     * 2. 为避免jar包冲突,所有依赖版本必须使用根POM中统一定义的版本;
     * 3. 服务根包名为:"com.beam", "com.umbrella.work", "com.beem" 范围之一;
     * 4. 服务必须打原包,不能使用SpringBoot 的"FAT JAR"方式打包(skip掉spring-boot-maven-plugin的repackage即可);
     * 5. 代码中禁止通过@Value("${spring.application.name}")获取服务名(原因:统一聚合启动时同样有覆盖问题;解决方案:服务名都是固定的,定义一个常量即可;);
     */
    public static void main(String[] args) {
        try {
            // system properties
            System.setProperty("java.net.preferIPv4Stack", "true");
            String appIn = System.getProperty(PROPERTY_KEY_APP_IN);
            String appOut = System.getProperty(PROPERTY_KEY_APP_OUT);
            String nacosGroup = System.getProperty(PROPERTY_KEY_NACOS_GROUP);
            if (StringUtils.isEmpty(nacosGroup)) {
                throw new BeemRuntimeException("Missing VM-Args NACOS_GROUP!");
            }

            // scan
            long begin = System.currentTimeMillis();
            Map<String, SpringApplicationBuilder> appMap = BootstrapHelper.getApplications(appIn, appOut, nacosGroup);
            if (MapUtils.isEmpty(appMap)) {
                BootstrapHelper.printSeparatedLog(log, "No Valid Application Need to Bootstrap!");
                return;
            }

            // startup
            Map<String, ConfigurableApplicationContext> appContextMap = new HashMap<>();
            for (Map.Entry<String, SpringApplicationBuilder> entry : appMap.entrySet()) {
                String appName = entry.getKey();
                SpringApplicationBuilder builder = entry.getValue();
                ConfigurableApplicationContext applicationContext = builder.run(args);
                appContextMap.putIfAbsent(appName, applicationContext);

                String logInfo = appName + " Startup Done";
                BootstrapHelper.printSeparatedLog(log, logInfo);
            }

            long time = System.currentTimeMillis() - begin;
            BootstrapHelper.BootstrapBanners.START.printBanner(log);
            log.info("All Application Startup Complete. time: {}", time);

            // shutdown
            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                for (Map.Entry<String, ConfigurableApplicationContext> entry : appContextMap.entrySet()) {
                    String appName = entry.getKey();
                    ConfigurableApplicationContext appContext = entry.getValue();
                    appContext.close();

                    String logInfo = appName + " Shutdown Done";
                    BootstrapHelper.printSeparatedLog(log, logInfo);
                }
                log.info("All Application Shutdown Complete.");
            }));
        } catch (Exception ex) {
            BootstrapHelper.BootstrapBanners.FAIL.printBanner(log);
            log.error("All Application Startup Error!", ex);
        }
    }
}

3. 验证

3.1 启动验证

在这里插入图片描述
日志太长了,一屏幕截不下;
在这里插入图片描述

3.2 Nacos服务注册验证

1、bw-ai-app服务
在这里插入图片描述
2、bw-job服务
在这里插入图片描述
从nacos中可以看到每个服务都按照预期进行了注册,这样前面的SpringCloud Gateway服务之前配置好个各种routes都不用更改;
在这里插入图片描述

4. 结束语

上面就是说了:“出于时间问题,我就不写一个完成的Demo了。方法和套路懂了,自行就能写出测试验证Demo。”,现在回过头来看,这玩意真的不难,不过如果没有想到又或者没有参考的话,好像很多人还是真不知道咋弄。毕竟SpringApplicationBuilder不常被用到,毕竟SpringApplication.run(XXX.class, args)写法上太简单,执行却又太重了,里面的1234567好像不去专门背几个八股文,又真的能有几个人能条理清晰的说出其要点呢(SpringBoot不就是一个希望大家使用简单的开发框架吗,TMD,面试却卷的要死)?所以我觉得就算是偷懒也得给出个实例参考(毕竟在我看来,不给出一个完整的Demo好像并不是那么的对读者负责,因此我也只能有图有真相了)。

在这里插入图片描述

封面由微软AI生成,一塌糊涂。。。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2231473.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

lvgl

lvgl 目录 lvgl Lvgl移植到STM32 -- 1、下载LVGL源码 -- 2、将必要文件复制到工程目录 -- 3、修改配置文件 将lvgl与底层屏幕结合到一块 -- lvgl也需要有定时器,专门给自己做了一个函数,告诉lvgl经过了多长时间(ms(毫秒)级别) 编写代码 lvgl的中文教程手册网站…

使用WebAssembly优化Web应用性能

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用WebAssembly优化Web应用性能 引言 WebAssembly 简介 安装工具 创建 WebAssembly 项目 编写 WebAssembly 代码 编译 WebAssem…

【docker】docker 环境配置及安装

本文介绍基于 官方存储库 docker 的环境配置、安装、代理配置、卸载等相关内容。 官方安装文档说明&#xff1a;https://docs.docker.com/engine/install/ubuntu/ 主机环境 宿主机环境 Ubuntu 20.04.6 LTS 安装步骤 添加相关依赖 sudo apt-get update sudo apt-get install…

【Linux】网络编程:初识协议,序列化与反序列化——基于json串实现,网络通信计算器中简单协议的实现、手写序列化与反序列化

目录 一、什么是协议&#xff1f; 二、为什么需要有协议呢&#xff1f; 三、协议的应用 四、序列化与反序列化的引入 什么是序列化和反序列化&#xff1f; 为什么需要序列化和反序列化&#xff1f; 五、序列化推荐格式之一&#xff1a;JSON介绍 六、网络版计算器编程逻…

基于MATLAB的加噪语音信号的滤波

一&#xff0e;滤波器的简述 在MATLAB环境下IIR数字滤波器和FIR数字滤波器的设计方 法即实现方法&#xff0c;并进行图形用户界面设计&#xff0c;以显示所介绍迷你滤波器的设计特性。 在无线脉冲响应&#xff08;IIR&#xff09;数字滤波器设计中&#xff0c;先进行模拟滤波器…

Java项目实战II基于Spring Boot的智能家居系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。 一、前言 随着物联网技术的快速发展和普及&#…

基于Spring Boot+Vue的助农销售平台(协同过滤算法、限流算法、支付宝沙盒支付、实时聊天、图形化分析)

&#x1f388;系统亮点&#xff1a;协同过滤算法、节流算法、支付宝沙盒支付、图形化分析、实时聊天&#xff1b; 一.系统开发工具与环境搭建 1.系统设计开发工具 后端使用Java编程语言的Spring boot框架 项目架构&#xff1a;B/S架构 运行环境&#xff1a;win10/win11、jdk1…

【C++笔记】容器适配器及deque和仿函数

【C笔记】容器适配器及deque和仿函数 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;C笔记 文章目录 【C笔记】容器适配器及deque和仿函数前言一.容器适配器1.1什么是容器适配器1.2 STL标准库中stack和queue的底层结构 二.stack2.1stack类模…

软考:中间件

中间件 中间件是一类位于操作系统软件与用户应用软件之间的计算机软件&#xff0c;它包括一组服务&#xff0c;以便于运行在一台或多台机器上的多个软件通过网络进行交互。 中间件的主要功能包括通信支持和应用支持。 通信支持为应用软件提供平台化的运行环境&#xff0c;屏蔽…

统信UOS设备驱动开发-常见问题

包含linux设备驱动开发的基础知识及统信UOS设备驱动的总体架构,常用的设备驱动开发调试优化手段及在环境搭建和代码编写过程中常见问题处理等。 文章目录 环境搭建如何编译驱动代码编写如何实现同源异构环境搭建 如何编译内核 下载并解压内核源码包,进入源码根目录,内核的编…

JS 异步 Promise、Async、await详解

目录 一、JS里的同步异步 二、Promise 1、状态 2、all()、race()、any() 3、简单案例 4、异步执行案例 5、解决异步嵌套繁琐的场景 三、async和await 1、async返回类型 2、async与await结合使用的简单案例 3、解决异步嵌套问题 4、批量请求优化 一、JS里的同步异步…

【Vue3】Vue3相比Vue2有哪些新特性?全面解析与应用指南

&#x1f9d1;‍&#x1f4bc; 一名茫茫大海中沉浮的小小程序员&#x1f36c; &#x1f449; 你的一键四连 (关注 点赞收藏评论)是我更新的最大动力❤️&#xff01; &#x1f4d1; 目录 &#x1f53d; 前言1️⃣ 响应式系统的改进2️⃣ Composition API的引入3️⃣ 更好的Type…

Vue 事件阻止 e.preventDefault();click.prevent

Vue 事件阻止 Vue 事件阻止 e.preventDefault(); click.prevent修饰符

基于vue3和elementPlus的el-tree组件,实现树结构穿梭框,支持数据回显和懒加载

一、功能 功能描述 数据双向穿梭&#xff1a;支持从左侧向右侧转移数据&#xff0c;以及从右侧向左侧转移数据。懒加载支持&#xff1a;支持懒加载数据&#xff0c;适用于大数据量的情况。多种展示形式&#xff1a;右侧列表支持以树形结构或列表形式展示。全选与反选&#xf…

leetcode-21-合并两个有序链表

题解&#xff1a; 1、初始化哑节点dum 2、 3、 代码&#xff1a; 参考&#xff1a;leetcode-88-合并两个有序数组

WPF怎么通过RestSharp向后端发请求

1.下载RestSharpNuGet包 2.请求类和响应类 public class ApiRequest {/// <summary>/// 请求地址/// </summary>public string Route { get; set; }/// <summary>/// 请求方式/// </summary>public Method Method { get; set; }/// <summary>//…

指派问题的求解

实验类型&#xff1a;◆验证性实验 ◇综合性实验 ◇设计性实验 实验目的&#xff1a;学会使用Matlab求解指派问题。 实验内容&#xff1a;利用Matlab编程实现枚举法求解指派问题。 实验例题&#xff1a;有5人分别对应完成5项工作&#xff0c;其各自的耗费如下表所示&#…

vue3 gsap 基于侦听器的动画

1、gsap实现动画 https://gsap.com/ .以上来自baidu ai 2、代码&#xff1a; 安装gsap&#xff1a;pnpm install gsap <script setup> import { ref, reactive, watch } from vue import gsap from gsapconst number ref(0) const tweened reactive({number: 0 })wat…

Flutter CustomScrollView 效果-顶栏透明与标签栏吸顶

CustomScrollView 效果 1. 关键组件 CustomScrollView, SliverOverlapAbsorber, SliverPersistentHeader 2. 关键内容 TLDR SliverOverlapAbsorber 包住 pinned为 true 的组件 可以被CustomScrollView 忽略高度。 以下的全部内容的都为了阐述上面这句话。初阶 Flutter 开发知…

江协科技STM32学习- P29 实验- 串口收发HEX数据包/文本数据包

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…