How Lombok saved my ass
By Tomasz Kuczma
Lombok is a Java library that generates boilerplate code for you during the compilation. You probably use it or at least heard how it can clean the code with annotations like @Data
or @Value
.
So I am not going to write yet another article on how to use the most popular annotations. I am going to show you one of the two most underrated features in Lombok - @Cleanup
.
@Cleanup
It is just (or maybe even) an alternative to try-with-resource introduced in Java 7. Wait. What? In Java 7? When was that? On July 28, 2011. Why am I writing about alternative to feature introduced natively in Java almost eight years ago? We will get back to it soon. Let’s see a simple example first. Copy input stream into output stream with Lombok:
@Cleanup InputStream in = new FooInputStream();
@Cleanup OutputStream out = new BarOutputStream();
in.transferTo(out); // since Java 9
With plain Java:
InputStream in = new FooInputStream();
try {
OutputStream out = new BarOutputStream();
try {
in.transferTo(out);
} finally {
if (out != null) {
out.close();
}
}
} finally {
if (in != null) {
in.close();
}
}
You can find low-level file copy example in Lombok’s documentation
As you can see Lombok handles nested resources closing properly. The same as try-with-resource syntax:
try(InputStream in = new FooInputStream();
OutputStream out = new BarOutputStream()) {
in.transferTo(out);
}
Why Lombok is better
There is little gotcha when using Java’s try-with-resource syntax. The resource has to implement AutoCloseable
interface, so the compiler can use void close()
method for closing a resource.
Lombok’s @Cleanup
, it doesn’t have to implement this interface. The resource just needs to have some closing method which by default is void close()
(but you can specify your own method name responsible for closing resource).
It may be irrelevant as all standard libraries implement AutoCloseable
for all resources. However in the past, I figured out that the AutoCloseable
interface is not implemented in some older libraries like Jersey Client 1 (it does in version 2).
Due to that, a production system crashed (of course after office hours) and engineers were not able to find anything suspicious quickly using standard debugging tools.
Whole Tomcat based application slowed down and started returning HTTP 500 status code as Jersey client was not closing resources (HTTP connection in this case) properly.
Let see how Lombok can save use in simple example:
FooDto getFooDtoFromRestService() {
@Cleanup
ClientResponse response = client
.resource("https://example.com")
.get(ClientResponse.class);
if(response.getStatus() == HttpStatus.SC_OK) {
return response.getEntity(FooDto.class);
}
throw new UexpectedStatusCodeException();
}
FooDto getFooDtoFromRestService() {
ClientResponse response = client
.resource("https://example.com")
.get(ClientResponse.class);
try {
if(response.getStatus() == HttpStatus.SC_OK) {
return response.getEntity(FooDto.class);
}
throw new UexpectedStatusCodeException();
} finally {
if (response != null) {
response .close();
}
}
}
Let’s analyze the above code a little bit.
ClientResponse
closes resources internally (input stream representing HTTP body) when method getEntity
is called. But what if it is not called. For example, we just check the status code like in the example. Then, unfortunately, connection/thread/memory is not released and we have a resource leak.
In that case, @Cleanup
saves us - it makes the program work properly (no resource leak) and hide boilerplate code. Let me note that ClientResponse::close()
method is idempotent so it is safe to call it after reading the response body (which is obviously a great design decision).
The most important fact is that no IDE, compiler nor Sonar would catch such bug like this one due to the fact that Jersey 1 does not implement AutoCloseable
interface.
Lombok also gives an example for SWT in documentation:
@Cleanup("dispose")
org.eclipse.swt.widgets.CoolBar bar = new CoolBar(parent, 0);
The proper resource clean up will be done using dispose
(see SWT: Managing Operating System Resources
Rule 1: If you created it, you dispose it.)
Isn’t @Cleanup a RAII pattern?
RAII (Resource acquisition is initialization) pattern says that holding resource is related to object lifetime (so object’s resources should be initialized in its constructor and released in its destructor). This pattern saves us from resource leak if there is no object leak. It is highly used in C++ to implement abstractions like “guards” e.g. lock guard . Let’s see simplified implementation of generic guard and example usage:
template <typename T> class Guard {
T& res;
public:
Guard(T& resource) : res(resource) {
}
~Guard() {
res.close();
}
};
void exampleFunction() {
FooResource fooResource;
Guard<FooResource> g(fooResource);
fooResource.doSth();
}
Guard
class just holds resource and calls close()
method in the destructor. In exampleFunction()
, you can see that we wrote one additional line to create a guard object.
C++ compiler does the rest of the job. When exampleFunction()
execution is completed (even if an exception has been thrown) guard’s destructor will be called and resource release.
The same pattern is used by @Cleanup
and try-with-resource syntax in Java - resources are initialized and released in the same method.
It is just handled a little bit different because Java uses automatic garbage collection where C++ leaves memory management to programmers (fortunately with RAII support).
Conclusion
@Cleanup
is a good alternative for try-with-resource syntax especially if you have to deal with older libraries. It can prevent serious resource leaks in production systems and does it really smoothly.
It can really save your ass and make you and your customers sleep well. In newer systems, where classes implement AutoCloseable
interface, it is up to you if you want to use @Cleanup or try-with-resource.
Oh and yes, @Cleanup
uses 1 line and 1 indent less ;)
What is the second most underrated feature in Lombok? Stay tuned for my next post!
Software engineer with a passion. Interested in computer networks and large-scale distributed computing. He loves to optimize and simplify software on various levels of abstraction starting from memory ordering through non-blocking algorithms up to system design and end-user experience. Geek. Linux user.