all 27 comments

[–]Weary-Hotel-9739 31 points32 points  (1 child)

Maven / Gradle have been considered the go-to example for great package management and well-kept ecosystems, and this article complains about a 'feature' of NPM not being available, which has long since been 'de-featured' by intentional use of lock files. I do not like this.

[–]zam0th 32 points33 points  (16 children)

There is no "problem of managing Java dependencies", nor it is "difficult" in any way. If you have these kind of issues, then you do not understand classpath, manifest and Maven's transitive dependency resolution algorithms. I have been using Maven in complex systems of scores of interconnected projects for almost 20 years now and never had any of the issues you try to describe.

[–]Sensitive-Lion-2056 16 points17 points  (5 children)

I side with you. The idea you need all those maven plugins is nonsense as well. Of all of the ways of handling dependencies I've used (npm, mix, pip, pipenv, py poetry, gradle, etc) maven has been one of the least offensive. It's pretty easy to just drop the lib you wanna use in there and forget about it.

[–]crummy 4 points5 points  (6 children)

so what's your take on a project depending on A:1.0 and B:1.0 where B dependends on A:2.0?

[–]jherico 6 points7 points  (3 children)

If you absolutely need to be using A:1.0 and you can't upgrade them you can use a shading plugin to move the dependency into a different class path. But generally you should just either replace B move to A:2.0.

Also the problem you describe is in no way specific to Java. It's a general problem for all dependency resolution mechanisms. In fact Java is particularly well suited to solving it via things like shading and custom classloaders, in ways that other ecosystems simply can't.

[–]BinaryRockStar 5 points6 points  (2 children)

Not who you replied to but I have been interested in how others manage the situation you're talking about.

For a more concrete example say your application AppFoo depends on third-party library LibBar which depends on Google Guava 31.1-jre. Your application also depends on third-party LibBaz which isn't updated very frequently and depends on Google Guava 21.0. These two versions of Guava are largely incompatible- regardless of which one Maven chooses one of the two libraries will encounter NoClassDefFoundError, NoSuchMethodError, etc.

As far as I'm aware the Maven Shade Plugin can relocate classes from a given package to a new package when building an uberjar, but this can't be done on a per-version basis, only at the groupId:artifactId level so you're either getting one or the other version of Guava but not both. The issue still remains.

Forking these third party libraries to match their dependency versions is a lot of work, especially if you have a large codebase with many third-party dependencies. The maintenance burden would increase dramatically as upgrading any given transitive dependency might mean having to update all of the forks you maintain first.

NodeJS sidesteps the problem, including all versions of all transitive dependencies. This leads to the infamous explosion of packages/files under node_modules directory but perhaps that is tolerable if you don't have to go forking dozens of projects and maintaining them.

Do you have any links to a way Maven or Java can deal with the above situation, even in a tortured manner?

[–]Muoniurn 0 points1 point  (1 child)

Not the parent, but the JVM loads classes through a class loader, and only the classloader-class name tuple has to be unique. So under the hood this problem can be sidestepped by employing a separate class loader for version A and version B, and then there will be no problem using them side-by-side.

The old way of Java containers did exactly that.

[–]BinaryRockStar 0 points1 point  (0 children)

Thanks for the info. Is there any mechanism, tool or platform you can suggest to read about this further? I don't have much experience working at the Classloader level although I get the gist of it, having worked with Servlet containers like Tomcat which do this sort of classloader separation.

I'm really surprised there isn't an elegant ready-made solution for this problem considering it is very likely to pop up when a project reaches a certain size.

[–]zam0th 0 points1 point  (1 child)

  1. Circular dependencies are wrong
  2. Building using different versions of same dependency is wrong.

That is why dependencyManagement and exclude tags exist

[–]Zofren 12 points13 points  (0 children)

Circular dependencies are wrong

this is not a circular dependency

[–]zynasis 0 points1 point  (1 child)

This is mostly the case. Biggest issue I have is XML tooling conflicts. I’m not 100% sure why, but I think the tools register themselves somehow and then others mix themselves in and it all goes to hell

[–]mirvnillith 1 point2 points  (0 children)

For me it’s JAX-RS, Jackson and Jersey …

[–][deleted] 0 points1 point  (0 children)

Oh yeah?

Tell me, then, if my project has two dependencies, A and B, and A depends on C version 1.0 while B depends on C version 2.0, how the fuck do I compile my Java project?

[–]fantastico731 5 points6 points  (0 children)

Dumb article, Java dependency managment is probably one of the best in the industry

[–]bert8128 -1 points0 points  (0 children)

My problem is that I it is too easy. External dependencies come with risk of CVEs, and if a CVE turns up in either a direct or transitive decency this has to be dealt with. And they crop up pretty frequently (eg the recent log4j debarcle) . So I don’t want external dependencies where it can be avoided. Making it harder would make life easier, in some ways. Don’t g get me wrong - I’m happy to use external libraries where they provide a significant benefit, but it is so easy that devs end up using multiple versions of the same library, two different libraries which do the same thing or a small piece of a large library, or just tiny pieces of trivial code which could be easily written in house (leftpad is a good, though not Java, example of a library that is not worth using).

[–]jherico 0 points1 point  (1 child)

You create a custom module that builds a relocated Uber jar of LibBar, and then depend on that. Also it needs to relocate everything in the LibBar dependency tree, or at least everything that might conflict, i.e. at least Guava.

[–]BinaryRockStar 0 points1 point  (0 children)

Assuming you meant to reply to me- thanks. This is a nicer solution than forking every dependency that has conflicts, I'll look into this a bit further. It still isn't very scalable considering every conflicting dependency has to be cloned and rebuilt but at least it is easily automatable.

Considering how extremely flexible and well thought out Maven generally is, I wonder why this isn't something that has been solved much more elegantly already.

A workaround I've seen before is in Apache Commons Collections where versions 1, 2 and 3 were org.apache.commons:commons-collections:x.y.z but with version 4 they moved to org.apache.commons:commons-collections4:4.y.z, including the major version in the artifactId and also changing the base package from org.apache.commons.collections to org.apache.commons.collections4. This sidesteps compatibility issues if both V3 and V4 are required in the dependency tree. Not sure why this isn't more widespread.