Wednesday, January 30, 2008

Code by convention with Flex and Spring

Some years ago XML configuration was considered as a great way to configure applications. Take for example Struts' struts-config.xml, in the beginning it was like heaven how you could define all your action mappings etc. But as applications grow it gets harder and harder to maintain this bunch of XML.

The point is applications and application frameworks evolve. In modern application frameworks code by convention is becoming an important feature which reduces repetition and increases uniformity and productivity.

Take for example Grails and Ruby on Rails which are build from the ground up with code by convention in mind. For example in Grails a *Controller.groovy class is a automatically a controller and a *Service.groovy class is automatically a service. You do not need any XML configuration.

Back in my holidays in December I tried Adobe's BlazeDS. From Adobe Labs: BlazeDS is the server-based Java remoting and web messaging technology that enables developers to easily connect to back-end distributed data and push data in real-time to Adobe Flex and Adobe AIR applications for more responsive rich Internet application (RIA) experiences.

What this means is that Flex clients can communicate with Java objects deployed on the server. BlazeDS contains a Java Adapter which forms the infrastructure to make this possible. With Jeff Vroom's Spring Integration you can even use Spring beans to communicate with.

The only drawback with the standard Java Adapter (and so also the Spring Integation) is that each Java object you want to call on the server, you need to configure in remoting-config.xml. Guess what? I don't like that! So what I did is I created a JDK5 annotation to autowire Spring beans directly into BlazeDS. It means when you use Spring and JDK5 you can annotate your service classes with a @RemotingDestination annotation, and they get automatically registered in BlazeDS. Some sort of code by convention I would say!

How it works:


  1. Download the @RemotingDestination annotation for Spring supporting files at http://www.adobe.com/cfusion/exchange/index.cfm?event=extensionDetail&loc=en_us&extid=1398018. It also includes Jeff Vroom's Spring Factory.

  2. Expand remotingdestination-annotation.zip

  3. Copy dist\remotingdestination-annotation.jar in the WEB-INF\lib directory of your web application. Instead you could also include the Java sources (located in src) in your project.

  4. Download the Spring framework at http://www.springframework.org/download

  5. Locate spring.jar in the dist directory and copy it in the WEB-INF\lib directory of your web application

  6. In your Flex services-config.xml configure the SpringAutowiringBootstrapService:

    <?xml version="1.0" encoding="UTF-8"?>
    <services-config>

    <services>
    <service-include file-path="remoting-config.xml" />
    <service-include file-path="proxy-config.xml" />
    <service-include file-path="messaging-config.xml" />

    <service id="spring-autowiring-bootstrap-service" class="flex.contrib.services.SpringAutowiringBootstrapService"/>

    <default-channels>
    <channel ref="my-amf"/>
    </default-channels>

    </services>

    ....
    The SpringAutowiringBootstrapService checks all @RemotingDestination annotated classes and registers them within BlazeDS at runtime. It also dynamically creates the Spring Factory.
    Note that the default channel is required when using runtime registration of remoting destinations.

  7. Finally annotate your service classes with the @RemotingDestination annotation:

    package flex.contrib.samples.mortgage;

    import org.springframework.beans.factory.annotation.Autowired;

    import flex.contrib.stereotypes.RemotingDestination;

    @RemotingDestination(destination="mortgageService")
    public class Mortgage {

    @Autowired
    RateFinder rateFinder;

    public void setRateFinder(RateFinder rateFinder) {
    this.rateFinder = rateFinder;
    }

    public double calculate(double amount) {
    int term = 360; // 30 years
    double rate = rateFinder.findRate();
    return (amount*(rate/12)) / (1 - 1 /Math.pow((1 + rate/12), term));
    }

    }

    By default the Spring bean name is used to register the remoting destination. The name can be overridden using the destination attribute of the annotation. The example class uses Spring annotation-based configuration using the @Autowired annotation. In this case the Spring application context would look like:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-2.5.xsd">

    <context:annotation-config/>
    <context:component-scan base-package="flex.contrib.samples"/>
    </beans>


