热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

SpringBoot支持Crontab任务改造的方法

这篇文章主要介绍了SpringBoot支持Crontab任务改造的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

在以往的 Tomcat 项目中,一直习惯用 Ant 打包,使用 build.xml 配置,通过 ant -buildfile 的方式在机器上执行定时任务。虽然 Spring 本身支持定时任务,但都是服务一直运行时支持。其实在项目中,大多数定时任务,还是借助 Linux Crontab 来支持,需要时运行即可,不需要一直占用机器资源。但 Spring Boot 项目或者普通的 jar 项目,就没这么方便了。

Spring Boot 提供了类似 CommandLineRunner 的方式,很好的执行常驻任务;也可以借助 ApplicationListener 和 ContextRefreshedEvent 等事件来做很多事情。借助该容器事件,一样可以做到类似 Ant 运行的方式来运行定时任务,当然需要做一些项目改动。

1. 监听目标对象

借助容器刷新事件来监听目标对象即可,可以认为,定时任务其实每次只是执行一种操作而已。

比如这是一个写好的例子,注意不要直接用 @Service 将其放入容器中,除非容器本身没有其它自动运行的事件。

package com.github.zhgxun.learn.common.task;

import com.github.zhgxun.learn.common.task.annotation.ScheduleTask;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 不自动加入容器, 用于区分是否属于任务启动, 否则放入容器中, Spring 无法选择性执行
 * 需要根据特殊参数在启动时注入
 * 该监听器本身不能访问容器变量, 如果需要访问, 需要从上下文中获取对象实例后方可继续访问实例信息
 * 如果其它类中启动了多线程, 是无法接管异常抛出的, 需要子线程中正确处理退出操作
 * 该监听器最好不用直接做线程操作, 子类的实现不干预
 */
@Slf4j
public class TaskApplicationListener implements ApplicationListener {
  /**
   * 任务启动监听类标识, 启动时注入
   * 即是 java -Dspring.task.class=com.github.zhgxun.learn.task.TestTask -jar learn.jar
   */
  private static final String SPRING_TASK_CLASS = "spring.task.class";

  /**
   * 支持该注解的方法个数, 目前仅一个
   * 可以理解为控制台一次执行一个类, 依赖的任务应该通过其它方式控制依赖
   */
  private static final int SUPPORT_METHOD_COUNT = 1;

  /**
   * 保存当前容器运行上下文
   */
  private ApplicationContext context;

  /**
   * 监听容器刷新事件
   *
   * @param event 容器刷新事件
   */
  @Override
  @SuppressWarnings("unchecked")
  public void onApplicationEvent(ContextRefreshedEvent event) {
    cOntext= event.getApplicationContext();
    // 不存在时可能为正常的容器启动运行, 无需关心
    String taskClass = System.getProperty(SPRING_TASK_CLASS);
    log.info("ScheduleTask spring task Class: {}", taskClass);
    if (taskClass != null) {
      try {
        // 获取类字节码文件
        Class clazz = findClass(taskClass);

        // 尝试从内容上下文中获取已加载的目标类对象实例, 这个类实例是已经加载到容器内的对象实例, 即可以获取类的信息
        Object object = context.getBean(clazz);

        Method method = findMethod(object);

        log.info("start to run task Class: {}, Method: {}", taskClass, method.getName());
        invoke(method, object);
      } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
      } finally {
        // 需要确保容器正常出发停止事件, 否则容器会僵尸卡死
        shutdown();
      }
    }
  }

  /**
   * 根据class路径名称查找类文件
   *
   * @param clazz 类名称
   * @return 类对象
   * @throws ClassNotFoundException ClassNotFoundException
   */
  private Class findClass(String clazz) throws ClassNotFoundException {
    return Class.forName(clazz);
  }

  /**
   * 获取目标对象中符合条件的方法
   *
   * @param object 目标对象实例
   * @return 符合条件的方法
   */
  private Method findMethod(Object object) {
    Method[] methods = object.getClass().getDeclaredMethods();
    List schedules = Stream.of(methods)
        .filter(method -> method.isAnnotationPresent(ScheduleTask.class))
        .collect(Collectors.toList());
    if (schedules.size() != SUPPORT_METHOD_COUNT) {
      throw new IllegalStateException("only one method should be annotated with @ScheduleTask, but found "
          + schedules.size());
    }
    return schedules.get(0);
  }

  /**
   * 执行目标对象方法
   *
   * @param method 目标方法
   * @param object 目标对象实例
   * @throws IllegalAccessException  IllegalAccessException
   * @throws InvocationTargetException InvocationTargetException
   */
  private void invoke(Method method, Object object) throws IllegalAccessException, InvocationTargetException {
    method.invoke(object);
  }

  /**
   * 执行完毕退出运行容器, 并将返回值交给执行环节, 比如控制台等
   */
  private void shutdown() {
    log.info("shutdown ...");
    System.exit(SpringApplication.exit(context));
  }
}

其实该处仅需要启动执行即可,容器启动完毕事件也是可以的。

2. 标识目标方法

目标方法的标识,最方便的是使用注解标注。

package com.github.zhgxun.learn.common.task.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface ScheduleTask {
}

3. 编写任务

package com.github.zhgxun.learn.task;

