The Intershop platform allows dependency injection via the Guice framework. This enables type-safe binding of implementations to interfaces, simple injection of members (e.g., @Inject UserMgr userMgr; ...
), and makes it possible to remove static singleton access from the code.
Until now, dependencies between objects have been implicit: Somewhere in the code, some manager is used. This makes dependencies hard to detect and document, and even harder to reduce.
The Guice framework is a lightweight DI implementation that is well-documented and (now) widely used. It uses annotations and generics to declare dependencies using Java language features only. Because of that, there are no external (XML) files to be kept in sync with the actual classes (e.g., no class names as strings). Also, type safety and other typical DI problems are enforced by the compiler and supported by every Java IDE without the need for additional plug-ins.
Term | Description |
---|---|
The assignment of an implementation to an interface. | |
Guice's term for the →object graph builder. | |
A configuration class which declares →Bindings to the →Injector. | |
Object graph | (Guice usage) An object graph contains all objects that are needed to satisfy the dependencies of the root object (and all dependencies of all the dependencies). Or, as Google states: "To construct an object, you first build its dependencies. But to build each dependency, you need its dependencies, and so on. So when you build an object, you really need to build an object graph." |
| (Intershop-specific usage) An object graph representation consisting of a configured →Injector and a set of associated →Modules |
Can be used to retrieve instances of classes without immediately injecting them. | |
A decorator for →Provider to define the visibility of instances per →Binding; predefined scopes are: |
Note
Since 7.0, the dependency injection mechanism replaces the usage of the NamingMgr
for registering and using managers and providers.
The pf_objectgraph cartridge contains the logic to instantiate ObjectGraphs
being managed by an ObjectGraphEngine
.
The actual DI-framework-specific ObjectGraphEngines
are responsible for building and managing their ObjectGraphs
.
The pf_objectgraph_guice cartridge contains one such implementation of an ObjectGraph
based on the Guice framework. It is responsible for:
In Guice, an object graph is typically represented by an Injector
. Since Injector
is a Guice class, using it in Enfinity / Intershop code would lead to a hard dependency on Guice.
This is avoided by using the ObjectGraph
interface and only binding an implementation (GuiceObjectGraph
) encapsulating the Guice Injector
to it. That way, all classes that require object graph access only depend on the ObjectGraph
interface.
On application startup, all object graph declarations and modifications for all cartridges from cartridgelist.properties are read and processed (see below).
The object graphs and the modules (=collections of bindings) for an object graph can be declared in a property file resources/<cartridgeName>/objectgraph/objectgraph.properties for each cartridge (i.e., each cartridge can add modules to object graphs).
# global bindings global.modules=com.intershop.beehive.core.internal.modules.CoreNamingModule global.overrideModules=com.customer.application.intershop.extensions.CustomerServletModule # application type specific bindings local.modules = com.intershop.beehive.core.internal.feature.LocalFeatureModule local.overrideModules = com.intershop.beehive.core.internal.feature.OverrideLocalFeatureModule
The identifier global declares that the modules are to be used in the ObjectGraph named "global". This object graph is available within the entire ICM. The identifier local declares that the modules are to be used in the application type specific ObjectGraph. These application type specific ObjectGraphs are children of the global ObjectGraph.
<graphID>.modules=A B C
entry in the objectgraph.properties and will get loaded into the Injector of object graph graphID. Bindings defined in these modules must be disjoint, so you can only add new bindings. The only exception is the Guice's Multibinding mechanism, where you can add items to a previously bound Multibinder
or MapBinder
.<graphID>.overrideModules=A' D
entries in the objectgraph.properties. These modules are loaded into the injector of object graph graphID and are able to override (replace) bindings defined by base modules of this object graph.Note
At the moment (prior to version 7.10.20.1), all injections in the server (incl. calls via NamingMgr
) only use the object graph "global". Starting from 7.10.20.1, all injections use the application type specific object graph (with fallback to the global object graph as of its child-parent-relation). When creating a different application (e.g., from the test runner), another object graph could be used (to allow easy mocking). This is done by calling NamingMgr.setObjectGraph(objectGraphEngine.getObjectGraph("<graphID>"));
before instantiating the first classes.
In addition, any ObjectGraph may be used to instantiate a class, and will automatically be used for injecting any annotated fields needed for that class.
Note
local.modules
and local.overrideModules
are supported since 7.10.20.1.
Each module contains bindings defining which implementation to use for injection of given interfaces.
Note
Modules are Java classes. Guice bindings cannot be declared in external files (e.g., XML). While this makes it a bit more difficult to change bindings, it enforces compile-time consistency (i.e., a non-existing implementation cannot be bound etc) and reduces the potential for errors.
This also means that overriding is not done implicitly anymore: You have to declare a module to be an overrideModule. Use this functionality with caution and only for bindings that actually override previous bindings.
No overrides in the standard product
Overriding is a means of customization for partners or customers and should not be used in the standard product.
A module might look like this:
public class CoreNamingModule extends AbstractNamingModule { @Override protected void configure() { ... // bind a simple manager bindManager(LocaleMgr.class).to(LocaleMgrImpl.class).in(Singleton.class); ... // bind two different PasswordValidators using "named bindings" bindProvider(PasswordValidator.class, "PasswordExpressionValidator", named("Expression")) .to(PasswordExpressionValidator.class).in(Singleton.class); bindProvider(PasswordValidator.class, "PasswordHistoryValidator", named("History")) .to(PasswordHistoryValidator.class).in(Singleton.class); ... // bind a simple service bindService(PermissionMgmtService.class).to(PermissionMgmtServiceImpl.class).in(Singleton.class); } ... }
Field injection (see below) now works in pipelets and preparers by default. Managers, providers and services support the whole set of injection methods (field injection, constructor injection, setter injection). Also, all classes instantiated by Guice can use injection. For details, take a look at the module classes of the appropriate cartridge.
All managers, providers and services managed by the NamingMgr
can be injected.
Note
Intershop's policy for now is to use only field injection (as in the example below).
For some objects, the control of the instance lifecycle should not be handed over to Guice. This is especially important for POs, BOs, or pipelets. Objects like this are not instantiated by Guice. Only field injection is made possible by the code controlling their lifecycle.
import javax.inject.Inject; ... public class SomePipelet extends Pipelet { ... @Inject protected SessionMgr sessionMgr; @Inject private DomainMgr domainMgr; @Inject private AuthorizationMgr authMgr; @Inject private UserPOFactory userFactory; @Inject @Named(StringEncodingUtils.ENCODING_HTML) private StringEncodingProvider htmlEncoder; ... }
The injected fields can then be used directly.
Unsupported classes are classes which are not instantiated by the Guice injector and the context was not injected by the class that instantiates the object. The use of injection in such classes is a sham, because the object itself defines the context and this is not the intention of dependency injection. Using the annotation @Inject
in that cases can be a preparation, nothing more. The question is: How to support instantiation and injection in such classes?
The instantiation of objects should be located in a factory class:
public interface MyFactory { MyObject createMyObject(parameter ...); }
and the according implementation:
public class MyFactoryImpl implements MyFactory { @Inject private Injector injector; @Inject private CurrentApplicationBOProvider provider; MyObject createMyObject(parameter ...) { MyObject result = new MyObjectImpl(parameter); injector.injectMembers(result); return result; } MyObject createMyObjectWithInjection(parameter ...) { return new MyObjectImpl(provider, parameter); } }
At the beginning, it looks a little bit confusing (additional interface and implementation), but if you collect the instantiation of your cartridge in that factory class you have several advantages:
injectMembers(result)
.
MyObjectImpl
in test cases in another context (e.g., using mocks to encapsulate the test).Last but not least, the binding is necessary in your cartridge or test module.
bind(MyFactory.class).to(MyFactoryImpl.class).asEagerSingleton();
Now your implementation does not depend on the injection context (injector). But now you need to get access to the factory, it does not solve the original problem to get access to an object, which is injectable, you need access to an object graph.
Most business object does not support injection for performance reasons, so @Inject MyFactory;
is not available. But the BusinessObjectContext contains the object graph, which was used to create the context.
class MyBusinessObjectImplWithFactory implements BusinessObject { private MyObject createMyObject() { return getContext().getObjectGraph().getInstance(MyFactory.class).createMyObject(parameter); } } class MyBusinessObjectImplWithoutFactory implements BusinessObject { private MyObject createMyObject() { MyObject result = new MyObjectImpl(parameter); getContext().getObjectGraph().injectMembers(result); return result; } }
First, try to get an object graph or Guice injector. This will avoid the fallback to the global single static object path.
If that is impossible, the last option is to use the global object graph of the NamingMgr.
class MyImplWithoutFactory { private MyObject createMyObject() { return NamingMgr.getObjectGraph().getInstance(MyFactory.class).createMyObject(parameter); } private MyObject createMyObject() { MyObjectBO result = new MyObjectImpl(parameter); NamingMgr.injectMembers(result); return result; } }
Normally, Guice will create a new instance each time a value is injected. This is perfect for objects that are stateless and inexpensive to instantiate.
The @Singleton
scope will be used where previously GoF Singletons (static access) were used, i.e., managers, providers etc.
The scope of an object can be defined:
In bindings (recommended for flexibility)
import javax.inject.Singleton; import com.google.inject.AbstractModule; ... public class CoreNamingModule extends AbstractModule { ... @Override protected void configure() { ... bind(NamingMgrImpl.class).in(Singleton.class); ... } ... }
As an annotation of the class:
import javax.inject.Singleton; ... @Singleton public class NamingMgrImpl extends NamingMgr { ... }
By annotating @Provides
methods
import javax.inject.Singleton; import com.google.inject.Provides; ... public class SomeModule { ... @Provides @Singleton public ApplicationServerMgr getApplicationServerMgr(ServerEnvironment serverEnvironment) { return serverEnvironment.getApplicationServerMgr(); } ... }
At the server startup, all object graph declarations and modifications for all cartridges from cartridgelist.properties are read and processed. After that, the bindings are known, and instances can be requested. Guice now also has the information it needs to instantiate classes in the correct order (resolving inter-dependencies).
When an instance of class SomeClass
is requested, Guice instantiates the class bound to SomeClass
in the processed modules. If for example, SomeClassImpl
is bound to SomeClass
, it will instantiate SomeClassImpl
. All required fields which are annotated using @Inject
will be instantiated too, and so on for all classes needed (recursively).
Since the object graph can be accessed via the NamingMgr
, the server startup works almost as before.