Having performed numerous customizations of third party or upstream/vendor applications based on the Spring framework, I’d like to share some patterns I’ve identified. These patterns form a hierarchy based on preferability - if it is not possible to implement one approach you may have to back off, and try the next best, repeating in stepwise fashion. I am going to list them from least preferable to most preferable in order to walk you through the rationale behind each approach.
We will assume an application distributed in binary form, containing a Spring context with a service interface com.sample.ApplicationService and implementation com.sample.ApplicationServiceImpl defined as an “applicationService” bean which we want to override with some local customization.
Direct class override
The simplest and most direct approach is to simply create a new class named com.sample.ApplicationServiceImpl and arrange for it to appear in the classpath before the upstream implementation. If you have no other options (or are simply prototyping or debugging something) then this might work for you, but it cannot be considered much more than a hack.
There are numerous problems with this approach. First, while it may initially be superficially possible to contrive a classloader chain which results in your implementation being picked up first, this is brittle and may lead to very confusing results when it breaks in the future. You will either be duplicating the service implementation by copying and pasting, or otherwise lying about the pedigree of this class. This will certainly make debugging harder, and will also convey an immediate maintenance overhead. Imagine upstream diverging from the base you forked - you will now have to continually reconcile any future changes to upstream (this may not phase users of decentralized version control systems but is certainly endless and error-prone tedium when working with central SCM like SVN). If you hoped to submit your customizations for inclusion upstream, then overriding the class directly may complicate acceptance. If behavior has changed, tests may fail. Or the change may be rejected because it is not suitable as a default for all users.
A better approach would be to implement a separate class and submit this implementation as an alternative (with tests specific to this implementation of course).
Explicit context inclusion
In order to supply an alternate implementation, you will need to override the service bean defined in the application’s Spring context (let’s assume there is such a bean defined). Unless the application has provided some way for you to inject a context to override this bean you are still in the first situation: having to directly override a file (the Spring context this time) relying on classloader precedence. Let’s assume the application has implemented a specific provision for you to specify your own Spring context for inclusion. It may look like this:
1 2 3 4 5 6 7 |
|
In this situation you can construct a Spring context which redefines the application bean by name (“applicationService”) and supply the context via a system parameter:
1
-DcustomContext=MySpecialCustomization.xml
However this requires explicit, programmatic, support by the application.
Automatic context inclusion
A better approach for the application to implement is to take advantage of Spring’s resource syntax to import a wildcard resource expression. For example:
1
|
|
If placed in the application’s context, this expression will cause the import of any contexts matching the glob “com/sample/*Customization.xml” in any classloader without any additional coordination between application and implementer.
The “classpath*:” syntax tells Spring to look in all classloaders. See Spring’s documentation for a better understanding of classpath resource expressions:
http://static.springsource.org/spring/docs/2.5.x/reference/resources.html#resources-app-ctx-wildcards-in-resource-pathsIf using Maven, for example, simply adding an additional dependency containing the customization (e.g. “com/sample/MySpecialCustomization.xml”) to your project is sufficient to implement the override.
One theoretical downside is that it may be expensive for Spring to iterate through classloaders looking for matching contexts. This is complete speculation on my part, and given the maintenance burden and complexity of the other solutions, it is probably worth a (startup) performance penalty.
Missing bean definition
The above approaches will not work (directly) if you encounter the following situation: You wish to override a class which is not defined as a bean. It is likely this class itself is used (eventually) by a service, which means you need to walk the caller hierarchy to find affected services, and then resume with the above approaches on those services. You will need to override the services, providing implementations (ideally shallow subclasses) which override the minimal number of methodsnecessary to replace invocations of the old class with your new class.
For bonus points, make the aforementioned class injectableas a Spring bean, and submit this enhancement upstream. This will allow you to migrate your customization to a simple bean and throw away your unnecessary wrapper classes/beans.
AOP magic
With the approaches discussed so far, you ultimately still have to replace an entire service. If the application has been modularized sufficiently then this may not be a concern. However, it may be the case that replacing an entire service is heavy-handed, or that the functionality you need implemented is not necessarily best done by replacing a service implementation.
In these situations there is another trick up our sleeve. We can wrap the existing implementation bean with a Spring AOP (Aspect Oriented Programming) aspect. For example we can surgically override a single method on a large interface. Or we can apply common logic to all methods (e.g. caching). We can secretly mutate method arguments and return values. In some situations this is much more convenient than overriding a service (imaging having to override a large service simply to change arguments and delegate to the wrapper service…).
While this is the most loosely coupled approach, there are several downsides. First, AOP can be hard to understand and maintain. Secondly, AOP can complicate debugging as errors can now occur “between” known call frames, and behavior introduced dynamically via AOP will not be obvious to the external observer. Lastly, AOP can only be applied to services defined as beans.
For details on Spring AOP see:
http://static.springsource.org/spring/docs/2.5.x/reference/aop.htmlConclusion
Well, despite the lack of code examples I hope that this post has been helpful. I may in the future construct a sample case that illustrates each of these approaches, and the amount of “maintenance” overhead they impose.
To sum up, I’d like to conclude with:
Direct class overriding: JUST DON’T DO IT
Automatic context inclusion: PLEASE implement this provision if you are writing an application or service in Spring which you anticipate users having to customize