当前位置:Java -> Cron Jobs vs. WatchService

Cron Jobs vs. WatchService

我们目前正在整合多个最初独立开发的子系统,以解决各种领域特定的挑战。在合并后,我们遇到一个问题,即所有的定时任务都停止了运行。尽管我们对代码进行了彻底的审查,但问题的根本原因仍然难以捉摸,让我们不确定如何解决这种情况。在进一步调查过程中,我们发现了WatchService,它似乎以与定时任务相似的原理运行,因为它也在等待特定事件。随后的研究证实,WatchService和定时任务都利用相同的底层机制进行事件监视。

定时任务是可以在特定时间或间隔运行的预定任务。它们通常用于自动化各种任务,比如备份文件或发送邮件通知。

WatchService是一个用于实时监控文件和目录变化的Java API。它可以用于多种目的,包括实时重新加载和自动项目重建。

在同一应用程序中运行时,并不一定会导致定时任务和WatchService之间发生冲突。它们是否会发生冲突取决于它们在应用程序中的配置和使用方式。如果设置正确,它们可以和平共处。但是,如果两者都在监视相同的文件或目录,则可能会出现冲突,应特别小心处理这种情况。

如果你担心会有冲突或想确保任务之间的隔离,建议在单个应用程序中只使用定时任务或WatchService,或者在单独的进程中运行它们是一个很好的做法。

在单独的进程或线程中运行它们可以帮助避免潜在的问题。

以下是关于在Java中定时任务和WatchService之间冲突的一些额外细节:

  • 定时任务和WatchService都使用相同的底层机制来监视文件系统的更改,这就是inotify API。
  • 当定时任务运行时,它会暂时禁用innotify对它正在监视的文件和目录。这将阻止WatchService收到关于这些文件和目录更改的通知。
  • 一旦定时任务完成运行,它会重新启用它曾经监视的文件和目录的inotify。然而,这并不保证WatchService会收到在inotify禁用期间发生的所有更改的通知。

基于这些原因,建议在单个应用程序中只使用定时任务或WatchService。如果需要同时使用两者,应在单独的进程或线程中运行它们。

要在Spring Boot应用程序中的单独进程中运行WatchService,可以按照以下步骤进行:

  1. 创建一个实现Runnable接口的新的Java类。该类将包含WatchService任务的代码。
  2. WatchService任务类的run()方法中,创建一个新的WatchService对象。
  3. 使用WatchService对象注册要监视的文件和目录。
  4. 创建一个新的ApplicationContext对象。这将允许您在WatchService任务类中访问Spring Beans。
  5. 启动一个新的线程,并在新线程中运行WatchService任务类。
  6. 在主Spring Boot应用程序类中,创建一个新的ProcessBuilder对象,并将运行WatchService任务类的命令指定为该命令。
  7. 通过在ProcessBuilder对象上调用start()方法来启动WatchService进程。

以下是一个使用Spring Beans的WatchService任务类的示例:

public class WatchServiceTask implements Runnable {

    private final WatchService watchService;
    private final MyService myService;

    public WatchServiceTask(WatchService watchService, MyService myService) {
        this.watchService = watchService;
        this.myService = myService;
    }

    @Override
    public void run() {
        while (true) {
            WatchKey key = watchService.take();
            for (WatchEvent<?> event : key.pollEvents()) {
                // Process the watch event
                myService.doSomething();
            }
            key.reset();
        }
    }
}


以下是在Spring Boot应用程序中单独进程中运行WatchService任务类的示例:

@SpringBootApplication
public class MainApplication {

    private final ApplicationContext applicationContext;

    public MainApplication(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(MainApplication.class, args);

        WatchService watchService = FileSystems.getDefault().newWatchService();

        // Register the files and directories that you want to monitor with the WatchService object
        watchService.register(Paths.get("/path/to/file"), StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);

        // Create a new WatchService task class
        WatchServiceTask watchServiceTask = new WatchServiceTask(watchService, applicationContext.getBean(MyService.class));

        // Start a new thread and run the WatchService task class in the new thread
        new Thread(watchServiceTask).start();

        // Create a new ProcessBuilder object and specify the command to run the WatchService task class as the command
        ProcessBuilder processBuilder = new ProcessBuilder("java", "-cp", System.getProperty("java.class.path"), WatchServiceTask.class.getName());

        // Start the WatchService process by calling the start() method on the ProcessBuilder object
        processBuilder.start();
    }
}


通过按照这些步骤,你可以在Spring Boot应用程序中单独进程中运行WatchService,从而防止与在主应用程序中运行的定时任务发生冲突。

