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.
EDIT(2022-11-18): There's a new/updated Gradle plugin to help us, and Spring 6 uses Jakarta EE 9 as a baseline. Continue reading for details.
EDIT(2023-05-15): Guice has made the switch to jakarta
in version 7, with a version 6 that stays on javax
but also supports jakarta.inject
to help in the transition.
Wait, how could it be worse‽
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. UPDATE(2023-05-15): Guice made the switch in version 7, so I can finally move on).
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 com.sun.mail:javax.mail
to com.sun.mail:jakarta.mail
to 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 javax.
with 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), smtp
, imap
and pop3
. Jakarta EE 9 followed the exact same layout, except for the javax
to 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 org.eclipse.angus:angus-mail
and 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 angus-core
, image
, smtp
, pop3
, and logging-mailhandler
.
It's also been a tradition to have JARs bundling all the EE APIs together: javax.javaee-web-api
and javax:javaee-api
, respectively jakarta.platform:jakartaee-web-api
and 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 javax.mail.*
or jakarta.mail.*
classes and how many contained com.sun.mail.*
classes?)
OK, so it's indeed way worse, but we have Gradle our savior, right?
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 javax
or jakarta
.
Detecting more conflicts
Using the same kind of component metadata rules as in our previous installment, we'd be able to teach Gradle that:
-
javax.mail:javax.mail-api
,com.sun.mail:javax.mail
, andcom.sun.mail:mailapi
(in version 1) are incompatible as they all containjavax.mail.*
classes -
com.sun.mail:javax.mail
is incompatible withcom.sun.mail:mailapi
,com.sun.mail:smtp
,com.sun.mail:imap
, andcom.sun.mail:pop3
as it's a bundle of those 4 JARs (it provides all the same capabilities than each one of them taken individually), same forcom.sun.mail:jakarta.mail
-
jakarta.mail:jakarta.mail-api
,com.sun.mail:jakarta.mail
,com.sun.mail:mailapi
(in version 2), andorg.eclipse.angus:jakarta.mail
are incompatible as they all containjakarta.mail.*
classes (and note that Angus Mail doesn't use the same versioning scheme, because it wasn't complicated enough) -
org.eclipse.angus:jakarta.mail
andorg.eclipse.angus:angus-mail
are incompatible with their subsets -
org.eclipse.angus
artifacts are incompatible with theircom.sun.mail
counterparts (and again beware of the new versioning scheme) -
com.sun.mail
artifacts in version 1 shouldn't be upgraded to version 2
(and I hope I haven't missed more cases‼)
Rewriting things?
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.
So what now?
Honestly, I'm fed up.
UPDATE(2022-11-18): the GradleX plugin now has most of those rules, at least for the official Java EE/Jakarta EE artifacts, i.e. not the Jetty, Tomcat or Glassfish flavors. See also this comment for current limitations; specifically it won't downgrade Jakarta EE 8 to Java EE 8, so Jakarta EE 9 dependencies might upgrade them and break things at runtime.
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).
UPDATE(2022-11-18): Spring Framework 6 has been released that uses Jakarta EE 9 as a baseline. This will undoubtedly drive adoption of the jakarta.*
namespace, but not all projects have an interest in such combination (e.g. Guice [tracking issue] and Dagger [tracking issue] will likely stay with javax.inject
for a good while, and Guice Servlets with ).javax.servlet
[tracking issue]
UPDATE(2023-05-15): Guice made the switch to jakarta.inject
, jakarta.servlet
, and jakarta.persistence
in version 7, and simultaneously published version 6 that's still on javax
but also supports jakarta.inject
, to help in the transition. Note that to support both namespaces, Guice 6 must depend on the javax.inject:javax.inject:1
artifact, which makes it incompatible with JPMS.
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 😁
Top comments (5)
While I share your pain, as all other java devs do, I find one thing is missing in this article, the root cause of all this: Oracle.
Basically, Oracle dumped all the Java stuff on the Eclipse Foundation but refused that the maintainers continue to use the
javax.*
namespace. So basically, Oracle not just abandonned it, they even forced the maintainers to fork it under another name, causing all this mess. 🤬Citing eclipse-foundation.blog/2019/05/03...
It's like the OpenJDK, it's actually not very open and you need Oracle's approval to touch it. 🤬
I addressed it in the first post, and I linked to that same blog post from the Eclipse Foundation there:
Ah, indeed, sorry. I only read that second article 🙄😅
Thank you for your article.
I do share your pain with develop to the jakarta.* space and the current difficulties.
I'm attempting to redevelop a Java Server Faces (JSF 2.x), JPA 2, Eclipselink to Jakarta with Eclipse IDE. Eclipse doesn't support Jakarta yet. The facets are very outdated and I cannot find workarounds.
Yes, it's a mess. I've had to replace Eclipse and use Intellij IDE.
I read somewhere Oracle owns javax.* so by renaming to jakarta.* it will separate from Oracle.
Yes, it's a pain that javax. packages in Jakarta EE were renamed to jakarta. and shame for Oracle. We at OmniFish are researching how to make the migration as smooth as possible and we started with a series of blog posts to provide guidance how to migrate with as little pain as possible: omnifish.ee/2023/05/06/how-to-upgr....
We're going to add more articles with examples and explanations. Those guides are related to GlassFish 7 but can be applied to any project that needs to migrate from javax. prefix to jakarta. And yes, a big part of the trick is using Eclipse Transformer, but we plan to explain how to use it effectively. We've already successfully migrated a few projects to Jakarta EE 10 and GlassFish 7, which faces a lot of the problems you wrote about (some dependencies have support for both javax. and jakarta., some only support javax., some have dependencies on other artifacts that depend on javax.).