【问题标题】:What is the best solution to perform scheduled tasks in Java, whatever the OS?无论操作系统如何,在 Java 中执行计划任务的最佳解决方案是什么?
【发布时间】:2012-08-07 13:51:48
【问题描述】:

我想在我的 Java 桌面应用程序上生成警报:

  • 警报设置有特定的日期/时间,可以是 5 分钟或 5 个月
  • 我需要能够在触发警报时创建 SWT 应用程序
  • 我需要它才能在任何操作系统上工作。软件用户可能会使用 Windows(其中 90%),其余为 Mac OS(包括我)
  • 软件许可证必须允许我在商业程序中使用它,而无需开源(因此,没有 GPL)
  • 我不能要求用户安装 Cygwin,所以实现需要是 Windows 和 Unix 原生的

我正在使用 Java、Eclipse、SWT 进行开发,并且我的应用程序是使用 Java Web Start 从我的服务器部署的。我正在使用 Mac OS X.6 进行开发。


我想我有几个选择:

  1. 在启动时运行我的应用程序,自己处理所有事情;
  2. 使用系统服务。
  3. 在 Unix 上使用 cron 表,在 Windows 上使用计划任务

启动时运行

我不太喜欢这个解决方案,我希望能有更优雅的解决方案。
参考:I would like to run my Java program on System Startup on Mac OS/Windows. How can I do this?

系统服务

如果我将它作为系统服务运行,我可以从中受益,因为操作系统会确保我的软件:

  • 一直在运行
  • 没有/不需要 GUI
  • 失败时重启

我研究了一些我可以使用的资源:

  • run4j — CPL — 仅在 Windows 上运行,似乎是一个有效的候选者
  • jsvc — Apache 2.0 — 仅限 Unix,似乎是一个有效的候选者
  • Java Service Wrapper — 各种 — 我买不起付费许可证,免费的是 GPL。因此,我不想/不能使用这个

我在系统服务选项中的问题是:

  1. 还有其他选择吗?
  2. 我计划的实施是否正确:

    • 在应用程序启动时,检查服务是否存在
    • 如果未安装:
      • 升级用户以安装服务(Unix 上的 root,Windows 上的 UAC)
      • 如果宿主操作系统是Windows,使用run4j注册服务
      • 如果宿主机操作系统是Unix,使用jsvc注册服务
    • 如果它没有运行,启动它

因此,在第一次运行时,应用程序将安装服务并启动它。当应用程序关闭时,服务仍在运行,并且不再需要该应用程序,除非它未注册。
但是,我想我仍然怀念“启动时运行”功能。

我说的对吗?我错过了什么吗?

cron / 任务计划程序

在 Unix 上,我可以轻松地使用 cron 表,而无需应用程序将用户升级为 root。我不需要处理重启、系统日期更改等。看起来不错。

在 Windows 上,我可以使用 Task Scheduler,即使在命令行中使用 AtSchTasks。这看起来不错,但我需要它与 XP 到 7 兼容,我无法轻松测试它。


那你会怎么做?我错过了什么?您有什么建议可以帮助我选择最好、最优雅的解决方案吗?