这种方法涉及在Spring Boot应用程序之外另行运行WatchServiceTask的单独Java进程。这是处理并发任务(如使用WatchService监视文件系统)的有效方式。然而,有一些需要考虑和注意的问题。

  1. 资源管理:运行一个单独的进程会消耗系统资源(CPU、内存),对于父Spring Boot应用程序和新进程都是如此。根据被监视文件的数量和文件系统事件的频率,可能会导致资源竞争。
  2. 错误处理:当运行一个单独的进程时,你应该考虑错误处理。如果子进程因任何原因(如未处理的异常)而失败,可能很难诊断问题。
  3. 协调:在提供的代码中,父应用程序启动子进程,但两个进程之间没有内在的协调。如果你的Spring Boot应用程序依赖于WatchServiceTask的结果或数据,你将需要实现一些进程间通信(IPC)机制。
  4. 类路径和依赖项:在ProcessBuilder中指定的类路径可能不包含WatchServiceTask类所需的所有依赖项。你可能需要明确处理类路径和依赖项,随着项目规模扩大,这可能变得复杂。
  5. 线程管理:在同一进程中以一个独立线程运行WatchServiceTask,同时又使用ProcessBuilder在新进程中启动它。这可能是一个不必要的复杂程度。你可以选择在同一应用程序上下文中将WatchServiceTask作为一个独立的Spring组件运行,利用Spring的异步特性来管理线程池。
  6. 测试:以单独的进程运行可能会使测试变得更具挑战性,因为WatchServiceTask的行为在单元测试中不易模拟或控制。

如果由于约束或特定要求而必须在你的用例中以单独的进程运行,你的做法可以奏效。然而,对于更简单和更易管理的解决方案,考虑在相同应用程序上下文中运行WatchServiceTask,更有效地管理资源和依赖项。

在Spring Boot应用程序中异步运行任务时,考虑使用Spring的内置功能,如@Async@Scheduled,或在同一应用程序上下文中创建自定义线程池。这些方法可以简化资源管理、错误处理和任务间的通信。

在Spring Boot应用程序中,你可以通过创建一个新的Spring Boot应用程序,并配置它以独立运行这项服务,以单独的进程运行DataFileService。你可以使用Spring Boot对@Async的支持,并创建独立的配置来指定这项服务应在不同线程池中运行。以下是实现此目标的步骤:

  1. 创建一个新的Spring Boot应用程序: 你应该创建一个新的Spring Boot应用程序,其中包括DataFileService作为一个组件。如果还没有,请确保你的项目包含了Spring Boot和Spring Framework所需的必要依赖。
  2. 为服务创建独立的配置: 创建一个独立的配置类,定义了自定义的@EnableAsync配置,带有专用的Executor bean。这将确保标注有@Async的方法(如startMonitoring())在一个独立的线程池中运行。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "customAsyncExecutor")
    public ThreadPoolTaskExecutor customAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5); // Adjust this as needed
        executor.setMaxPoolSize(10); // Adjust this as needed
        executor.setQueueCapacity(25); // Adjust this as needed
        executor.setThreadNamePrefix("CustomAsyncExecutor-");
        executor.initialize();
        return executor;
    }
}


在这个示例中,我们定义了一个名为“customAsyncExecutor”的自定义ThreadPoolTaskExecutor。你可以根据你的特定需求调整corePoolSizemaxPoolSize和其他属性。

  1. 更新DataFileService 确保DataFileService类正确地用@Service@Component标注,以便Spring能够发现它并创建一个bean。此外,添加@Autowired注解将customAsyncExecutor bean注入到服务中。
  1. 运行应用程序: 现在你可以运行你的Spring Boot应用程序,DataFileService将按照AsyncConfig类中的配置在一个独立的线程池中执行startMonitoring()方法。

通过遵循这些步骤,你可以在Spring Boot应用程序中以单独的进程运行DataFileService。这种分离允许你独立于主应用程序逻辑执行异步任务,这在监控、后台处理或其他长时间运行的操作中是非常有用的。

@Service
public class DataFileService {
    ...
    private final ThreadPoolTaskExecutor watchServiceAsyncExecutor;

    public DataFileService(..., @Qualifier("customAsyncExecutor") ThreadPoolTaskExecutor customAsyncExecutor) {
        ...
        this.watchServiceAsyncExecutor = customAsyncExecutor;
    }

    @PostConstruct
    public void init() {
        customAsyncExecutor.execute(() -> startMonitoring());
    }

    @Async
    public void startMonitoring() {
        ...
    }
    ...
}


通过使用@Qualifier,并显式地注入customAsyncExecutor bean,你确保在从init方法中调用startMonitoring方法时使用了正确的执行程序。使用customAsyncExecutor.execute(() -> startMonitoring())可以使你以所指定的执行程序异步开始startMonitoring方法,这是实现你所期望行为的好方法。

总之,主要要记住的一点是,虽然定时作业和WatchService可以共存于同一应用程序中。如果你对这些任务的行为有顾虑或特定要求,将它们运行在单独的进程或线程中可以提供清晰的分离,并避免潜在问题。

推荐阅读: 38.SpringBoot的核心注解是哪个?它主要由哪几个注解组成的?

本文链接: Cron Jobs vs. WatchService