HOAB

History of a bug

Un projet Web avec Hibernate tout simple (avec Maven, déjà ce n'est plus simple...)

Rédigé par gorki Aucun commentaire

Le problème :

Pour faire des tests, il peut être utile d'avoir un projet Hibernate minimaliste, sans fioriture pour tester simplement une fonctionnalité particulière du framework.

Et aussi si ça pouvait être une application WEB, ça serait cool.

Maven c'est pour éviter d'avoir à télécharger les librairies quand on a déjà Eclipse/Maven sur son poste.

On a bien quelques liens sur google, mais je ne trouvais pas ça super simple pour quelqu'un qui connaissait un peu hibernate : ici, google.

Solution :

Librement inspirée de ce site.

Prérequis :

  • savoir ce qu'est Hibernate, Maven, Eclipse, Java, JPA. Je n'explique rien ici, mais le web est là pour ça.
  • avoir un Eclipse récent qui supporte Maven en natif (à partir de la version Kepler je crois)
  • savoir créer un projet sous Eclipse et savoir où se placent les fichiers dans une arbo Maven (Sinon télécharger l'archive en fin de page, je sais GitHub existe...)
  • savoir piocher les bouts qui vont bien pour votre propre besoin, c'est un peu le mode bloc note ici.

.

  1. Créer le pom qui va bien :
    • Servlet pour la partie annotation Web
    • HSQLDB pour la base
    • SLF4J et Logback pour les logs
    • Hibernate pour Hibernate
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

	<modelVersion>4.0.0</modelVersion>
	<groupId>com.mygroup</groupId>
	<artifactId>testApp</artifactId>
	<packaging>war</packaging>
	<version>1.0-SNAPSHOT</version>

	<name>Webapp</name>

	<dependencies>
		<!-- Servlet -->

		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>3.1.0</version>
			<scope>compile</scope>
		</dependency>

		<!-- BDD -->

		<dependency>
			<groupId>org.hsqldb</groupId>
			<artifactId>hsqldb</artifactId>
			<version>2.3.2</version>
		</dependency>

		<!-- Logger -->

		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.7.6</version>
		</dependency>

		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>1.1.1</version>
		</dependency>

		<!-- ORM -->

		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-core</artifactId>
			<version>4.3.4.Final</version>
		</dependency>
		


	</dependencies>

	<build>

		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.7</source>
					<target>1.7</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>
  1. Un fichier web.xml tout petit :
    • Attention ! à la version des servlets : 3.0 - pour pouvoir les déployer avec des annotations @WebServlet
    • Un servlet de startup pour initialiser la base mémoire
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
          http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
	version="3.0">


	<display-name>Test Application</display-name>


	<servlet>
		<servlet-name>StartupGenerique</servlet-name>
		<servlet-class>com.mygroup.servlet.StartupServlet</servlet-class>
		<load-on-startup>0</load-on-startup>
	</servlet>

</web-app>
  1. Une servlet qui répond à toutes les requêtes (on fait dans le super simple)
    • La servlet répond au motif HTTP : "/". Donc toutes les requêtes....
    • Algorithme de haut-niveau :
      • Créer un objet DOG
      • Le sauvegarde
      • Recherche de l'objet DOG sauvé
      • Retourne une réponse
package com.mygroup.servlet;

// Import required java libraries
import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mygroup.dao.DogDAO;
import com.mygroup.model.Dog;

/**
 * The Class TestServlet1.
 */
@WebServlet("/")
public class TestServlet extends HttpServlet {

	/** The Constant serialVersionUID. */
	private static final long serialVersionUID = 1L;

	/** The Constant LOGGER. */
	private static final Logger LOGGER = LoggerFactory.getLogger(TestServlet.class);

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
	 */
	@Override
	public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		DogDAO dogDAO = new DogDAO();

		dogDAO.startConnection();

		String responseString = null;

		try {
			/*
			 * Save Dog
			 */
			Dog dog = new Dog();
			dog.setName("Beethoven " + System.currentTimeMillis());
			dog.setWeight(45);
			dogDAO.save(dog);

			/*
			 * Find dog
			 */
			dog = dogDAO.find(dog.getId());
			LOGGER.debug("Dog saved : {}", dog.getName());

			/*
			 * Response
			 */
			responseString = "OK, Dog saved : " + dog.getName();
		} catch (Exception e) {
			LOGGER.error("Erreur générale", e);
			responseString = "Erreur, voir log";
		} finally {
			dogDAO.commitConnection();
			dogDAO.closeConnection();
		}

		// Set response content type
		response.setContentType("text/html");
		// Actual logic goes here.
		PrintWriter out = response.getWriter();
		out.println(responseString);
	}
}
  1. Le DAO qui permet de manipuler les objets DOG et d'ouvrir les connexions à la BDD (via DatabaseUtils ci-dessous)
package com.mygroup.dao;

import java.util.List;

import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.criterion.Restrictions;

import com.mygroup.model.Dog;

public class DogDAO {

	private Transaction newTransaction;
	private Session newSession;

	public void startConnection() {
		newSession = DatabaseUtils.getSessionFactory().openSession();
		newTransaction = newSession.beginTransaction();

	}

	public void closeConnection() {
		newSession.close();
	}

	public void commitConnection() {
		newTransaction.commit();
	}

	public void save(Dog dog) {
		newSession.persist(dog);
	}

	public void edit(Dog dog) {
		newSession.merge(dog);
	}

	public Dog find(int dogId) {
		return (Dog) newSession.createCriteria(Dog.class).add(Restrictions.idEq(dogId)).uniqueResult();
	}

	public void remove(Dog dog) {
		newSession.delete(dog);
	}

	public List listALL() {
		return newSession.createQuery("from " + Dog.class).list();
	}
}
  1. Le modèle DOG qui est persisté en base de données (et quand est-ce qu'on pourra se passer de ces getters/setters....)
package com.mygroup.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "dog")
public class Dog {

	public static final String LIST_ALL = "listALL";

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private int id;
	private String name;
	private double weight;

	// Getters and Setters
	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public double getWeight() {
		return weight;
	}

	public void setWeight(double weight) {
		this.weight = weight;
	}

}
  1. La création de connexion via Hibernate
package com.mygroup.dao;

import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;

import com.mygroup.model.Dog;

public class DatabaseUtils {

	private static SessionFactory sessionFactory;

	static {
		Configuration cfg = new Configuration().addAnnotatedClass(Dog.class);
		ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings(cfg.getProperties()).build();
		sessionFactory = cfg.buildSessionFactory(serviceRegistry);
	}

	public static SessionFactory getSessionFactory() {
		return sessionFactory;
	}

}
  1. Le fichier hibernate.properties :
hibernate.connection.driver_class = org.hsqldb.jdbcDriver
hibernate.connection.url = jdbc:hsqldb:mem:.
hibernate.connection.username = sa
hibernate.connection.password = 
hibernate.hbm2ddl.auto = update
hibernate.c3p0.min_size=1
hibernate.c3p0.max_size=2
hibernate.c3p0.timeout=5
hibernate.c3p0.max_statements=50
hibernate.dialect = org.hibernate.dialect.HSQLDialect
  1. Le fichier logback pour les logs :
<configuration>

	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<!-- encoders are assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder 
			by default -->
		<encoder>
			<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
			</pattern>
		</encoder>
	</appender>


	<logger name="com.mygroup" level="DEBUG" />


	<root level="error">
		<appender-ref ref="STDOUT" />
	</root>
</configuration>

Et voilà.

Alors attention, ça ne part pas en production comme ça, on est d'accord ! :)

Le zip avec le tout ici : SimpleWebappHibernate.7z

 

 

Excel - Fonction personnalisée (UDF) = #NOM?

Rédigé par gorki Aucun commentaire

Le problème :

Suite à la définition d'une nouvelle fonction en VBA, j'essaye de l'utiliser dans mon classeur et j'ai un "=#NOM?"

Plusieurs liens internet en parle mais je n'ai y pas trouvé ma réponse, les voici cependant :

Activer analysis toolpack : failed

Niveau de sécurité, emplacement "sûr" ou "trusted directory" : failed

Créer des fonctions personnalisées, la page de microsoft : ~failed

Solution :

... et en fait le dernier lien, ça marche si on fait exactement ce qu'ils disent (quand on connait la solution):

"Press Alt+F11 to open the Visual Basic Editor (VBE), and then choose Insert, Module. A new module appears, as shown in Figure 2."

Voilà, je mettais ma fonction dans la partie "Microsoft Excel Objects -> ThisWorkbook" alors qu'elle semble devoir être dans "Modules->ModuleX". Allez comprendre pourquoi... il doit y avoir une subtilité que je n'ai pas repéré, mais du coup ma fonction.... fonctionne :)

