Stop Using @PostConstruct in Your Java Applications
This story is a sequel to one of my previous articles Stop Using Setters. If you haven’t read it yet, you should check it out.
If you are a Java developer, there is no need to explain the concept of the Spring Framework. We are well aware of @Component
, @Autowired
, and many other useful annotations. Perhaps you prefer Jakarta EE stack, then your choice is @ManagedBean
, @Inject
and so on. In both of these cases, there is one thing that seems convenient but makes your code less maintainable and more fragile. The @PostConstruct
annotation. In this article, I’ll try to convince you that you should forget about it for good.
What’s the purpose of @PostConstruct
? Usually, we use it to defer some events that must be executed after object instantiation. For instance, suppose our application needs a service that accepts a user’s request and adds it to the queue. How shall we design QueueService
? Here is one option.
Firstly, QueueService
checks whether a user has required permissions. If it’s so, it adds the new request to the NativeQueue
. But the queue needs to be initiated in the beginning. We can’t put the initialization inside addQueueEvent
, because it should be invoked only once. Using @PostConstruct
block seems like a good approach. But if you watch closely, you will notice that it’s not so different from the setter. In both cases, we create something that is not ready for usage yet. And then after some special manipulations, an object becomes complete. Sadly it’s not the only problem. Let’s discuss them one by one.
There is no way to write a proper unit test for QueueService
As you can see, initializeQueue
method is private
. Spring (as well as Jakarta EE) does not care about it, because the framework uses Reflection API to execute annotated methods. But we do care about it. It’s impossible to cover this method with unit tests due to the fact that it’s inaccessible from the outer scope and nothing calls it inside the class.
How can it be fixed? Well, the easiest way is to change the method scope. We can declare initializeQueue
as package-private
or even public
one. But in the same way, it allows other components to call it whenever they like to. That may lead to unpleasant consequences (like unexpected queue flushing).
One may say that instead, we can use Reflection API to call the method from the test. It means that the functionality still remains hidden to the world. Perhaps I’ll write another story about it, but for now, I just want to say:
Please, don’t use
Reflection API
in your tests! It will bring you nothing but maintainability hell.
Recently I had a negotiation with my colleague about putting @PostConstruct
on private
methods. He told me that it’s absolutely fine. And if we want to verify the class, we have to start the Spring Context in our test. I absolutely agree that integration tests should be present as well as unit ones. But I consider that unit tests are necessary in any case. A class must be always verifiable separately from the system. Integration tests should expand but not replace unit tests.
The class becomes more fragile and less reusable
Suppose we need to add some default requests to the queue on system launching. That’s how it can be done.
The funny thing is that this code is not deterministic. It will do its work but only from time to time. There is no guarantee that @PostConstruct
will be executed before QueueFulfillingService
instantiation. So, sometimes the app shall be crashed with an unexpected exception.
Can it be fixed? Well, kind of. We can replace @PostConstruct
with BeanFactoryPostProcessor
and PriorityOrdered
interface. The first one defines an action that ought to be executed after the object's instantiation. The second interface tells the Spring the order of the component’s initialization. Although it solves the problem, it shall make our code verbose and too coupled with the Spring ecosystem. Not the best way to deal with such an easy case.
NativeQuery initialization failure means the app crashing
Is the QueueService
an obligatory feature? Not necessary. But if NativeQuery.init()
fails, it will crush the whole application. One may say that it’s exactly what we want from the system. If something went wrong, it would be better to identify an error as soon as possible. I doubt this statement. And here is why.
I use Gmail almost every day. If you do as well, you know that you can examine contact info by putting a mouse pointer on the email's sender name. Have you known that this operation invokes another HTTP-request? You can notice it by opening the development console. I think that this feature is not so important. If there are some troubles on the server-side, it’s absolutely fine to see no popup. But if this feature would be verified on application startup? Any error would mean the whole system stop. I think that the price is just too high.
As far as I know, this feature is implemented in a separate microservice. In this case, startup failure would not kill the whole email cluster, but my point is that all needed initializations should be invoked only when it’s needed, not in advance.
Solution
The most obvious solution is to put all the required steps inside a constructor. Although it solves the first two problems (unit testing and reusability), it does nothing with failure on startup. What we need are lazy initializations that must be called only once and when it’s needed. Here is the way.
CachedResultSupplier
is a decorator for Supplier
interface. It calculates the given lambda only on the first get
call. Further invocations return the cached value.
As a matter of fact, NativeQueue
instantiation and initialization executes only on the first call of addQueueEvent
. If something breaks, we can notify a user correctly. Because now we are aware of possible errors and we can deal with them in a proper way. Besides, we can add advanced logging and audit to identify an error more efficiently.
Conclusion
I hope that I convinced you that @PostConstruct
usage is a bad practice. More than that, it can be easily replaced even without Spring or Jakarta EE features. If you have any questions or suggestions, please, leave your comments down below. Thanks for reading!