无论操作系统如何,在Java中执行计划任务的最佳解决方案是什么?

我想在我的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上使用Scheduled Tasks

在启动时运行

我真的不喜欢这个解决方案,我希望有更优雅的东西。
参考: 我想在Mac OS / Windows上的System Startup上运行我的Java程序。 我怎样才能做到这一点?

系统服务

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

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

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

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

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

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

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

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

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

cron /任务计划程序

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

在Windows上,我可以使用任务计划程序 ,即使在使用At或SchTasks的命令行中也是如此。 这看起来不错,但是我需要这个兼容从XP到7,我不能轻易测试这个。


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

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

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

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

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

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

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

这是我最终实现的内容:

 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) { Le("Unable to retrieve OS!"); } else if ("Mac OS X".equals(osName)) { currentOS = SupportedOS.MAC_OS; } else if (osName.contains("Windows")) { currentOS = SupportedOS.WINDOWS; } else { Le("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", }; Ld("create command: "+Util.join(commandLine, " ")); child = Runtime.getRuntime().exec(commandLine); break; } } catch (final IOException e) { Le("Unable to schedule alarm #"+alarmId, e); return; } Li("Created alarm #"+alarmId); } /** * Removes an alarm from the system list of scheduled tasks * @param alarmId */ public void removeAlarm(final long alarmId) { Li("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) { Le("Unable to remove alarm #"+alarmId, e); } } public void triggerAlarm(final long alarmId) { // Do stuff //... Li("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) { Le("Unable to parse alarm !", e); } } } 

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

我相信你的情况是正确的。 由于服务是系统特定的东西,恕我直言,你不应该使用通用包来覆盖它们,但是每个系统都有一个特定的机制。