success !!

P.S : pour résoudre mon problème, j'ai enregistré une macro classique (via le lien dans "Affichage") et je l'ai édité.

Transaction is not active

Rédigé par gorki Aucun commentaire

Le problème :

Lors de l'exécution d'un service, l'erreur suivante apparait :

javax.resource.ResourceException: IJ000459: Transaction is not active

Plusieurs liens internet en parle, c'est systématiquement lié à JBoss puisque c'est lui qui gère ce niveau transactionnel en mode container et que le code IJ... c'est du JBoss.

Solution :

En court : Bien tracer toutes les erreurs, une erreur précédent celle-ci est survenue et a mis la transaction en rollback-only (on pourrait rapprocher ce problème de ceci).

En long :

Contexte d'exécution : JBoss 7.1.1, Hibernate 4.1.6

  • Si une exception est remontée par hibernate, elle met par défaut la transaction au statut ABORT
  • L’erreur de haut niveau lorsqu’on essaye d’utiliser une transaction avec le statut ABORT (en lecture ou écriture) est : « Could not open connection »

Ce genre de cas arrive lorsque la gestion d’erreur est mal codée (le service de haut niveau ne se termine par immédiatement et on essaye de mettre à jour quelque chose en base…)

Scénario d'exemple :

  1. 1ere erreur hibernate :
    • EJB Invocation failed on component
    • javax.persistence.OptimisticLockException
    • Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect):
  2. 2eme erreur :
    • EJB Invocation failed on component
    • javax.persistence.PersistenceException: org.hibernate.exception.GenericJDBCException: Could not open connection
    • org.hibernate.exception.GenericJDBCException: Could not open connection
    • java.sql.SQLException: javax.resource.ResourceException: IJ000460: Error checking for a transaction
    • javax.resource.ResourceException: IJ000460: Error checking for a transaction
    • javax.resource.ResourceException: IJ000459: Transaction is not active: tx=TransactionImple  <.... ActionStatus.ABORT_ONLY >

