Wednesday, May 16, 2007

Clean solutions are the best

Don't you love it when things just seem to fit together exactly as you want them to? It doesn't happen all too often, but when it does, you just want to blog at the top of your lungs...

I'm working on a project where we communicate with an index server over HTTP, and recieve the query results in XML format. It's a data source, which basically boils down to this interface - the DocumentSource:

Document retrieveDocument( URL url );

The contract of this component is that if the document retrieval succeeds, we get a parsed and ready org.w3c.Document back, and in all other cases it throws some kind of exception. Handling exceptions is done in another part of the application, so don't worry about that right now.

Now, a failure can be either uncontrolled, for example if the index application hits a bug, if there's a network problem, the server might be on fire and so on. It could also be a problem with our implementation of of the afromentioned interface (not likely). In either case, the component will throw an exception to the caller, possibly wrapped in some way.

But a failure can also be controlled, which means that the index server returns a valid XML error message along with HTTP status 200 (OK), if the query is invalid in any way. This is also considered an error, and we handle in like this:

if (isErrorResult(document)) {
throw new IndexErrorException(method, document);

The method variable is the HttpMethod that we tried to execute, containing host, port and query information, and document is the parsed XML response from the index, in this case an error message.

What we want to do now is log this error, as transparently as possible. The snippet above expresses that we're not really interested in handling the error in detail, we just leave it at "ok, there was an error, so let's throw an exception. Here's all the information I have on why and where the error occurred".

That's all very well, but we still have to inspect the XML error message to present the error in a readable way. The best way to do that is of course the message property of the exception.

Here's where another aspect of our application comes in: we use Freemarker to build views (HTML and others) using the XML data we retrieve from the DocumentSource interface shown above. And since the error message is also XML, and we want to build a kind of view - a String - why not use the same approach here? That way we won't have to deal with cumbersome Java DOM apis, and we have maximum power to extract and format the error message the way we want. Sounds like a good idea.

It's also the case that we've abstracted away a lot of the fuss around the template engine, as well as the fact that we're using Freemarker, behind this very simple TemplateService interface:

String mergeTemplateIntoString( String templateName, Map model );

This service is a component, a Spring bean, in our application. It would be great if this service could be used to render the logged string from the document and the HTTP method, but in that case we have to supply the newly instantiated exception with a reference from the context. Sure, this could be accomplished by holding an (otherwise useless) reference in the DocumentSource implementation that is passed along the exception, but I prefer it when things Just Work.

Enter @Configurable and compile-time AspectJ weaving! We slap on an a TemplateService setter and an annotation (with autowire=Autowire.BY_NAME to avoid the need for a boilerplate bean definition) on the exception class. We're using AspectJ aspects for various other tasks already, and weaving is done at compile-time to avoid the proxy problem. The Codehaus Maven plugin works great!

In order to weave the Spring AnnotationBeanConfigurerAspect, we add the following to pom.xml:


And the aspect must be made aware of the Spring context (in any application context file):

<aop:spring-configured />
So from here on, the exception message may be rendered like this:

public String getMessage( ) {
return templateService.mergeTemplateIntoString( "error", model );

where "error" is the name of a template, and the model contains the XML document and some other stuff. The error message is now ready to be read and logged, for example in an @AfterThrowing aspect, but that's another story.

1 comment:

Robert said...

Nice one. Didn't see that one comming in the beginning.

Great post.