提升Spring Boot应用性能的秘密武器:揭秘@Async注解的实用技巧

引言

在日常业务开发中,异步编程已成为应对并发挑战和提升应用程序性能的关键策略。传统的同步编程方式,由于会阻碍主线程执行后续任务直至程序代码执行结束,不可避免地降低了程序整体效率与响应速度。因此,为克服这一瓶颈,开发者广泛采用异步编程技术,将那些可能阻塞的长时间运行任务委派至后台线程处理,从而确保主线程始终保持高效和灵敏的响应能力。

SpringBoot作为一款广受欢迎的应用开发框架,极大地简化了异步编程实践。其中,@Async注解是SpringBoot为实现异步编程提供的便捷工具之一。通过巧妙地应用@Async注解,开发者能够无缝地将方法调用转化为异步执行模式,进而增强系统的并发性能表现。

本文将深度剖析SpringBoot中的@Async注解,包括其内在原理、具体使用方法以及相关注意事项。我们将深入探讨@Async的工作机制,展示如何在实际的SpringBoot项目中有效运用该注解。

@Async的原理

SpringBoot中,@Async注解的实现原理基于Spring框架的AOP和任务执行器(Task Executor)机制。

@Async的启用

开启对异步方法的支持需要在配置类上添加@EnableAsync注解,然后就可以激活了一个Bean后处理器:AsyncConfigurationSelector,它负责自动配置AsyncConfigurer,为异步方法提供所需的线程池。
image.png

AsyncConfigurationSelector中默认使用PROXY的代理,即使用ProxyAsyncConfiguration,而ProxyAsyncConfiguration是用于配置Spring异步方法的代理模式的配置类。

image.png

当然我们还可以指定使用另外一个代理模式:AdviceMode.ASPECTJ,以便使用AspectJ来进行更高级的拦截和处理。

它继承至AbstractAsyncConfiguration,在AbstractAsyncConfiguration中配置AsyncConfigurersetConfigurers方法用于设置异步任务执行器和异常处理器。

image.png

AsyncConfigurer中提供了一种便捷的方式来配置异步方法的执行器(AsyncTaskExecutor)。通过实现AsyncConfigurer接口,可以自定义异步方法的执行策略、线程池等配置信息。默认情况下Spring会先搜索TaskExecutor类型的bean或者名字为taskExecutorExecutor类型的bean,都不存在使用SimpleAsyncTaskExecutor执行器。

public interface AsyncConfigurer {  
	/*
	* 该方法用于获取一个AsyncTaskExecutor对象,用于执行异步方法。
	* 可以在这个方法中创建并配置自定义的AsyncTaskExecutor,例如ThreadPoolTaskExecutor或SimpleAsyncTaskExecutor等。
	*/
    @Nullable  
    default Executor getAsyncExecutor() {  
       return null;  
    }  

	/*
	* 该方法用于获取一个AsyncUncaughtExceptionHandler对象,用于处理异步方法执行中未捕获的异常。如果异步方法执行过程中出现异常而没有被捕获,Spring会调用这个方法来处理异常。
	* 可以在这个方法中返回自定义的AsyncUncaughtExceptionHandler实现,以实现对异步方法异常的处理逻辑。
	*/
    @Nullable  
    default AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {  
       return null;  
    }  
  
}

同时ProxyAsyncConfiguration中的AsyncAnnotationBeanPostProcessor会扫描应用上下文中的所有Bean,检查它们的方法是否标记了@Async注解。对于标记了@Async注解的方法,AsyncAnnotationBeanPostProcessor会创建一个代理对象,用于在调用该方法时启动一个新的线程或使用线程池执行该方法。这样就实现了异步执行的功能。同时它还负责处理@Async注解中的其他属性,例如设置异步方法的执行超时时间、指定线程池名称等。

异步方法注解与代理

当服务类的方法被@Async注解修饰时,Spring AOP会检测到这个注解,并利用动态代理技术为该类创建一个代理对象。其他组件通过Spring容器调用带有@Async注解的方法时,实际上是调用了代理对象的方法。

