Modern Java/JVM Build Practices
github.com
github.com
New major gradle releases tend to break a lot of stuff which is a lot of work for the full-time expert build engineer you're going to need on hand to maintain the thing for medium or large sized projects.
Now I prefer to stick with Maven, it's simpler, safer, super predictable, and gets the job done just fine. Actually, the job is done better because it involves less headaches and time wasted on figuring out the damn build system.
Case in point: The length of the README in this "Modern gradle" reference repo - that's a lot of complexity just to get dependencies, compilation, tests, and packaging. I prefer to copy-pasta some XML fragments and move on to solving the high ROI business-value issues.
Edit: @bcrosby95 sir, if it were Maven only, you could shrink the document by ~75%, because most of the bullet points are trying to explain the sharp edges of.. gradle.
All my love to all of you folks, goodnight!
yep. This is the conclusion i end up with as well.
There's three stages to maven (and generally, build systems too). First, you use a tool like maven, and find that it sucks, because it didn't do exactly what you wanted. Then, second is you find a new tool to do _exactly_ what you wanted, which is usually something similar to make or ant (and gradle, in this case). It works, and you think you've reached nirvana. Then, third stage is you find out how much maintenance, headaches and gotchas you hit after using that tool. You curse and finally, having had enough, you switch back to maven (or use maven for a different/next project).
Now every time i see a build too in java that's not maven, i sigh. But only people who have reached stage three above truly understand, and accept maven, and the rest are always itching to move to something that "works better".
Nowadays, in the most extreme case I write a new Maven plugin. But in 99% of cases I can come up with a solution using existing stuff. It's not always the straightforward solution, but in the long term it's usually better to maintain than what I'd cooked up with an imperative system like Gradle.
That's what it can do, and it's honestly pretty neat.
Now using Gradle, on the other hand, is a serious pain in the ass for all the reasons you listed. Breaking changes every release, weird dependence on JVM versions despite Java being a very backward compatible language, error messages that make gcc's c++ errors in 2007 look like the model of clarity, it's way up there with the most frustrating software I'm aware of.
Strap me to a chair and slowly pull out my fingernails, you're never getting me to admit I know anything about Gradle in a professional setting. I don't want to be the Gradle guy anymore than I want to be the guy who knows how to fix your broken Jenkins pipelines.
> you're never getting me to admit I know anything about Gradle in a professional setting. I don't want to be the Gradle guy anymore than I want to be the guy who knows how to fix your broken Jenkins pipelines.
I feel this.
If it was more stable, it would be possible to use e.g. stackoverflow to build a base of solutions to common problems, a canonical way of doing things, but since gradle keeps shifting and changing with every other release, that really isn't possible.
There is a point I want to make about the excessive complexity of projects funded by Enterprise support/training revenue models. However, I cannot do so because I have no good data to back up any claims I want make.
If someone has the data, please enlighten us.
Has it?
For example, I have a very standard "build.gradle" for a React Native app that, in about 60 lines,
- defines debug, release etc. build variants
- sets SDK versions (to values from elsewhere)
- contains passwords in plain text (for Java key stores)
- sets the name of the app
- conditionally chooses arcane linking instructions
- includes freshly downloaded stuff from node_modules
It's a classic instance of "they were so excited that they could, that they didn't stop to think whether they should".Build systems should be declarative rather than imperative. If you need complex stuff doing, it should be done outside of the build system. make was sufficient (subject to a few minor changes).
Rust - cargo.toml. That removes an entire tranche of problems with the language. Versus typical google queries - C++ "modern cmake" or java "how do I create a fat jar".
No developer should have to spend their time messing around with build systems.
Android ecosystem, and those that failed to learn from Ant.
The repo is a reference Gradle and Maven repo. The author, in his linked opinion piece, agrees with you:
> Gradle brought the two above great features that Maven integrated, proving that competition is good. Despite this, I still find no benefit of Gradle.
It looks like most people commenting here, would be better off reading that article and commenting. I would (like another commenter) like to see a comparison to bazel as well - and maybe a discussion of how/if Maven could be reasonably used with Kotlin/android...
I think if you have a project in an enterprise setting where you want a dependable build on a codebase that has a lot of people with relatively low investment contributing then you either use Maven or you have to be very strict and disciplined with how you setup your Gradle build(s).
BUT. For myself I wouldn't trade Gradle for the world. I can setup my projects to develop in a live coding style even though I am working in a compiled language.
Right now I am hacking on a Kotlin Spring Boot project with JOOQ and HTMX and I have set it up so that I can use JOOQ code in buildSrc[1] to output SQL that is then used by JOOQ codegen[2] to generate Records, POJOS, DAOs and so on. I just run 'gradle build -t' and my and 'gradle run' in parallel and because of Spring Hotreload and using the LiveReload Chrome extension I can see my changes taking effect in almost real time. When I make a change that leads to a compile error then my build script will give the Star Trek computer error chirp. I don't think I can convey how happy and productive this setup makes me.
It would be extremely hard to do all of that in maven but it's so simple in gradle. And if you do it right, which is not hard if you follow the docs, then you automatically have dirty checking / lazy behavior. SQL is only generated when buildSrc changes. JOOQ artifacts are only generated when the SQL changes. Spring only reloads the application when code or resources change. Everything is quite fast too. For my project it's currently around 3 seconds from changing the code to seeing my changes reflected on the frontend. And everything from the DDL (schema definition) to DML (sql queries), backend and frontend is in my favorite language: Kotlin!
I hope that everyone's build tool makes them as happy as gradle does me.
[1] buildSrc let's you write custom Groovy/Kotlin/Java build tasks that you can then use in your project's build
[2] with the artifacts that JOOQ codegen generates it's possible to use SQL in a fully typesafe, database independent and refactorable way. Here is a good one page overview of how to set it up and what you can do with it: https://www.baeldung.com/kotlin/jooq
I often end up defending gradle because I do think that people fail to understand it still (which is partially on gradle, I agree).
A build system is basically “just” creating a directed graph. If we want to get to target node X, you just execute every transitive dependency of it. To make it fast, you do it in parallel, and only do work that is not currently up-to-date. Gradle is really good at these “behind-the-scene” steps.
The imperative part is only about creating this graph. You can use some imperative code to generate this graph, but otherwise it is completely static, after config phase.
Edit: most of the readme has nothing to do with either maven or gradle, but the tools and principles both of them are using.
However, not everything is magically better with Maven. It is slower (although the Maven daemon is an improvement), the XML config is a bit verbose and modules are not as straightforward as with Gradle. But the convention over configuration approach (without exceptions like in Gradle) makes maintenance way easier.
Completely agree with your sentiments, it reminds me of a similar topic that was here on CI tools, and that everyone has a phase of finding their usecase not covered, switching to Jenkins to write their own Groovy workflow and then realising the footguns they've unleashed.
1. Use maven.
2. If you use spring, use spring boot parent, you don't really need to read the rest.
3. By default maven uses ancient plugin versions (for reasons I don't understand). So you have to define every single plugin you're using with latest version, including standard ones like maven-compiler-plugin.
4. Do not specify versions in <version> tag. Use properties for every single dependency and plugin.
5. Use "versions" maven plugin, it supports properties.
6. Learn maven, it's tiny. Learn about <dependencyManagement>, <pluginManagement> tags.
7. Do not use multi-project mono-repo setups.
8. If you really want to use multi-project setup, use Gradle. It sucks, but maven multi project configuration is hardly usable.
I see this a lot, but... why? Feels like putting far apart information that should be together (name of a dependency and its version).
It makes sense I guess when multiple artifacts have the same version, like a number of spring deps will all have the same verson, and then you only need to change it in one place, and you are certain they are synchronized. But when there is only one thing with its own version number, why define a property and put its value in a different place in the file?
"dependency-x": "1.0"
"dependency-y": "3.2.1"
This is basically what Cargo.toml, package.json, even requirements.txt give you.Compare that to the 6 lines minimum that each maven dependency is, plus the fact that you sometimes need to specify the same dependency again in the <dependencyManagement> section for what other languages's tools do with lockfiles, which could be several pages away because of the aforementioned bloat. So it ends up being a pain to find the version information, and to update it.
Then you add in the multi-module dependencies, where you're supposed to keep the versions in sync. Sure, there's parent POMs to help with this, but you can only use one at a time, and ultimately someone still needs to maintain the parent POM.
So then rather than specify the same version number 8 or so times in very distant locations, people put a bunch of properties with version numbers to simulate a less clunky dependency specification format.
The only time you have to repeat the versions of dependencies is when there are related dependencies that should be the same version. E.g. dependencies with separate packages for modules, like Jackson or Spring. And if the dependency has a BOM pom, that's not needed either.
Another reason is uniformity. You'll inevitably have some group of dependencies with identical version, so it certainly makes sense to move that info to the property. And then you'll have your versions information spread around. Not very nice.
Many collection of artifacts that need to have the same version provide a BOM to define the version in a single place. And you can always use local variables anyway
I like maven for its simplicity, although I do dislike XML for the build configuration language, seems to verbose and outdated. Mavens plugins are elsewhere (eg published jars), but with gradle I can also specialize and have local plugins kept with the code as well as use published plugins. I like that flexibility.
All said, Gradle has changed a lot of over the years and with that its best practices, so many of my projects are way behind the new standard way of doing things. But with either tool once its all up and running maintaining it is relatively simple.
Perhaps the new Amper project by Jetbrains (Gradle configuration via YAML) will simplify things for the 95% of cases and still allow users to extend and fall back to kotlin / groovy configuration where specialization is needed.
Who are these people that enjoy yaml, and why? It's so easy to get indentation and lists wrong (yes, with editor support).
I like XML as well, especially if combined with a clear schema so it's easy to write correct markup.
I can't say I've ever used Groovy. It seems like Kotlin's Gradle DSL has completely replaced it in practice, so I can't really comment on it.
Every configuration format has its pros and cons. It all depends on what you're using it for. I'm not a fan of the endless unstructured yaml in Kubernetes (I'd much rather have something that can be schema checked easily for config that huge) but I wouldn't use Groovy for that either.
Wait, there's yaml with schema support? Do you have an example on hand?
Ed: > I like yaml. It's easy and readable.
I humbly disagree that deeply nested yaml is easy to read (and write) Kubernetes is awful - but so can complex docker compose files be.
> the fact you can feed any modern yaml parser JSON and still make it work is a benefit for those that want to avoid yaml at all costs.
Not really - JSON is a little easier to edit, but doesn't support comments - they're both pretty bad.
Even something bespoke like open tofu/terraform is better to work with IMNHO.
You basically just write any imperative code, and if you refer to your other functions from within, they will automagically be added as an input for that function (this is possible through scala metaprogramming magic, but you are never exposed to that).
The output of your function will be serialized and cached. That’s it.
Bazel and monorepo are a bit exotic to most enterprise Java devs, but now that I've experienced it, I'll never go back to Gradle or Maven or sbt.
One area that needs work that I haven't had time to invest more in yet is a proper split layer OCI image build with rules_oci similar to what Jib does out of the box. Right now the example in the repo just dumps in the _deploy.jar which works but results in one fat layer that has to be pushed every time even if only app code changed.
Overall though I think Bazel is awesome.
I still end up using Gradle but try not to use it for projects other people need to work on, Gradle's comprehensibility cliff is worse than Bazel IMO and the more constrained nature of Bazel keeps people from making too bad of a mess.
Although I've limited experience, I actually much prefer the overall bazel approach/design than that of maven. That experience was with a large C++ project but it only required building on Linux. However I've much more maven experience so can always figure my way out of maven issues - while I am much more inclined to just give up when I get bazel build errors.
Another idea: publish as inheritable POM for Maven and Gradle convention plugin so that people don't need to copy all that boilerplate (with some refactoring to make it reusable)
(And if you want to manage dependencies a bit more strictly, like everywhere in oop: composition over inheritence.)
The other thing is that maven and gradle aren't just pure build tools, but they serve other roles too - chiefly dependency management, they are test runners, generate reports from those tests, can upload the generated JARs into artifact repositories, run the built application etc.
- building fat jar (jar with all dependencies, including transitive ones) or even whole docker image or just deploying the jar you created to the maven repository
- unit tests
- integration tests, with things like starting database
- code coverage
- dependency checks (version clashes of transitive dependencies, security issues)
- code quality checks
Additionally it is nice if you can do following through maven CLI:
- update major/minor/patch version
- deploy application to chosen environment
- create javadocs for the project
Above things are done through plugins, which release versions independently. Additionally plugins might have dependencies - for example maven-checkstyle-plugin depend on checkstyle checkstyle library - you might want to override checkstyle library version that is used by the plugin. Some plugins' default configuration might also not work nicely with each other - you need to update those.
On top of that maven is using XML and on top of that maven creators chosen to make it even more verbose by ditching XML attributes. That way you end with really big pom.xml files (~3 times bigger than gradle file in this project).
I think NPM made one step too far - it removed whole build structure from the build tool. NPM is just package manager and simple run utility (both based on horrible package.json configuration, even maven's verbose XML pom.xml are better). There is no compilation step, verification step etc. - there are just cli commands you can preconfigure in package.json.
The key trick is to reuse common tasks between different projects by importing them. Originally we did this using XML entities.
https://ant.apache.org/faq.html#xml-entity-include
Eventually the Ant import task came along and we now use that.
That way the project specific stuff stays very small, all projects have the same set of commands - but you can still customise relatively easily if you really need to.
I find it funny when people blame the messy complexity of some build script on the tool rather than the people who built it...
Getting a grip on how things are structured, let alone why, is always the hard part for me. But maybe people are better at just hacking away and figuring out that stuff as needed than I am.
You can use the local agent entirely for free and upgrade to Cloud caching if it fits your case.
Maven and Gradle are two of our best integrations.
What Maven and Gradle have in common is that they are quite convoluted and complicated tools for what they do. I have a love-hate relationship with both as they can be serious time sinks when they don't work as advertised. But I'm also smart enough to know that I need to use one of them as the alternatives are usually worse.
I've been on projects where people tried to make maven do things it clearly wasn't good at. I learned a hard rule there: never replace a 5 line bash script with an unreadable 300 line blob of maven xml depending on all sorts of exotic plugins. It will break on you. Repeatedly. And it's going to be a PITA every time. That five line bash script is looking really tempting when you've banged your head against the wall in frustration for a few hours. Same with Gradle. Both tools have things they do well, things they can technically do that are a bit of an afterthought, and things they just really suck at. And you usually end up with a mix of all of those things.
Having said that, doing custom things with gradle tends to be a whole lot easier. You can basically just do whatever with files and directories in some custom task. I've not used Ant in about 15-20 years or so. Used to be quite OK as an option when the Java world was a lot simpler. But I've also dealt with some really hacky & convoluted build scripts back then.
A good practice with any build tool is to maintain and nurture your build files and update tools and dependencies regularly. I've been on projects where people just stopped caring and and fixing those projects is usually a chunk of work. It's a form of technical debt that you should probably avoid. Once you can't upgrade because all hell breaks loose when you do, it's too late.
What I like with gradle is that it has the gradle wrapper. Maven has a wrapper as well these days. So you can control which version of your tool of choice builds your project and developers don't have to worry about installing the right version. Every few months, I just update my projects to require the latest gradle version and fix whatever needs fixing. Doing this incrementally is pretty straightforward. If you wait five years, good luck. Maven is a lot easier with this because effectively it barely changes for the last decade plus. And not because there's nothing left to improve. There just isn't a whole lot happening in terms of new stuff in the maven world. Maven still is on the same major version since I last used it; a decade ago. And there haven't been a lot of minor version bumps either.
Another thing I use is the refreshVersions plugin for Gradle (mentioned in the article). I highly recommend taking a look at that because it's great and really easy to take into use. It makes keeping your dependencies up to date really easy. Out of date dependencies are another form of technical debt. So you don't want all the security, stability, correctness, and other fixes? Or is it just that you can't be bothered to find out if there are any? If the answer is the latter, you probably have a pile of technical debt right there. The longer you delay updating, the harder it gets. BTW. that's portable advice to just about any language and build tool.
> https://github.com/binkley/modern-java-practices#keep-your-b...
It boggles my mind that in this day in age, Maven still recompiles everything if you touch anything. The problem of figuring out what files need to be built and building only them was solved for C in I think the late 70s (makedepend).
I don't know if Gradle is better in this respect. (It certainly can't be worse.)
Every Gradle project I’ve worked on has had non-deterministic builds where you sometimes need to manually clean and rebuild to fix it, and still does unnecessary work every time. I think it’s worse.
Gradle uses a proper task graph with cached incremental builds, out of the box. The only way to break this is if you add your own task types that don't follow their rules, which as a sibling comment observes, is something they've been tightening up in recent releases. Now it is too easy to break those rules and end up with weird problems, and that is a valid criticism of Gradle. But it's also one they're fixing.
One of the reasons Gradle is so prevalent over Maven, especially at large companies, is because Gradle scales drastically better and has far better performance for large projects. A Maven project with hundreds of modules will become impractical quickly due to its lack of a proper task graph and poor support for incremental compilation. Gradle can handle this easily.
The other reason Gradle has become so widespread is because the path for creating custom tasks is far smoother. Often in build systems you need to define ad-hoc tasks, which may grow over time into more powerful collections of build logic. This is easier and less verbose in Gradle than in Maven, which goes straight from declarative to "write everything as plain Java on your own" really fast.
As a result of that there are tons of Gradle plugins available for different features and frameworks, and they are much easier to find. Gradle just has a bigger ecosystem and that means collectively it has a lot of features, which is hard to compete with. In fact, there are many frameworks that can only be used via Gradle for this reason.
I've spent a lot of time studying how to make a build system that can compete well with Gradle and despite the product's obvious flaws, it's not as easy to beat as you might think.
I'm actually kind of a Gradle fan, but it's hard sometimes.
Nonetheless, once you include all the plugins, Gradle has an enormous feature set. Usability is the clear problem with it, but it's also the vaguest. Pointing out problems is eashy, coming up with alternative designs is medium hard, getting everyone to agree those alternatives are better is really hard.
It also seems very, very slow. A trivial wizard-generated app in Android Studio takes significant time to set up and build, for no good reason that I can see. Builds where nothing has changed take time -- it spends many seconds just parsing the build files.
I've tried Bazel and it's both faster and infinitely more robust. I really wish Google had adopted that instead of Gradle for Android Studio.
In a personal project I use Ninja build files generated by hand-rolled Python scripts and that's much, much faster than Gradle. Plenty robust for my purposes and I'm not afraid of clean builds because it's fast.
Not sure what general lesson I should take away, except that Gradle is bad and I don't like it!
I suppose it could be one of those things where you need to learn it properly to appreciate it fully, but the more Gradle I learn the less I like it. Trying to do any kind of custom code generation is really painful, for example.
Edit to add: despite all my whining, I would still use Gradle for a new Android project just because the official tooling support trumps everything else. I definitely wouldn't want to use it outside of Android.
Bazel has been around for years but never managed to displace Gradle, even in Google's own platforms. It's designed for a very specific corporate environment in which policy constrains the way you write software to a tremendous degree. If you match those policies it's great. If you don't then you'll quickly find cases where stuff is easy with Gradle and harder with Bazel. For example, for the longest time even pulling deps from Maven Central took extra third party rules (probably fixed now?), but it's reflective of the mentality.
This seems like a feature to me. Bazel is a generic, extensible build tool; the core project shouldn't need to know the specifics of one dependency system for one language.
With recent versions of Bazel and its new module system (bzlmod), it's much easier to pull in third party rules. In this case, just one line:
bazel_dep(name = "rules_jvm_external", version = "5.3")
https://registry.bazel.build/modules/rules_jvm_externalOn the other side of mobile OSs, xcode is definitely not faring better.
if( Random.nextBoolean()) {
project.delete(
fileTree(project)
)
}
And delete your whole project on roughly every second build.Yes, so maybe it's too easy to shoot yourself in the foot but the "non-deterministic" behavior isn't coming from anything gradle does inherently but from your build script.
In my experience most critique of gradle happens in a vacuum. Is it perfect? No. But on planet earth most people use gulp for builds and npm for dependencies that together have a superset of any issues that gradle has.
Gradle has a correct view of the build graph, unlike maven. There are never a case (unless deliberately broken) where you have to clean build for correctness.
Like, I don't know, maybe the problem was caused by a dependency they were using rather than their own build scripts. How do you know, how do you fix it? What are you supposed to do, not use dependencies?
A gradle plugin could potentially break something, but a) you ain’t writing gradle plugins willy-nilly, b) if it’s a “big” one, it’s likely not gonna be that bad.
The default compiler plugin configuration is quite eager to recompile allot if something changed.
https://maven.apache.org/plugins/maven-compiler-plugin/compi...
"true (default) in this mode the compiler plugin determines whether [...] any source file was added, removed or changed since the last compilation. If this is the case, the compiler plugin recompiles _all_ sources.
Maven doesn't even offer javac's incomplete dependency checking by default, though I believe it can be forced to. (0.5 points for correctness over efficiency, I suppose.)