Skip to content

Commit

Permalink
Updating all java best practices so that they are now able to be disp…
Browse files Browse the repository at this point in the history
…layed on my website, rather than elsewhere.
  • Loading branch information
JonathanGiles committed Jan 9, 2024
1 parent 57736b6 commit 6e29d91
Show file tree
Hide file tree
Showing 30 changed files with 88 additions and 37 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<dependency>
<groupId>net.jonathangiles.tools</groupId>
<artifactId>sitebuilder</artifactId>
<version>0.0.5</version>
<version>0.0.6</version>
</dependency>

<!-- Used to host the local output of the sitebuilder, for testing purposes only -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
slug: /jbp/jbp-1
title: Use BOMs whenever available
template: jbp
---

A BOM is a *Bill of Materials* that in the Java ecosystem is typically represented as a specialized Maven POM file. It includes details of many libraries, including a specific version number. When a project decides to include a BOM dependency, the project is relieved of the requirement of specifying a dependency version for any library that is specified within the BOM file. In fact, it is encouraged that when a BOM dependency is taken, any explicit dependency versions specified in your projects configuration file (be it a Maven `pom.xml`, a Gradle `build.gradle`, or something else) be removed, so that version decisions are left up to the BOM.
Expand All @@ -19,7 +20,7 @@ Many popular projects have BOMs that developers can choose to use. For example,

The immediate question to ask is: why use BOMs? The answer is simply that the people publishing a BOM have likely taken some amount of effort to ensure that the dependencies listed within the BOM work together. For example, in the Azure SDK for Java, the dependencies listed in the BOM are tested (and proven through rigorous tooling) to ensure that the transitive closure of all dependencies is as minimal as possible (which is just a fancy way of saying we really try our hardest to avoid dependency conflicts for you).

Once you move to using a BOM, your burden of [keeping dependencies up to date](/JPB-3) is lessened due to the fact that you have far fewer dependencies whose dependency is explicitly expressed. Now, your burden is to upgrade BOM versions at times that are appropriate for you.
Once you move to using a BOM, your burden of [keeping dependencies up to date](/jbp/jbp-3.html) is lessened due to the fact that you have far fewer dependencies whose dependency is explicitly expressed. Now, your burden is to upgrade BOM versions at times that are appropriate for you.

## How to use a BOM

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
slug: /jbp/jbp-2
title: Minimize dependencies
template: jbp
---

The Java ecosystem is great for its breadth and depth of libraries. Their availability helps to fill in gaps in the JDK, and they make us all much more productive. The problem with dependencies is the fact that everyones development style and philosophy around breaking changes, versioning, dependency management, and so on is all so varied, and we import these different approaches into our projects when we include a dependency. We have to be sure that these dependencies pay for the cost of including it.
Expand All @@ -21,7 +22,7 @@ Because there are so many libraries out in the Java ecosystem, we will often fin

## Understanding dependencies

Fortunately build tools such as Maven and Gradle give us useful tools to understand our dependencies (including all transitive dependencies). Developers should frequently run the following commands on their project, particularly when a dependency is added or [upgraded to a newer version](/JBP-4):
Fortunately build tools such as Maven and Gradle give us useful tools to understand our dependencies (including all transitive dependencies). Developers should frequently run the following commands on their project, particularly when a dependency is added or [upgraded to a newer version](/jbp/jbp-4.html):

