Hi,
Because the "dynamic service" approach might be conceptually tricky, I
came up with a different approach, called configuration groups. This
doesn't mean that I've thrown "dynamic services" away, just that I spend
some time on a different solution. It's a free market, I suppose the
best solution wins. I'm also not sure if there IS an actual "market" for
dynamic services.
Summary: you can add @ConfigurationGroup("...") to N services, then if
you contribute M markers, NxM services will be created. Service
dependencies are automatically resolved to use services with the same
marker.
In order to prevent any other core overloads in your (and Howard's)
brain, I'll try to explain how it works step by step.
=== CONFIGURATION GROUPS ===
A configuration group means that of the same service implementation,
multiple services will be created, for example with id's "Service:Red",
"Service:Blue" and markers @Red and @Blue. The id's are automatically
generated in the format <service>:<marker>, but implementations do not
need to be aware of this. In fact, if there are name clashes, they are
automatically resolved by adding a number. By default, if no markers are
contributed, only "Service" will be created without markers. If "null"
is contributed to the configuration group, this service will be created
as well.
I defined an annotation that can be added to a service:
@ConfigurationGroup("yourGroupIdHere")
public Service buildService(...) { ... }
This works with the service binder too:
binder.bind(interface,
implementation).withConfigurationGroup("hibernate-core");
You can contribute to configuration groups using a second annotation I
defined:
@ContributeConfigurationGroup("yourGroupIdHere")
public static void contribute(Configuration<Class> configuration) {
configuration.add(Red.class);
configuration.add(Blue.class);
configuration.add(null);
}
This method MUST be "public static void (Configuration<Class>)". This is
important. We want to know the configurations to create before creating
the Module objects, it makes life much simpler. If you add a "null"
marker, there will also be a configuration without added markers.
A contribution to the original service will be contributed to all its
copies as well. You can contribute to specific copies using their marker:
Contribute(Service.class) @Red
public void someMethodName(...)
=== INJECTION ===
We can use a third annotation to get information about our marker(s):
public static Service buildService(@ConfigurationGroupMarker Class
myMarker, @ConfigurationGroupMarker Set<Class> allMarkers) { ... }
Indeed, the same annotation (@ConfigurationGroupMarker) is used for
Class and for Sets of Class. You can ONLY use this annotation on
services that are in a configuration group.
The set of markers:
- empty or null, meaning that the services are defined without markers
- non-empty, including null, meaning that the services are defined
without markers and with all markers
- non-empty, without null, meaning that the services are only defined
with markers
Now there are two extra injection rules, to make sure that all services
in the same configuration group only use services that have their marker
as well:
* @InjectService(serviceId) and serviceId is in our configuration group
: get service with same marker
* Injection of a service interface also in our configuration group:
first try to resolve it with my marker added, then just default behavior
So you can have the parameter "@Local Service something", and it will
try to resolve the service with markers @Local and @MyMarker.
There are also extra injection rules for all services (inside or outside
configuration groups) and components (in core):
* @InjectService with a marker: get the service created from this
service with the marker
*If an injection results in multiple services with the same interface,
but all in the same configuration group, IoC will return the unmarked
(null) service if it exists, or throw an exception.
* I modified @Inject for components (in core), so it will first resolve
services/marker matches and then use the MasterObjectProvider (so they
will be handled BEFORE ServiceOverride, just like getObject(...) of the
registry already does for the IoC services) ==> was this a bug in
tapestry-core?
=== USAGE FOR HIBERNATE ===
I've used this for hibernate-core.
* Add @ConfigurationGroup("hibernate-core") annotation to all relevant
services
* Add a HibernateUtil class, that uses ServiceActivityScoreboard to find
out which sessions are realized (used by hibernate integration with
tapestry)
* Modify DefaultHibernateConfigurer to use /hibernate-<marker>.cfg.xml
* Modify CommitAfter to accept arrays of markers
I had to do a little more in hibernate integration with tapestry-core,
because it needs to figure out to which session an object belongs (this
is where HibernateUtil from core is used, making sure no unnecessary
connections are made to the database)
Good news! All test cases work fine if no markers are contributed, so I
expect existing applications will work just fine. My test web
application also works, so the case of multiple databases works. Also, I
can sneak in the ServiceOverride without problems now.
=== TECHNICAL STUFF ===
Now, in order to make it all go smooth, I had to modify the injection
stuff in InternalUtils and other objects. Most importantly, I had to add
a lot of functionality to ObjectLocator:
* getService(serviceId, serviceInterface, Class configurationMarker)
* getObject(objectType, annotationProvider, boolean required?)
* getObject(serviceInterface, Class... markers)
* getMarkers() = returns all used markers in the Registry, used for
filtering of annotations in InternalUtils
* Set<Class> getConfigurationGroupMarkers(String groupId) = returns
all markers used in a configuration group (including null)
ServiceResources also implements ConfigurationGroupResources with a
number of methods, and I added extra methods to InternalRegistry, so
RegistryImpl is a bit bigger. These are mostly changes that are
necessary to implement the injection changes.
I needed to modify the DefaultInjectionProvider in tapestry-core to use
"locator.getObject(...) with required=false", because @Inject with
markers would not work otherwise. This may be a bug in Tapestry-core.
I'm not sure, but the DefaultInjectionProvider right now uses ONLY the
MasterObjectProvider, while its equivalent in IoC first checks
services+marker matches.
I'm sitting on a very large number of modified files (77 files right
now, 23 new files mostly for test cases and stuff), partially because
various services needed tiny changes to work with the new interfaces
(e.g. modified tests and mocks)
=== CONCLUSIONS ===
While both approaches work, this approach has the advantage of offering
very concise syntax for the general problem of "services with multiple
configurations", of which "hibernate with multiple databases" is a
specific case.
Dynamic services offers more flexibility, being a more general solution
that solves the problem of "services that cannot be defined at
compile-time" but in return, it takes a bit more coding to convert a
bunch of ordinary services to a bunch of dynamic services, as opposed to
just adding an annotation here and there.
I do not have the code on git or anywhere else yet. I ought to add
javadoc first... and I expect it will take at least a few weeks until
5.3 anyway, so there's no hurry... ( well I want to use this in a
project for a customer, but other than that... ) Also, I still need to
add proper test cases to tapestry-hibernate for the multiple databases
case, instead of the custom web application I use now.
Anyone know how I can run a test webapp without running all that
selenium stuff?
Feel free to give me feedback. I'm sort of poking around blindly if you
experts don't say anything.
Tom.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]