Docs

Documentation versions (currently viewingVaadin 24)
Documentation translations (currently viewingChinese)

本页面内容由官方文档 http://vaadin.com/docs 机器翻译而来,可能包含错误、不准确或表述不清之处。Vaadin 对译文的准确性、可靠性或时效性不作任何保证或声明。

后台任务

如何处理后台任务。

许多业务应用程序需要在后台线程中执行操作。这些任务可能是用户触发的长时间运行操作,或在特定时间或间隔运行的定时任务。

使用多个线程会增加程序出错的风险。此外,实现后台任务的方法有很多。为了降低风险,您应学习并掌握一种方法,并在所有Vaadin应用中始终如一地应用它。

线程

在Vaadin应用程序中处理后台线程时,绝不应直接创建新的 Thread 对象。首先,启动新线程的代价较高。其次,Java应用程序中并发线程数量有限。虽然具体上限取决于多种因素,Java应用通常支持数千个线程。

应避免手动创建线程,推荐使用线程池或虚拟线程。

线程池由一个队列和一组运行中的线程组成。线程从队列中取出任务并执行。当线程池收到新的任务时,会将其添加到队列。队列有最大容量限制。如果队列已满,线程池会拒绝任务,并抛出异常。

虚拟线程是在Java 21中引入的。与普通线程由操作系统管理不同,虚拟线程由Java虚拟机管理。它们的启动和运行成本更低,意味着并发虚拟线程数远高于普通线程。如果您的虚拟机支持虚拟线程,应优先选用。

有关虚拟线程的详细信息,请参见 Java Documentation

任务执行

后台任务本身不应管理自己的线程池或虚拟线程,而应当使用 executors。executor 是一个对象,它接收一个 Runnable,并在将来某个时间点来执行它。Spring 提供了 TaskExecutor,您应在后台任务中使用它。

默认情况下,Spring Boot 在您的应用上下文中设置了 ThreadPoolTaskExecutor。您可以通过 spring.task.executor.* 配置属性来调整该executor的参数。

要使用虚拟线程,可通过将 spring.threads.virtual.enabled 配置属性设置为 true 来启用。在这种情况下,Spring Boot 会设置 SimpleAsyncTaskExecutor,并为每个任务创建一个新的虚拟线程。

您可以通过直接方式或通过注解声明方式与 TaskExecutor 交互。

在直接方式中,您可以在代码中注入一个 TaskExecutor 实例,然后向其提交任务。

以下是一个使用 TaskExecutor 的类示例:

Source code
Java
import org.springframework.core.task.TaskExecutor;

@Service
public class MyWorker {

    private final TaskExecutor taskExecutor;