Lorsque l’exception numéro 1 est arrivée, votre transaction devient inutilisable, pas la peine d'essayer de faire autre chose avec.

Exemple de code incorrect :

Pour tous les objets

     Try {

           Mise à jour objet

     } catch() {

           Logger l’erreur. // Pour rappel, on loggue bien sur les informations fonctionnelles ET techniques (pile d’exception)

     }

Fpour

Mise à jour d’un statut OK ou KO <= impossible si la ligne « mise à jour objet » à mis la transaction en rollback.

Solution pour cet exemple :

  1. sortir de la boucle en remontant l'erreur, ne pas mettre à jour de statut (rien n’est mis à jour)
  2. sortir de la boucle en remontant l'erreur, les statuts sont stockés au fur et à mesure et mis à jour dans nouvelle transaction
  3. tracer l’erreur, continuer la boucle, les statuts sont mis à jour en même temps que l'objet
  4. et bien d'autres cas possibles suivant votre fonctionnel...

Fuite mémoire sur QueryCachePlan

Rédigé par gorki Aucun commentaire

Le problème :

L'objet QueryPlanCache d'Hibernate fait 400Mo. (Récupéré via un dump mémoire de la JVM, analysé via MAT). Normal ou pas ?

Oui et non :)

C'est normal sans aucun doute, mais alors vous avez un problème.

Le QueryPlanCache conserve l'analyse par Hibernate des requêtes HQL qui sont parsées, explosées et stockées dans un objet pour faciliter la vie à Hibernate la prochaine fois qu'il rencontre votre requête.

