Spring et DWR

Direct Web remoting est un framework Ajax permettant un lien direct entre Javascript et Java coté serveur.

D'apparence assez simple, son intégration avec Spring peut rapidement devenir compliquée, notamment à cause de la gestion des namespaces dans le fichier XML de configuration. En effet, les versions récentes de DWR fournissent un namespace facilitant l'écriture des éléments spécifiques :


<dwr:controller id="dwrController" debug="true"/>
<dwr:configuration>...</dwr:configuration>

Mais puisque tout n'est pas rose, j'ai été confronté à une erreur récurrente au démarrage du contexte Spring : No bean named '__dwrConfiguration' found...

Quelque soit la version du namespace utilisé, pas moyen de m'en sortir... Voici donc comment revenir à une configuration plus simple basée sur des éléments XML plus classiques :

application-context-dwr.xml :


<?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:dwr="http://www.directwebremoting.org/schema/spring-dwr"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                           http://www.directwebremoting.org/schema/spring-dwr
                           http://www.directwebremoting.org/schema/spring-dwr-2.0.xsd">
    
    <dwr:url-mapping />

    <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />

    <bean id="myDwrServerBean" class="my.package.MyDwrServerBean">
        <dwr:remote javascript="serverBridge"></dwr:remote> <!-- nom de l'objet javascript utilisable -->
    </bean>


    
    <bean id="__dwrController" class="org.directwebremoting.spring.DwrController"> 
        <property name="configurators">
         <list>
            <ref bean="__dwrConfiguration"/>
         </list>
      </property>
      <property name="debug" value="true"/>
    </bean>
    
   <bean id="__dwrConfiguration" class="org.directwebremoting.spring.SpringConfigurator">
        <property name="creators">
            <map>
                <entry key="firstBean">
                    <bean class="org.directwebremoting.spring.CreatorConfig">
                        <property name="creator">
                            <bean class="org.directwebremoting.spring.BeanCreator">
                                <property name="bean" ref="myDwrServerBean"/>
                                <property name="javascript"><value>serverBridge</value></property>
                            </bean>
                        </property>
                    </bean>
                </entry>
            </map>
        </property>
        <property name="converters">
            <map>
                <entry key="my.package.MyDwrServerBean">
                    <bean class="org.directwebremoting.spring.ConverterConfig">
                        <property name="type">
                            <value>bean</value>
                        </property>
                    </bean>
                </entry>
            </map>
        </property>
    </bean>
</beans>

Il est également possible de configurer DWR via un bean dans le contexte Spring en utilisant le FluentConfigurator.

En espérant vous avoir fait économiser quelques heures de recherches!


Fichier(s) joint(s) :



Gestion de l'authentification sur Google App Engine

Lors de la création d'une application sur la plateforme GAE, il est possible de configurer l'usage des comptes Google pour s'y connecter, comme décrit ici : Configuration de l'authentification de votre application