* For **Maven**, run `mvn dependency:tree`
* For **Gradle**, run `./gradlew dependencies`
Expand All @@ -45,10 +46,10 @@ It is important to properly apply the correct scope (especially the 'test' scope
</dependency>
```

Note that in the example above, no version is specified, as it is expected that you would use the [JUnit BOM](/JBP-1).
Note that in the example above, no version is specified, as it is expected that you would use the [JUnit BOM](/jbp/jbp-1.html).

Gradle has a similar concept of scopes, and the equivalent JUnit dependency statement in Gradle would be the following:

```groovy
```gradle
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.7.2'
```
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
---
slug: /jbp/jbp-3
title: Don't float dependency versions
template: jbp
---

Libraries are developed by people who work with different philosophies to our own when it comes to breaking changes, release schedules, and so on. Build tooling almost universally has support for specifying a dependency with a fixed version value, or a range of allowed versions. Fortunately the Java ecosystem has by and large settled on always having each dependency be a single fixed version.

The argument for fixing a single dependency version is clear: we want reproducible builds of our applications, and when a dependency may change beneath our feet between builds of our application, we can have little faith that our application will continue to work without a lot of testing. It is better to remove this variable from our build process, and to therefore only move dependencies when the timing is right for us. This might be at the start of a development cycle, following a release of our Java code when we have time to thoroughly test our dependencies. This is a good time to follow the guidance [to keep dependencies up to date](/JPB-4).
The argument for fixing a single dependency version is clear: we want reproducible builds of our applications, and when a dependency may change beneath our feet between builds of our application, we can have little faith that our application will continue to work without a lot of testing. It is better to remove this variable from our build process, and to therefore only move dependencies when the timing is right for us. This might be at the start of a development cycle, following a release of our Java code when we have time to thoroughly test our dependencies. This is a good time to follow the guidance [to keep dependencies up to date](/jbp/jbp-4.html).

In the sections below I provide sample Maven dependencies that specify a floating version, so that you will know what to look out for.

{% include warning.html
content="Do not do the following! Having unbounded ranges is a code smell that you should avoid!" %}
!! Do not do the following! Having unbounded ranges is a code smell that you should avoid!

## Maven guidance

Expand All @@ -24,7 +24,7 @@ Maven has well-documented guidance on what a [valid dependency version string is
</dependency>
```

As you can see in the Maven [dependency version requirement specification](https://maven.apache.org/pom.html#Dependency_Version_Requirement_Specification), if we see a version string in a dependency with brackets, then we should start to sense a code smell. We would prefer to see a fixed version, or in the case of the `azure-storage-blob` library, we would rather follow our [use BOMs whenever available](/JBP-1) guidance and [use the Azure SDK BOM](/AZSDK-1).
As you can see in the Maven [dependency version requirement specification](https://maven.apache.org/pom.html#Dependency_Version_Requirement_Specification), if we see a version string in a dependency with brackets, then we should start to sense a code smell. We would prefer to see a fixed version, or in the case of the `azure-storage-blob` library, we would rather follow our [use BOMs whenever available](/jbp/jbp-1.html) guidance and use the Azure SDK BOM.

## Gradle guidance

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
---
slug: /jbp/jbp-4
title: Keep dependencies up to date
template: jbp
---

Despite the guidance to [minimize dependencies](/JBP-2), it is inevitable that most projects will have multiple dependencies. It is good practice to always work to keep these libraries up to date. Updating to newer versions protects against security vulnerabilities, and often brings new features, performance improvements, and bug fixes. Tracking dependencies manually can be quite burdensome, so fortunately there are tools that make our lives easier.
Despite the guidance to [minimize dependencies](/jbp/jbp-2.html), it is inevitable that most projects will have multiple dependencies. It is good practice to always work to keep these libraries up to date. Updating to newer versions protects against security vulnerabilities, and often brings new features, performance improvements, and bug fixes. Tracking dependencies manually can be quite burdensome, so fortunately there are tools that make our lives easier.

Before we begin, it is worth reiterating that the burden of dependency versioning should ideally be limited to [BOM files only](/JBP-1), as this drastically reduces the number of dependency versions we actually care about in our project. When we update a BOM, all of the referenced dependencies within that BOM are updated to newer versions, with the added benefit that we can have a higher degree of confidence in the transitive closure of all dependencies, which should result in greater compatibility and lower likelihood of diamond dependency problems.
Before we begin, it is worth reiterating that the burden of dependency versioning should ideally be limited to [BOM files only](/jbp/jbp-1.html), as this drastically reduces the number of dependency versions we actually care about in our project. When we update a BOM, all of the referenced dependencies within that BOM are updated to newer versions, with the added benefit that we can have a higher degree of confidence in the transitive closure of all dependencies, which should result in greater compatibility and lower likelihood of diamond dependency problems.

With that out of the way, we can now decide to investigate our dependencies locally, or defer it to automated services. We will cover both below.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
slug: /jbp/jbp-5
title: Make use of logging APIs
template: jbp
---

Having a good logging story in all Java applications can be a real lifesaver when something goes wrong. The challenge is learning what to log and how to use the logging frameworks to their full potential to spread log output across the numerous logging levels (e.g. info, verbose, warning, etc), as well as to not impact the performance of the application with all the extra logging code.
Expand All @@ -9,11 +10,11 @@ Java has a plethora of logging frameworks, so it can often be confusing how to g

## Choosing a logging framework

As noted in the best practice on [minimizing dependencies](/JBP-2), sometimes the decision about which dependency we should use is made for us by the fact that one of our dependencies has a dependency (directly or transitively) on a particular type of framework, and this is especially the case with logging frameworks! If we already find a logging framework on our dependency classpath, we should try our best to use it.
As noted in the best practice on [minimizing dependencies](/jbp/jbp-2.html), sometimes the decision about which dependency we should use is made for us by the fact that one of our dependencies has a dependency (directly or transitively) on a particular type of framework, and this is especially the case with logging frameworks! If we already find a logging framework on our dependency classpath, we should try our best to use it.

It's a good idea to not depend on a specific logger whenever possible, and instead rely on a facade. This means we do not need to directly concern ourselves with which particular logging implementation we use, and that gives us some flexibility to change as appropriate.

For users on Java 8 and earlier, a good choice is to use [SLF4J](http://www.slf4j.org/). For users who can baseline on Java 9 or later, it is preferable to [minimize our dependencies](/JBP-2) and make use of the [platform logging APIs that shipped in Java 9](https://www.baeldung.com/java-9-logging-api).
For users on Java 8 and earlier, a good choice is to use [SLF4J](http://www.slf4j.org/). For users who can baseline on Java 9 or later, it is preferable to [minimize our dependencies](/jbp/jbp-2.html) and make use of the [platform logging APIs that shipped in Java 9](https://www.baeldung.com/java-9-logging-api).

## Understanding logging levels

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
---
slug: /jbp/jbp-6
title: Configure build plugins
template: jbp
---

The Java ecosystem has created a lot of very useful build tools, the most popular being Maven and Gradle. Projects using these build tools are fortunate to have a good number of plugins that enable static code analysis and other kinds of project validation. These plugins can provide great insights into issues with your project, so it is worthwhile spending time enabling these plugins and ensuring that they are configured properly.

## Dependency checking

We've already spoken about using the Maven Dependency and Versions plugins for Maven in the [keep our dependencies up to date](/JBP-4) best practice.
We've already spoken about using the Maven Dependency and Versions plugins for Maven in the [keep our dependencies up to date](/jbp/jbp-4.html) best practice.

## SpotBugs

Expand All @@ -25,5 +26,4 @@ A good goal for SpotBugs in any project is to get the codebase entirely clean of

[JaCoCo](https://www.jacoco.org/jacoco/) is a code-coverage tool that can visually display the amount of code that is tested by your unit tests. It helps you to identify where test coverage may be lacking, and can be used, in conjunction with Maven, to enforce a minimum level of coverage at all times.

{% include note.html
content="We should not be so data-driven that we write tests purely for the sake of satisfying JaCoCo! Our goals in writing unit tests should always be to ensure correctness of our implementation, and to protect against the accidental introduction of bugs." %}
! We should not be so data-driven that we write tests purely for the sake of satisfying JaCoCo! Our goals in writing unit tests should always be to ensure correctness of our implementation, and to protect against the accidental introduction of bugs.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
slug: /jbp/jbp-7
title: Use the latest Java Long-Term Support release
template: jbp
---

Java releases are made every six months in a 'feature-release-train' model, rather than having a major release planned for every two to four years as historically was the case. This rapid release cadence enables Oracle, and other participants in the Java ecosystem, to designate *Long-Term Support* (or LTS) releases every three years, with the most recent LTS releases being Java 11 (September 2018) and Java 17 (September 2021).
Expand Down Expand Up @@ -34,7 +35,7 @@ If you are looking for a supported offering, your best choice is to choose a bui

## Ecosystem support for Java 9 and later

Oftentimes the reason a Java version choice is made is because of a dependency of our project having a hard dependency on a particular version of Java. This has often been true historically (and still holds true today to an extent) for application frameworks and other critical tooling, with their dependency on Java 8 (or, more precisely, their lack of support for Java 9 and later). Every day that goes by sees this argument become less strong, as more and more of the critical libraries and frameworks we depend on beginning to support Java 9 and later. It is beneficial every release cycle, when we are looking at [updating our dependencies](/JBP-4) to identify which, if any, of our dependencies still force us back to earlier versions of Java, and to ask questions:
Oftentimes the reason a Java version choice is made is because of a dependency of our project having a hard dependency on a particular version of Java. This has often been true historically (and still holds true today to an extent) for application frameworks and other critical tooling, with their dependency on Java 8 (or, more precisely, their lack of support for Java 9 and later). Every day that goes by sees this argument become less strong, as more and more of the critical libraries and frameworks we depend on beginning to support Java 9 and later. It is beneficial every release cycle, when we are looking at [updating our dependencies](/jbp/jbp-4.html) to identify which, if any, of our dependencies still force us back to earlier versions of Java, and to ask questions:

* Do I still need this dependency?
* Is there a suitable replacement for this dependency?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
---
slug: /jbp/jbp-8
title: A little copying is better than a new dependency
template: jbp
---

Libraries typically take an evolutionary path and grow in functionality over time, and it is often the case that if the library we depend on were decomposed into separate smaller libraries, we might be able to get away with using one or a few of these smaller libraries, as we rarely need to use *everything* in a library.

Whilst we can't control the scale and modularity of libraries we depend on, we do have the option to *copy* the critical parts of a library into our code. This may seem counter-intuitive at first - why would we copy a part of a library when build tools such as Maven and Gradle make it so easy for us to just depend on it?

The answer is that if we can copy a small amount of a library into our code base, we are able to more easily abide by our other best practice of [keeping our dependencies minimal](/JBP-2). This may seem like a minor victory, but by copying the required code into our code base, we can avoid all of the diamond dependency issues of having an explicit, full-blown dependency in our project.
The answer is that if we can copy a small amount of a library into our code base, we are able to more easily abide by our other best practice of [keeping our dependencies minimal](/jbp/jbp-2.html). This may seem like a minor victory, but by copying the required code into our code base, we can avoid all of the diamond dependency issues of having an explicit, full-blown dependency in our project.

## Is this shading?

Technically this is shading, which goes against the guidance to [avoid shading dependencies](/JLBP-10), so it is important to understand all of these implications. In particular, we have to appreciate that any code we copy into our project will no longer be maintained by the project where the code originated, and so it becomes our burden to keep this updated (if necessary).
Technically this is shading, which goes against the guidance to [avoid shading dependencies](/jbp/jlbp-10.html), so it is important to understand all of these implications. In particular, we have to appreciate that any code we copy into our project will no longer be maintained by the project where the code originated, and so it becomes our burden to keep this updated (if necessary).

When we copy code into our project, we should move the code into a package namespace that we own. This prevents classpath collisions if the library comes onto our classpath through other means, but this also means that we have to be careful when we copy this code into our project that it does not form part of our public API (that is, that it is not exposed through our APIs to end users). This is because we will confuse our users with the existence of equivalently-named APIs in different package namespaces.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
slug: /jbp/jlbp-1
title: Characteristics of good API
template: jbp
---

As software engineers we write code every day, and it is inconceivable that this code would ever exist in a vacuum, isolated from all other software ever written. Never has the 'standing on the shoulders of giants' metaphor been more apt than it is today in software engineering, with GitHub, Stack Overflow, Maven Central, and all other directories of code, support, and software libraries available at our fingertips.
Expand Down
Loading

0 comments on commit 6e29d91

Please sign in to comment.