Spring IoC vs.Google Guice: Comparing inversion of control containers
Comparing Spring vs. Google Guice: By Example
In my last article, I detailed a high-level comparison of two dependency injection frameworks: Guice and Spring. As I mentioned then, although they both fully embrace the principles of DI, they have very different approaches to implementing them. In this article I'll walk you through some actual code examples reflecting some basic use cases, and we'll see how both frameworks implement them.
Before digging into the code, however, a quick word about Spring XML: with the advent of Spring JavaConfig and annotation-driven configuration, Spring made the attempt to move away from XML and embrace the value of compiler-enforced type safety. They furthered their commitment to Java-based configuration by rolling many of those changes into Spring 3. I cheered those moves, and for that reason, I've confined my examples below to exclusively Java.
Updated inversion of control (IoC) tutorials and dependency injection examples
TheServerSide has been updating its resources in terms of the Spring framework, Java EE contexts and various different approaches to inversion of control (IoC) and dependency injection (DI). Check out these newer resources on the topic:
- Inversion of control explained fast with a Spring IoC example
- Spring MVC Java web development with Spring Boot
- Drawbacks to IoC problems and how to solve inversion of control problems
- Spring IoC vs. Google Guice's inversion of control approach
- Getting started with Google Guice
- Getting started with the Spring IoC container
- The beauty of Spring without XML: Annotation based IoC
However, criticism of Spring's traditional dependence on XML is still relevant for at least two reasons. First, there are still many Spring applications being actively developed using XML. I'm not speaking of legacy applications for which a complete refactoring would be infeasible (and for which an integration path with newer, annotation-based code makes sense), but new applications that, while leveraging the power of Java-based configuration, still depend in part on XML. My sense is that many new Spring applications employ a hybrid approach, using a combination of XML and Java. This is underscored by Spring's bean documentation whose examples remain largely in XML, and which openly states, "Spring's @Configuration class support does not aim to be a 100% complete replacement for Spring XML."
Second, and more importantly, the implementation of Java-based configuration bears the imprint of the original XML-based architecture. This is by intent: the Spring authors acknowledged with Spring JavaConfig that they were trying to keep XML-config and JavaConfig semantics the same. That approach certainly makes it easier for developers to transition their code to use Java without changing their architecture, but it also causes Java configuration to suffer from some of XML's shortcomings by proxy. With that said, let's look at some examples.
Baby, You Can Autowire My Car
Let's say I have a simple Car class that has a concrete dependency on Engine, which itself has a concrete dependency on Piston. I like immutability, so I'm going to build my objects using constructor injection. Here's how the domain and configuration code would look using Spring annotated autowiring. (For the sake of brevity, I've omitted the private members of the class to which the incoming parameters would normally be assigned.)
package spring.package1; public class SpringMain {
@Component
@Component
@Component
And here's the same setup using Guice:
package guice.package1; public class GuiceMain { public static void main(String[] args) { public class Car {
public class Engine { public class Piston {} |
As you can see, Spring and Guice appear virtually identical, both in intrusion in domain code and in configuration complexity. Similar as they look, however, the first cracks in the foundation of how Spring wires its components are already apparent.
When using the AnnotationConfigApplicationContext in Spring, there are essentially two choices to load candidate components for autowiring (identified by annotations such as @Component): (1) itemizing the individual components and/or their annotated containers, or (2) using classpath scanning to auto-detect components.
Explicitly enumerating the individual components, although possible, seems tedious and error-prone to me. I’d prefer my DI container to detect my components automatically, and that is what the second option offers. Using Spring component auto-detection, you can hypothetically drop new components into the application without any change to configuration. (I'm ignoring possible performance concerns for this discussion.)
The first weakness with this approach, however, should be obvious: the string identifier "spring" that gets passed to the constructor. By requiring a string package name for scanning, you're already starting out with type information that can only be checked at runtime. But there's a second issue that's more problematic, which can be exposed by adding a second Car class in a different sub-package.
package spring.package2; |
This throws an exception in Spring even though these two Cars are of two totally different types. Here's the rub: despite claiming to wire classes by type, the framework still uses string identifiers to track them as beans. When you first pass the package name to the context object, Spring builds a list of candidate components and uses its default bean name generator to assign an identifier to each one. As a result, since Spring requires bean identifiers to be unique, the framework reports a conflict between two beans named "car" in our package structure. Confusingly, the error message even reports that the spring.package2.Car interferes with another bean definition "of same name and class," even though it is clearly not the same class.
One might ask how often we have classes with the exact same name in different sub-packages, and admittedly, it's not that often. But this exposes a weakness in how Spring handles beans under the hood that will be a harbinger of other problems to come. As for resolving this particular problem, Spring's solution is to either narrow the package being scanned, or else add an explicit bean identifier to one of the two classes to help Spring resolve them internally. What sort of identifier? Yet another string.
package com.spring.package1; @Component package com.spring.package2;
// different Car in different package albeit with the same class name @Component(value="mySecondCar") Now the Spring component scanner will be happy, but we end up with another string-identified class (more on that in a moment). Compare this to Guice, which handles this situation with ease: package guice.package2; // different Car in different package albeit with the same class name |
Run the main method and you'll see the Guice framework has no problem bootstrapping the two types of classes. In addition, you’ll notice there’s no need to use special annotations for scanning and differentiating component classes. Guice uses Just-In-Time bindings to wire classes together. That means if a particular dependency isn’t explicitly bound in a module (as in examples below), Guice will create one on the fly. In this sense, all concrete classes are available to be autowired (or "injected") by default without a need to explicitly identify them to the container. And that means less configuration.
Build a Better Engine
Next, let's say that you have a new requirement to enhance the Engine you’re passing to your Car. This new engine—which I’ll creatively call BetterEngine—should subclass Engine, and the DI framework should be able to pass it to Car without having to change Car’s constructor signature. In Spring, to communicate to the framework via autowiring that you'd like to substitute a subclass for its parent, you use the @Qualifier annotation:
package spring.package1; @Component
@Component public class BetterEngine extends Engine { |
The "value" parameter of the @Qualifier annotation instructs Spring to look for a bean with that qualifier name and wire it. Absent a matching qualifier name, Spring "falls back" to using the bean name. As mentioned previously, Spring still uses bean names to identify classes under the hood, and so "betterEngine" is the bean name for BetterEngine. Thus Spring easily selects and autowires the correct implementation.
Guice has an annotation very similar to Spring's @Qualifier called @Named, which also uses a string identifier to match a potential implementation. But to avoid the hazards of string identifiers, Guice has a better, type-safe way using a concept called binding annotations. Contrast the Spring example to the Guice implementation below, using binding annotations and the introduction of Guice's modular configuration file:
package guice.package1; @BindingAnnotation public @interface Better {} public class GuiceMain { public static void main(String[] args) { } public class MyModule extends AbstractModule { public class Car { } public class BetterEngine extends Engine { } |
At first glance, it appears Spring has the advantage. Spring lets you effect the change with only a single modification to source code and no extra configuration, while Guice requires us to not only modify configuration (MyModule) but also create a new annotation (@Better).
However, let's look at this a little more closely. As with our duplicate Car example above, in order to substitute an alternate implementation, Spring requires that we sacrifice type safety. The compiler has no way to verify that a bean with a qualifier value of "betterEngine" actually exists, or that it is compatible with the Engine type. You must wait until runtime for the framework to initialize before you discover a possible error.
Worse, use of the @Qualifier annotation in this manner creates an implicit binding to an implementation. Since Spring uses the class name to create the bean name, and since the default qualifying value for @Qualifier is the bean name, Car is now implicitly bound to BetterEngine in the source code. The same is true of any other object that might autowire a "betterEngine" bean. What happens if you want to rename BetterEngine? Or worse, what if you later designed an EvenBetterEngine that you wanted to use instead? You’d be required to either change all string identifiers in all classes where "betterEngine" is referenced as a dependency or be left with an odd naming dilemma such as this:
// new implementation that we prefer to autowire for @Qualifier("betterEngine") @Component(value="betterEngine") public class EvenBetterEngine extends Engine { ... } // what bean name do we use now? @Component(value= ??? ) public class BetterEngine extends Engine { ... } |
To be fair, the Spring documentation discourages this, since it amounts to mapping beans by id and defeats the purpose of using @Autowired for type-driven injection in the first place. (It recommends a completely different annotation, @Resource, for that purpose.)
Instead, the intended use of @Qualifier is more as a type filter, with a string identifier not directly tied to a bean name. Or as an alternative, Spring also lets you create custom annotations to flag special implementations. Following this convention, the Spring wiring would look something like this:
@Component @Autowired } @Component @Autowired }
@Target ( { ElementType.TYPE, ElementType.PARAMETER } ) @Retention ( RetentionPolicy.RUNTIME ) @Qualifier public @interface Better{} |
Look familiar? This is very similar to the Guice approach with one major difference: you're annotating the class to be autowired, rather than the relationship between the class and its injection point. As such, this implementation is only providing the illusion of type safety because the compiler has no way to know if a class annotated with @Better actually matches the parameter annotated with @Better. What's to stop me from doing the following at compile-time instead of annotating Engine?
@Component @Better |
This compiles just fine, and it isn't until runtime that you discover that NotAnEngine is incompatible with the constructor Car(@Better Engine).
Guice avoids all this confusion in two ways: it uses actual types to wire dependencies together rather than bean names (or qualifier values), and it requires the module be in charge of resolving ambiguities using bindings. In the Guice example above, the special binding annotation @Better not only flags the injection point of Engine, but the module MyModule then binds that annotation to the desired implementation (BetterEngine). No need for string identifiers. No need to understand semantic differences between @Qualifier and @Resource or to wonder whether you're wiring by qualifier value or by bean id. And everything is type-checked at compile time.
Of course, this does mean that in some instances, you may have a little more configuration in Guice than you would with Spring autowiring. But Guice is based on the philosophy that you will be using dependencies far more than you will be defining them, and thus the upfront "cost" is worth the tradeoff in control, clarity, and extensibility. This benefit becomes even clearer in our next example.
Write Once, Substitute Everywhere
Let's say the new BetterEngine is performing so well, I now want to substitute it for all Engine references across the entire scope of my application, rather than just Car. And, I want to do it without having to modify those source files directly as in the previous example. Here's what such an implementation might look like using Spring:
@Configuration @Bean @Bean }
@Component // @Component @Autowired @Component @Autowired |
Here we see the @Configuration annotation for the first time, which marks a transition from autowired annotation-based Spring to Java-configuration-based Spring. According to the Spring documentation, @Configuration is "the central artifact in Spring's new Java-configuration support." Unlike its counterpart @Component, classes annotated with @Configuration are special-use classes in Spring. Rather than invoking bean methods directly, Spring subclasses and proxies its methods using CGLIB in order to intercept and provide Spring-specific functionality. Let's review the source code above to see how this works.
A @Configuration-annotated class is basically a container class of @Bean-annotated methods that define beans in Java, much in the same way <bean> elements define beans in XML. These methods are typically simple factory methods which wrap an invocation of "new," and that introduces a problem for this application. Because "new" is an explicit invocation of a constructor, the compiler requires you to supply the appropriate arguments, and that means you can no longer use constructor autowiring.
This problem leaves two alternatives: wire the objects by hand or switch to autowiring by method injection. Because I still want to maintain immutability and because I don't want to have to change the superclass Engine to also use method injection, wiring by hand is the preferred choice. I chose to create a second @Bean method for Piston so the container can have control over how Piston gets created too. In a minor irritation, I also had to comment out Engine as a component, otherwise Spring autowiring will choose that instead of my special @Bean method that returns a BetterEngine. (Of course, I could use @Qualifier("betterEngine"), but we already know the problems with that!)
Just for comparison, let’s see how the same Spring code would look, configured to use autowiring by method injection instead:
@Configuration public class TestConfig { // @Component @Component |
Odd as this may look, it actually works. Even though our engine() bean method contains no explicit call to setPiston(), a look at our runtime Car object shows that it has a BetterEngine, which indeed has a Piston. So, how does Spring do this? The framework intercepts the invocation of engine() during autowiring using the aforementioned proxy. After the creation of the BetterEngine() object, Spring invokes any @Autowired methods on that object before returning it to the class that requires the dependency (in this case, Car). BetterEngine inherits an @Autowired setPiston() method from Engine, so Spring invokes it.
This seems like a confusing approach to me. Why would I want to spread out my object initialization across both a bean factory method and container autowiring? To preserve clarity in my @Configuration class, the better choice is to do all the wiring there, which, however, would mean I must abandon autowiring altogether.
But the proxying goes further; Spring also uses it to enforce scope. The @Bean-annotated piston() method explicitly invokes "new," so you would ordinarily expect that each call to piston() by the framework would produce a new instance of Piston. But this is not the case. Again, using proxying, Spring intercepts subsequent calls to piston() and skips the call to "new" if the object already exists, thus enforcing Spring’s default bean scope of singleton. The expected behavior—a new instance on each call—must be explicitly enabled using the @Scope("prototype") annotation. (Need I point out at this stage yet another string identifier?)
To add to the confusion, all these proxy rules apply only to @Bean-annotated methods within a @Configuration class. They don't apply to @Bean-annotated methods in a @Component class, which does not use proxying and behaves with standard Java semantics.
This multi-layered approach to bean definition and wiring strikes me as error-prone and needlessly complicated. I can't look at the contents of a @Bean-annotated method and immediately understand how the class is configured. Instead, I have to run through the following questions: Is the method in a @Configuration class or a @Component class? Is this singleton scope or prototype scope? Do I have autowired method injection on this class, or not?
Compare this to the Guice implementation to do the same thing:
public class MyModule extends AbstractModule { }
public class Car { |
This implementation should look very familiar. It's almost identical to the previous Guice configuration from the last section, except I've eliminated the custom binding annotation @Better. With a single call to bind(), Guice automatically applies BetterEngine everywhere it finds an Engine. No changes are needed to any source code files. No need to worry about understanding how the framework intercepts calls to subclassed method proxies.
In addition, we still get the full power of automatic injection. It is irrelevant to the configuration module whether the class receiving injection uses field, method, or constructor injection—the configuration is identical. This gives you the flexibility to use whichever method makes sense in your source classes, and even to change them at will, without needing to modify configuration.
This is a microcosm of the power of true by-type wiring, and it may be Guice's most persuasive feature. With a single-line change to my configuration file, I can substitute a new implementation across my entire application. Although Spring autowiring hypothetically offers this same power, it has limitations. Using @Configuration or @Component are viable type-safe alternatives, but the factory-like @Bean methods require wiring code explicitly, leaving us with a near 1:1 relationship between bean definitions and the source files they describe.
This is a key distinction. Regardless of whether your framework is Spring or Guice, if you need to add a new dependency to 50 different classes, you must modify 50 different source files to declare that dependency. However, Spring Java configuration also requires modifying 50 different @Bean methods to manually wire in that new dependency. Guice, conversely, can handle it with a single line, or in the case of a new concrete dependency, no lines at all. It’s difficult to understate the impact of this feature on code reusability.
Conclusion
In short, let's sum up the key points made through this code comparison:
1. Although Spring 3's support for annotations and Java configuration does provide a certain level of compile-time type safety, its implementation still supports and even encourages heavy use of string identifiers in component scanning, bean naming, qualifier values, scoping names, and so on. In addition, Spring’s under-the-hood approach to bean naming (which by default also uses string identifiers) can lead to confusion in autowiring and problems with compile-time type safety. Guice avoids all these problems by simply avoiding string identifiers.
2. Spring autowiring itself tends to be too magical, trying too hard to automatically pick matching implementations without requiring explicit configuration and potentially complicating more advanced wiring scenarios. Guice's approach to injection is plain—types tell the story. A source file plus a module will tell you exactly what gets injected.
3. Most non-trivial applications will not be able to rely on Spring autowiring alone, and will require some level of configuration via @Configuration-annotated classes or XML. Beans defined in these classes are by definition unable to support autowired constructor injection and offer confusing semantics for autowired method injection and the enforcement of singleton scope. Guice modules implicitly support all forms of injection and do not need to change if the underlying form of injection changes.
4. Absent autowiring, Spring's @Bean-annotated methods behave as factories, explicitly wiring dependencies together and requiring maintenance of a parallel structure of bean definitions. Because Guice wires by type, such explicit wiring is unnecessary, keeping module files more concise and minimizing the need to modify them in tandem with source code.
In fairness, Spring does offer other solutions to some of these problems that fall outside the scope of this article. For example, another way to defeat the bean naming problem I’ve discussed is to implement the BeanNameGenerator interface and derive your own bean-naming strategy. In my opinion, however, such alternatives only compound the complexity of the software and raise barriers of entry to those new to your application. Notice, for example, how verbose I have had to be throughout this article in explaining the complexities of Spring autowiring or Java configuration, and how many different ways there are to achieve the same result, including support for configurations discouraged by Spring best practice. Some Spring advocates see this flexibility as a strength and even encourage mixing the three forms of Spring DI (XML, Annotation, Java), but I see this as potential code smell, indicative of an API that tries to do too much.
Well-known Java expert Joshua Bloch once advised that a good API should be easy to use and difficult to misuse. Guice's approach to dependency injection embraces this maxim, with a concerted effort to keep the API small and highly applicable to well-defined use cases. The framework also starts from a fresh slate, without the need to rely on the concept of a bean or maintain backward compatibility with XML. It's not as magical as Spring autowiring, but it maintains a simple and elegant DSL-style configuration approach which makes dependency wiring clear and minimizes surprises. Guice eschews using string identifiers except in rare circumstances, preferring to wire all dependencies by type, and it provides the powerful concept of the binding annotation to enforce that safety at runtime, even across multiple implementations.
The fundamental difference between Guice and Spring's approaches to DI can probably be summed up in one sentence: Spring wires classes together by bean, whereas Guice wires classes together by type. Therein lie many of the key differences. Hopefully these code examples give developers a better understanding of those differences and how they can benefit further from Google Guice.
Ryan Nelson has more than 10 years of software development experience on both large and small projects. He presently develops educational software as a Senior Software Developer for Pearson Digital Learning in Chandler, Arizona. He can be reached at [email protected]
Interested in articles and opinion pieces from Cameron McKenzie? Check these out:
- Why the Amazon S3 outage was a Fukushima moment for cloud computing
- Software ethics and why ‘Uber developer’ stains a professional resume
- It was more than user input error that caused the Amazon S3 outage
- Don’t let fear-mongering drive your adoption of Docker and microservices?
- Stop adding web UI frameworks like JSR-371 to the Java EE spec
Recommended Books for Learning Spring
Spring in Action ~ Craig Walls
Spring Recipes: A Problem-Solution Approach ~ Gary Mak
Professional Java Development with the Spring Framework ~ Rod Johnson PhD
Pro Java EE Spring Patterns: Best Practices and Design Strategies ~ Dhrubojyoti Kayal