Si le cache utilise beaucoup de mémoire, c'est qu'il y a beaucoup de requête différentes et des grosses requêtes. (Par grosse requête j'entends : beaucoup de tables, beaucoup de conditions, etc...)

1ere raison évidente :

Vous n'utilisez pas de paramètres bindés dans vos requêtes, exemple : 

"SELECT * FROM mytable where id = " + myId;

Ce n'est pas bien, il faut, des explications ici par exemple.

2eme raison moins triviale :

Vous utilisez des grosses collections avec des clauses IN.

En HQL vous écrivez

SELECT * FROM mytable WHERE id in (:collection);

C'est bien, paramètre bindé, pas de problème. Sauf qu'en SQL les collections ça n'existe pas, donc Hibernate traduit votre requête en :

SELECT * FROM mytable WHERE id in (:item0, :item1, :item2, ...);

C'est bien, toujours des paramètres bindés, sauf que si la collection à 3 éléments ou 4 éléments, ça fait 2 requêtes

SELECT * FROM mytable WHERE id in (:item0, :item1, :item2);
SELECT * FROM mytable WHERE id in (:item0, :item1, :item2, item3);

Et donc deux entrées dans le cache.

Avec 1000 éléments (taille max de la clause IN chez Oracle), ça fait 1,5Mo par entrée du cache...

Solution :

1) Réduire la taille du cache : la taille du cache est de 2048, et peut être configuré via :

hibernate.query.plan_cache_max_size

2) Modifier la classe QueryPlanCache pour ne pas stocker les requêtes qui ont plus de X paramètres.

La 2eme solution est plus sûre au niveau mémoire et performance, mais nécessite 5 lignes de codes dans Hibernate, l'autre c'est un paramètre....

3) Si ce n'est pas trop tard, utiliser des tables temporaires et faire un join sur ces tables.

Retour d'expérience SAHI

Rédigé par gorki Aucun commentaire

Le problème :

Sahi, ça marche ou ça marche pas ?

Solution :

Bah euh.... difficile à dire.

Sahi c'est un peu comme Selenium, avec évidemment des différences listées ici.

L'utilisation : faire des tests de non-régression sur une IHM GWT.

La cible : Chrome sous Windows

 

Les plus (+) :

- le studio d'enregistrement marche bien

- il est possible d'ajouter des fonctions custom dans les scénarios.

- il est facile de l'intégrer avec Jenkins (en générant des rapports spéciaux)

 

Les moins (-) :

** sur les architectures testées, ça marche plus ou moins bien :

Jenkins + Chrome sous Linux en local :

(sans interface graphique, i.e. sans server X, i.e. headless, i.e avec XVFB) : ça marche mal : problème de stabilité / reproductibilité. Penser à exporter le server X sans XVFB pour débugger les problèmes Chrome Linux.

Jenkins+ Chrome sous Windows à distance :

pas parfait, mais mieux. Les problèmes ne viennent pas forcément de Sahi, mais des VM

Dans les deux cas : Chrome relancé entre chaque test (résultats pas constants) vs. Chrome utilisé en single session (mieux)

 

