Gérer la concurrence d’accès

Question récurrente dans les projets séparant front et back office: comment gérer la concurrence d’accès ?

Ce document a vocation à intégrer la concurrence d’accès dans des projets déjà existants. Un soin particulier sera apporté à être le moins intrusif possible et donc minimiser les impacts.

 

Mais d’abord, qu’est-ce que la concurrence d’accès ?

La concurrence d’accès vise à garantir que lorsqu’on appelle un service fonctionnel visant à modifier un objet métier, cet objet n’ait pas été modifié durant le laps de temps qu’a duré le cas d’utilisation ayant entraîné cette modification.

Un petit exemple pour illustrer le propos.

J’ai un cas d’utilisation «Modifier l’adresse d’un utilisateur». Ce cas d’utilisation va d’abord récupérer l’adresse de l’utilisateur, puis la modifier et enfin la valider.

Le but de la concurrence d’accès est de garantir qu’entre la récupération de l’adresse et sa validation, personne ne l’ait modifié (avec le même cas d’utilisation fait par un autre utilisateur ou avec un autre cas d’utilisation).

Si l’adresse a été modifiée, le cas d’utilisation devra tomber en erreur avec un traitement spécifique côté IHM («L’adresse a été modifiée par un autre utilisateur, veuillez reprendre l’opération depuis le début» par exemple).

 

Comment s’en sortir ?

On s’aperçoit via notre exemple que nous avons deux notions à gérer:

  • lorsqu’on me demande de modifier une adresse, cette adresse est-elle à jour ?
  • pendant que je modifie une adresse, cette adresse est-elle modifiée par quelqu’un d’autre ?

Nous nommerons ‘concurrence d’accès avant’ la première problématique et ‘concurrence d’accès pendant’ la seconde.

 

La concurrence d’accès avant a deux solutions possibles, illustrées par ces diagrammes:

Lock optimiste déconnecté: la vérification est faite lors de la modification de l’objet. S’il a déjà été modifié, le service de mise à jour plante.

Lock pessimiste déconnecté: lors de la lecture de l’objet, si quelqu’un travaille déjà dessus, le service de lecture plante.

 

Le lock pessimiste déconnecté a une mauvaise expérience utilisateur: ce dernier doit attendre que l’autre utilisateur ait déroulé son cas d’utilisation pour pouvoir continuer son processus. Sachant qu’un cas d’utilisation concerne plusieurs écrans et que les pauses cafés ne sont pas à occulter, cette solution peut bloquer pendant trèèèèèès longtemps l’utilisateur.

Pour cette raison, le lock optimiste déconnecté est souvent privilégié. On partira donc sur cette hypothèse dans cet article.

Si la concurrence d’accès avant concerne tout un cas d’utilisation, la concurrence d’accès pendant ne concerne qu’un service fonctionnel. La solution choisie sera le verrouillage (lock). Afin d’être le moins intrusif, cet aspect sera traité en AOP.

 

Enfin, une dernière problématique à traiter: la cible du verrouillage.

En effet, doit-on verrouiller l’objet concerné par la modification, ou son parent ? Ou encore plus haut, par exemple l’objet le plus haut dans la grappe d’objet (si ça a un sens) ? On peut même imaginer que chaque service fonctionnel verrouille à un niveau différent, voir encore plus fin.

 

Principe général

Le principe sera d’utiliser l’AOP. On créera donc une annotation @ConcurrenceAcces qu’on placera sur les services susceptible de générer de la concurrence d’accès (principalement les services de modification).

Cette annotation aura deux buts:

  • avant le process, on vérifie que la version est bien celle en base et on lock l’objet
  • après le process, on récupère la nouvelle version et on relâche le lock

Cette annotation devra se trouver dans une transaction afin de bénéficier des mécanismes de rollback.

Il y a deux informations nécessaires au bon fonctionnement de la concurrence d’accès: l’aspect doit avoir connaissance de la version connue de l’appelant et il doit être capable de récupérer l’objet en base (pour pouvoir récupérer sa version puis le locker). Afin d’être le plus souple possible, on découpera la deuxième information en deux: un type et un identifiant. Le type permettant de dire sur quelle genre d’objet porte la concurrence d’accès et l’identifiant pour savoir précisément quel objet.

