Maven vs. Gradle and the Best of Both Worlds
See how both Maven and Gradle succeed and fail and consider static modules, an idea that would bring out and combine their best aspects.
Join the DZone community and get the full member experience.
Join For FreeFor several years, I’ve been really frustrated with the build tools for Java-based projects. In my work, I have seen several Java projects and modules that use different build systems. Thankfully, most projects I have seen over the last years are built on Maven or Gradle. Based on this trend, you only need to know two different systems to understand the basic structure and dependencies of a project. I don’t want to give an overview of all build systems that can currently be used to define the build of a Java based project — I think that they can all can be easily split into two different types:
- Script-based build systems.
- Configuration based build systems.
Currently, Maven and Gradle are the best-known build systems, and each of them is related to one of those types. Let’s have a deeper look at both of them.
About Gradle
From my point of view, Gradle is a script-based build system. For such a build system, you can define your own tasks based on commands or an API. In Gradle, you can write custom build tasks easily by using Groovy. Gradle provides some basic plugins for a common build workflow of Java projects. Based on this, you do not need to define any task, like the compilation of the Java sources for each project, again and again. But since Gradle is based on tasks that can easily be defined in the build script, you do not have any defined structure or best practice for how you should structure your build script and how you should define information like the project name or the dependencies of a project. Don’t get me wrong: Gradle provides good APIs for all this, but it’s up to the developers to decide where in the build script they, for example, define the dependencies of a project. In addition, you always need to run the build script to get information about the project.
About Maven
The functionality of Maven against a script-based build tool very limited. All the information about a project and how to build it must be specified in a pom.xml file. Internally, you can configure the project and the build by using XML syntax. All the possibilities that you have to configure the project are defined by Maven in a XSD file. By doing so, it’s quite easy to define static metadata of the project like the name or the project description. Even technical information like groupId, artifactId, version, or static dependencies can easily be defined. By using Maven as a build tool, your project will be build by using a best practice workflow to build Java projects that are defined in several tasks. This is fine for small projects and APIs, but if you need to do something special, you need to add plugins to Maven. Such a plugin must be defined for your project by using the limited XML syntax of Maven. The functionality of such a plugin must be coded in Java and provided as a JAR. For large and complex projects, you will need several of these projects that will always end in a large and unreadable XML file as the description and build definition of your project.
Conclusion
One point that, thankfully, both approaches have in common is the way dependencies will be resolved. Both Maven and Gradle will download (transitive) dependencies from any artifact repository. Maven uses Maven Central here as the default and Gradle uses JCenter. In addition, any other repository (like a private company repository) can easily be defined. Because artifact repositories follow some common standards, all the mentioned repositories can easily be used in Maven or Gradle.
On the other hand, both tools/build systems have some big disadvantages from my point of view. It’s quite easy to define project metadata and dependencies in Maven, but it’s absolutely horrible to create a highly customized build with Maven. If you want to create asciidoc-based documentation or upload the final artifact to a Java EE server. your POM file will quickly become unreadable. The Maven pom of the Hazelcast project has, for example, more than 1,000 lines. Understanding a 1,000-line XML-based build definition can be very frustrating for a new developer. So, Maven is nice for small modules and APIs, like Apache commons or GSON. Developers can get a quick overview about the project and its dependencies by simply having a look at the POM file. In addition, tools do not need to run a build process/script to get information. The POM file can simply be parsed.
Gradle, on the other hand, provides a lot of flexibility. Since it is based on a script, you can do pretty much everything, and supporting custom build steps is much easier than in Maven. This is very good if you want to deploy your artifacts to a server or create, for example, documentation. But based on the flexibility, a build script can become complex, too. In most big Java projects I've seen in the last few years, only a few developers know how to change something in the Gradle build. In addition, any tool needs to run a Gradle build to get basic information about the project. Since Gradle build scripts are based on a scripting language, it’s impossible to parse them. So, in the end, Gradle is sadly not the perfect solution for building Java projects.
Based on this, I would say that, currently, no Java build tool is the perfect solution for all Java-based projects. Maven is too limited but great for small projects that follow the defined Maven lifecycle and definitions. Gradle, on the other hand, can do everything that you want, but even small projects may differ in its definition because you can structure/define your build description in any way. In addition, you need to run the build to receive information about the project.
Based on these points, I think that there is a way for all the benefits to be simply combined. When having a look at the build systems for JavaScript, you can see a difference. Modern build systems for JavaScript, like gulp, work more or less like Gradle. You can easily define your own custom tasks based on a scripting language. In addition, the metadata of the project is often defined in a separate file. Most projects that I have seen the last few months use bower to define the static metadata of a project. This includes a description of the project (name, description, license, etc.) and its dependencies. Build tools like gulp can now use the information of bower to build the project (yes, this is a very easy description of the internal process). Based on this, I ask myself why Gradle, for example, can’t do the same. Let’s think about a static definition for Java projects and modules that can easily integrated into a Gradle or Maven build. Since such a definition would be used by both tools it would be much easier for developers to learn how to read and use such a static definition. In addition, tools do not need to start any external process, like a build script, to get information about the Java project. For complete projects, you can simply create custom Gradle tasks that internally get all the information of the static definition and reuse it for the real build.
Below, I will try to sketch how such a static definition might look like and how it could be used.
Defining a Static Module Description
A static module definition should contain a readable description of the module. This should include several parameters like:
- Module name
- Module description
- License
- URLs (repo, issue tracker, doc, etc.)
- Developer information (name, mail, etc.)
Based on this information a module description might look like this:
{
"name": "My cool API",
"description": "A very cool API",
"licence": "Apache 2.0",
"urls": [{
"name": "Github repository",
"url": "www.github.com/cool/api",
"type": "repo"
},{
"name": "Stackoverflow section",
"url": "www.stackoverflow.com/cool",
"type": "custom"
}],
"developers": [{
"name": "John Doe",
"mail": "john.doe@mail.com"
}]
}
Aside from this information, a project needs a unique identifier. This can easily be defined by the groupId and artifactId, so a static definition should reuse these properties:
{
"groupId": "com.cool.api",
"artifactId": "cool-api"
}
To define a specific version of a project, the versionId should be added, too. Based on this information, everything to provide the module to Maven Central or JCenter is defined. In addition, a Maven pom.xml can easily be created based on this information, and other modules can depend on this module. After adding the properties. a static project definition might look like this:
{
"name": "My cool API",
"description": "A very cool API",
"licence": "Apache 2.0",
"groupId": "com.cool.api",
"artifactId": "cool-api",
"version": "2.1.9"
}
To compile a Java module, we need some additional information. I think the most basic information is the source encoding and the Java version that should be used to compile. Here, we need to specify a Java version that defines the minimum version that is needed to compile the sources and a Java version that defined the compile target version. Adding this information to a module description might end in the following file:
{
"name": "My cool API",
"description": "A very cool API",
"licence": "Apache 2.0",
"groupId": "com.cool.api",
"artifactId": "cool-api",
"version": "2.1.9",
"encoding": "UTF-8",
"source-java": "8",
"target-java": "8"
}
Based on this information, a project that needs no additional classes next to the basic Java classes in the class path can easily be compiled. Since the complete module definition is provided in a static way, support for this can easily be integrated into any IDE or build tool.
Since most projects depend on external APIs and modules, the static module definition should provide information about the dependencies of the module. Like in Maven or Gradle, a definition of the dependencies based on artifactId, groupId, and version is the best way to do it. At compilation, a build tool or IDE can then easily download the (transitive) dependencies from Maven Central or JCenter. A static project definition should offer mostly all features that are part of the Maven dependency definition, but in most use cases, simply adding the needed dependencies is all you need. By adding dependency information, a module definition will look like this:
{
"name": "My cool API",
"description": "A very cool API",
"licence": "Apache 2.0",
"groupId": "com.cool.api",
"artifactId": "cool-api",
"version": "2.1.9",
"encoding": "UTF-8",
"source-java": "8",
"target-java": "8",
"dependencies": [{
"groupId": "com.cool.spi",
"artifactId": "cool-spi",
"version": "1.0.0"
},{
"groupId": "com.cool.logging",
"artifactId": "cool-logging",
"version": "1.0.0"
}]
}
A Java module that is defined by such a static structure must follow some best practices and basic rules that are well known from Maven and Gradle-based projects:
- All Java sources must be placed under src/main/java.
- All resources, like images or configuration files, must be placed under src/main/resources.
- All Java sources for unit tests must be placed under src/test/java.
- All resources for unit tests must be placed under src/test/resources.
- The static definition of the project must be defined in a UTF-8 based file in the root folder of the project. The file must be named "metadata.jmm: (jmm stands for Java module metadata).
Why Should I Use This?
Most of you will already use a build tool like Maven, Gradle, or maybe Ant to define the build of a Java project. I think this is fine and should be used in the future, too. But especially when using Gradle, which is the newest of the mentioned build tools, developers have so many possibilities to create a custom build file that normally each build works in a different way — and it can be hard to understand the build process. In all these files, the information about the build process (like a build script) and the metadata of a project are mixed. By encapsulating the metadata from the build, it will be much easier to get a general overview of a module or a build. In addition, each build file or script depends on the used build system. This means that a developer that always used Maven often can not read or interpret a Gradle build script. By defining the metadata in a tool-independent way, any developer can understand the information of any Java project as soon as they've worked with at least one project that provides static metadata. The metadata will not only offer a better readability for developers, but build tools could provide support to interpret the metadata. By doing so, all information that is part of the metadata file should not be redefined in the build script. The build tool can directly use the information from the metadata file to build the project. By doing so, a Gradle file can build a JAR file based on a module that has a static metadata description:
apply plugin: 'java'
apply plugin: 'jmm'
This will be enough to compile all sources of the project, run all unit tests, and build a JAR whose name is created by the artifactId and version value of the metadata file.
Next to general build tools, IDEs can provide support for the static module metadata. In this case, you do not even need a build script. Based on the information that can be defined in the metadata, an IDE can download all needed dependencies, compile the sources of the project, and run all the unit tests. And this is only the beginning. Based on this approach, it would be possible to scan all Java projects at GitHub, GitLab, and BitBucket to find the usage of a module.
Providing a graphical overview of the transitive dependencies of a module will be easy, too. Instead of running a Gradle build to receive information of the dependencies, any tool can simply parse the static metadata. A build tool doesn't even have to be installed to do so. Next to build tools and IDEs, several other tools like build servers will benefit from this approach.
Once the most important tools offer support for static metadata, the maintenance of mostly all Java projects will be much easier. Aspects like the version will be defined at only one central point, and changing the version for a release will be really easy.
Limitations of the Approach
Static metadata will not work perfectly for every project. Some projects need to generate sources at runtime or have dynamic dependencies. For such projects, the static metadata might not be enough to compile the project. But even here, such a metadata definition is not useless. Readable metadata like the name or the license can be specified and all non-dynamic dependencies, and those can be part of the metadata file, too. This will end in less code that is needed for the build file, and tools that interpret the static metadata can at least work with a subset of the project description. And yes, this approach is currently not usable for projects that are based on another programming language, like Kotlin or Groovy. But as said: this is just an initial idea, and I would like to get your thoughts about this topic.
Published at DZone with permission of Hendrik Ebbers. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments