Easy steps for better living with Java
Here are some easy steps I try to do on every Java-project I’m working on. They are based upon what I’ve seen as major problem sources in different code bases, and should be fairly easy to apply - at least on new code.
Tooling
The first steps are to enable some basic tooling that can help us keep the project a bit of better shape. All of these comes with a cost, namely build time, but it’s totally worth it.
Enable strict compilation
The javac-compiler exposes flags that increases the amount of linting the compiler does, and how it should treat the warnings produced by the compiler.
Enabling it is as easy as adding -Xlint:all -Werror
to the compiler arguments.
Using maven with the maven-compiler-plugin version 3.1 and up, simple do something like this in your pom.xml:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<groupId>org.apache.maven.plugins</groupId>
<version>3.1</version>
<configuration>
<compilerArgs>
<arg>-Werror</arg>
<arg>-Xlint:all</arg>
</compilerArgs>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
or if you use Gradle, then this should do the trick:
compileJava {
options.compilerArgs << "-Xlint:all" << "-Werror"
options.encoding = 'UTF-8'
}
Use static analysis
Static analysis is an extremely useful tool for catching potential errors early. Aside from the built-ins in the compiler, my favorite tool is FindBugs. The latest release (3.0) includes support for Java 8, which has been a deal breaker for a while.
Adding it as a part of your build is quite easy, add the following snippet to your pom.xml:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>findbugs-maven-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<xmlOutput>true</xmlOutput>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
This does add a bit of time to your build, but I think it is worth it. If your codebase is so large that this adds an unacceptable amount of build time, consider running it on jenkins or something.
Don’t allow duplicate library versions
Any non-trivial java application that uses third-party libraries are prone to having multiple versions of libraries included in the classpath. Since the java classloading is not guaranteed to be deterministic, this can cause you immense headaches really fast.
If you use gradle with the java plugin, you can detect these problems out of the box. Just add the following to your build file:
configurations.all {
resolutionStrategy {
failOnVersionConflict()
}
}
If you use Maven on the other hand, you probably want the enforcer plugin. To use it, add the following to your pom.xml:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>1.3.1</version>
<executions>
<execution>
<id>enforce</id>
<configuration>
<rules>
<DependencyConvergence />
</rules>
</configuration>
<goals>
<goal>enforce</goal>
</goals>
</execution>
</executions>
</plugin>
The code
These steps are more related to the code you write. There are, as with all things, exceptions where these guidelines does not apply, but in the general case, these should serve you well to remember.
Null is the exception
We’ve all seen NullPointerException
stacktraces in our logs, and they are always annoying to fix. In my experience, they are often just fixed with a null-check, and then passing the problem on the caller of the function where occurred. This results in a code base littered with null-checks, and a lot of defensive - or blatantly unsafe code, where you are never sure if something is set or not.
Unfortunately we have no way of disabling nulls in Java, so all the burden is on us to defend the outer borders of our code against this.
Now, some data is inherently optional, but they can be marked as such. In Java 8 you have a built-in Optional<T>
-type, and if you are on 1.6 or 1.7 you can use the Optional-class from Guava.
So, instead of returning null, return Optional.empty()
or if returning an empty value does not make sense, throw an exception. Its what they are for.
Do not allow broken data
Most enterprisy java projects have to deal with broken external systems, such as old databases, or some SOAP-service where everything is optional. Regardless of the type, we should guard our own software from the dirty data. Use Optional<T>
for the fields where is does make sense, and if the data provided from the external system does not hold up to a minimal set of requirements, don’t pass it along, drop it on the floor!
If these causes some requirement not to be fulfilled, then fix it in the integration layer, but do not send shitty data down the line.
One way of ensuring that bad instances of your models can be created, use the constructor and validate the input.
public class Person {
private final String name;
private final Integer age;
public Person(final String name, final Integer age) {
this.name = checkNotNull(name, "name cannot be null");
this.age = checkNotNull(age, "age cannot be null");
}
}
checkNotNull(obj, message)
is included with Guava, and throws a NullPointerException
if the given instance is null
.
One pattern we have deployed successfully is to have a “safe factory” for the model, which returns an Optional<T>
of the instance.
public Optional<Person> safeCreate(final String name, final Integer age) {
try {
return new Person(name, age);
} catch (NullPointerException npe) {
LOGGER.warn("Invalid data provided for {}. Name: {}, Age: {}", Person.class.getName(), name, age);
return Optional.empty();
}
}
Using this pattern you can easily map input data to a list of valid instances. Using Java 8, one might do something like this:
externalSystem.getPeople().stream()
.map(p -> Person.safeCreate(p.getName(), p.getAge()))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
or in Java 6/7:
List<Optional<Person>> transformed = new ArrayList<>();
for (RawPerson rp : externalSystem.getPeople()) {
transformed.add(Person.safeCreate(rp.getName(), rp.getAge()));
}
List<Person> persons = Optional.presentInstances(transformed);
The point is not to include objects that you are unsure of through the stack. The complexity starts dominating the system so fast.
Default to immutable objects
Prefer and default to Immutable objects. Throw out the old Java Beans book, and enter into a much nicer world. Creating mostly immutable objects is quite easy. Mark your fields as final, and only allow them to be set during construction. NO SETTERS!
Collections require a bit more attention, but using the immutable collections from Guava takes you a long way.
If you need to modify the instance, return a new copy with the modified data.
IoC Containers
IoC containers are quite popular, with Spring arguably is the most popular one. They can do you good, but be aware of the ease. Since it is so easy to inject instances all over the place, things get real messy very fast. See https://klette.us/some-tips-to-prevent-dependency-hell-in-the-land-of-ioc/ for patterns on how to reduce the pain.
Avoid if possible. There is nothing wrong with calling new MyClass()
by yourself.
Reflection
Reflection is an escape-hatch that should only be used if you have no other options. It might be tempting, but it destroys everything from performance, to automated refactoring tools, and flow analysis tools.
Please just dont. Find another way, and you’ll be happier for it.
Documentation
In most real projects, there comes a time where you have to get your hands dirty, and code some complex logic, or deal with some other broken system. Documentation in these cases is paramount.
I have seen a secondary effect to documenting the code, that is documenting the methods before you write the implementation. Similar to writing the tests first, this seems to, for me at least, keep me much more honest and the methods are much clearer.
Give it a try for a couple of days.