This repository contains illustrative material for a talk I gave on 15th October 2016 at Hong Kong Code Conf, entitled In Defence of Boilerplate Code. The accompanying slides may be found here.
The main argument of the talk is that while boilerplate code is certainly undesirable all else being equal, sometimes the cure is worse than the disease: abstraction techniques used to eliminate boilerplate code can sometimes have hidden costs which cause worse problems than the boilerplate itself.
The repository consists of several Java modules, each of which illustrates this theme in a different way.
The 00-app
module contains a set of "business" interfaces and objects used
by several other modules. I have made no attempt to give them realistic names
and purposes: I simply called the interfaces A
, B
, and C
and
the implementations AImpl
, BImpl
, and CImpl
.
AImpl
depends on B
and C
; BImpl
depends on C
; so
we have a simple acyclic dependency graph.
The 01-depinject
series of modules illustrates several different
techniques for wiring the application objects together, and overriding those
wirings for testing.
The idea is we want to use a single instance of each interface: for example
AImpl
and BImpl
should share the same instance of CImpl
.
There are four modules in total: three use the common dependency injection
frameworks Spring,
Guice and
Dagger, while the fourth,
01-depinject-diy
, uses a simple framework-free technique.
My preferred method is 01-depinject-diy
. At the cost of a small amount of
boilerplate code (the fields in the Config class and their initializers), it is
simple, requires no dependencies or special compiler plugins, and discourages
XML configuration, Aspect Oriented Programming, and other techniques which
complicate the application and make it harder to reason about, debug, test,
maintain etc.
Defenders of dependency injection frameworks will doubtless produce a long list
of features of their pet framework which are missing from the simple approach in
01-depinject-diy
. But this is actually the point: all my application needs
is some simple object wiring and simple configuration. Why pull in a complex
framework when your needs are simple? I would further argue that this approach
can be smoothly extended to meet the needs of the vast majority of real-world
applications.
The overall philosophy is: rather than start with a large framework which even complex applications will only ever use a small fraction of, it is better to start with a set of abstractions which are as simple as possible, provided you see no barriers to extending them as your application grows.
If I were forced to use a dependency-injection framework, I would prefer Guice or Dagger to Spring, as they are more focussed. Guice uses a runtime reflection approach similar to Spring, which makes it harder to reason about the application's behaviour at compile time; Dagger uses compile-time code generation, which addresses this problem at the expense of complicating the build (for example, not every JVM language will necessarily have the required annotation-processing plugins).
In 01-depinject-spring
and 01-depinject-diy
, I have illustrated two
approaches to managing configuration, in the spirit of each module.
In the former, I have used Spring's implicit configuration parsing through the
@Value
annotation with ${}
placeholder expressions. This is supposed
to give flexible configuration management through the Spring
environment,
with the ability to define a hierarchy of PropertySource
s, for example
allowing properties from the configuration file to be overridden on the command
line.
In the latter, I have written a small class ConfigProperties
which wraps
java.util.Properties
, providing a method which throws an exception when a
property is missing, with a message which includes the source from which the
property was loaded.
By now, you will not be surprised to learn that I strongly favour the latter approach. It greatly simplifies configuration management, making it far less likely for mistakes to occur (as each property has one unambiguous source), as well as far easier to find and fix errors when they do happen. In addition, it requires no new dependencies on the classpath. These benefits are easily worth the price of the small amount of boilerplate code required to parse and use a properties file.
Using simpler, more direct abstractions tends to produce better error messages.
To demonstrate this, I have included an errors
package in both
01-depinject-spring
and 01-depinject-diy
.
In MissingBeanDemo
, I show what happens when you forget to declare a
required bean in Spring: you get a long and complex exception message. The
longer the chain from the desired bean to the missing one, the longer the
message, but even in this very simple case, it is meaty (23 lines)!
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'a' defined in uk.org.medworth.boilerplate.spring.config.Config: Unsatisfied dependency expressed through method 'a' parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean found for dependency [uk.org.medworth.boilerplate.C]: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:749)
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:467)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1128)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1023)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:510)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:751)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:861)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:541)
at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:84)
at uk.org.medworth.boilerplate.spring.errors.MissingBeanDemo.main(MissingBeanDemo.java:14)
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean found for dependency [uk.org.medworth.boilerplate.C]: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1463)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1094)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1056)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:835)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
... 19 more
Users of Spring can expect to spend large amounts of time reading and debugging
error messages similar to this one; notice how the stack traces contain no
frames whatsoever from the Config
class, so in more complex cases (perhaps
involving missing annotations or circular dependencies), the only options are to
debug the Spring Framework code itself (not an enjoyable exercise) or trawl the
Web in the hope that someone else has had a similar problem and posted the
solution to it.
There is no equivalent of MissingBeanDemo
in the diy
module, because
the plain vanilla Java compiler would make it impossible to make such a mistake
(as you would have to call a method which does not exist). So here we see
another advantage of using a little boilerplate: the compiler can help us check
that it does what we want it to do!
In SpringMissingPropertyDemo
and DIYMissingPropertyDemo
, I show what
happens in the two approaches when you refer to a property which does not exist
in the configuration file. In the DIY version, you get a simple straightforward
exception message telling you which property name was missing from which
configuration file:
Exception in thread "main" uk.org.medworth.boilerplate.diy.config.ConfigProperties$MissingPropertyException: Key [Does.Not.Exist] missing from properties loaded from classpath file [app.properties]
at uk.org.medworth.boilerplate.diy.config.ConfigProperties.getRequiredString(ConfigProperties.java:46)
at uk.org.medworth.boilerplate.diy.errors.DIYMissingPropertyDemo.main(DIYMissingPropertyDemo.java:12)
In the Spring version, you get a message many times longer, full of class names
such as AutowiredAnnotationBeanPostProcessor
and
DefaultSingletonBeanRegistry
, but missing the most important piece of
information, namely the name of the file in which the program looked for the
missing property!
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springMissingPropertyDemo.WrongConfig': Injection of autowired dependencies failed; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'Property.Doesnt.Exist' in string value "${Property.Doesnt.Exist}"
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:355)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1219)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:543)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:751)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:861)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:541)
at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:84)
at uk.org.medworth.boilerplate.spring.errors.SpringMissingPropertyDemo.main(SpringMissingPropertyDemo.java:22)
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'Property.Doesnt.Exist' in string value "${Property.Doesnt.Exist}"
at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:174)
at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:126)
at org.springframework.core.env.AbstractPropertyResolver.doResolvePlaceholders(AbstractPropertyResolver.java:219)
at org.springframework.core.env.AbstractPropertyResolver.resolveRequiredPlaceholders(AbstractPropertyResolver.java:193)
at org.springframework.context.support.PropertySourcesPlaceholderConfigurer$2.resolveStringValue(PropertySourcesPlaceholderConfigurer.java:172)
at org.springframework.beans.factory.support.AbstractBeanFactory.resolveEmbeddedValue(AbstractBeanFactory.java:813)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1076)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1056)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:566)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:349)
... 17 more
Ask yourself which of these two error messages you would prefer to see in a real application! I know my answer.
The reason the property filename does not appear in the Spring error message is presumably that Spring allows a hierarchy of property sources, so it does not know that the missing property was supposed to come from the file, as opposed to, for example, the command line.
It would be theoretically possible for Spring to log a summary of the property source hierarchy in this case, which would aid debugging. However my overall point remains that Spring's design sacrifices simplicity and ease of debugging for a flexibility which brings no real benefits for a great many applications.
The 02-transactions
module illustrates two different ways of using
Spring's transaction management
features.
This is not intended as an endorsement of Spring's facilities in this area:
rather, it is intended to demonstrate the consequences of using Aspect Oriented
Programming (AOP) and how those consequences can often be avoided at the expense
of just a little boilerplate code.
The module contains two different implementations of the same DAO
interface: AnnotatedDAO
uses Spring's @Transactional
annotation,
which is the style typically recommended in Spring's documentation and
tutorials, whereas BoilerPlateDAO
avoids the use of AOP by injecting the
Spring PlatformTransactionManager
into the class and using
TransactionTemplate
(this is what the Spring documentation calls
programmatic transaction
management).
Both implementations do nothing more than throw an exception. Running
TransactionalApp
runs both: in both cases you will see "rollback" being
invoked on the transaction manager, followed by a stack trace. With the
"boilerplate DAO", you will see just one Spring stack frame in the trace:
java.lang.RuntimeException: So you can see the stack trace
at uk.org.medworth.boilerplate.tx.BoilerPlateDAO.lambda$getValue$2(BoilerPlateDAO.java:21)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:133)
at uk.org.medworth.boilerplate.tx.BoilerPlateDAO.getValue(BoilerPlateDAO.java:20)
at uk.org.medworth.boilerplate.tx.TransactionalApp.main(TransactionalApp.java:24)
Whereas with the "annotated DAO", there are eight, due to the AOP proxy, transaction interceptor etc:
java.lang.RuntimeException: So you can see the stack trace
at uk.org.medworth.boilerplate.tx.AnnotatedDAO.getValue(AnnotatedDAO.java:12)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
at com.sun.proxy.$Proxy19.getValue(Unknown Source)
at uk.org.medworth.boilerplate.tx.TransactionalApp.main(TransactionalApp.java:14)
This shows just how much complexity and additional hidden dependencies are introduced by the use of AOP.
If I were forced to use Spring transaction management, I would strongly prefer
the BoilerPlateDAO
style. In addition to the above advantages, this style
avoids the notorious differences between self-invocation and external invocation
caused by the use of AOP proxies (search for "self-invocation" in the Spring
transaction docs linked above). In my view, these advantages are well worth the
small amount of boilerplate code.
In 03-web-spring
and 03-web-vertx
, you will find two implementations
of an identical web application, inspired by this
tutorial.
It is a trivial application with two endpoints: a static index.html
and a
/speech
endpoint which renders a simple HTML template written in
ThymeLeaf according to the value of the
occasion
parameter passed in the URL.
In each module, you will find an integration test which starts a server and makes a few test calls against it.
03-web-spring
uses Spring Boot,
a framework which proudly "[t]akes an opinionated view of building
production-ready Spring applications... to get you up and running as quickly as
possible".
03-web-vertx
uses Vert.x (in particular
vertx-web), which has a very different
design philosophy: on its home page, under a large heading "Unopinionated", it
says "Vert.x is not a restrictive framework or container and we don't tell you a
correct way to write an application. Instead we give you a lot of useful bricks
and let you create your app the way you want to."
Comparing the two implementations, it is clear that the Spring Boot version
requires impressively little code: just one tiny controller class and a one-line
main
method. We don't have to explicitly tell it to serve static content
from the resources/static
folder, which template engine to use, where to
find the templates, or even where the controller and main class are to be found:
all this is done automatically through standard conventions and classpath
scanning.
The test is also very concise: just a couple of annotations and an
@Autowired
REST template, and we can easily write three methods testing
the key cases.
By contrast, the Vert.x implementation requires us to explicitly create an HTTP server and router, register the various handlers, and render the template. And in the test, we need to explicitly find a free port, start the server, and write asynchronous code to make the assertions.
However, the Spring Boot approach has considerable hidden costs. The first
becomes evident as soon as you create the module: Spring Boot expects you to
inherit from a special parent POM, spring-boot-starter-parent
. This means
we cannot inherit any of the default behaviours we defined in our own root
pom.xml
. Since we want to use certain plugins to enforce hygiene (see
below), we have to repeat those sections.
Secondly, Spring Boot pulls a lot more dependencies onto the classpath: analysing the compile-time and runtime dependency trees for the two POMs (i.e. ignoring test dependencies) gives the following results:
03-web-spring
: 39 JARs, total size: 20.2 Mb, total classes: 13,01603-web-vertx
: 26 JARs, total size: 8.1 Mb, total classes: 4,898
So Spring Boot is causing us to pull 1.5 times as many JARs, almost 2.5 times as many bytecode megabytes, and over 2.5 times as many classes onto our classpath, compared to the identical application written in Vert.x.
Combining the increased dependencies with the repetition from the root POM in
03-web-spring
, if you run wc -l
on the repo files for the two
modules, the total line count is actually about the same. (Admittedly, I didn't
have to explicitly list each direct dependency in the 03-web-spring
pom.xml
- I could have just let them come in from the parent POM as in
the demo - but
I think this is a good practice and I have done it consistently across all
the modules.)
Thirdly, look at a representative summary of Maven build times (starting from clean on my system):
[INFO] 03-web-spring ...................................... SUCCESS [ 4.848 s]
[INFO] 03-web-vertx ....................................... SUCCESS [ 1.638 s]
So the Spring Boot approach is almost three times slower to compile and test: by far the slowest module in the project. 4.8 seconds might not seem like much, but this is a trivial web app, and the duration is only going to go up as more realistic functionality and tests get added. Having a fast build is a major benefit in achieving agility and avoiding wasted time.
(I initially thought this performance difference might be at least partly due to
the fact that the spring-boot-maven-plugin
produces a "fat JAR", bundling
the app with all its dependencies in a single JAR, but then I commented out the
invocation of the plugin from the 03-web-spring
POM and found it made
almost no difference.)
If you look in the build logs, you will see that the Spring Boot test logs a considerable volume at DEBUG level by default. I'm sure there is a way to turn this off, but the fact that I tried various web searches and configuration options for several minutes without success is a small taste of the kind of problems the "opinionated" everything-implicit approach can cause.
Fourthly, in a similar way to what we saw earlier with "regular" Spring, the "magic" behaviours of Spring Boot (the classpath scanning, the property source hierarchy, the annotation language, the auto-discovery of static files and templates) cause considerable fragility: for example, a small change in the classpath or a small refactoring could break the app completely in a non-obvious way. These features also, again, introduce dynamic behaviour which is not predictable from reading the code or using static analysis tools.
The same features which got us up and running so quickly could easily cost us hours of debugging and trawling through debug logs as soon as we do something which slightly conflicts with Spring Boot's expectations.
The vast majority of software engineering effort is spent on maintenance, not initial construction: it is the former we should be optimising for, not the latter. I would rather have "wiring" like property reading and web request routing explicitly in the application code, so it is transparent, readable, searchable etc in the future - even if that means a little bit more typing at the beginning to achieve a working application.
In these two modules, we again see an example of two fundamentally different design approaches at work. A Spring Boot application starts heavy and opaque, and tends to get more so as time goes on. A Vert.x application can start simple and transparent, and gradually add just as much complexity as needed, as it is needed. I greatly prefer the latter approach.
The project uses Apache Maven to build. The
maven-dependency-plugin
is used to generate a set of dependency reports
for each module under /target/dependency-reports
: this shows the
consequences of each technique for the dependencies which need to be pulled in.
Anyone who has maintained a sizeable Java application for any length of time
knows how important a consideration this is.
As a bonus, in the root POM I have turned on dependency cleanliness enforcement, duplicate class bans and dependency convergence which are techniques I would highly recommend to anyone maintaining Java projects.
Finally, I have included a script dependency.py
, which can be run after
a successful Maven build, and analyses the dependency trees output by the
maven-dependency-plugin
. Ideally you would be able to get this kind of
information from the plugin itself, but until then, this is a useful
substitute.
In the unlikely event that you want to use any material in this repository for your own purposes, you may do so under the Apache License 2.0, which is available as noted below or in LICENSE.txt.
Copyright 2016 Andrew Medworth
Licensed under the Apache License, Version 2.0 (the "License"); you may not use these files except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.