3

I have a jpackage created application that consists of a single application jar file plus some third-party dependencies. With jpackage I create installers for Linux, macOS, and Windows. All works well.

But I don't want my users to have to reinstall the application every time my application changes, so I'm looking for a way to let the user download the application jar and have that replace the one that was included with the installer.

I think the best option would be for the application jar to be downloaded into a $HOME/application_name/ directory and set the installed application's CLASSPATH to have that directory at the beginning, so that it finds the newly updated jar before the one that came with the installation. Is this possible? If so, how? I can't find anything in the jpackage documentation to suggest that capability exists. Alternatively, is it possible to directly replace the installed application jar from within the application? If so, how?

If neither is possible, is there another way? In 2+ years-old question about the same thing, the only answer suggested use of a JWS replacement, "getdown", but I don't want to go that route, as I'd lose the other benefits of jpackage.

wolfman
  • 123
  • 9
  • I suppose you could create a relatively simple "framework" that is packaged with `jpackage`, then have that code download the application JAR(s) from some location, checking for updates every time the application is launched. Then in the "framework" code, you create a `ClassLoader` (or `ModuleLayer`) that can read the classes from the downloaded JAR(s). You can then use reflection or an SPI to start the real application. – Slaw Aug 18 '23 at 15:42
  • I sure do miss Java Web Start, which handled this superbly. For what it’s worth, Firefox requires downloading each update as a new installation. Discord too. It seems to be increasingly common. – VGR Aug 18 '23 at 16:59

2 Answers2

0

"Don't fix what ain't broken." - Bert Lance

Do you have any reason to use jpackage? Cause you can get the best of it with a technology I use for every desktop application I have, JDeploy:

jDeploy is a tool for Java developers to more easily distribute their desktop apps as native bundles. You can publish your app to the cloud using npm or GitHub releases, and it provides a download page with links to download native bundles for Mac, Windows, and Linux. For more information about jDeploy and its features see the jDeploy website.

Their website:

[https://www.jdeploy.com/][JDeploy site]

Following exactly what this doc shows allow you to do what you're aiming for:

[https://www.jdeploy.com/docs/manual/][JDeploy developers guide]

Let me know if you have any questions!

FARS
  • 313
  • 6
  • 20
  • One reason to use jpackage: it’s part of the JDK. No third-party tools needed. – VGR Aug 21 '23 at 23:16
  • Thanks for this suggestion. I'm looking into bundling my app this way as it not only seems to solve my update problem, but also improves my workflow as I would no longer have to build separate native packages nor deal with certificates for Mac and Windows. The only downside I've come across so far is that it doesn't seem to have any provisions for displaying EULAs during the installation process - jpackage does that nicely. But I guess I can move its display into the application very easily. – wolfman Aug 21 '23 at 23:43
  • @wolfman that's correct, jDeploy still do not support built-in EULAs. But as you mentioned, you could implement it by your own, which is no hard task at all. All you need is a Parent node (for this one I would go with a VBox) with a Label as title and a ScrollPane for reading into the EULA. Add a Button to the bottom and there you have it! – FARS Aug 22 '23 at 00:52
0

Updating a jpackage application in place is not directly supported by jpackage itself. However, the way you're thinking about updating it is quite reasonable and can be implemented with a bit of creativity. Below is a method that might work for you:

Step-by-step Method:

1. App Directory Creation

  • When your application starts for the first time, you can check if $HOME/application_name/ exists. If not, create it.

2. Check for Newer JAR:

  • Every time your application starts, before it runs its primary function, it should look in $HOME/application_name/ for a newer JAR file. If found, it loads classes from that JAR instead of the original bundled one.

3. Update Mechanism:

  • Implement a simple update mechanism within your app:

    • Check a version file from your server (could be as simple as a text file with a version number or a more complex JSON with metadata).
    • Compare the version with the current version of the app. If the server version is newer, download the newer JAR to $HOME/application_name/.

4. Set Class Loader:

  • Modify your main class's loading mechanism to use a custom class loader that first checks the $HOME/application_name/ directory for JARs, and if not found, falls back to the bundled JAR.

Here's a basic way to do it in code:

File jarUpdateFolder = new File(System.getProperty("user.home") + "/application_name/");
URL[] urls = {jarUpdateFolder.toURI().toURL()};
URLClassLoader loader = new URLClassLoader(urls);

Class<?> clazz = loader.loadClass("com.your.main.ClassName");
Method main = clazz.getDeclaredMethod("main", String[].class);
String[] args = {}; // or whatever args you need
main.invoke(null, (Object) args);

5. User Notification:

  • Optionally, you can add a user interface that notifies the user when there's an update, shows download progress, etc. This depends on how user-friendly and seamless you want the update process to be.

6. Fallback Mechanism:

  • It's also wise to have a fallback mechanism, in case the new JAR has issues or fails to start. This way, your users can still use the application even if an update goes awry.

Considerations:

  • Security: Ensure you sign and verify your JARs so that a malicious user can't replace the JAR with harmful code.

  • Permissions: On some systems, you might face permission issues, especially if your application requires elevated privileges. Ensure that the application has the required permissions to download and replace JAR files.

  • Backup: It's advisable to keep a backup of the old JAR. In case there's a major issue with the new JAR, users can rollback.

  • Logging & Debugging: Keep comprehensive logs, especially during the update process. It'll help in debugging issues that might arise during the update process.

  • Limitations: This approach essentially bypasses jpackage for updating the app, so you don't get any inherent benefits that jpackage might provide for this process.

While this method involves manual setup and requires careful consideration of different edge cases, it gives you the flexibility you need without waiting for jpackage to provide such functionality.

  • Thanks for the suggestion! I have not used custom class loaders before, so the first question I have regarding your step (4) is whether class dependencies not in the updated jar in $HOME/application_name/ would be still be found and loaded? I.e. my application has a couple dozen third-party jars that get bundled alongside the main jar - would they still be found - e.g. by the default class loader? If that's the case, this seems like a good solution to my question. – wolfman Aug 21 '23 at 23:39
  • Yes,. Class dependencies that are not in the updated JAR in $HOME/application_name/ would still need to be loaded for your application to work correctly. – Salman Hussain Aug 25 '23 at 17:29
  • The good news is that class loaders in Java work in a hierarchical fashion. When a class loader is asked to load a class, it first delegates the request to its parent class loader. If the parent cannot find the class, the class loader attempts to load the class itself. Therefore, if your custom class loader doesn't find a class, it will delegate the request to its parent, which, in most application setups, would be the default application class loader that loaded all your bundled JARs. – Salman Hussain Aug 25 '23 at 17:29
  • Here's how you could adjust the custom class loader creation to make it explicitly delegate to the default application class loader: `File jarUpdateFolder = new File(System.getProperty("user.home") + "/application_name/"); URL[] urls = {jarUpdateFolder.toURI().toURL()}; // Explicitly set the parent to the default system class loader URLClassLoader loader = new URLClassLoader(urls, ClassLoader.getSystemClassLoader());` – Salman Hussain Aug 25 '23 at 17:30
  • By setting the system class loader as the parent, you ensure that if your custom class loader doesn't find the class in $HOME/application_name/, it will fall back to the system class loader, which should locate the class within the original bundled JARs. This should allow your application to find classes from third-party JARs that were bundled with the original application, even if those classes are not present in the updated JAR in $HOME/application_name/. – Salman Hussain Aug 25 '23 at 17:31
  • So, to answer your question: yes, this approach should work well with third-party JARs bundled alongside your main JAR. Your custom class loader will look in $HOME/application_name/ first, and if it doesn't find the class there, it will delegate to the default system class loader, which will look in your application's original classpath. – Salman Hussain Aug 25 '23 at 17:31
  • I wish I could mark two responses as answers to my question - both yours and the suggestion of using jDeploy instead are good ones. Yours actually answers my question, but the jDeploy suggestion is what I feel is actually the better approach for me. – wolfman Aug 26 '23 at 19:17
  • Tried your suggestion: suppose updatable jar is called "app.jar" and it comes bundled as part of the installation. When I run the code, it always loads classes in the bundled app.jar rather than the one I put into $HOME/app_name/app.jar. Only if I remove the bundled app.jar do the classes get loaded from $HOME/app_name/app.jar. Looking at a class loader tutorial at baeldung.com this seems actually correct behavior: class loading is always delegated to parent class loaders first - only if they fail to load a class do children's URLClassLoaders get used. – wolfman Aug 31 '23 at 20:02