I just looked at update4j again and it looks like they have done alot of work recently updating it.
They seem to take a different approach than me as I made my updater agnostic of java, modules, class path or anything really. I use paths. I write the paths used by an archive to a version file when building the app or updater. This allowed me to use jpackage or any distribution method, like zip files built by the distribution plugin, interchangeably. The calling app uses jpackage and the updater is a standard distribution created by the distribution plugin that gets extracted into the calling app’s image during build via gradle.
All I have to do to force an update is extract the archive of either one of these on the update server. Only different versions of any file gets downloaded though. I added an extra files download argument for un-versioned files that must be downloaded, like startscripts for example. They will always change when a version of some jar changes.
As for starting the app after download, the calling app is responsible for moving any new updater files and starting the updater and the updater is responsible for moving the calling app files and starting the calling app, each using a path to a start scripts or exe that either jpackage or the distribution plugin builds. The updater does all the downloading for both.
Each uses the exact same paths found in whatever version file it’s interested in to locate the file on the update server and move the downloaded file on the local machine.
This method requires you to know where the calling app lives though, like on windows I force install into AppData and other os in user home.