En mode développement, le serveur de test simule automatiquement cette utilisation de compte Google et met en place un système de redirection vers une page de connexion spécifique (https://developers.google.com/appengine/docs/java/tools/devserver#Using_Users) :

L'utilisateur connecté sera alors accessible simplement avec :


import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.appengine.api.users.User;
...
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();

Pour ce qui est de la déconnexion, il faut créer une redirection vers l'URL gérée par Google. Pour ce faire, voici un exemple de servlet :


import com.google.appengine.api.users.UserServiceFactory;
import java.io.IOException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.logging.Level;
import java.util.logging.Logger;

public class LogoutController {

    @RequestMapping(method = RequestMethod.GET)
    public void logout(HttpServletRequest request, HttpServletResponse response) {

        SecurityContextHolder.clearContext();

        // Remove the cookie
        Cookie terminate = new Cookie(
                TokenBasedRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
                null);
        terminate.setMaxAge(-1);
        terminate.setPath(request.getContextPath() + "/");
        response.addCookie(terminate);
        try {
            response.sendRedirect(UserServiceFactory.getUserService().createLogoutURL("/"));
        } catch (IOException ex) {
            Logger.getLogger(LogoutController.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

Hope this helps!


Fichier(s) joint(s) :

Configurer une application web pour Google App Engine

GAE requiert une configuration bien précise, voir spécifique, de l'application web destinée à être déployée sur ces serveurs. Je vais essayer de détailler ci-dessous les éléments que j'ai rencontré.

Descripteur de déploiement

En plus du fichier web.xml commun à toutes les applis web, GAE demande la mise en place d'un nouveau fichier de description spécifique, appengine-web.xml présenté dans ce chapitre de la documentation.

Validation des fichiers XML

Le serveur de test peut parfois poser des problèmes lors de la phase de validation Spring des fichiers XML. Pour la désactiver, il faut indiquer dans le fichier web.xml une classe spécifique de configuration :


<context-param>
 <param-name>contextClass</param-name>
 <param-value>my.package.MyXmlWebApplicationContext</param-value>
</context-param>

Pour ce besoin, son contenu est assez simple :


import com.google.appengine.api.utils.SystemProperty;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.web.context.support.XmlWebApplicationContext;

public class MyXmlWebApplicationContext extends XmlWebApplicationContext {
    @Override
    protected void initBeanDefinitionReader(XmlBeanDefinitionReader beanDefinitionReader) {
        super.initBeanDefinitionReader(beanDefinitionReader);
        if (SystemProperty.environment.value() == SystemProperty.Environment.Value.Development) {
            beanDefinitionReader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE);
        }
    }
}

persistence.xml

Afin de lier notre application au datastore de Google, voici un exemple basique de fichier de persistence :


<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="transactions-optional">
        <provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
        </properties>
    </persistence-unit>

</persistence>

Déclaration du bean "entityManagerFactory" via Spring

Afin de rendre effective la connexion au datastore, il est nécessaire d'ajouter au fichier application-context.xml de Spring :


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
    <bean id="entityManagerFactory" class="com.my.MyEntityManagerFactory"/>
</beans>

Voici le contenu de la classe indiquée :


import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceException;
import org.springframework.orm.jpa.LocalEntityManagerFactoryBean;

public class MyEntityManagerFactory extends LocalEntityManagerFactoryBean {
    @Override
    protected EntityManagerFactory createNativeEntityManagerFactory() throws PersistenceException {
        return EMF.get();
    }
}

La classe EMF est tirée des préconisations Google (https://developers.google.com/appengine/docs/java/datastore/jpa/overview?hl=fr) :


import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public final class EMF {
    private static final EntityManagerFactory emfInstance =
        Persistence.createEntityManagerFactory("transactions-optional");
    
    private static EntityManager entityManager = emfInstance.createEntityManager();

    private EMF() {}

    public static EntityManagerFactory get() {
        return emfInstance;
    }

    public static EntityManager getEntityManager() {
        return entityManager;
    }
}

Avec ces éléments, vous devriez pourvoir démarrer votre application rapidement.


Fichier(s) joint(s) :

Maven, Netbeans et Google App Engine

Comme promis dans mon précédent article, voici quelques explications sur la configuration du POM requise pour créer une application compatible avec GAE.

Les éléments présentés ci-dessous ne le sont qu'à titre d'exemple, le but étant essentiellement de les regrouper de manière accessible.

Server de test

Lors de la phase de développement, il est possible d'utiliser Maven pour démarrer un server App Engine de test basé sur Jetty :


<plugin>
 <groupId>org.mortbay.jetty</groupId>
 <artifactId>jetty-maven-plugin</artifactId>
 <version>8.0.1.v20110908</version>
 <configuration>
  <scanIntervalSeconds>1</scanIntervalSeconds>
  <stopKey>stop-jetty</stopKey>
  <stopPort>9999</stopPort>
 </configuration>
</plugin>
<plugin>
 <groupId>com.google.appengine</groupId>
 <artifactId>appengine-maven-plugin</artifactId>
 <version>1.8.0</version>
</plugin>

Configuration de Netbeans

Pour démarrer ce serveur, il faut également configurer les actions du projet :

Activer le debug du serveur de test avec Netbeans

Dans un profil par exemple :


 <profile>
 <id>appengine</id>
 <properties>
  <env>appengine</env>
 </properties>
 <activation>
  <activeByDefault>true</activeByDefault>
 </activation>
 <build>
  <plugins>
   <plugin>
    <groupId>com.google.appengine</groupId>
    <artifactId>appengine-maven-plugin</artifactId>
    <version>1.8.0</version>
    <configuration>
     <jvmFlags>
      <jvmFlag>-Xdebug</jvmFlag>
      <jvmFlag>-Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n</jvmFlag>
     </jvmFlags>
    </configuration>
   </plugin>
  </plugins>
 </build>
</profile>

De cette manière, après le lancement du serveur de test avec la commande mvn appengine:devserver, il sera possible d'attacher un debugger au port du processus distant.

Dépendances à supprimer

Suite aux explications données dans l'article précédent, les dépendances ci-dessous ne sont plus nécessaires :

  • mysql (ou autre JAR de driver/connector de SGBD)
  • ehcache
  • aspectj
  • hibernate
  • spring AOP
  • spring tx (pour les transactions)

Couche de persistence

Voici les dépendances nécessaires :


<dependency>
 <groupId>com.googlecode.objectify</groupId>
 <artifactId>objectify</artifactId>
 <version>4.0b3</version>
</dependency>
<dependency>
 <groupId>com.google.appengine.orm</groupId>
 <artifactId>datanucleus-appengine</artifactId>
 <version>2.0.1.1</version>
 <!-- Need to exclude the enhancer since it interfere with the enhancer plugin.
 <exclusions>  
  <exclusion>  
   <groupId>org.datanucleus</groupId>  
   <artifactId>datanucleus-enhancer</artifactId>  
  </exclusion>  
 </exclusions> -->  
</dependency>
<dependency>
 <groupId>org.datanucleus</groupId>
 <artifactId>datanucleus-core</artifactId>
 <version>3.0.11</version>
</dependency>
<dependency>
 <groupId>org.datanucleus</groupId>
 <artifactId>datanucleus-api-jpa</artifactId>
 <version>3.0.11</version>
</dependency>
<dependency>
 <groupId>org.apache.geronimo.specs</groupId>
 <artifactId>geronimo-jpa_2.0_spec</artifactId>
 <version>1.0</version>
</dependency>
<dependency>
 <groupId>javax.jdo</groupId>
 <artifactId>jdo-api</artifactId>
 <version>3.0</version>
</dependency>

Pour activer le tissage des classes métier par Datanucleus lors de la compilation (enhancer), voici les plugins à déclarer dans la section build :


<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-surefire-plugin</artifactId>
 <version>2.14.1</version>
</plugin>
<plugin>
 <groupId>org.datanucleus</groupId>
 <artifactId>maven-datanucleus-plugin</artifactId>
 <version>3.0.2</version>
 <configuration>
  <api>JPA</api>
  <verbose>true</verbose>             
  <mappingIncludes>**/domain/*.class</mappingIncludes>
  <fork>false</fork>
 </configuration>
 <executions>
  <execution>
   <phase>process-classes</phase>
   <goals>
    <goal>enhance</goal>
   </goals>
  </execution>
 </executions>
</plugin>

Dans l'article suivant, je présenterai les autres fichiers de configuration nécessaires, notamment la configuration de Spring pour la persistence.


Fichier(s) joint(s) :

Migration d’une application vers Google App Engine

Cet article ouvre une série de billets ayant pour but de condenser et expliquer l'ensemble des difficultés que j'ai rencontrées lors du déploiement d'une application web standard vers GAE.

L'application initiale utilise quelques technologies classiques du web : Ajax (framework DWR), JSP, MySQL, Hibernate, JPA, Spring (AOP, MVC, DAO).

Configuration du projet

La plateforme GAE requiert l’ajout de dépendances vers :

  • API App Engine : pour l’accès aux fonctionnalités spécifiques (récupération de l’utilisateur connecté...)
  • Datanucleus (ORM) : le data store de App Engine étant du NoSQL, Hibernate n’est plus utilisable.
  • Objectify (recommandé) : couche d’abstraction de persistence.

Spring

GAE préconise plusieurs optimisations de la configuration Spring ainsi que certains usages spécifiques à sa plateforme, détaillés ci-dessous.

source : https://developers.google.com/appengine/articles/spring_optimization

L’application initiale est basée sur la détection automatique de beans typés par interfaces : la configuration du contexte ci-dessous permet à Spring de parcourir les packages indiqués pour trouver les implémentations à injecter :

<context:component-scan base-package="tudu.service"/>

Sur GAE, ce système est déconseillé (pour des raisons de performances de la plateforme) et il est recommandé de préférer un usage plus "simple" de l’injection, via une déclaration explicite des beans, par exemple :


<bean id="userService" class="tudu.service.impl.UserServiceImpl"/>
<bean id="tenantService" class="tudu.service.impl.TenantServiceImpl"/>
...

De la même façon, les dépendances doivent de préférence être entièrement configurées dans le fichier de contexte plutôt qu’automatiquement détectées par l’annotation @Autowired, par exemple:

Pour le lien entre un controller et un DAO du type :


public class MovieController {
  MovieDaoInterface movieDao;
  public void setMovieDao(MovieDaoInterface movieDao) {
    this.movieDao = movieDao;
  }
}

Il faut indiquer la configuration :


<bean class="com.example.controller.MovieController">
  <property name="movieDao" ref="movieDao"></property>
</bean>
<!-- movieDao is defined elsewhere in the container configuration -->
<bean id="movieDao" class="com.example.dao.MovieDao" />

De la même façon, l’injection de l’entityManager via l’annotation @PersistenceContext doit être remplacé par l’appel à une classe statique.

source : https://developers.google.com/appengine/docs/java/datastore/jpa/overview?hl=fr#Getting_an_EntityManager_Instance

SGBD

L’infrastructure de GAE utilise une base de donnée NoSQL Big Table développée en interne par Google, ce qui peut impliquer la refonte de toute la couche de persistence et d’accès à la BDD.

source : http://blog.xebia.fr/2009/07/16/la-persistance-dans-google-app-engine-partie-une-le-datastore/
source : https://developers.google.com/appengine/docs/whatisgoogleappengine?hl=fr

ORM et persistence

Le SGBD utilisé par GAE impose le remplacement de Hibernate/JPA (relationnel) par Datanucleus/Objectify.

source : http://blog.publysher.nl/2012/02/improving-appengine-performance-from.html
source : http://turbomanage.wordpress.com/2010/01/28/simplify-with-objectify/

Transactions

L’application initiale se base sur l’annotation @Transactional de Spring AOP pour l’injection des dépendances.

Ce mécanisme ne semble pas compatible avec GAE, qui compte quelques bugs à ce sujet.

source : https://groups.google.com/forum/?fromgroups#!topic/google-appengine-java/68qhETz-UFU
source : http://objectuser.wordpress.com/2009/06/30/spring-jdo-in-google-app-engine/

Il faut donc complètement revoir cet aspect et préférer la manipulation explicite des transactions, via Objectify ou plus directement comme par exemple :


DatastoreService datastore = DatastoreServiceFactory.getDatastoreService()
try {
    Transaction txn = datastore.beginTransaction();
    // do stuff
    txn.commit();
}
finally {
    if (txn.isActive()) {
        txn.rollback();
    }
}

source : http://stackoverflow.com/questions/5386144/two-different-methods-for-google-app-engine-datastore-transactions-which-to-u
source : https://developers.google.com/appengine/docs/java/datastore/transactions

Logs

Puisque GAE utilise un JRE modifié ne contenant plus les librairies java.io.*, il n’est plus possible de se reposer sur Log4j ou Logback. Il n’y a plus d’appender à définir pour conserver la trace des logs, ils seront seulement disponibles dans la console d’administration de l’application.

Dans l'article suivant, je détaillerai la configuration nécessaire du POM projet pour réaliser une application GAE.


Fichier(s) joint(s) :