A Defense of Plain Old Java
Published: 2021-09-07 15:09:01

by Larry Hohm – September 1, 2020

The Java ecosystem is exploding with frameworks. There are frameworks for dependency injection, web services, servlet containers, data access, persistence, configuration, user interfaces, and more. Like many Java developers, I have a love-hate relationship with frameworks. I love them when they make my life easier, and hate them when they make it more difficult.

Some developers are eager to embrace frameworks, some are reluctant, and many are somewhere in the middle of the scale. I am generally reluctant to adopt frameworks, for a number of reasons. I will continue to use them, especially those preferred by my teammates. But I believe it is always worthwhile to think twice before adopting a framework. I say this out of deep respect for the power, flexibility, expressiveness, and simplicity of plain old Java.

I offer one example to illustrate my viewpoint. Suppose you want to select a good strategy for implementing dependency injection in a new project. The three main alternatives these days are Spring, Guice, and plain old Java. Before weighing the pros and cons of Spring or Guice, let’s ask a simple question: why do we need a framework at all?

(I realize that Spring Boot is used widely at OpenMarket and throughout the industry, and is favored by many thought leaders in our industry. And it is much, much, more than just a dependency injection framework. My discussion here is concerned only with the issue of dependency injection.)

Dependency injection is a very simple idea. Your code injects one object (the dependency) into another object (the target). What could be simpler? This can be done with constructor injection or setter injection. It is hard to imagine anything simpler! Every Java developer understands constructors and setters. We all learn about them in elementary school. How could any framework make this task simpler? Why do we need a framework for dependency injection?

Some will suggest that if you need to inject many dependencies, a framework would be better. A constructor with lots of parameters can get ugly, because the order of the parameters is important, so the developer who is using the constructor might need to track down the source code or documentation for your class. It’s even worse if some of the parameters are optional.

However, this problem can be solved easily using plain old Java. If you have two or three dependencies, it’s not much of a problem. If you have more, then you should probably think about applying the Single Responsibility Principle, and refactor your class into two or more classes. But even if you decide that you want one class with lots of dependencies, it is still easy to implement in plain old Java, using the Builder Pattern. (Joshua Bloch has a nice discussion of it ) For example:

    public class MyDao {
        private final AuthClient authClient;
        private final Configuration config;
        private final DataSource dataSource;
        private MyDao(Builder builder) {
            this.authClient = builder.authClient;
            this.config = builder.config;
            this.dataSource = builder.dataSource;
        }
        public static Builder builder() {
            return new Builder();
        }
        . . .
        public static class Builder {
            private AuthClient authClient;
            private Configuration config;
            private DataSource dataSource;
            public Builder withAuthClient(AuthClient authClient) {
                this.authClient = authClient;
                return this;
            }
            public Builder withConfiguration(Configuration config) {
                this.config = config;
                return this;
            }
            public Builder withDataSource(DataSource dataSource) {
                this.dataSource = dataSource;
                return this;
            }
            public MyDao build() {
                return new MyDao(this);
            }
        }
    }

USAGE:

    MyDao myDao = MyDao.builder()
        .withAuthClient(new AuthClient())
        .withConfiguration(getConfigurationFromSomewhere())
        .withDataSource(getDataSourceFromSomewhere())
        .build();

The builder pattern offers a hybrid approach to dependency injection: setter injection (with fluid setters) is used to inject dependencies into the builder, and constructor injection is used to inject the builder into the target class. This makes it easy to build immutable objects, and to guarantee that all required dependencies are set before constructing the target object. (There are plugins for IDEA that auto-generate builders, and the Lombok library also auto-generates them.)

Suppose your project has dozens of objects that need to be initialized with dependency injection, and you want to have one central place in your code where all of the “wiring” takes place. Is a framework needed to handle this? Of course not. It is absolutely trivial to do this in plain old Java. Most web applications have a high-level object representing the entry point for the entire web app, such as a ServerMain or an ApplicationContext, and that is an obvious place to instantiate your objects with all of their dependencies.

Suppose your ServerMain or ApplicationContext gets bloated because of all the “wiring” needed. Is a framework needed to handle this situation? Of course not. It is absolutely trivial to break up your class into a few well-organized classes, all within one package that is responsible for wiring all components in your project.

On the other hand, if we use a dependency injection framework, life is more complicated. For example, with the Spring framework, dependency injection is typically handled by Spring’s autowiring, which is often cited as an advantage of using Spring. But autowiring is not trivial. It comes in many flavors, such as byName, byType, constructor, and autodetect. And it is controlled by a number of annotations working together, including @Autowired, @Configuration, @Component, @Bean, @Server, and @Qualifier. These annotations have their own syntax, semantics, and nuances; they effectively comprise a domain specific language. It takes some studying to understand their complexities. Autowiring is significantly more complicated than constructor injection and setter injection, which require no annotations.

Autowiring also complicates unit testing. If you are trying to test a class with autowired dependencies, simple Junit tests won’t be enough. Your class depends on the Spring container to inject its dependencies, but simple Junit tests are not running inside of a Spring container, so your autowired dependencies will be null. To make unit tests work, you need to use another Spring annotation, @RunWith, and perhaps @SpringBootTest. Or you could use another framework such as Mockito and the @InjectMocks annotation. And if your tests do not behave as expected, you might need to research the semantics for @Runwith or @InjectMocks. Your unit tests become tightly coupled to the framework.

Spring’s autowiring is a classic example of taking something simple and making it complicated. If you search the web for “Spring autowiring”, you will find plenty of tutorials and user guides that explain how to use it; and you will also find plenty of discussions among people having trouble understanding how it works, getting it to work, and trouble-shooting it when it doesn’t work as expected. Occasionally, you will hear people talk about “autowiring hell”, when they are deep into trouble-shooting, and a bit frustrated by it. If you search the web for “Guice dependency injection” you will find similar results.

By contrast, if you search the web for “constructor injection” or “setter injection”, you won’t find anyone who has had trouble with them. There is no need for lengthy tutorials and documentation. No one has trouble understanding them, implementing them, testing them, or trouble-shooting when something doesn’t work. Writing unit tests for classes that use constructor or setter injection could not be more straight-forward. You can inject mocks or stubs in exactly the same way you would inject real objects in your production code. You can write your own mock objects or use your favorite mocking framework. All of this can be done without the magic of mysterious annotations that add another layer of complexity to your code, and make your code tightly coupled to a framework. That is the beauty of plain old Java.

It is instructive to compare a simple task, such as dependency injection, to a complicated task, such as JSON parsing and serialization. It would be tedious and error prone to write your own JSON parser and serializer. This is a task that cries out for a library or framework. The widely used Jackson library is a good example of an annotation-driven library that greatly simplifies our lives. It stands in stark contrast to Spring’s autowiring, which complicates our lives.

Frameworks often promise to reduce the amount of boiler-plate code in your projects, which makes them enticing. But they often come with a price to be paid when the time comes for testing and trouble-shooting. It is often straight-forward to test and trouble-shoot plain old Java. It is often difficult to test and trouble-shoot code that is replete with framework annotations.

Frameworks typically lead to tight coupling. In many cases, once you start using a framework, it creeps into every corner of your code, and your code quickly becomes tightly coupled to the framework. It would be difficult to remove a dependency on Spring from a project that uses it; doing so would basically require a complete rewrite.

The next time you are tempted to reach for a framework, pause for a moment, take a breath, and ask yourself: what problem are you trying to solve with the framework? And how easy or difficult would it be to solve the problem with plain old Java?