Advanced Use of Eclipse 4’s Dependency Injection Framework
Brian de Alwis Manumitting Technologies, Inc (includes work with Landmark Graphics Corp)
Advanced Use of Eclipse 4s Dependency Injection Framework Brian de - - PowerPoint PPT Presentation
Advanced Use of Eclipse 4s Dependency Injection Framework Brian de Alwis Manumitting Technologies, Inc (includes work with Landmark Graphics Corp) Agenda Problem (Why DI?) Why use DI in Eclipse? Whats different about Eclipses
Brian de Alwis Manumitting Technologies, Inc (includes work with Landmark Graphics Corp)
Eclipse’s DI?
modified without breaking others
new situations But it’s a blurry distinction between what’s “related” and “unrelated”
PlatformUI.getWorkbench().getActiveWorkbenchWindow() .getSelectionService() MyAppThingy.getInstance() Platform.getAdapterManager()
The Problem: Most objects are not created independently, but are created as part of something larger. Yet we build these objects as if they were independent, and must specify how to access their needed services. Result: structural dependencies (e.g., singletons), or creator responsible for extensive configuration (or service locators) There are over 1600 references to PlatformUI.getWorkbench() in the Eclipse Platform alone! (Which likely makes the RAP developers cry.)
these details “EEK”: External Extraneous Knowledge)
@Inject Scalpel scalpel;
Let’s consider a different situation: medical surgery. A surgeon obtains items and information from those in the environment. People in the environment notify surgeon when circumstances change. The surgeon is not responsible for knowing what is going on and where items are found. Thus transplant surgeons can travel long distances to unfamiliar hospitals and yet be immediately effective. They make assumptions of their environment, and their environment provides.
Annotations: the annotations spare us from writing a lot of boilerplate
PlatformUI.getWorkbench().getActiveWorkbenchWindow() .getSelectionService() MyAppThingy.getInstance() Platform.getAdapterManager() @Inject ISelectionService ss; @Inject IAdapterManager am; @Inject MyAppThingy thingy;
What we want to be able to do is get rid of this EEK and instead say “find me the appropriate selection service / adapter manager / thingy.” But how do we get these “thingies”? We push this problem to whomever created us, transforming the problem into a runtime configuration issue.
Workbench Window Editor View
in the context of some
lifecycle
for providing the configuration required by its children
Servlet Webapp Servlet Jetty Container: some other component who determines this object’s lifecycle Often used in the context of web apps (e.g., Tomcat, Jetty). A container may have been itself created by some other container. We now use DI extensively within the Eclipse UI, and due to how we need injection to work, we have our own injector.
DI provides advantages for code: separation of configuration from use Simplify access: avoid IStatusLineManager problem
In other DI systems, like Guice or Spring, there is a single-level of configuration. UIs are a bit different, and we needed something more flexible.
changed or the injection situation changes The last item is key differentiator from other injectors
public class PropertiesPart {
}
Contrast @Named vs FQTN Avoids boilerplate for installing/removing listeners. Compare & contrast the code for tracking the active part with the IPartService.
public class CommandHandler {
}
@Inject @Preference(nodePath="my.plugin.id", value="dateFormat") protected String dateFormat;
private void setDateFormat(
}
IEclipsePreferences preferences;
}
public class GameDisplayPart {
}
also bounded by the context in which it was created (rather than the bundle).
JavaFX/etc widgets
DI in javax.inject
injection-related annotations)
@PreDestroy
But we have common underpinnings. It’s possible to write injectable code that should work across any JSR330- compliant injector, like the Eclipse injector.
@EventTopic / @UIEventTopic, @Preference, @OSGiBundle
And you can define your own field and parameter annotations to have them be custom injected
public class PropertiesPart {
}
Process @Inject items:
constructor
with inheritance in lookups
up the chain until satisfied
leaf
(ContextInjectionFactory) or…
ViewPart Application Persp Window Persp Window EditorPart OSGi Services
“org.eclipse.ui.ISelectionService” a SelectionService “localContexts” [“org.eclipse.ui…”, “…”] “org.eclipse.swt.widgets.Composite” host composite “customValue” domain object “other state value” “a string” “org.eclipse.core.runtime.IExtensionRegistry” a Registry “org.eclipse.e4.core.services.adapter.Adapter” an Adapter “activePart” a Part “activePartId” “org.eclipse.ui.views.Pro…” “activeEditor" an EditorPart “activeEditorId” “org.eclipse.e4.tools.emf…” “activeWorkbenchWindow” a Window “applicationXMI” “org.eclipse.platform/LegacyIDE.xmi”
Key: a string; often the FQTN of the value’s class or interface Value: any object, including null
Key workbench components have a corresponding IEclipseContext Key: any string An unsuccessful lookup at the editor level (e.g. for “IExtensionRegistry”) traverses upwards You can store whatever values you like, including custom app state Top level looks up services in OSGi level, including DS
with null
hierarchy is the OSGi service layer
value)”
be re-injected with the new value
context, then use “context.modify(name, value)” to set that value from any child context
Can always obtain the context used for injection via “@Inject IEclipseContext context”
particular context
the RAT to be re-run while the RAT returns “true”.
IContextFunction
the originating context
IInjector has 4 principle methods:
(But typically use ContextInjectionFactory) An injector is used to create or inject objects; will be required for objects that aren’t managed by the Eclipse framework. The injector attempts to resolve injection requests using a set of provided object suppliers; our primary supplier is based around the IEclipseContext hierarchy. Typically use the wrappers on ContextInjectionFactory.
public abstract class InjectableViewPart extends ViewPart { private IEclipseContext _context;
_context = (IEclipseContext) site.getService(IEclipseContext.class); assert _context != null; super.init(site); ContextInjectionFactory.inject(this, _context); }
super.dispose(); ContextInjectionFactory.uninject(this, _context); } }
If you create/inject an object, you should remember to uninject But IECs have a lifecycle: can be disposed, which cleans up any previously-injected objects. Child context lifetimes are bounded by the parent.
as an OSGi service
@Inject private void editorSelectionChanged(
}
@Inject private void editorCursorLocation(
}
@Named(“bug398728”) required prior to 4.4
be notified only of changes within its windows?
/** * This annotation causes arguments and fields in the requesting object to be resolved from the active editor's context * rather than the requesting object's source context. Values will be re-injected when a different editor is activated. * * <p>The scope of the editor is determined by the requesting object. For example, if the object is a {@link ViewPart} * then the search scope is normally restricted to finding the active editor within the same window. The requesting object * may implement or be adaptable to {@link LookupScope} to explicitly provide a different scope. If the scope cannot * be determined, then it is assumed to be workbench-wide and the receiver will receive updates for the active editor * and active window.</p> * * <p>The procedure for specifying the identifier used to resolve an object for injection is slightly different from * normal injection. Unlike normal resolution, which uses the {@link Named} annotation, the object identifier is taken * either from the annotation's value (e.g., {@code "@ActiveEditor("identifier")}) or otherwise from the field/method * type name. This difference is required as the @ActiveEditor annotations are processed as a second stage of injection. * The {@link Named} annotation is instead used to provide a nonsense value so as to prevent the successful * resolution in the primary stage of injection; this nonsense value is usually required when the injected object * is provided as an OSGi service. See Eclipse bug 398728 for details.</p> * * <p>Use of the {@link Optional} annotation is normally required since there is no guarantee that an active editor is available to inject.</p> * […] */
@Qualifier @Documented @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) public @interface ActiveEditor { /** The context key: if unspecified, uses the requestor type */ String value() default ""; }
<?xml version="1.0" encoding="UTF-8"?> <scr:component xmlns:scr=“http://www.osgi.org/xmlns/scr/v1.1.0"
<implementation class="com.lgc.dsaf.ui.impl.ActiveEditorObjectSupplier"/> <service> <provide interface="org.eclipse.e4.core.di.suppliers.ExtendedObjectSupplier"/> </service> <property name="dependency.injection.annotation" type=“String"
</scr:component>
We expose the annotation and extended supplier via an OSGi Service. This is a specific detail to the default InjectorImpl.
provide object describing the injection site
type and annotations of the field/parameter
method, field, constructor; can re-trigger The IRequestor also provides access to the object being injected.
public class ActiveEditorObjectSupplier extends ScopedObjectSupplier {
} }
We’ll explain scope in a sec Note get(): “track” means whether changes to this value should be tracked and cause re-injection; “group” whether the requestor changes can be grouped together getServiceName() looks at the IObjectDescriptor to get details of the @Named or the requested type name.
class ValueRequestor { IEclipseContext context; Map.Entry<IRequestor,String> key; Object value = IInjector.NOT_A_VALUE;
Map.Entry<IRequestor,String> key, IEclipseContext context) { this.key = key; this.context = context; updateValueAndInstallTracker(); }
new RunAndTrack() { @Override public boolean changed(
if (!key.getKey().isValid()) { _valueRequestors.remove(key); return false; } if (context != cxt) { return false; } Object x = context.get(key.getValue()); if (!Objects.equal(x, value)) { value = x; if (value != IInjector.NOT_A_VALUE) { runExternalCode(toReinject); } } return true; } }
@Override public void run() { key.getKey().resolveArguments(false); key.getKey().execute(); } };
value = context.get(key.getValue()); context.runAndTrack(contextTracker); }
// the context of an editor should not // ever change. boolean newContext = cxt != context; context = cxt; if (newContext) { updateValueAndInstallTracker(); } return value; } }
We’ll explain scope in a sec ValueRequestor uses
public interface LookupScope { Object getLookupScope(); }
extends ExtendedObjectSupplier { ListMultimap<Object, IRequestor> _scopedRequestors …;
IRequestor requestor) { Object o = requestor.getRequestingObject(); LookupScope lookup; Object scope = o; if (o instanceof LookupScope) { scope = ((LookupScope) o).getLookupScope(); } else if ((lookup =_adapter.adapt(o, LookupScope.class)) != null) { scope = lookup.getLookupScope(); } if (scope instanceof IEditorPart || scope instanceof IWorkbenchWindow || scope instanceof IWorkbench) { return scope; } else if (scope instanceof IWorkbenchPart) { return ((IWorkbenchPart) o).getSite() .getWorkbenchWindow(); } else if (scope instanceof WorkbenchWindowControlContribution) { return ((WorkbenchWindowControlContribution) scope).getWorkbenchWindow(); } return getWorkbench(); }
boolean track, Object scope, IRequestor requestor) { if(track && requestor.isValid()) { if(!scopedRequestors. containsEntry(scope, requestor)) { _scopedRequestors.put(scope, requestor); } } else { _scopedRequestors.remove(scope, requestor); } }
We’ll explain scope in a sec Note get(): “track” means whether changes to this value should be tracked and cause re-injection; “group” whether the requestor changes can be grouped together
public abstract class ScopedObjectSupplier extends ExtendedObjectSupplier { ListMultimap<Object, IRequestor> _scopedRequestors …;
protected void init() { /* install part and window listeners that reinject all scoped IRequestors when the active part/editor/window changes */ }
IWorkbenchWindow window) { processRequestors(_scopedRequestors.get(editor)); processRequestors(_scopedRequestors.get(window)); processRequestors(_scopedRequestors.get(window.getWorkbench())); }
for(Iterator<IRequestor> iter = requestors.iterator(); iter.hasNext();) { IRequestor r = iter.next(); if(r.isValid()) { r.resolveArguments(false); r.execute(); } else { iter.remove(); }
We’ll explain scope in a sec Note get(): “track” means whether changes to this value should be tracked and cause re-injection; “group” whether the requestor changes can be grouped together
track==true
method, and instantiate in the right place
level IEclipseContext
(UIEvents.Context.TOPIC_CONTEXT)
OSGi-level Service
<?xml version="1.0" encoding="UTF-8"?> <scr:component xmlns:scr=“http://www.osgi.org/xmlns/scr/v1.1.0"
<service> <provide interface="org.eclipse.e4.core.contexts.IContextFunction"/> </service> <property name="service.context.key" type=“String"
<property name="service.level.key" type="String" value="workbench"/> <property name="service.implementation.key" type=“String"
</scr:component>
Combines OSGi DS with E4 IContextFunctions. Context functions are provided the original look-up hierarchy. On access, WSI uses service.level.key to find appropriate IEclipseContext in the ancestry, and instantiates and registers the implementation.
public class ActivePartLookupFunction extends ContextFunction {
}
If you create/inject an object, you should remember to uninject But IECs have a lifecycle: can be disposed, which cleans up any previously-injected objects. Child context lifetimes are bounded by the parent.
InjectionException: will provide the key being looked up
requestor and suppliers
“display list” and “cursor location”
manager
manager
public class ExampleViewPart extends InjectableViewPart {
}
So how might we use DI to remove the structural dependency for obtaining the DLIP or CursorLocation?
when processed by the injector, lookup two objects using the type names whenever these values are changed, re-inject me Note @Optional if CLPGrMgr is removed, passed in null if DLPFnMgr is removed, then results in an injection exception
But still need to install listeners.
public class LGCSimplifiedPart extends InjectableViewPart {
}
But we’re really not interested in getting the managers — we really want the actual DLIP and cursor location, and to know when they change.
the moment)
IEclipseContext#lookup() pluggable… somehow) — avoid WorkbenchServiceInstaller
performed (e.g., on SWT thread) [contribution from Markus Kuppe]
automagically created when a reference cannot be resolved
require @Inject!
execution context
#invoke() vs #make()
context lookup
IWorkbench, and more
Eclipse parts:
InjectableWorkbenchContribution