That's it! The Mortgage service will be automatically registered without any XML configuration. The @RemotingDestination annotation allows you to easily expose services within BlazeDS. Together with Spring's annotation-based configuration this provides a productive code by convention experience for writing Flex applications with Spring on the server-side.

For more info about the @RemotingDestination annotation, SpringAutowiringBootstrapService see the Javadocs in docs\api.

25 comments:

Anonymous said...

Cool! Really useful stuff! I will try it soon and comment more on this. Annotations are great for RAD and this is ideal for integration with BlazeDS.

Ryan said...

Nice annotations. I noticed a few issues with it:

1 - it calls getBean on every bean that is configured. If you have any abstract beans, it will throw an exception. (Hibernate's transactionProxy bean is abstract)

2 - It doesn't handle the case where the destination is already configured (such as in an xml file) - and throws an exception for that.

Here's the code to trap those exceptions:

try {
Object bean = ctx.getBean(beanName);

// if the bean is a remoting destination bean, create the remoting destination
if (AnnotationUtils.isAnnotationDeclaredLocally(RemotingDestination.class, bean.getClass())) {
FlexUtils.createRemotingDestination(bean, beanName, mb);
}

}
catch (org.springframework.beans.factory.BeanIsAbstractException e) {
// do nothing... abstract beans aren't going to be remote destination beans
}
catch (flex.messaging.config.ConfigurationException e) {
log.error("Configuraton Exception. - The remoting destination for " + beanName + " cannot be autowired.", e);
}
catch (RuntimeException re) {
log.error("Runtime exception prevented remoting destination for " + beanName + " from being configured");
}

(I added a static logger to mine also - at the top:

private static Log log = LogFactory.getLog(SpringAutowiringBootstrapService.class);

If you want my updated .java file, let me know and I can email it to you to post here.

Jason Sheedy said...

Hi Marcelo,
nice use of annotations. However, I've run into a bit of a problem and am working on a work around for it at the moment. If I find a solution I'll post it back here later on.

It's kind of related to the post by 'canyoneering' except the problem for me is that proxied beans do not register as being annotated with @RemoteDestination. i.e. if a bean is being advised, the resulting proxy class will not have isAnnotationDeclaredLocally returning true in the SpringAutowiringBootstrapService.

Also, I found the the jar included in the distributed is corrupt and import properly...maybe something to do with the manifest of something. I had to rebuild the jar from scratch.

Jason Sheedy said...

Hi again Marcel. I've created a fix for the AOP proxy problem. I'll email it to you directly to have a look.

Marcel Overdijk said...

Thanks guys,

I will try to apply the fixes this weekend and release them on the Adobe Flex Exchange.

Cheers,
Marcel

robcos said...

Hi Marcel!
Did you manage to apply the fixes you received? It would be nice if you could at least publish a non corrupt jar on flex exchange.
Thank you
/roberto

Marcel Overdijk said...

@robos,

Yes I managed to do an update. I had uploaded it to the Adobe Exchange (and it was approved) but still the old one is downloaded every time..

I contacted Adobe but did not get any answer from them...

The biggest problem is I lost the sources as my computer crashed...
I will try to contact Adobe again, and see if the file is still there.

Marcel Overdijk said...

Hope I will get a reaction this time: http://www.adobeforums.com/webx/.59b4f738

Marcel Overdijk said...

PS: the current version on the Adobe Flex Exchange contains a jar which was compild with 1.6. So it was not really corrupt. However the Java souces are included so (1) add them directly in your project or (2) compile them again to a new jar

Anonymous said...

Thank you Marcel.
I compiled the jar my self and works fine (your jar does not give any output on "jar -tvf". Does java 6 change the jar format?)

To bad that you lost the aop fix...

Anonymous said...

Hi Marcel,

Do you have any news on the updating of the sample code? I am having a lot of problems getting what is on the Adobe site up and running.

Thanks!

Marcel Overdijk said...

I did not get any reply from Adobe; not via mail and not via their forum (see http://www.adobeforums.com/webx/.59b4f738). I'm really dissapointed in Adobe Exchange.

What's your exact problem, perhaps I can help.

Cheers,
Marcel

Anonymous said...

Hi Marcel,

I keep getting the error:
"[RPC Fault faultString="No destination with id 'mortgageService' is registered with any service." faultCode="Server.Processing" faultDetail="null"]"

from the flash client. For some reason, the "autowiring" is not working. I am putting in traces throughout the code to see what is wrong.

I am getting no errors in Tomcat, but for some reason, the remoteobject is not being registered.

I'll post an update if I find the cause of the problem.

Ajit Nilakantan said...

Hi Marcel,

After a lot of digging, I think I found the problem. It is actually a Spring (2.5.3) bug (see: http://jira.springframework.org/browse/SPR-3815 ).

Spring's context:component-scan does not work if the directory entries are missing in the jar file. I am using old school Makefiles and calling something like:
jar cvf mortagesample.jar flex/contrib/samples/Mortgage.class flex/contrib/samples/RateFinder.class flex/contrib/samples/SimpleRateFinder.class

, and copying the jarfile to WEB-INF/lib

Sun's "jar" command does not create the empty directory entries that Spring expects.

Note that Eclipse uses its own implementation of jar and offers the "Add directory entries" option that works around this bug.

If I create the jarfile with the directory entries (e.g. something like: jar cvf morgagesample.jar flex) -or- expand the jarfile into classes, everything works.



Thanks again.

Anonymous said...

Hi Marcelo, Very cool post! Have you considered submitting this to the BlazeDS code base? This would make it more stable when new releases of Blaze come out.

Marcel Overdijk said...

@Brian

If BlazeDS wants to include this that's fine with me. I don't know they are interested, as it's tight to Spring.

Cheers,
Marcel

Unknown said...

Marcel

I read you were not interested in developing Flex scaffolding generation with the Flex - Grails plugin.

Would you change your mind if I offer some help?

Thanx...

Ryan said...

In lines with the other problem I had before (the abstract bean problem) - if any beans are scoped to a session or a request, the bootstrapping service will throw an exception because there is no thread local request for spring to find - so it pukes and dies.

I didn't bother trying to fix that issue, because I ran into a couple other quirks in the way I was implementing it for a situation where I wanted to make a one-off prototype object that lived was tied to the session that I could inject the httpSession into so I could also pull things from the session (i.e. verify that they were logged in at the endpoint, and handle some other security stuff)

In any case - it's something to be aware of.

dodol said...

Maybe if Adobe won't include this in their release. I think you can contribute this to Spring. I really hope this cool @RemotingDestination exists in Spring Remoting/Web Services.

Anonymous said...

Hi Marcel,it very useful.can you send me a newest copy.thx! my e-mail:bencmai@21cn.com

François said...

Hello Marcel,

Thanks a lot for this blog entry and the associated source code.

I re-used and included your stuff in a library I released and documented here :
http://fna.googlecode.com/svn/trunk/fna/site/flex-contrib-spring/index.html

This library is also referenced in a maven archetype of mine:
http://fna.googlecode.com/svn/trunk/fna/site/mvn_archetypes/blazeds-autowired-spring-hibernate-archetype/index.html
Blazeds-autowired-spring-hibernate-archetype helps you generate a multi-module maven project : a flex front-end application communicating with the backend through Adobe's blazeds messaging. The back-end relies on a spring hibernate architecture.


I'd be happy to update this library with your newest findings.

Cheers !
François

Anonymous said...

Good words.

Bob Walker said...

Hi,

I patched RemotingDestination.java and SpringAutowiringBootstrapService.java to include the ability to specify a security constraint annotation.

If anyone is interested, let me know and I'll post the revised code.

Anonymous said...

Thanks ur information



Small business website design

tcoulson said...

This seems to be a bit outdated. There is no spring.jar file anymore. How can we use this with Spring 3?