Going from memory, we do something like this:
For WARs: In a mvn build step:
- identify candidate dependencies to exclude from the WAR. This includes a filter step to keep internal dependencies in the WEB-INF dir.
- generate a JSON manifest of all the excluded dependencies, including their local SHAs, and store the manifest in WEB-INF.
- copy a shell script into the new Maven Archive (MAR) that knows how to reverse the process above and output a WAR
Then, in our build process, we pass around the MAR files until the moment of deploy, at which time we expand back into a WAR and deploy.
We looked at just deploying the MARs themselves, but the way our containers manage file cache space, it was easier to rehydrate them pre-deploy.
For executable JARs: again, we start in a Maven build phase, and again identify the dependencies to include vs. the dependencies to exclude and fetch later. We create a totally-new JAR that contains all the classes in the thin Maven-built jar (I believe as a referenced JAR, not as expanded classes). We then bundle all the included JARs directly into the target JAR, and write a CSV manifest identifying which external dependencies are needed at runtime. We then copy a raw .class file with no dependencies [1] into the target JAR and write a manifest that identifies that class as the Main-Class. That class is responsible for invoking 'mvn' via shell execution to load dependencies, building a classloader with the right classpath, etc., and then handing off to the original Main-Class.
[1] hence CSV instead of JSON