Spring integration first cut
dan.j.allen Sep 26, 2010 10:33 PMI just pushed out a first draft of the Spring integration, available in my spring-prototype branch. For now, I've linked against Spring 3.0. We can easily refactor the code somewhat to support Spring 2.5 as well.
http://github.com/mojavelinux/arquillian/tree/spring-prototype
Overview
This integration is interesting because it challenges some the assumptions we make in Arquillian.
The embedded Spring container is straightforward. It's an embedded standalone akin to embedded Weld, OpenEJB and OpenWebBeans. However, Spring is the first programming model we've faced that is not available out of the box in a full embedded or remote container. And it doesn't make sense to create a remote or managed Spring container. Instead, it's implemented as a framework integration that's added to a container, sort of like JSFUnit. We have to extend the programming model of the target container, and thus of the test.
In all, the Spring integration consists of three components:
- Spring test enricher (generic)
- Spring embedded container
- Spring framework integration
Example
Here's an example of a Spring Arquillian test that uses the @Autowired annotation for dependency injection (similar in nature to @Inject).
@RunWith(Arquillian.class) public class SpringEmbeddedAutoAnnotationConfigTestCase { @Deployment public static JavaArchive createDeployment() { return ShrinkWrap.create(JavaArchive.class) .addClasses(AutoWiredBean.class) .addResource(EmptyAsset.INSTANCE, "applicationContext.xml"); } @Autowired SampleBean bean; /** * Ensures the annotation-based injection works. */ @Test public void shouldInjectAutoWiredBean() { assertNotNull("Bean was not injected", bean); assertEquals("Spring", bean.getProgrammingModel()); } }
The location of the Spring configuration file is assumed to be applicationContext.xml in the root of the classpath. You control which configuration file is used by putting the one you want to use into the archive in this location. The default location can be changed using the container configuration.
This test case takes advantage of one of the tricks I've implemented. If you add an empty applicationContext.xml, it will automatically enable the annotation-based wiring. Alternatively, you can:
- use the typical Spring XML-based activation
- set the autowire strategy in arquillian.xml (shown below)
<arquillian xmlns="http://jboss.com/arquillian" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:spring="urn:arq:org.jboss.arquillian.container.spring.embedded_3"> <spring:container> <spring:autowire>annotated</spring:autowire> </spring:container> </arquillian>
The annotated auto-wiring support is only working in the embedded Spring container at the moment.
Class loading
I've spent some more time thinking about the class loading issues in an embedded container. Spring is pretty flexible with regards to plugging in a custom classloader. That's a good fit for the ShrinkWrap class loader. However, there is still an issue.
While the ShrinkWrap class loader makes assets in the ShrinkWrap archive available on the classpath, it does not enforce that only classes and resources in the archive should be visible to the test runner. This is important. Otherwise, the embedded containers could give false results by allowing external classes and resources to be visible, those which won't be there once the archive is deployed remotely.
At first, the instinct is to pass in a null parent class loader. But this is asking for trouble since some classes will already be loaded by the time the test is run and you'll run into linkage errors. A better approach is to refuse to load a class which is not present in the archive, but then allow delegation through the normal chain. Here's how it looks in pseudo-code:
@Override public Class<?> loadClass(String name) throws ClassNotFoundException { if (resource not in archive) { throw new ClassNotFoundException("Class not found in ShrinkWrap archive: " + name); } return parent.loadClass(name); }
We never want to use the parent class loader for loading resources, so I just added:
@Override public URL getResource(String name) { // bypass parent; the only place we should look for resources is in the archives return findResource(name); }
I also found a problem with the way the ShrinkWrap class loader was caching input streams. When Spring attempted to use the input stream, it was marked as closed (can't get to the bottom of why).
Packaging Spring
When using Spring in a remote container, it's necessary to package Spring in the test archive so that it's available to the deployed application. This is where we are challenging some past assumptions because we've never really dealt with an "add-on" programming model (except for test integrations like JSFUnit).
The hard part is knowing what to package. One approach is to simply package all the Spring classes available on the test classpath using an auxiliary archive appender:
final JavaArchive archive = ShrinkWrap.create(JavaArchive.class, "arquillian-spring-libs.jar"); archive.addPackages(true, Package.getPackage("org.springframework"));
However, I ran into a major limitation with this API method. ShrinkWrap attempts to load each class as it's being added to the archive. If one of those classes implements or extends a class/interface that is not available, the operation blows up. What we really want is to just transfer the bytes of the class into the archive (no need to load it). I implemented a little hack using an ArchiveFilter to add the classes as resources. But we should think more about this case.
Another approach is to have the user designate a location where all the Spring dependencies reside and merge all those libraries together into a spring-all.jar (or have them just package a spring-all.jar library).
We want to minimize the burden on the developer without painting them into a corner. So I think we need to brainstorm the best way to ship a framework to the container.
Looking for feedback
This prototype is a start. Now that you have something to play with, perhaps it will begin to move along quickly as a complete replacement for Spring's testing framework (the point being to align w/ the portability and in-container options that Arquillian provides).