一个带有@Async注解的方法被调用时,Spring AOP会拦截这个方法调用。此时就会触发处理异步调用的核心拦截器:AsyncExecutionInterceptor。它的主要任务是将被@Async修饰的方法封装成一个Runnable或者Callable任务,并将其提交给TaskExecutor管理的线程池去执行。这个过程确保了异步方法的执行不会阻塞调用者线程。

image.png

TaskExecutor与线程池

TaskExecutor是一个接口,定义了如何执行RunnableCallable任务。SpringBoot提供了多种实现,如SimpleAsyncTaskExecutorThreadPoolTaskExecutor等。通常我们会自定义一个ThreadPoolTaskExecutor以满足特定需求,比如设置核心线程数、最大线程数、队列大小等参数,以确保异步任务能够高效并发执行。AsyncExecutionInterceptor将异步方法封装的任务提交给配置好的TaskExecutor管理的线程池执行。

异步方法执行与结果返回

异步方法的实际执行在独立的线程中进行,不阻塞调用者线程。异步方法的返回类型可以是voi 或者具有返回值,如果异步方法有返回值,那么返回类型通常应该是java.util.concurrent.Future,这样调用者可以通过Future对象来检查异步任务是否完成以及获取最终的结果。

@Async使用

SpringBoot中,使用@Async注解可以轻松地将方法标记为异步执行。下面来看一下如何在Spring Boot项目中正确地使用@Async注解,包括配置方法和注意事项。

在方法上添加@Async注解

要使用@Async注解,首先需要在要异步执行的方法上添加该注解。这样Spring就会在调用这个方法时将其封装为一个异步任务,并交给线程池执行。

@Service  
public class AsyncTaskService {  
  
    /**  
     * 通过@Async 注解表明该方法是个异步方法,  
     * @param i  
     */  
    @Async  
    public void executeAsyncTask(Integer i) {  
        System.out.println(Thread.currentThread().getName()+" 执行异步任务:" + i);  
    }
}    
启用异步功能

SpringBoot应用中,需要在配置类上添加@EnableAsync注解来启用对异步方法的支持。

@EnableAsync  
@SpringBootApplication  
public class SpringBootBaseApplication {  
  
    public static void main(String[] args) {  
       SpringApplication.run(SpringBootBaseApplication.class, args);  
    }  
}
配置线程池

默认情况下,SpringBoot会使用一个默认的线程池来执行异步任务(SimpleAsyncTaskExecutor)。但是,为了更好地控制线程池的行为,我们可以自定义ThreadPoolTaskExecutor,并通过AsyncConfigurer进行配置。

@Configuration  
public class TaskExecutorConfig implements AsyncConfigurer{  
  
  
    @Override  
    public Executor getAsyncExecutor() {  
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
        executor.setCorePoolSize(5);  
        executor.setQueueCapacity(25);  
        executor.setMaxPoolSize(10);  
        executor.setThreadNamePrefix("MyAsyncThread-");  
        executor.initialize();  
        return executor;  
    }
 }   
测试
@SpringBootTest  
public class SpringBootBaseApplicationTests {  
  
    @Autowired  
    private AsyncTaskService asyncTaskService;  
  
    @Test  
    public void asyncTest() {  
       for (int i = 0; i < 10; i++) {  
          asyncTaskService.executeAsyncTask(i);  
       }  
    }  
  
}

输出结果如下:

image.png

注意事项

  1. @Async必须配合@EnableAsync注解一起使用。两者缺一不可。

  2. 异步方法必须定义在Spring Bean中,因为Spring AOP是基于代理对象来实现的。假如我们把AsyncTaskService类中的@Service去掉。就不创建Bean。然后测试代码中修改为如下:

@Test  
public void asyncTest() {  
    AsyncTaskService asyncTaskService = new AsyncTaskService();  
    for (int i = 0; i < 10; i++) {  
       asyncTaskService.executeAsyncTask(i);  
    }  
}

执行结果如下:
image.png

