The component framework is introduced to declare dependencies between code artifacts. The major idea behind "declaring dependencies" is that each piece of the application can define the corresponding requirements and functionality which can be used by other components. The result is that it is now possible to fulfill requirements of a component with different implementations.
The component framework can also be used to declare the instances and wiring. Different application types can use different instances and/or wirings, but it is also possible to use the same instances. Using the same instances is very helpful in case of UnitTests; required contracts can be implemented with some kind of test-aware implementations.
The intention of the component framework is to provide a (LEGO) brick system that can be composed as needed. The contracts are the glue between the bricks. So it is important to think about generalized contracts and reusable implementations.
The component framework allows developers to write and organize functional components. Splitting functionality into components has the following advantages:
Both implementation I and U do not know to which components the instances will be wired. The instance U does not need to resolve instance I. U gets I injected.
Benefits
It is possible to
For common questions in this context, refer to the corresponding cookbook.
More information on XML definitions can be found in the Reference - Component Framework.
The component framework uses an XML declaration of contracts, implementations and instances. Initially, we started with two options: 1) annotations and-or 2) XML declaration. Annotations work well for contracts and implementation declaration, but not for instance declaration. Additionally, annotations modify the source code of the component classes, so the implementation depends on the framework. That was a major disadvantage, especially while other frameworks are on the market, like Spring or Guice. Now the component implementations can be instantiated via "new" and configured for test cases without the framework.
Defining contracts is as "simple" as writing Java interfaces. It is important that you can (and hopefully you will) split the interfaces to requirement interfaces and provided interfaces. For example, normally you put getter and setter together in one interface to configure the instance. You can remove these types of methods from the provided interface. The provided interface should declare what the instance should do and not what is required to fulfill this functionality. A good example is the CartridgeListProvider contract. It does not matter where the cartridges come from - the CartridgeListProvider provides a list of cartridge names.
public interface CartridgeListProvider { public List<String> getSelectedCartridges(); }
Summary:
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <contract name="[name]" class="[classname]"/> </components>
The XML description allows the following tags:
An implementation for a component instance cannot store request-, user-, session- or domain-specific information. The instance is running during the whole lifetime of an application server. This is currently a huge difference compared to business objects. Sometimes the performance requirements forces a caching of business objects, therefore you can use a cache, which is registered at the CacheAPI.
Defining a good structure for the implementations is a hard goal. This includes especially the partitioning of the functionality. If you separated the different aspects of the implementation, you will see that some implementation requires some other, and other implementations do not require anything.
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <implementation name="[name]" implements="[contract-name]" factory="[factory-name]" class="[class-name]" start="nameOfStartMethod" stop="nameOfStopMethod"> <!-- multiple implements can be listed --> <implements contract="[contract-name]" /> <requires name="[property-name]" contract="[contract-name]" cardinality="[1..1|0..1|1..n|0..n]" /> </implementation> </components>
The XML description has the following allowed tags:
The ComponentFW will instantiate all defined instances on the first access. Therefore the instance is created with the defined factory (default JavaBeanFactory). After the instantiation of the required instances, the requirements will be fulfilled.
Summary:
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <!-- an instance for an implementation without requirements --> <instance name="[name]" with="[implementation-name]" /> <!-- an instance for an implementation with directly fulfilled requirements --> <instance name="[name]" with="[implementation-name]"> <!-- fulfill the requirement with a constant --> <fulfill requirement="[property-name]" value="[constant]"/> <!-- fulfill the requirement with another instance --> <fulfill requirement="[property-name]" with="[instance-name]" /> </instance> <!-- outside of instance tag --> <fulfill requirement="[property-name]" of="[instance-name]" with="[instance-name]" /> <fulfill requirement="[property-name]" of="[instance-name]" value="[constant]" /> <!-- instance inside of fulfill tag --> <instance name="[name]" with="[implementation-name]"> <!-- with attribute of fulfill tag is implicit - filled with inner instance element(s) --> <fulfill requirement="[property-name]"> <!-- name of instance is optional - anonymous instances are allowed here --> <instance with="[implementation-name]"> <fulfill requirement="[property-name]" with="[instance-name]" /> </instance> <!-- recursive declaration of instances and fulfillment --> <instance with="[implementation-name]"> <fulfill requirement="[property-name]"> <instance with="[implementation-name]"> </fulfill> </instance> </fulfill> </instance> <!-- replace an instance with a new one, the old is available via the name - value of delegate attribute --> <replace name="[name]" with="[implementation-name]" delegate="[renamed-instance-name]"> <fulfill requirement="[delegate-property-name]" with="[renamed-instance-name]" /> <!-- other fulfill tags ... --> </replace> </components>
The XML description has the following allowed tags:
The introduction of the application framework made it necessary to define scopes for instances, so that for different applications different instances were created. The scope defined in the XML file defines the context in which the instance is created.
<instance name="[aName]" with="[anImplementation]" scope="global|app" />
There are two values available:
global
: with this scope defined, only one instance of the implementation is created. The instance will be put into the "global" context and is available from all applications.app
: this value tells the framework to create one instance of the implementation per application. The instance is created exclusively for the current application and cannot be accessed by any other application.If no scope is defined within the instance definition, the scope will be global
.
The following example shows a contract (MailService), two implementations (one SMTP service and one dispatcher/director). The instance definition defines 2 SMTP services and one dispatcher which delegates the requests to one of the SMTP services. Both implementations use the JavaBeanFactory (which is the default factory). This factory injects the dependencies via simple set
* and add
methods. The set
method is used if the requirement has the cardinality 0..1 or 1..1, a single object has to be assigned (like the algorithm, port or host requirement). The add
method is used if multiple objects can be assigned (like the delegate requirement of MailDirector).
Defining the MailService contract:
public interface MailService { public abstract void sendMail(Mail mail); }
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <contract name="MailService" class="com.intershop.beehive.component.mail.MailService" /> <contract name="String" class="java.lang.String" /> <contract name="Integer" class="java.lang.Integer" /> </components>
Followed by 2 implementations:
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <implementation name="SMTPMail" implements="MailService" class="com.intershop.beehive.mail.SMTPMailService"> <requires name="host" contract="String" /> <requires name="port" contract="Integer" /> </implementation> </components>
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <implementation name="MailDirector" implements="MailService" class="com.intershop.beehive.mail.DirectoryMailService"> <requires name="delegate" contract="MailService" cardinality="1..n" /> <requires name="algorithm" contract="String" cardinality="0..1" /> </implementation> </components>
public class DirectoryMailService implements MailService { private List<MailService> delegates = new ArrayList<MailService>(); private boolean isRoundRobin = false; public DirectoryMailService() { } public void addDelegate(MailService delegate) { delegates.add(delegate); } public void setAlgorithm(String algorithm) { isRoundRobin = algorithm.equals("round-robin"); } public void sendMail(Mail mail) { } }
Defining instances and wiring them together.
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <instance name="mail" with="MailDirector"> <fulfill requirement="delegates"> <instance with="SMTPMail"> <fulfill requirement="port" value="23" /> <fulfill requirement="host" value="intershop.mail.smtp1.host" /> </instance> <instance with="SMTPMail"> <fulfill requirement="port" value="99"/> <fulfill requirement="host" value="host2"/> </instance> </fulfill> </instance> </components>
All component implementations should use requirement declarations and responsible "setter" and "adder" methods to get other component instances wired. So the instance does not need to know which other instance (name or type) is used or how to get such an instance.
Note
Business objects or business object extensions will be instantiated by factories which are component instances. So other component instances can be transferred via these factories.
Some artifacts, e.g. pipelets, will be instantiated by other global components. So these need a possibility to access components without wiring.
It is possible to get the instance from the component manager or from the current application via the name of the instance.
T instance = NamingMgr.getManager(ComponentMgr.class).getGlobalComponentInstance(instanceName);
AppContext currentAppContext = AppContextUtil.getCurrentAppContext(); App app = currentAppContext.getApp(); T instance = app.getNamedObject(name);
Note
The AppContextUtil.getCurrentAppContext()
does not guarantee to return an AppContext
. Always make sure a current AppContext
is actually available before calling currentAppContext.getApp()
.
The lookup for an instance provides 3 possibilities:
Example:
The component framework instantiates the yellow instances. All instances are registered at the ComponentContext. The developer can wire instances to the app (e.g., instance A is wired to the currentApp);
this will override the instance lookup for this special app. In the example, instance "A" will be returned.
Note
Do not use getComponent(String)
inside of a component implementation. The initialization could fail under some circumstances. Use an additional requirement for your implementation and wire the instance instead.