In the previous post, I described how the Javax to Jakarta migration was a mess, but doing more research on the subject I discovered that it's actually way worse than that.
Until now, I focused on APIs, but what I forgot about were implementations. I didn't really forgot about them, as they were partly what led me to do the research to begin with, but I forgot about how you may want to have implementations for both Java EE and Jakarta EE 9+ at the same time.
The case that got me looking at the subject was a combination of Resteasy, jOOQ, and Sentry-Java. Resteasy and Sentry-Java both require a Servlet implementation (I'm using an embedded Jetty server, but I could have picked Tomcat or Undertow). Resteasy and jOOQ both depend on XML Binding. Resteasy and jOOQ (and Pac4j and Jackson, that I also use) actually maintain two parallel versions, comptible with either Java EE 8 or Jakarta EE 9 (and in the case of jOOQ at least, they're not really parallel, it's more that the previous version is kept perfused with all the bugfixes being backported, but the featureset of the Java EE 8 compatible version is not the same as of the Jakarta EE 9 compatible one). Sentry-Java on the other side is only compatible with the
javax. flavor of Servlets. What this means is that I had to choose between using the older versions of Resteasy and jOOQ, or forking Sentry-Java to bring it to Jakarta EE 9 (this is what I ended up doing, as it's only 3 classes). And of course I discovered the problem totally by accident, looking the results of a
./gradlew dependencies where I had the older version of Resteasy but the latest version of jOOQ, and noting that the
jakarta.xml.bind:jakarta.xml.bind-api transitive dependency of Resteasy was being upgraded to 3.0.0 because of jOOQ.
If it was only a matter of waiting for everyone to provide a Jakarta EE compatible version, it could possibly be workable, although it would take years and some companies don't have incentives in such upgrades (anyone knows when Guice, or Dagger, will move to
jakarta.inject? fortunately I don't use anything that depends on Dependency Injection, so I can live very well with
javax.inject alongside everything
jakarta.*, and that's indeed what I'm currently doing).
No, what I had totally forgotten actually were reference implementations, and specifically (in my case) for Mail. Many EE APIs are frameworks (Servlets, REST) where you have to pick one flavor and one implementation anyway: you wouldn't use Java EE Servlets within a Jakarta EE Servlet container, just like you wouldn't use Vert.x handlers in such container either, or a Spring Web resource within Resteasy or Jersey, and you wouldn't use Resteasy and Jersey at the same time either. But Mail is different, it's a library that you use to send mails, and because it knows how to parse multipart content, it can be used transitively by other libraries that you'll depend on (honestly, I don't think that will be the case for me, fortunately).
So you may want or need to use both JavaMail and Jakarta Mail at the same time. And it happens that this is not possible, because Jakarta EE decided to keep using the
com.sun.mail.* package names while migrating to Jakarta EE 9 APIs. Note that those exact same class names are actually published at separate Maven coordinates, similar to the Java EE 8 vs. Jakarta EE 8, but this time it extends to the whole Jakarta EE irrespective of the version (and to make the matter worse, they changed the Maven coordinates again for Jakarta EE 10: from
org.eclipse.angus:jakarta.mail ; oh, and they reset the versioning scheme, so you also have to somehow guess that Angus Mail 1.0.0 is Jakarta Mail 2.1)
It's as if everything was deliberately made to make the migration as painful as possible, and specifically make it impossible (or at the very least make zero effort to make it possible) to have both Java EE and Jakarta EE in the same project, for no technical reason (besides we'll only have to search/replace
jakarta., and nothing else, which you'll convene is rather weak an argument).
Another thing I had totally forgotten, was Java EE's tradition to publish API jars that are only good to compile against, and then reference implementations that also contain the API classes, but this time with actual code in the methods (this is also why you have EE vendor flavors of them by the way, because they have to change one constant somewhere to point to their actual implementation class).
This means that
com.sun.mail:javax.mail contains the same classes as
javax.mail:javax.mail-api. And because they apparently always have to complicate things, this JAR is also a bundle of
mailapi (that still also contains the API classes),
pop3. Jakarta EE 9 followed the exact same layout, except for the
jakarta renaming (but keeping the
com.sun.mail package names though, remember?) Angus Mail (the Jakarta EE 10 reference implementation) doesn't depart from that tradition and also provides
org.eclipse.angus:jakarta.mail as a bundle of both
jakarta.mail:jakarta.mail-api (but this time it seems like
jakarta.mail:jakarta.mail-api contains normal classes, not a version stripped out of the methods' code to keep only the ABI), and
org.eclipse.angus:angus-mail is a bundle of
It's also been a tradition to have JARs bundling all the EE APIs together:
jakarta.platform:jakartaee-api (fortunately, they now also publish a
jakarta.platform:jakartaee-bom that simply references the other Maven artifacts rather than bundling them in a fat jar).
Don't get me wrong, it's OK to build such JARs for people who don't use Maven or Maven-compatible dependency resolvers ; what is not OK is to publish them to the Central Repository with their own Maven coordinates (did you keep count of how many JARs contained
jakarta.mail.* classes and how many contained
Gradle can help indeed: we can teach it to fail the build if we ever have conflicting JARs in our classpaths, but there are cases that don't have a clean solution besides picking one side and rewriting everything to either
Using the same kind of component metadata rules as in our previous installment, we'd be able to teach Gradle that:
com.sun.mail:mailapi(in version 1) are incompatible as they all contain
com.sun.mail:javax.mailis incompatible with
com.sun.mail:pop3as it's a bundle of those 4 JARs (it provides all the same capabilities than each one of them taken individually), same for
com.sun.mail:mailapi(in version 2), and
org.eclipse.angus:jakarta.mailare incompatible as they all contain
jakarta.mail.*classes (and note that Angus Mail doesn't use the same versioning scheme, because it wasn't complicated enough)
org.eclipse.angus:angus-mailare incompatible with their subsets
org.eclipse.angusartifacts are incompatible with their
com.sun.mailcounterparts (and again beware of the new versioning scheme)
com.sun.mailartifacts in version 1 shouldn't be upgraded to version 2
(and I hope I haven't missed more cases‼)
There are tools to rewrite JARs and classes, the most complete probably being Eclipse Transformer, that is apparently used by the Payara application server to rewrite EARs dynamically at deployment time.
There's a Gradle plugin by Hibernate, but it's not much documented and it looks like Hibernate didn't even use it in their migration. The plugin seems to be tailored to creating JARs to be deployed (historically, the
*-jakarta Hibernate JARS, even though there were made without the plugin as far as I can tell), not for rewriting those dependencies that you might be using.
To rewrite your dependencies, you could theoretically use an artifact transform. Registering it would probably require either identifying the dependencies that need rewriting (by way of attributes), or apply it to each and every dependency (by adding said attribute to all variants of all components). That mess was decided years ago, we should be passed the point where someone already packaged it so you only have to apply one Gradle plugin and it Just Works™, but the community seems to have decided that we should all suffer this mess and wait for every library you depend on to have migrated, and in the mean time be locked with older versions, possibly unmaintained, of your other dependencies.
Note that this rewriting wouldn't magically solve all your problems: you'd still have to have capabilities rule to prevent having several components with different Maven coordinates provide the same package names (including after the rewriting), but this time maybe resolve the conflicts automatically to pick the highest Jakarta EE version.
Honestly, I'm fed up.
Maybe I'll try to make a plugin with all these rules (or contribute them to Jendrik Johannes' Java Ecosystem Capabilities Gradle Plugin) so at least I can verify that I don't have issues. If anyone would like to help create a list of all the conflicts (including the vendor libraries), get in touch (but I don't promise anything).
But for the rest, the best thing to do is probably to poke at project maintainers so they do the upgrade and/or provide parallel flavors (possibly helped by the Eclipse Transformer, and by yourself: please don't be assholes with open source maintainers, lend them a hand or sponsor them).
And maybe in the future I won't use "standard APIs" as often as I used to: I'd rather have libraries that don't know how to talk to each other, and write some glue code, than libraries you cannot even put in the same classpath. So maybe I'll try alternatives to Servlets and/or Jakarta RS, trying to minimize my dependency on them through clear segregation (already what I'm doing mostly, where the Jakarta RS endpoints only translate the HTTP request to a business-oriented service, and translate the result back to an HTTP response), such that rewriting the Web layer/adapter would indeed be costly, but entirely doable. I'm glad I never actually tried to use
javax.json for instance, similar to how I already ditched
javax.ws.rs.client for OkHttp a few years ago.
Theoretically, I could also embrace the Java Module System (JPMS), as it's good to detect duplicate packages and missing dependencies (e.g. when Jakarta EE 8 is upgraded to Jakarta EE 9, assuming a change in module name), but it's a whole other mess as it adds yet another naming scheme. Just as an example, the JBoss version of Jakarta RS 3.0 is still using the
java.ws.rs module name rather than
jakarta.ws.rs, and Jetty's version of Jakarta Servlet 5.0 is using
jetty.servlet.api rather than
jakarta.servlet, Jakarta EE 8 dependencies themselves have sometimes switched module name during patch releases; this means that you cannot just swap one JAR for another as the JVM will then complain that some
requires is not fulfilled. Not to mention that many JARs still aren't compatible with JPMS, with not even an
Automatic-Module-Name: Resteasy 6 and Sentry-Java for instance, and some Java EE dependencies too (e.g.
javax.inject:javax.inject) so downgrading Jakarta EE 8 to Java EE 8 is not without problems either. That would probably deserve a third post, but for my sanity I'd rather wait a couple years 😁