都是主线程同步方法。

  1. 异步方法不能定义为private或static,因为Spring AOP无法拦截这些方法。我们修改AsyncTaskService类中的方法修改为private或者static,则会发生编译错误:

image.png
image.png

  1. 异步方法内部的调用不能使用this关键字,因为this关键字是代理对象的引用,会导致异步调用失效。
@Service  
public class AsyncTaskService {
	@Async  
	public void asyncMethod() {  
	    System.out.println("Async method executed in thread: " + Thread.currentThread().getName());  
	}  
	  
	  
	public void callAsyncMethod() {  
	    // 在同一个类中直接调用异步方法  
	    this.asyncMethod(); // 这里调用不会触发异步执行  
	    System.out.println("callAsyncMethod executed in thread: " + Thread.currentThread().getName());  
	}
}	
  1. @Async注解修饰的方法不能直接被同一个类中的其他方法调用。原因是Spring会在运行时生成一个代理类,调用异步方法时实际上是调用这个代理类的方法。因此,如果在同一个类中直接调用异步方法,@Async注解将不会生效,因为这样调用会绕过代理对象,导致异步执行失效。
@Service  
public class AsyncTaskService {

	@Async  
	public void asyncMethod() {  
	    System.out.println("Async method executed in thread: " + Thread.currentThread().getName());  
	}  
	  
	public void callAsyncMethod() {  
	    // 在同一个类中直接调用异步方法  
	    asyncMethod(); // 这里调用不会触发异步执行  
	    System.out.println("callAsyncMethod executed in thread: " + Thread.currentThread().getName());  
	}
}

测试代码:

@SpringBootTest  
public class SpringBootBaseApplicationTests {  
  
    @Autowired  
    private AsyncTaskService asyncTaskService;  
  
    @Test  
    public void asyncTest2(){  
       asyncTaskService.callAsyncMethod();  
    }  
  
}

执行结果如下:

image.png

  1. 不同的异步方法间不要相互调用
    异步方法间的相互调用会显著增加代码的复杂性层级,由于异步执行的本质在于即时返回并延迟完成任务,因此,嵌套或递归式的异步调用容易导致逻辑难以梳理和维护,特别是在涉及多异步操作状态追踪、顺序控制及依赖关系管理时尤为突出。

当异步方法内部进一步调用其他异步方法,并且牵涉到同步资源如锁、信号量等时,极易引发死锁问题。例如,一个线程在等待自身启动的另一个异步任务结果的同时,该任务却尝试获取第一个线程所持有的锁,如此循环等待,形成无法解开的死锁。

无节制地在异步方法内部启动新的异步任务,特别是在高并发场景下,可能导致线程池资源迅速耗尽,使得系统丧失处理更多请求的能力。此外,直接的异步方法调用还增加了错误处理与日志记录的难度,特别是遇到异常情况时,往往难以追溯原始调用链路以精准定位问题源头。

若需要确保异步方法按照特定顺序执行,直接调用会导致逻辑混乱不清。为解决这一问题,通常推荐采用回调机制、Future/CompletionStage链式编程、响应式编程模型(如RxJava、Project Reactor)等方式来确保有序执行并降低耦合度。

同时,频繁且低延迟的任务间直接互相调用可能会引入额外的上下文切换开销,从而对系统的整体性能造成潜在负面影响。

  1. 合理配置线程池
    Spring Boot默认提供的线程池配置可能无法充分满足特定应用在复杂多变生产环境下的需求,例如其预设的线程数、队列大小和拒绝策略等参数可能不尽合理。为确保资源的有效管理和精细控制,我们可以通过自定义线程池来灵活设定核心线程数、最大线程数、线程空闲超时时间、任务等待队列容量以及饱和策略(如任务拒绝策略)等关键属性,从而适应不同业务场景对并发执行任务数量及资源消耗的精准调控。

另外,不同类型异步任务具有不同的执行特性:有的任务耗时较长,而有的则短促且频繁。针对这种情况,为各类任务配置独立的线程池有助于实现更好的资源隔离,避免任务间的相互影响,进而保障系统的稳定性和响应速度。同时,为了满足特定的安全规范或性能要求,自定义线程池还可以支持诸如设置守护线程、优先级、线程命名格式化等功能。