** la configuration de base marche mal :

  1. Si Chrome est arrêté entre chaque test, il est killé (cf os.properties) => le profile Chrome est alors corrompu
    • il vaut mieux remplacer dans le browser.xml le chemin vers Chrome par un shell à vous pour dézipper un profile clean avant de relancer.
    • du coup préparer votre profile et dézipper le avant chaque lancement de navigateur
  2. Pour bien être intégrer à Jenkins, mettre sous le gestionnaire de sources (dans des répertoires différents, ils n'évoluent pas au même rythme, pas la peine de vérifier les binaires sahi à chaque déploiement) :
    • userdata,
    • binaires sahi,
    • scripts
  3. Utiliser dans ces scripts les variables pour bien maitriser l'environnement :
    • $WORKSPACE de Jenkins
    • $SAHI_HOME
    • ...
  4. Les scripts de base dans userdata ne sont pas très robustes :
    • Pas de kill de Chrome / Arrêt de Sahi si on kill le build Jenkins
    • Il faut bien nettoyer les logs avant de démarrer SAHI
    • Pas d'attente de démarrage de SAHI
    • La sélection des process à killer est plus ou moins hasardeuse à mon avis, on est resté en singleThread

Et ce n'est qu'une vue rapide.

Conclusion :

Bref, des bonnes idées, mais ce n'est pas du "sur étagère", il y a du boulot pour qu'il soit intégré et robuste, sans parler de la rejouabilité des tests que je ne trouve pas exceptionnelle.

Mais bon quelle idée de contrôler un soft qui n'est pas fait pour être contrôlé (les navigateurs ne sont pas pensé pour ça !!)...

Entre Sahi et Selenium ? Celui que vous connaissez le mieux.

 

Quelques scripts :

Exemple de script de démarrage et attente du lancement de SAHI :

#!/bin/bash

./check.sh
if [ $? -eq 1 ] ; then
        echo "Environnement incorrect"
        exit 1
fi


echo --------
echo SAHI_HOME: $SAHI_HOME
echo SAHI_USERDATA_DIR: $SAHI_USERDATA_DIR_TMP
echo SAHI_EXT_CLASS_PATH: $SAHI_EXT_CLASS_PATH
echo --------

#rm -rf $SAHI_USERDATA_DIR/temp/*
#rm -rf $SAHI_USERDATA_DIR/logs/*
#rm -rf $SAHI_USERDATA_DIR/database/*

OUTPUT_LOG=$SAHI_USERDATA_DIR/logs/sahi_output.log

export POI_JARS=$SAHI_HOME/extlib/poi/excelpoi.jar:$SAHI_HOME/extlib/poi/poi-3.7-20101029.jar:$SAHI_HOME/extlib/poi/dom4j-1.6.1.jar:$SAHI_HOME/extlib/poi/poi-ooxml-3.7-20101029.jar:$SAHI_HOME/extlib/poi/poi-ooxml-schemas-3.7-20101029.jar:$SAHI_HOME/extlib/poi/xmlbeans-2.3.0.jar
SAHI_CLASS_PATH=$SAHI_HOME/lib/sahi.jar:$SAHI_HOME/extlib/rhino/js.jar:$SAHI_HOME/extlib/apc/commons-codec-1.3.jar:$SAHI_HOME/extlib/db/h2.jar:$SAHI_HOME/extlib/license/truelicense.jar:$SAHI_HOME/extlib/license/truexml.jar:$POI_JARS

cd $SAHI_HOME
java -Xmx512m -Djava.io.tmpdir=$SAHI_USERDATA_DIR/temp -classpath $SAHI_EXT_CLASS_PATH:$SAHI_CLASS_PATH net.sf.sahi.Proxy "$SAHI_HOME" "$SAHI_USERDATA_DIR" &> $OUTPUT_LOG &
echo $! > $SAHI_USERDATA_DIR/temp/sahi.pid

echo "Attente du demarrage de sahi"

i=0
STATUS_CODE=1
TIMEOUT=20
while [ $STATUS_CODE -eq 1 ] && [ $i -lt $TIMEOUT ]; do
        cat $OUTPUT_LOG | grep "Finished preparing report" > /dev/null
        STATUS_CODE=$?
        echo -n "."
        sleep 1
        ((i++))
done
echo "Serveur sahi OK"

Exemple de script de lancement de tests et kill de Chrome en fin de script (le userdata.properties est créé à la volé pour les paramètres tel que le port du proxy) :

#!/bin/bash

./check.sh
if [ $? -eq 1 ] ; then
        echo "Environnement incorrect"
        exit 1
fi
URL=$1
BUILD_ID=$2
DEBUG=$3

if [ ! $URL ] || [ ! $BUILD_ID ]
then
        echo "usage : start_and_run.sh <URL> <BUILD ID>"
        exit 1
fi

# Generation du fichier de configuration user
echo "############################################" > $SAHI_USERDATA_DIR/config/userdata.properties
echo "# Fichier généré automatiquement, ne pas modifier (cf start_and_run.sh" >> $SAHI_USERDATA_DIR/config/userdata.properties
echo "############################################" >> $SAHI_USERDATA_DIR/config/userdata.properties
cat $SAHI_USERDATA_DIR/config/userdata.properties.template  >> $SAHI_USERDATA_DIR/config/userdata.properties
printf "\n" >> $SAHI_USERDATA_DIR/config/userdata.properties
echo "proxy.port=$SAHI_PROXY_PORT" >> $SAHI_USERDATA_DIR/config/userdata.properties

# There is only one bypass list for both secure and insecure.
$SAHI_USERDATA_DIR/bin/start_sahi.sh

trap 'cat $SAHI_USERDATA_DIR/temp/sahi.pid | xargs kill' SIGINT SIGTERM EXIT

echo "Attente lancement de SAHI"
sleep 2

if [ "$DEBUG" = "DEBUG" ] ; then
        $SAHI_USERDATA_DIR/bin/testrunner.sh tests.suite $URL chromedebug $BUILD_ID
else
        $SAHI_USERDATA_DIR/bin/testrunner.sh tests.suite $URL chrome $BUILD_ID
fi

 

Fil RSS des articles