Un léger aparté: certains services font à la fois de la création et de la modification (pas en même temps évidemment). Lors de la création, la concurrence d’accès n’a aucun sens et ne doit pas être déroulée tandis que lors de la modification, il le faut. Nous sommes en présence d’un service qui, selon le contexte, aura besoin de concurrence d’accès ou non. Pour pallier à cette problématique, on pourra rajouter une information supplémentaire: un flag permettant d’ignorer la concurrence d’accès.

NB: on parle de la création de l’objet que l’on souhaite verrouiller. Certains services de créations ne sont que des modifications vis-à-vis de la concurrence d’accès. Par exemple, si j’ai un utilisateur qui a une adresse, le service creerUtilisateur sera bien un service de création alors que le service creerAdresse sera un service de modification (le référentiel est l’utilisateur).

 

On a notre aspect, on a nos informations, maintenant, comment notre aspect accède-t-il à nos informations ?

Une solution simple est de passer par un ThreadLocal. Dans le cas d’appels front/back intra jvm, le front n’aura qu’à poser ces informations dans un ThreadLocal pour que notre aspect y ait accès.

Dans le cas de Web Services, des intercepeurs placés dans le bus (c’est-à-dire utilisé par tous les Web Service) se chargeront en amont de récupérer les informations du ThreadLocal pour les mettres dans les headers HTTP et en aval de récupérer les headers HTTP, en extraire les informations et les mettre dans un ThreadLocal.

 

 Implémentation