更重要的是,自定义线程池有利于系统内部执行状态的深度监控与问题诊断。通过制定合理的命名规则、详尽的日志记录以及精确的metrics统计分析,我们可以清晰洞察每个线程池的工作状况,及时发现并优化潜在的性能瓶颈。

如果不进行自定义线程池配置,仅依赖于默认或简化的线程池实现,在面对大量涌入的任务时,可能会因线程资源耗尽导致整个系统响应能力和可用性受损。因此,采用合理配置的自定义线程池能够在高负载环境下有效防范此类风险,有力支撑系统的稳健运行。

@Configuration  
public class TaskExecutorConfig implements AsyncConfigurer{  
  
  
    @Override  
    public Executor getAsyncExecutor() {  
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
        // 核心线程数  
        executor.setCorePoolSize(5);  
        // 设置队列容量  
        executor.setQueueCapacity(25);  
        // 最大线程数  
        executor.setMaxPoolSize(10);  
        // 自定义线程名称前缀  
        executor.setThreadNamePrefix("MyAsyncThread-");  
        executor.initialize();  
        return executor;  
    }
}    

关于自定义线程池的参数讲解请参考我这篇文章:重温Java基础(二)之Java线程池最全详解

  1. 异常处理:
    异步方法内部的异常通常不会被调用方捕获到,因此需要在异步方法内部进行异常处理,可以通过try-catch块:
@Async  
public void asyncMethod() {  
    try {  
        // 异步操作代码  
    } catch (Exception ex) {  
        log.error("Error occurred in async method", ex);  
        // 其他错误处理逻辑  
    }  
}

或者使用@Async注解的exceptionHandler属性来处理异常并进行适当的日志记录或错误处理。

@Configuration  
public class TaskExecutorConfig implements AsyncConfigurer{

	@Override  
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {  
        return new CustomAsyncExceptionHandler();  
    }  
  
}  

// 自定义异常处理器
class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {  
  
    @Override  
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {  
        log.error("Uncaught exception in async method: " + method.getName(), ex);  
        // 其他错误处理逻辑  
    }  
}
  1. 与事务的交互
    默认情况下,当我们在Spring应用中使用@Async注解标记一个方法为异步执行时,这个方法将不会参与到其调用者所处的事务上下文中。这意味着,如果调用异步方法的方法在一个事务内执行,该事务将在调用异步方法后正常提交或回滚,但异步方法内部的操作并不会受到这个事务的影响。

例如,若在同步方法中修改了数据库记录,并随后调用了一个异步方法来更新其他相关的数据,那么如果同步方法中的事务在调用异步方法后提交,而异步方法在执行过程中抛出了异常导致更新失败,这时第一部分已提交的数据和第二部分未成功更新的数据之间就会产生不一致的情况。

为了确保异步方法能够正确地参与事务管理,可以通过设置@Async注解的事务传播行为属性(@Transactionalpropagation属性值)来解决这个问题。

@Transactional(propagation = Propagation.REQUIRES_NEW)  
@Async  
public void asyncMethod() {  
    System.out.println("Async method executed in thread: " + Thread.currentThread().getName());  
    // 具体业务
}

这里通过设置Propagation.REQUIRES_NEW,指示Spring在执行异步方法时开启一个新的、与当前事务无关的事务。这样即使异步方法内部发生异常,它自己的事务会独立进行提交或回滚,从而保证了数据的一致性。不过要注意的是,这种做法可能会增加系统资源消耗,因为每次异步任务都会创建新的事务上下文。

总结

通过本文的介绍,我们了解了SpringBoot@Async注解的原理、使用方法以及需要注意的事项。

@Async注解能够将方法标记为异步执行,利用了Spring框架的AOP和任务执行器机制,使得异步方法能够在后台线程池中并发执行,提高系统的并发能力和响应性。