import com.github.zhgxun.learn.common.task.annotation.ScheduleTask;
import com.github.zhgxun.learn.service.first.LaunchInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class TestTask {

  @Autowired
  private LaunchInfoService launchInfoService;

  @ScheduleTask
  public void test() {
    log.info("Start task ...");
    log.info("LaunchInfoList: {}", launchInfoService.findAll());

    log.info("模拟启动线程操作");
    for (int i = 0; i <5; i++) {
      new MyTask(i).start();
    }

    try {
      TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

class MyTask extends Thread {
  private int i;
  private int j;
  private String s;

  public MyTask(int i) {
    this.i = i;
  }

  @Override
  public void run() {
    super.run();
    System.out.println("第 " + i + " 个线程启动..." + Thread.currentThread().getName());
    if (i == 2) {
      throw new RuntimeException("模拟运行时异常");
    }
    if (i == 3) {
      // 除数不为0
      int a = i / j;
    }
    // 未对字符串对象赋值, 获取长度报空指针错误
    if (i == 4) {
      System.out.println(s.length());
    }
  }
}

4. 启动改造

启动时需要做一些调整,即跟普通的启动区分开。这也是为什么不要把监听目标对象直接放入容器中的原因,在这里显示添加到容器中,这样就不影响项目中类似 CommandLineRunner 的功能,毕竟这种功能是容器启动完毕就能运行的。如果要改造,会涉及到很多硬编码。

package com.github.zhgxun.learn;

import com.github.zhgxun.learn.common.task.TaskApplicationListener;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

@SpringBootApplication
public class LearnApplication {

  public static void main(String[] args) {
    SpringApplicationBuilder builder = new SpringApplicationBuilder(LearnApplication.class);
    // 根据启动注入参数判断是否为任务动作即可, 否则不干预启动
    if (System.getProperty("spring.task.class") != null) {
      builder.listeners(new TaskApplicationListener()).run(args);
    } else {
      builder.run(args);
    }
  }
}

5. 启动注入

-Dspring.task.class 即是启动注入标识,当然这个标识不要跟默认的参数混淆,需要区分开,否则可能始终获取到系统参数,而无法获取用户参数。

java -Dspring.task.class=com.github.zhgxun.learn.task.TestTask -jar target/learn.jar

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


推荐阅读
  • 基于KVM的SRIOV直通配置及性能测试
    SRIOV介绍、VF直通配置,以及包转发率性能测试小慢哥的原创文章,欢迎转载目录?1.SRIOV介绍?2.环境说明?3.开启SRIOV?4.生成VF?5.VF ... [详细]
  • 探讨如何真正掌握Java EE,包括所需技能、工具和实践经验。资深软件教学总监李刚分享了对毕业生简历中常见问题的看法,并提供了详尽的标准。 ... [详细]
  • 科研单位信息系统中的DevOps实践与优化
    本文探讨了某科研单位通过引入云原生平台实现DevOps开发和运维一体化,显著提升了项目交付效率和产品质量。详细介绍了如何在实际项目中应用DevOps理念,解决了传统开发模式下的诸多痛点。 ... [详细]
  • 实体映射最强工具类:MapStruct真香 ... [详细]
  • 深入解析 Apache Shiro 安全框架架构
    本文详细介绍了 Apache Shiro,一个强大且灵活的开源安全框架。Shiro 专注于简化身份验证、授权、会话管理和加密等复杂的安全操作,使开发者能够更轻松地保护应用程序。其核心目标是提供易于使用和理解的API,同时确保高度的安全性和灵活性。 ... [详细]
  • 本文详细介绍了Python编程语言的学习路径,涵盖基础语法、常用组件、开发工具、数据库管理、Web服务开发、大数据分析、人工智能、爬虫开发及办公自动化等多个方向。通过系统化的学习计划,帮助初学者快速掌握Python的核心技能。 ... [详细]
  • 本文探讨了在 ASP.NET MVC 5 中实现松耦合组件的方法。通过分离关注点,应用程序的各个组件可以更加独立且易于维护和测试。文中详细介绍了依赖项注入(DI)及其在实现松耦合中的作用。 ... [详细]
  • Startup 类配置服务和应用的请求管道。Startup类ASP.NETCore应用使用 Startup 类,按照约定命名为 Startup。 Startup 类:可选择性地包括 ... [详细]
  • 网易严选Java开发面试:MySQL索引深度解析
    本文详细记录了网易严选Java开发岗位的面试经验,特别针对MySQL索引相关的技术问题进行了深入探讨。通过本文,读者可以了解面试官常问的索引问题及其背后的原理。 ... [详细]
  • 自己用过的一些比较有用的css3新属性【HTML】
    web前端|html教程自己用过的一些比较用的css3新属性web前端-html教程css3刚推出不久,虽然大多数的css3属性在很多流行的浏览器中不支持,但我个人觉得还是要尽量开 ... [详细]
  • 本文将深入探讨如何在不依赖第三方库的情况下,使用 React 处理表单输入和验证。我们将介绍一种高效且灵活的方法,涵盖表单提交、输入验证及错误处理等关键功能。 ... [详细]
  • 本文探讨了如何在日常工作中通过优化效率和深入研究核心技术,将技术和知识转化为实际收益。文章结合个人经验,分享了提高工作效率、掌握高价值技能以及选择合适工作环境的方法,帮助读者更好地实现技术变现。 ... [详细]
  • 探索电路与系统的起源与发展
    本文回顾了电路与系统的发展历程,从电的早期发现到现代电子器件的应用。文章不仅涵盖了基础理论和关键发明,还探讨了这一学科对计算机、人工智能及物联网等领域的深远影响。 ... [详细]
  • FinOps 与 Serverless 的结合:破解云成本难题
    本文探讨了如何通过 FinOps 实践优化 Serverless 应用的成本管理,提出了首个 Serverless 函数总成本估计模型,并分享了多种有效的成本优化策略。 ... [详细]
  • 尽管深度学习带来了广泛的应用前景,其训练通常需要强大的计算资源。然而,并非所有开发者都能负担得起高性能服务器或专用硬件。本文探讨了如何在有限的硬件条件下(如ARM CPU)高效运行深度神经网络,特别是通过选择合适的工具和框架来加速模型推理。 ... [详细]
author-avatar
jrvzx342841
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有