mercredi 19 novembre 2008

GraniteDS Tide for Spring

Tide for JBoss Seam has been available for a while now. The integration is as close as possible mainly because the architecture of the Tide client-side library is heavily inspired by the Seam architecture on the server : stateful components, bijection, conversations...

Spring is different as it promotes a more stateless server architecture and is overall more focused on providing sophisticated low level abilities. Moreover we at graniteds.org have far less experience with it than with JBoss technologies (except Bouiaw who contributed most of the server-side integration).

Consequently the current Tide integration with Spring is maybe not as complete as it could but is already working and provides the most important features :
- Simplified zero configuration remoting
- Managed entities caching on the client
- Data paging supporting remote sorting and filtering
- Transparent loading of lazy collections
- Integration with Hibernate validator
- Tide Flex 3 client framework

Here is a short presentation of the current state of this integration. Most things will work exactly as the Seam version, with very minor differences. Note that most of these features are already present in the GDS 1.1.0 version but will be more polished in the upcoming minor release, with corresponding documentation.

First the inevitable Hello World :

Spring bean (2.5 style with annotations) :

@Service("helloBean")
public class HelloBeanImpl implements HelloBean {
public String sayHello(String name) {
return "Hello, " + name;
}
}


Flex code :

import org.granite.tide.spring.Spring;
import org.granite.tide.spring.Context;

public function init():void {
// The main piece : getting the client context
var tideContext:Context = Spring.getInstance().getSpringContext();
// Triggers the remote call of the 'helloAction' component
tideContext.helloBean.sayHello("Jimi", resultHandler);
}

private function resultHandler(event:TideResultEvent):void {
// Should output 'Hello, Jimi'...
trace(event.result);
}


It's hard to find a difference with the Seam version, for the good reason that there is none, except the name of Tide static factory object (Spring) and of the various packages.

In fact the only client-side impact of using Spring on the server is that you cannot use injection/outjection to transmit data to and from the server beans.
All remote calls will have to be stateless. This seems in line with the Spring application style.


Entity caching

A particular case is when managed entities are transmitted as arguments or return objects of remote calls through Tide.

Spring bean (2.5 style with annotations) :

@Service("personBean")
public class PersonBeanImpl implements PersonBean {

@PersistenceContext
private EntityManager manager;

public Person createPerson(Person person) {
manager.persist(person);
manager.flush();
return person;
}
}


Flex code :

import org.granite.tide.spring.Spring;
import org.granite.tide.spring.Context;

[Bindable]
public var person:Person;

public function createPerson():void {
person = new Person();
person.firstName = "Jimi";
person.lastName = "Hendrix";
tideContext.personBean.createPerson(person, createPersonResult);
}

private function createPersonResult(event:TideResultEvent):void {
person = event.result as Person;
}


There seems to be nothing very special here, except that Tide has merged the content of the original entity with the data from the Person object received from the server. Only the modified properties will then trigger PropertyChangeEvents, not the complete person variable. This is important if you have bound UI components to this particular Person instance.
In fact the whole result handler could even be omitted in this case, because Tide has already modified the person variable before the assignment. There will always be only one instance of an entity with a particular uid in a Tide context (the entity uid is very important for this reason, see corresponding section 'Managed entities' in the docs).


The other particular case is the one of collections :

@Service("personsBean")
public class PersonsBeanImpl implements PersonsBean {

@PersistenceContext
private EntityManager manager;

public List findAllPersons() {
Query q = manager.createQuery("select p from Person p");
return q.list();
}
}


Flex code :

import org.granite.tide.spring.Spring;
import org.granite.tide.spring.Context;

[Bindable] [Out]
public var persons:ArrayCollection;

public function findPersons():void {
person = new Person();
person.firstName = "Jimi";
person.lastName = "Hendrix";
tideContext.personsBean.findAllPersons(findAllPersonsResult);
}

private function findAllPersonsResult(event:TideResultEvent):void {
persons = event.result as ArrayCollection;
// Could have been replaced by tideContext.persons = event.result as ArrayCollection if client framework ([Out] annotation) is not used.
}


Here it is important to assign the result of the remote call to a context variable (either directly or through outjection) during the result handler method if you want to maintain the same collection variable instance between multiple calls to 'findPersons' (very handy when using data binding in DataGrids and only way to correcly use Flex data effects).

The variable assignment will in fact not replace the variable itself but instead merge the received collection content with the content of the existing variable instance. This is (a little) more involving than for entities because there is no uid here that can be used to match the received collection with the existing one (with Seam oujection we could use the name of the context variable).
Of course if you don't make this assignment you will get the correct results but the collection instance will be different (and data bindings will likely be broken also).


Data paging

The good news here is that there is a PagedQuery component in the Tide Spring client library. The bad news is that Spring does not provide an equivalent of the Seam Query component, it will be necessary to build one for each query. It is certainly possible to provide a generic Query component for Spring but this is definitely not in the scope of GDS and it seems that this feature has already been requested by people on the Spring framework.

For now it will be necessary to implement a Spring bean having a method with the following signature :

public Map find(Map filter, int first, int max, String order, boolean desc);


first, max, order and desc are straightforward.
filter is a map containing the parameter values of the query. These values can be set on the client by tideContext.queryBean.filter.parameter1 = value1;
The return object must be a map containing 4 properties :
- firstResult : should be exactly the same as the argument passed in
- maxResults : should be exactly the same as the argument passed in, except when its value is 0, meaning that the client component is initializing and needs a max value. In this case, you have to set the page value (must absolutely be greater than the maximum expected number of elements displayed simultaneously in a table).
- resultCount : number of results
- resultList : list of results