    MyWorker(TaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    public void performTask() {
        taskExecutor.execute(() -> {
            System.out.println("Hello, I'm running inside thread " + Thread.currentThread());
        });
    }
}
Important
当您注入 TaskExecutor 时,参数名称必须为 taskExecutor。应用上下文中可能包含多个实现了 TaskExecutor 接口的bean。如果参数名称与bean名称不匹配,Spring 无法确定注入哪一个实例。

如需使用注解,需先启用。方法是在主应用类或者任意一个 @Configuration 类上添加 @EnableAsync 注解。

下面是将注解添加到主应用类的示例:

Source code
Java
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class Application{

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

现在您可以使用 @Async 注解,告知Spring将在后台线程中执行您的代码。

以下为前述 MyWorker 示例的注解形式,使用 @Async 而非 TaskExecutor

Source code
Java
import org.springframework.scheduling.annotation.Async;

@Service
public class MyWorker {

    @Async
    public void performTask() {
        System.out.println("Hello, I'm running inside thread " + Thread.currentThread());
    }
}

有关任务执行的更多信息,请参见 Spring Documentation

任务执行注解注意事项

使用注解可使代码更简洁,然而也存在一些注意事项。

如果您忘记为应用添加 @EnableAsync,您的 @Async 方法将同步地在调用线程中运行,而不是在后台线程。同时,不能在bean内部调用自身的 @Async 方法。这是因为Spring默认采用代理模式处理 @Async 注解,而本地方法调用将绕过代理。

以下示例中,performTask() 在后台线程中执行,performAnotherTask() 在调用线程中执行:

Source code
Java
@Service
public class MyWorker {

    @Async
    public void performTask() {
        System.out.println("Hello, I'm running inside thread " + Thread.currentThread());
    }

    public void performAnotherTask() {
        performTask(); // 此调用会在调用线程中运行
    }
}

如果直接与 TaskExecutor 交互,您将避免此问题。在以下示例中,performTask()performAnotherTask() 均在后台线程中执行:

Source code
Java
@Service
public class MyWorker {

    private final TaskExecutor taskExecutor;

    MyWorker(TaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    public void performTask() {
        taskExecutor.execute(() -> {
            System.out.println("Hello, I'm running inside thread " + Thread.currentThread());
        });
    }

    public void performAnotherTask() {
        performTask(); // 此方法将在后台线程中执行任务
    }
}

任务调度

Spring 也内置了任务调度(scheduling)支持,通过 TaskScheduler 来实现。您同样可以通过直接方式或注解方式与其交互。在两种方式下,都需要在主应用类或任意 @Configuration 类上添加 @EnableScheduling 注解来启用。

以下为将注解添加到主应用类的示例:

Source code
Java
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Application{

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

直接与 TaskScheduler 交互时,您可以将其注入到代码中,并用其来调度任务。

这是一个使用 TaskScheduler 每五分钟执行一次 performTask() 方法的示例:

Source code
Java
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.scheduling.TaskScheduler;

@Service
class MyScheduler implements ApplicationListener<ApplicationReadyEvent> {

    private final TaskScheduler taskScheduler;

    MyScheduler(TaskScheduler taskScheduler) {
        this.taskScheduler = taskScheduler;
    }

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        taskScheduler.scheduleAtFixedRate(this::performTask, Duration.ofMinutes(5));
    }

    private void performTask() {
        System.out.println("Hello, I'm running inside thread " + Thread.currentThread());
    }
}

也可以使用 @Scheduled 注解实现同样效果,如下所示:

Source code
Java
import org.springframework.scheduling.annotation.Scheduled;

@Service
class MyScheduler {

    @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES)
    public void performTask() {
        System.out.println("Hello, I'm running inside thread " + Thread.currentThread());
    }
}

有关任务调度的详细信息,请参见 Spring Documentation

任务调度注意事项

Spring 为任务调度使用单独的线程池,任务本身也会使用这个线程池执行。如果您只有少量且短时任务,这种方式没问题。但如果有大量或长时间任务,可能会遇到问题。例如,调度的任务可能因为线程池耗尽而停止执行。

为避免此问题,建议仅用调度线程池进行任务调度,然后将任务交由任务执行线程池来实际执行。您可以将 @Async@Scheduled 注解结合使用,如下所示:

Source code
Java
@Service
class MyScheduler {

    @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES)
    @Async
    public void performTask() {
        System.out.println("Hello, I'm running inside thread " + Thread.currentThread());
    }
}

您也可以直接与 TaskSchedulerTaskExecutor 交互,如下所示:

Source code
Java
@Service
class MyScheduler implements ApplicationListener<ApplicationReadyEvent> {

    private final TaskScheduler taskScheduler;
    private final TaskExecutor taskExecutor;

    MyScheduler(TaskScheduler taskScheduler, TaskExecutor taskExecutor) {
        this.taskScheduler = taskScheduler;
        this.taskExecutor = taskExecutor;
    }

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        taskScheduler.scheduleAtFixedRate(this::performTask, Duration.ofMinutes(5));
    }

    private void performTask() {
        taskExecutor.execute(() -> {
            System.out.println("Hello, I'm running inside thread " + Thread.currentThread());
        });
    }
}

构建

实现作业
如何实现后台作业。
触发作业
如何触发后台作业。
用户界面交互
如何通过用户界面与作业进行交互。