然而,在使用@Async注解时,需要注意避免异步方法之间相互调用,合理配置线程池,进行异常处理,处理上下文丢失以及与事务的正确交互。这些注意事项能够确保异步方法的可靠性和稳定性,提高应用程序的性能和可维护性。

总的来说,@Async注解是SpringBoot中用于实现异步方法的重要特性,能够有效地提升应用程序的性能和并发能力,但在使用时需要谨慎考虑其使用场景和注意事项,以充分发挥其优势。

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

win11环境安装VmwareLinux

VMware 安装Vmware 操作系统&#xff1a; win11 VM版本&#xff1a; 重启系统 输入许可证秘钥 安装centos finalshell连接linux服务 配置虚拟机运行状态 查询linux服务器的ip地址 下载finalshell 访问FinalShell官网 (hostbuf.com)

Spring6入门到高级-动力节点老杜

文章目录 OCP开闭原则依赖倒置原则控制反转依赖注入DISet方法注入构造注入 Sping特点代理模式代理模式中的角色动态代理JDK动态代理newProxyInstance() 的三个参数 JDK实现代理的步骤第一步&#xff1a;创建目标对象第二步&#xff1a;创建代理对象第三步&#xff1a;调用代理对…

C语言学习--八种排序算法

目录 排序的概念 1.直接插入排序 基本思想 代码实现 算法分析 2.希尔排序 基本思想 代码实现 算法分析 3.冒泡排序 基本思想 代码实现 算法分析 4.快速排序 基本思想 代码实现 算法分析 5.简单选择排序 基本思想 代码实现 算法分析 6.堆排序 基本思想 代…

合合信息扫描全能王亮相静安区3·15活动,AI扫描带来绿色消费新体验

保护消费者的合法权益&#xff0c;是全社会的共同责任。为优化消费环境、促进品质消费高地建设&#xff0c;打造安全优质和谐的消费环境&#xff0c;上海静安区消保委于3月15日举办静安区2024年“315”国际消费者权益日活动。 “激发消费活力&#xff0c;绿色低碳同行”是本次3…

Vue+SpringBoot打造民宿预定管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 用例设计2.2 功能设计2.2.1 租客角色2.2.2 房主角色2.2.3 系统管理员角色 三、系统展示四、核心代码4.1 查询民宿4.2 新增民宿4.3 新增民宿评价4.4 查询留言4.5 新增民宿订单 五、免责说明 一、摘要 1.1 项目介绍 基于…

傻傻分不清目标检测、语义分割和实例分割,看这篇就够了

⭐️ 导言 随着深度学习技术的飞速发展&#xff0c;计算机视觉领域取得了巨大的进步。目标检测、语义分割和实例分割是计算机视觉中的重要任务&#xff0c;它们在图像理解和视频分析等方面发挥着关键作用。本文将深入探讨这三个任务的概念、原理、常用算法以及在实际应用中的案…

(css)vue 自定义背景 can‘t resolve

(css)vue 自定义背景 can’t resolve 旧写法&#xff1a; background-image: url(/assets/images/step-bg.jpg);background-size: 100% 100%; 新写法&#xff1a; background-image: url(~/assets/images/step-bg.jpg);background-size: 100% 100%; 解决参考&#xff1a;https…

shopee无货源出单了怎么发货?shopee怎么做无货源?

在Shopee的电商大舞台上&#xff0c;“无货源出单”就像是一场神奇的魔术表演。你的店铺是舞台&#xff0c;买家的订单是观众的掌声&#xff0c;而你&#xff0c;就是那位神秘的魔术师。订单来了&#xff0c;你却没有货&#xff1f;这可不是什么障碍&#xff0c;因为你有着更为…

算法详解——选择排序和冒泡排序

一、选择排序 选择排序算法的执行过程是这样的&#xff1a;首先&#xff0c;算法遍历整个列表以确定最小的元素&#xff0c;接着&#xff0c;这个最小的元素被置换到列表的开头&#xff0c;确保它被放置在其应有的有序位置上。接下来&#xff0c;从列表的第二个元素开始&#x…

[MySQL]数据库基础