The following code snippet is a quick and dirty implementation :

@Service("people")
@Transactional(readOnly=true)
public class PeopleServiceImpl implements PeopleService {

@PersistenceContext(type = PersistenceContextType.EXTENDED)
protected EntityManager manager;


public Map find(Map filter, int first, int max, String order, boolean desc) {
Map result = new HashMap(4);
String from = "from " + Person.class.getName() + " e ";
String where = "where lower(e.lastName) like '%' || lower(:lastName) || '%' ";
String orderBy = order != null ? "order by e." + order + (desc ? " desc" : "") : "";
String lastName = filter.containsKey("lastName") ? (String)filter.get("lastName") : "";
Query qc = manager.createQuery("select count(e) " + from + where);
qc.setParameter("lastName", lastName);
long resultCount = (Long)qc.getSingleResult();
if (max == 0)
max = 36;
Query ql = manager.createQuery("select e " + from + where + orderBy);
ql.setFirstResult(first);
ql.setMaxResults(max);
ql.setParameter("lastName", lastName);
List resultList = ql.getResultList();
result.put("firstResult", first);
result.put("maxResults", max);
result.put("resultCount", resultCount);
result.put("resultList", resultList);
return result;
}
}



Lazy loading of collections

As with Seam, there is almost nothing to do to have this working.
You will just have to specify the name of the EntityManagerFactory Spring bean that will be used to execute the loading as a property of the Tide destination.

<destination id="spring">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>tideSpringFactory</factory>
<entityManagerFactoryBeanName>entityManagerFactory</entityManagerFactoryBeanName>
</properties>
</destination>


Alternatively, it's possible to use a non-JPA persistence manager by defining a persistenceManagerBeanName instead of the entityManagerFactoryBeanName. For example, a plain Hibernate persistence manager is available and can be configured with :
<destination id="spring">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<factory>tideSpringFactory</factory>
<persistenceManagerBeanName>tidePersistenceManager</persistenceManagerBeanName>
</properties>
</destination>


And in applicationContext.xml, add the persistence manager and define it as transactional read-only :
<bean id="tidePersistenceManager" class="org.granite.tide.hibernate.HibernateSessionManager"
scope="request">
<constructor-arg>
<ref bean="sessionFactory"/>
</constructor-arg>
</bean>

<aop:config>
<aop:pointcut id="tidePersistenceManagerMethods"
expression="execution(* org.granite.tide.ITidePersistenceManager.*(..))"/>
<aop:advisor advice-ref="tidePersistenceManagerMethodsTxAdvice"
pointcut-ref="tidePersistenceManagerMethods"/>
</aop:config>

<tx:advice id="tidePersistenceManagerMethodsTxAdvice"
transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" read-only="true"/>
</tx:attributes>
</tx:advice>



Integration with Hibernate Validator

The Tide Flex validators should work with Spring and server validation is enabled by default if Hibernate Validator in found on the classpath.


Conclusion

We now have seen the whole thing.
There is nothing particularly impressive compared to the Seam integration, but this is a good step forward.
More than usually we need feedback from the courageous people who will try this to improve and extend the integration with features more specific to Spring.

6 commentaires:

Seto a dit…

tideContext.personsBean.findPersons(findPersonsResult);

Here should be findAllPersons instead? And ther result handler change a name? Is it a mistake?

// Triggers the remote call of the 'helloAction' component tideContext.helloAction.sayHello("Jimi", resultHandler);

And here should be helloBean instead? A mistake again?

William Draï a dit…

Thanks for the remark. It's fixed.

Seto a dit…

The second mistake isn't fixed in the article.
And any solution of hard access of the blog for Chinese users? The domain blogspot is blocked in daytime in China. Is it possible for this blog access with a domain www.graniteds.org/blog? Maybe it should help.

Unknown a dit…

I encountered the following error :

Context initialization failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'tidePersistenceManager' defined in ServletContext resource [/WEB-INF/applicationContext.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [org.granite.tide.hibernate.HibernateSessionManager]: Constructor threw exception; nested exception is org.hibernate.HibernateException: No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here


Isn't it related to the transaction manager which is bypassed ?

I'm using Spring 2.5.6 with annotation based config for spring and hibernate

Unknown a dit…

The reason is explained in the comments of this page :

http://blog.springsource.com/2007/06/26/so-should-you-still-use-springs-hibernatetemplate-andor-jpatemplate/

And more precisely, the comment made by Juergen Hoeller :

Actually, "sessionFactory.getCurrentSession()" will throw an exception when called outside of a managed scope (i.e. in case of no thread-bound Session management being active). So that style can effectively only be used within a transaction and/or an active OpenSessionInViewFilter/Interceptor. This is equivalent to HibernateTemplate's "allowCreate=false" behavior.


I did a quick and dirty (no comments on the sysout and catch Exception :-) ) workaround to be able to go further in my testing.
I chandeg this in the source of the HibernateSessionManager (tide-hibernate).

This is probably not the right way, but it's okay for my testing purposes.
(suggestions on the best practices are welcome, especially if there's a clean way to make it work with the transaction manager managed by Spring which manages Hibernate's session).

public HibernateSessionManager(SessionFactory sf) {
try {
this.session = sf.getCurrentSession();
}
catch (Exception e) {
System.out.println("No hibernate session found - requesting new one");
this.session = sf.openSession();
}
}

Unknown a dit…

I truly like to reading your post. Thank you so much for taking the time to share such a nice information.