Commençons par la classe chargée de centraliser toutes les informations nécessaires à la concurrence d’accès:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConcurrenceAccesInformations {

  /** Identifiant technique de l’objet. */
  private Long id;

  /** Version de l’objet. */
  private long version;

  /** Type de l’objet. */
  private ConcurrenceAccesTypeEnum type;

  /** Flag pour ignorer la concurrence d’accès (pour les services de création/modification). */
  private boolean ignoreConcurrenceAcces = false;

Idéalement, cette classe est à placer dans un projet ‘common’ dont dépendent la plupart de vos modules.

Pour la partie mise en session / mise en threadlocal, il n’y a rien de particulier à dire.

Ensuite viennent les intercepteurs (ici, CXF est utilisé mais c’est facilement adaptable à votre API de Web Service):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class ConcurrenceAccesOutInterceptor extends AbstractPhaseInterceptor<Message> {
    /**
     * Constructeur de la classe
     */

    public ConcurrenceAccesOutInterceptor() {
        super(Phase.PREPARE_SEND);
    }

    /**
     * Interceptor CXF qui récupère les informations nécessaires à la concurrennce d’accès présentes dans un
     * ThreadLocal et les place dans les headers. Le header doit être de la forme
     * “id=aaa,version=bbb,type=ccc,ignore=ddd”.
     * @param message
     *          message.
     */

    @Override
    public void handleMessage(
            final Message message) {
        Map<String, List<String>> headers = CastUtils.cast((Map<?, ?>) message.get(Message.PROTOCOL_HEADERS));
        ConcurrenceAccesInformations infos = MonThreadLocalHolder.getConcurrenceAccesInformations();

        if (headers == null || infos == null) {
            /*
            * Si on n’arrive pas à récupérer les informations de la concurrence d’accès alors cet intercepteur n’a
            * plus lieu d’être.
            */

            return;
        }

        List<String> concurrenceHeaders = new ArrayList<String>();
        String id = "";
        if (infos.getId() != null) {
            id = infos.getId().toString();
        }

        String type = "";
        if (infos.getType() != null) {
            type = infos.getType().name();
        }

        StringBuilder identifiants = new StringBuilder();
        identifiants.append(IConstantesConcurrenceAcces.HEADER_ID + IConstantes.EGALE).append(id);
        concurrenceHeaders.add(identifiants.toString());
        identifiants = new StringBuilder();
        identifiants.append(IConstantesConcurrenceAcces.HEADER_VERSION + IConstantes.EGALE).append(
                Long.toString(infos.getVersion()));
        concurrenceHeaders.add(identifiants.toString());
        identifiants = new StringBuilder();
        identifiants.append(IConstantesConcurrenceAcces.HEADER_TYPE + IConstantes.EGALE).append(type);
        concurrenceHeaders.add(identifiants.toString());
        identifiants = new StringBuilder();
        identifiants.append(IConstantesConcurrenceAcces.HEADER_IGNORE + IConstantes.EGALE).append(
                Boolean.toString(infos.isIgnoreConcurrenceAcces()));
        concurrenceHeaders.add(identifiants.toString());

        headers.put(IConstantesConcurrenceAcces.HEADER_CONCURRENCE_ACCES, concurrenceHeaders);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class ConcurrenceAccesInInterceptor extends AbstractPhaseInterceptor<Message> {
    /**
     * Constructeur de la classe
     */

    public ConcurrenceAccesInInterceptor() {
        super(Phase.RECEIVE);
    }
   
    /**
     * Interceptor CXF qui va récupérer les headers et en extraire les informations sur la concurrence d'accès.
     * Le header contient une chaine de la forme "id=aaa,version=bbb,type=ccc,ignore=ddd".
     * @param message
     *          message.
     */

    @Override
    public void handleMessage(final Message message) {
        Map<String, List<String>> headers = CastUtils.cast((Map<?, ?>) message.get(Message.PROTOCOL_HEADERS));
       
        if (headers == null) {
            // Si les headers ne sont pas disponibles (ne devrait pas arriver), on ne peut rien faire.
            return;
        }
       
        List<String> jetonConcurrenceAcces = headers.get(IConstantesConcurrenceAcces.HEADER_CONCURRENCE_ACCES);
       
        if (jetonConcurrenceAcces != null) {
            ConcurrenceAccesInformations concurrenceAccesInformations = new ConcurrenceAccesInformations();
           
            for (String concurrenceAcces : jetonConcurrenceAcces) {
                String[] morceaux = concurrenceAcces.split(IConstantes.VIRGULE);
                for (String morceau : morceaux) {
                    String[] attribute = morceau.split(IConstantes.EGALE);
                    if (attribute != null && attribute.length == IConstantesConcurrenceAcces.HEADER_NOMBRE_PARAM
                            && !StringUtils.isEmpty(attribute[1])) {
                        String name = attribute[0];
                        String value = attribute[1];
                       
                        if (StringUtils.equals(name, IConstantesConcurrenceAcces.HEADER_ID)) {
                            try {
                                concurrenceAccesInformations.setId(Long.parseLong(value));
                            } catch (NumberFormatException e) {
                                // On n'arrive pas à parser l'identifiant, il ne sera pas stocké.
                                logger.warn("L'identifiant de concurrence d'accès du header n'est pas un Long: " + value);
                            }
                        } else if (StringUtils.equals(name, IConstantesConcurrenceAcces.HEADER_VERSION)) {
                            try {
                                concurrenceAccesInformations.setVersion(Long.parseLong(value));
                            } catch (NumberFormatException e) {
                                // On n'arrive pas à parser l'identifiant, il ne sera pas stocké.
                                logger.warn("La version de concurrence d'accès du header n'est pas un Long: " + value);
                            }
                        } else if (StringUtils.equals(name, IConstantesConcurrenceAcces.HEADER_TYPE)) {
                            concurrenceAccesInformations.setType(ConcurrenceAccesTypeEnum.valueOf(value));
                        } else if (StringUtils.equals(name, IConstantesConcurrenceAcces.HEADER_IGNORE)) {
                            concurrenceAccesInformations.setIgnoreConcurrenceAcces(Boolean.parseBoolean(value));
                        }
                    }
                }
            }
           
            MonThreadLocalHolder.setConcurrenceAccesInformations(concurrenceAccesInformations);
        } else {
            MonThreadLocalHolder.setConcurrenceAccesInformations(null);
        }
    }
}

Vos services devant faire l’objet de la concurrence d’accès auront alors l’annotation adéquate:

1
2
3
4
5
6
7
8
public class MonBeauServiceImpl implements IMonBeauService {

    @Override
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    @ConcurrenceAcces
    public void modifierXXX(XXX xxx) {

    …

Enfin l’intercepteur AOP qui va se charger de l’implémentation de la concurrence d’accès:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@Aspect
public class ConcurrenceAccesInterceptor implements Ordered {
    /** Pointcut AOP. */
    public static final String POINTCUT_MAS = "execution(@package.vers.annotation.ConcurrenceAcces * *(..)) "
            + "&& @annotation(package.vers.annotation.ConcurrenceAcces)";
    /**
     * Ordre par defaut de l’aop. Plus la valeur est grande et plus la priorite est basse.
     */

    public static final int DEFAULT_ORDER = 99;
   
    /** Ordre de l’aop. */
    private int order = DEFAULT_ORDER;
   
    /** Concurrence accès service. */
    private IConcurrenceAccesService concurrenceAccesService;
   
    /** Transforme des types en entités. */
    private ITypeToEntite typeToEntite;
   
    /**
     * Constructeur de la classe
     */

    public ConcurrenceAccesInterceptor() {
    }
   
    /** Post Constructeur. */
    @PostConstruct
    public final void after() {
        logger.info("Interceptor AOP pour les fonctions suivante: {}" + POINTCUT_MAS);
    }
   
    /**
     * Englobe une méthode de service d’une gestion de concurrence d’accès.
     * @param pjp
     *          ProceedingJoinPoint
     * @return l’objet retourné par le service aopisé.
     * @throws Throwable
     *           lancée lorsque la méthode n’a pas été executée correctement.
     */

    @Transactional(propagation = Propagation.MANDATORY)
    @Around(POINTCUT_MAS)
    public Object aroundServiceMetier(ProceedingJoinPoint pjp)
            throws Throwable {ConcurrenceAccesInformations infos = MonThreadLocalHolder.getConcurrenceAccesInformations();
        if (infos == null) {
            throw new TechniqueException(
                    IConstantesConcurrenceAccesErreur.INFORMATIONS_MANQUANTES, new String[] { "infos" }, null);
        }
        if (infos.isIgnoreConcurrenceAcces()) {
            // Si le flag ignore est à true, cet aspect ne doit rien faire.
            return pjp.proceed();
        }if (infos.getId() == null) {
            throw new TechniqueException(
                    IConstantesConcurrenceAccesErreur.INFORMATIONS_MANQUANTES, new String[] { "id" }, null);
        }
        if (infos.getType() == null) {
            throw new TechniqueException(
                    IConstantesConcurrenceAccesErreur.INFORMATIONS_MANQUANTES, new String[] { "type" }, null);
        }
        if (gestionnaireTracesService.isDebugEnabled()) {
            logger.debug(
                    "Identifiant: '" + infos.getId() + "' , version: '" + infos.getVersion() + "' , type: '"
            + infos.getType().name() + "', ignore: '" + infos.isIgnoreConcurrenceAcces() + "'.",
            ConcurrenceAccesInterceptor.class);
        }
       
        Class<? extends IConcurrenceAccesEntite> entite = typeToEntite.getEntite(infos.getType());
        if (entite == null) {
            throw new TechniqueException(IConstantesConcurrenceAccesErreur.ENTITE_MANQUANTE,
                    new String[] { infos.getType().name() }, null);
        }
       
        try {
            concurrenceAccesService.verrouiller(entite, infos.getId(), infos.getVersion());
        } catch (StaleObjectStateException e) {
            throw new TechniqueException(
                    IConstantesConcurrenceAccesErreur.CONCURRENCE_ACCES, null, e);
        } catch (OptimisticLockException e) {
            throw new TechniqueException(
                    IConstantesConcurrenceAccesErreur.CONCURRENCE_ACCES, null, e);
        }
        Object retour = pjp.proceed();

        // Une fois le service appelé, on va récupérer la nouvelle version de l’objet auquel on a forcé
        // l’incrément de version.
        long version = concurrenceAccesService.recupererNouvelleVersion(entite, infos.getId());
        infos.setVersion(version);

        return retour;
    }

    // [getters.setters]
}

On s’aperçoit que le type est transformé en entité grâce à une interface ITypeToEntite dont voici la définition:

1
2
3
4
5
6
7
8
9
public interface ITypeToEntite {
    /**
     * Récupère la classe de l’entité correspondant au type.
     * @param type
     *          dont on veut la classe de l’entité.
     * @return la classe de l’entité correspondant au type.
     */

    Class<? extends IConcurrenceAccesEntite> getEntite(ConcurrenceAccesTypeEnum type);
}

Inutile de mettre ici une implémentation vu qu’elle dépend complètement du projet. Le principe est de faire correspondre une entité à un type. Il faudra que cette entité implémente l’interface IConcurrenceAccesEntite:

1
2
3
4
5
6
7
public interface IConcurrenceAccesEntite {
    /**
     * Récupère la version.
     * @return la version.
     */

    long getVersion();
}

Pour la gestion de la version, vous pouvez la gérer manuellement mais l’annotation @Version d’Hibernate est très satisfaisante.

Pour terminer, l’intercepteur faisait appel à un service pour deux méthodes: verrouiller et recupererNouvelleVersion. Selon votre architecture, vous allez avoir plus ou moins de couche jusqu’à atteindre la partie persistence, que voici:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class ConcurrenceAccesPersistanceImpl extends AbstractPersistanceImpl implements IConcurrenceAccesPersistance {
    /**
     * {@link EntityManager}.
     */

    @PersistenceContext
    private EntityManager entityManager;
   
    /**
     * Constructeur de la classe
     */

    public ConcurrenceAccesPersistanceImpl() {
    }
   
    /**
     * {@inheritDoc}
     */

    @Override
    public void verrouiller(Class<? extends IConcurrenceAccesEntite> classeEntite, Long id, long version) {
        IConcurrenceAccesEntite entite = getEntityManager().find(classeEntite, id);
        if (entite.getVersion() != version) {
            throw new TechniqueException(
                    IConstantesConcurrenceAccesErreur.VERSION_INCORRECTE, new String[] { entite.getVersion() + "", version + "" }, null);
        } else {
            getEntityManager().lock(entite, LockModeType.PESSIMISTIC_FORCE_INCREMENT);
        }
    }
   
    /**
     * {@inheritDoc}
     */

    @Override
    public long recupererNouvelleVersion(Class<? extends IConcurrenceAccesEntite> classeEntite, Long id) {
        IConcurrenceAccesEntite entite = getEntityManager().find(classeEntite, id);
        getEntityManager().flush();
       
        return entite.getVersion();
    }
   
    // [getters/setters]
}

Il y a deux locks envisageables:

  •  OPTIMISTIC_FORCE_INCREMENT
  • PESSIMISTIC_FORCE_INCREMENT

Si jamais deux threads lockent le même objet et le modifient, le lock optimiste ne va planter qu’en fin de transaction pour le dernier arrivé tandis que le lock pessimiste va attendre que le premier ait terminé et planter s’il y a eu une modification.

Le lock optimiste ne va incrémenter la version que lors du flush, c’est pourquoi il est possible de vérifier que la version est toujours la même (c’est le test commenté). Mais c’est aussi pourquoi il faut faire un flush lors de recupererNouvelleVersion.

Le lock pessimiste va tout de suite incrémenter la version, c’est pourquoi le test est commenté: il plantera. Vous pouvez vérifier que la version est égale à l’ancienne incrémentée de 1, mais je n’aime pas être lié à l’implémentation d’Hibernate, c’est pourquoi il n’y a pas de test. Ce test est quasiment superflu: en effet, il faudrait que deux threads passent ensemble le premier test d’égalité, puis qu’un test lock, fasse son service, termine la transaction et flush pendant que l’autre ne fasse rien. On parle ici d’une centaine d’instructions faites par un thread pendant que l’autre n’en fait aucune (et une centaine pour un service élémentaire, ça peut monter à plusieurs milliers s’il y a des règles de gestion à vérifier).

Le lock optimiste a un autre désavantage: il a un bug.

Attention ! Si vous n’utilisez pas l’annotation @Version, alors il vous faudra gérer la partie FORCE_INCREMENT du lock vous-même.

 

L’élégance de cette solution consiste dans son impact faible. Lorsqu’un projet vit déjà depuis quelque temps, c’est appréciable de ne pas avoir à mettre du code dans chaque service. Ici, une annotation suffira. Par contre, il faudra alimenter cette annotation. C’est-à-dire que côté front, il vous faudra gérer plusieurs aspects:

  • l’alimentation des informations en début de cas d’utilisation
  • la transmission des informations à chaque appel de service nécessitant la concurrence d’accès

 

Dans votre cas d’utilisation, vous allez à un moment récupérer l’objet qui sera utilisé pour le verrouillage. Il faut alors mettre les informations intéressantes en sessions:

1
2
3
4
5
6
7
8
9
10
11
12
13
Utilisateur utilisateur = service.getUtilisateur(vue.getUtilisateurId());

/* Stocker les infos pour la concurrence d’accès en session. */

ConcurrenceAccesInformations infos = new ConcurrenceAccesInformations();

infos.setId(utilisateur.getId());

infos.setVersion(utilisateur.getVersion());

infos.setType(ConcurrenceAccesTypeEnum.UTILISATEUR);

putInSession(IConstantesConcurrenceAcces.HEADER_CONCURRENCE_ACCES, infos);

Tandis que lors de l’appel à un service soumis à la concurrence d’accès, il suffira de:e les informations intéressantes en sessions:

1
2
3
4
5
6
7
8
9
10
11
12
/* On récupère les infos de la concurrence d’accès en session et on les met dans un ThreadLocal. */
ConcurrenceAccesInformations infos = (ConcurrenceAccesInformations) getFromSession(IConstantesConcurrenceAcces.HEADER_CONCURRENCE_ACCES);
SessionFaeton.setConcurrenceAccesInformations(infos);

service.modifierUtilisateur(utilisateur);

/*
* On récupère les informations mises à jour (la version essentiellement) et on remplace l’objet en
* session.
*/

ConcurrenceAccesInformations infos = SessionFaeton.getConcurrenceAccesInformations();
putInSession(IConstantesConcurrenceAcces.HEADER_CONCURRENCE_ACCES, infos);

Et voilà, vous avez mis en place la concurrence d’accès !

Share

Encoder correctement une URL en Java

Introduction

 

Lors du développement d’une application, on est parfois amené à construire manuellement une URL pour faire appel à un service web donné. Il y a toujours un collègue bien avisé qui vous demande si vous avez “correctement encodé” l’URL en question en insistant sur le “correctement”. Si vous restez perplexe devant cette interrogation alors cet article peut vous donner quelques éléments de réponse. Si vous êtes confiants, lisez-le quand même : la vie est pleine de surprises et ce n’est jamais que 10 minutes de perdues !

 

Lire la suite…

Share

Liferay : Service Builder et Liferay Portlet

Bonjour à tous !

Dans cet article, je vais aborder un ensemble de points expliquant comment créer un portlet pour Liferay, les frameworks disponibles, leurs avantages et inconvénients, questions revenant régulièrement lorsqu’un nouveau portlet est à créer. Pour notre exercice, nous allons nous baser sur la création d’un Wiki avec la possibilité d’ajouter, éditer et supprimer des articles.

Je vais vous parler du Service Builder et du Liferay Portlet MVC, framework le plus couramment utilisé dans la construction de portlets pour Liferay. Ready ? … GO !
Lire la suite…

Share

Concevoir une API RESTful avec Spray.io

Introduction

Spray

Spray est un projet proposant plusieurs librairies scala destinées à facilier la construction d’APIs performantes utilisant HTTP.
Elles fournissent des fonctionnalités asynchrones et non bloquantes en se basant sur les acteurs Akka et sur Java NIO. Le tout restant léger, modulaire et testable.

Dans cet article je vous propose un petit tour d’horizon de l’usage standard de Spray en mettant en place une petite application RESTful gérant des employés.
En particulier nous utiliserons la librairie spray-routing, la couche d’abstraction la plus élevée de Spray, qui permet de construire rapidement une API REST.

Lire la suite…

Share
Categories: Non classé Tags: , , , , ,

Développer ses propres règles PMD pour Sonar

PMD est un analyseur de code source. Il permet de détecter le code inutile, trop complexe… Il est par exemple intégré à Sonar, qui l’utilise conjointement avec FindBugs (plutôt destiné, comme son nom l’indique, à détecter les sources potentielles de bugs) et de Checkstyle (qui vérifie le respect de règles de codage).

PMD est intégré à Sonar avec un ensemble de règles prédéfinies. Il est cependant possible d’écrire soi-même des règles de détection puis de packager ces nouvelles règles sous la forme d’un plugin pour Sonar. C’est ce que nous allons faire dans cet article.

Lire la suite…

Share
Categories: Non classé Tags: , ,