【问题讨论】:

    标签: java service cron scheduled-tasks


    【解决方案1】:

    我相信你的情况是正确的。由于服务是系统特定的东西,恕我直言,您不应该使用通用包来涵盖所有服务,而是为每个系统提供特定的机制。

    【讨论】:

      【解决方案2】:

      在您列出的可用选项中,恕我直言,选项 3 更好。 由于您只在寻找执行应用程序的外部触发器,因此 CRON 或计划任务是比您列出的其他选项更好的解决方案。通过这种方式,您消除了应用程序的复杂性,并且您的应用程序不需要一直运行。它可以在外部触发,当执行结束时,您的应用程序将停止。因此,避免了不必要的资源消耗。

      【讨论】:

        【解决方案3】:

        您也可以尝试使用 Quartz http://quartz-scheduler.org/ 。它具有类似 CRON 的语法来安排作业。

        【讨论】:

        • 我喜欢 cron 在 unix 上的一点是它已经安装并且不需要 root 权限。如果我可以使用类似 cron 的语法,Quartz 看起来不错,但是我不希望我的用户安装额外的软件只是为了运行我的(运行 cygwin 也是如此)。
        • 嗯,它当然可以让您的生活更轻松,而且不需要太多额外的 JAR。
        • 哦等等,我看错了产品的描述。它看起来真的很好!我会仔细观察一整天,我现在就试试。非常感谢。
        • 除非我弄错了,否则我仍然需要找到一种方法来在启动时自动启动一个空程序,该程序将启动 Quartz 调度程序,当我需要它们时,这些作业将实际运行。所以 Quartz 解决了报警/调度部分,而不是实际的后台进程管理。这仍然需要用户首先启动我的程序,对吧?
        • 是的,完全正确。此外,如果您使用 Quartz 的应用程序/服务在根据您的类似 cron 的调度程序表达式应该发生触发器时没有运行,您将错过该事件。
        【解决方案4】:

        这是我最终实现的:

        public class AlarmManager {
            public static final String ALARM_CLI_FORMAT = "startalarm:";
            public static SupportedOS currentOS = SupportedOS.UNSUPPORTED_OS;
        
            public enum SupportedOS {
                UNSUPPORTED_OS,
                MAC_OS,
                WINDOWS,
            }
        
            public AlarmManager() {
                final String osName = System.getProperty("os.name");
                if (osName == null) {
                    L.e("Unable to retrieve OS!");
                } else if ("Mac OS X".equals(osName)) {
                    currentOS = SupportedOS.MAC_OS;
                } else if (osName.contains("Windows")) {
                    currentOS = SupportedOS.WINDOWS;
                } else {
                    L.e("Unsupported OS: "+osName);
                }
            }
        
            /**
             * Windows only: name of the scheduled task
             */
            private String getAlarmName(final long alarmId) {
                return new StringBuilder("My_Alarm_").append(alarmId).toString();
            }
        
            /**
             * Gets the command line to trigger an alarm
             * @param alarmId
             * @return
             */
            private String getAlarmCommandLine(final long alarmId) {
                return new StringBuilder("javaws -open ").append(ALARM_CLI_FORMAT).append(alarmId).append(" ").append(G.JNLP_URL).toString();
            }
        
            /**
             * Adds an alarm to the system list of scheduled tasks
             * @param when
             */
            public void createAlarm(final Calendar when) {
                // Create alarm
                // ... stuff here
                final long alarmId = 42;
        
                // Schedule alarm
                String[] commandLine;
                Process child;
                final String alarmCL = getAlarmCommandLine(alarmId);
                try {
                    switch (currentOS) {
                    case MAC_OS:
                        final String cron = new SimpleDateFormat("mm HH d M '*' ").format(when.getTime()) + alarmCL;
        
                        commandLine = new String[] {
                                "/bin/sh", "-c",
                                "crontab -l | (cat; echo \"" + cron + "\") | crontab"
                        };
                        child = Runtime.getRuntime().exec(commandLine);
                        break;
        
                    case WINDOWS:
                        commandLine = new String[] {
                                "schtasks",
                                "/Create",
                                "/ST "+when.get(Calendar.HOUR_OF_DAY) + ":" + when.get(Calendar.MINUTE),
                                "/SC ONCE",
                                "/SD "+new SimpleDateFormat("dd/MM/yyyy").format(when.getTime()), // careful with locale here! dd/MM/yyyy or MM/dd/yyyy? I'm French! :)
                                "/TR \""+alarmCL+"\"",
                                "/TN \""+getAlarmName(alarmId)+"\"",
                                "/F",
                        };
                        L.d("create command: "+Util.join(commandLine, " "));
                        child = Runtime.getRuntime().exec(commandLine);
                        break;
                    }
                } catch (final IOException e) {
                    L.e("Unable to schedule alarm #"+alarmId, e);
                    return;
                }
        
                L.i("Created alarm #"+alarmId);
            }
        
            /**
             * Removes an alarm from the system list of scheduled tasks
             * @param alarmId
             */
            public void removeAlarm(final long alarmId) {
                L.i("Removing alarm #"+alarmId);
                String[] commandLine;
                Process child;
                try {
                    switch (currentOS) {
                    case MAC_OS:
                        commandLine = new String[] {
                                "/bin/sh", "-c",
                                "crontab -l | (grep -v \""+ALARM_CLI_FORMAT+"\") | crontab"
                        };
                        child = Runtime.getRuntime().exec(commandLine);
                        break;
        
                    case WINDOWS:
                        commandLine = new String[] {
                                "schtasks",
                                "/Delete",
                                "/TN \""+getAlarmName(alarmId)+"\"",
                                "/F",
                        };
                        child = Runtime.getRuntime().exec(commandLine);
                        break;
                    }
                } catch (final IOException e) {
                    L.e("Unable to remove alarm #"+alarmId, e);
                }
            }
        
            public void triggerAlarm(final long alarmId) {
                // Do stuff
                //...
                L.i("Hi! I'm alarm #"+alarmId);
        
                // Remove alarm
                removeAlarm(alarmId);
            }
        }
        

        用法很简单。使用以下方法安排新警报:

        final AlarmManager m = new AlarmManager();
        final Calendar cal = new GregorianCalendar();
        cal.add(Calendar.MINUTE, 1);
        m.createAlarm(cal);
        

        像这样触发警报:

        public static void main(final String[] args) {
            if (args.length >= 2 && args[1] != null && args[1].contains(AlarmManager.ALARM_CLI_FORMAT)) {
                try {
                    final long alarmId = Long.parseLong(args[1].replace(AlarmManager.ALARM_CLI_FORMAT, ""));
                    final AlarmManager m = new AlarmManager();
                    m.triggerAlarm(alarmId);
                } catch (final NumberFormatException e) {
                    L.e("Unable to parse alarm !", e);
                }
            }
        }
        

        在 Mac OS X.6 和 Windows Vista 上测试。 L 类是 System.out.println 的助手,G 保存我的全局常量(这里,我的服务器上的 JNLP 文件用于启动我的应用程序)。

        【讨论】:

          【解决方案5】:

          Bicou:很高兴您分享了您的解决方案!

          请注意,“schtasks.exe”存在一些本地化问题,如果您想使用它创建每日触发器,在英语 Windows 上您必须使用“daily”,在德语 Windows 上(例如)您'必须改用“täglich”。

          为了解决这个问题,我使用/xml-选项实现了对schtasks.exe 的调用,提供了一个我通过模板创建的临时xml 文件。

          创建此类模板的最简单方法是“手动”创建任务并使用任务管理 GUI 工具中的“导出”功能。

          【讨论】:

            猜你喜欢
            • 2021-10-13
            • 1970-01-01
            • 1970-01-01
            • 2019-09-04
            • 1970-01-01
            • 2013-03-08
            • 2015-12-06
            • 1970-01-01
            • 2011-11-10
            相关资源
            最近更新 更多