文章目录 1.连接服务器2.理解mysql3.初见数据库4.主流数据库5.服务器&#xff0c;数据库&#xff0c;表关系6.数据逻辑存储7.MySQL架构8.SQL分类9.存储引擎 1.连接服务器 mysql -h 127.0.0.1 -P 3306 -u root -p -h&#xff1a;指明登录部署mysql服务的主机。没有写 -h 127.0.…

【链表】Leetcode 21. 合并两个有序链表【简单】

合并两个有序链表 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4] 解题思路 1、比较两个链表的头结点&#xff0c;选择其…

jumpserver管理集群

git地址&#xff1a;https://github.com/jumpserver/jumpserver.git 1、下载 jumpserver 需要docker来拉取镜像&#xff0c;没有的话会自动下载docker curl -sSL https://resource.fit2cloud.com/jumpserver/jumpserver/releases/latest/download/quick_start.sh | bash 拉取的…

Prometheus修改默认数据存储时间

Prometheus的默认数据存储时间可以通过修改启动脚本中的相关参数来调整。具体来说&#xff0c;可以通过修改--storage.tsdb.retention.time参数来改变数据保留的时长。该参数决定了何时删除旧数据&#xff0c;默认为15天。如果需要延长数据保留时间&#xff0c;可以将该参数的值…

鸿蒙Harmony应用开发—ArkTS声明式开发(容器组件:Hyperlink)

超链接组件&#xff0c;组件宽高范围内点击实现跳转。 说明&#xff1a; 该组件从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。该组件仅支持与系统浏览器配合使用。 需要权限 使用网络时&#xff0c;需要申请权限ohos.per…

AI论文速读 | UniST:提示赋能通用模型用于城市时空预测

本文是时空领域的统一模型——UniST&#xff0c;无独有偶&#xff0c;时序有个统一模型新工作——UniTS&#xff0c;感兴趣的读者也可以阅读今天发布的另外一条。 论文标题&#xff1a;UniST: A Prompt-Empowered Universal Model for Urban Spatio-Temporal Prediction 作者&…

大势智慧与云世纪签署战略合作,实景三维赋能低空经济,泛测绘助力城市数据更新更高效

2024年《政府工作报告》提出“要大力推进现代化产业体系建设&#xff0c;加快发展新质生产力”、“积极打造商业航天、低空经济等新增长引擎”。 近日&#xff0c;武汉大势智慧科技有限公司&#xff08;以下简称“大势智慧”&#xff09;和青岛云世纪信息科技有限公司&#xf…

android 顺滑滑动嵌套布局

1. 背景 最近项目中用到了上面的布局&#xff0c;于是使用了scrollviewrecycleview&#xff0c;为了自适应高度&#xff0c;重写了recycleview&#xff0c;实现了高度自适应&#xff1a; public class CustomRecyclerView extends RecyclerView {public CustomRecyclerView(Non…

麒麟信安出品 | 无惧停服挑战!看C2K平台如何轻松拿捏CentOS迁移

2020年Redhat公司面向全球公布&#xff0c;于2021年底停止维护开源服务器操作系统CentOS 8&#xff0c;并将于2024年6月30日停止维护CentOS 7&#xff0c;届时CentOS全系列版本将停止维护。 在CentOS系统逐步停服的背景下&#xff0c;麒麟信安为满足各行各业现存的大量CentOS系…

基于树莓派实现 --- 智能家居

最效果展示 演示视频链接&#xff1a;基于树莓派实现的智能家居_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Tr421n7BM/?spm_id_from333.999.0.0 &#xff08;PS&#xff1a;房屋模型的搭建是靠纸板箱和淘宝买的家居模型&#xff0c;户型参考了留学时短租的公寓~&a…

el-tree 设置默认展开指定层级

el-tree默认关闭所有选项&#xff0c;但是有添加或者编辑删除的情况下&#xff0c;需要刷新接口&#xff0c;此时会又要关闭所有选项&#xff1b; 需求&#xff1a;在编辑时、添加、删除 需要将该内容默认展开 <el-tree :default-expanded-keys"expandedkeys":da…
最新文章