<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>JTips</title>
    <description>Ce site a été créé par des développeurs, spécialistes de java, dans le but de recueillir  les informations techniques rassemblées au cours des projets. 
</description>
    <link>https://www.jtips.info</link>
    
    
      
        <item>
          <title>Clustering Tomcat - Kubernetes</title>
          <description>
Cette page est en complément de Clustering Tomcat.
Elle apporte plus de détails sur la configuration de clusters sur Kubernetes.








Les configurations ont été testée avec Minikube.





Déploiement simple


Dans un premier temps, on va partir d&#8217;un déploiement simple, avec une seule instance de Tomcat, dans sa configuration par défaut.







On commence à configurer un Ingress en frontal, qui renverra les requêtes sur le service tomcat0 qu&#8217;on devra configurer ensuite.



apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress
spec:
  ingressClassName: nginx
  tls:
  - secretName: tls-key
  rules:
  - host: tomcat.jtips.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: tomcat0
            port:
              number: 8080



Pour le service, on lui associe un déploiement qui démarre un conteneur de l&#8217;image officielle de Tomcat.



apiVersion: v1
kind: Service
metadata:
  name: tomcat0
  labels:
    component: tomcat
spec:
  clusterIP: None
  selector:
    app: tomcat
  ports:
  - port: 8080




apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat
spec:
  selector:
    matchLabels:
      app: tomcat
  template:
    metadata:
      labels:
        app: tomcat
    spec:
      containers:
      - name: tomcat
        image: tomcat:11
        ports:
        - name: http
          containerPort: 8080
          protocol: TCP





Configuration personnalisée


Le but est de tester les différentes configurations de cluster pour Tomcat.
Kubernetes n&#8217;est que le support de ces tests.


Pour la suite, je vais utiliser une image construite localement, avec un fichier server.xml spécifique.



FROM tomcat:11

ARG SERVER_XML_FILE=server-standalone.xml

COPY conf/$SERVER_XML_FILE conf/server.xml
COPY msg.war webapps/msg.war

ENV CATALINA_OPTS="-XX:MaxRAMPercentage=75"

EXPOSE 8080 4000



Avec ce fichier Dockerfile, je prépare une série de fichier de configuration pour Tomcat et construire plusieurs images.



minikube image build --tag jtips/tomcat ./tomcat
minikube image build --tag jtips/tomcat:k8s --build-opt="build-arg=SERVER_XML_FILE=server-k8s.xml" ./tomcat
...





Découverte en multicast







Commençons par la configuration par défaut du clustering pour Tomcat.
On ajoute un élément &lt;Cluster&gt; vide dans &lt;Engine&gt;.



&lt;!-- conf/server-simple.xml ---&gt;
&lt;Server port="-1"&gt;
  ...
  &lt;Engine name="Catalina" defaultHost="localhost"&gt;
    &lt;Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"&gt;
    ...
  &lt;/Engine&gt;
  ...
&lt;/Server&gt;




minikube image build --tag jtips/tomcat:simple --build-opt="build-arg=SERVER_XML_FILE=server-simple.xml" ./tomcat



On reconfigure le déploiement pour qu&#8217;il utilise notre image, avec plusieurs replicas.



apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat
spec:
  selector:
    matchLabels:
      app: tomcat
  replicas: 2
  template:
    metadata:
      labels:
        app: tomcat
    spec:
      containers:
      - name: tomcat
        image: jtips/tomcat:simple
        ports:
        - name: http
          containerPort: 8080
          protocol: TCP



Cette configuration fonctionne mal.
On peut utiliser l&#8217;application, mais les sessions ne sont pas synchronisées entre les instances.


Ça s&#8217;explique par le fait que dans cette configuration, les instances se découvrent via des messages multicast, et que Kubernetes ne le supporte pas.




Découverte en DNS







Les instances ont leurs propres adresses IP, mais partage un nom DNS commun, qui est le nom du service.
Tomcat peut exploiter ça en remplaçant l&#8217;élément de &lt;Membership&gt; par défaut par le CloudMembershipService.



&lt;!-- conf/server-dns.xml ---&gt;
&lt;Server port="-1"&gt;
  ...
  &lt;Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"&gt;
    &lt;Channel className="org.apache.catalina.tribes.group.GroupChannel"&gt;
      &lt;Membership className="org.apache.catalina.tribes.membership.cloud.CloudMembershipService"
                  membershipProviderClassName="org.apache.catalina.tribes.membership.cloud.DNSMembershipProvider"/&gt;
    &lt;/Channel&gt;
  &lt;/Cluster&gt;
  ...
&lt;/Server&gt;



Dans l&#8217;extrait ci-dessus, l&#8217;attribut membershipProviderClassName n&#8217;est pas nécessaire car DNSMembershipProvider est la valeur par défaut.



minikube image build --tag jtips/tomcat:dns --build-opt="build-arg=SERVER_XML_FILE=server-dns.xml" ./tomcat



Pour configurer le provider, ça passe par des variables d&#8217;environnement.
DNS_MEMBERSHIP_SERVICE_NAME est utilisée pour rechercher les instances par leur nom de service.
Sans cette variable, il utilise le nom tomcat.



apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat
spec:
  selector:
    matchLabels:
      app: tomcat
  replicas: 2
  template:
    metadata:
      labels:
        app: tomcat
    spec:
      containers:
      - name: tomcat
        image: jtips/tomcat:dns
        imagePullPolicy: Never
        ports:
        - name: http
          containerPort: 8080
          protocol: TCP
        - name: cluster
          containerPort: 4000
          protocol: TCP
        env:
        - name: DNS_MEMBERSHIP_SERVICE_NAME
          value: tomcat0



Avec cette configuration, la synchronisation de sessions fonctionne, et l&#8217;utilisateur retrouve la même session quelle que soit l&#8217;instance sur laquelle le répartiteur l&#8217;oriente.




Découverte par l&#8217;API de Kubernetes







En passant l&#8217;attribut membershipProviderClassName à KubernetesMembershipProvider, Tomcat utilise l&#8217;API de Kubernetes pour découvrir les autres membres.
Ça apporte plus de souplesse que la découverte par DNS.
En les cherchant par un label, les instances de Tomcat peuvent être dans des services différents.
Par contre, elles doivent être dans le même namespace.



&lt;!-- conf/server-dns.xml ---&gt;
&lt;Server port="-1"&gt;
  ...
  &lt;Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"&gt;
    &lt;Channel className="org.apache.catalina.tribes.group.GroupChannel"&gt;
      &lt;Membership className="org.apache.catalina.tribes.membership.cloud.CloudMembershipService"
                  membershipProviderClassName="org.apache.catalina.tribes.membership.cloud.KubernetesMembershipProvider"/&gt;
    &lt;/Channel&gt;
  &lt;/Cluster&gt;
  ...
&lt;/Server&gt;




minikube image build --tag jtips/tomcat:k8s --build-opt="build-arg=SERVER_XML_FILE=server-k8s.xml" ./tomcat



Pour configurer le provider, ça passe à nouveau par des variables d&#8217;environnement.




KUBERNETES_NAMESPACE indique le namespace, sa valeur par défaut est tomcat.


KUBERNETES_LABELS indique le filtre sur les labels.




Par ailleurs, pour que Tomcat puisse accéder à l&#8217;API, il faut que son pod ait un compte de service (serviceAccountName) avec les bons droits.



apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat
spec:
  selector:
    matchLabels:
      app: tomcat
  replicas: 2
  template:
    metadata:
      labels:
        app: tomcat
    spec:
      serviceAccountName: tomcat
      containers:
      - name: tomcat
        image: jtips/tomcat:k8s
        imagePullPolicy: Never
        ports:
        - name: http
          containerPort: 8080
          protocol: TCP
        - name: cluster
          containerPort: 4000
          protocol: TCP
        env:
        - name: KUBERNETES_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: KUBERNETES_LABELS
          value: "app=tomcat"



Le compte de service est défini à part et associé à un rôle qui lui donne des droits en lecture sur l&#8217;API.



apiVersion: v1
kind: ServiceAccount
metadata:
  name: tomcat
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: tomcat-role
  namespace: default
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""]
  resources: ["endpoints"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tomcat-rolebinding
  namespace: default
subjects:
- kind: ServiceAccount
  name: tomcat
  namespace: default
roleRef:
  kind: Role
  name: tomcat-role
  apiGroup: rbac.authorization.k8s.io



</description>
          <pubDate>2026-02-05T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Tomcat/Kubernetes</link>
          <guid isPermaLink="true">https://www.jtips.info/Tomcat/Kubernetes</guid>
        </item>
      
    
      
        <item>
          <title>Configuration externalisée</title>
          <description>
La configuration de Tomcat est centralisée dans le fichier server.xml.
Ce fichier est relativement simple à comprendre et à modifier, ce qui le rend adapté à des configurations classiques.
En revanche, cette approche devient moins pratique dans des environnements modernes ou dynamiques, en particulier dans le cloud, où la configuration doit souvent être externalisée.


Lecture de la configuration


Au démarrage de Tomcat, le fichier server.xml est lu et interprété par le composant interne appelé digester.
Ce dernier est chargé de parser les fichiers XML de configuration et de construire les objets Java correspondants.


Le digester offre une fonctionnalité de substitution de placeholders.
Ceux-ci sont remplacés par des valeurs issues de différentes sources de propriétés, ce qui permet de rendre la configuration plus flexible et adaptable à l&#8217;environnement d&#8217;exécution.


Par défaut, les placeholders sont remplacés par des system properties.




Propriétés système


Un placeholder est défini à l&#8217;aide de la syntaxe ${key} ou ${key:-default-value}.
Si la propriété n&#8217;est pas définie, la valeur par défaut est utilisée.


Dans l&#8217;exemple ci-dessous, on déclare une datasource dans server.xml, avec la possibilité de définir ses attributs principaux dans des propriétés système.



&lt;Resource name="jdbc/my-global-ds"
          type="javax.sql.DataSource" auth="Container"
          driverClassName="org.database.Driver"
          url="${datasource.url:-jdbc:derby://localhost:1527/sewadb}"
          username="${datasource.username:-sa}"
          password="${datasource.password:-sapwd}" /&gt;



Les propriétés peuvent être définies comme propriétés système au démarrage de Tomcat, par exemple via le script setenv.sh :



# setenv.sh
CATALINA_OPTS="-Ddatasource.url=jdbc:database://server:1234/schema \
               -Ddatasource.username=dbusername \
               -Ddatasource.password=dbpassword"



Il est également possible de les définir dans le fichier catalina.properties :



# catalina.properties
datasource.url=jdbc:database://server:1234/schema
datasource.username=dbusername
datasource.password=dbpassword



La substitution des propriétés ne se limite pas au fichier server.xml.
Elle s&#8217;applique à la majorité des fichiers XML de configuration de Tomcat, notamment :




la configuration globale (conf/server.xml, conf/context.xml, conf/web.xml) ;


la gestion des utilisateurs (conf/tomcat-users.xml) ;


la configuration des applications (fichiers de contexte, web.xml, fragments web).




Par défaut, Tomcat ne remplace les placeholders qu&#8217;à partir des propriétés système.
Toutefois, à l&#8217;image de frameworks comme Spring Boot ou Quarkus, il est possible d&#8217;enrichir ce mécanisme grâce aux property sources.


lorsqu&#8217;on configure d&#8217;autres property sources, celle par défaut reste active.
Il est toutefois possible de la désactiver :



# catalina.properties
org.apache.tomcat.util.digester.REPLACE_SYSTEM_PROPERTIES=false





Variables d&#8217;environnement


Tomcat peut être configuré pour lire les propriétés à partir des variables d&#8217;environnement.
Pour cela, il faut déclarer une source de propriétés spécifique avec org.apache.tomcat.util.digester.PROPERTY_SOURCE en propriété système ou dans catalina.properties :



# catalina.properties
org.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.EnvironmentPropertySource



Les variables sont ensuite définies dans l&#8217;environnement, par exemple via setenv.sh :



# setenv.sh
DB_URL=jdbc:database://server:1234/schema
DB_USERNAME=dbusername
DB_PASSWORD=dbpassword



Elles peuvent alors être utilisées directement dans les fichiers de configuration :



&lt;Resource name="jdbc/my-global-ds"
          type="javax.sql.DataSource" auth="Container"
          driverClassName="org.database.Driver"
          url="${DB_URL:-jdbc:derby://localhost:1527/sewadb}"
          username="${DB_USERNAME}"
          password="${DB_PASSWORD}" /&gt;





Avec Kubernetes


Dans un environnement Kubernetes, Tomcat propose une source de propriétés dédiée permettant d&#8217;exploiter les mécanismes de service binding :



# catalina.properties
org.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.ServiceBindingPropertySource



Pour le mettre en oeuvre, on peut utiliser de ConfigMap ou des Secret.



apiVersion: v1
kind: Secret
metadata:
  name: tomcat-secret
data:
  db_password: c2Fwd2QK









Le secret db_password est stocké en base64.





Ce secret doit ensuite être monté en volume dans le pod qui doit l&#8217;utiliser.



apiVersion: apps/v1
kind: Pod
metadata:
  name: tomcat
spec:
  containers:
  - name: tomcat
    image: jtips/tomcat
    imagePullPolicy: Never
    ports:
    - name: http
      containerPort: 8080
      protocol: TCP
    env:
    - name: SERVICE_BINDING_ROOT
      value: /binding
    volumeMounts:
    - name: secret-binding
      mountPath: "/binding/secrets"
      readOnly: true
  volumes:
  - name: secret-binding
    secret:
      secretName: tomcat-secret



Avec ce volume, il y a un fichier /binding/secrets/db_password dans le pod, qui contient le mot de passe.
On peut donc l&#8217;utiliser dans la configuration server.xml, avec le préfixe chomp pour enlever d&#8217;éventuels caractères indésirables.



&lt;Resource name="jdbc/my-global-ds"
          type="javax.sql.DataSource" auth="Container"
          driverClassName="org.database.Driver"
          url="jdbc:derby://localhost:1527/sewadb"
          username="sa"
          password="${chomp:secrets.db_password}" /&gt;



La variable d&#8217;environnement SERVICE_BINDING_ROOT sert à spécifier le répertoire racine des bindings.
La clé de substitution est forcément en deux parties.




Source personnalisée


Tomcat permet également de définir une source de propriétés personnalisée en implémentant l&#8217;interface PropertySource.
Cette source personnalisée est ensuite déclarée dans org.apache.tomcat.util.digester.PROPERTY_SOURCE.


Cette personnalisation ouvre de nombreuses possibilités :




lecture depuis un autre fichier de propriétés ;


utilisation de formats alternatifs (XML, YAML, etc.) ;


gestion de valeurs chiffrées ;


intégration avec un caveau de secrets.




Certains ont déjà fait ce travail et l&#8217;ont partagé.
Par exemple, Tomcat External PropertySource permet d&#8217;utiliser un fichier properties externe, avec éventuellement du chiffrement.
Vault for Apache Tomcat utilise PicketLink, comme les anciennes versions de WildFly, pour extraire des valeurs stockées en caveau.


</description>
          <pubDate>2026-01-21T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Tomcat/PropertySource</link>
          <guid isPermaLink="true">https://www.jtips.info/Tomcat/PropertySource</guid>
        </item>
      
    
      
        <item>
          <title>Authentification OIDC avec Quarkus</title>
          <description>
Quarkus a un module pour faire de l&#8217;authentification.
La documentation explique comment l&#8217;utiliser avec Keycloak.
J&#8217;ai du l&#8217;utiliser avec Strava.


Extension


Extension quarkus-oidc



  &lt;dependency&gt;
    &lt;groupId&gt;io.quarkus&lt;/groupId&gt;
    &lt;artifactId&gt;quarkus-oidc&lt;/artifactId&gt;
  &lt;/dependency&gt;



Référence: https://quarkus.io/guides/security-oidc-configuration-properties-reference




Keycloak


Configuration


quarkus:
  oidc:
    auth-server-url: http://localhost:8180/realms/quarkus
    client-id: frontend
    application-type: web-app
    authentication:
      redirect-path: /tokens
  http:
    auth:
      permission:
        authenticated:
          paths: /*
          policy: authenticated



Avec cette configuration, une authentification est demandée pour n&#8217;importe quelle requête.
Pour ça, la requête est redirigée vers Keycloak.


Keycloak doit être démarré au préalable et le realm fourni dans l&#8217;exemple doit être importé.



docker run --name keycloak -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:26.0.7 start-dev



Sans la propriété quarkus.oidc.auth-server-url, en dev, Quarkus démarre une instance de Keycloak, sous forme de Dev Service.


Transport par le cookie q_session.



Code

Il y a plusieurs possibilités d&#8217;injection.



  @Inject
  SecurityIdentity securityIdentity;





io.quarkus.security.identity.SecurityIdentity



isAnonymous(): boolean


getRoles(): Set&lt;String&gt;


getPrincipal(): Principal


getCredentials(): Set&lt;Credential&gt;


getCredential(Class&lt;T&gt; credentialType): T


getAttributes(): Map&lt;String, Object&gt;


getAttribute(String name): T







C&#8217;est assez générique, mais pas très lisible.



  @Inject
  IdTokenCredential idToken;

  @Inject
  AccessTokenCredential accessToken;

  @Inject
  RefreshToken refreshToken;





io.quarkus.oidc.SecurityIdentity



getToken(): String


getType(): String


isInternal(): boolean









io.quarkus.oidc.AccessTokenCredential



getToken(): String


getType(): String


isOpaque(): boolean









io.quarkus.oidc.RefreshToken



getToken(): String


getType(): String







Ça manque d&#8217;informations utilisables.



  @Inject @IdToken
  JsonWebToken idToken;

  @Inject
  JsonWebToken accessToken;

  @Inject
  JsonWebToken refreshToken;



Là on a le détail des tokens JWT.
Ça marche parce que Keycloak fournit des tokens JWT.





Strava


Strava fait partie des fournisseurs prédéfinis, comme Google, Microsoft, Facebook,&#8230;&#8203;



quarkus:
  oidc:
    provider: strava
    client-id: 45678
    credentials:
      secret: 0123456789abcdef0123456789abcdef01234567
    application-type: web-app
    authentication:
      redirect-path: /tokens
  http:
    auth:
      permission:
        authenticated:
          paths: /*
          policy: authenticated



Pour les injections, on a des différences car les tokens fournis par Strava sont opaques.
Et aucun id token n&#8217;est fourni.
A la place, Quarkus en construit un en interne à partir des user info.


Si on injecte des JsonWebToken, une OIDCException est lancée, expliquant qu&#8217;un token opaque ne peut pas être converti en JWT.



io.quarkus.oidc.OIDCException: Opaque access token can not be converted to JsonWebToken
	at io.quarkus.oidc.runtime.OidcJsonWebTokenProducer.getTokenCredential(OidcJsonWebTokenProducer.java:67)
	at io.quarkus.oidc.runtime.OidcJsonWebTokenProducer.currentAccessToken(OidcJsonWebTokenProducer.java:41)



Pas de problème pour l&#8217;id token, qui est généré en interne, en JWT.




Personnalisation


Pour stocker les tokens en base de données: DatabaseTokenStateManager (extends TokenStateManager).


Pour personnaliser les rôles (par exemple avec les clubs): RolesAugmentor (extends SecurityIdentityAugmentor).


Pour les deux, on a la même impossibilité d&#8217;injecter des beans de scope request ou session.
C&#8217;est dû au fait que le contexte de RequestScoped n&#8217;est pas encore actif.


</description>
          <pubDate>2025-03-11T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Quarkus/AuthOIDC</link>
          <guid isPermaLink="true">https://www.jtips.info/Quarkus/AuthOIDC</guid>
        </item>
      
    
      
        <item>
          <title>BufferedInputStream</title>
          <description>
BufferedInputStream est une classe qui fait partie du paquetage java.io et qui hérite de InputStream.
Elle ne fait pas réellement d&#8217;entrées/sorties mais décore un autre InputStream, en ajoutant des fonctionnalités de buffering.


InputStream


On lit le contenu octet par octet.



File file = new File("data/example.txt");

byte[] result = new byte[(int) file.length()];
try (InputStream input = new FileInputStream(file)) {
    int currentByte;
    int pos = 0;
    while ((currentByte = input.read()) &gt; 0) {
        result[pos++] = (byte) currentByte;
    }
}

System.out.println(new String(result));



La lecture octet par octet n&#8217;est pas performante.
Autre solution, lire tout le contenu (JDK 9).



  result = input.readAllBytes();



ou un paquet (JDK 11)



  buffer = input.readNBytes(size);



Pour les plus anciens JDK



  byte[] buffer = new byte[size];
  input.read(buffer);



</description>
          <pubDate>2024-12-01T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/BufferedInputStream</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/BufferedInputStream</guid>
        </item>
      
    
      
        <item>
          <title>Gestions des threads dans WildFly</title>
          <description>
Les sous-systèmes de WildFly utilisent pas mal de pools de threads, avec des possibilités de configuration et de monitoring assez hétérogènes.


On peut globalement scinder ces sous-systèmes en deux groupes. D&#8217;une part il y a ceux qui ont des threads liés au réseau qui utilisent les workers du sous-système io. D&#8217;autre part, il y a ceux qui gèrent leurs threads en interne.


Sous-système IO


Le sous-système IO est responsable de la configuration des workers de la librairie XNIO.
Ces workers sont utilisés pour les entrées/sorties Web via le sous-système undertow et pour d&#8217;autres entrées/sorties réseau avec le sous-système remoting.


Configuration des workers

Dans ce sous-système, un worker est une instance de XnioWorker qui implémente ExecutorService.
Un worker est donc un pool de threads.


Le sous-système a un worker par défaut.



/subsystem=io:read-resource(recursive=true)



Le default worker n&#8217;a aucune configuration explicite.
Il a donc uniquement des caractéristiques  par défaut.



{
  "outcome" =&gt; "success",
  "result" =&gt; {
    "default-worker" =&gt; "default",
    "buffer-pool" =&gt; undefined,
    "worker" =&gt; {"default" =&gt; {
      "io-threads" =&gt; undefined,
      "stack-size" =&gt; undefined,
      "task-core-threads" =&gt; undefined,
      "task-keepalive" =&gt; undefined,
      "task-max-threads" =&gt; undefined,
      "outbound-bind-address" =&gt; undefined,
      "server" =&gt; undefined
    }}
  }
}





io-threads



par défaut: cpuCount x 2





max-pool-size


stack-size



mémoire stack des threads


par défaut: 0 (???)





task-core-threads


task-keepalive


task-max-threads





cd /subsystem=io
./worker=custom-worker/:add(   \
      io-threads=10,           \
      stack-size=5,            \
      task-keepalive=600,      \
      task-max-threads=100)



Problème:




Log sur appel Web &#8658; "default task-1"


1 seul thread "default task-NN", avec du XNIO dedans (cf. stack trace). Et 0 threads au départ, même avec task-core-threads&gt;0



task-core-threads serait inutile? cf. https://developer.jboss.org/thread/261489


il avait disparu entre les versions 10 et 12, il réapparait avec la version 13





32 threads "default IO-NN" pour 16 processeurs, de type WorkerThread (XNIO)




Remarque:


Il y a des "management task-NN" ainsi que des management IO-NN.





Web


see https://www.mastertheboss.com/web/jboss-web-server/using-a-custom-thread-pool-for-web-applications-running-on-wildfly/




EJB


subsystem ejb3 / thread-pool


configuration: https://docs.wildfly.org/32/wildscribe/subsystem/ejb3/thread-pool/index.html


ressources qui font référence au thread pool:




service=async


service=remote


service=timer-service






EE Concurrency Utilities


see
* https://jakarta.ee/learn/docs/jakartaee-tutorial/current/supporttechs/concurrency-utilities/concurrency-utilities.html
* https://jakarta.ee/learn/specification-guides/concurrency-explained/


subsystem EE (concurrent child)


dans la configuration fournie, on a 1 service par catégorie
on peut en ajouter et les injecter dans le code



@Resource(name = "concurrent/CustomManagedExecutorService")
ManagedExecutorService executorService;




@Asynchronous(executor="java:module/env/concurrent/myExecutor")
public CompletableFuture&lt;String&gt; myMethod() {
    // Processing
    return Asynchronous.Result.complete(string);
}



managed-executor-service

tâches asynchrones


configuration




core-threads: The minimum number of threads to be used by the executor. If left undefined the default core-size is calculated based on the number of processors. A value of zero is not advised and in some cases invalid. See the queue-length attribute for details on how this value is used to determine the queuing strategy.


max-threads


queue-length




attributs runtime




active-thread-count


completed-task-count


current-queue-size


max-thread-count


task-count


thread-count





managed-scheduled-executor-service

tâches planifiées


mêmes attributs que managed-executor-service



managed-thread-factory

threads gérés par le conteneur, avec propagation de contexte


pas beaucoup d&#8217;attributs: context-service et jndi-name, comme les autres, et priority





Autres


batch



JCA

&#8658; MDB?



JMS

subsystem=messaging-activemq


Pools de threads pour les clients ActiveMQ




global-client-scheduled-thread-pool-max-size


global-client-thread-pool-keepalive-time


global-client-scheduled-thread-pool-max-size


global-client-scheduled-thread-pool-keepalive-time




Pools de threads /server




thread-pool-max-size


scheduled-thread-pool-max-size





CDI

subsystem=weld




thread-pool-size







remoting


utilise le defaut worker




Virtual Thread


WildFly 32 n&#8217;annonce pas encore le support officiel de JDK 21 et ne supporte pas encore les virtual threads.


</description>
          <pubDate>2024-07-17T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WildFly/Thread</link>
          <guid isPermaLink="true">https://www.jtips.info/WildFly/Thread</guid>
        </item>
      
    
      
        <item>
          <title>Accès à RabbitMQ depuis Spring</title>
          <description>
Template


Comme dans d&#8217;autres parties de Spring, le template est une couche d&#8217;abstraction entre les beans et le client RabbitMQ.
Il permet d&#8217;envoyer ou de recevoir des messages.


La conception du template est plutôt souple, dans le sens où une instance de template peut servir pour un ou plusieurs exchange(s) et une ou plusieurs clés de routage pour l&#8217;envoi.
Il peut aussi revevoir sur une ou plusieurs queues.


Par ailleurs, il peut travailler avec des instances de Message (de Spring) ou des types personnalisés pour lesquels il gère la transformation.




RabbitTemplate



ConnectionFactory connectionFactory


String defaultReceiveQueue


String exchange


String routingKey


// Send to default exchange and routing key


send(Message message)


convertAndSend(Object object)


sendAndReceive(Message message): Message


convertSendAndReceive(Object message): Object


&lt;T&gt; convertSendAndReceiveAsType(Object message, ParameterizedTypeReference&lt;T&gt; responseType): T


// Send to specific routing key


send(String routingKey, Message message)


&#8230;&#8203;


// Send to specific exchange and routing key


send(String exchange, String routingKey, Message message)


&#8230;&#8203;


// Receive from default queue


receive(): Message


receiveAndConvert(): Object


&lt;T&gt; receiveAndConvert(String queueName, ParameterizedTypeReference&lt;T&gt; type): T


&lt;R, S&gt; receiveAndReply(ReceiveAndReplyCallback&lt;R, S&gt; callback): boolean


// Receive from specific queue


receive(String queueName): Message







Toutes les méthodes de réception peuvent avoir un timeout en paramètre.








Les templates ne sont généralement pas de beans.







Listener Container


Un listener est un objet qui se met à l&#8217;écoute sur une queue pour en consommer les messages.
Dans Spring, les listeners sont gérés par des conteneurs, eux-même créés par des fabriques.



RabbitListenerContainerFactory&lt;?&gt; -&gt; AbstractMessageListenerContainer -&gt; MessageListener



Le conteneur peut être créé manuellement.
Pour ça notre code doit appeler la méthode de création sur la fabrique, ajouter le listener ainsi que la queue, ou les queues,  à écouter.
Enfin, notre code doit appeler la méthode de démarrage du conteneur.
Cette façon de faire permet aussi de configurer le conteneur indépendamment de la fabrique.


Le conteneur peut aussi être créé automatiquement.
C&#8217;est le fonctionnement avec l&#8217;annotation @RabbitListener.







Spring propose deux sortes de conteneur: simple ou direct.


Direct container

Le conteneur direct est le plus simple à comprendre et aussi le plus récent.


Un conteneur direct peut écouter plusieurs queues.
Pour chaque queue, il crée un ou plusieurs consommateurs, selon la configuration.


Le conteneur établit la connexion à RabbitMQ et chaque consommateur ouvre un canal.








Simple container

Le conteneur simple a un fonctionnement un peu plus complexe et est la première implémentation de Spring.


Un conteneur simple peut aussi écouter plusieurs queues.
La principale différence, c&#8217;est que plusieurs consommateurs écoutent sur l&#8217;ensemble des queues.


Le conteneur établit la connexion à RabbitMQ et chaque consommateur ouvre un canal.








Programmation

Quand il s&#8217;agit de créer un conteneur, cela peut être fait par code, en invoquant la méthode createListenerContainer() sur la fabrique.


Après la création, nous devons ajouter des queues d&#8217;attente et un écouteur de message.
Nous pouvons également ajouter des paramètres personnalisés.
À la fin, le conteneur doit être explicitement démarré.



// Create
listenerContainer = listenerContainerFactory.createListenerContainer();
// Add queue(s)
listenerContainer.addQueueNames(UPLOAD_QUEUE_NAME);
// Set the message listener
listenerContainer.setMessageListener(this::onMessage);
// Custom settings (direct)
listenerContainer.setConsumersPerQueue(50);
// Start
listenerContainer.start();




Déclaration

Nous pouvons simplement déclarer les méthodes en tant qu&#8217;écouteurs de messages avec l&#8217;annotation @RabbitListener.
Le conteneur est alors créé par Spring Framework.



@RabbitListener(queues = "ClientQueue")
public ClientResponse onClientMessage(ClientRequest request) {
  ...
}






Comparaison


La documentation n&#8217;est pas très bavarde sur les points forts et faibles des deux types de conteneurs.
La seul chose qui y est bien expliquée, c&#8217;est que les conteneurs directs supportent mieux l&#8217;ajout et le retrait de queues.


Ajout et retrait de queues

L&#8217;ajout des queues à écouter se fait normalement avant le démarrage du conteneur.
Il reste possible d&#8217;ajouter ou de retirer des queues à un conteneur en cours de fonctionnement.


Pour un conteneur direct, chaque consommateur écoute une queue.
L&#8217;opération revient à ajouter ou supprimer un consommateur, ce qui se fait réellement dynamiquement sans effet de bord sur les autres consommateurs.


Pour un conteneur simple, l&#8217;opération nécessite le redémarrage du conteneur et de tous ses consommateurs.



Beaucoup de queues

Si un même conteneur doit être à l&#8217;écoute d&#8217;un nombre important de queues, les conteneurs peuvent être limités par le nombre de canaux.


En effet, le conteneur ouvre une connexion et chaque consommateur utilise son propre canal dans cette connexion.
Le nombre de consommateurs par conteneur est donc limité par le nombre maximum de canaux par connexion (channel_max).
Ce sujet a déjà été traité dans la page sur la configuration RabbitMQ.


Pour un conteneur direct, le nombre de queues qu&#8217;il peut écouté est donc channel_max / consumersPerQueue.
Pour écouter sur un grand nombre de queues, il faut donc augmenter channel_max et diminuer consumersPerQueue.


Cette dernière modification peut se faire dans les propriétés Spring pour la fabrique ou directement sur le conteneur.
On peut aussi garder la valeur par défaut qui est 1.



listenerContainer.setConsumersPerQueue(2);



Pour un conteneur simple, chaque consommateur écoute en même temps sur toutes les queues.
La limite n&#8217;est donc jamais atteinte, sauf à paramétrer concurrentConsumers et maxConcurrentConsumers avec des valeurs énormes, ce qui est de toute façon contreproductif.



Gros débit de messages

La capacité de traité des messages est forcément limité.
Comment est-ce que le conteneur va réagir à un excès de messages?


Avec un conteneur simple, la capacité de traitement des messages dépend du nombre de consommateurs.
Si le débit en entrée est supérieur à la capacité des consommateurs, les messages sont mis en buffer.


Avec un conteneur direct, la capacité de traitement dépend aussi du nombre de consommateurs.
Mais la réaction à un débit entrant trop important dépend du nombre de queues.
Avec un nombre de queues faible, les messages sont aussi mis en buffer, alors qu&#8217;avec beaucoup de queues, on a une exception.



ERROR c.r.c.impl.ForgivingExceptionHandler.log(119) - An unexpected connection driver error occurred
  java.util.concurrent.RejectedExecutionException: Task com.rabbitmq.client.impl.ConsumerWorkService$WorkPoolRunnable@5f340329
      rejected from org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor$1@1de16a73
      [Running, pool size = 20, active threads = 20, queued tasks = 100, completed tasks = 31]
    at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2081)
    at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:841)
    at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1376)
    at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor$1.execute(ThreadPoolTaskExecutor.java:269)
    at com.rabbitmq.client.impl.ConsumerWorkService.addWork(ConsumerWorkService.java:88)
    at com.rabbitmq.client.impl.ConsumerDispatcher.execute(ConsumerDispatcher.java:214)









Il faut que j&#8217;approfondisse le fonctionnement du buffer. A priori, il doit être au niveau du consommateur.






</description>
          <pubDate>2024-07-02T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/RabbitMQ</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/RabbitMQ</guid>
        </item>
      
    
      
        <item>
          <title>La balise location dans nginx</title>
          <description>
Priorité


Dans l&#8217;ordre de priorité&#8230;&#8203;




exactlty =: correspondance exacte





location  = / {
  # Uniquement la racine du serveur
  ...
}





prefix prefix ^~





location ^~ /msg {
  # Application msg

}





regular expression case sensitive ~





location ~ ^/msg {
  # Application msg

}





regular expression case insensitive ~*





location ~ ^/msg {
  # Application msg, fonctionne aussi avec Msg, MSG,...

}





prefix: correspondance du préfixe, priorité au plus long préfixe





location  / {
  # Configuration par défaut
  ...
}
location  /msg {
  # Application msg
  ...
}





Localisation nommée


La technique de named location permet à d&#8217;autres directives de faire des renvois.



location @msg {
    proxy_pass http://tomcat:8080;
}



On peut l&#8217;utiliser depuis une error_page ou try_files.



location /msg {
  try_files $uri @msg;
}



Dans cet exemple, on cherche à résoudre les requêtes localement et on renvoie sur la localisation nommée en cas d&#8217;échec.
On devrait arriver au même résultat par la gestion d&#8217;erreur.


On peut aussi utiliser try_files pour faire un renvoi systématique.
Sous cette forme, la localisation nommée sert surtout à faire de la réutilisation.



location /msg {
  try_files /dev/null @msg;
}





Localisations imbriquées


On peut imbriquer les localisations entre elles, sauf pour les correspondances exactes et les localisations nommées.
Les chemins de correspondance sont toujours absolues.


</description>
          <pubDate>2024-06-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Nginx/Location</link>
          <guid isPermaLink="true">https://www.jtips.info/Nginx/Location</guid>
        </item>
      
    
      
        <item>
          <title>HTTP/2 avec Nginx</title>
          <description>
Avant de commencer, il faut vérifier si notre nginx supporte HTTP/2.
En effet, pour avoir le module ngx_http_v2_module il faut que nginx soit compilé avec l&#8217;option --with-http_v2_module.



~$ nginx -V

configure arguments: --with-cc-opt='-g -O2 -ffile-prefix-map=/build/nginx-zctdR4/nginx-1.18.0=. -flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -flto=auto -ffat-lto-objects -flto=auto -Wl,-z,relro -Wl,-z,now -fPIC' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --modules-path=/usr/lib/nginx/modules --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-compat --with-debug --with-pcre-jit --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module --with-threads --add-dynamic-module=/build/nginx-zctdR4/nginx-1.18.0/debian/modules/http-geoip2 --with-http_addition_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_sub_module



h2


La façon d&#8217;activer HTTP/2 dépend de la version de nginx.
Dans les anciennes versions, c&#8217;est une option de la directive listen.



server {
  listen 443 ssl http2;
  ...
}



Depuis la version 1.25.1, c&#8217;est devenu une directive.



server {
  listen 443 ssl;

  http2 on;
  ...
}





h2c


La directive http2 active le protocole sur tous les ports du server, y compris les ports sans TLS, comme 80.
Comme les navigateurs ne supportent pas h2c, il faut un autre outil, comme curl avec l&#8217;option --http2 car l&#8217;upgrade n&#8217;est pas automatique.



~$ curl --http2 http://localhost



Il faut noter qu&#8217;avant la directive, nginx supportait h2c, mais sans l&#8217;upgrade HTTP/1 !&gt; HTTP/2.
Il fallait donc l&#8217;activer sur un port séparé.



server {
  listen 80 default_server;
  listen 81 http2;
  ...



Pour tester avec curl, il fallait bien demander HTTP/2 directement.



~$ curl --http2-prior-knowledge http://localhost:81



</description>
          <pubDate>2024-06-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Nginx/HTTP2</link>
          <guid isPermaLink="true">https://www.jtips.info/Nginx/HTTP2</guid>
        </item>
      
    
      
        <item>
          <title>Configuration RabbitMQ</title>
          <description>
Limites


Il n&#8217;y a pas de limite au nombre de connexions.
Les limitation viennent du système avec le nombre de fichiers ouverts et la mémoire.


La principale limitation logicielle par RabbitMQ concerne le nombre de canaux par connexion.
En effet, avec AMQP 0.9, on multiplexe les connexions grâce aux canaux.


Par défaut, la valeur maximale est de 2047.
Elle est négociée à chaque connexion entre le client et le serveur, pour prendre la limitation la plus basse des deux.
Il faut donc la configurer coté RabbitMQ et coté client.


Dans mes essais en local, il y a aussi une limite système de l&#8217;ordre 30000 à 35000.


RabbitMQ


# /etc/rabbitmq/conf.d/30-limit.conf
channel_max = 4000




Java

Lorsqu&#8217;on utilise directement le client Java, on fixe la limite sur la fabrique de connexion.



ConnectionFactory cf = new ConnectionFactory();
cf.setRequestedChannelMax(4000);



Lorsqu&#8217;on utilise Spring Boot, ça se fait par la propriété spring.rabbitmq.requested_channel_max.



spring:
  rabbitmq:
    host: rabbit.jtips.info
    port: 5672
    username: jtips
    password: jtips-pwd
    requested_channel_max: 4000




</description>
          <pubDate>2024-06-06T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/RabbitMQ/Settings</link>
          <guid isPermaLink="true">https://www.jtips.info/RabbitMQ/Settings</guid>
        </item>
      
    
      
        <item>
          <title>Virtual Threads</title>
          <description>
Un Virtual Thread est une version allégée de thread, dont la technique a été introduite en preview dans le JDK 19 et en version finale dans le JDK 21.


La promesse est de permettre une scalabilité bien plus importante qu&#8217;avec les threads classiques sans payer le surcoût du développement réactif, comme avec Vert.x ou RxJava.


Introduction


Un thread classique, ou platform thread, est essentiellement géré par le système d&#8217;exploitation.
Du point de vue Java, on a juste une enveloppe autour du thread système.


Le principal défaut des platform threads, c&#8217;est que ça consomme pas mal de ressources, en particulier en mémoire stack.
Pour contourner ce défaut, on a pris l&#8217;habitude de gérer des pools de threads.


La programmation réactive a poussé encore plus loin l&#8217;économie de ressource, en permettant une grande scalabilité avec peu de threads.
Malheureusement, ça se fait au détriment de la facilité de développement.
La programmation réactive est plus complexe que la programmation impérative.


Le virtual thread est une sorte de compromis pour permettre la programmation impérative concurrente en économisant des ressources.




API


java.lang.Thread

Il y a plusieurs façons de créer un thread avec la classe Thread:




directement avec la méthode statique Thread.startVirtualThread(runnable),


via Thread.Builder.OfPlatform pour plus de souplesse.






Thread



+ startVirtualThread(Runnable task): Thread


+ ofVirtual(): Thread.Builder.OfVirtual


+ ofPlatform(): Thread.Builder.OfPlatform


+ isVirtual(): boolean









Thread.Builder.OfVirtual



+ name(String name): OfVirtual


+ name(String prefix, long start): OfVirtual


+ factory(): ThreadFactory


+ start(Runnable task): Thread


+ unstarted(Runnable task): Thread







Exemple pour un thread:



ThreadFactory factory = Thread.ofVirtual().name("virtual-exec").unstarted(task);



Exemple via une fabrique de threads:



ThreadFactory factory = Thread.ofVirtual().name("virtual-exec-", 0).factory();
Thread thread = factory.newThread(task);




java.util.concurrent.Executor

Les applications utilisent fréquemment un Executor ou un ExecutorService avec un pool de threads.
Or il n&#8217;est pas question de placer un virtual thread dans un pool, c&#8217;est contraire à leur nature.


Le JDK 21 a donc introduit des nouvelles méthodes qui construisent des executors avec un nouveau thread pour chaque tâche.



ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()





Executors



+ newVirtualThreadPerTaskExecutor(): ExecutorService


+ newThreadPerTaskExecutor(ThreadFactory factory): ExecutorService







Je préfère généralement la variante longue à la méthode, avec une fabrique car avec la méthode newVirtualThreadPerTaskExecutor() on construit des threads sans nom.
La fabrique permet d&#8217;avoir des noms de threads avec un préfixe et un compteur.



ThreadFactory factory = Thread.ofVirtual().name("virtual-exec-", 0).factory();
ExecutorService executor = Executors.newThreadPerTaskExecutor(factory);






Frameworks


Tomcat

Le support des virtual threads a été ajouté dans Tomcat 11 et backporté dans Tomcat 9 et 10.


Il peut être configuré pour un connector.



&lt;Service name="Catalina"&gt;
  &lt;Connector port="8080" useVirtualThreads="true"/&gt;
  ...
&lt;/Service&gt;



Il peut aussi être utilisé via un executor.



&lt;Service name="Catalina"&gt;
  &lt;Executor name="virtualThreadsExecutor"
            className="org.apache.catalina.core.StandardVirtualThreadExecutor" /&gt;
  &lt;Connector executor="virtualThreadsExecutor"
             port="8080" /&gt;
  ...
&lt;/Service&gt;



Avec cette configuration, les threads ne sont plus préfixés par http-nio-exec, avec tomcat-virt.



Spring Boot

Depuis Spring Boot 3.2, on active le support des virtual threads avec une seule propriété,



spring
  threads
    virtual
      enabled: true



Ça fait passer Tomcat ou Jetty embarqué sur des virtual threads.
Pour Undertow, ça semble plus complexe à cause de fuites de mémoire et l&#8217;implémentation est repoussée (au moins après 3.3).


Dans le cas de Tomcat, les threads ne sont plus préfixés par http-nio-exec, mais par tomcat-handler.


La propriété touche aussi les threads pour les méthodes asynchrone (@Async), le scheduling, Kafka, Redis et AMQP.
Evidemment, si on fait des configurations personnalisées pour ces modules, on risque d&#8217;écraser la nouvelle configuration par défaut.
Il y a d&#8217;ailleurs une nouvelle annotation conditionnelle @ConditionalOnThreading(Threading.VIRTUAL) qu&#8217;on peut aussi utiliser dans nos configurations personnalisées.





Inspection


Ce sujet semble être le point faible des virtual threads.


J&#8217;imagine que des améliorations vont arriver.
Les notes de ce paragraphe ont été faites avec le JDK 21, et peuvent devenir obsolètes.


Thread dump

Par défaut, un thread dump n&#8217;inclut pas les virtual threads.
C&#8217;est valable le bean JMX ThreadMXBean, ainsi que pour les outils jstack, jconsole et visualvm.


Je suppose que c&#8217;est parce que ces outils passent par ThreadMXBean dont l&#8217;implémentation sun.management.ThreadImpl appelle une méthode native.
Et qui dit natif, dit uniquement platform threads.



// Get an array of platform threads
ThreadInfo[] threads = ManagementFactory.getThreadMXBean().dumpAllThreads(true, true)



Avec jcmd, les threads virtuels n&#8217;apparaissent pas avec la commande Thread.print, même avec l&#8217;option -e (extended), mais uniquement avec Thread.dump_to_file (qui n&#8217;est dans la documentation de référence qu&#8217;à partir du JDK 22).



~$ jcmd &lt;pid&gt; Thread.dump_to_file dump.txt



Cette nouvelle commande utilise le bean JMX HotSpotDiagnosticMXBean qui conserve des informations de virtual threads.
Dommage que la méthode dumpThreads(&#8230;&#8203;) ne fonctionne qu&#8217;avec une écriture fichier.


Le endpoint /threaddump de l&#8217;actuator de Spring Boot ne renvoie que les platform threads.



Flight recorder

Il y a 4 nouveaux événements dans JFR:




jdk.VirtualThreadStart, jdk.VirtualThreadEnd


jdk.VirtualThreadSubmitFailed


jdk.VirtualThreadPinned





System properties

La propriété système jdk.trackAllThreads influence le résultat de HotSpotDiagnosticMXBean.dumpThreads(&#8230;&#8203;), et donc de la commande jcmd &lt;pid&gt; Thread.dump_to_file.


Quand sa valeur est false, les virtual threads créés directement n&#8217;apparaissent plus dans le dump.
Par contre, les threads créés via un executor ne sont pas impactés.


La propriété n&#8217;est pas prise en compte si elle est modifiée en cours d&#8217;exécution avec System.setProperty("jdk.trackAllThreads", "false").
Il faut obligatoirement la renseigner en passant l&#8217;option à la VM java -Djdk.trackAllThreads=false &#8230;&#8203;.


La différence entre les threads créés directement et ceux par executor peut s&#8217;expliquer par une mécanique interne. Chaque thread est rattaché à un container (jdk.internal.vm.ThreadContainer), soit  racine (RootContainer ou TrackingRootContainer) pour les threads créés directement, soit pour un executor. Or le dump est configuré au niveau du conteneur.
La racine est exposée en fonction de la propriété, alors que les threads d'executor sont toujours exposés.





Mécanique interne


Carrier thread

En introduction, on a vu la différence entre un virtual thread et un platform thread.


Pour fonctionner, un virtual thread s&#8217;appuie sur un platform thread qui s&#8217;appelle alors un carrier thread.
Dans sa vie, un virtual thread peut être attaché (mount) et détaché (unmount) plusieurs fois d&#8217;un carrier thread.
Le détachement se fait à chaque fois qu&#8217;il y a des entrées/sorties bloquantes.


L&#8217;ensemble des carrier thread est géré sous la forme d&#8217;un ForkJoinPool qui est dimensionné en fonction des processeurs disponibles.
Ce pool peut être configuré avec les propriétés système suivants:




jdk.virtualThreadScheduler.parallelism


jdk.virtualThreadScheduler.maxPoolSize


jdk.virtualThreadScheduler.minRunnable





Pinned thread

Il y a des cas particuliers à cette mécanique de détachement:




lorque l&#8217;entrée/sortie est dans un bloc synchronized,


lors de l&#8217;appel d&#8217;une méthode native.




Dans une telle condition, on dit que le virtual thread est pinned.
Il faut l&#8217;éviter car le carrier thread est bloqué et on perd l&#8217;avantage des virtual threads.
Autant que possible, il faut remplacer les blocs synchronized par d&#8217;autres familles de verrous.
Le diagnostic peut être fait avec la propriété système jdk.tracePinnedThreads (full/short).



ThreadLocal

La mécanique de ThreadLocal fonctionne avec les  virtual thread comme avec les platform thread.


La propriétés système jdk.traceVirtualThreadLocals permet de détecter la mise en ThreadLocal de données.



Thread pools

Du fait de sa nature même, la spécification interdit de mettre des virtual threads en pool.



Daemon Thread

Un virtual thread est toujours de type daemon.
Si un JVM n&#8217;a que des threads virtuels, elle s&#8217;arrête.



</description>
          <pubDate>2024-05-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/VirtualThread</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/VirtualThread</guid>
        </item>
      
    
      
        <item>
          <title>UUID en Java</title>
          <description>
java.util.UUID


Le JDK a une classe java.util.UUID depuis la version 5 en 2004.
L&#8217;interface publique de cette classe n&#8217;a pas changé entre le JDK 5 et 25 en 2025.




UUID



+ randomUUID(): UUID


+ nameUUIDFromBytes(byte[] name): UUID


+ fromString(String name): UUID


+ version(): int


+ variant(): int


+ timestamp(): long


+ clockSequence(): int


+ node(): long


+ getMostSignificantBits(): long


+ getLeastSignificantBits(): long







Les méthodes statiques permettent de générer des UUIDs, avec des capacités limitées à la version 4 (UUID.randomUUID()) et la version 3 (UUID.nameUUIDFromBytes(name.getBytes())).
Il n&#8217;y a rien pour les versions 1 ou 5.
Il n&#8217;y a rien non plus pour les versions 6 à 8, trop récentes pour le JDK 21.


La méthode fromString(&#8230;&#8203;) permet de créer une instance d&#8217;UUID à partir d&#8217;une chaîne de caractères.
Elle ne se préoccupe pas de la version et ne vérifie pas le contenu.
Il faut juste que le texte respecte le format UUID.
Ça permet de charger des UUIDs générés par ailleurs.


Les méthodes timestamp(), clockSequence() et node() ne fonctionnent qu&#8217;avec des UUID v1.
Pour les autres versions, elles lèvent une UnsupportedOperationException.


La génération de UUID v7 a été ajoutée au JDK 26 (2026), avec une nouvelle méthode de création ofEpochMillis(long timestamp).
Pour les versions précédentes, on peut utiliser des librairies tierces.




Java UUID Generator


java-uuid-generator, aka JUG, supporte les versions 1 à 7 de UUID.



  &lt;dependency&gt;
    &lt;groupId&gt;com.fasterxml.uuid&lt;/groupId&gt;
    &lt;artifactId&gt;java-uuid-generator&lt;/artifactId&gt;
    &lt;version&gt;5.0.0&lt;/version&gt;
  &lt;/dependency&gt;



UUID v1


// based on preferred MAC address
TimeBasedGenerator generatorV1 = Generators.timeBasedGenerator(EthernetAddress.fromPreferredInterface());
UUID idV1 = generatorV1.generate();

// based on random multicast address
TimeBasedGenerator generatorV1Random = Generators.timeBasedGenerator();
UUID idV1Random = generatorV1Random.generate();




UUID v3


NameBasedGenerator generatorV3 = Generators.nameBasedGenerator(null, MessageDigest.getInstance("MD5"));
UUID idV3 = generatorV3.generate("JTips");




UUID v4


// based on SecureRandom
RandomBasedGenerator generatorV4 = Generators.randomBasedGenerator();
UUID idV4 = generatorV4.generate();




UUID v5


RandomBasedGenerator generatorV5 = Generators.NameBasedGenerator();
UUID idV5 = generatorV5.generate("JTips");




UUID v6


TimeBasedReorderedGenerator generatorV6 = Generators.timeBasedReorderedGenerator();
UUID idV6 = generatorV6.generate();




UUID v7


TimeBasedEpochGenerator generatorV5 = Generators.timeBasedEpochGenerator();
UUID idV7 = generatorV7.generate( );






UUID Creator



&lt;dependency&gt;
  &lt;groupId&gt;com.github.f4b6a3&lt;/groupId&gt;
  &lt;artifactId&gt;uuid-creator&lt;/artifactId&gt;
  &lt;version&gt;5.3.7&lt;/version&gt;
&lt;/dependency&gt;



UUID v1


// based on preferred MAC address
UUID idV1 = UuidCreator.getTimeBasedWithMac();

// based on random multicast address
UUID idV1Random = UuidCreator.getTimeBasedWithRandom();




UUID v3


UUID idV3 = UuidCreator.getNameBasedMd5("JTips");




UUID v4


// based on Random
UUID idV4 = UuidCreator.getRandomBased();




UUID v5


UUID idV5 = UuidCreator.getNameBasedSha1("JTips");




UUID v6


UUID idV6 = UuidCreator.getTimeOrdered();




UUID v7


UUID idV7 = UuidCreator.getTimeOrderedEpoch();




</description>
          <pubDate>2024-05-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/UUID</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/UUID</guid>
        </item>
      
    
      
        <item>
          <title>UUID, Universally Unique IDentifier</title>
          <description>
Un UUID, ou Universally Unique IDentifier, est un nombre entier de 128 bits, encodé en 32 chiffres hexadécimaux.
Plus préciséement, le format habituellement utilisé est constitué de 8 + 3x4 + 12 chiffres: 00000000-0000-0000-0000-000000000000.


Les UUIDs étaient utilisés initialement par des systèmes d&#8217;exploitation (Microsoft Windows), middlewares (DCE) et applications () pour identifier des composants.
Les RFCs citées en référence sont d&#8217;ailleurs basées sur la spécification de DCE.
Ces UUID peuvent être utilisés sous des noms différents comme GUID, CLSID, OID,&#8230;&#8203;
Aujourd&#8217;hui on utilise couramment le type UUID en base de données pour les clés primaires.


Si le format de l&#8217;UUID est stable, il y a plusieurs versions pour la génération, définies dans la RFC 4122 en juillet 2005.




v1: basé sur une adresse MAC sur 48 bits et un horodatage (100 ns) sur 74 bits


v2: compatibilité avec DCE


v3: basé sur un nom hashé en MD5


v4: totalement aléatoire


v5: idem v3, mais en SHA1




Depuis mai 2024, avec la RFC 9562, il y en a 3 de plus.




v6: idem v1, mais avec un ordre plus logique


v7: basé sur un horodatage avec EPOCH comme base


v8: implémentations spécifiques




Le 1° chiffre du 3° groupe représente la version.



 00000000-0000-9000-0000-000000000000









En plus de la version (4 bits), il y a une information de variante, sur 2 bits, en début de 3° groupe.





UUID v1


En version 1, l&#8217;UUID est généré en 3 parties.


La fin de l&#8217;UUID, sur 48 bits, est une composante spatiale, basée sur l&#8217;adresse MAC.
Cette partie est donc constante sur une même machine, avec la même interface réseau.


Le début, sur 60 bits, est une composante temporelle, basée sur le temps passé (par tranche de 0.1 µs) depuis l&#8217;origine du calendrier grégorien (15/10/1582, 00h00 UTC).
Ça ressemble au timestamp d&#8217;Unix, mais avec une base au 15 octobre 1582 au lieu du 1° janvier 1970 et avec une unité d&#8217;un dizième de microseconde au lieu de la milliseconde.


Au milieu, sur 14 bits, il y a une séquence pour éviter les confits.


Si on ajoute la version (4 bits) et (2 bits), on arrive au total de 128 bits.



1 2 3 4 5 6 7 8  -  1 2 3 4  -  V 2 3 4  -  1 2 3 4  -  1 2 3 4 5 6 7 8 9 0 1 2
time low         -  time mid -  time high-  clock seq-  node (MAC address)



Quand on génère une série de UUID v1, le début change à chaque fois, la fin de la composante temporelle reste relativement stable et la fin ne change jamais.



00000000-0000-1000-9c26-04d9f5d35a2b    // 1582-10-15T00:00:00.000
075bcd15-0000-1000-9c26-04d9f5d35a2b    // 1582-10-15T00:00:12.345
73ce2ff2-0b3a-1000-9c26-04d9f5d35a2b    // 1582-10-29T06:56:07.890
a630f34e-9b4b-11b6-9c26-04d9f5d35a2b    // 1974-01-02T19:15:01.234





UUID v4


Cette version ne se base sur aucune information externe.
L&#8217;UUID est généré de façon (pseudo-)aléatoire.



1 2 3 4 5 6 7 8  -  1 2 3 4  -  V 2 3 4  -  1 2 3 4  -  1 2 3 4 5 6 7 8 9 0 1 2
random           -  random   -  random   -  random   -  random



C&#8217;est la version qui est utilisée habituellement pour les clés primaires en base de données.
Quand on génère une série de UUID v4, la totalité change à chaque fois.



d1418e65-43fb-4610-9db2-dde2cddbe6d8
18c61ac0-8bf9-41bf-8fba-e13657671cb0
aff02b92-fbcf-4e69-a5d0-929df63916a3
982e7059-ffef-477e-8cf9-fea2e725ce66





UUID v3 et v5


Les versions 3 et 5 se ressemblent.
Elles sont basée sur le hash d&#8217;un texte, en MD5 pour la v3 et SHA-1 pour la v5.



1 2 3 4 5 6 7 8  -  1 2 3 4  -  V 2 3 4  -  1 2 3 4  -  1 2 3 4 5 6 7 8 9 0 1 2
sha1 high        -  sha1 high-  sha1 mid -  sha1 low -  sha1 low



Il n&#8217;y a aucune variation dans l&#8217;algorithme.
Si on génère plusieurs fois l&#8217;UUID avec le même texte, on aura le même résultat.



7c1b4a99-65ac-5caa-8e2d-da5963020190    // JTips
7c1b4a99-65ac-5caa-8e2d-da5963020190    // JTips
7c1b4a99-65ac-5caa-8e2d-da5963020190    // JTips
9df07c28-781c-5dd8-9472-5c5d80a2370f    // jtips





UUID v6 et v7


Ces versions sont plus récentes que les précédentes, elles ont été publiées en mai 2024, avec la RFC-9562  qui remplace la RFC-4122.
Leur objectif principal est de rendre l&#8217;UUID plus facile à indexer, tout en se basant sur un timestamp, comme la version 1.


La version 6 utilise les mêmes données que la version 1, mais avec les parties hautes du timestamp au début.



1 2 3 4 5 6 7 8  -  1 2 3 4  -  V 2 3 4  -  1 2 3 4  -  1 2 3 4 5 6 7 8 9 0 1 2
time high        -  time mid -  time low -  clock seq-  node (MAC address)



Quand on génère une série de UUID v6, le début est relativement stable, la fin de la composante temporelle change beaucoup et la fin ne change jamais.
La série est strictement croissante.



00000000-0000-6000-9c26-04d9f5d35a2b    // 1582-10-15T00:00:00.000
00000000-75bc-6d15-9c26-04d9f5d35a2b    // 1582-10-15T00:00:12.345
0000b3a7-3ce2-6ff2-9c26-04d9f5d35a2b    // 1582-10-29T06:56:07.890
1b69b4ba-630f-634e-9c26-04d9f5d35a2b    // 1974-01-02T19:15:01.234



La version 7 utilise un timestamp en millisecondes avec Epoch comme référence (01/01/1970, 00h00 UTC), sur 48 bits.
Le reste est complété avec un nombre aléatoire sur 74 bits.



1 2 3 4 5 6 7 8  -  1 2 3 4  -  V 2 3 4  -  1 2 3 4  -  1 2 3 4 5 6 7 8 9 0 1 2
unix time        -  unix time-  random   -  random   -  random



Quand on génère une série de UUID v7, le début est relativement stable, la fin de la composante temporelle change beaucoup et la fin change à chaque fois.
La série est strictement croissante.



00000000-0000-7642-8300-19ae07cdae82    // 1970-01-01T00:00:00.000
00000000-3039-798d-ba83-1cb0fd8eed69    // 1970-01-01T00:00:12.345
00004996-02d2-7beb-ab8e-3f63b12a1dd8    // 1970-01-15T06:56:07.890
011f71fb-04cb-7f5d-a1f8-94fead4bbb84    // 2009-02-13T23:31:30.123



C&#8217;est une bonne alternative à la version 4 pour les clés primaires en base de données.
Sa valeur ajoutée est la possibilité de trier par date de création sans colonne supplémentaire.
Et il est même possible d&#8217;extraire le timestamp de création de la clé.




Java


Le JDK a une classe java.util.UUID.
Par contre, il n&#8217;a que la génération d&#8217;UUID v4.



UUID id = UUID.randomUUID()



Pour les autres versions, on peut utiliser des librairies.
FasterXML, qui gère Jackson, propose java-uuid-generator pour ça.


Le sujet est traité avec plus de détails dans une page dédiée.




PostgreSQL


PostgreSQL a une fonction de génération d&#8217;UUID v4.



SELECT gen_random_uuid();

CREATE TABLE my_table (
    id uuid DEFAULT gen_random_uuid() NOT NULL,
    ...
)



L&#8217;extension uuid-ossp permet de générer des UUID en version 1, 3, 4 ou 5.
La génération v4 diffère de gen_random_uuid() par le générateur random.



CREATE EXTENSION "uuid-ossp" SCHEMA public;

SELECT uuid_generate_v4();

CREATE TABLE my_table (
    id uuid DEFAULT uuid_generate_v4() NOT NULL,
    ...
)



PostgreSQL supporte nativement UUID v7 depuis la version 18.
Il y a deux nouvelles fonctions: uuidv4() qui est identique à gen_random_uuid() et uuidv7()



SELECT uuidv7();

CREATE TABLE my_table (
    id uuid DEFAULT uuidv7() NOT NULL,
    ...
)



Pour les versions précédentes, il faut passer par des extensions tierces, comme pg_uuidv7, ou une simple fonction comme uuid_generate_v7().



CREATE EXTENSION pg_uuidv7 SCHEMA public;

SELECT uuid_generate_v7();
SELECT uuid_v7_to_timestamptz('011f71fb-04cb-7f5d-a1f8-94fead4bbb84');
=&gt; 2009-02-13 23:31:30.123+00

CREATE TABLE my_table (
    id uuid DEFAULT uuid_generate_v7() NOT NULL,
    ...
)



Le support d&#8217;UUID v7 est prévu pour la version 17, avec une fonction uuidv7().
Par soucis de cohérence, la fonction gen_random_uuid() a un nouvel alias uuidv4()


Par contre, il ne semble pas y avoir quoi que ce soit pour UUID v6.


</description>
          <pubDate>2024-05-14T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Misc/UUID</link>
          <guid isPermaLink="true">https://www.jtips.info/Misc/UUID</guid>
        </item>
      
    
      
        <item>
          <title>Ingress</title>
          <description>
Ingress est un type de service Kubernetes qui centralise l&#8217;accès HTTP à des services du cluster.
Les instances Ingress sont gérée par un Ingress Controller, dont il existe plusieurs implémentations.







Ingress Controller


L&#8217;implémentation la plus classique de contrôleur Ingress se base du nginx.
Le projet Kubernetes fournit aussi des implémentations avec AWS ou Google Cloud.
Beaucoup d&#8217;autres implémentations existent, dans des projets indépendants, comme HAProxy, Traefik, Azure,&#8230;&#8203;


Le contrôleur peut être activé en utilisant le fichier de configuration proposé dans la documentation.



~$ kubectl apply -f ingress-nginx-deploy.yaml



Dans Minikube, le contrôleur est fourni sous forme d&#8217;un module complémentaire.



~$ minikube addons enable ingress





Routage HTTP


Le routage HTTP est la mission principale d&#8217;Ingress.


Contrairement à un nginx nu, Ingress ne relaie pas les requêtes vers n&#8217;importe quel host et port.
Il le fait uniquement dans le cadre d&#8217;un cluster Kubernetes, vers des services de type NodePort ou LoadBalancer.


L&#8217;essentiel de la configuration d&#8217;un service Ingress est fait de règles de renvoi vers les services.



apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
spec:
  rules:
  - ...
  - ...



Ces règles peuvent se baser sur le chemin de la requête, sous forme de préfixe (Prefix) ou en chemin exact (Exact).








  - http:
      paths:
        - path: /app1
          pathType: Prefix
          backend:
            service:
              name: app1
              port:
                number: 8080



Elles peuvent aussi se baser sur le nom d&#8217;hôte de la requête, soit avec un nom exact (app1.example.com) soit avec un jocker (*.example.com).








  - host: app1.example.com
    http:
      paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: app1
              port:
                number: 9999





TLS


Pour activer le support des requêtes TLS, sur le port 443, il faut créer un secret de type tls, avec les clés.



apiVersion: v1
kind: Secret
metadata:
  name: tls-keys
  namespace: default
data:
  tls.crt: MIIEKTCCApGgAwIBAgIQHsvvNTJVqVoJBk+C0pPZ3jANBgkqhkiG9w0BA...
  tls.key: MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDWkHM3g...
type: kubernetes.io/tls



Ça peut aussi se faire en ligne de commande, à partir de fichiers.



~$ kubectl create secret tls tls-keys --key example.key --cert example.crt









Les clés peuvent être créées avec OpenSSL ou tout autre outil dérivé.
mkcert est pratique dans un environnement local.





Les clés ajoutées peuvent être utilisées explicitement dans une configuration Ingress



apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
spec:
  tls:
  - secretName: tls-key
    hosts:
    - app1.example.com
  rules:
  - ...



Elles peuvent être déclarées comme clés par défaut avec Minikube.



~$ minikube addons configure ingress
-- Enter custom cert (format is "namespace/secret"): default/tls-keys
✅  ingress was successfully configured

~$ minikube addons disable ingress
🌑  "The 'ingress' addon is disabled

~$ minikube addons enable ingress
🌟  The 'ingress' addon is enabled





Annotations


L&#8217;ajout d&#8217;annotations dans la configuration Ingress permet d&#8217;utiliser des fonctionnalités propres au contrôleur, comme avec nginx:




le support d&#8217;expressions régulières,


la réécriture d&#8217;URL,


l&#8217;authentification,


le canary deployment,


&#8230;&#8203;




La réécriture d&#8217;URL s&#8217;utilise conjointement avec le support d&#8217;expressions régulières.
Avec l&#8217;exemple ci-dessous, Ingress remplace le préfixe des chemins qui commenncent par /app par /service avant de transférer les requêtes vers la cible.



apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /service$2
spec:
  rules:
  - http:
      paths:
      - path: /app(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: app
            port:
              number: 8080



On active la fonctionnalité d&#8217;expression régulière avec l&#8217;annotation nginx.ingress.kubernetes.io/use-regex et en utilisant un type de chemin ImplementationSpecific.




Routage TCP / UDP


Ingress ne supporte officiellement que le protocole HTTP.
Certains contrôleurs peuvent quand même router d&#8217;autres protocoles TCP et/ou UDP.


C&#8217;est le cas du contrôleur nginx qui peut être configuré via des ressources ConfigMap.


Dans Minikube, dès qu&#8217;on active l&#8217;extension Ingress, une ConfigMap est créée pour TCP et une autre pour UDP.
Toutes deux sont dans l&#8217;espace de nommage ingress-nginx sans donnée et peuvent être patchées.


Par exemple, pour exposer une base de données PostgreSQL déployée dans un service postgres de l&#8217;espace de nommage par défaut.



~$ kubectl patch configmap tcp-services --nanespace ingress-nginx                \
                           --patch '{"data":{"5432":"default/postgres:5432"}}'



Il faut aussi modifier la configuration du contrôleur pour qu&#8217;il écoute sur le port 5432.



~$ kubectl patch deployment ingress-nginx-controller --nanespace ingress-nginx   \
                            --patch-file ingress-nginx-controller-patch.yaml



Avec le contenu suivant pour le fichier ingress-nginx-controller-patch.yaml, on configure le contrôleur pour qu&#8217;il écoute les ports PostgreSQL, Redis et AMQP.



spec:
  template:
    spec:
      containers:
      - name: controller
        ports:
         - containerPort: 5432
           hostPort: 5432
         - containerPort: 6379
           hostPort: 6379
         - containerPort: 5672
           hostPort: 5672



</description>
          <pubDate>2024-05-06T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Kubernetes/Ingress</link>
          <guid isPermaLink="true">https://www.jtips.info/Kubernetes/Ingress</guid>
        </item>
      
    
      
        <item>
          <title>Déploiement en parallèle avec Tomcat</title>
          <description>
Le déploiement parallèle de Tomcat permet de faire cohabiter plusieurs versions d&#8217;une mếme application.


La fonctionnalité est apparue avec Tomcat 7.
On peut la visualiser dans le Web Application Manager, avec une colonne Version dans la liste des applications, et un champ Version (for parallel deployment) dans la zone de déploiement de fichier WAR.


Déploiement avec le manager


Le déploiement parallèle n&#8217;est proposé que pour le déploiement d&#8217;un répertoire ou fichier WAR déjà présent sur le serveur.







Il y a un champ facultatif pour la version.
Après le déploiement, cette version apparraît dans la liste des applications.







Le déploiement parallèle peut aussi être fait en ligne de commande.



~$ curl -T msg.war "http://alexis:hassler@localhost:8080/manager/text/deploy?path=/msg&amp;update=true&amp;version=0.5"
OK - Deployed application at context path [/msg##0.5]

~$ curl "http://alexis:hassler@localhost:8080/manager/text/list"
OK - Listed applications for virtual host [localhost]
/:running:0:ROOT
/msg:running:0:msg##0.5
/msg:running:0:msg##0.2
/manager:running:0:manager





Déploiement sans le manager


L&#8217;ajout d&#8217;une version à l&#8217;application se fait par le nom du répertoire et/ou du fichier WAR dans le répertoire webapps.
Ça se fait en suffixant le nom du fichier avec ##&lt;version&gt;.



apache-tomcat
  ↳ bin
  ↳ conf
  ↳ lib
  ↳ logs
  ↳ temp
  ↳ webapps
    ↳ manager
    ↳ msg##0.1.war
    ↳ msg##0.2.war
    ↳ msg##0.3.war
    ↳ ROOT
  ↳ work





Gestion des requêtes


Lorsque plusieurs versions de la même application sont déployées, elles ont le même contexte.


Tomcat choisit sur quelle version orienter chaque requête selon une logique d&#8217;affinité de session.
Si une des versions a une session pour le JSESSIONID, alors la requête y est envoyée.
Sinon c&#8217;est la dernière version est privilégiée.


L&#8217;ordre des versions n&#8217;est pas documenté mais il n&#8217;a rien à voir avec la date du déploiement ou du build.
Il semble être simplement alphabétique (testé avec Tomcat 10.1).


Quelques exemples:




2.0 &gt; 1.1 &gt; 1.0


1.2 &gt; 1.1 &gt; 1.12 &gt; 1.0


c &gt; b &gt; a






Retrait automatique


Garder une ancienne version peut être utile pour plusieurs raisons:




conserver la possibilité d&#8217;annuler le déploiement d&#8217;une version plus récente et revenir à cette version ancienne,


continuer de servir des sessions en cours.




Evidemment, c&#8217;est la seconde motivation qui est la plus importante.
Et quand toutes les sessions de la version ancienne ont été invalidées, celle-ci peut être retirée.


Tomcat est capable de faire automatiquement cette action, à condition que l&#8217;option undeployOldVersions soit activée sur le host.



&lt;Host name="localhost"  appBase="webapps"
      unpackWARs="true" autoDeploy="true"
      undeployOldVersions="true"&gt;





Problèmes classiques


Si l&#8217;application veut enregistrer des composants JMX, il y a un risque de conflit entre les versions.


En effet, le serveur JMX est au niveau de la JVM et donc commun à toutes les applications et à leurs versions.
Si l&#8217;application veut enregistrer un MBean, elle doit choisir un nom qui n&#8217;entre pas en conflit avec les autres MBeans.
C&#8217;est rarement fait et loin d&#8217;être évident de le faire avec plusieurs instances de la même application.


</description>
          <pubDate>2024-05-01T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Tomcat/ParallelDeployment</link>
          <guid isPermaLink="true">https://www.jtips.info/Tomcat/ParallelDeployment</guid>
        </item>
      
    
      
        <item>
          <title>Minikube</title>
          <description>
DRAFT


Minikube permet de faire fonctionner Kubernetes sur une machine de travail.


L&#8217;environnement Kubernetes est simplifié car il ne tourne que sur un seul noeud.
Il est empaqueté dans un conteneur, avec un réseau dédié.


Minikube est intégré dans Podman Studio.
L&#8217;intégration est légère, elle permet d&#8217;installer Minikube, de créer, démarrer, arrêter et supprimer un cluster Minikube depuis l&#8217;interface graphique.


Ses principaux concurrents sont Kind et K3s.


Images pour Minikube


En local, un cluster K8s peut avoir besoin d&#8217;une image qui n&#8217;est pas poussée dans un registre.


Une image construite simplement sur la machine locale n&#8217;est pas utilisable depuis Minikube car il a son propre démon Docker.
Il y a plusieurs solutions à ce problème.
La plus simple c&#8217;est de construire l&#8217;image via une commande minikube.



minikube image build -t local/my-image



Pour la 2° solution, ll faut construire l&#8217;image en local, avec Docker, puis la charger dans Minikube.



docker build -t local/my-image
minikube image load local/my-image



On peut aussi construire l&#8217;image avec le Docker de Minikube, soit depuis le conteneur (minkube ssh), soit en remote.



eval $(minikube -p minikube docker-env)
docker build -t local/my-image





Dashboard


Le dashboard est une interface graphique pour naviguer dans les ressources d&#8217;un cluster Kubernetes.







Il est intégré à Minikube:



minkube dashboard &amp;





Cycle de vie


Démarrer le cluster, avec le namespace par défaut.



minikube start



Arrêter le cluster



minikube stop



Supprimer le cluster



minikube delete





Autres commandes



minkube ssh




minikube mount $PWD:/host




minikube docker-env



Activer une extension (Ingress ici)



minikube addons enable ingress



</description>
          <pubDate>2024-04-25T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Kubernetes/Minikube</link>
          <guid isPermaLink="true">https://www.jtips.info/Kubernetes/Minikube</guid>
        </item>
      
    
      
        <item>
          <title>Kubernetes</title>
          <description>
DRAFT


Principaux concepts


Configuration

Un ConfigMap est un objet qui contient des paires clé / valeur utilisable par les autres objets.
L&#8217;objectif est d&#8217;isoler les valeurs qui dépendent de l&#8217;environnement.



apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-secret
data:
  POSTGRES_PASSWORD: admin-pwd
  POSTGRES_HOST_AUTH_METHOD: trust



Pour les données sensibles, on utilise plutôt un Secret.



apiVersion: v1
kind: Secret
metadata:
  name: citus-secrets
type: Opaque
data:
  password: cGFzc3dvcmQ=



Le mot de passe est encodé en base 64.


Type de secrets:




Opaque: (défaut)


kubernetes.io/service_account-token: (legacy)


kubernetes.io/dockercfg: pour l&#8217;accès à une registre d&#8217;images, avec un fichier ~/.dockercfg (ancien format)


kubernetes.io/dockerconfigjson: pour l&#8217;accès à une registre d&#8217;images, avec un fichier ~/.docker/config.json (nouveau format)


kubernetes.io/basic-auth


kubernetes.io/ssh-auth


kubernetes.io/tls


bootstrap.kubernetes.io/token




En production, on utilise un stockage externe des secrets.



Volumes

Un PersistentVolume est une ressource du cluster qui définit statiquement un mode de stockage.



apiVersion: v1
kind: PersistentVolume
metadata:
  name: postgres-volume
spec:
  storageClassName: manual
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteMany
  hostPath:
    path: /data/postgresql



En réalité, les pods utilisent de PersistentVolumeClaims comme volumes.
Ces derniers font le lien entre le pod et le persistent volume.


D&#8217;ailleurs, si le persistent volume n&#8217;a pas été déclaré, il est créé dynamiquement.



apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-volume-claim
  labels:
    app: postgres
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Gi



Un conteneur peut aussi monter un répertoire local comme volume.



apiVersion: apps/v1
kind: Deployment
metadata:
  name: citus-master
spec:
  selector:
    matchLabels:
      app: citus-master
  replicas: 1
  template:
    metadata:
      labels:
        app: citus-master
    spec:
      containers:
      - name: citus-master
        image: citusdata/citus:11.1.4-pg14
        ports:
        - containerPort: 5432
        volumeMounts:
        - name: storage
          mountPath: /var/lib/postgresql/data
        - name: init-sql
          mountPath: /docker-entrypoint-initdb.d/init-master.sql
          readOnly: true
      volumes:
        - name: storage
          persistentVolumeClaim:
            claimName: citus-master-pvc
        - name: init-sql
          hostPath:
            path: /host/init-master.sql
            type: FileOrCreate



Pour que ça marche, il faut que le fichier soit présent sur la machine hôte de Kubernetes.
Avec Minikube, il faut monter le répertoire /host.



~$ minikube mount $PWD:/host




Deployments

Un Service définit un moyen d&#8217;exposer une application dans le cluster.
Il définit comment les pods sont accessibles.
L&#8217;ensemble des pods associés dans le service sont définis par le selector.



apiVersion: v1
kind: Service
metadata:
  name: postgres
  labels:
    type: infra
    component: postgres
spec:
  type: NodePort
  ports:
    - port: 5432
  selector:
    run: postgres



Un service peut avoir les types suivants:




ClusterIP: (par défaut) il a une adresse IP interne au cluster, inaccessible de l&#8217;extérieur


NodePort: accessible depuis l&#8217;extérieur, en choisissant le port


LoadBalancer: loadbalancer accessible depuis l&#8217;extérieur


ExternalName: (pas bien compris) utilise un enregistrement DNS de type CNAME





~$ kubectl get services



L&#8217;attribut selector permet de spécifier le label des pods gérés par le service.
Dans l&#8217;exemple ci-dessus, les pods qui ont le label run: postgres seront associés au service.


Un Deployment est l&#8217;object le plus concret.
C&#8217;est à partir de lui que sont créés dynamiquement les conteneurs et les pods.



apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      run: postgres
  template:
    metadata:
      labels:
        run: postgres
    spec:
      containers:
        - name: postgres
          image: 'postgres:14'
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 5432
          envFrom:
            - configMapRef:
                name: postgres-secret
          volumeMounts:
            - mountPath: /var/lib/postgresql/data
              name: postgresdata
      volumes:
        - name: postgresdata
          persistentVolumeClaim:
            claimName: postgres-volume-claim



Ingress Controller relaye le traffic entre l&#8217;extérieur et l&#8217;intérieur du cluster.
Il y a 3 variantes: nginx, traeffik et HAProxy.


Eventuellement, il faut l&#8217;activer manuellement (exemple en local avec Minikube).



~$ minikube addons enable ingress




apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: postgres-ingress
spec:
  rules:
  - host: db.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: postgres-service
            port:
              number: 5432






RBAC


Nécessaire à SBA pour accéder à l&#8217;API



~$ kubectl create clusterrolebinding make-default-sa-cluster-admin \
                      --serviceaccount=default:default
                      --clusterrole=cluster-admin





Troubleshooting



~$ kubectl logs &lt;podname&gt;



Après un restart



~$ kubectl logs &lt;podname&gt; --previous




~$ kubectl describe pods &lt;podname&gt;




~$ kubectl exec &lt;podname&gt; -it -- sh



Pod de diagnostic DNS



apiVersion: v1
kind: Pod
metadata:
  name: dnsutils
  namespace: default
spec:
  containers:
  - name: dnsutils
    image: registry.k8s.io/e2e-test-images/jessie-dnsutils:1.3
    command:
      - sleep
      - "infinity"
    imagePullPolicy: IfNotPresent
  restartPolicy: Always



</description>
          <pubDate>2024-04-25T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Kubernetes/Bases</link>
          <guid isPermaLink="true">https://www.jtips.info/Kubernetes/Bases</guid>
        </item>
      
    
      
        <item>
          <title>Spring Boot Admin</title>
          <description>
Le but de Spring Boot Admin est de centraliser l&#8217;accès aux endpoints /actuator d&#8217;un ensemble d&#8217;applications Spring.
C&#8217;est particulièrement utile pour des déployements en cluster, lorsque des applications sont déployées sur plusieurs instances.


Dans cette situation, il faut identifier chaque instance et faire de la réécriture de requête.
Ça peut se gérer assez facilement dans un environnement à l&#8217;ancienne, avec quelques instances et nginx, ou équivalent, configuré de façon statique.
Par contre, avec un déploiement plus dynamique, les choses se compliquent.
C&#8217;est là que Spring Boot Admin est utile, en collectant les informations des différentes instances et centralisant les accès.







Premiers pas avec Spring Boot Admin


Spring Boot Admin est un projet géré par Johannes Edmeier et son ancien employeur Codecentric.
Et même si le projet est indépendant de Spring, il est intégré au Spring Initializr.







Installation

Pour utiliser Spring Boot Admin, il faut faire sa propre application, avec le starter de.codecentric:spring-boot-admin-starter-server, en activant le serveur avec l&#8217;annotation @EnableAdminServer.



  &lt;dependency&gt;
    &lt;groupId&gt;de.codecentric&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-admin-starter-server&lt;/artifactId&gt;
  &lt;/dependency&gt;




@SpringBootApplication
@EnableAdminServer
public class AdminApplication {

	public static void main(String[] args) {
		SpringApplication.run(AdminApplication.class, args);
	}

}



On va donc pouvoir le personnaliser comme n&#8217;importe quelle application Spring Boot, par exemple pour la sécurité et les restrictions d&#8217;accès.



Fonctionnalités

Spring Boot Admin a des endpoints pour la liste des applications, la liste des instances et relayer des requêtes à ces endpoints.


Il a aussi une interface graphique, qu&#8217;on peut supprimer en excluant la dépendance.



  &lt;dependency&gt;
    &lt;groupId&gt;de.codecentric&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-admin-starter-server&lt;/artifactId&gt;
    &lt;exclusions&gt;
      &lt;exclusion&gt;
        &lt;groupId&gt;de.codecentric&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-admin-server-ui&lt;/artifactId&gt;
      &lt;/exclusion&gt;
    &lt;/exclusions&gt;
  &lt;/dependency&gt;




Configuration

La configuration de notre application d&#8217;admin est similaire à une application classique: port, sécurité, logging,&#8230;&#8203;
Les propriétés propres à Spring Boot Admin sont dans la catégorie spring.boot.admin.


La seule propriété qui doit être configurée concerne CORS.
En effet, sans elle le header origin est transmis entre l&#8217;application d&#8217;admin et le endpoint d&#8217;actuator, ce qui peut causer des erreurs 403 CORS.





Enregistrement des instances


Pour fonctionner, notre application d&#8217;admin doit connaître les instances auxquelles il devra transmettre les requêtes.


Pour les connaître, il y a 3 logiques:




chaque instance s&#8217;enregistre auprès du serveur,


le serveur est configuré pour connaître les instances,


le serveur interroge un registre comme Eureka, Consul ou K8S pour y récupèrer la liste des instances.




Spring Boot Admin client

Dans cette configuration, chaque instance vient s&#8217;enregistrer auprès du serveur SBA.
Pour ça, on doit ajouter la dépendance au client SBA à chaque application.



  &lt;dependency&gt;
      &lt;groupId&gt;de.codecentric&lt;/groupId&gt;
      &lt;artifactId&gt;spring-boot-admin-starter-client&lt;/artifactId&gt;
  &lt;/dependency&gt;



Ensuite on configure l&#8217;URL du serveur SBA auquel elle doit s&#8217;enregistrer.



spring:
  boot:
    admin:
      client:
        url: http://localhost:9999



Ainsi, chaque instance envoie des informations pour que le serveur puisse se connecter, collecter des informations et transmettre les requêtes à l'`/actuator`.


Par défaut, le client envoie des informations de connexion basée sur le canonical hostname en utilisant la méthode InetAddress::geCanonicalHostName.
Le problème classique, c&#8217;est que ce nom peut être un nom privé, impossible à exploiter pour le serveur.
A la place, on peut envoyer l&#8217;adresse IP, avec la propriété spring.boot.admin.client.instance.service-host-type.



spring:
  boot:
    admin:
      client:
        instance
          service-host-type: IP



Si l&#8217;actuator est sécurisé, le client doit envoyer les informations au serveur, dans les métadonnées de l&#8217;instance (spring.boot.admin.client.instance.metadata).



spring:
  boot:
    admin:
      client:
        url: http://localhost:9999
        instance:
          metadata:
            user.name: monitor
            user.password: monitor-pwd



Si le serveur est sécurisé, le client doit avoir les informations d&#8217;authentification.



spring:
  boot:
    admin:
      client:
        url: http://localhost:9999
        username: admin
        password: admin-pwd



Synthèse: configuration complète du client SBA



spring:
  boot:
    admin:
      client:
        url: http://localhost:9999
        username: admin
        password: admin-pwd
        instance:
          service-host-type: IP
          metadata:
            user.name: monitor
            user.password: monitor-pwd




Découverte statique

Avec cette solution, la liste des instances et leurs données de connexion est directement dans la configuration du serveur.
Cette solution est simple à implémenter mais a l&#8217;inconvénient d&#8217;être purement statique, et donc contraire aux objectifs.


Même pour cette solution statique, il faut que le serveur ait une dépendance vers Spring Cloud.



  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
    &lt;artifactId&gt;spring-cloud-starter&lt;/artifactId&gt;
  &lt;/dependency&gt;



Il n&#8217;y a rien à configurer coté instance.
Du coté du serveur, il faut ajouter les informations de connexion, avec éventuellement des informations d&#8217;authentification.



spring:
  cloud:
    discovery:
      client:
        simple:
          instances:
            api:
              - uri: https://api1.example.com
                metadata:
                  user.name: monitor
                  user.password: monitor-pwd
              - uri: https://api2.example.com




Découverte Kubernetes

Si l&#8217;application est déployée sur un Kubernetes, Spring est capable de découvrir la topologie du cluster grâce à l&#8217;API de Kubernetes.


Pour exploiter cette possibilité, il faut que le serveur SBA ait une dépendance vers le service de découverte pour Kubernetes.



  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
    &lt;artifactId&gt;spring-cloud-starter-kubernetes-all&lt;/artifactId&gt;
  &lt;/dependency&gt;



L&#8217;ajout du starter suffit à activer la découverte.
Il faut toutefois que les permissions soient activées au niveau de l&#8217;API Kubernetes
(commande testée avec Minikube, sur le namespace par défaut).



~$ kubectl create clusterrolebinding make-default-sa-cluster-admin \
           --serviceaccount=default:default --clusterrole=cluster-admin



Il faut aussi activer le scheduling pour que la recherche des instances se fasse à intervalle régulier.



@SpringBootApplication
@EnableAdminServer
@EnableScheduling
public class AdminApplication {

	public static void main(String[] args) {
		SpringApplication.run(AdminApplication.class, args);
	}

}



Enfin, il faut filtrer les services par label afin de ne récupérer que les nodes Spring qui ont un actuator.



spring:
  cloud:
    kubernetes:
      discovery:
        service-labels:
          category: application



Si les actuateurs sont sécurisés, on peut renseigner le nom d&#8217;utilisateur et le mot de passe au niveau global, pour toutes les instances.



spring:
  boot:
    admin:
      instance-auth:
        default-user-name: monitor
        default-password: monitor-pwd



La configuration spécifique d&#8217;une instance est prioritaire par rapport à cette configuration globale.
Pour ça, il faut ajouter les informations au niveau des labels du service Kubernetes.



apiVersion: v1
kind: Service
metadata:
  name: app1
  labels:
    category: application
    user.name: monitor
    user.password: monitor-psswd



Attention, il faut bien mettre user.name ET user.password.
Avec un seul des deux, il sera ignoré.





API


Je n&#8217;ai pas trouvé de documentation de l&#8217;API.
On peut la découvrir en parcourant l&#8217;interface graphique ou en parcourant le code.








J&#8217;ai essayé d&#8217;ajouter springdoc, mais il ne détecte pas automatiquement les endpoints.





Applications



GET {{admin.url}}/applications
liste des applications avec la liste des instances par application


GET {{admin.url}}/applications/{name}
détail d&#8217;une application, avec la liste de ses instances


XXX {{admin.url}}/applications/{name}/actuator/{endpoint}
endpoint de l&#8217;actuator d&#8217;une application, la requête est envoyée à chaque instance et la réponse aggrège les différentes réponses





Instances



GET {{admin.url}}/instances
liste des instances


GET {{admin.url}}/instances?name={name}
liste des instances, peut être limitée à une seule application avec le paramètre name


GET {{admin.url}}/instances/{id}
détail d&#8217;une instance





Actuators

Le serveur SBA fonctionne en relais entre le client et l&#8217;actuateur.
Par conséquents les endpoints disponibles dépendent des endpoints activés pour chaque application et instance.


Les endpoints sont généralement appelés pour une instance.




XXX {{admin.url}}/instances/{id}/actuator/{endpoint}
endpoint de l&#8217;actuator d&#8217;une instance, quelle que soit la méthode GET, POST,&#8230;&#8203;; la racine n&#8217;est pas accessible




Exemple pour /loggers




GET {{admin.url}}/instances/{id}/actuator/loggers
liste et configuration des loggers de l&#8217;instance


GET {{admin.url}}/instances/{id}/actuator/loggers/{logger.name}
configuration d&#8217;un' logger de l&#8217;instance


POST {{admin.url}}/instances/{id}/actuator/loggers/{logger.name}
modification de la configuration d&#8217;un' logger de l&#8217;instance




Il y a aussi la possibilité d&#8217;appeler un endpoint pour une application.
Dans ce cas, la requête est transmise à chaque instance et la réponse est aggrégée.




XXX {{admin.url}}/applications/{name}/actuator/{endpoint}
endpoint de l&#8217;actuator d&#8217;une application




Exemple pour /loggers




POST {{admin.url}}/applications/{name}/actuator/loggers/{logger.name}
modification de la configuration d&#8217;un' logger pour toutes les instances de l&#8217;application







Tips


CORS

Le header Origin est transmis à l&#8217;instance, ce qui peut poser des erreurs CORS sur les POST


Il y a 2 solutions:




ajouter SBA dans les URLs autorisées par l&#8217;instance,


empécher le transfert du header Origin.




Je préfère largement la 2°, d&#8217;autant qu&#8217;elle est assez simple à mettre en oeuvre, coté SBA, avec la propriété spring.boot.admin.instance-proxy.ignored-headers.



spring:
  boot:
    admin:
      instance-proxy:
        ignored-headers: Cookie, Set-Cookie, Authorization, Origin



La valeur par défaut de cette propriété est Cookie, Set-Cookie, Authorization, j&#8217;y ajoute Origin.


Il faut noter que les headers suivants sont ignorés quelle que soit la configuration, car rangés dans la catégorie hop_by_hop: "Host", "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", "Trailer", "Transfer-Encoding", "Upgrade", "X-Application-Context".



oauth2

L&#8217;authentification par oauth2 n&#8217;est que partiellement supportée.


SBA peut être sécurisé en utilisant une authentification oauth2 avec une validation des token par JWKS.


Par contre pe n&#8217;ai rien trouvé pour l&#8217;interface graphique concernant le support d&#8217;authentification oauth2 et de token.
De plus, en enregistrement par le client il faut un autre mode.


De même pour la sécurisation des actuators, si on utilise oauth2, il faut aussi un mode annexe car SBA vient interroger périodiquement les endpoints de l&#8217;actuator, indépendamment de toute requête d&#8217;utilisateur.



ID d&#8217;instances

Les IDs des instances survivent au redémarrage de SBA.
Ils sont calculés à partir de l&#8217;URL du service.
C&#8217;est le SHA-1 de de l&#8217;URL de health, en hexadécimal.



</description>
          <pubDate>2024-04-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/Admin</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/Admin</guid>
        </item>
      
    
      
        <item>
          <title>Principaux types d&apos;enregistrements DNS</title>
          <description>
L&#8217;organisation utilisée ici reflète celle d&#8217;OVH (en avril 2024).







Pointer records




A est l&#8217;enregistrement classique.
Il associe un nom de domaine à une adresse IPv4.





dl IN A 213.186.33.2





AAAA est comme un enregistrement A, mais en plus moderne.
Il associe un nom de domaine à une adresse IPv6.





dl IN AAAA 2001:41d0:1:1b00:213:186:33:2





CNAME est un enregistrement d&#8217;alias.
Il associe un nom de domaine à un autre nom de domaine.





pz IN CNAME prez.sewatech.fr.





DNAME lui ressemble, mais en plus large.
Il associe les sous-domaines d&#8217;un nom aux sous-domaines d&#8217;un autre nom.


NS enregistre les serveurs DNS du serveur.






Extended records


Parmi les types d&#8217;enregistrement étendus, je n&#8217;utilise que le TXT.
Pour les autres, je n&#8217;en vois aucun usage à court terme.




TXT enregistre une information non structurée. 
Il peut être utilisé par des prestataires de service pour vérifier la propriété d&#8217;un domaine, comme pour les vérifications SPF, DKIM ou DMARC. OVH l&#8217;utilise aussi pour faire de la redirection de domaine.





pz IN TXT "1|download.sewatech.fr."





CAA sert pour la certification d&#8217;un domaine.


NAPTR permet d&#8217;associer un numéro de téléphone à un nom de domaine.


SRV associe un service, comme SIP ou XMPP, à un serveur.


LOC donne des informations de localisation géographique, avec longitude, latitude et altitude.


SSHFP est un enregistrement de clés publiques.


TLSA est utilisé dans la cadre du protocole DANE (DNS - based Authentication of Named Entities), que je ne connais pas du tout.






Mail records




Un enregistrement MX associe un nom à l&#8217;adresse IP d&#8217;un serveur de courriel.





IN MX 10 mail.protonmail.ch.
IN MX 20 mailsec.protonmail.ch.





SPF, DKIM et DMARC sont proposés par OVH comme des enregistrements DNS, mais c&#8217;est du TXT qui est généré.





IN TXT "v=spf1 include:_spf.protonmail.ch ~all"
_dmarc IN TXT "v=DMARC1; p=quarantine"



</description>
          <pubDate>2024-04-19T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Network/DNS-Record</link>
          <guid isPermaLink="true">https://www.jtips.info/Network/DNS-Record</guid>
        </item>
      
    
      
        <item>
          <title>Management API de RabbitMQ</title>
          <description>
L&#8217;API HTTP de management permet d&#8217;une part de créer, modifier des entités (exchanges, queues) de RabbitMQ, mais aussi de lire ou publier des messages.


Cette page traite très partiellement le sujet.
Il y aura peut-être plus de contenu plus tard&#8230;&#8203;


Installation


Pour activer l&#8217;API de management, il faut le même plugin que pour la console.



$~ rabbitmq-plugins enable --offline rabbitmq_management



En local, la racine de l&#8217;API est http://localhost:15672/api.


Il faut aussi un utilisateur avec un tag management ou administrator.



{
  "name": "test",
  "password_hash": "...",
  "tags": ["management"]
}



L&#8217;utilisateur doit avoir les permissions sur les entités, configure pour les créer ou modifier, read/write pour échanger des messages.



{
  "user": "test",
  "vhost": "/",
  "read": ".",
  "write": "."
}



La méthode d&#8217;authentification est BASIC.




Manipulation d&#8217;entités


Vue globale


$~ curl http://localhost:15672/api/overview

{
  "management_version": "3.12.2",
  "rates_mode": "basic",
  "sample_retention_policies": {...},
  "exchange_types": [
      {
          "name": "direct",
          "description": "AMQP direct exchange, as per the AMQP specification",
          "enabled": true
      },
      ...
  ],
  "product_version": "3.12.2",
  "product_name": "RabbitMQ",
  "rabbitmq_version": "3.12.2",
  "cluster_name": "rabbitmq",
  ...
}




Connexions

Liste de toutes les connexions



$~ curl http://localhost:15672/api/connexions

[
  {
    "auth_mechanism": "PLAIN",
    "channel_max": 2047,
    "channels": 60,
    "client_properties": {
      "capabilities": {
        "authentication_failure_close": true,
        "basic.nack": true,
        "connection.blocked": true,
        "consumer_cancel_notify": true,
        "exchange_exchange_bindings": true,
        "publisher_confirms": true
      },
      "connection_name": "rabbitConnectionFactory#3fe3bbff:0",
      "copyright": "Copyright (c) 2007-2021 VMware, Inc. or its affiliates.",
      "information": "Licensed under the MPL. See https://www.rabbitmq.com/",
      "platform": "Java",
      "product": "RabbitMQ",
      "version": "5.19.0"
    },
    "connected_at": 1717690184514,
    "host": "172.27.4.148",
    "port": 5672,
    "name": "172.27.4.146:42240 -&gt; 172.27.4.148:5672",
    "peer_host": "172.27.4.146",
    "peer_port": 42240,
    "protocol": "AMQP 0-9-1",
    "user": "datamgmt-dev",
    "vhost": "/dev",
    ...
  },
  ...
]




Canaux

Liste de tous les canaux



$~ curl http://localhost:15672/api/channels

[
  {
    "name": "172.27.4.146:42240 -&gt; 172.27.4.148:5672 (1)",
    "connection_details": {
        "name": "172.27.4.146:42240 -&gt; 172.27.4.148:5672",
        "peer_host": "172.27.4.146",
        "peer_port": 42240
    },
    "consumer_count": 1,
    "idle_since": "2024-06-06T16:10:03.006+00:00",
    "state": "running",
    "vhost": "/dev",
    ...
  },
  ...
]



Liste des canaux d&#8217;une connexion



$~ curl http://localhost:15672/api/connections/&lt;name&gt;/channels






Envoi de message


L&#8217;envoi d&#8217;un message se fait par un POST sur le endpoint /api/exchanges/&lt;vhost&gt;/&lt;echange&gt;/publish/.


Par exemple, pour envoyer un message sur l&#8217;exchange par défaut (nom vide) du virtual host /, en local:



$~ curl http://localhost:15672/api/exchanges/%2f//publish   \
        --user test:test-pwd --request POST                 \
        --data '...'



Le contenu du POST est en JSON.
Il a quatre propriétés:




properties, dont les en-têtes


routing_key


payload


payload_encoding qui peut être string pou base64




Avec un payload texte, si c&#8217;est du JSON, ça fait du JinJ (JSON in JSON) avec des échappements de doubles-quotes.



$~ payload='{"id": "3172fc96-4786-4146-ad80-4c993f64db30", "description": "Hello World"}'
$~ curl http://localhost:15672/api/exchanges/%2f//publish   \
     --user test:test-pwd --request POST                 \
     --data @- &lt;&lt;EOF
{
  "properties":{
    "headers": {
      "event-type": "hello"
    }
  },
  "routing_key":"q.jtips",
  "payload":"${payload//\"/\\\"}",
  "payload_encoding":"string"
}
EOF



Sinon, on peut opter pour un payload base64.



$~ payload='{"id": "3172fc96-4786-4146-ad80-4c993f64db30", "description": "Hello World"}'
$~ payload64=$(echo $payload | base64)
$~ curl http://localhost:15672/api/exchanges/%2f//publish   \
        --user test:test-pwd --request POST                 \
        --data @- &lt;&lt;EOF
{
  "properties":{
    "headers": {
      "event-type": "hello"
    }
  },
  "routing_key":"q.ah.test",
  "payload":"$payload64",
  "payload_encoding":"base64"
}
EOF



</description>
          <pubDate>2023-12-07T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/RabbitMQ/API</link>
          <guid isPermaLink="true">https://www.jtips.info/RabbitMQ/API</guid>
        </item>
      
    
      
        <item>
          <title>Application Spring derrière un reverse proxy</title>
          <description>
Lorsqu&#8217;on déploie une application derrière un reverse proxy, l&#8217;application risque de publier des URLs non confirmes.
En effet, Spring contruit des URLs et pour ça il utilise le hostname privé alors qu&#8217;il devrait utiliser le hostname public, c&#8217;est-à-dire celui utilisé par le client.


Dans une architecture avec un reverse proxy, celui-ci reçoit les requêtes HTTP et les transfère vers l&#8217;application Spring.
De façon classique, le reverse proxy ajoute des headers X-Forwarded-Xxx que Spring peut utiliser.


RemoteIpValve


Il peut le faire de deux façons, soit en le gérant lui-même, soit en le délégant à Tomcat avec la RemoteIpValve.
Ce choix se fait avec la propriété server.forward-headers-strategy.


Pour activer la valve RemoteIpValve de Tomcat:



server.forward-headers-strategy=NATIVE





ForwardedHeaderFilter


Pour utiliser le filtre ForwardedHeaderFilter de Spring Framework:



server.forward-headers-strategy=FRAMEWORK



Ce dernier supporte à la fois les classiques X-Forwarded-Xxx, mais aussi le header Forwarded de la RFC-7239.




Références




RFC-7239, Forwarded HTTP Extension




</description>
          <pubDate>2023-11-27T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/ReverseProxy</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/ReverseProxy</guid>
        </item>
      
    
      
        <item>
          <title>RabbitMQ Streams</title>
          <description>
Dans RabbitMQ, les streams sont des queues conçues pour les objectifs suivants:




performances, avec un débit optimisé,


persistance, avec un stockage optimisé pour un grand nombre de messages et pour permettre le rejeu des messages,


mode mixte entre abonnement et consommation.




Avec ces caractéristiques, ça s&#8217;éloigne d&#8217;AMQP 0.9 et ça peut ressembler un peu à Kafka.


On peut utiliser des stream powered queues avec le protocole AMQP.
Mais pour profiter du débit optimisé, il faut utiliser le protocole dédié, et pour ça il faut activer le plug-in et utiliser un client spécifique.


Stream powered queues


Une telle queue se construit de façon classique, avec un argument "x-queue-type"="stream"
Ça impose quelques contraintes car une stream powered queue est forcément durable, non exclusive et sans suppression automatique.



  "queues":[
    {
      "name": "s.activity",
      "vhost": "/jtips",
      "durable": true,
      "auto_delete": false,
      "exclusive": false,
      "arguments": {
        "x-queue-type": "stream"
      }
    }
  ]



Ou avec le client AMQP



channel.queueDeclare(
    "s.activity",
    true,         // durable
    false, false, // not exclusive, not auto-delete
    Map.of("x-queue-type", "stream")
);



Cette queue peut être attachée à n&#8217;importe quel exchange, comme une queue classique.
Ses messages peuvent aussi être consommés par n&#8217;importe quel client AMQP.




Stockage et rétention


Avec une queue classique, une fois qu&#8217;un message est consommé, il est supprimé du stockage.
Avec une queue orientée stream, le message reste dans le stockage.
Tous les messages, qu&#8217;ils soient consommés ou pas restent dans le stockage dans les limites de la configuration de rétention de la queue.


Sans configuration de rétention, le stockage peut grossir jusqu&#8217;à saturation du disque.
On peut configurer




la taille maximale de la queue avec x-max-length-bytes,


l&#8217;age maximun des messages avec x-max-age,





  "queues":[
    {
      "name": "s.activity",
      "vhost": "/jtips",
      "durable": true,
      "auto_delete": false,
      "exclusive": false,
      "arguments": {
        "x-queue-type": "stream",
        "x-max-age": "3M",
        "x-max-length-bytes:": "100000000000"
      }
    }
  ]



Ou avec le client AMQP



channel.queueDeclare(
    "s.activity",
    true,         // durable
    false, false, // not exclusive, not auto-delete
    Map.of(
        "x-queue-type", "stream",
        "x-max-age", "6h",
        "x-max-length-bytes:", 10_000_000_000
    )
);



Ces paramètres de rétention sont les seuls utilisés.
Ça signifie, qu&#8217;on ne peut pas choisir sur chaque message s&#8217;il est durable et son TTL, contrairement aux messages classiques.




Protocole et librairie cliente


Pour bénéficier d&#8217;un débit optimal, il faut accéder aux queues via le protocole de stream, au lieu d&#8217;AMQP.
Au lieu de 5672, il utilise le port 5552, qui est ouvert et pris en charge par le plugin stream.



$&gt; rabbitmq-plugins enable rabbitmq_stream



Et pour utiliser ce protocole, les clients doivent utiliser des librairies dédiées.



  &lt;dependency&gt;
    &lt;groupId&gt;com.rabbitmq&lt;/groupId&gt;
    &lt;artifactId&gt;stream-client&lt;/artifactId&gt;
    &lt;version&gt;0.13.0&lt;/version&gt;
  &lt;/dependency&gt;



L&#8217;API est complètement différente de celle d&#8217;AMQP.
On crée directement un stream, qui une sorte de queue, et il n&#8217;y a plus d'exchange.



Environment environment = Environment.builder().build();
environment.streamCreator()
    .stream("s.activity")
    .maxAge(Duration.ofHours(6))
    .maxLengthBytes(ByteCapacity.GB(10))
    .create();



On connecte les producteurs et consommateurs au stream.



Producer producer = environment.producerBuilder()
        .stream("s.activity")
        .build();

producer.messageBuilder()
        .addData(data.toJson().asBytes())
        .build()




Consumer consumer = environment.consumerBuilder()
        .stream("s.activity")
        .messageHandler((context, message) -&gt; {
          ...
        })
        .build();



Le protocole a été conçu pour fonctionner en cluster, avec un noeud principal et des noeuds répliqués.
Lorsqu&#8217;un producteur se connecte, il est rattaché au noeud principal, alors que les consommateurs sont connectés aux noeuds répliqués.
Pour qu&#8217;il soit accessible, chaque noeud transmet son hostname.
Comme souvent dans le cloud, ou avec des conteneurs, le nom utilisé peut être privé et ne permet pas d&#8217;établir la connexion.
On peut contourner ce problème en configurant RabbitMQ ou en réécrivant les adresses coté client.


Coté broker, on peut configurer la propriété stream.advertised_host.
Par exemple, en environnement de développement, j&#8217;utilise un noeud RabbitMQ dans un conteneur, avec la configuration ci-dessous.



# stream.conf
stream.advertised_host = localhost



La variable d&#8217;environnement RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS peut aussi être utilisée au lancement de RabbitMQ.



$&gt; docker run --publish 5552:5552                                                               \
      --env RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS='-rabbitmq_stream advertised_host localhost'    \
      rabbitmq:3.9



Coté client, on peut ajouter un AddressResolver à l&#8217;instance d'Environment.



Address endpoint = new Address("localhost", 5552);
environment = Environment.builder()
        .host(endpoint.host())
        .port(endpoint.port())
        .addressResolver(address -&gt; endpoint)
        ...
        .build();





Rejeu et consommations multiples


Le rejeu (ou replay) est possible parce que les messages ne sont pas supprimés à la consommation.
Quand un message est ajouté dans une queue, il est numéroté de façon séquentiel.


Au démarrage, le consommateur peut juste attendre le prochain message.
Il peut aussi demander de démarrer à un index plus ancien, ou depuis le début.


Ce fonctionnement permet aussi à plusieurs consomateurs de se brancher sur la même queue et de lire les mêmes messages.
Chaque consommateur peut s&#8217;attacher à n&#8217;importe quel offset.



Consumer consumer = environment.consumerBuilder()
        .name(clientId)
        .stream("s.activity")
        .offset(OffsetSpecification.last())
        .messageHandler((context, message) -&gt; {
          ...
        })
        .build();





OffsetSpecification



none(): OffsetSpecification


first(): OffsetSpecification


offset(long offset): OffsetSpecification


timestamp(long timestamp): OffsetSpecification


last(): OffsetSpecification


next(): OffsetSpecification







Les messages consommés au démarrage dépendent de l'OffsetSpecification passé au builder.




none(): aucun message, c&#8217;est la valeur par défaut
La consommation ressemble à celle d&#8217;un topic JMS.
Au démarrage, aucun message n&#8217;est consommé et par la suite, tous les consommateurs reçoivent les messages.


first(): tous les messages stockés
Le consommateur reçoit tous les messages depuis le plus ancien qui est stocké.


offset(&#8230;&#8203;): les messages à partir de l&#8217;offset passé en paramètre
On choisit le n° du message à partir duquel on veut démarrer.
L&#8217;offset du message consommé peut être lu sur le contexte.





Consumer consumer = environment.consumerBuilder()
        .offset(OffsetSpecification.offset(storedOffset))
        .messageHandler((context, message) -&gt; {
              long offset = context.offset();
        })
        ...





timestamp(&#8230;&#8203;): les messages à partir du timestamp passé en paramètre
On choisit le timestamp à partir duquel on veut démarrer.





Consumer consumer = environment.consumerBuilder()
        .offset(OffsetSpecification.timestamp(Instant.now().minus(1, ChronoUnit.HOURS).toEpochMilli()))
        ...





last(): le dernier message publié
Le consommateur reçoit le dernier message enregistré, sauf s&#8217;il a déjà été consommé et que le broker a enregistré cette consommation.
Cet enregistrement est lié au tracking, comme pour la spécification next().


next(): les messages à partir de l&#8217;offset enregistré par le broker
La consommation ressemble à celle d&#8217;un topic JMS durable.
L&#8217;enregistrement de l'offset dépend du tracking.
Et sans tracking, next() est équivalent à none().




Avec le tracking, l'offset de chaque consommateur est sauvegardé par le broker.
Ainsi lors d&#8217;un redémarrage, en spécification next(), le consommateur continue de consommer les messages là où il en était.


Le premier prérequis pour utiliser le tracking, c&#8217;est que le consommateur ait un nom, et qu&#8217;il porte le même nom au redémarrage.
Ensuite, on peut utiliser du tracking automatique ou manuel. A partir du moment où il y a un nom, le tracking par défaut est automatique.



consumer = environment.consumerBuilder()
        .name(clientId)
        .stream("s.activity")
        .autoTrackingStrategy().builder()
        .offset(OffsetSpecification.next())
        ...



Le broker stocke ces informations dans la queue, au milieu des messages.
De ce fait, stocker l&#8217;index à chaque message pourrait avoir un impact non négligeable sur le volume de stockage et sur les performances.
La documentation de RabbitMQ préconise de le stocker tous les quelques milliers de messages.
Par défaut, c&#8217;est tous les 10 000 messages, ou toutes les 5 secondes.



consumer = environment.consumerBuilder()
        .name(clientId)
        .stream("s.activity")
        .autoTrackingStrategy()
            .messageCountBeforeStorage(10_000)
            .flushInterval(Duration.ofSeconds(5))
            .builder()
        .offset(OffsetSpecification.next())
        ...



En tracking manuel, le stockage de l&#8217;offset se fait explicitement.



consumer = environment.consumerBuilder()
        .name(clientId)
        .stream("s.activity")
        .manualTrackingStrategy().builder()
        .offset(OffsetSpecification.next()
        .messageHandler((context, message) -&gt; {
            context.storeOffset();
        })
        ...



Le choix du mode de démarrage est compatible avec un client AMQP.
Par contre, le tracking ne l&#8217;est pas.


Pour ça, on peut lire le header x-stream-offset sur les messages reçu, et on doit ajouter un argument du même nom au consommateur.



channel.basicConsume(
        "s.activity", false,
        Map.of("x-stream-offset", lastOffset),
        (consumerTag, message) -&gt;
            storeLocallyLastOffset(message.getProperties().getHeaders().get("x-stream-offset")),
        (consumerTag, message) -&gt; {});



Les valeurs de l&#8217;argument ressemblent à celles de OffsetSpecification.
On peut mettre une valeur textuelle :




"first" pour recevoir tous les messages,


"last" pour recevoir le dernier message, qu&#8217;il ait déjà été reçu ou pas puisqu&#8217;il n&#8217;y a pas de tracking,


"next" pour ne rien recevoir,


un intervalle, avec une valeur et une unité (Y, M, D, h, m, s) comme par exemple "6h".




Pour choisir l'offset de départ, on passe une valeur mumérique.
Pour le timestamp, on passe une instance de Date.




Single Active Consumer


Plus haut, je disais que qu&#8217;un stream fonctionnait comme un topic JMS et que les messages étaient consommés par tous les messages.
C&#8217;est généralement vrai, mais pas toujours.


Avec la fonctionnalité de Single Active Consumer, plusieurs consommateurs s&#8217;abonnent au même stream et un seul est actif.
Il reçoit les messages alors que les autres sont inactifs et ne reçoivent rien.
Un consommateur inactif devient actif lorsque son collègue précédemment actif s&#8217;arrête, ainsi ça permet une continuité dans la consommation.


Pour que ça fonctionne, tous les consommateurs doivent avoir le même nom et activer la fonctionnalité.



consumer = environment.consumerBuilder()
        .name(clientId)
        .stream("s.activity")
        .singleActiveConsumer()
        ...



Ce mode annule le choix la spécification d'offset.
Au démarrage, le consommateur se comporte forcément comme avec next(), d&#8217;après mes essais.


D&#8217;autres consommateurs avec des noms différents recevront aussi le message.
Ainsi on peut faire des grappes de single active consumers.




Avec Spring


Spring propose une librairie "Spring Cloud Stream" pour utiliser Kafka ou RabbitMQ comme brique d&#8217;intégration entre micro-services.
Malgré son nom, par défaut elle fait de l&#8217;AMQP avec RabbitMQ, et le support de RabbitMQ stream n&#8217;est qu&#8217;une option secondaire.


Par ailleurs, dans Spring AMQP, il y a un RabbitStreamTemplate et un StreamListenerContainer.



&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.amqp&lt;/groupId&gt;
  &lt;artifactId&gt;spring-rabbit-stream&lt;/artifactId&gt;
  &lt;version&gt;3.0.9&lt;/version&gt;
&lt;/dependency&gt;





Synthèse


Les fonctionnalités suivantes sont accessibles aux clients AMQP et Stream, sans obligation d&#8217;avoir le plugin stream.




Création de stream powered queues


Distribution des messages à plusieurs consommateurs, à la façon des topics JMS (Large fan-outs)


Choix de la position de démarrage dans l&#8217;historique des messages (Replay, Time-travelling)




Les fonctionnalités suivantes ne sont accessibles qu&#8217;aux clients Stream et ont besoin du plugin stream.




Optimisation des performances, grâce au débit optimisé


Sauvegarde de la position de lecture (Tracking)


Single Active Consumer


Envoi direct de message, sans passer par un exchange




</description>
          <pubDate>2023-10-07T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/RabbitMQ/Stream</link>
          <guid isPermaLink="true">https://www.jtips.info/RabbitMQ/Stream</guid>
        </item>
      
    
      
        <item>
          <title>Profils avec Quarkus</title>
          <description>
La notion de profile ressemble à celle de Spring Framework, avec quelques différences inhérentes au fonctionnement de Quarkus.


Malgré les différences, on peut reprendre le même exemple courant avec la datasource qui peut être différente entre les environnements de développement, de test et de déploiement.


Configuration


Propriété système quarkus.profile



java -Dquarkus.profile=stage org.example.Main



En properties ou yaml, préfixe %dev.
En variable d&#8217;environnement, préfixe DEV


Profils prédéfinis : dev, prod, test




Prog


ConfigUtils.getProfiles()


On trouve encore ProfileManager.getLaunchMode(), mais il est déprécié.




Profils par défaut


Il dépend du mode de démarrage.




Profils de test


Par défaut, une classe de test annotée par @QuarkusTest démarre avec le profil test.
Ainsi on peut redéfinir certaines propriétés dans le fichier YAML.



# application.yml
...
"%test":
  quarkus:
    http:
      auth:
        permission:
          webhook:
            paths: /webhook, /token



Le profil de test peut être modifié avec la propriété système quarkus.test.profile.


On peut aussi choisir un profil pour chaque classe de test avec l&#8217;annotation @TestProfile et une classe de spécification du profil.
Ça ressemble à Spring, mais en plus typé.



public class IntegrationTestProfile implements QuarkusTestProfile {

    @Override
    public String getConfigProfile() {
        return "integration-test";
    }

}



Ce profil pourra être utilisé dans les tests d&#8217;intégration.



@QuarkusTest
@TestProfile(IntegrationTestProfile.class)
class SomeIT {

    @Test
    void something_should_work() {
      ...
    }

}



La classe peut aussi redéfinir des propriétés et quelques autres informations.



public class IntegrationTestProfile implements QuarkusTestProfile {

    ...

    @Override
    public Map&lt;String, String&gt; getConfigOverrides() {
        return Map.of("quarkus.http.auth.permission.webhook.paths", "/webhook,/token");
    }


}



</description>
          <pubDate>2023-06-30T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Quarkus/Profile</link>
          <guid isPermaLink="true">https://www.jtips.info/Quarkus/Profile</guid>
        </item>
      
    
      
        <item>
          <title>Queues dans RabbitMQ</title>
          <description>
RabbitMQ est construit sur AMQP 0.9.
Il implémente la notion de queue conformément à cette spécification.


Une queue sert à stocker les messages en vu d&#8217;être délivrés à un consommateur.


Caractéristiques


Une queue a les caractéristiques suivantes :




name, unique dans le virtual host,


durable, elle survit au redémarrage du serveur,


exclusive, elle est attachée à une seule connexion dont la fermeture la supprime,


auto-delete, elle est supprimée quand le dernier consommateur se désabonne.




Les autres paramètres doivent être fixé grâce aux x-arguments.




x-queue-type: classique (vide), quorum ou stream,


x-message-ttl, durée de vie des messages s&#8217;il n&#8217;est pas redéfini au niveau du message,


x-max-length et x-max-length-bytes, taille limite de la queue, en nombre de messages et en taille,


x-max-priority, priorité maximale des messages.






Définition



  "queues": [
    {
      "name": "q.activity",
      "vhost": "/jtips",
      "type": "stream",
      "arguments": {
        "x-queue-type": "stream",
        "x-message-ttl": 60000,
        "x-max-length": 1000
      },
      "durable": true,
      "auto_delete": false,
      "exclusive": false,
    }
  ]





Client Java


Déclaration d&#8217;une queue

Habituellement, on déclare une queue avec toutes ses caractéristiques.



channel.queueDeclare(
    "q.activity",
    true,         // durable
    false,        // not exclusive,
    false,        // not auto-delete
    Map.of()      // no argument
);



Dans certaines conditions, on déclare une queue sans paramètre.
Dans ce cas, elle est nommée par le broker, et on récupère son nom.



String queueName = channel.queueDeclare().getQueue();




Utilisation d&#8217;une queue

En AMQP 0.9, utiliser une queue signifie lire ses messages.


Ça peut se faire de façon synchrone par basic.get.



Channel channel = connectionFactory
                      .createConnection()
                      .createChannel();
channel.basicGet(queueName, true); // auto-ack mode



On préfère généralement le faire de façon asynchrone, sous forme d&#8217;abonnement, par basic.consume.
Cette façon de faire permet en plus de passer des paramètres.



Channel channel = connectionFactory
                      .createConnection()
                      .createChannel(false);
channel.basicConsume(
    queueName, true, // auto-ack mode
    new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(
              String consumerTag,
              Envelope envelope,
              AMQP.BasicProperties properties,
              byte[] body) {
            ...
        }
    });






Spring AMQP


Déclaration d&#8217;une queue


@Bean
public class MessageService {
  private final AmqpAdmin admin;

  public void createQueue(String name) {
    // Declare queue:
    // - creation if needed,
    // - nothing if already exists,
    // - error 406 if not compliant with existing
    admin.declareQueue(
        QueueBuilder.durable(name)
                    .ttl(60_000)
                    .build());
  }
}




Utilisation d&#8217;une queue


public class MessageService {
  private RabbitTemplate rabbitTemplate;

  public void send(String name, String key, String message)
    rabbitTemplate.setExchange(name);
    rabbitTemplate.setRoutingKey(key);
    rabbitTemplate.convertAndSend(message);
  }

  // ou

  @Bean
  public Consumer&lt;String&gt; consumer(RabbitTemplate rabbitTemplate) {
    return message
        -&gt; log.info("Received '{}' on queue {}", message, name));
  }
}




</description>
          <pubDate>2023-06-05T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/RabbitMQ/Queue</link>
          <guid isPermaLink="true">https://www.jtips.info/RabbitMQ/Queue</guid>
        </item>
      
    
      
        <item>
          <title>Authentification et autorisations avec OAuth2 pour RabbitMQ</title>
          <description>
En remplacement ou en complément du backend d&#8217;authentification classique, on peut activer le backend d&#8217;authentification OAuth2.
Dans la pratique, il s&#8217;agit plutôt d&#8217;authentification et d&#8217;autorisation basées sur des tokens JWT.


Configuration


L&#8217;activation du backend se fait dans la configuration de RabbitMQ.
Il peut être déclaré seul ou en complément du backend interne.



# conf.d/21-auth.conf
auth_backends.1 = rabbit_auth_backend_oauth2
auth_backends.2 = rabbit_auth_backend_internal



Les tokens doivent répondre à certaines contraintes, en particulier ils doivent être signés.
RabbitMQ doit être configuré de façon à pouvoir valider la signature.


Dans l&#8217;exemple ci-dessous, on a une signature HS256 (symétrique) pour laquelle on doit renseigner le secret partagé.



# advanced.config
[
  {rabbitmq_auth_backend_oauth2, [
    {key_config, [
      {default_key, &lt;&lt;"main-key"&gt;&gt;},
      {signing_keys, #{
        &lt;&lt;"main-key"&gt;&gt; =&gt;
          {map, #{
              &lt;&lt;"alg"&gt;&gt; =&gt; &lt;&lt;"HS256"&gt;&gt;,
              &lt;&lt;"value"&gt;&gt; =&gt; &lt;&lt;"459af6c584f776a72ba748d7944082edae6c42d3f22ada2c7270d3bae275c187"&gt;&gt;,
              &lt;&lt;"kty"&gt;&gt; =&gt; &lt;&lt;"MAC"&gt;&gt;}
          }
        }
      }
    ]}
  ]}
].



Il faut aussi configurer le resource_server_id qui devra être repris dans le jeton.



# conf.d/21-auth.conf
auth_backends.1 = rabbit_auth_backend_oauth2
auth_oauth2.resource_server_id = rabbitmq





Génération du jeton JWT


Le token JWT utilisé par le client peut venir d&#8217;un serveur OAuth2, comme Keycloak, ou par un serveur applicatif.
En Java, on peut utiliser simplement Nimbus.


Dans l&#8217;exemple ci-dessous, le jeton est généré pour un utilisateur nommé "user", valide pendant une heure, avec des permissions complètes pour les entités (queues et exchanges) du virtual host "/".



  private String buildToken() throws JOSEException {
    String secret = "459af6c584f776a72ba748d7944082edae6c42d3f22ada2c7270d3bae275c187";
    JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
        .audience("rabbitmq")
        .subject("user")
        .expirationTime(Date.from(Instant.now().plus(1, ChronoUnit.HOURS)))
        .claim("scope", List.of(
                "rabbitmq.write:%2F/.*",
                "rabbitmq.configure:%2F/.*",
                "rabbitmq.read:%2F/.\*"
        ))
        .build();

    SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
    signedJWT.sign(new MACSigner(secret));
    return signedJWT.serialize();
  }



Les permissions sont dans le scope.
On y met une liste de read, write et configure, précédés du resource_server_id.
Pour chaque élément de la liste, après le double point, on donne l&#8217;entité ou les entités concernées.


La spécification des entités se fait en deux parties, le virtual host et le nom de l&#8217;entité, séparées par un slash.
Les noms du vhost et de l&#8217;entité doivent être encodés façon URL, d&#8217;où le %2F.


Pour les exchanges de type topic, on peut ajouter une troisième partie pour la clé de routage.



    JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
        // ...
        .claim("scope", List.of(
                "rabbitmq.write:%2F/q%2Fuser%2F" + id, // queue
                "rabbitmq.configure:%2F/q%2Fuser%2F" + id, // queue
                "rabbitmq.read:%2F/x%2Fclient%2FA/user%2F" + id,  // topic exchange
                "rabbitmq.read:%2F/q%2Fuser%2F" + id // queue
        ))
        .build();



On notera que la permission rabbitmq.read est répétée pour plusieur entités.




Utilisation du jeton JWT


Pour utiliser le jeton généré au paragraphe précédent, on le passe comme mot de passe au client RabbitMQ, sans nom d&#8217;utilisateur.



    CachingConnectionFactory connectionFactory = ...;
    connectionFactory.setPassword(token);



Si on a activé le backend oauth2 en complément au backend interne, le service d&#8217;authentification de RabbitMQ tente de valider le jeton
et s&#8217;il échoue, il transmet au backend interne.


</description>
          <pubDate>2023-06-05T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/RabbitMQ/OAuth2</link>
          <guid isPermaLink="true">https://www.jtips.info/RabbitMQ/OAuth2</guid>
        </item>
      
    
      
        <item>
          <title>Exchanges dans RabbitMQ</title>
          <description>
RabbitMQ est construit sur AMQP 0.9.
Il implémente la notion d'exchange conformément à cette spécification.


Un exchange sert à un producteur pour envoyer des messages.
Il ne stocke aucune message, il n&#8217;est qu&#8217;un point de passage.


Caractéristiques


Un exchange a les caractéristiques suivantes:




name, unique dans le virtual host,


type, direct, topic, fanout ou headers,


durable, s&#8217;il survit au redémarrage du serveur,


auto-delete, supprimée quand le dernier consommateur se désabonne,






Définition



  "exchanges": [
    {
      "name": "x.activity.ride",
      "type": "topic",
      "auto_delete": false,
      "durable": true
    }
  ]





Client Java


Déclaration d&#8217;un exchange

En version courte:



channel.exchangeDeclare(
    "q.activity.ride",
    "direct"      // type
);



En version longue:



channel.exchangeDeclare(
    "x.activity",
    "direct",     // type
    true,         // durable
    false,        // not auto-delete
    Map.of()      // no argument
);




Utilisation d&#8217;un exchange

En AMQP 0.9, utiliser un exchange signifie y publier des messages.



String message = "This is a message.";
channel.basicPublish(
    "x.activity",
    "ABC",          // routing key (see binding)
    null,           // properties
    message.getBytes());






Spring AMQP


Déclaration d&#8217;un exchange


@Component
public class MessageService {
  private final AmqpAdmin admin;

  public void createExchange(String name) {
    admin.declareExchange(
        ExchangeBuilder.topicExchange(name).durable(true).build());
  }

  ...
}




Utilisation d&#8217;un exchange

La valeur ajoutée de Spring c&#8217;est RabbitTemplate que peut publier n&#8217;importe que type d&#8217;objet et le transformant, en JSON par exemple, avant d&#8217;en faire un byte[].



@Component
public class MessageService {
  private final AmqpAdmin admin;

  public void publish(Activity activity) {
    rabbitTemplate.convertAndSend(
            "x.activity",
            activity.key,   // routing key (see binding)
            activity);
  }

  ...
}




</description>
          <pubDate>2023-06-05T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/RabbitMQ/Exchange</link>
          <guid isPermaLink="true">https://www.jtips.info/RabbitMQ/Exchange</guid>
        </item>
      
    
      
        <item>
          <title>Bindings dans RabbitMQ</title>
          <description>
RabbitMQ est construit sur AMQP 0.9.
Il implémente la notion de binding conformément à cette spécification.


Un binding est un lien entre un exchange et une queue.
Il peut éventuellement relier un exchange avec un autre exchange .


Définition



  "bindings": [
    {
      "source": "x.activity",
      "destination": "q.activity.ride",
      "destination_type": "queue",
      "routing_key": "ride",
      "vhost": "/jtips",
      "arguments": {}
    }
  ]





Client Java



  public void bindQueueToExchange() {
    ...
    channel.queueBind(
        "q.activity.ride",  // queue (destination)
        "x.activity",       // exchange (source)
        "ride"              // routing key
    );
  }





Spring AMQP



@Bean
public class MessageService {
  private final AmqpAdmin admin;

  public void bindQueueToExchange(Queue queue, Exchange exchange) {
    String key = ...;
    admin.declareBinding(
        BindingBuilder.bind(queue).to(exchange).with(key).noargs());
  }

  // ou

  public void bindQueueToExchange(String queueName, String exchangeName, String routingKey) {
    admin.declareBinding(
      new Binding(queueName, DestinationType.QUEUE, exchangeName, routingKey, Map.of());
  }
}



</description>
          <pubDate>2023-06-05T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/RabbitMQ/Binding</link>
          <guid isPermaLink="true">https://www.jtips.info/RabbitMQ/Binding</guid>
        </item>
      
    
      
        <item>
          <title>Définitions pour RabbitMQ</title>
          <description>
Le fichier de définitions permet de déclarer les principaux éléments d&#8217;un serveur RabbitMQ.


Il peut être pris en compte au démarrage ou importé a posteriori.


Virtual hosts



  "vhosts": [
    {
      "limits": [],
      "metadata": {
        "description": "Virtual host for jtips examples",
        "tags": []
      },
      "name": "/jtips"
    }
  ]





Queues et exchanges


Queues

Les types de queues peuvent être classic, en mono-noeud ou quorum en distribué, ainsi que stream.



  "queues": [
    {
      "name": "q.activity.AB",
      "type": "classic",
      "durable": true,
      "vhost": "/jtips"
    },
    {
      "name": "q.segment",
      "type": "stream",
      "arguments": {
        "x-queue-type": "stream"
      },
      "durable": true,
      "vhost": "/jtips"
    }
  ]



Référence:




Queues (documentation de référence)





Exchanges

Les types d'exchanges peuvent être direct, topic ou fanout.



  "exchanges": [
    {
      "name": "x.segment",
      "type": "topic",
      "auto_delete": false,
      "durable": true,
      "vhost": "/jtips"
    },
    {
      "name": "x.activity.ride",
      "type": "topic",
      "auto_delete": false,
      "durable": true,
      "vhost": "/jtips"
    }
  ]




Bindings


  "bindings": [
    {
      "source": "x.segment",
      "destination": "q.segment",
      "destination_type": "queue",
      "routing_key": "#",
      "vhost": "/jtips"
    },
    {
      "source": "x.activity",
      "destination": "q.activity.ride",
      "destination_type": "queue",
      "routing_key": "ride",
      "vhost": "/jtips"
    }
  ]






Users et permissions


Utilisateurs

Pour chaque utilisateur, le mot de passe est digéré.
Ça peut être fait avec rabbitmqctl et sa commande hash_password.



$ rabbitmqctl hash_password adminpwd
Will hash password adminpwd
NYvHheVKOpEzhHTrugfvr1PpmA0Hb6SAMXHWRnFD9uO5xdsW




  "users": [
    {
      "name": "admin",
      "password_hash": "NYvHheVKOpEzhHTrugfvr1PpmA0Hb6SAMXHWRnFD9uO5xdsW",
      "tags": [
        "administrator"
      ]
    },
    {
      "name": "varko",
      "password_hash": "VUKoAxZoyVAhC2vJZZPpa+U1dUTGoCW1L52WHq8tJINtduzs",
      "tags": []
    }
  ]




Permissions

Pour chaque utilisateur, on peut définir les queues et exchanges auquels il a accès, en lecture / écriture et pour configuration.
Ça se fait par des expressions régulières.


Dans l&#8217;exemple ci-dessous, l&#8217;utilisateur admin a tous les accès, du moins pour le virtual host /,
alors que l&#8217;utilisateur gateway a un accès limité.



  "permissions": [
    {
      "user": "admin",
      "vhost": "/jtips",
      "read": ".",
      "write": ".",
      "configure": "."
    },
    {
      "user": "varko",
      "vhost": "/jtips",
      "read": "(q\.segment|x\.activity\..)",
      "write": "(q\.segment|q\.activity\..|x\.client\..|x\.activity\..)",
      "configure": "(q\.segment|q\.client\..|x\.client\..|x\.user\..)"
    }
  ],

  "topic_permissions": [
    {
      "user": "varko",
      "vhost": "/jtips",
      "exchange": "x\.activity\.ABC",
      "read": ".",
      "write": "."
    }
  ]



Les topic permissions servent uniquement pour les exchanges de type topic.
Elles doivent être vues comme une surcouche aux permissions classique et non un remplacement.


Dans cette partie, le nom de l'exchange est complet, sans caractère joker ou regex.
Les parties read et write acceptent les expressions régulières.
Il n&#8217;y a pas de configure.





Import / Export


Au démarrage


# /etc/rabbitmq/conf.d/20-definitions.conf ou /etc/rabbitmq/rabbitmq.conf
load_definitions = /etc/rabbitmq/definitions.json




Sans le plugin de management


rabbitmqctl export_definitions /etc/rabbitmq/definitions-export.json
# ou
rabbitmqctl export_definitions -




rabbitmqctl import_definitions /etc/rabbitmq/definitions.json




Avec le plugin de management


rabbitmqadmin export
# ou
curl --user admin:adminpwd --request GET http://localhost:15672/api/definitions




rabbitmqadmin import /etc/rabbitmq/definitions.json
# ou
curl --user admin:adminpwd                                \
     --header "Content-Type: application/json"            \
     --upload-file /etc/rabbitmq/definitions.json         \
     --request POST http://localhost:15672/api/definitions




</description>
          <pubDate>2023-06-04T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/RabbitMQ/Definition</link>
          <guid isPermaLink="true">https://www.jtips.info/RabbitMQ/Definition</guid>
        </item>
      
    
      
        <item>
          <title>Spring Expression Language (SpEL)</title>
          <description>
SpEL est Le langage d&#8217;expression spécifique à Spring Framework.
Il est utilisé à plusieurs endroits, comme dans l&#8217;annotation @Value ou avec les annotations de Spring Security


Exemples


Dans ce premier exemple, on récupère la clé d&#8217;API du service mail.
Il a été enregistré sous forme de propriété dans le fichier application.yml.



@Configuration
public class EmailConfig {

  @Value("${application.email.api-key}")
  private String apiKey;

  ...
}



Dans ce deuxième exemple, on réstreint l&#8217;accès à une méthode aux utilisateurs qui ont le rôle d&#8217;administrateur.



@Service
public class ProductService {

  @PreAuthorize("hasRole('ROLE_ADMIN')")
  private void create(Product product) {
    ...
  }

  ...
}





Syntaxe


Pour la syntaxe complète, il faut aller voir la documentation de référence du langage.
Une bonne partie de la syntaxe ressemble à Java, avec quelques spécificités.




Expression simple


Classe: T(java.lang.Math)


Paramètres (ou variable): #product, avec 2 variables spécifiques: #this et #root


Beans: @productService s&#8217;il y a un bean resolver, et &amp;productService pour s&#8217;adresser directement au beanFactory, comme pour les propriétés


Propriétés: ${application.email.api-key}


Expressions mixtes, c&#8217;est à dire une expression dans du texte statique: "random number is #{T(java.lang.Math).random()}"


Fonctions et méthodes






Résolution d&#8217;expression


On se préoccupe rarement de l&#8217;API de résolution puisque l&#8217;usage le plus courant est dans les annotations.
Dans certains cas, on peut avoir besoin de le faire par programmation.


Expression simple


String exp = "0.1 + T(java.lang.Math).random()";

ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(exp);

Double value = expression.getValue(Double.class);




Expression avec paramètre


String exp = "new java.util.Random().nextInt(#bound)";

ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(exp);

StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("bound", 100);

Integer value = expression.getValue(context, Integer.class);




Expression avec beans

Si l&#8217;expression fait référence à des beans, ceux-ci doivent être résolus.



String exp = "@randomService.nextInt(#bound)";

ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(exp);

StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("bound", 100);
context.setBeanResolver(new BeanFactoryResolver(beanFactory));

Integer value = expression.getValue(context, Integer.class);




Expression de sécurité

Les annotations de sécurité comme @PreAuthorize ou @PostFilter utilisent des méthodes comme isAuthenticated(), isAnonymous() ou hasRole(&#8230;&#8203;).
Ces méthodes sont dans la classe SecurityExpressionRoot qu&#8217;il faut donc utiliser comme racine du parser.



String exp = "hasRole('ROLE_ADMIN')";

ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(exp);

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SecurityExpressionRoot securityRoot = new SecurityExpressionRoot(authentication) {};

Boolean value = expression.getValue(securityRoot, Boolean.class);



Pour les exemples complets, on peut passer un contexte et une racine.



Boolean value = expression.getValue(context, securityRoot, Boolean.class);




</description>
          <pubDate>2023-03-30T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/SpEL</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/SpEL</guid>
        </item>
      
    
      
        <item>
          <title>Problem for HTTP API avec Spring</title>
          <description>
Cette RFC permet d&#8217;uniformiser la façon de répondre en cas d&#8217;erreur, avec un contenu plus riche que le status code.
Le contenu des réponse peut être en JSON ou XML ; je vais me focaliser sur la variante JSON.


RFC


La spécification s&#8217;est faite en deux étapes, avec la RFC 7807 puis la RFC 9457.


Elle définit le media type application/problem+json avec les attributs suivants:




type (URI): type du problème, référence à une documentation lisible par un humain


title (texte): titre court, lisible par un humain, éventuellement traduit


status (nombre): status code HTTP


detail (texte): description détaillée


instance (URI): référence pour l&#8217;occurence du problème





HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en

{
  "type": "https://example.com/probs/out-of-credit",
  "title": "You do not have enough credit.",
  "detail": "Your current balance is 30, but that costs 50.",
  "instance": "/account/12345/msgs/abc",
}



La spécification est souple pour l&#8217;ajout d&#8217;attributs supplémentaires.




Intégration dans Spring


Avec Spring Boot 2, l&#8217;intégration se faisait via la librairie tierce Zalando Problem.
Malheureusement, la compatibilité avec Spring Boot 3 est arrivée avec un peu de retard.


Par contre Spring Boot 3 supporte nativement des fonctionnalités similaires.


Gestion des exceptions

Dans Spring MVC, il y a une gestion par défaut des exceptions, et pour personnaliser on fait des méthodes annotées par @ExceptionHandler dans les classes de contrôleurs ou de façon centralisée dans des classes annotées par @ControllerAdvice.


La gestion par défaut est faire par la classe DefaultHandlerExceptionResolver.
Elle renvoie des réponses à un format propre à Spring, construites par le BasicErrorController.



{
  "timestamp": "2024-04-02T07:18:46.445+00:00",
  "status": 400,
  "error": "Bad Request",
  "path": "/cours"
}



Ce contenu peut être enrichi grâce aux propriétés server.error.*, pour y ajouter les détails de l&#8217;exception.



server:
  error:
    include-exception: true
    include-message: always
    include-stacktrace: always
    include-binding-errors: always



La première propriété peut être true ou false.
Les trois dernières peuvent avoir les valeurs never, always ou on_param.
Avec on_param, le détail n&#8217;est retourné que pour les requêtes qui le demandent explicitement.




include-exception: classe de l&#8217;exception


include-message: message de l&#8217;exception, paramètre message=true


include-stacktrace: stacktrace, paramètre true=true


include-binding-errors: erreurs associées, en particulier pour bean validation, paramètre errors=true





{
  "timestamp": "2024-04-02T07:18:46.445+00:00",
  "status": 400,
  "error": "Bad Request",
  "path": "/cours",
  "exception": "org.springframework.web.bind.MethodArgumentNotValidException",
  "message": "Validation failed for object='cours'. Error count: 1",
  "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in
            public ResponseEntity&lt;Cours&gt; CoursResource.create(Cours)\n\tat ...",
  "errors": [
    {
      "codes": [
        "NotEmpty.code"
      ],
      "arguments": [
        {
          "codes": [
            "cours.code",
            "code"
          ],
          "arguments": null,
          "defaultMessage": "code",
          "code": "code"
        }
      ],
      "defaultMessage": "must not be empty",
      "objectName": "cours",
      "field": "code",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotEmpty"
    }
  ]
}




ExceptionHandler par défaut

Un premier niveau de gestion d&#8217;exceptions est implémenté dans ResponseEntityExceptionHandler, classe abstraite de Spring Framework.
Spring Boot l&#8217;utilise en auto-configuration associée à la propriété spring.mvc.problemdetails.enabled (classe ProblemDetailsExceptionHandler).



spring:
  mvc:
    problemdetails:
      enabled: true



En activant cette option, une vingtaine d&#8217;exceptions sont gérées spécifiquement.
Les autres types d&#8217;exceptions sont traitées par les resolvers traditionnels, avec le format interne à Spring.



{
    "type": "about:blank",
    "title": "Bad Request",
    "status": 400,
    "detail": "Invalid request content.",
    "instance": "/cours"
}



C&#8217;est un début, mais malheureusement ce n&#8217;est pas très cohérent.
De plus, les propriétés server.error.* ne sont plus prises en charge.


On peut arriver au même résultat en implémentant nous-même la classe abstraite ResponseEntityExceptionHandler.



@ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
}



Le résultat est exactement le même qu&#8217;avec la propriété spring.mvc.problemdetails.enabled, mais maintenant nous avons un point d&#8217;entrée pour enrichir le comportement.





Gestion personnalisée


Réponses personnalisées

On peut ajouter des informations personnalisées en redéfinissant la méthode createResponseEntity(&#8230;&#8203;), ou handleExceptionInternal(&#8230;&#8203;).
L&#8217;idée c&#8217;est enrichir le corps de la réponse, qui est probablement de type ProblemDetail.




ProblemDetail



type: URI


title: String


status: int


detail: String


instance: URI


properties: Map&lt;String,Object&gt;







On peut ajouter des paramètres personnalisés, voire réactiver le support des propriétés server.error.*.



@RestControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {

  ...

  @Override
  protected ResponseEntity&lt;Object&gt; handleExceptionInternal(
        Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
    ResponseEntity&lt;Object&gt; response = super.handleExceptionInternal(ex, body, headers, statusCode, request);
    if (response.getBody() instanceof ProblemDetail problemDetail) {
      addExceptionProperties(problemDetail, ex, request);
    }
    return response;
  }

  private void addExceptionProperties(ProblemDetail problemDetail, Exception exception, WebRequest request) {
    problemDetail.setInstance(URI.create(request.getContextPath()));
    if (errorProperties.isIncludeException()) {
      problemDetail.setProperty("exception", exception.getClass());
    }
    if (errorProperties.getIncludeMessage() == ALWAYS
            || errorProperties.getIncludeMessage() == ON_PARAM
            &amp;&amp; getBooleanParameter(request, "message")) {
      problemDetail.setProperty("message", exception.getMessage());
    }
    if (errorProperties.getIncludeStacktrace() == ALWAYS
            || errorProperties.getIncludeStacktrace() == ON_PARAM
            &amp;&amp; getBooleanParameter(request, "trace")) {
      problemDetail.setProperty("trace", Arrays.toString(exception.getStackTrace()));
    }
    if (errorProperties.getIncludeBindingErrors() == ALWAYS
            || errorProperties.getIncludeBindingErrors() == ON_PARAM
            &amp;&amp; getBooleanParameter(request, "errors")) {
      if (exception instanceof BindingResult bindingResult) {
          problemDetail.setProperty("errors", bindingResult.getAllErrors());
      }
    }
  }

}




Erreurs personnalisées

On peut aussi ajouter des handlers pour nos propres exceptions dans la même classe.



@RestControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {

  @ExceptionHandler
  public ProblemDetail handleTechnicalException(TechnicalException exception, WebRequest request) {
    ProblemDetail problem = createProblemDetail(exception, HttpStatus.INTERNAL_SERVER_ERROR, "Technical problem", request);
    addExceptionProperties(problem, exception, request);
    return problem;
  }

  @ExceptionHandler
  public ProblemDetail handleBusinessException(BusinessException exception, WebRequest request) {
    ProblemDetail problem = createProblemDetail(exception, HttpStatus.BAD_REQUEST, "Business problem", request);
    addExceptionProperties(problem, exception, request);
    return problem;
  }

  ...

}




i18n

ErrorResponse sert à ça, avec l&#8217;utilisation d&#8217;un MessageSource.
Pas mal d&#8217;exceptions de Spring implémentent ErrorResponse.




ErrorResponse



getBody(): ProblemDetail


getTitleMessageCode(): String


getDetailMessageCode(): String


getDetailMessageArguments(): String


getHeaders(): HttpHeaders


getStatusCode(): HttpStatusCode







Au lieu de spécifier un titre et une description détaillées, on passe des codes, du type 'problemDetail.title.unknown-product' et 'problemDetail.detail.unknown-product'.
La recherche des vrais messages se fait pas l&#8217;intermédiaire d&#8217;un MessageSource, avec la locale comme paramètre.


La classe ErrorResponseException implémente cette interface et peut servir de base aux exceptions personnalisées.


Dans ResponseEntityExceptionHandler, les instances de l&#8217;interface sont utilisées comme intermédiaire pour construrire une instance de ProblemDetail.
Mais une méthode annotée en @ExceptionHandler peut aussi renvoyer directement un objet de type ErrorResponse.


Ça peut faire du code très léger!



  @ExceptionHandler
  public ErrorResponse handleCustomException(CustomException exception) {
	  return exception;
  }



L&#8217;instance de MessageSource est enregistrée dans ResponseEntityExceptionHandler.



@RestControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {

  @PostConstruct
  public void init() {
    ReloadableResourceBundleMessageSource messageSource
          = new ReloadableResourceBundleMessageSource();
    messageSource.setBasename("classpath:problem");
    messageSource.setDefaultEncoding("UTF-8");

    this.setMessageSource(messageSource);
  }

  ...

}



Il ne reste plus qu&#8217;à ajouter les messages dans le fichier problem.properties&#8230;&#8203;





Synthèse


J&#8217;avoue ne pas trop apprécier cette architecture.
La dualité entre PoblemDetail et ErrorResponse me pose problème.
Et le code qui exploite l&#8217;héritage de `ResponseEntityExceptionHandler` et personnaliser les réponses est loin d&#8217;être limpide.


De ce fait, il vaut peut-être mieux faire un handler autonome et reprendre la gestion de toutes les exceptions.s




Solutions alternatives / complémentaires


Il existe des solutions alternatives sous forme de librairies tierces.
Elles sont plus riches que le support natif.


Zalando Problems for Spring

C&#8217;est la solution historique, qui était largement utilisée avec Spring Boot 2.


Après quelques hésitation, elle supporte maintenant Spring Boot 3.
La migration s&#8217;est faite par une adaptation a minima, sans essayer de se caler sur l&#8217;architecture intégrée et sans utiliser les classes de Spring.



Spting Boot Problem Handler

Cette librairie a été conçue directement pour Spring Boot 3.
Elle n&#8217;étend pas ResponseEntityExceptionHandler, mais utilise ProblemDetail.








Je n&#8217;ai pas encore testé.






</description>
          <pubDate>2023-03-26T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/Problem</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/Problem</guid>
        </item>
      
    
      
        <item>
          <title>Génération de documentation OpenAPI avec Spring</title>
          <description>
Swagger UI est un outil de visualisation d&#8217;API HTTP, basé sur Swagger v2 ou OpenAPI v3.
Il exploite des données des données JSON enregistrées depuis un éditeur ou générée depuis le code de l&#8217;application.







Habituellement, dans les projets basés sur Spring Framework, j&#8217;utilisais Springfox pour générer les informations Swagger v2 directement depuis l&#8217;application déployée.
En migrant Spring Boot v3, j&#8217;ai dû passer à Springdoc et OpenAPI v3.


OpenAPI v3


Les endpoints sont enrichis avec des annotation OpenAPI v3, en vue de la génération d&#8217;une page Swagger UI.














Certaines annotations d&#8217;OpenAPI v3 ont le même nom qu&#8217;en Swagger v2, il faut donc être attentif aux imports: io.swagger.v3.oas.annotations. Ces classes homonymes sont généralement préfixées par Api, comme @ApiResponse.






Classes Controller

Une classe Controller est un regroupement de endpoints et est décrite par un tag, avec l&#8217;annotation @Tag.



@Tag(name = "Products")
public class ProductController {
  ...
}




Endpoint method

Chaque méthode de endpoint est décrite par une annotation @Operation.
L&#8217;attribut summary est obligatoire et description est optionnel.



  @Operation(summary = "Create a new product")
  public ResponseEntity&lt;CreationDTO&gt; create(@RequestBody Product product) {
    ...
  }



Les réponses possibles sont dévrites par des annotations @ApiResponse.
Celles-ci peuvent être placées dans une annotation @ApiResponses, mais contrairement à Swagger v2 ce n&#8217;est plus obligatoire.




responseCode (required) est le status code au format texte.


description (optional) est une description de la response ;
avec la configuration ci-dessous, on peut faire en sorte que la valeur par défaut est la reason phrase du statues HTTP.


content (optional) est généralement inutile car inféré du type de retour ;
la configuration peut aussi mettre une valeur par défaut pour les erreurs.





  @Operation(summary = "Bla bla")
  @ApiResponse(responseCode = "201")
  @ApiResponse(responseCode = "401")
  @ApiResponse(responseCode = "409", description = "Conflict, similar product already exists")
  public ResponseEntity&lt;CreationDTO&gt; create(@RequestBody Product product) {
    ...
  }




Parameters

Par défaut, les paramètres de requêtes sont décrit en JSON.
Si un paramètre représente un ensemble de paramètres éclatés, il faut annoté l&#8217;argument de la méthode par @ParameterObject.











Ce comportement était différent avec Springfox.


L&#8217;annotation @ParameterObject ne fait pas partie d&#8217;OpenAPI, elle est spécifique à Springdoc









  @Operation(summary = "Bla bla")
  public ResponseEntity&lt;List&lt;Product&gt;&gt; getAll(@ParameterObject Pageable pageable) {
    ...
  }



Cette annotation peut aussi être utilisée sur la classe du paramètre.



@ParameterObject
public class ProductFilter {
  ...
}



Ainsi elle s&#8217;appliquera à chaque utilisation.



  @Operation(summary = "Bla bla")
  public ResponseEntity&lt;Page&lt;Product&gt;&gt; getPage(
            @ParameterObject Pageable pageable, ProductFilter filter) {
    ...
  }



On peut aussi déclarer des paramètres supplémentaires avec l&#8217;annotation @Parameter sur la méthode.
Ces paramètres sont généralement récupérés sur l&#8217;objet request plutôt que par un argument de la méthode.



Headers

Les entêtes de requêtes sont aussi déclarée avec l&#8217;annotation @Parameter, en mettant l&#8217;attibut in à ParameterIn.HEADER.



  @Parameter(name = TENANT_HEADER, in = ParameterIn.HEADER, required = true)
  public ResponseEntity&lt;Product&gt; get(@PathVariable UUID id) {
    ...
  }



Des en-têtes peuvent aussi être ajoutées en fonction de la présence d&#8217;annotations personnalisées.
Dans la configuration, on peut détecter la présence d&#8217;une annotation sur une méthode et ajouter le header, comme expliqué ci-dessous.





Springdoc


Springdoc permet d&#8217;exploiter les annotations OpenAPI et de générer une structure de métadonnées exploitable par SwaggerUI.
Springfox fait la même chose, mais ne fonctionne plus avec Spring Boot 3 et Spring Framework 6.







Configuration

La configuration de Springdoc se fait un peu dans le fichier application.yml et beaucoup par la production de beans.



@Configuration
public class OpenApiConfiguration {

  @Bean
  public OpenAPI openApi() {
    ...
  }

  @Bean
  public GroupedOpenApi openApiMain() {
    ...
  }

}



Le bean OpenAPI permet de spécifier quelques informations générales qui apparaitront dans la page d&#8217;accueil de SwaggerUI.



  @Bean
  public OpenAPI openAPI() {
    return new OpenAPI()
              .info(new Info()
                      .title("JTips API Documentation")
                      .description("This is a fake documentation, as JTIps does not have an API.")
                      .version("v1.0.4"));
  }



On peut avoir plusieurs beans de type GroupedOpenApi, chacun représentant une partie de l&#8217;API.
Pour chaque groupe, on spécifie quelques information générales, comme le nom, les paquetages Java à scanner et les chemins HTTP à prendre en compte.
L&#8217;essentiel de la configuration consiste à ajouter des éléments de personnalisation à différents niveaux.



  @Bean
  public GroupedOpenApi openApiMain() {
    return GroupedOpenApi.builder()
                .group("main")
                .packagesToScan("info.jtips")
                .pathsToMatch("/**")
                .addOpenApiCustomizer(openApi -&gt; ...)
                .addOperationCustomizer((operation, handlerMethod) -&gt; ...)
                .build();
  }




Tri de éléments

L&#8217;ordre des tags et des opérations à l&#8217;intérieur d&#8217;un tag peut être déterminé par des propriétés de configuration.


application.yml

springdoc:
  swagger-ui:
    tagsSorter: alpha
    operationsSorter: alpha
    docExpansion: none



Par contre l&#8217;ordre des schémas d&#8217;entrée et de sortie doit se faire par du code.



  @Bean
  public GroupedOpenApi openApiMain() {
    return GroupedOpenApi.builder()
              // Sort schemas
              .addOpenApiCustomizer(openApi -&gt; {
                    Map&lt;String, Schema&gt; schemas = openApi.getComponents().getSchemas();
                    openApi.getComponents().setSchemas(new TreeMap&lt;&gt;(schemas));
              })
              ...
  }




Gestion d&#8217;erreurs

Si toutes les réponses d&#8217;erreurs retournent un contenu JSON du même format (Problem for HTTP APIs par exemple), on peut spécifier ce format globalement.
Ça se fait en 2 étapes:




déclarer le schéma de retour,


enrichir les réponses d&#8217;erreur avec ce schéma.





  @Bean
  public GroupedOpenApi openApiMain() {
    this.errorResponseSchema = new Schema&lt;&gt;();
    this.errorResponseSchema.setName(ExceptionDTO.class.getSimpleName());
    this.errorResponseSchema.set$ref("#/components/schemas/" + ExceptionDTO.class.getSimpleName());

    return GroupedOpenApi.builder()
              .addOpenApiCustomizer(openApi -&gt; {
                  // Register additional DTO schemas
                  openApi.getComponents().getSchemas().putAll(ModelConverters.getInstance().read(ExceptionDTO.class));
              })
              .addOperationCustomizer((operation, handlerMethod) -&gt; {
                  operation.getResponses().forEach(this::enrichErrorResponse);
                  return operation;
              })
              ...
  }

  private void enrichErrorResponse(String code, ApiResponse apiResponse) {
    try {
      HttpStatus status = HttpStatus.resolve(Integer.parseInt(code));
      if (status != null &amp;&amp; status.isError()) {
        apiResponse.content(
              new Content()
                  .addMediaType(APPLICATION_JSON_VALUE,
                                new MediaType().schema(errorResponseSchema)));
      }
    } catch (NumberFormatException ignored) {
      // if the code is not a number, we cannot enrich the response
    }
  }




Valeurs par défaut

Plutôt que de répéter la même description pour les réponses d&#8217;erreur, on peut récupérer la phrase associée au status et en faire la valeur par défaut.



  @Bean
  public GroupedOpenApi openApiMain() {
    return GroupedOpenApi.builder()
              .addOperationCustomizer((operation, handlerMethod) -&gt; {
                  operation.getResponses().forEach(this::enrichAnyResponse);
                  return operation;
              })
              ...
  }

  private void enrichAnyResponse(String code, ApiResponse apiResponse) {
    try {
      HttpStatus status = HttpStatus.resolve(Integer.parseInt(code));
      if (status != null &amp;&amp; apiResponse.getDescription() == null) {
        apiResponse.description(status.getReasonPhrase());
      }
    } catch (NumberFormatException ignored) {
      // if the code is not a number, we cannot enrich the response
    }
  }




Authentification

Il est possible d&#8217;utiliser (presque) exclusivement les propriétés de application.yml avec quelques annotations, mais comme je préfère le code, je n&#8217;en utilise que le strict minimum.
Ici, c&#8217;est pour préciser qu&#8217;on utilise une authentification Oauth2 / OIDC, avec PKCE.


Cette étape n&#8217;est utile que pour utiliser Swagger UI pour tester l&#8217;API.
Le mode d&#8217;essai peut d&#8217;ailleurs être désactivé (springdoc.swagger-ui.supported-submit-methods=[]).
Et le bouton Authorize n&#8217;est affiché que si on a déclaré un SecurityScheme par personnalisation ou avec l&#8217;annotation @SecurityScheme.



  public GroupedOpenApi openApiMain() {
    this.oauth2Requirement = new SecurityRequirement().addList(SECURITY_NAME);

    return GroupedOpenApi.builder()
              .addOpenApiCustomizer(openApi -&gt;
                  openApi.getComponents()
                         .addSecuritySchemes(SECURITY_NAME,
                            new SecurityScheme()
                                  .type(SecurityScheme.Type.OAUTH2)
                                  .flows(new OAuthFlows().authorizationCode(
                                              new OAuthFlow()
                                                    .authorizationUrl(properties.getIssuerUri() + "/oauth2/authorize")
                                                    .tokenUrl(properties.getIssuerUri() + "/oauth2/token")))))
              ...
  }



application.yml

springdoc:
  swagger-ui:
    oauth:
      use-pkce-with-authorization-code-grant: true
      client-id: console-ui
      client-secret: "xxxxxx"









Le client-secret est obligatoire car Swagger UI ne peut pas authentifier un utilisateur avec un client en mode none ; pour que ça fonctionne, j&#8217;ai ajouté un mode client_secret_post.





Ensuite, pour chaque endpoint sécurisé, on déclare un élément de sécurité par l&#8217;annotation @SecurityRequirement ou par personnalisation.



  public GroupedOpenApi openApiMain() {
    this.oauth2Requirement = new SecurityRequirement().addList(SECURITY_NAME);

    return GroupedOpenApi.builder()
              .addOpenApiCustomizer(openApi -&gt; openApi.getPaths().forEach(this::securePath))
              ...
  }

  private void securePath(String key, PathItem pathItem) {
    if (key.startsWith("/secured")) {
      secureOperation(pathItem.getGet());
      secureOperation(pathItem.getPost());
      secureOperation(pathItem.getPut());
      secureOperation(pathItem.getPatch());
      secureOperation(pathItem.getDelete());
    }
  }

  private void secureOperation(Operation operation) {
    if (operation != null &amp;&amp; operation.getSecurity() == null) {
      operation.addSecurityItem(oauth2Requirement);
    }
  }




Annotations personnalisées

En utilisant ces techniques de personnalisation, il devient facile de prendre en compte des annotations personnalisées.


2 exemples:




@ApiMultiTenant pour un endpoint qui nécessite un header de requête Tenant.





  @Bean
  public GroupedOpenApi openApiMain() {
    return GroupedOpenApi.builder()
              .addOperationCustomizer((operation, handlerMethod) -&gt; {
                ApiMultiTenant multiTenant = handlerMethod.getMethodAnnotation(ApiMultiTenant.class);
                if (multiTenant != null
                    &amp;&amp; (operation.getParameters() == null
                      || operation.getParameters()
                                  .stream()
                                  .noneMatch(parameter -&gt; ParameterIn.HEADER.name().equals(parameter.getIn())
                                                       &amp;&amp; TENANT_HEADER.equalsIgnoreCase(parameter.getName()))) {
                  operation.addParametersItem(
                    new HeaderParameter()
                      .name(TENANT_HEADER)
                      .required(multiTenant.required())
                      .example("00000000-0000-0000-0000-000000000000"));
                }
                return operation;
              })
              ...
  }





@ApiSecured pour un endpoint sécurisé qui peut donc répondre 401 ou 403.





  @Bean
  public GroupedOpenApi openApiMain() {
    ApiResponse response401 = new ApiResponse()
              .description(HttpStatus.UNAUTHORIZED.getReasonPhrase())
              .content(new Content().addMediaType(APPLICATION_JSON_VALUE, new MediaType().schema(errorResponseSchema)));
    ApiResponse response403 = new ApiResponse()
              .description(HttpStatus.FORBIDDEN.getReasonPhrase())
              .content(new Content().addMediaType(APPLICATION_JSON_VALUE, new MediaType().schema(errorResponseSchema)));
    return GroupedOpenApi.builder()
              .addOperationCustomizer((operation, handlerMethod) -&gt; {
                ApiSecured secured = handlerMethod.getMethodAnnotation(ApiSecured.class);
                if (secured != null) {
                  operation.getResponses()
                           .addApiResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()), response401)
                           .addApiResponse(String.valueOf(HttpStatus.FORBIDDEN.value()), response403);
                }
                return operation;
              })
              ...
  }




</description>
          <pubDate>2023-03-23T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/Swagger</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/Swagger</guid>
        </item>
      
    
      
        <item>
          <title>Métadonnées JPA (WIP)</title>
          <description>
Le point d&#8217;accès est Metamodel, accessible depuis l&#8217;entity manager.



Metamodel metamodel = entityManager.getMetamodel();
//...



Métadonnées des entités


Pour chaque entité, on a un objet EntityType accessible depuis le metamodel.



    EntityType&lt;Product&gt; productEntityType = metamodel.entity(Product.class);
    // ou
    Set&lt;ManagedType&lt;?&gt;&gt; managedTypes = metamodel.getManagedTypes();





EntityType&lt;X&gt; &#8594; IdentifiableType&lt;X&gt; &#8594; ManagedType&lt;X&gt; &#8594; Type&lt;X&gt;



getName(): String


getJavaType(): Class&lt;X&gt;


getAttributes(): Set&lt;Attribute&lt;? super X, ?&gt;&gt;


getAttribute(String name): Attribute&lt;? super X, ?&gt;


getSingularAttributes(): Set&lt;SingularAttribute&lt;? super X, ?&gt;&gt;


getSingularAttribute(String name): SingularAttribute&lt;? super X, ?&gt;


getSingularAttribute(String name, Class&lt;Y&gt; type): SingularAttribute&lt;? super X, Y&gt;


getPluralAttributes(): Set&lt;PluralAttribute&lt;? super X, ?, ?&gt;&gt;


getCollection(String name): CollectionAttribute&lt;? super X, ?&gt;


getCollection(String name, Class&lt;E&gt; elementType): CollectionAttribute&lt;? super X, E&gt;


getSet(String name, Class&lt;E&gt; elementType): SetAttribute&lt;? super X, E&gt;


getSet(String name): SetAttribute&lt;? super X, E&gt;


getList(String name, Class&lt;E&gt; elementType): ListAttribute&lt;? super X, E&gt;


getList(String name): ListAttribute&lt;? super X, E&gt;


getMap(String name, Class&lt;K&gt; keyType, Class&lt;V&gt; valueType): MapAttribute&lt;? super X, K, V&gt;


getMap(String name): MapAttribute&lt;? super X, K, V&gt;


hasSingleIdAttribute(): boolean


hasVersionAttribute(): boolean


getIdType(): Type&lt;?&gt;


getId(Class&lt;Y&gt; type): SingularAttribute&lt;? super X, Y&gt;


getVersion(Class&lt;Y&gt; type): SingularAttribute&lt;? super X, Y&gt;


getPersistenceType(): PersistenceType







PersistenceType peut prendre les valeurs suivantes: ENTITY, EMBEDDABLE, MAPPED_SUPERCLASS, BASIC.




Attribute&lt;X, Y&gt;



getName(): String


getPersistentAttributeType(): PersistentAttributeType


getDeclaringType(): ManagedType&lt;X&gt;


getJavaType(): Class&lt;Y&gt;


getJavaMember(): Member


isAssociation(): boolean


isCollection(): boolean









SingularAttribute&lt;X, T&gt; &#8594; Attribute&lt;X, Y&gt;



getType(): Type&lt;T&gt;


isId(): boolean


isVersion(): boolean


isOptional(): boolean









PluralAttribute&lt;X, T&gt; &#8594; Attribute&lt;X, Y&gt;



getCollectionType(): CollectionType


getElementType(): Type&lt;E&gt;







CollectionType peut prendre les valeurs suivantes: COLLECTION, SET, LIST, MAP




CollectionAttribute&lt;X, T&gt; &#8594; PluralAttribute&lt;X, Y&gt;






SetAttribute&lt;X, T&gt; &#8594; PluralAttribute&lt;X, Y&gt;






ListAttribute&lt;X, T&gt; &#8594; PluralAttribute&lt;X, Y&gt;






MapAttribute&lt;X, T&gt; &#8594; PluralAttribute&lt;X, Y&gt;




PersistentAttributeType peut prendre les valeurs suivantes: BASIC, EMBEDDED, ONE_TO_ONE, MANY_TO_ONE, MANY_TO_MANY, ONE_TO_MANY, ELEMENT_COLLECTION.


</description>
          <pubDate>2023-01-09T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Hibernate/Metadata</link>
          <guid isPermaLink="true">https://www.jtips.info/Hibernate/Metadata</guid>
        </item>
      
    
      
        <item>
          <title>Metadonnées de PostgreSQL</title>
          <description>
Version de PostgreSQL



SELECT version();





Liste des tables



SELECT t.tablename
FROM pg_tables t
WHERE t.schemaname = 'public'





Supprimer toutes les tables



CREATE OR REPLACE FUNCTION drop_tables(schema_name text)
RETURNS void AS
$$
DECLARE r record;
BEGIN
  FOR r IN SELECT quote_ident(t.tablename) AS table_name
           FROM pg_tables t
           WHERE t.schemaname = schema_name
  LOOP
    RAISE INFO 'Dropping table %.%', schema_name, r.tablename;
    EXECUTE format('DROP TABLE IF EXISTS %I.%I CASCADE', quote_ident(schema_name), r.table_name);
  END LOOP;
END
$$ LANGUAGE plpgsql;



Puis



SELECT drop_tables('public');





Liste des vues



SELECT v.viewname
FROM pg_views v
WHERE v.schemaname = 'public'





Liste des colonnes


Pour avoir la liste des colonnes d&#8217;une table, avec le type de chaque colonne, on utilise la table information_schema.columns.



SELECT *
FROM information_schema.columns
WHERE table_name = 'sw_users';





Liste des fonctions



SELECT routine_name
FROM information_schema.routines
WHERE routine_type = 'FUNCTION'
  AND routine_schema = 'public';



Dans une version plus détaillée:



SELECT p.oid::regprocedure
FROM pg_catalog.pg_proc p
    JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid
WHERE p.prokind = 'f'
  AND n.nspname = 'public';





Supprimer toutes les fonctions



CREATE OR REPLACE FUNCTION drop_functions(schema_name text)
  RETURNS void AS
$$
DECLARE r record;
BEGIN
  FOR r IN SELECT p.oid::regprocedure as qualified_name
           FROM pg_catalog.pg_proc p
               JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid
           WHERE p.prokind = 'f'
             AND n.nspname = schema_name
  LOOP
    RAISE INFO 'Dropping function %.%', schema_name, r.qualified_name;
    EXECUTE 'DROP FUNCTION IF EXISTS ' || schema_name || '.' || r.qualified_name;
  END LOOP;
END
$$ LANGUAGE plpgsql;



Puis



SELECT drop_functions('public');





Liste des clés étrangères



SELECT conname AS constraint_name,
       conrelid::regclass AS source_table,
       confrelid::regclass AS target_table,
       pg_get_constraintdef(oid) AS definition
FROM   pg_constraint
WHERE  contype = 'f'
  AND  connamespace = 'public'::regnamespace
ORDER  BY conrelid::regclass::text, contype DESC;





Références




https://database.guide/3-ways-to-list-all-functions-in-postgresql/




</description>
          <pubDate>2022-12-09T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/PostgreSQL/Metadata</link>
          <guid isPermaLink="true">https://www.jtips.info/PostgreSQL/Metadata</guid>
        </item>
      
    
      
        <item>
          <title>Metadonnées de Citus</title>
          <description>
Version de Citus et de PostgreSQL



SELECT version(), citus_version();

+--------------------------------------------------+---------------------+
| PostgreSQL 14.5 (Debian 14.5-2.pgdg110+2) on ... | Citus 11.1.4 on ... |





Workers



SELECT master_get_active_worker_nodes();

+-------------------------------+
| (docker-citus_worker2-1,5432) |
| (docker-citus_worker1-1,5432) |
| (docker-citus_worker3-1,5432) |



Cette requête permet de vérifier si on est effectivement en distribué.
En démarrant Citus sur un seul noeud, on aura un coordinateur (master) sans worker.
Dans ce cas, les contraintes liées au caractère distribué sont allégées.




Noeuds actifs



SELECT * FROM pg_dist_node;

+---------+---------+-----------------------+----------+-----+------------------+
| nodeid  | groupid | nodename              | nodeport | ... | shouldhaveshards |
+---------+---------+-----------------------+----------+-----+------------------+
| 1       | 1       | docker-citus_worker-1 | 5432     |     | true             |
| 3       | 0       | localhost             | 5432     |     | false            |



Des noeuds peuvent être ajoutés ou retirés avec les fonction citus_add_node() et citus_remove_node().



SELECT citus_add_node('localhost', 5432, groupId =&gt; 0);
SELECT citus_remove_node('localhost', 5432);





Sharding


Globalement



SELECT * from pg_dist_shard;

+--------------+---------+--------------+---------------+---------------+
| logicalrelid | shardid | shardstorage | shardminvalue | shardmaxvalue |
+--------------+---------+--------------+---------------+---------------+
| sw_dist01    | 102104  | t            | -2147483648   | -2013265921   |
| sw_dist01    | 102105  | t            | -2013265920   | -1879048193   |
| sw_dist01    | 102106  | t            | -1879048192   | -1744830465   |
| sw_dist01    | 102107  | t            | -1744830464   | -1610612737   |
| ...



Pour une table



SELECT * from pg_dist_shard where logicalrelid='sw_ref01'::regclass;



La table citus_shards stocke aussi des données de distribution.
Elle permet de retrouver facilement quelles tables sont distribuées.



SELECT distinct table_name::text, citus_table_type, colocation_id
FROM citus_shards
ORDER BY table_name::text;

+--------------+------------------+---------------+
| table_name   | citus_table_type | colocation_id |
+--------------+------------------+---------------+
| sw_dist01    | distributed      | 18            |
| sw_ref01     | reference        | 19            |



On peut aussi demander la situation pour une table.



SELECT *
FROM citus_shards
WHERE table_name='sw_dist01'::regclass;

+------------+---------+-------------------+------------------+---------------+-----------+----------+------------+
| table_name | shardid | shard_name        | citus_table_type | colocation_id | nodename  | nodeport | shard_size |
+------------+---------+-------------------+------------------+---------------+-----------+----------+------------+
| sw_dist01  | 102040  | sw_dist01_102040  | distributed      | 2             | worker1-1 | 5432     | 0          |
| sw_dist01  | 102041  | sw_dist01_102041  | distributed      | 2             | worker2-1 | 5432     | 0          |
| sw_dist01  | 102042  | sw_dist01_102042  | distributed      | 2             | worker3-1 | 5432     | 0          |



Par ailleurs, on peut demander avoir le shard d&#8217;une valeur de distribution dans le cluster.
Et indirectement avoir le nombre de lignes par shard.



SELECT get_shard_id_for_distribution_column('sw_dist01', '00000000-0000-0000-0000-000000000000');

SELECT count(*), get_shard_id_for_distribution_column('sw_dist01', tenant_key) shard_id
FROM sw_dist01
GROUP BY shard_id;





Référence




https://docs.citusdata.com/en/v11.0/develop/api_metadata.html




</description>
          <pubDate>2022-12-06T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Citus/Metadata</link>
          <guid isPermaLink="true">https://www.jtips.info/Citus/Metadata</guid>
        </item>
      
    
      
        <item>
          <title>Types de tables avec Citus</title>
          <description>
Il y a 4 sortes de tables avec Citus




Distributed


Reference


Local


Managed Local




La différence entre ces tables se situe dans leur localisation dans un environnement distribué de Citus.


Pour rappel, un environnement Citus est constitué de noeuds:




1 master


n workers


1 manager




Tables distribuées


Les tables distribuées sont réparties en shards qui sont distribuées sur les workers.
Toutes les lignes qui ont la même valeur dans la colonne de distribution sont localisées dans le même shard.


La colonne de distribution doit faire partie de la clé primaire.
Elle doit être de type entier, UUID ou text ; le type varchar pose des problèmes depuis la version 11.1.



CREATE TABLE sw_dist01 (
    id uuid,
    tenant_key uuid,
    value text,
    PRIMARY KEY (tenant_key, id)
);

SELECT create_distributed_table('sw_dist01', 'tenant_key');



On peut regrouper les tables pour qu&#8217;elles aient la même distribution.



CREATE TABLE sw_dist02 (
    id uuid,
    tenant_key uuid,
    dist01_id uuid,
    value text,
    PRIMARY KEY (tenant_key, id)
);
ALTER TABLE sw_dist02 ADD CONSTRAINT fk_dist02_01 FOREIGN KEY (tenant_key, dist01_id) REFERENCES sw_dist01;

SELECT create_distributed_table('sw_dist02', 'tenant_key', colocate_with =&gt; 'sw_dist01');



La façon dont les données sont réparties est stockée dans les métadonnées.



SELECT * FROM pg_dist_shard WHERE logicalrelid='sw_dist01'::regclass;

+--------------+---------+--------------+---------------+---------------+
| logicalrelid | shardid | shardstorage | shardminvalue | shardmaxvalue |
+--------------+---------+--------------+---------------+---------------+
| sw_dist01    | 102104  | t            | -2147483648   | -2013265921   |
| sw_dist01    | 102105  | t            | -2013265920   | -1879048193   |
| sw_dist01    | 102106  | t            | -1879048192   | -1744830465   |
| sw_dist01    | 102107  | t            | -1744830464   | -1610612737   |
| ...                                                                   |





Tables de référence


Pour les tables de référence, toutes les données sont présentes sur tous les workers.
Ça permet à n&#8217;importe quelle table distribuée d&#8217;avoir une foreign key vers cette table.



CREATE TABLE sw_ref01 (
    id uuid not null,
    value text,
    PRIMARY KEY (id)
);

SELECT create_reference_table('sw_ref01');



Ce type est adapté aux tables peu volumineuses et/ou avec peu de mise à jour.


Ces tables se retrouvent aussi dans les métadonnées.



SELECT * FROM pg_dist_shard WHERE logicalrelid='sw_ref01'::regclass;

+--------------+---------+--------------+---------------+---------------+
| logicalrelid | shardid | shardstorage | shardminvalue | shardmaxvalue |
+--------------+---------+--------------+---------------+---------------+
| sw_ref01     | 102168  | t            |               |               |





Tables locales


Les tables locales ne sont présentes que sur le master.


Une table est locale tant qu&#8217;elle n&#8217;a été déclarée ni distributed ni reference.


Une table distributed ou reference peut être déclassée en locale.
C&#8217;est la même requête, que la table soit distribuée ou référence.



SELECT undistribute_table('sw_local01');



Les tables locales gérées ont été ajoutées dans les métadonnées de Citus.
On les retrouve dans la vue citus_shards, contrairement aux tables locales simples.


Un table locale devient automatiquement gérée lorsqu&#8217;on établit une clé étrangère avec une table de référence.
On peut aussi la déclarer ainsi.



SELECT citus_add_local_table_to_metadata('sw_local01');



La gestion de la table peut être annulée avec la même fonction undistribute_table(&#8230;&#8203;) déjà vue ci-dessus.


Enfin, on peut forcer toutes les tables locales à être gérées dès leur création.



ALTER SYSTEM SET citus.use_citus_managed_tables TO true;
SELECT pg_reload_conf();





Relations entre tables


Les contraintes pour établir une foreign key entre deux tables sont les suivantes.









Table source
Table cible
Autorisé




Local
Local
Oui


Local
Distribuée
Non


Local
Référence
Oui


Référence
Local
Oui, si le coordinateur est un noeud


Référence
Distribuée
Non


Référence
Référence
Oui


Distribuée
Local
Non


Distribuée
Distribuée
Oui, si collocalisée


Distribuée
Référence
Oui




Pour déclarer le coordinateur comme un noeud:



SELECT citus_add_node('localhost', 5432, groupId =&gt; 0);



D&#8217;autres contraintes peuvent apparaître en plus de celles-ci dans certaines requêtes complexes.
Par exemple, dans une requête avec sous-requête ci-dessous, le tableau est légèrement différent.



SELECT t1.id
FROM sw_dist01 t1
WHERE (
  EXISTS(
    SELECT 1
    FROM sw_dist03 t3
    WHERE t3.t01_id = t1.id
  )
);










Table requête
table sous-requête
Autorisé




Local
Local
Oui


Local
Distribuée
Non


Local
Référence
Oui, si le coordinateur est un noeud


Référence
Local
Oui, si le coordinateur est un noeud


Référence
Distribuée
Non


Référence
Référence
Oui


Distribuée
Local
Non


Distribuée
Distribuée
Oui, si collocalisée


Distribuée
Référence
Oui




Ces contraintes sont principalement dues à la jointure t3.t01_id = t1.id.


Pour cet exemple, on peut obtenir le même résultat sans ces contraintes avec une jointure simple ou avec une sous-requête suivante:



SELECT t3.id
FROM sw_dist03 t3
INNER JOIN sw_dist01 t1 ON t1.id = t3.t01_id;

-- ou

SELECT t3.id
FROM sw_dist03 t3
WHERE (
  t3.t01_id IN (
    SELECT t1.id
    FROM sw_dist01 t1
  )
);





Mises à jour distribuées


Certaines tables ne peuvent pas être mises à jour dans la même transaction.


Par exemple, la transaction suivante ne fonctionne que dans certaines conditions.
Elle concerne une table distribuée (sw_dist01) et une table locale (sw_local01).



BEGIN;
DELETE FROM sw_dist01;
INSERT INTO sw_local01 (id, value) VALUES (uuid_generate_v4(), 'Local 01');
COMMIT;



Telle quelle, la transaction se passe sans problème.
En revanche s&#8217;il existe une clé étrangère entre une table de référence et sw_local01, elle échoue.



CREATE TABLE sw_local01 ()
    id uuid,
    value text,
    PRIMARY KEY (id)
);

CREATE TABLE sw_ref01 (
    id uuid,
    value text,
    local01_id uuid,
    PRIMARY KEY (id)
);
SELECT create_reference_table('sw_ref01');
ALTER TABLE sw_ref01 ADD CONSTRAINT fk_ref01_local01 FOREIGN KEY (local01_id) REFERENCES sw_local01;



Dans ces conditions, la transaction donne l&#8217;erreur suivante:



ERROR: cannot modify table "sw_local01" because there was a parallel operation on a distributed table
Detail: When there is a foreign key to a reference table or to a local table,
        Citus needs to perform all operations over a single connection per node to ensure consistency.
Hint: Try re-running the transaction with "SET LOCAL citus.multi_shard_modify_mode TO 'sequential';"



Pour résoudre le problème, on peut passer la propagation de la transaction en séquentiel.



BEGIN;
SET LOCAL citus.multi_shard_modify_mode TO 'sequential';
DELETE FROM sw_dist01;
INSERT INTO sw_local01 (id, value) VALUES (uuid_generate_v4(), 'Local 01');
COMMIT;



La transaction peut fonctionner sans ça si la première requête n&#8217;est pas réellement distribuée, mais se localise sur un seul shard.
Pour ça, on doit limiter la requête sur la colonne de distribution.



BEGIN;
DELETE FROM sw_dist01 WHERE tenant_key = '942b411f-e414-4468-b98c-8d491563c7d8';
INSERT INTO sw_local01 (id, value) VALUES (uuid_generate_v4(), 'Local 01');
COMMIT;





Références


Cette page a été rédigée sur la base de Citus 11.1.




Getting started - Concepts


Foreign key support in Citus: before &amp; after Citus 10


Version 11 release - Citus managed local tables




</description>
          <pubDate>2022-12-05T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Citus/Tables</link>
          <guid isPermaLink="true">https://www.jtips.info/Citus/Tables</guid>
        </item>
      
    
      
        <item>
          <title>Suivi d&apos;une application Spring avec Actuator</title>
          <description>
Spring Boot publie des informations et métriques sur le déploiement et le fonctionnement d&#8217;une application avec Actuator.


Configuration


Pour activer Actuator, on ajoute une dépendance vers son starter.



  &lt;dependency&gt;
      &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
      &lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt;
  &lt;/dependency&gt;



Actuator a nativement un ensemble de endpoints qu&#8217;il publie en Web ou en JMX.
Par défaut, seul le endpoint Health est publié.




Web endpoints


Le endpoint racine /actuator donne la liste des endpoints actifs, avec leur URL.
On l&#8217;appelle la page de découverte et elle peut être désactivée.



management:
  endpoints:
    web:
      discovery:
        enabled: false



Il faut noter que ce contexte est relatif au contexte racine de l&#8217;application défini par server.servlet.context-path.
Il peut être modifié et cette modification est répercutée sur tous les endpoints.



management:
  endpoints:
    web:
      base-path: /management



On peut aussi modifier le port.
Dans ce cas, l&#8217;URL d&#8217;accès ne contient plus le contexte racine de l&#8217;application, mais celui du management qui est vide par défaut.



management:
  server:
    port: 9999
    base-path: /actuator



Par défaut, seul le endpoint Health est publié, en version simplifiée.
On peut publier explicitement chaque endpoint en Web, inclure l&#8217;ensemble de ceux qui sont actifs ou en exclure certains.



management:
  endpoints:
    web:
      exposure:
        include: metrics,info
        #ou
        include='*'
        exclude=health,beans



La liste des endpoints actifs par défaut dépend de l&#8217;application.
Les suivants sont systématiquement (ou souvent) présents pour connaître l&#8217;état de l&#8217;application :




/health : état de santé de l&#8217;application et de certains composants


/metrics


/heapdump, /threaddump : génération d&#8217;un heap dump ou d&#8217;un thread dump




Les suivants sont systématiquement (ou souvent) présents pour le débogage d&#8217;un déploiement :




/info : informations statiques de l&#8217;application


/mappings : liste des endpoints applicatifs


/beans, /conditions, /configprops, /env : pour déboguer un déploiement, liste de composants Spring dans le contexte, évaluation des conditions pour l&#8217;auto-configuration, données utilisées pour les classes annotées avec @ConfigurationProperties et contenu du ConfigurableEnvironment


/scheduledtasks, /quartz : tâches planifiées


/loggers et /logfile : liste et configuration des loggers


/liquibase, /flyway : initialisations de la base de données par Liquibase ou Flyway




Les endpoints suivants ne sont publiés que dans certaines conditions.




/startup ; fournit les étapes de démarrage si l&#8217;application est configurée avec un BufferingApplicationStartup


/shutdown : arrêt de l&#8217;application avec une requête POST ; doit être activé explicitement et CSRF désactivé


/httpexchanges[1], /httptrace[2] : échanges requête/réponse HTTP, s&#8217;il y a un bean `HttpExchangeRepository`[1] ou `HttpTraceRepository`[2]


/sessions : gestion des sessions pour une application stateful


/auditevents : événement d&#8217;audit, s&#8217;il y a un bean AuditEventRepository


/integrationgraph : avec Spring Integration




Les chemins des différents endpoints peuvent aussi être reconfigurés.
Par exemple pour /health :



management:
  endpoints:
    web:
      path-mapping:
        health: healthcheck



Chaque endpoint activé par défaut peut être désactivé dans la configuration.
Par exemple pour /health:



management:
  endpoint:
    health:
      enabled: false



A contrario, un endpoint désactivé par défaut, peut être activé.



management:
  endpoint:
    shutdown:
      enabled: true



D&#8217;autres endpoints peuvent être ajoutés via des librairies ou développements personnalisés.




Health


Le endpoint /health est activé par défaut.
Par défaut, ce endpoint ne donne qu&#8217;une information globale.



~$ curl http://localhost:9999/actuator/health

{"status":"UP"}



Détails

On peut le configurer pour avoir l&#8217;état des composants.



management:
  endpoint:
    health:
      show-components: always




~$ curl http://localhost:9999/actuator/health

{
  "status":"UP",
  "components": {
    "db":{
      "status":"UP",
    },
    "diskSpace": {
      "status":"UP",
    },
    "ping":{
      "status":"UP"
    }
  }
}



Dans ces conditions, on peut aussi demander l&#8217;état d&#8217;un composant.



~$ curl http://localhost:9999/actuator/health/db

{
  "status":"UP",
}



On peut aussi configurer le endpoint pour avoir plus de détails.



management:
  endpoint:
    health:
      show-details: always




~$ curl http://localhost:9999/actuator/health

{
  "status":"UP",
  "components": {
    "db":{
      "status":"UP",
      "details":{
        "database":"PostgreSQL",
        "validationQuery":"isValid()"
      }
    },
    "diskSpace": {
      "status":"UP",
      "details": {
        "total":502392610816,
        "free":381957959680,
        "threshold":10485760,
        "exists":true
      }
    },
    "ping":{
      "status":"UP"
    }
  }
}




Kubernetes

Par ailleurs, le endpoint peut aussi publier des informations adaptées à Kubernetes.



management:
  endpoint:
    health:
      probes:
        enabled: true




~$ curl http://localhost:9999/actuator/health/liveness

{
    "status": "UP"
}




~$ curl http://localhost:9999/actuator/health/readiness

{
    "status": "UP"
}






Metrics


Le endpoint /metrics est activé par défaut.
Sa racine fournit une liste de noms, sans valeur.



~$ curl http://localhost:9999/actuator/metrics

{
  "names": [
    "application.ready.time",
    "application.started.time",
    "disk.free",
    "disk.total",
    "executor.active",
    "executor.completed",
    "executor.pool.core",
    "executor.pool.max",
    "executor.pool.size",
    ...
  ]
}



La structure des noms est unidimensionnelle, à la façon de prometheus.
Les informations les plus couramment utilisées concernent la ou les datasource(s) (hikaricp.connections.*), la mémoire (jvm.memory.*), le ramasse-miettes (jvm.gc.*) ou la CPU (process.cpu.usage).
Chaque nom peut ensuite être utilisé pour accéder à une métrique unitaire.



~$ curl http://localhost:9999/actuator/metrics/jvm.memory.used

{
  "name": "jvm.memory.used",
  "description": "The amount of used memory",
  "baseUnit": "bytes",
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 383617384
    }
  ],
  "availableTags": [
    {
      "tag": "area",
      "values": [
        "heap",
        "nonheap"
      ]
    },
    {
      "tag": "id",
      "values": [
        "G1 Survivor Space",
        "Compressed Class Space",
        "Metaspace",
        "CodeCache",
        "G1 Old Gen",
        "G1 Eden Space"
      ]
    }
  ]
}



Comme on le voit dans cet exemple, les métriques sont organisées via des tags, qu&#8217;on peut utiliser en paramètre des requêtes.



~$ curl http://localhost:9999/actuator/metrics/jvm.memory.used?tag=area:heap

{
  "name": "jvm.memory.used",
  "description": "The amount of used memory",
  "baseUnit": "bytes",
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 132698240
    }
  ],
  "availableTags": [
    {
      "tag": "id",
      "values": [
        "G1 Survivor Space",
        "G1 Old Gen",
        "G1 Eden Space"
      ]
    }
  ]
}



Métriques Spring MVC

La métrique http.server.requests des requêtes traitées par Spring MVC est activée par défaut.
Elle peut être désactivée dans la configuration.



management:
  metrics:
    web:
      server:
        request:
          autotime:
            enabled: false



Sans tag, elle fournit des statistique globales, avec le nombre de requêtes traitées, le temps total de traitement et la durée de traitement maximal pour une requête.



~$ curl http://localhost:9999/actuator/metrics/http.server.requests

{
  "name": "http.server.requests",
  "baseUnit": "seconds",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 754
    },
    {
      "statistic": "TOTAL_TIME",
      "value": 134.09375702
    },
    {
      "statistic": "MAX",
      "value": 0.76434196
    }
  ]
}



On peut ensuite accéder à des données plus précises via les tags method, uri et status.



~$ curl http://localhost:9999/actuator/metrics/http.server.requests?tag=uri:/secured/roles&amp;tag=method:GET

{
  "name": "http.server.requests",
  "description": null,
  "baseUnit": "seconds",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 56
    },
    {
      "statistic": "TOTAL_TIME",
      "value": 5.121863713
    },
    {
      "statistic": "MAX",
      "value": 0.143918815
    }
  ]
}



C&#8217;est pas vraiment pratique pour détecter les requêtes lentes, mais on a pas mal de détails.



Métriques Spring Data

La métrique spring.data.repository.invocations des repositories de Spring Data est activée par défaut.
Elle peut être désactivée dans la configuration.



management.metrics.data.repository.autotime.enabled=false



Son fonctionnement est similaire à celui de Spring MVC, avec une vue globale.



~$ curl http://localhost:9999/actuator/metrics/spring.data.repository.invocations

{
  "name": "spring.data.repository.invocations",
  "description": null,
  "baseUnit": "seconds",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 293
    },
    {
      "statistic": "TOTAL_TIME",
      "value": 2.293597305
    },
    {
      "statistic": "MAX",
      "value": 0
    }
  ]
}



On retrouve aussi des tags pour entrer dans les détails.



~$ curl http://localhost:9999/actuator/metrics/spring.data.repository.invocations?tag=repository:ClientRepository'&amp;'method=findAll




Statistiques Hibernate

Les statistiques d&#8217;Hibernate sont désactivées par défaut.
Pour les avoir dans les métriques, il faut les activer et ajouter l&#8217;extension Micrometer.


Pour activer les statistiques Hibernate, sans les publier dans actuator:



spring:
  jpa:
    properties:
      hibernate:
        generate_statistics: true



Puis pour les publier, il faut ajouter une dépendance vers hibernate-micrometer.



    &lt;dependency&gt;
        &lt;groupId&gt;org.hibernate&lt;/groupId&gt;
        &lt;artifactId&gt;hibernate-micrometer&lt;/artifactId&gt;
        &lt;version&gt;${hibernate.version}&lt;/version&gt;
    &lt;/dependency&gt;




Micrometer

La mécanique interne des métriques de l'actuator s&#8217;appuie sur Micrometer, avec la publication sur le endpoint /actuator/metrics.


Grâce à Micrometer, il est aussi possible d&#8217;ajouter des métriques personnalisées.
Depuis n&#8217;importe quel bean, en injectant MeterRegistry, on peut ajouter les métriques de son choix.



  private final Counter createClientCallCounter;

  public ClientService(MeterRegistry meterRegistry) {
      Counter createClientCallCounter = Counter.builder("jtips.client.create")
                                              .description("Nombre de création de Client")
                                              .register(meterRegistry);
  }

  public Client create(Client client) {
    createClientCallCounter.increment();
    ...
  }



L&#8217;autre solution, plus modulaire, c&#8217;est de publier un bean qui implémente MeterBinder et qui publie un ensemble de meters.


Grâce à Micrometer, les métriques peuvent être publiées sur de nombreux outils de monitoring externes comme Graphite, Prometheus ou Elastic.



JMX

Les métriques peuvent être publiées en JMX, en plus du endpoint.
Pour l&#8217;activer', il faut ajouter une dépendance vers io.micrometer:micrometer-registry-jmx.



  &lt;dependency&gt;
    &lt;groupId&gt;io.micrometer&lt;/groupId&gt;
    &lt;artifactId&gt;micrometer-registry-jmx&lt;/artifactId&gt;
    &lt;version&gt;${micrometer.version}&lt;/version&gt;
  &lt;/dependency&gt;



Par défaut, les informations sont dans le domaine metrics, ce qui peut être modifié.



management:
  jmx:
    metrics:
      export:
        domain: info.jtips






Prometheus


Les métriques peuvent aussi être publiées au format Prometheus.
Il suffit d&#8217;ajouter une dépendance vers io.micrometer:micrometer-registry-prometheus pour activer le endpoint /prometheus.



  &lt;dependency&gt;
    &lt;groupId&gt;io.micrometer&lt;/groupId&gt;
    &lt;artifactId&gt;micrometer-registry-prometheus&lt;/artifactId&gt;
    &lt;version&gt;1.11.3&lt;/version&gt;
  &lt;/dependency&gt;



Les données exposées ne sont pas les mêmes que pour le endpoint /metrics.
Elles viennent d&#8217;un registre spécifique.




Dump


Le endpoint /threaddump publie un export de l&#8217;ensemble des threads au format JSON.
On peut demander une réponse au format traditionnel avec l&#8217;en-tête "Accept": "text/plain".



curl http://127.0.0.1:9999/actuator/threaddump --header 'Accept: text/plain'



Le endpoint /heapdump publie un export de la mémoire au format binaire.



curl http://127.0.0.1:9999/actuator/heapdump -o heap.dump





Info


Le endpoint /info publie des informations statiques de l&#8217;application.
Les informations sont publiées par l&#8217;intermédiaire de contributeurs qui sont tous désactivés par défaut.
De ce fait, sans autre configuration, le résultat reste vide.


Le contributeur env publie l&#8217;ensemble des propriétés info.*.
Il était activé par défaut jusqu&#8217;à Spring Boot 2.5 et a été désactivé dans les versions suivantes (attention aux migrations).



management:
  info:
    env:
      enabled: true

info:
  application:
    name: JTips examples
    version: 1.2.3




~$ curl http://localhost:9999/actuator/info

{
  "application": {
    "name": "JTips examples",
    "version": "1.2.3"
  }
}



Le contributeur java publie des informations du runtime Java.



management.info.java.enabled=true




~$ curl http://localhost:9999/actuator/info

{
  "java": {
    "vendor": "Private Build",
    "version": "17.0.5",
    "runtime": {
      "name": "OpenJDK Runtime Environment",
      "version": "17.0.5"
    },
    "jvm": {
      "name": "OpenJDK 64-Bit Server VM",
      "vendor": "Private Build",
      "version": "17.0.5"
    }
  }
}



Le contributeur build publie les informations du fichier /META-INF/build-info.properties généré au moment du build.


Ça se configure avec Maven (ou gradle).



  &lt;build&gt;
    &lt;plugins&gt;
      &lt;plugin&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
        &lt;executions&gt;
          &lt;execution&gt;
            &lt;goals&gt;
              &lt;goal&gt;build-info&lt;/goal&gt;
            &lt;/goals&gt;
          &lt;/execution&gt;
        &lt;/executions&gt;
      &lt;/plugin&gt;
    &lt;/plugins&gt;
  &lt;/build&gt;



Ce contributeur est activé par défaut et publie les informations à condition que le fichier soit présent.



~$ curl http://localhost:9999/actuator/info

{
  "build": {
    "artifact": "spring-boot-example",
    "name": "JTips examples with Spring Boot",
    "time": 1672075975.823000000,
    "version": "1.2.3",
    "group": "info.jtips"
  }
}



Le contributeur git publie les informations présentes dans le fichier git.properties généré au moment du build.



  &lt;build&gt;
    &lt;plugins&gt;
      &lt;plugin&gt;
        &lt;groupId&gt;io.github.git-commit-id&lt;/groupId&gt;
        &lt;artifactId&gt;git-commit-id-maven-plugin&lt;/artifactId&gt;
        &lt;version&gt;5.0.0&lt;/version&gt;
        &lt;executions&gt;
          &lt;execution&gt;
            &lt;goals&gt;
              &lt;goal&gt;revision&lt;/goal&gt;
            &lt;/goals&gt;
          &lt;/execution&gt;
        &lt;/executions&gt;
        &lt;configuration&gt;
          &lt;generateGitPropertiesFile&gt;true&lt;/generateGitPropertiesFile&gt;
        &lt;/configuration&gt;
      &lt;/plugin&gt;
    &lt;/plugins&gt;
  &lt;/build&gt;



Ce contributeur est activé par défaut et publie les informations à condition que le fichier soit présent.



~$ curl http://localhost:9999/actuator/info

{
  "git": {
    "branch": "main",
    "commit": {
      "id": "0cff7b1",
      "time": 1671009439.000000000
    }
  }
}










Les informations publiées par ce endpoint sont disponibles sous forme de beans, de type EnvironmentInfoContributor, GitInfoContributor, BuildInfoContributor et JavaInfoContributor.






Il est possible d&#8217;ajouter des informations personnalisées en publiant un bean de type InfoContributor.




Env


Depuis Spring Boot 3, les valeurs fournies par le endpoint /env sont cachées.
Pour les afficher, il faut configurer la propriété management.endpoint.env.show-values avec pour valeurs possibles:




ALWAYS, pour voir les valeurs,


NEVER, par défaut,


WHEN_AUTHORIZED, si la requête est authentifiée.




Il y a le même comportement pour /configprops, et la propriété management.endpoint.configprops.show-values équivalente.


Lorsqu&#8217;on décide d&#8217;afficher les valeurs, on se retrouve avec des mots de passe ou des secrets qu&#8217;on ne souhaiterait pas montrer.
Pour choisir quelles valeurs doivent être cachées malgré tout, il faut créer un bean qui implémente SanitizingFunction.



@Configuration
public class ActuatorConfiguration {
  @Value("${application.actuator.keys-to-sanitize:}")
  private String[] excludedKeys;

  @Bean
  public SanitizingFunction actuatorSanitizingFunction {
    return data -&gt; {
      if (Arrays.stream(excludedKeys)
                .anyMatch(excludedKey -&gt; data.getKey().equals(excludedKey))) {
          return data.withValue(SanitizableData.SANITIZED_VALUE);
      }
      return data;
    };
  }
}





HttpExchanges


Le endpoint /httpexchanges de Spring Boot 3 remplace /httptrace de Spring Boot 2.
Il permet de consulter le détail des requêtes HTTP reçues par l&#8217;application.


Dans les deux cas, le endpoint n&#8217;est pas activé par défaut.
Il faut déclarer un bean de type HttpExchangeRepository.



@Configuration
public class ActuatorConfiguration {
  @Bean
  public HttpExchangeRepository httpExchangeRepository() {
    return new InMemoryHttpExchangeRepository();
  }

  ...
}



InMemoryHttpExchangeRepository est la seule implémentation fournie pas Spring.
Elle conserve les 100 denières requêtes en mémoire.




AuditEvents


Le endpoint /auditevents permet de consulter les événements d&#8217;authentification.
Il s&#8217;active de la même façon que /httpexchanges, en déclarant un bean de type AuditEventRepository.



@Configuration
public class ActuatorConfiguration {
  ...

  @Bean
  public AuditEventRepository auditEventRepository() {
    return new InMemoryAuditEventRepository();
  }
}



InMemoryAuditEventRepository est la seule implémentation fournie pas Spring.
Elle conserve les 100 deniers événements en mémoire.




Loggers


Le endpoint /loggers publie les détails de tous les loggers de l&#8217;application.


Avec une requête GET, on peut obtenir la configuration d&#8217;un logger unique.



~$ curl http://localhost:8081/actuator/loggers/com.meshimer.cfws

{
    "configuredLevel": "INFO",
    "effectiveLevel": "INFO"
}



Avec une requête POST, on peut changer la configuration d&#8217;un logger, à chaud.



~$ curl --request POST http://localhost:8081/actuator/loggers/info.jtips   \
        --data '{ "configuredLevel": "DEBUG" }'



Après cette requête, les logs de debug sont écrits, jusqu&#8217;au prochain redémarrage.
Pour l&#8217;annuler, on peut envoyer la requête suivante:



~$ curl --request POST http://localhost:8081/actuator/loggers/info.jtips   \
        --data '{}'



Si le système de logs est configuré avec une sortie fichier (propriété logging.file.name), on peut en demander le contenu sur le endpoint /logfile.



~$ curl --header 'Accept: text/plain' http://localhost:8081/actuator/logfile

2023-12-08T21:25:48.866Z  INFO 632981 --- [restartedMain] info.jtips.Application    : Starting Application using Java 17 with PID 632981
2023-12-08T21:25:48.868Z  INFO 632981 --- [restartedMain] info.jtips.Application    : The following 1 profile is active: "local"
2023-12-08T21:25:49.738Z  INFO 632981 --- [restartedMain] o.s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-12-08T21:25:50.058Z  INFO 632981 --- [restartedMain] o.s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 310 ms. Found 65 JPA repository interfaces.
...





Endpoint personnalisé


On peut aussi créer nos propres endpoints.


Technique

Pour ça, il faut créer un bean annoté avec @Endpoint, avec au moins une opération, annotatée avec @ReadOperation, @WriteOperation ou @DeleteOperation.



@Component
@Endpoint(id = "echo")
public class EchoEndpoint {
    @ReadOperation
    public String echo(@Selector String input) {
        return "Hello " + input;
    }
}



L&#8217;annotation @Selector permet de gérer des path parameters, alors qu&#8217;un parameter de méthode sans annotation gère un paramètre de requête.


Comme pour un endpoint applicatif, on peut affiner ses capacités.
Par exemple, on peut choisir le type de réponse dans l&#8217;annotation @ReadOperation, faire une opération par type accepté.


Plutôt que de retourner un objet simple, on peut retourner un WebEndpointResponse&lt;?&gt;, en choisissant le status code.
Par contre, on ne peut pas modifier les headers de la réponse.



@Component
@Endpoint(id = "echo")
public class EchoEndpoint {
    @ReadOperation(produces = "text/plain")
    public WebEndpointResponse&lt;String&gt; echoSimple(@Selector String input) {
        return new WebEndpointResponse&lt;&gt;("Hello " + input);
    }

    @ReadOperation(produces = "application/json")
    public WebEndpointResponse&lt;Message&gt; echoJson(@Selector String input) {
        return new WebEndpointResponse&lt;&gt;(new Message("Hello", input));
    }
    public record Message(String hi, String dest) {}
}




Exemple

J&#8217;ai utilisé cette technique pour créer un endpoint de métriques qui utilise le registre Prometheus et qui est capable d&#8217;exposer les données au format Prometheus, en JSON ou en CSV.



/
 * An @Endpoint for exposing aggregated metrics from Prometheus registry.
 * 
 * Can be requested in JSON format (default), in CSV (text/csv) or in Prometheus format ("text/plain").
 */
@Component
@Endpoint(id = "metrix")
public class MetrixEndpoint {

  private final PrometheusScrapeEndpoint prometheusScrapeEndpoint;
  private final CollectorRegistry collectorRegistry;

  public MetrixEndpoint(PrometheusScrapeEndpoint prometheusScrapeEndpoint, CollectorRegistry collectorRegistry) {
    this.prometheusScrapeEndpoint = prometheusScrapeEndpoint;
    this.collectorRegistry = collectorRegistry;
  }

  /
    * JSON format, default
    */
  @ReadOperation()
  public List json(@Nullable Set includedNames) {
    Enumeration samples = (includedNames != null)
            ? this.collectorRegistry.filteredMetricFamilySamples(includedNames)
            : this.collectorRegistry.metricFamilySamples();
    return Collections.list(samples);
  }

  /
    * Prometheus format ("text/plain")
    */
  @ReadOperation(producesFrom = TextOutputFormat.class)
  public WebEndpointResponse prometheus(TextOutputFormat format, @Nullable Set includedNames) {
    return prometheusScrapeEndpoint.scrape(format, includedNames);
  }

  /
    * CSV format ("text/csv")
    */
  @ReadOperation(produces = "text/csv")
  public String csv(@Nullable Set includedNames) {
    return json(includedNames).stream()
            .flatMap(metric -> metric.samples.stream().map(sample -> new MetricSample(metric, sample)))
            .map(MetricSample::toCsv)
            .collect(Collectors.joining());
  }

  @ReadOperation
  public Collector.MetricFamilySamples single(@Selector String requiredMetricName) {
    return this.collectorRegistry.filteredMetricFamilySamples(Set.of(requiredMetricName)).nextElement();
  }

  private record MetricSample(Collector.MetricFamilySamples metric, Collector.MetricFamilySamples.Sample sample) {
    String toCsv() {
      Stream labels = StreamUtils.zip(
              sample.labelNames.stream(),
              sample.labelValues.stream(),
              (name, value) -> String.format("%s=\"%s\"", name, value));
      return "%s;%s;%s;%s%n"
              .formatted(sample.name, labels.collect(Collectors.joining(",")), sample.value, metric.unit);
    }
  }

}






Références


Les exemples de cette page ont été testés avec Spring Boot 2.7 et Spring Boot 3.1.




Spring Boot, Production-ready Features


Spring Boot Actuator Web API


Micrometer


Health Indicators in Spring Boot (Baeldung)


Spring Boot Custom Health Indicators (SpringHow)








1. Spring Boot 3


2. Spring Boot 2

</description>
          <pubDate>2022-11-27T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/Actuator</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/Actuator</guid>
        </item>
      
    
      
        <item>
          <title>Migration vers Elytron</title>
          <description>
Entre les versions 11 et 25 de WildFly, les sous-systèmes elytron et security ont cohabité.
C&#8217;est le cas aussi pour JBoss EAP 7.x.


Le script ci-dessous permet de migrer une configuration complète vers elytron et de supprimer le sous-système security.



batch

# Management
/core-service=management/management-interface=http-interface                    \
    :undefine-attribute(name=security-realm)
/core-service=management/management-interface=http-interface                    \
    :write-attribute(name=http-authentication-factory,                          \
                     value=management-http-authentication)
/core-service=management/management-interface=http-interface                    \
    :write-attribute(name=http-upgrade.sasl-authentication-factory,             \
                     value=management-sasl-authentication)

# Undertow
/subsystem=undertow/server=default-server/https-listener=https                  \
    :write-attribute(name=ssl-context, value=applicationSSC)
/subsystem=undertow/server=default-server/https-listener=https                  \
    :undefine-attribute(name=security-realm)

/subsystem=elytron/http-authentication-factory=application-http-authentication  \
    :add(security-domain=ApplicationDomain, http-server-mechanism-factory=global)
/subsystem=elytron/http-authentication-factory=application-http-authentication  \
    :write-attribute(                                                           \
        name=mechanism-configurations,                                          \
        value=[{mechanism-name=BASIC,                                           \
                mechanism-realm-configurations=[{realm-name=ApplicationRealm}]}])
/subsystem=undertow/server=default-server/host=default-host/setting=http-invoker\
    :undefine-attribute(name=security-realm)
/subsystem=undertow/server=default-server/host=default-host/setting=http-invoker\
    :write-attribute(name=http-authentication-factory,                          \
                     value=application-http-authentication)

# Remoting
/subsystem=elytron/http-authentication-factory=application-sasl-authentication  \
    :add(security-domain=ApplicationDomain, http-server-mechanism-factory=global)
/subsystem=elytron/http-authentication-factory=application-sasl-authentication  \
    :write-attribute(                                                           \
        name=mechanism-configurations,                                          \
        value=[{mechanism-name=JBOSS-LOCAL-USER,                                \
                realm-mapper="local"},                                          \
               {mechanism-name=DIGEST-MD5,                                      \
                mechanism-realm-configurations=[{realm-name=ApplicationRealm}]}])
/subsystem=remoting/http-connector=http-remoting-connector                      \
    :undefine-attribute(name=security-realm)
/subsystem=remoting/http-connector=http-remoting-connector                      \
    :write-attribute(name=sasl-authentication-factory,                          \
                     value=application-sasl-authentication)

# Messaging
/subsystem=messaging-activemq/server=default                                    \
    :write-attribute(name=elytron-domain, value=ApplicationDomain)

# EJB3
/subsystem=ejb3/application-security-domain=other                               \
    :add(security-domain=ApplicationDomain)

# Cleaning
/core-service=management/security-realm=ApplicationRealm:remove
/core-service=management/security-realm=ManagementRealm:remove
/subsystem=security:remove

run-batch



Script testé avec WildFly 22 et JBoss EAP 7.4.7, en profil full.
Pour le passer en profil par défaut, il faut supprimer la commande qui concerne /subsystem=messaging-activemq.


L&#8217;utilisation de ce script est particulièrement utile avec JBoss EAP 7.4 depuis son support de JDK 17 (&gt;= 7.4.7).
En effet, les anciens security domains ne sont plus supportés depuis le JDK 14.
</description>
          <pubDate>2022-10-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WildFly/ElytronMigration</link>
          <guid isPermaLink="true">https://www.jtips.info/WildFly/ElytronMigration</guid>
        </item>
      
    
      
        <item>
          <title>Scripts pre-request et test dans Postman</title>
          <description>
Les scripts pre-request permettent d&#8217;exécuter du code JavaScript avant une requête, ou avant n&#8217;importe quelle requête d&#8217;une collection.


Les scripts test permettent d&#8217;exécuter du code JavaScript après une requête, ou après n&#8217;importe quelle requête d&#8217;une collection.


OIDC - get token


J&#8217;utilise les scripts test pour sauvegarder les tokens JWT après l&#8217;authentification.



if (pm.response.code &lt; 300) {
  pm.collectionVariables.set("access_token", pm.response.json().access_token);
  pm.collectionVariables.set("refresh_token", pm.response.json().refresh_token);
}



Ça permet à n&#8217;importe quelle autre requête d&#8217;être authentifiée grâce à un bearer token et à la variable {{access_token}}.




OIDC - refresh token


Les scripts pre-request peuvent être utilisés pour renouveler automatiquement un token d&#8217;accès.


Decode

La première étape est de récupérer le token dans sa variable et de le décoder.



const access_token = pm.collectionVariables.get('access_token');
const [header, payload, signature] = access_token.split('.');
const decodedPayload = JSON.parse(
  CryptoJS.enc.Utf8.stringify(
    CryptoJS.enc.Base64.parse(payload)
  ));




Expiry

On peut ensuite vérifier l&#8217;expiration du token.



if (decodedPayload.exp &lt; new Date().getTime()/1000) {
  // ...
}




Refresh

Et si le token a expiré, on envoie une requête de renouvellement au serveur d&#8217;autorisation, en lui passant



    pm.sendRequest({
        url:  'https://&lt;host&gt;:&lt;port&gt;/auth/realms/&lt;realm&gt;/protocol/openid-connect/token',
        method: 'POST',
        header: {
            'Accept': 'application/json',
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: {
            mode: 'formdata',
            formdata: [
                {'key': 'refresh_token',  'value': pm.collectionVariables.get('refresh_token')},
                {'key': 'grant_type',  'value': 'refresh_token'},
                {'key': 'client_id',  'value': 'jtips'},
                {'key': 'client_secret',  'value': pm.collectionVariables.get('client_secret')}
            ]
        }
    }, function (err, res) {
        if (!err) {
            pm.collectionVariables.set("access_token", res.json().access_token);
            pm.collectionVariables.set("refresh_token", res.json().refresh_token);
        } else {
            console.log('err:', err);
        }
    });




Bearer

Ce script peut être placé au niveau de la requête ou de la collection.
Dans ce cas, la requête n&#8217;est utile que pour les requêtes qui utilisent le token.
On peut le vérifier en début de script.



if (!pm.request.auth || pm.request.auth.type !== 'bearer') {
    return;
}




Synthèse

Voici le script que j&#8217;utilise au niveau de ma collection.



// La requête a-t-elle besoin d'un jeton ?
if (!pm.request.auth || pm.request.auth.type !== 'bearer') {
    console.log('Skip oidc');
    return;
}

// Est-ce qu'il y a déjà un jeton d'accès ?
const access_token = pm.collectionVariables.get('access_token');
if (!access_token) {
    console.log('No access token');
    return;
}

// On décode le jeton...
const [header, payload, signature] = access_token.split('.');
const decodedPayload = JSON.parse(
  CryptoJS.enc.Utf8.stringify(
    CryptoJS.enc.Base64.parse(payload)
  ));

// Est-ce qu'il a expiré ?
if (decodedPayload.exp &lt; new Date().getTime()/1000) {
    console.log('Access token expired');
    // On envoie la requête de renouvellement de jeton
    pm.sendRequest({
        url:  'http://localhost:8081/oauth2/token',
        method: 'POST',
        header: {
            'Accept': 'application/json',
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: {
            mode: 'formdata',
            formdata: [
                {'key': 'refresh_token',  'value': pm.collectionVariables.get('refresh_token')},
                {'key': 'grant_type',  'value': 'refresh_token'}
            ]
        }
    }, function (err, res) {
        // Si tout va bien, on remplace les anciens jetons
        if (!err) {
            pm.collectionVariables.set("access_token", res.json().access_token);
            pm.collectionVariables.set("refresh_token", res.json().refresh_token);
        } else {
            console.log('err:', err);
        }
    });

}




</description>
          <pubDate>2022-09-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Postman/script</link>
          <guid isPermaLink="true">https://www.jtips.info/Postman/script</guid>
        </item>
      
    
      
        <item>
          <title>Opérateurs de temps dans RxJS</title>
          <description>
Créateurs


interval

Toutes les x ms, il émet un élément entier, incrémenté.
Le premier élément est émis après x ms.




interval(300)


---1---2---3---4--





timer

Il émet un élément après x ms.
Éventuellement, il peut émettre d&#8217;autres éléments toutes les y ms.




timer(200)


--1-|-------------------






timer(200, 300)


--1---2---3---4--




timer(500, 500) &lt;&#8658; interval(500)



setInterval vs interval





Opérateurs


delay

retarde chaque élément, ne filtre pas, ne transforme pas




--1---2---3---4----


delay(300)


-----1---2---3---4-





throttleTime

Il laisse passer le dernier élément émis toutes les x ms
Le premier élément est émis sans délai.




--1-2---3--4---


throttleTime(500)


--1-----2-----4--





auditTime

Il laisse passer le dernier élément émis au cours des x ms.




--1-2----3--4---


throttleTime(1000)


----------2-------4-










auditTime(ms) &#8656;&#8658; audit) &#8658; timer(ms






sampleTime

Il laisse passer le dernier élément émis au cours des x ms.
(pas vraiment compris la différence avec auditTime)




--1-2----3--4---


throttleTime(1000)


----------2-------4-





debounceTime

Il laisse passer un élément après x ms d&#8217;inactivité.




--1-2----3---4--


debounceTime(500)


-----------2----3---





timeout



</description>
          <pubDate>2022-06-06T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/RxJS/Time</link>
          <guid isPermaLink="true">https://www.jtips.info/RxJS/Time</guid>
        </item>
      
    
      
        <item>
          <title>Principaux opérateurs de RxJS</title>
          <description>
Création d&#8217;un observable


Hors opérateurs

Dans Angular, les observables viennent souvent du client HTTP.



const product$ = this.httpClient.get&lt;Product&gt;(
    ${API_v1_URL}/product/${id}
  );



Ils sont parfois créés sous la forme de Subject, pour pouvoir appeler la méthode next plus tard.
La classe Subject&lt;?&gt; hérite de la classe Observable&lt;?&gt;.



const reload$ = new Subject&lt;boolean&gt;();
...
reload$.next(true);




from

from crée un observable à partir d&#8217;un table




from([1, 2, 3, 4, 5])


--1---2---3---4---5--|--





from([1, 2, 3, 4, 5])
    .subscribe(val =&gt; console.log(val));




of

of crée un observable à partir d&#8217;une série de valeurs




of(1, 2, 3, 4, 5)


--1---2---3---4---5--|--





of(1, 2, 3, 4, 5)
    .subscribe(val =&gt; console.log(val));




ajax

ajax envoie une requête HTTP.
C&#8217;est ce qui se rapproche le plus de ce qu&#8217;on fait avec Angular.



ajax('https://gitlab.com/api/v4/projects')
    .subscribe(val =&gt; console.log(val));






Filtrage


Les opérateur de filtre sont importés de rxjs/operators doivent être appelé dans pipe(&#8230;&#8203;).
Ils réduisent le nombre d&#8217;éléments, mais n&#8217;arrêtent pas l&#8217;observable.


filter

filter exlut les éléments qui ne satisfont pas au prédicat.




--1---2---3---4---5--|--


filter(val &#8658; val % 2 === 0)


--------2---------4--------|--





of(1, 2, 3, 4, 5)
  .pipe(filter((val) =&gt; val % 2 === 0))
  .subscribe((val) =&gt; console.log(val));



ignoreElements serait une version extrème de filter puisqu&#8217;il exclut tous les éléments.
On l&#8217;utilise quand on ne veut s&#8217;intéresser qu&#8217;à l&#8217;arrêt, normal ou en erreur.




--1---2---3---4---5--|--


ignoreElements()


-------------------------------|--





of(1, 2, 3, 4, 5)
  .pipe(ignoreElements())
  .subscribe({
    next: (val) =&gt; console.log('next should never happen', val),
    complete: () =&gt; console.log('complete'),
  });




skip, skipUntil, skipWhile

skip ignore les premiers éléments émits.
C&#8217;est le contraire de take.
skipUntil est le contraire de takeUntil, il ignore tous les éléments tant que l&#8217;observable secondaire n&#8217;a rien émit.
Enfin skipWhile est le contraire de takeWhile, il ignore tous les éléments tant que l&#8217;observable secondaire émet.




--1---2---3---4---5--|--


skip(2)


--------------3---4---5--|--





of(1, 2, 3, 4, 5)
  .pipe(skip(2)
  .subscribe((val) =&gt; console.log(val));




debounce, debounceTime

Avec debounceTime, l&#8217;observable élimine les éléments qui arrivent trop vite.
Le compteur de temps est remis à zéro à chaque élément, le suivant n&#8217;est émis que s&#8217;il arrive après le délai spécifié.
Ça ressemble un peu à un timeout à l&#8217;envers.
Avec debounce, la durée est contrôlée par un observable secondaire, ce qui lui permet de varier dans le temps.




--1------2-3--4--------5--|-


debounceTime(100)


--------1----------------4------|-





interval(100)
  .pipe(
    concatMap(() =&gt; timer(Math.random() * 1000)),
    take(20),
    debounceTime(500)
  )
  .subscribe((val) =&gt; console.log('ok'));




timeout

Avec timeout, si la source n&#8217;émet aucun élément pendant le délai, le flux est arrêté en erreur.




--1-2-3---------5-----|-


timeout(200)


--1-2-3------X-----------




Pour faire un timeout sans erreur, on peut l&#8217;associer à un catchError.



interval(100)
  .pipe(
    concatMap(() =&gt; timer(Math.random() * 1000)),
    take(20),
    timeout(300),
    catchError((error) =&gt; of(error.message))
  )
  .subscribe({
    next: (val) =&gt; console.log('timeout', val),
  });




distinct

distinct exclut les doublons, avec la possibilité de passer une fonction d&#8217;extraction de clé de comparaison.




--1--2--3--1--1--4--|-


distinct()


--1--2--3------------4--|-




distinctUntilChanged et distinctUntilKeyChanged éliminent aussi les doublons, mais uniquement s&#8217;ils se suivent.




--1--2--3--1--1--4--|-


distinctUntilChanged()


--1--2--3--1-------4--|-





of(1, 2, 3, 2, 3, 4)
  .pipe(distinct())
  .subscribe((val) =&gt; console.log(val));






Transformation


Les opérateur de transformation sont importés de rxjs/operators doivent être appelé dans pipe(&#8230;&#8203;).


map

map permet la transformation des éléments de la source en éléments d&#8217;un autre type.




--1--2--3--1--1--4--|-


map(x &#8658; x*2)


--2--4--6--2--2--8--|-





of(1, 2, 3, 4)
  .pipe(map(val =&gt; val * 2))
  .subscribe((val) =&gt; console.log(val));




flatMap

mergeMap, concatMap, switchMap, exhaustMap sont dans la famille des flat maps, dans le sens où il permettent la transformation des éléments de la source en observables, pour ils applatissent le résultat en enlevant une couche d&#8217;observable.





Combinaison


Les opérateur de combinaisons simples sont appelés de façon statique, avec un ensemble d&#8217;observables en paramètre.
Les opérateur xxxAll() sont utilisés sur une instance dans pipe(&#8230;&#8203;).


Combinaisons statiques

concat exécute plusieurs observables en séquence.




--1--2--3--|-


--A--B--C--|-


--X--Y--|------


concat


--1--2--3--A--B--C--X--Y--|-





concat(
    of(1, 2, 3),
    of('A', 'B', 'C'),
    of('X', 'Y')
  )
  .subscribe((val) =&gt; console.log(val));



zip exécute plusieurs observables de façon concurrente, attend que tous aient produit un élément et les combine.
L&#8217;observable combiné est arrêté lorsque le premier sous-observable s&#8217;arrête.




--1--2--3--|--


---A--B--C--|-


--X----Y--|-----


zip


---1AX---2BY--|-----





zip(
    of(1, 2, 3),
    of('A', 'B', 'C'),
    of('X', 'Y')
  )
  .subscribe((val) =&gt; console.log(val));



forkJoin exécute aussi plusieurs observables de façon concurrente, attend que tous soient terminés et produit un résultat avec le dernier élément de chaque observable.
Il ressemble à zip lorsque tous les sous-observables ne produisent qu&#8217;un élément.




--1--2--3--|-


--A--B--C--|-


--X--Y--|-


forkJoin


-----------------3--C--Y--|-





forkJoin([
    of(1, 2, 3),
    of('A', 'B', 'C'),
    of('X', 'Y')
  ])
  .subscribe((val) =&gt;
    console.log('forkJoin', val)
  );



L&#8217;opérateur merge exécute plusieurs observables de façon concurrente, mais sans combiner les éléments.
Il les restitue simplement dans l&#8217;ordre d&#8217;arrivée.




--1--------2----3-|-----------


----A--B--C-|-----------------


---X------------Y-|-------------


merge


--1-X-A-B-2-C-Y-3-|-





merge(
    of(1, 2, 3),
    of('A', 'B', 'C'),
    of('X', 'Y'))
  .subscribe((val) =&gt; console.log(val)
);



Avec l&#8217;opérateur race seul l&#8217;observable qui produit son premier élément est utilisé.
Les autres observables sont stoppés.




--1--------2----3-|-


----A--B--C-|-------


---X------------Y-|---


race


--1--------2----3-|-





race(
    of(1, 2, 3),
    of('A', 'B', 'C'),
    of('X', 'Y'))
  .subscribe((val) =&gt; console.log(val)
);




Combinaisons avec pipe(&#8230;&#8203;)

Avec withLatestFrom, chaque élément de l&#8217;observable principal est combiné avec le dernier élément produit par l&#8217;observable secondaire.
Ainsi, un élément de l&#8217;observable secondaire peut apparaître plusieurs fois dans le résultat.
combineAll fonctionne comme withLatestFrom avec un ensemble de sous-observables.




--1--2-3----4-|-


----A------B--C-|-


withLatestFrom


-------A2-A3----C4-|-





of(1, 2, 3)
  .pipe(withLatestFrom(of('A', 'B', 'C')))
  .subscribe((val) =&gt; console.log(val));



concatAll fonctionne comme concat.
mergeAll fonctionne comme merge.





Arrêt


Les opérateurs take, takeUntil et takeWhile permettent d&#8217;interrompre l&#8217;émission d&#8217;éléments d&#8217;un observable.
Ils sont importés de rxjs/operators doivent être appelé dans pipe(&#8230;&#8203;).


Avec take, on spécifie le nombre d&#8217;éléments au bout duquel l&#8217;observable arrête d&#8217;émettre.
Il fonctionne à l&#8217;opposé de skip.



interval(100)
  .pipe(take(5))
  .subscribe((val) =&gt; console.log(val));



Avec takeUntil, l&#8217;observable principal arrête d&#8217;émettre lorsque l&#8217;observable secondaire commence.
Ça peut être un timer, un sujet déclenché manuellement ou une autre source d&#8217;événements.




--1---2---3---4---5-|--


------------------X-|----------


takeUntil


--1---2---3--|-------------




takeWhile fonctionne avec un prédicat.
Au premier élément pour lequel il n&#8217;est plus satisfait, l&#8217;observable s&#8217;arrête.


Avec first, l&#8217;observable est arrêté au premier élément, s&#8217;il n&#8217;y a pas de prédicat, ou le premier qui satisfait le prédicat.
first() sans paramètre ressemble à take(1), à ceci près que first() sort en erreur s&#8217;il n&#8217;y a aucun élément.
L&#8217;opérateur single a aussi des ressemblances avec first, mais il sort aussi en erreur s&#8217;il y a plusieurs éléments qui satisfont au prédicat.
Enfin, find ressemble aussi à first(&#8230;&#8203;) avec prédicat.




--1---2---3---4---5-|--


first()


--1-|--------------------------





of(1, 2, 3)
  .pipe(single())
  .subscribe({
    error: (err) =&gt; console.error('single', err),
  });



</description>
          <pubDate>2022-05-25T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/RxJS/Operators</link>
          <guid isPermaLink="true">https://www.jtips.info/RxJS/Operators</guid>
        </item>
      
    
      
        <item>
          <title>Opérateurs flat map dans RxJS</title>
          <description>
mergeMap, concatMap, switchMap, exhaustMap sont dans la famille des flat maps, dans le sens où il permettent la transformation des éléments de la source en observables, puis ils applatissent le résultat en enlevant une couche d&#8217;observable.


Pour illustrer les différences entre les quatre opérateurs, j&#8217;ai réutilisé le même exemple.


L&#8217;observable source produit un élément chaque seconde, pendant un peu plus de six secondes, soit six éléments.



export function source() {
  return interval(1000).pipe(
    takeUntil(timer(6100))
  );
}



Pour chaque élément, il est produit un sous-observable qui produit un élément tous les six dixièmes de seconde pendant deux secondes et demi, soit quatre éléments.



export function inner(prefix: any) {
  return interval(600).pipe(
    takeUntil(timer(2500)),
    map((item) =&gt; `${prefix}/${item}\`)
  );
}



Ces observables sont associés via les diffrents opérateurs détaillés ci-dessous.


concatMap


Pour chaque élément de l&#8217;observable source, on crée un nouvel observable.


Les sous-observables sont exécutés de façon séquentielle.
Chaque nouveau sous-observable attend que le précédent soit terminé avec de démarrer.




--A--------B--------C-|-----------------


--O--1--2-|--


concatMap


--AO--A1--A2-BO--B1--B2-CO--C1--C2-|-




Exemple


source()
  .pipe(concatMap((item) =&gt; inner(item)))
  .subscribe((response) =&gt; console.log(response));



Résultat :



0/0, 0/1, 0/2, 0/3, END inner,
1/0, 1/1, 1/2, 1/3, END inner, END source,
2/0, 2/1, 2/2, 2/3, END inner,
3/0, 3/1, 3/2, 3/3, END inner,
4/0, 4/1, 4/2, 4/3, END inner,
5/0, 5/1, 5/2, 5/3, END inner



On a les 24 résultats attendus, dans l&#8217;ordre.
Cet ordre a un prix, le traitement complet dûre plus longtemps que pour mergeMap.





mergeMap


Le fonctionnement est similaire à concatMap, mais sans attendre la complétion du précédent sous-observable.


Son ancien nom était flatMap.




--A--------B--------C-|---------------


--O--1--2-|--


mergeMap


--AO--A1--BO-A2-B1-CO-B2-C1---C2-|-




Exemple


source()
  .pipe(mergeMap((item) =&gt; inner(item)))
  .subscribe((response) =&gt; console.log(response));



Résultat :



0/0, 0/1, 1/0, 0/2, 1/1, 0/3, END inner,
2/0, 1/2, 2/1, 1/3, END inner,
3/0, 2/2, 3/1, 2/3, END inner,
4/0, 3/2, END source, 4/1, 3/3, END inner
5/0, 4/2, 5/1, 4/3, END inner,
5/2, 5/3, END inner



On a bien les 24 résultats attendus.
On constate aussi qu&#8217;ils ne sont pas séquentiels.





switchMap


Pour chaque élément de l&#8217;observable source, on crée un nouvel observable.
Le nouvel observable est interrompu à l&#8217;arrivée de chaque nouvel élément.




--A--------B--------C-|-------------


--O---1---2-|--


switchMap


--AO---A1--BO---B1--CO---C1---C2-|-




Exemple


source()
  .pipe(switchMap((item) =&gt; inner(item)))
  .subscribe((response) =&gt; console.log(response));



Résultat :



0/0, END inner,
1/0, END inner,
2/0, END inner,
3/0, END inner,
4/0, END inner, END source,
5/0, 5/1, 5/2, 5/3, END inner



Les premiers sous-observables produisent un élément puis sont interrompus par une nouvel élément.
Seul le dernier sous-observable produit tous ses éléments.





exhaustMap


Pour chaque élément de l&#8217;observable source, on crée un nouvel observable, ou presque.
Les éléments produits par la source sont ignorés tant que le sous-observable n&#8217;est pas terminé.




--A--------B--------C-|-------------


--O---1---2-|--


exhaustMap


--AO---A1---A2-------CO---C1---C2-|-




Exemple


source()
  .pipe(exhaustMap((item) =&gt; inner(item)))
  .subscribe((response) =&gt; console.log(response));



Résultat :



0/0, 0/1, 0/2, 0/3, END inner,
3/0, 3/1, 3/2, END source, 3/3, END inner



Le premier sous-observable produit ses éléments.
Comme il n&#8217;a pas fini son travail à l&#8217;arrivée du deuxième élément de la source, celui-ci est ignoré.
Quand le troisième arrive, il est pris en charge car le travail est terminé.



</description>
          <pubDate>2022-05-25T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/RxJS/FlatMap</link>
          <guid isPermaLink="true">https://www.jtips.info/RxJS/FlatMap</guid>
        </item>
      
    
      
        <item>
          <title>Mécanisme d&apos;extension pour JUnit 5</title>
          <description>
Le mécanisme d&#8217;extension de JUnit 5 est plus simple et plus pratique que ceux de JUnit 4.
Il remplace à la fois @RunWith, @Rule et @ClassRule.


Utiliser une extension


JUnit a deux annotations pour appliquer une extension sur une classe de test, @ExtendWith et @RegisterExtension.


@ExtendWith s&#8217;utilise habituellement sur la classe de test, comme @RunWith de JUnit 4.
La première différence, c&#8217;est qu&#8217;en JUnit 5, il est possible d&#8217;utiliser plusieurs extensions, en passant un tableau à l&#8217;annotation ou en l&#8217;utilisant plusieurs fois (elle est repeatable).
La deuxième différence, c&#8217;est qu&#8217;elle peut s&#8217;utiliser sur un méthode de test, un champ ou un paramètre de méthode.



@ExtendWith(LoggingExtension.class)
public class SomeTest {
  //...
}



En remplacement de @ExtendWith, on peut aussi utiliser une annotation dédiée, elle-même annotée par @ExtendWith.
Cette annotation peut être fournie avec l&#8217;extension ou faite maison.



@Inherited
@Retention(RUNTIME)
@ExtendWith(LoggingExtension.class)
public @interface Logging {
}




@Logging
public class SomeTest {
  //...
}



@RegisterExtension s&#8217;utilise uniquement sur un champ, comme @Rule ou @ClassRule de JUnit 4.
Sa valeur ajoutée par rapport à @ExtendWith, c&#8217;est qu&#8217;on instancie nous-même l&#8217;extension, avec la possibilité de choisir des paramètres d&#8217;initialisation.



public class SomeTest {
  @RegisterExtension
  LoggingExtension loggingExtension = LoggingExtension.named("@RegisterExtension");

  //...
}





Extensions par défaut


Les extensions ci-dessous sont activées systématiquement.
Elles permettent d&#8217;utiliser des annotations ou d&#8217;injecter des paramètres aux méthodes de tests ou du cycle de vie.




DisabledCondition : désactive les tests annotés avec @Disabled


TempDirectory : création de répertoire temporaire avec @TempDir


TimeoutExtension : applique un délai d&#8217;exécution aux tests annotés avec @Timeout


RepeatedTestExtension : exécute de façon répétée les tests annotés avec @RepeatedTest


TestInfoParameterResolver : injecte les paramètres de tests de type TestInfo


TestReporterParameterResolver : injecte les paramètres de tests de type TestReporter






Extensions fournies


En plus des extensions activée par défaut, les extensions ci-dessous sont fournies par JUnit et utilisables dans nos tests.
Elles sont généralement utilisées sous la forme d&#8217;une annotation dédiée.


La plus grande famille concerne les annotations conditionnelles.




@DisabledForJreRange, @EnabledForJreRange


@DisabledIfEnvironmentVariable, @EnabledIfEnvironmentVariable


@DisabledIfSystemProperty, @EnabledIfSystemProperty


@EnabledForJreRange, @DisabledForJreRange


@DisabledOnOs, @EnabledOnOs`


@DisabledIf, @EnabledIf





  @Test
  @DisabledIf("tooLate")
  public void testIf() {
    System.out.println("Enabled at hour: " + LocalTime.now());
  }

  private boolean tooLate() {
    return LocalTime.now()
              .isAfter(LocalTime.of(22, 0));
  }









Les conditions peuvent être utilisées sur les méthodes du cycle de vie, pour initialiser le contexte du test en fonction de l&#8217;environnement.





La plus utilisée permet d&#8217;exécuter des tests paramétrés.
Elle est dans l&#8217;artéfact org.junit.jupiter:junit-jupiter-params.




@ParameterizedTest






Extensions tierce




MockitoExtension, qui peut aussi être utilisé via l&#8217;annotation @MockitoSettings


@Testcontainers, qui ne peut pas être remplacée par @ExtendWith car l&#8217;extension vérifie la présence de l&#8217;annotation


SpringExtension, avec les annotations @SpringJUnitConfig, @SpringJUnitWebConfig, @SpringBootTest






Développer une extension


Une extension est une classe qui implémente l&#8217;interface Extension.
Cette interface n&#8217;est qu&#8217;un marqueur puisqu&#8217;elle est vide.
Concrètement, il faut implémenter une de ses sous-interface qui s&#8217;intègre dans le cycle de vie de JUnit.


Extension de cycle de vie

Ces extensions permettent de s&#8217;intégrer dans le cycle de vie du test et d&#8217;y ajouter des comportements.
Par exemple, il est classique de démarrer un conteneur de composants ou un serveur avant le démarrage des tests.
On peut aussi faire des extensions qui traitent certains types d&#8217;exceptions ou qui restituent les résultats de façon personnalisée.




BeforeAllCallback, AfterAllCallback


BeforeEachCallback, AfterEachCallback


BeforeTestExecutionCallback, AfterTestExecutionCallback


TestExecutionExceptionHandler


TestInstancePostProcessor


TestInstancePreDestroyCallback




Exemple :



public class SpringExtension
    implements BeforeAllCallback, AfterAllCallback,
               TestInstancePostProcessor,
               BeforeEachCallback, AfterEachCallback,
               BeforeTestExecutionCallback, AfterTestExecutionCallback,
               ParameterResolver {
  //...
}




Fournisseur de paramètres

Même sans faire des tests paramétrés, on peut déclarer des paramètres à une méthode de test simple.
L&#8217;injection de ces paramètre est prise en charge par des extensions de type ParameterResolver.
C&#8217;est aussi valable pour les méthodes du cycle de vie.


Dans l&#8217;exemple ci-dessous, la méthode de préparation prend un Instant en paramètre.
Cet Instant est injecté par le InstantParameterResolver.



@ExtendWith(InstantParameterResolver.class)
public class ParameterTest {
  @BeforeEach
  void setUp(Instant instant) {
    System.out.println("@BeforeEach " + instant);
  }
  //...
}




public class InstantParameterResolver implements ParameterResolver {
  @Override
  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
      throws ParameterResolutionException {
    return parameterContext.getParameter().getType() == Instant.class;
  }

  @Override
  public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
      throws ParameterResolutionException {
    return Instant.now();
  }
}



Les paramètres de type TestInfo et TestReporter sont gérés nativement par JUnit grâce à des extensions par défaut.



Fabrique de test

La catégorie d&#8217;extension qui est probablement la moins utilisée est TestInstanceFactory qui permet d&#8217;instancier le test de façon programmatique.


Ça permet d&#8217;instancier une sous-classe du test ou de passer des paramètres au constructeur sans avoir recours à un ParameterResolver.
L&#8217;exemple avec l'`Instant` donnerait ceci avec une fabrique :



public class FactoryTest {

  @RegisterExtension
  static TestInstanceFactory factory
      = (factoryContext, extensionContext) -&gt; new FactoryTest(Instant.now().minus(1, ChronoUnit.DAYS));

  private final Instant instant;

  public FactoryTest(Instant now) {
    this.instant = now;
  }

  @Test
  void should_work() {
    System.out.println("@Test " + instant);
  }
}






Synthèse


L&#8217;utilisation d&#8217;extensions par l&#8217;annotation @ExtendWith est la plus évidente, mais plusieurs autres usages sont masqués.
Ainsi, les annotations @Timeout ou @Disabled viennent d&#8217;annotations activées par défaut.
Les annotations de condition, @DisabledXxx et @EnabledXxx sont des méta-annotations d&#8217;extensions.


Par ailleurs, si l&#8217;intérêt d&#8217;activer globalement une extension, via le fichier org.junit.jupiter.api.extension.Extension dans le répertoire /META-INF/services, ne parait pas évident dans le cas général, on comprend plus facilement lorsqu&#8217;il s&#8217;agit de résolveur de paramètre ou du support d&#8217;une annotation personnalisée.


Bref, les extensions de JUnit 5 sont plus puissantes et bien plus pratiques que ce qui existait dans les versions précédentes.




Références




JUnit, documentation de référence


Code source des exemples




</description>
          <pubDate>2022-05-03T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JUnit/Extension</link>
          <guid isPermaLink="true">https://www.jtips.info/JUnit/Extension</guid>
        </item>
      
    
      
        <item>
          <title>Messages asynchrones avec Spring Integration</title>
          <description>
Spring Integration supporte l&#8217;échange de messages asynchrone sans avoir besoin d&#8217;ajouter de librairie externe.
Il supporte aussi bien le mode point-to-point que le mode publish-subscribe, avec plusieurs variantes.


Principaux concepts


Channel

Un channel est le canal de communication entre clients.
Il y  a principalement trois interfaces dans Spring Framework (module spring-messaging, pas besoin de Spring Integration).


MessageChannel sert à envoyer des messages.




MessageChannel



send(Message message): boolean


send(Message message, long timeout): boolean








MessageChannel channel = ...;
channel.send(new GenericMessage&lt;&gt;(content));



PollableChannel hérite de MessageChannel.
En plus de l&#8217;envoi de message, elle sert à réceptionner des messages de façon bloquante.




PollableChannel
&#8594; MessageChannel



receive(): Message&lt;?&gt;


receive(long timeout): Message&lt;?&gt;








PollableChannel channel = ...;
Message message = channel.receive();



SubscribableChannel hérite de MessageChannel.
En plus de l&#8217;envoi de message, elle sert à s&#8217;abonner à la réception des futurs messages.




SubscribableChannel
&#8594; MessageChannel



subscribe(MessageHandler handler): boolean


unsubscribe(MessageHandler handler): boolean








SubscribableChannel channel = ...;
MessageHandler handler = message -&gt; ...
channel.subscribe(handler);




Message

L&#8217;interface définissant un message est dans Spring Framework.
Elle est assez simple, avec des headers et un contenu de type libre.




Message&lt;T&gt;



getHeaders(): MessageHeaders


getPayload(): T







L&#8217;implémentation classique de Message&lt;T&gt; est GenericMessage&lt;T&gt;, immuable avec des headers et un contenu générique.




GenericMessage&lt;T&gt;
&#8594; Message&lt;T&gt;



GenericMessage(T payload)


GenericMessage(T payload, Map&lt;String,Object&gt; headers)**


getHeaders(): MessageHeaders


getPayload(): T







ErrorMessage est une sous-classe de c, et contient un Throwable en contenu.
Ce type de message peut aussi contenir le message qui serait à l&#8217;origine de l&#8217;erreur.




GenericMessage&lt;T&gt;
&#8594; GenericMessage&lt;Throwable&gt;



ErrorMessage(&#8230;&#8203;)


getHeaders(): MessageHeaders


getPayload(): Throwable


getOriginalMessage(): Message&lt;?&gt;







Cette interface et ces deux classes sont dans Spring Framework.


Spring Integration propose aussi MutableMessage&lt;T&gt; qui, contrairement à GenericMessage&lt;T&gt; autorise la modification des headers.
Cette classe doit être utilisée avec précaution car rien ne garantie qu&#8217;elle soit thread safe.
Il propose aussi AdviceMessage&lt;T&gt; a un contenu générique, et le message d&#8217;entrée, et est utilisé avec de l&#8217;AOP.





Modèles de communication


Spring Framework n&#8217;a qu&#8217;une seule implémentation de canal, ExecutorSubscribableChannel, en mode publish-subscribe et qui implémente SubscribableChannel.


Spring Integration est beaucoup plus riche.




PublishSubscribeChannel en mode publish-subscribe, implémente SubscribableChannel


QueueChannel en mode point-to-point et FIFO, implémente PollableChannel


PriorityChannel en mode point-to-point avec un ordre de priorité des messages, hérite de QueueChannel, implémente PollableChannel


RendezvousChannel en mode point-to-point en bloquant le producteur, utilisable pour faire du request-reply, hérite de QueueChannel


DirectChannel en mode point-to-point, mais en implémentant SubscribableChannel,


ExecutorChannel en mode point-to-point, ressemble au DirectChannel mais avec un send(&#8230;&#8203;) non bloquant




Publish-subscribe

Dans ce mode, chaque message envoyé dans le canal peut être réceptionné par plusieurs consommateurs.



                                                      ┌────────────┐
┌───────────┐       ┌─────────────────────────┐   ┌──&gt;| Consumer#1 |
| Publisher ├──────&gt;| PublishSubscribeChannel ├───┼──&gt;| Consumer#2 |
└───────────┘       └─────────────────────────┘   └──&gt;| Consumer#3 |
                                                      └────────────┘





PublishSubscribeChannel
&#8594; SubscribableChannel



PublishSubscribeChannel()


PublishSubscribeChannel(boolean requireSubscribers)


PublishSubscribeChannel(Executor executor)


PublishSubscribeChannel(Executor executor, boolean requireSubscribers)
&#160;


setApplySequence(boolean applySequence)


setErrorHandler(ErrorHandler errorHandler)


setIgnoreFailures(boolean ignoreFailures)


setMinSubscribers(int minSubscribers)
&#160;


send(Message message): boolean


send(Message message, long timeout): boolean


subscribe(MessageHandler handler): boolean


unsubscribe(MessageHandler handler): boolean







Avec le constructeur par défaut, la consommation des messages se fait dans le même thread que leur envoi.
Pour découpler la consommation de l&#8217;envoi, il faut associer un executor.


Le paramètre de constructeur requireSubscribers permet de vérifier si le channel a des consommateurs.
Dans ce cas, un message envoyé à un canal sans consommateur va déclencher une exception.



MessageChannel channel = new PublishSubscribeChannel(true);
channel.send(new GenericMessage&lt;&gt;(content));

org.springframework.integration.MessageDispatchingException:
Dispatcher has no subscribers, failedMessage=GenericMessage
[payload=Hello,
 headers={id=c77a385e-a3fc-2874-3434-23c24c4892a2,
          timestamp=1648380606878}]



ExecutorSubscribableChannel est aussi utilisable, un peu plus simple.
Il peut aussi fonctionner de façon synchrone ou avec un executor.
Ça permet d&#8217;avoir un event bus dans Spring Framework, sans Spring Integration.



Point-to-point

Dans ce mode, chaque message envoyé dans le canal ne sera réceptionné que par un consommateur, même si plusieurs sont connectés.
De plus, le message reste dans le canal tant qu&#8217;il n&#8217;est pas consommé.



                                           ┌────────────┐
┌───────────┐       ┌──────────────┐       | Consumer#1 |
| Publisher ├──────&gt;| QueueChannel ├───┐   | Consumer#2 |
└───────────┘       └──────────────┘   └──&gt;| Consumer#3 |
                                           └────────────┘



QueueChannel fonctionne avec une file de messages.
Lorsqu&#8217;un producteur envoie un message, celui-ci est stocké dans la file jusqu&#8217;à ce qu&#8217;un consomateur vienne le récupérer.
Par défaut, cette file n&#8217;est pas limité en taille.


Les messages sont mis à disposition des consommateurs dans leur ordre d&#8217;arrivée : c&#8217;est du FIFO.




QueueChannel
&#8594; PollableChannel



QueueChannel()


QueueChannel(int capacity)


QueueChannel(Queue&lt;Message&lt;?&gt;&gt; queue)
&#160;


send(Message&lt;?&gt; message): boolean


send(Message&lt;?&gt; message, long timeout): boolean


purge(MessageSelector selector)


receive(): Message&lt;?&gt;


receive(long timeout): Message&lt;?&gt;








// Publisher
MessageChannel channel = new QueueChannel();
channel.send(new GenericMessage&lt;&gt;(content));




// Consumer
PollableChannel channel = ...; // find the channel
Message&lt;?&gt; message = channel.receive();



PriorityChannel fonctionne de façon similaire, avec un notion de priorité qui détermine l&#8217;ordre de mise à disposition.
Par défaut, les messages sont triés par le header priority.
De façon optionnelle, on peut utiliser un comparateur de messages.




PriorityChannel
&#8594; QueueChannel



PriorityChannel()


PriorityChannel(int capacity)


PriorityChannel(Comparator&lt;Message&lt;?&gt;&gt; comparator)


PriorityChannel(int capacity, Comparator&lt;Message&lt;?&gt;&gt; comparator)








// Publisher
MessageChannel channel = new QueueChannel();
channel.send(
      new GenericMessage&lt;&gt;(
            content,
            // Priority header
            Map.of(IntegrationMessageHeaderAccessor.PRIORITY, 1)
      )
);




Point-to-point avec abonnement

Dans le chapitre précédent, les consommateurs doivent prélever les messages un à un.
Avec DirectChannel, les consommateurs s&#8217;abonnent à l&#8217;arrivée de nouveaux messages.
L&#8217;API ressemble à celle du mode pub/sub, mais chaque message n&#8217;est délivré qu&#8217;à un seul consommateur.




DirectChannel
&#8594; SubscribableChannel



DirectChannel()


DirectChannel(LoadBalancingStrategy loadBalancingStrategy)


send(Message message): boolean


send(Message message, long timeout): boolean


subscribe(MessageHandler handler): boolean


unsubscribe(MessageHandler handler): boolean








// Consumer
SubscribableChannel channel = new DirectChannel();
channel.subscribe(message -&gt; ...);




// Publisher
MessageChannel channel = ...; // find the channel
channel.send(new GenericMessage&lt;&gt;(content));



Si plusieurs consommateurs souscrivent aux messages d&#8217;un canal, chaque message n&#8217;est consommé que par un seul d&#8217;entre eux.
Par défaut, la répartission des messages entre les consommateurs se fait en round robin.
Une répartission différente peut être choisie en passant une LoadBalancingStrategy à la création du canal.


Par contre, le choix est limité puisque seul le round robin est disponible dans Spring Integration.
Pour une autre stratégie, il faut implémenter soi-même l&#8217;interface.




LoadBalancingStrategy



getHandlerIterator(Message&lt;?&gt; message, Collection&lt;MessageHandler&gt; handlers): Iterator&lt;MessageHandler&gt;







ExecutorChannel est une variation sur le même thème où l&#8217;envoi des messages se fait de façon non bloquante, via les threads d&#8217;un executor.




ExecutorChannel
&#8594; SubscribableChannel



ExecutorChannel(Executor executor)


ExecutorChannel(Executor executor, LoadBalancingStrategy loadBalancingStrategy)








Requête / réponse

Le mode requête / réponse est un dérivé du point à point.
Le message initial est envoyé puis consommé en point à point.
Une fois le message envoyé, le producteur se met en attente d&#8217;un message de réponse sur une file temporaire.
Et quand le consommateur reçoit le message, il le traite et envoie un message de réponse dans cette file temporaire.



                                           ┌────────────┐
┌───────────┐       ┌──────────────┐       | Consumer#1 |
| Publisher ├──────&gt;| QueueChannel ├───┐   | Consumer#2 |
└───────────┘&lt;┐     └──────────────┘   └──&gt;| Consumer#3 |
              |                            └──────┬─────┘
              |    ┌───────────────────┐          |
              └────┤ RendezvousChannel |&lt;─────────┘
                   └───────────────────┘



RendezvousChannel est approprié pour servir de file temporaire.
Il est bloquant des deux cotés, y compris pour le producteur qui est mis en attente d&#8217;un consommateur.



// Publisher
PollableChannel replyChannel = new RendezvousChannel();
Message&lt;?&gt; message =
      new GenericMessage&lt;&gt;(
            content,
            Map.of(MessageHeaders.REPLY_CHANNEL, replyChannel)
      );
channel.send(message);
Message&lt;?&gt; resultMessage = replyChannel.receive();



On peut simplifier le code avec GenericMessagingTemplate.








GenericMessagingTemplate n&#8217;utilise pas de RendezvousChannel, mais un canal privé qui ne peut contenir qu&#8217;un seul message.








Annotations


Spring Integration propose un stéréotype de composant avec @MessageEndpoint.



@MessageEndpoint
public class EventEndpoint {
  // ...
}



@ServiceActivator

Cette annotation doit être apposée sur une méthode d&#8217;un bean, de préférence annotée avec @MessageEndpoint.
Elle permet de s&#8217;abonner aux messages d&#8217;un canal, qu&#8217;il soit subscribable ou pollable.



@MessageEndpoint
public class EventEndpoint {
  @ServiceActivator(inputChannel = "channel/subscribable")
  public void onMessage(String message) {
    // ...
  }
}



Le paramètre d&#8217;entrée de la méthode peut être un Message ou directement le type du contenu, comme dans l&#8217;exemple ci-dessus.
Dans le cas du contenu, on peut aussi ajouter des paramètres d&#8217;en-tête, annotés avec @Header, ou un paramètre pour l&#8217;ensemble des valeurs d&#8217;en-tête, de type Map&lt;String, Object&gt; et annoté avec @Headers.



@MessageEndpoint
public class EventEndpoint {
  @ServiceActivator(inputChannel = "channel/subscribable")
  public void onMessage(String message, @Header("token") String token) {
    // ...
  }
}




@Poller

Si le canal est pollable, il faut associé au poller.
Il peut être défini sous forme d&#8217;un bean global.



  @Bean(name = PollerMetadata.DEFAULT_POLLER)
  public PollerMetadata defaultPoller() {
    PollerMetadata pollerMetadata = new PollerMetadata();
    pollerMetadata.setTrigger(new PeriodicTrigger(1, TimeUnit.SECONDS));
    return pollerMetadata;
  }



Dans l&#8217;exemple présenté ici, la récupération des messages se fait toutes les secondes.
Les triggers sont ceux de Spring Task Executor.


Pour qu&#8217;une méthode utilise un poller alternatif, il faut utiliser l&#8217;annotation @Poller en passant le nom du bean.



@MessageEndpoint
public class EventEndpoint {
  @ServiceActivator(
      inputChannel = "channel/subscribable",
      poller = @Poller("sw.slowPoller"))
  public void onMessage(String message, @Header("token") String token) {
    // ...
  }
}



La même annotation peut être utilisée pour utiliser un poller interne.



@MessageEndpoint
public class EventEndpoint {
  @ServiceActivator(
      inputChannel = "channel/pollable",
      poller = @Poller(fixedDelay = "1000"))
  public void onMessage(String message, @Header("token") String token) {
    // ...
  }
}




@ServiceActivator avec retour

La méthode peut aussi retourner un message, ou un objet quelconque qui servira de contenu à un message.
Ce message sera envoyé dans le canal spécifié comme outputChannel.



  @ServiceActivator(
      inputChannel = "channel/pollable",
      outputChannel = "channel/subscribable")
  public String onMessage(String message) {
    systemLogger.log(INFO, "Message received on channel/pollable: " + message);
    return "Reply-" + message;
  }



Si aucun outputChannel n&#8217;est pas spécifié, le retour est envoyé dans le replyChannel du message entrant.
Et s&#8217;il n&#8217;y en a pas non plus, une exception est levée.








Si le message a un replyChannel et qu&#8217;un outputChannel est spécifié, alors c&#8217;est outputChannel qui est utilisé.






@Publisher

On peut aussi envoyer des messages via des méthodes annotées.
Pour que ça fonctionne, il faut activer spécifiquement la fonctionnalité.



@Configuration
@EnableIntegration
@EnablePublisher
public class IntegrationConfiguration {
  // ...
}



Après ça, on peut annoter des méthodes de bean avec @Publisher.
L&#8217;appel de ces méthodes déclenche l&#8217;envoi d&#8217;un message avec le résultat de la méthode en payload.
Le nom passé en paramètre est le nom de bean du canal.



  @Publisher("channel/publish")
  public String publish(String message) {
    // ...
    return result;
  }



Comme pour les méthodes annotées avec @ServiceActivator, il est possible d&#8217;ajouter des paramètres d&#8217;en-tête annotés avec @Header.



  @Publisher("channel/publish")
  public String publish(String message, @Header("token") String token) {
    // ...
    return result;
  }






Concepts avancés


ChannelInterceptor

Comme le nom l&#8217;indique bien, un ChannelInterceptor est rattaché à un Channel.
Il s&#8217;insère au niveaux des interactions entre le message et le canal, avant/après l&#8217;envoi et avant/après la réception.




ChannelInterceptor



preSend(Message&lt;?&gt; message, MessageChannel channel): Message&lt;?&gt;


postSend(Message&lt;?&gt; message, MessageChannel channel, boolean sent)


afterSendCompletion(Message&lt;?&gt; message, MessageChannel channel, boolean sent, Exception ex)


preReceive(MessageChannel channel): boolean


postReceive(Message&lt;?&gt; message, MessageChannel channel): Message&lt;?&gt;


afterReceiveCompletion(Message&lt;?&gt; message, MessageChannel channel, Exception ex)







Toutes les méthodes de l&#8217;interface sont default.
On n&#8217;implémente que celles qu&#8217;on veut effectivement redéfinir.



@Component
public class SecurityInterceptor implements ChannelInterceptor {

  private final SecurityService securityService;

  public SecurityInterceptor(SecurityService securityService) {
    this.securityService = securityService;
  }

  @Override
  public Message&lt;?&gt; preSend(Message&lt;?&gt; message, MessageChannel channel) {
    String token = message.getHeaders().get("token", String.class);
    securityService.validateToken(token);
    return ChannelInterceptor.super.preSend(message, channel);
  }
}



L&#8217;intercepteur peut être associé à un ou plusieurs canaux.
De même un canal peut avoir plusieurs intercepteurs.



  @Bean
  public PollableChannel pollableChannel(SecurityInterceptor securityInterceptor) {
    QueueChannel channel = new QueueChannel();
    channel.addInterceptor(securityInterceptor);
    return channel;
  }



Un intercepteur peut fonctionner de façon non intrusive, pour du logging par exemple.
Il peut empêcher l&#8217;envoi du message, dans une logique de sécurité.
Il peut aussi transformer un message avant l&#8217;envoi ou après la réception.



EIP patterns

Spring Integration implémente plein de patterns d&#8217;intégration.
Ça fait plusieurs sujet à approfondir sur ce thème.




Message Store


Channel adapter


Message Gateway


Message Adapter / Service Activator


Message Transformer


Filter


Message Router


Sequencer / Splitter


Aggregator





</description>
          <pubDate>2022-03-27T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/SpringIntegration/MessageChannel</link>
          <guid isPermaLink="true">https://www.jtips.info/SpringIntegration/MessageChannel</guid>
        </item>
      
    
      
        <item>
          <title>Framework Executor du JDK : CountedCompleter</title>
          <description>
Dans la famille des tâches de fork / join, le dernier arrivé est CountedCompleter.
Il a été ajouté dans le JDK 8 alors que les autres datent du JDK 7.


Comme le développement d&#8217;une telle tâche est un peu plus complexe, et que les bonnes références sont difficiles à trouver sur le Web, CountedCompleter a droit à sa propre page.




CountedCompleter
&#8594; ForkJoinTask&lt;V&gt;



compute()


getRawResult(): V


onCompletion(CountedCompleter&lt;?&gt; caller)
&#160;


tryComplete()


setPendingCount(int count)


addToPendingCount(int delta)







Comme pour les actions recursives, il faut implémenter une méthode compute(): void.
Cette méthode peut exécuter effectivement l&#8217;action ou se scinder tâches plus petites.
Comme pour les tâches récursives, la méthode getRawResult() renvoie le résultat.
Par contre, par défaut, elle renvoie null et c&#8217;est à nous de la redéfinir pour avoir un vrai résultat.


Compteur


La grande différence avec les tâches et actions récursives, c&#8217;est la notion de complétion basée sur un compteur.
Chaque objet CountedCompleter a son propre compteur de sous-tâches qu&#8217;on doit incrémenter expliciter à chaque appel de fork().



    this.addToPendingCount(2);
    new BigTask(this, leftList).fork();
    new BigTask(this, rightList).fork();





Fin de tâche


Chaque tâche doit appeler tryComplete() en fin de traitement pour que le compteur de son parent puisse être décrémenté.
A l&#8217;appel de cette méthode, si le compteur est positif, il est décrémenté et s&#8217;il est à zéro, l&#8217;action est considérée comme terminée et la méthode onCompletion(&#8230;&#8203;) est appélée.



  @Override
  public void compute() {
    // ...
    tryComplete();
  }





Arbre de tâches


L&#8217;autre grande différence, avec les tâches et actions récursive est sous-tendue par ce fonctionnement : les tâches sont organisées explicitement sur des relations parent / enfant.
Pour assurer ça, il faut bien appeler le constructeur CountedCompleter(CountedCompleter&lt;?&gt; completer).



class BigTask extends CountedCompleter&lt;Void&gt; {
  public BigTask(List&lt;Integer&gt; data) {
    this.data = data;
  }

  public BigTask(CountedCompleter parent, List&lt;Integer&gt; data) {
    super(parent);
    this.data = data;
  }

  //...
}





Action sans retour


Si on n&#8217;attend pas de résultat, c&#8217;est assez proche d&#8217;un action récursive.



public BigTask extends CountedCompleter&lt;Void&gt; {
  public void compute() {
    if (simple) {
      return doTheJob();
    } else {
      // Compteur
      this.addToPendingCount(2);
      // Fork
      new CustomTask(this, leftData).fork();
      new CustomTask(this, rightData).fork();
    }
    // Fin de la tâche, pas de join
    this.tryComplete();
  }
}





Tâche avec retour


C&#8217;est un peu plus compliqué pour calculer et retourner un résultat global.
Tout d&#8217;abord, il faut redéfinir la méthode getRawResult().
Et pour ça, il faut que notre objet ait calculé un résultat à retourner.


Pour ça, il y a plusieurs possibilité.
Par exemple, on peut utiliser un objet partagé sous forme d&#8217;un AtomicLong ou d&#8217;un AtomicInteger pour des formats simples, ou un AtomicReference&lt;?&gt; pour un objet plus complexe.
Cette façon de faire est assez simple mais ne me plait pas parce que cet objet peut devenir un point de contension si on augmente le nombre de threads.


Je préfère une solution basée sur une redéfinition de la méthode onCompletion(&#8230;&#8203;) où chaque tâche calcul son résultat à partir de celui de ses enfants.



public BigTask extends CountedCompleter&lt;Void&gt; {
  private final List&lt;BigTask&gt; children = new ArrayList&lt;&gt;();

  public void compute() {
    if (simple) {
      return doTheJob();
    } else {
      children.add(new BigTask(this, leftList));
      children.add(new BigTask(this, rightList));
      // Compteur
      this.addToPendingCount(children.size());
      // Fork
      children.forEach(ForkJoinTask::fork);
    }
    // Fin de la tâche
    this.tryComplete();
  }

  @Override
  public void onCompletion(CountedCompleter&lt;?&gt; caller) {
    // compute result based on children and local result
    this.result = ...;
  }

  @Override
  public Integer getRawResult() {
    return result;
  }
}



</description>
          <pubDate>2022-03-20T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/Executor-CountedCompleter</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/Executor-CountedCompleter</guid>
        </item>
      
    
      
        <item>
          <title>Framework Executor du JDK</title>
          <description>
Le rôle principal du framework executor est d&#8217;ajouter une couche d&#8217;abstraction au dessus de la classe Thread.


Il est constitué d&#8217;un ensemble de classes et d&#8217;interfaces qui gèrent des pools de threads et l&#8217;exécution de tâches en parallèle.
Elles sont rassemblées dans le paquetage java.util.concurrent.


Il permet aussi de planifier des tâches récurrentes avec ScheduledExecutorService.
Comme quoi, il n&#8217;y a pas besoin de Quartz ou de Spring Task Scheduler pour couvrir des besoins simples.







Interfaces


L&#8217;interface Executor est au sommet de la hiérarchie.
Elle définit juste une méthode pour exécuter une tâche Runnable.
Elle n&#8217;a pas beaucoup d&#8217;intérêt toute seule, et il n&#8217;y a pas d&#8217;implémentation directe dans le JDK.




Executor



execute(Runnable command)







L&#8217;interface ExecutorService définit une façon de soumettre des tâches, puis de récupérer leurs résultats sour forme de Future&lt;?&gt;.
Elle a aussi des méthodes pour arrêter le pool de threads.




ExecutorService



submit(Callable&lt;T&gt; task) : Future&lt;T&gt;


invokeAll(Collection&lt;Callable&lt;T&gt;&gt; tasks) : List&lt;Future&lt;T&gt;&gt;


shutdown()







L&#8217;interface ScheduledExecutorService permet de planifier des tâches répétées.




ScheduledExecutorService



schedule(Callable&lt;V&gt; callable, long delay, TimeUnit unit) : ScheduledFuture&lt;V&gt;


schedule(Runnable command, long delay, TimeUnit unit) : ScheduledFuture&lt;?&gt;


scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) : ScheduledFuture&lt;?&gt;


scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) : ScheduledFuture&lt;?&gt;









Pool de threads


La classe ThreadPoolExecutor implémente ExecutorService et permet d&#8217;exécuter des tâches asynchrones sur pool de threads.
La configuration de ce pool est à plusieurs dimensions, permettant des comportements variés.


Soumettre une tâche

Une tâche est une implémentation de Runnable ou Callable&lt;?&gt;, sous forme de classe ou d&#8217;expression lambda.


Une tâche est soumise au pool par la méthode submit(&#8230;&#8203;), si on veut récupérer le résultat de la tâche via un Future&lt;?&gt;, ou la méthode execute(&#8230;&#8203;), sans résultat.



  Future&lt;Instant&gt; future = executor.submit(() -&gt; someTask());
  ...
  Instant result = future.get();




Soumettre plusieurs tâches

En soumettant un ensemble de tâches, on récupère une liste de Future&lt;?&gt;, qui permettent de récupérer leurs résultats.
Cette méthode fonctionne de façon synchrone.
Elle attend que toutes les tâches soient terminées pour rendre la main.



  List&lt;Future&lt;Instant&gt;&gt; futures = executor.invokeAll(
    List.of(
      () -&gt; firstTask(),
      () -&gt; secondTask(),
      () -&gt; otherTask()
    )
  );



La méthode invokeAny(&#8230;&#8203;) permet de soumettre un ensemble de tâche et de récupérer le résultat de la tâche qui finit en premier.
Les autres tâches sont annulées.



  Instant firstFinished = executor.invokeAny(
    List.of(
      () -&gt; firstTask(),
      () -&gt; secondTask(),
      () -&gt; otherTask()
    )
  );




Instancier un ThreadPoolExecutor

On peut instancier directement le ThreadPoolExecutor, ou passer par les méthodes de fabrique d'Executors.
On passe des paramètres de dimensionnement et de file d&#8217;attente au constructeur.



var executor = new ThreadPoolExecutor(
                        10, 20,
                        10, TimeUnit.SECONDS,
                        new LinkedBlockingQueue&lt;Runnable&gt;());



Dans l&#8217;ordre, les paramètres sont les suivants :




corePoolSize = taille du pool principal


maximumPoolSize = taille maximale du pool


keepAliveTime et unit = durée d&#8217;inactivité d&#8217;un thread avant son éviction


workQueue = file d&#8217;attente des tâches




En jouant avec ces paramètres, on a une gamme de comportements variés, avec ou sans pool secondaire, sans file d&#8217;attente, avec une file d&#8217;attente limitée ou illimitée.
Les méthodes de création de la classe Executors permettent d&#8217;instancier des ThreadPoolExecutor avec des configurations prédéfinies.




Executors



newSingleThreadExecutor(): ExecutorService


newFixedThreadPool(int nThreads): ExecutorService


newWorkStealingPool(int parallelism): ExecutorService


unconfigurableExecutorService(ExecutorService executor): ExecutorService







newFixedThreadPool(n) permet de créer une pool à taille fixe.
En réalité, le pool démarre avec 0 thread et voit sa taille augmenter selon le besoin.
Tous les threads sont dans le pool principal, et y restent, même inutilisés.
Lorsque le pool est plein, les nouvelles tâches sont mises en attente dans une file illimitée.



ExecutorService pool = Executors.newFixedThreadPool(42);
// Equivalent à
ExecutorService pool = new ThreadPoolExecutor(
                              42, 42,
                              0L, TimeUnit.MILLISECONDS,
                              new LinkedBlockingQueue&lt;Runnable&gt;());



Avec un tel pool, les tâches soumises sont réparties sur les différents threads tant qu&#8217;il en reste à disposition.
S&#8217;ils sont tous occupés, les tâches sont mises en attente jusqu&#8217;à ce qu&#8217;un thread se libère.


newSingleThreadExecutor() fait la même chose, avec un seul thread.
De plus, le pool créé ainsi n&#8217;est pas reconfigurable, contrairement à un pool construit avec newFixedThreadPool(1), dont on peut changer la taille ultérieurement.



ExecutorService pool = Executors.newSingleThreadExecutor();



newCachedThreadPool() permet de créer une pool à taille illimitée (ou presque).
Les threads sont créé au fil de la demande et sont détruits après une inactivité d&#8217;une minute.



ExecutorService pool = Executors.newCachedThreadPool();
// Equivalent à
ExecutorService pool = new ThreadPoolExecutor(
                              0, Integer.MAX_VALUE,
                              60L, TimeUnit.SECONDS,
                              new SynchronousQueue&lt;Runnable&gt;());



Avec ce pool, les threads sont créés au fil du besoin.
Lorsqu&#8217;on soumet une tâche, un thread existant lui est affecté et si aucun n&#8217;est disponible, un nouveau thread est créé et ajouté au thread.



Fonctionnement du pool

Quand une tâche est soumise,




si un thread est disponible, il est utilisé,


sinon, si corePoolSize n&#8217;est pas atteint, un nouveau thread est démarré pour exécuter la tâche,


sinon, s&#8217;il y a de la place dans la file, la taĉhe y est mise en attente,


sinon, si maximumPoolSize n&#8217;est pas atteint, un nouveau thread est démarré pour exécuter la tâche,


sinon la tâche est rejetée.




Il y a essentiellement deux types de files d&#8217;attente :




Avec SynchronousQueue, il n&#8217;y a pas de fil d&#8217;attente, la file n&#8217;a aucune capacité.
Celui qui veut insérer est mis en attente d&#8217;une demande de retrait.


Avec LinkedBlockingQueue, on a une vraie file d&#8217;attente, en FIFO.
Par défaut la capacité est (quasi-)infinie, mais peut être réduite.





Arrêter le pool et/ou les tâches

Le pool est arrêté avec la méthode shutdown() ou shutdownNow().


L&#8217;appel de shutdown() n&#8217;a pas d&#8217;impact sur les tâches déjà soumises, elle sont exécutées normalement.
Par contre, la soumission de nouvelles tâches échoue avec une  RejectedExecutionException.


On peut savoir que toutes les tâches sont terminées avec la méthode isTerminated().
On peut aussi attendre avec awaitTermination(&#8230;&#8203;).




ExecutorService



shutdown()


shutdownNow(): List&lt;Runnable&gt;


awaitTermination(long timeout, TimeUnit unit): boolean


isShutdown(): boolean


isTerminated(): boolean







Chaque tâche peut être interrompue individuellement grâce à la méthode cancel() de Future&lt;?&gt;.
Si on passe false, ça n&#8217;annulera que les tâches qui n&#8217;ont pas démarré.
Si on passe true, la méthode essaie d&#8217;interrompre la tâche, via la méthode interrupt() du thread.




Future&lt;V&gt;



cancel(boolean mayInterruptIfRunning)(): boolean


isCancelled(): boolean










Fork / Join


Comme ThreadPoolExecutor, la classe ForkJoinPool implémente ExecutorService.
Mais son rôle est conceptuellement très différent, il sert à exécuter des tâches qui se scindent puis fusionnent pour récolter le résultat.
Si la tâche est assez simple, elle est exécutée, sinon elle est scindée en sous-tâches.


Les exemples les plus couramment rencontrés sur le Web sont les suivants :




Calcul sur un gros tableau de nombres (min, max, somme,&#8230;&#8203;)


Parcour recursif de répertoires


Aspiration de site Web




Soumettre une tâche



ForkJoinPool



execute(ForkJoinTask&lt;?&gt; task)


invoke(ForkJoinTask&lt;T&gt; task): T


submit(ForkJoinTask&lt;T&gt; task): ForkJoinTask&lt;T&gt;







Les méthodes execute(&#8230;&#8203;) et invoke(&#8230;&#8203;) sont synchrones.
La première ne retourne rien alors que la seconde retourne le résultat de la tâche.
Toutes deux fonctionnent avec une tâche de type ForkJoinTask&lt;T&gt;.


La méthode submit(&#8230;&#8203;) renvoie la tâche elle-même qui implémente Future&lt;T&gt;, qui permet de récupérer le résultat plus tard.




ForkJoinTask&lt;V&gt;



fork(): ForkJoinTask&lt;V&gt;


join(): V


exec(): boolean


getRawResult(): V


setRawResult(V value)








Scinder la tâche et collecter le résultat

La méthode fork() permet d&#8217;ajouter la tâche à la file d&#8217;attente.
Elle ne doit normalement être appelée que depuis une tâche parente en cours d&#8217;exécution sur le pool.
C&#8217;est cette méthode qu&#8217;on appelle sur chaque sous-tâche lors d&#8217;une scission.
A la place, on peut aussi appeler ForkJoinTask.invokeAll(&#8230;&#8203;) en passant les sous-tâches en paramètre.


La méthode join() est appelée sur un tâche pour récupérer le résultat et le combiner à celui des autres sous-tâches.
Elle ressemble au get() de Future&lt;?&gt;.



Implémenter une tâche

Pour utiliser un ForkJoinPool, on doit implémenter le traitement dans une classe qui hérite de ForkJoinTask.
On hérite rarement directement de ForkJoinTask, mais d&#8217;une de ses sous-classes.






RecursiveTask&lt;V&gt;
&#8594; ForkJoinTask&lt;V&gt;



compute(): V









RecursiveAction
&#8594; ForkJoinTask&lt;Void&gt;



compute()









CountedCompleter
&#8594; ForkJoinTask&lt;V&gt;



compute()











RecursiveTask&lt;V&gt; est une tâche recursive avec retour.


RecursiveAction est une tâche recursive sans retour.


CountedCompleter est le dernier arrivé (JDK 8) et est un peu plus complexe à coder.
Il a sa propre page.





public BigTask extends RecursiveTask&lt;Result&gt; {
  public Result compute() {
    if (simple) {
      return doTheJob();
    } else {
      var subTask1 = new CustomTask();
      subTask1.fork();

      var subTask2 = new CustomTask();
      subTask2.fork();

      return subTask1.join().combine(subTask2.join());
    }
  }
}



La tâche initiale est soumise directement au pool.
Ça peut être fait de façon synchrone.



    ForkJoinPool pool = new ForkJoinPool();
    Result result = pool.invoke(new BigTask(initialData));
    ...



Ça peut aussi être fait de façon asynchrone.



    ForkJoinPool pool = new ForkJoinPool();
    ForkJoinTask&lt;Result&gt; future = pool.submit(new BigTask(initialData));
    ...
    Result result = future.join();






Planification de tâches


La classe ScheduledThreadPoolExecutor, qui implémente l&#8217;interface ScheduledExecutorService permet de planifier des tâches répétées, avec choix de la taille du pool de threads.


Instancier et configurer le scheduler

Comme pour ThreadPoolExecutor, on peut instancier directement ScheduledThreadPoolExecutor ou passer par les fabriques de Executors.
Par contre, la configuration du pool est plus simple que celui du ThreadPoolExecutor.


Même si ScheduledThreadPoolExecutor hérite de ThreadPoolExecutor, elle n&#8217;en exploite pas les possibilités de pool.
Elle utilise juste un pool de taille fixe avec une fille d&#8217;attente illimitée.



  ScheduledThreadPoolExecutor scheduler
        = new ScheduledThreadPoolExecutor(5);





Executors



newSingleThreadScheduledExecutor(): ScheduledExecutorService


newScheduledThreadPool(int corePoolSize): ScheduledExecutorService







newScheduledThreadPool(n) permet de créer un pool de taille fixe.



  ScheduledExecutorService scheduler
        = Executors.newScheduledThreadPool(42);



newSingleThreadScheduledExecutor() permet d&#8217;utiliser un thread unique, sans possibilité de reconfiguration.



  ScheduledExecutorService scheduler
        = Executors.newSingleThreadScheduledExecutor();




Planifier une tâche

Avec la méthode schedule(&#8230;&#8203;), on planifie une tâche à exécution ultérieure.
Cette tâche s&#8217;exécutera une seule fois.



  ScheduledFuture&lt;Result&gt; future
        = scheduler.schedule(..., 3, TimeUnit.MINUTES);



Avec la méthode scheduleAtFixedRate(&#8230;&#8203;), on planifie une tâche qui va s&#8217;exécuter plusieurs fois, à intervalle régulier.
L&#8217;intervalle est calculé entre le démarrage d&#8217;une tâche et le démarrage de la tâche précédente.
Lorsqu&#8217;on programme la tâche, on choisit dans quel délai elle s&#8217;exécutera pour la première fois.



  ScheduledFuture&lt;Result&gt; future
        = scheduler.scheduleAtFixedRate(..., 10, 3, TimeUnit.MINUTES);



Avec la méthode scheduleWithFixedDelay(&#8230;&#8203;), l&#8217;intervalle est calculé entre le démarrage d&#8217;une tâche et la fin de la tâche précédente.



  ScheduledFuture&lt;Result&gt; future
        = scheduler.scheduleWithFixedDelay(..., 10, 3, TimeUnit.MINUTES);









Le respect des intervalles et délais n&#8217;est pas garanti.
En effet, l&#8217;exécution des tâches dépend de la disponibilité de threads dans le pool.





Toutes ces méthodes renvoient un ScheduledFuture&lt;?&gt;, qui hérite de Future&lt;?&gt; et permet donc de récupérer le résultat ultérieurement.
Il hérite aussi de Delayed qui donne le délai avant la prochaine exécution.




ScheduledFuture&lt;V&gt;



getDelay(TimeUnit unit): long


get(): V








Arrêter le scheduler et/ou les tâches

Le scheduler s&#8217;arrête avec les méthodes shutdown() ou shutdownNow().
Une question supplémentaire se pose par rapport à un pool simple, concernant les tâches qui sont planifiées à plus tard ainsi que les tâches périodiques.




ScheduledExecutorService



setExecuteExistingDelayedTasksAfterShutdownPolicy(boolean value)


setContinueExistingPeriodicTasksAfterShutdownPolicy(boolean value)
&#160;


shutdown()


shutdownNow(): List&lt;Runnable&gt;


awaitTermination(long timeout, TimeUnit unit): boolean


isShutdown(): boolean


isTerminated(): boolean







Par défaut, les tâches ultérieures sont conservées après un shutdown().
En revanche, les tâches périodiques sont déprogrammées.


Quels que soient ces paramètres, un appel de shutdownNow() arrête toutes les tâches.



</description>
          <pubDate>2022-03-05T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/Executor</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/Executor</guid>
        </item>
      
    
      
        <item>
          <title>Configuration de Spring Task Scheduler</title>
          <description>
Le TaskScheduler de Spring permet de planifier des tâches de façon simple et pratique.


Configuration


XML

On utilise le namespace task avec l&#8217;élément &lt;task:scheduled&gt;.
Celui-ci fait référence à une méthode d&#8217;un bean déclaré par ailleurs.


Il a 4 variantes :




fixed-rate permet de fixer un délai entre le début d&#8217;une exécution et le début de la suivante.


fixed-delay permet de fixer le délai entre la fin d&#8217;une exécution et le début de la suivante.


cron utilise une expression cron.


trigger fait référence à un bean qui implémente org.springframework.scheduling.Trigger.





&lt;beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="http://www.springframework.org/schema/task
                           https://www.springframework.org/schema/task/spring-task.xsd"&gt;
  &lt;task:scheduled-tasks&gt;
      &lt;task:scheduled fixed-rate="10000" ref="scheduledBean" method="scheduledRate" /&gt;
      &lt;task:scheduled fixed-delay="10000" ref="scheduledBean" method="scheduledDelay" /&gt;
      &lt;task:scheduled cron="*/10 * * * * *" ref="scheduledBean" method="scheduledCron" /&gt;
      &lt;task:scheduled trigger="advancedTrigger" ref="scheduledBean" method="scheduledTrigger" /&gt;
  &lt;/task:scheduled-tasks&gt;
&lt;/beans&gt;




Annotations

Les annotations spécifiques au scheduling peuvent activés par XML avec l&#8217;élément &lt;task:annotation-driven/&gt;.



&lt;beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="http://www.springframework.org/schema/task
                           https://www.springframework.org/schema/task/spring-task.xsd"&gt;
  &lt;task:annotation-driven/&gt;
&lt;/beans&gt;



Les annotations spécifiques au scheduling peuvent activés avec l&#8217;annotation de configuration @EnableScheduling.



@Configuration
@EnableScheduling
@ComponentScan("info.jtips.spring")
public class ApplicationConfiguration {
  ...
}



Une fois cette activation faire, on peut annoter les méthodes d&#8217;un bean avec @Scheduled.


On retrouve trois des options déjà présentes en XML :




fixedDelay avec la possibilité de choisir l&#8217;unité de temps (par défaut, milliseconde),


fixedRate avec la possibilité de choisir l&#8217;unité de temps (par défaut, milliseconde),


cron avec la possibilité de choisir le fuseau horaire de l&#8217;expression (par défaut, celui de la JVM).




L&#8217;option trigger n&#8217;existe pas avec cette annotation.



@Component
public class ScheduledBean {

  @Scheduled(fixedDelay = 10, timeUnit = TimeUnit.SECONDS)
  private void scheduledRate() throws InterruptedException {
    ...
  }

  @Scheduled(fixedRate = 10, timeUnit = TimeUnit.SECONDS)
  private void scheduledDelay() throws InterruptedException {
    ...
  }

  @Scheduled(cron = "*/10 * * * * *", zone = "UTC")
  private void scheduledCron() throws InterruptedException {
    ...
  }
}




Programmation

Les tâches peuvent aussi être planifiée en utilisant directement les méthodes de la classe org.springframework.scheduling.TaskScheduler.
On retrouve à nouveau les quatre options :




scheduleAtFixedRate(Runnable task, Duration delay)


scheduleWithFixedDelay(Runnable task, Duration delay)


schedule(Runnable task, Trigger trigger), où le trigger peut être une instance de CronTrigger.




Il est aussi possible de planifier une tâche à exécution unique avec schedule(Runnable task, Instant startTime).


Ce mode de fonctionnement a comme prérequis l&#8217;existence d&#8217;un bean de type TaskScheduler, qui était optionnel en XML et avec les annotations.



@Component
public class ScheduledDynamicBean {

  private final TaskScheduler taskScheduler;

  public ScheduledDynamicBean(TaskScheduler taskScheduler) {
    this.taskScheduler = taskScheduler;
  }

  @PostConstruct
  private void init() {
    taskScheduler.scheduleWithFixedDelay(
        () -&gt; ...,
        Duration.of(10, SECONDS)
    );

    taskScheduler.scheduleAtFixedRate(
        () -&gt; ...,
        Duration.of(10, SECONDS)
    );

    taskScheduler.schedule(
        () -&gt; ...,
        new CronTrigger("*/10 * * * * *")
    );

    taskScheduler.schedule(
        () -&gt; ...,
        triggerContext -&gt; nextCustomDate()
    );
  }

}




Transactions

Quel que soit le mode de planification des tâches, la gestion des transactions doit être indépendante.
Par exemple, les méthodes annotées par @Scheduled ne peuvent pas être annotées en plus par @Transactional, ça provoque une TransactionRequiredException comme si la méthode n&#8217;était pas annotée.



javax.persistence.TransactionRequiredException:
          No EntityManager with actual transaction available for current thread



La solution est d&#8217;appeler une méthode transactionnelle sur un bean de service distinct du bean qui supporte les tâches planifiées.





Mécanique interne


Dans ce dernier exemple, on a vu le besoin éventuel d&#8217;un bean de type TaskScheduler.
Cette interface a des méthodes de planification, mais ses implémentations ont aussi la responsabilité de la gestion des threads.


Sans TaskScheduler

L&#8217;exécution des tâches est gérée par le ScheduledTaskRegistrar.


S&#8217;il trouve un bean de type TaskScheduler, il l&#8217;utilise.
Sinon il crée son propre scheduler, mono-thread, mais ne le publie pas sous forme de bean.



  // ScheduledTaskRegistrar
  this.localExecutor = Executors.newSingleThreadScheduledExecutor();
  this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);



Par conséquent, sans TaskScheduler les tâches sont exécutées de façon séquentielle dans un seul thread.
Ça peut poser problème si des tâches ont un délai d&#8217;exécution long.



Avec TaskScheduler

Lorsqu&#8217;il y a un bean de type TaskScheduler, les tâches planifiées par annotation l&#8217;utilisent automatiquement.
On peut éventuellement ajouter une référence explicite pour résoudre d&#8217;éventuels conflits.


C&#8217;est facile à faire en XML.



  &lt;task:annotation-driven scheduler="taskScheduler"/&gt;



C&#8217;est un peu plus complexe en Java.



@Configuration
@EnableScheduling
public class ApplicationConfiguration implements SchedulingConfigurer {

  @Bean
  public TaskScheduler taskScheduler() {
    //...
    return poolTaskScheduler;
  }

  @Override
  public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    taskRegistrar.setTaskScheduler(taskScheduler());
  }
}










Les tâches déclarées en XML dans l&#8217;élément &lt;task:scheduled-tasks&gt; n&#8217;utilisent pas le task scheduler par défaut.
Il faut y faire référence explicitement.



  &lt;task:scheduled-tasks scheduler="taskScheduler"&gt;
    &lt;task:scheduled .../&gt;
  &lt;/task:scheduled-tasks&gt;








Implémentations de TaskScheduler

En XML, l&#8217;élément &lt;task:scheduler&gt; crée une instance de ThreadPoolTaskScheduler.
Cette classe utilise un ScheduledThreadPoolExecutor de l&#8217;API concurrent du JDK, avec la possibilité de choisir la taille du pool de threads.



  &lt;task:scheduler id="taskScheduler" pool-size="5"/&gt;



Avec la configuration Java, il n&#8217;y a pas d&#8217;implémentation par défaut, il faut explicitement instancier l&#8217;objet.



  @Bean
  public TaskScheduler taskScheduler() {
    ThreadPoolTaskScheduler poolTaskScheduler = new ThreadPoolTaskScheduler();
    poolTaskScheduler.setPoolSize(5);
    return poolTaskScheduler;
  }



Nous avons le choix avec une autre implémentation, ConcurrentTaskScheduler qui utilise pool de threads via un ScheduledExecutorService de l&#8217;API concurrent du JDK, qu&#8217;on passe explicitement ou qu&#8217;il détecte automatiquement.
Ça permet d&#8217;avoir un pool de threads au comportement plus élaboré.


En environnement Java EE / Jakarta EE, on choisira la troisième implementation, DefaultManagedTaskScheduler qui hérite de ConcurrentTaskScheduler et qui utilise un executor service récupéré via JNDI.



Avec Spring Boot

Spring Boot crée automatiquement un bean de type TaskScheduler, avec un pool d&#8217;un seul thread.



@SpringBootApplication
@EnableScheduling
public class SpringBootExampleApplication {

  public static void main(String[] args) {
    ...
  }

}



La configuration du bean TaskScheduler se fait par des propriété spring.task.scheduling.* dans le fichier application.properties ou application.yml.



# application.properties
spring.task.scheduling.pool.size=5
spring.task.scheduling.thread-name-prefix=app-task-



Pour personnaliser plus profondément le bean, on peut le créer soi-même, ou implémenter SchedulingConfigurer, comme dans un environnement sans boot.
On peut aussi créer un bean de type TaskSchedulerBuilder ou un bean de type TaskSchedulerCustomizer.



  @Bean
  public TaskSchedulerCustomizer taskSchedulerCustomizer() {
    return taskScheduler -&gt; taskScheduler.setPoolSize(
                                Runtime.getRuntime().availableProcessors());
  }






Solutions alternatives


Le TaskScheduler de Spring est simple et pratique, mais il a quelques lacunes.
Voici quelques fonctionnalités absentes et les solutions pour les avoirs dans Spring.


Intégration de Quartz




Persistance des tâches




Spring Batch




Reprise sur incident


Séparation lecture / traitement / écriture


Regroupement transactionnel


Traitement séquentiel ou parallèle




</description>
          <pubDate>2022-03-04T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/Scheduler</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/Scheduler</guid>
        </item>
      
    
      
        <item>
          <title>Conteneurs Docker pour les tests avec Spring</title>
          <description>
Dans la page dédiée à l&#8217;utilisation des conteneurs Docker pour les tests d&#8217;intégration, on voit comment Testcontainer est utilisé dans des tests JUnit pour démarrer et arrêter des conteneurs.
On y voit aussi comment récupérer les métadonnées des conteneurs, comme les ports exposés ou l&#8217;URL JDBC pour les bases de données.


Ici, nous allons voir comment ces métadonnées peuvent être prises en compte pour l&#8217;initialisation d&#8217;un contexte Spring, ou comment la configuration de Spring peut être utilisée pour la création des conteneurs.


ApplicationContextInitializer


On a déjà implémenté un ApplicationContextInitializer dans la page sur les profils Spring.
Une telle classe permet de modifier des informations avant la finalisation du contexte Spring et l&#8217;instanciation des beans.
Nous pouvons donc en créer une spécifiquement pour les tests, ou pour certains tests.



public class TestContainersInitializer
	implements ApplicationContextInitializer&lt;ConfigurableApplicationContext&gt; {

  @Override
  public void initialize(ConfigurableApplicationContext applicationContext) {
    TestPropertyValues
        .of(Map.of(
            "spring.datasource.url", pg.getJdbcUrl(),
            "spring.datasource.username", pg.getUsername(),
            "spring.datasource.password", pg.getPassword()))
        .applyTo(applicationContext);
  }

}



Avec ça, on démarre les conteneurs avant l&#8217;initialisation du contexte Spring et on récupère les valeurs qui nous intéressent.
La classe utilitaire TestPropertyValues permet de remplacer certaines propriétés, avant l&#8217;instanciation des beans.


Dans le test

L'ApplicationContextInitializer peut être déclaré commune classe interne du test, capable de lire les données des conteneur.



@Testcontainers
@SpringBootTest
@ContextConfiguration(initializers = {ContainerTest.ContainerInitializer.class})
public class ContainerTest {

  @Container
  static PostgreSQLContainer&lt;?&gt; pg = new PostgreSQLContainer&lt;&gt;("postgres:14");

  @Test
  void should_be_able_to_connect_to_pg() {
    //...
  }

  static class ContainerInitializer implements ApplicationContextInitializer&lt;ConfigurableApplicationContext&gt; {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
      TestPropertyValues
          .of(Map.of(
              "spring.datasource.url", pg.getJdbcUrl(),
              "spring.datasource.username", pg.getUsername(),
              "spring.datasource.password", pg.getPassword()))
          .applyTo(applicationContext);
    }
  }

}



Le conteneur peut être déclaré comme static pour avoir une instance partagée par toutes les méthodes du test.
Dans ce cas, la classe ContainerInitializer doit aussi être static.


Le conteneur peut être déclaré comme champ d&#8217;instance pour avoir une instance dédiée à chaque méthode du test.
Dans ce cas, la classe ContainerInitializer doit aussi être non static.



Singleton

En faisant une classe ContainerInitializer indépendante et en déclarant le conteneur dans un champ static la classe, celui-ci devient un singleton, partagé entre toutes les classes de test.



public class TestContainersInitializer
    implements ApplicationContextInitializer&lt;ConfigurableApplicationContext&gt; {

  static final PostgreSQLContainer&lt;?&gt; pg = new PostgreSQLContainer&lt;&gt;("postgres:14")
      .withDatabaseName("viseo3")
      .withUsername("chambersign_admin")
      .withPassword("secret")
      .withInitScript("init-db.sql");
  static final GenericContainer&lt;?&gt; smtp = new GenericContainer&lt;&gt;("munkyboy/fakesmtp")
      .withExposedPorts(25);

  @Override
  public void initialize(ConfigurableApplicationContext context) {
    if (!pg.isRunning()) {
      pg.start();
    }
    if (!smtp.isRunning()) {
      smtp.start();
    }

    TestPropertyValues
        .of(Map.of(
            "spring.datasource.url", pg.getJdbcUrl(),
            "spring.datasource.username", pg.getUsername(),
            "spring.datasource.password", pg.getPassword(),
            "spring.mail.port", smtp.getMappedPort(25).toString()
        ))
        .applyTo(context);
  }

}






On peut faire autrement, mais c&#8217;est moins bien


Ce qu&#8217;on a vu jusqu&#8217;à maintenant, ce sont des bonnes façons de faire.
Voyons maintenant une fausse bonne idée : utiliser la configuration des beans Spring pour initialiser le conteneur.


La première condition pour que ça fonctionne, c&#8217;est que les données nécessaires à la configuration du conteneur soient disponibles dans les beans.
Le seconde condition, c&#8217;est que les beans n&#8217;aient pas besoin de se connecter au conteneur pendant l&#8217;initialisation du contexte, puisque le conteneur n&#8217;est pas encore démarré.


Conteneur SMTP

Tout d&#8217;abord, voyons d&#8217;abord un cas facile, avec un serveur SMTP utilisé via les propriétés suivantes :


application-test.properties

sewatech.smtp.host=localhost
sewatech.smtp.port=8025



Nous pouvons injecter ces propriétés dans le test pour initialiser le conteneur.
Par ailleur, on injecte le JavaMailSender de Spring pour tester l&#8217;envoi du mail.



@SpringBootTest
public class SmtpSenderTest {

  GenericContainer&lt;?&gt; smtp;

  @Value("${spring.mail.port}")
  int smtpPort;

  @Autowired
  JavaMailSender emailSender;

  @BeforeEach
  public void init() {
    smtp = new GenericContainer&lt;&gt;("ghusta/fakesmtp")
        .withExposedPorts(25)
        .withCreateContainerCmdModifier(
            cmd -&gt; cmd.getHostConfig()
                      .withPortBindings(
                          new PortBinding(
                              Ports.Binding.bindPort(smtpPort),
                              ExposedPort.tcp(25))
                      )
        );
    smtp.start();
  }
  @AfterEach
  public void clean() {
    smtp.stop();
  }

  @Test
  void should_be_able_to_send_mail() {
    SimpleMailMessage message = new SimpleMailMessage();
    message.setFrom("author@jtips.info");
    message.setTo("reader@jtips.info");
    message.setSubject("Should work");
    message.setText("This email should be sent.");

    emailSender.send(message);
  }

}



Vous constatez que l&#8217;API de Testcontainers n&#8217;est pas du tout pratique pour fixer le port du conteneur.
C&#8217;est normal parce que c&#8217;est une mauvaise pratique.


Si vous êtes conscient des limites d&#8217;une telle démarche, alors l&#8217;exemple ci-dessus peut vous convenir : ça fonctionne.



Serveur de base de données

Pour augmenter la difficulté, voyons comment ça peut marcher avec une base de données utilisée via JPA et une DataSource.


A priori, la technique devrait être similaire.
Mais on constate vite quelques difficultés supplémentaires.


Tout d&#8217;abord, pour le port, il n&#8217;y a pas de propriété spring.datasource.port.
Le port de connexion à la base de données est compris dans l&#8217;URL JDBC spring.datasource.url.
Le port peut donc être extrait, comme le nom de la base de données.



@SpringBootTest
@TestPropertySource("classpath:application-test.properties")
public class DatabaseTest {

  @Value("${spring.datasource.url}")
  String datasourceUrl;
  @Value("${spring.datasource.username}")
  String datasourceUsername;
  @Value("${spring.datasource.password}")
  String datasourcePassword;

  @BeforeEach
  public void init() {
    Pattern pattern = Pattern.compile(".://(.):(.)/([^?])?.*");
    Matcher urlMatcher = pattern.matcher(datasourceUrl);
    if (urlMatcher.matches()) {
	  String databaseHost = urlMatcher.group(1);
	  String databasePort = urlMatcher.group(2);
      String databaseName = urlMatcher.group(3);

      new PostgreSQLContainer&lt;&gt;("postgres:14")
          .withDatabaseName(databaseName)
          .withUsername(datasourceUsername)
          .withPassword(datasourcePassword)
          .withCreateContainerCmdModifier(
              e -&gt; e.getHostConfig()
                  .withPortBindings(
                      new PortBinding(
                          Ports.Binding.parse(databasePort),
                          new ExposedPort(5432))
                  )
          )
		  .start();
    }
  }

}



Les vrais problèmes commencent maintenant.
En effet, ceci ne fonctionne pas parce qu&#8217;on démarre la base de données après la création de la DataSource et après l&#8217;initialisation de l'EntityManagerFactory.


Le premier règlage consiste à désactiver l&#8217;initialisation du schéma de la base de données.


Le second règlage est la désactivation de la lecture des métadonnées.
Cette lecture permet à Hibernate de récupérer certaines informations qui doivent être remplacée par un surplus de configuration.
Pour notre test, il faudra surtout ajouter le dialecte.


application-test.properties

spring.jpa.hibernate.ddl-auto=none

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL10Dialect
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false






Conclusion


Dans le test, ça permet surtout de choisir ses conteneurs.
Dans une classe indépendante, ça évite de se répéter.
A l&#8217;envers, ça ne sert à rien, il vaut mieux passer par un initialiseur.


Références




Code source des exemples




</description>
          <pubDate>2022-01-31T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/Testcontainers</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/Testcontainers</guid>
        </item>
      
    
      
        <item>
          <title>Conteneurs Docker pour les tests d&apos;intégration</title>
          <description>
Pour les tests d&#8217;intégration, on a besoin d&#8217;accéder à des bases de données, brokers de messages,&#8230;&#8203;
Docker nous facilite la tâche si on l&#8217;intègre aux tests.
</description>
          <pubDate>2022-01-31T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JUnit/Testcontainers</link>
          <guid isPermaLink="true">https://www.jtips.info/JUnit/Testcontainers</guid>
        </item>
      
    
      
        <item>
          <title>Annotations AspectJ avec Spring</title>
          <description>
Avec l&#8217;AOP[1], on ajoute des comportements aux classes et méthodes existante, en se basant sur leurs métadonnées : nom, package, annotations,&#8230;&#8203;


Ainsi la gestion transactionnelle peut aussi être réglée par cette technique, de même que l&#8217;ajout de traces ou la mesure de performances.


Configuration


Pour commencer, il faut activer la prise en compte d&#8217;AspectJ dans une classe de configuration.



@Configuration
@EnableAspectJAutoProxy
public class ApplicationConfiguration {
}





Configuration


La classe d&#8217;aspect est implémentée avec des annotations d'AspectJ.
Elle doit aussi être un bean Spring.



@Aspect @Component
public class LoggingInterceptor {
}



Dans cette classe, on peut définir les pointcuts et les advices.


Un pointcuts définit une règle d&#8217;exécution des aspects, par exemple :




à l&#8217;exécution d&#8217;une méthode dont le nom commence par find&#8230;&#8203;,


à l&#8217;exécution d&#8217;une méthode annotée par @Logging




Un point de jonction définit la façon dont le traitement va s&#8217;intégrer par rapport au pointcut, par exemple :




avant le pointcut,


après que le pointcut ait produit un résultat,


après que le pointcut ait levé une exception




Un advice, parfois traduit en greffon, définit le traitement qui doit être exécuté à un point de jonction d&#8217;un pointcut.




Advice


Pour définir un advice, on utilise une de ces annotation d&#8217;AspectJ sur une méthode de la classe d&#8217;aspect :




@Before



avant le pointcut,


la méthode d'advice a un paramètre optionnel de type JoinPoint qui représente les métadonnées du point de jonction.





@After



après le pointcut,


la méthode d'advice a un paramètre optionnel de type JoinPoint qui représente les métadonnées du point de jonction.





@AfterReturning



après le pointcut, sans levée d&#8217;exception


la méthode d'advice a un paramètre optionnel de type JoinPoint qui représente les métadonnées du point de jonction,


la méthode d'advice a un paramètre optionnel pour le retour du pointcut.





@AfterThrowing



après le pointcut, avec une levée d&#8217;exception,


la méthode d'advice a un paramètre optionnel de type JoinPoint qui représente les métadonnées du point de jonction,


la méthode d'advice a un paramètre optionnel pour l&#8217;exception levée par le pointcut.





@Around



autour du pointcut (avant et après)


la méthode d'advice a un paramètre de type ProceedingJoinPoint qui représente les métadonnées du point de jonction,


la méthode proceed(&#8230;&#8203;) doit être appelée explicitement sur le point de jonction pour qu&#8217;il soit effectivement exécuté.







Le paramètre passé à l&#8217;annotation définit l&#8217;expression du pointcut ou fait référence à un pointcut définit ailleurs.



@Component
@Aspect
public class LoggingInterceptor {

  @Before("execution(* info.jtips.spring.dao.*.find*(..))")
  public void logDaoFind() throws Throwable {
    System.out.println("==&gt; Début dao.find");
  }

  @Around("service()")
  public Object logService(ProceedingJoinPoint point) throws Throwable {
    String methodName = point.getSignature().toString();
    System.out.printf("==&gt; Début de service : '%s'%n", methodName);
    Object obj = point.proceed();
    System.out.printf("&lt;== Fin de service : '%s'%n", methodName);

    return obj;
  }

  //...

}



Pour @AfterReturning et @AfterThrowing, il faut préciser le nom du paramètre qui recevra le retour ou l&#8217;exception.



  @AfterThrowing(
        pointcut = "execution(* info.jtips.spring..(..))",
        throwing = "exception")
  public void logProblem(JoinPoint point, Exception exception) {
    System.out.printf("==&gt; Problème : %.100s sur '%s'%n", exception, point.getSignature());
  }

  @AfterReturning(
        pointcut = "execution(* info.jtips.spring..(..))",
        returning = "result")
  public void logReturn(JoinPoint point, Product result) {
    System.out.printf("==&gt; Résultat : %.100s de '%s'%n", result, point.getSignature());
  }



Sans cette précision, Spring et AspectJ lèvent une exception à l&#8217;initialisation, dont le message n&#8217;est pas très parlant.



java.lang.IllegalArgumentException: error at ::0 formal unbound in pointcut
    at org.aspectj.weaver.tools.PointcutParser.parsePointcutExpression(PointcutParser.java:319)
    at org.springframework.aop.aspectj.AspectJExpressionPointcut...
    at ...





Pointcut


Les pointcuts sont définis directement dans les annotations des advices, comme dans la méthode logDaoFind() de l&#8217;exemple ci-dessus, ou à part pour être réutilisées entre plusieurs advices.



@Aspect @Component
public class LoggingInterceptor {

  @Around("service()")
  public Object logService(ProceedingJoinPoint point) throws Throwable {
    //...
  }

  @Pointcut("execution(* info.jtips.spring.service..(..))")
  private void service() {
  }

}



L&#8217;annotation @Pointcut s&#8217;utilise sur une méthode vide.
Elle sert juste à définir le nom du pointcut.


L&#8217;expression qui définit le pointcut est écrite dans le langage d&#8217;AspectJ.
En réalité, Spring AOP supporte uniquement une partie des primitives du langage.


Dans l&#8217;exemple ci-dessus, le pointcut execution(* info.jtips.spring.service..(..)) représente l&#8217;exécution de n&#8217;importe quelle méthode de n&#8217;importe quelle classe du paquetage info.jtips.spring.service.




Pointcut sur annotation


Plutôt que de définir un pointcut par le nom des paquetages, classes et méthodes , il est possible de le définir par la présence d&#8217;une annotation.
L&#8217;annotation peut être recherchée sur une classe, avec la primitive @within, ou sur une méthode avec  @annotation.



@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Logging {
}




@Aspect @Component
public class LoggingAnnotationInterceptor {

  @Pointcut("@annotation(Logging) || @within(Logging)")
  private void logging() {
  }

}



Avec un tel pointcut, on peut créer des advices qui vont opérer sur les méthodes annotées avec @Logging et celles qui sont dans des classes annotées avec @Logging.


Cette technique peut s&#8217;utiliser avec des annotations existantes.
Par exemple, on peut définir un comportement pour toutes les méthodes des classes annotées par @Service.



@Aspect @Component
public class LoggingAnnotationInterceptor {

  @Pointcut("@within(org.springframework.stereotype.Service)")
  private void service() {
  }

}



Ce pointcut peut servir de base à une gestion des transactions avec les objectifs définis dans le paragraphe sur les annotations de transaction.




Références




Spring Core Technologies - Aspect Oriented Programming


AspectJ








1. Aspect Oriented Programming

</description>
          <pubDate>2022-01-07T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/AspectJ</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/AspectJ</guid>
        </item>
      
    
      
        <item>
          <title>Profils avec Spring Boot</title>
          <description>
La notion de profile permet d&#8217;activer ou désactiver des beans en fonction du contexte de déploiement.


Un exemple courant est la spécification d&#8217;une datasource qui peut être différente entre les environnements de développement, de test et de déploiement.


Fonctionnement de base


@Profile

Un bean annoté avec @Profile ne sera valable que si le profil spécifié est actif dans le contexte Spring.



@Component
@Profile("dev")
public class DevOnlyBean implements SomeBean {
  ...
}




@Component
@Profile("prod")
public class ProdOnlyBean implements SomeBean {
  ...
}




Active profiles

Il y a plusieurs façons d&#8217;activer des profils.




par propriété système





~# java -jar -Dspring.profiles.active=prod app.jar





par paramètre de programme





~# java -jar app.jar --spring.profiles.active=prod





par programmation, cf. API


par configuration, cf. Configuration











Par le passé, il était aussi possible de spécifier des profils additionnels avec la propriété ou le paramètre spring.profiles.include.
Cette possibilité a disparu avec Spring Boot 2.4, au profit de la notion de groupe de profils.







Default profiles

Si aucun profil n&#8217;est spécifié au démarrage, ce sont les beans et configurations marquée en "default" qui sont utilisés.
Il est aussi possible de changer ça avec la propriété ou le paramètre spring.profiles.default.



~# java -jar -Dspring.profiles.default=none app.jar



Je ne sais pas trop à quoi ça peut servir, mais je le note au cas où&#8230;&#8203;





Configuration


Avec Spring Boot, la configuration est dans un fichier application.properties ou application.yml.


Ajout de profil

Il est possible de spécifier les profils dans le fichier de configuration.


application.properties

spring.profiles.active=cloud
...



Si des profils ont déjà été spécifié via la propriété spring.profiles.active, ceux du fichier de configuration viennent s&#8217;ajouter.



Configuration spécifique

En plus de rattacher des beans à un profil, on peut aussi faire une configuration spécifique.
Celle-ci se fait dans un fichier nommé application-{profile}.properties ou application-{profile}.yml.


Par exemple, on peut configurer une datasource uniquement pour le développement.


application-dev.yml

spring:
  datasource:
    jdbc-url: jdbc:postgresql://localhost:5432/postgres
    username: 'postgres'
    password: 'pgpwd'










On ne peut pas activer d&#8217;autres profils dans un fichier de configuration dédié à un profil.







Configuration multi-profils

Depuis Spring Boot 2.4, il est aussi possible de regrouper les configurations de plusieurs profils dans le même fichier.
Pour ça, on utilise la notion de fichiers multi-documents de YAML.
Et chaque document du fichier indique quel profil il configure.


application.yml

...
---
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    jdbc-url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: pgpwd



Spring Boot supporte aussi ça avec des fichiers properties, avec le séparateur #---.


application.properties

...
#---
spring.config.activate.on-profile=dev
spring.datasource.jdbc-url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=pgpwd






API


On peut lire la liste des profils actifs via le bean d&#8217;environnement.



@Component
public class SomeBean {

  private final Environment environment;

  public MainBean(Environment environment) {
    this.environment = environment;
  }

  @PostConstruct
  public void init() {
    var defaultProfiles = environment.getDefaultProfiles();
    var activeProfiles = environment.getActiveProfiles();
  }

}



On peut aussi modifier la liste de profils actifs par programmation, mais uniquement avant le démarrage du contexte Spring.




Démarrage d&#8217;une application Spring Boot





@SpringBootApplication
public class SpringExampleApplication {

  public static void main(String[] args) {
    new SpringApplicationBuilder()
        .sources(SpringExampleApplication.class)
        .profiles("profile-a", "profile-b")
        .build()
        .run(args);
  }

}





Initialisation du contexte Spring
(à déclarer au démarrage de l&#8217;application Spring Boot, dans web.xml ou dans la classe de test)





public class CommonProfileInitializer
    implements ApplicationContextInitializer&lt;ConfigurableApplicationContext&gt; {

  @Override
  public void initialize(ConfigurableApplicationContext context) {
    context.getEnvironment()
           .addActiveProfile("common");
  }

}





Post-processeur
(à déclarer dans le fichier META-INF/spring.factories)





public class CommonProfilePostProcessor
    implements EnvironmentPostProcessor {

  @Override
  public void postProcessEnvironment(
      ConfigurableEnvironment environment,
      SpringApplication application) {
    environment.addActiveProfile("common");
  }

}



META-INF/spring.factories

org.springframework.boot.env.EnvironmentPostProcessor=info.jtips.spring.CommonProfilePostProcessor





Tests automatisés


Utiliser les profils pour les tests JUnit passe évidemment par l&#8217;utilisation d&#8217;un profil "test" !


@ActiveProfiles

Pour choisir les profils actifs au démarrage d&#8217;un test, on utilise habituellement l&#8217;annotation @ActiveProfiles.



@SpringBootTest
@ActiveProfiles("test")
class MainBeanTest {
  ...
}



Le fonctionnement de cette annotation est un peu particulier puisque les profils spécifiés ici remplacent tous les autres. Les propriétés système sont ignorées, ainsi que les profils activés dans la configuration application.properties ou application.yml.
Ce comportement est définit dans l'ActiveProfilesResolver par défaut.



ActiveProfilesResolver

Pour qu&#8217;une autre source de profils soit prise en compte avec l&#8217;annotation @ActiveProfiles,  il faut adopter un ActiveProfilesResolver personnalisé.



@SpringBootTest
@ActiveProfiles(profiles="test", resolver=EnhancedActiveProfileResolver.class)
class MainBeanTest {
  ...
}



Pour ce test, c&#8217;est dans la classe EnhancedActiveProfileResolver qu&#8217;on définit les sources de profils.
Dans l&#8217;implémentation ci-dessous, on combine les profils de l&#8217;annotation @ActiveProfiles avec ceux de la propriété système spring.profiles.active.



public class EnhancedActiveProfileResolver
        implements ActiveProfilesResolver {

    public static final String PROPERTY_KEY = "spring.profiles.active";

    private final DefaultActiveProfilesResolver defaultActiveProfilesResolver
                = new DefaultActiveProfilesResolver();

    @Override
    public String[] resolve(Class&lt;?&gt; testClass) {
        return Stream
            .concat(
                Stream.of(defaultActiveProfilesResolver.resolve(testClass)),
                Stream.of(this.getPropertyProfiles())
            )
            .toArray(String[]::new);
    }

    private String[] getPropertyProfiles() {
        return System.getProperties().containsKey(PROPERTY_KEY)
                ? System.getProperty(PROPERTY_KEY).split("\\s*,\\s*")
                : new String[0];
    }
}



Cette solution a encore des défauts.
En effet, cette classe n&#8217;a aucune information de contexte Spring, elle ne peut donc pas récupérer les profils qui seraient activés dans le fichier de configuration application.properties ou application.yml.



ApplicationContextInitializer

On a vu la possibilité ci-dessus la possibilité d&#8217;ajouter un profil dans une classe d&#8217;initialisation du contexte.
Cette solution a l&#8217;avantage de conserver tous les autres profils.


Il est possible de déclarer cette initialisation dans le test.



@ContextConfiguration(initializers = ProfileInitializer.class)
class MainBeanWithInitializerTest {
  //...
}




EnvironmentPostProcessor

On a aussi vu la possibilité d&#8217;ajouter un profil dans une classe post-processeur.
Cette solution conserve aussi tous les autres profils.


Pour activer le post-processeur, il faut l&#8217;activer dans un fichier META-INF/spring.factories.
Pour ne l&#8217;activer qu&#8217;en test, il suffit de le déclarer dans le META-INF/spring.factories de test.



ContextCustomizerFactory

Une autre solution passe par le développement d&#8217;un ContextCustomizerFactory.
Cette solution a aussi l&#8217;avantage de conserver tous les autres profils.
Elle a l&#8217;autre avantage d&#8217;être globale, et évite d&#8217;ajouter une annotation à chaque test.



public class CustomContextCustomizerFactory
        implements ContextCustomizerFactory {

  @Override
  public ContextCustomizer createContextCustomizer(
                Class&lt;?&gt; testClass,
                List&lt;ContextConfigurationAttributes&gt; configAttributes) {
    return (context, mergedConfig) -&gt; {
      context.getEnvironment().addActiveProfile("test");
    };
  }
}



Cette fabrique doit être déclarée dans META-INF/spring.factories.


META-INF/spring.factories

org.springframework.test.context.ContextCustomizerFactory=info.jtips.spring.profiles.CustomContextCustomizerFactory



Si toutefois on continue d&#8217;utiliser l&#8217;annotation @ActiveProfiles, le personnalisateur de contexte ajoute son profil à ceux de l&#8217;annotation.
Et si c&#8217;est un profil identique, il n&#8217;a pas d&#8217;effet, puisque les doublons sont éliminés.





Synthèse


Dans cette page, nous avons vu les cas d&#8217;usage suivants :




profils pour un processus : spring.profiles.active en propriété système ou paramètre du processus,


profils pour tous les processus (hors tests) : springApplicationBuilder.profiles(&#8230;&#8203;),


profils pour les tests : @ActiveProfiles ou CustomContextCustomizerFactory,


profils pour tous les processus (hors tests) : application.yml et tests avec @ActiveProfiles,


profils pour tous les processus (tests compris) : application.yml et tests avec CustomContextCustomizerFactory.




Spring Boot

Parmi les techniques citées, les suivantes sont spécifiques à Spring Boot :




paramètre du processus


springApplicationBuilder.profiles(&#8230;&#8203;)


application.yml





Références



Exemples de code, avec Spring 5.3, Spring Boot 2.6 et JUnit 5.8


Spring Boot Core Features - Profiles





</description>
          <pubDate>2021-12-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/Profile</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/Profile</guid>
        </item>
      
    
      
        <item>
          <title>Build Docker avec paramètres</title>
          <description>
On peut définir des arguments (ARG) dans un fichier Dockerfile.
Ces arguments sont des variables utilisées au moment du build, ce qui les différencie des variables d&#8217;environnement (ENV) qui sont utilisées au moment du run.


Définition d&#8217;un argument


Dans les exemples ci-dessous, j&#8217;utilise l&#8217;exemple de construction d&#8217;une image pour WildFly, à partir de son code source.


Grâce à un argument de construction, on va pouvoir isoler la version de WildFly qu&#8217;on veut utiliser.



FROM openjdk:11
MAINTAINER Alexis Hassler &lt;alexis.hassler@sewatech.fr&gt;

ARG WILDFLY_VERSION=24.0.1.Final

RUN (curl -skL http://download.jboss.org/wildfly/$WILDFLY_VERSION/wildfly-$WILDFLY_VERSION.tar.gz | tar xfz -) &amp;&amp; \
    mv /wildfly-$WILDFLY_VERSION /wildfly &amp;&amp; \
    rm -r /wildfly/welcome-content &amp;&amp; \
    /wildfly/bin/add-user.sh --silent alexis hassler

COPY html /wildfly/welcome-content

EXPOSE 8080 8009 9990

ENTRYPOINT ["/wildfly/bin/standalone.sh"]
CMD ["-c", "standalone-full.xml"]



Ici, la variable WILDFLY_VERSION contient la version à télécharger au build.
On l&#8217;utilise comme une variable d&#8217;environnement (ENV), avec le symbole $.


Pour construire une autre version, on la spécifie dans la commande de build.



~# docker build --build-arg WILDFLY_VERSION=11.0.0.Final -t sewatech/wildfly:11 .





Argument en tête


Contrairement aux variables d&#8217;environnement, les arguments peuvent être déclarés avant le FROM.
De ce fait, l&#8217;image de base ou son tag, peut être défini sous forme d&#8217;argument de build.



ARG JDK_VERSION=11

FROM openjdk:11
MAINTAINER Alexis Hassler &lt;alexis.hassler@sewatech.fr&gt;

ARG WILDFLY_VERSION=24.0.1.Final
...



Ainsi, au moment du build, on peut choisir à la fois la version de WildFly et celle du JDK.



~# docker build --build-arg JDK_VERSION=8 --build-arg WILDFLY_VERSION=11.0.0.Final -t sewatech/wildfly:11_jdk8 .





Argument en tête multi-stage build


La portée d&#8217;un argument est le fichier Dockerfile.
Dans un mutli-stage build, un argument peut donc être partagé entre les étapes.


Evidemment, ce build devrait être fait en 2 étapes :



ARG JDK_VERSION=11

# =====
FROM openjdk:$JDK_VERSION as wildfly-build
MAINTAINER Alexis Hassler &lt;alexis.hassler@sewatech.fr&gt;

ARG WILDFLY_VERSION=24.0.1.Final

RUN (curl -skL https://download.jboss.org/wildfly/$WILDFLY_VERSION/wildfly-$WILDFLY_VERSION.tar.gz | tar xfz -) &amp;&amp; \
    mv /wildfly-$WILDFLY_VERSION /wildfly &amp;&amp; \
    rm -r /wildfly/welcome-content &amp;&amp; \
    /wildfly/bin/add-user.sh --silent alexis hassler

COPY html /wildfly/welcome-content

# =====
FROM openjdk:$JDK_VERSION

RUN groupadd -r wildfly -g 1000 &amp;&amp; \
    useradd -u 1000 -r -g wildfly -m -d /opt/wildfly -s /sbin/nologin -c "WildFly user" wildfly &amp;&amp; \
    chmod 755 /opt/wildfly

COPY --from=wildfly-build --chown=jboss:0 /wildfly /opt/wildfly

WORKDIR /opt/wildfly
USER wildfly
ENV LAUNCH_JBOSS_IN_BACKGROUND true

EXPOSE 8080 8009 9990

ENTRYPOINT ["bin/standalone.sh"]
CMD ["-c", "standalone-full.xml"]





Synthèse


En résumé,




on définit un argument de build dans le Dockerfile avec la directive ARG,





ARG APP_VERSION=7.42





on l&#8217;utilise avec le symbole $,





RUN (curl -skL https://dl.sewa.tech/src/app-$APP_VERSION.tar.gz | tar xfz -)





on la modifie au build avec l&#8217;option --build-arg.





~# docker build --build-arg APP_VERSION=7.31 -t sewatech/app:7_31 .



</description>
          <pubDate>2021-12-10T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Docker/BuildParam</link>
          <guid isPermaLink="true">https://www.jtips.info/Docker/BuildParam</guid>
        </item>
      
    
      
        <item>
          <title>Propriétés système Java</title>
          <description>
Propriétés classiques


Les propriétés système du niveau JVM ou JDK sont souvent utilisées en lecture.
Voici une liste de quelques propriétés qu&#8217;on modifie souvent au démarrage.




-Duser.language=en pour forcer la langue par défaut de la JVM


-Duser.timezone=UTC, pour fixer le fuseau horaire de la JVM ; ce serait tellement plus simple si tout était en UTC


-Djava.io.tmpdir=&#8230;&#8203; pour fixer le répertoire temporaire de la JVM


-Djava.awt.headless=true pour une application serveur qui utilise des classes AWT


-Djava.net.preferIPv4Stack=true pour forcer l&#8217;utilisation de IPv4, sinon les sockets sont ouvertes en priorité en IPv6






Modifier une propriété


Au démarrage



java -Duser.timezone=UTC Application



Dans le code



System.setProperty("java.home", "UTC");





Lire les propriétés (depuis le code)



System.getProperties()
      .list(System.out);




String home = System.getProperty("java.home");

// avec une valeur par défaut
String timezone = System.getProperty("user.timezone", "UTC");





Lire les propriétés (CLI)



java -XshowSettings:all -version




jinfo -sysprops &lt;pid&gt;




jcmd  &lt;pid&gt; VM.system_properties



</description>
          <pubDate>2021-12-02T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/SystemProperty</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/SystemProperty</guid>
        </item>
      
    
      
        <item>
          <title>Administration JBoss AS 7 en ligne de commande</title>
          <description>






Certaines commandes sont trop longues pour être affichées sur une seule ligne ; dans ce cas, je les ai affichées sur plusieurs lignes, avec '\' comme signe de continuation.





JBoss AS 7 / EAP 6


La plupart des commandes sont identiques entre JBoss et WildFly. Il y a quand même quelques commandes qui sont différentes.


Les commandes décrites ici concernent les anciennes versions de WildFly que sont JBoss AS 7 et son dérivé commercial JBoss EAP 6.
Pour WildFly, les lignes de commandes sont dans la page Administration WildFly en ligne de commande.


Access Log

Pour JBoss AS 7, le subsystem undertow n&#8217;existait pas, son équivalent était le subsytem web, et la configuration était légèrement différente :



[host:9990 /] /subsystem=web/virtual-server=default-host/access-log=configuration:add

[host:9990 /] cd /subsystem=web/virtual-server=default-host/access-log=configuration
[host:9990 /] :write-attribute(name=pattern, value=%h\ %l\ %u\ %t\ "%r"\ %s\ %b\ %D)



Dans JBoss EAP 6, on a aussi le subsystem web, mais la commande est légèrement différente :



[host:9990 /] /subsystem=web/virtual-server=default-host/configuration=access-log:add

[host:9990 /] cd /subsystem=web/virtual-server=default-host/configuration=access-log
[host:9990 /] :write-attribute(name=pattern, value="%h %l %u %t \"%r\" %s %b %D")




Authentification

J&#8217;ai mis un peu de temps pour comprendre comment construire cette commande pour JBoss AS 7, mais la voilà, pour un security domain qui utilise une base de données.



[host:9990 /] /subsystem=security/security-domain=sw-domain:add(cache-type=default)
[host:9990 /] /subsystem=security/security-domain=sw-domain/authentication=classic                                \
                  :add(login-modules=[                                                                            \
                        {"code"=&gt;"Database", "flag"=&gt;"required",                                                  \
                         "module-options"=&gt;{"dsJndiName" =&gt; "java:/datasources/SewaDS",                           \
                                            "principalsQuery" =&gt; "select pwd from Users where username=?",        \
                                            "rolesQuery" =&gt; "select roles, 'Roles' from Roles where username=?"}}])



Et pour un security domain qui utilise des fichiers propoerties, ça ressemble beaucoup.



[host:9990 /] /subsystem=security/security-domain=sw-domain:add(cache-type=default)
[host:9990 /] /subsystem=security/security-domain=sw-domain/authentication=classic                                \
                   :add(login-modules=[                                                                           \
                        {"code"=&gt;"UsersRoles", "flag"=&gt;"required",                                                \
                         "module-options"=&gt;{"usersProperties"=&gt;"${jboss.server.config.dir}/sw-users.properties",  \
                                            "rolesProperties"=&gt;"${jboss.server.config.dir}/sw-roles.properties"}}])



La configuration de JBoss EAP 6 est similaire à WildFly.



Développement

Dans JBoss AS 7, la commande était légèrement différente, puisqu&#8217;on avait un sous-système web à la place de undertow :



[host:9990 /] /subsystem=web/configuration=jsp-configuration:write-attribute(name=development, value=true)




Communications SSL

Après avoir créé la paire de clés, on crée un connector sur le port 8443, qui utilise la clé (placée dans le répertoire configuration de mon WildFly).



[host:9990 /] /subsystem=web/connector=https:add                               \
                     (protocol=HTTP/1.1,                                       \
                      scheme=https,                                            \
                      secure=true,                                             \
                      socket-binding=https)
[host:9990 /] /subsystem=web/connector=https/ssl=configuration:add             \
                     (certificate-key-file=${jboss.server.config.dir}/sewa.ks, \
                      key-alias=sewa,                                          \
                      password=sewakey,                                        \
                      protocol=TLS)




</description>
          <pubDate>2021-12-01T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss7/CLI</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss7/CLI</guid>
        </item>
      
    
      
        <item>
          <title>Archives</title>
          <description>
Les articles ci-dessous commencent à être anciens, mais conservent un intérêt dans quelques rares cas&#8230;&#8203;






Développement Java



Nouveautés JavaSE 7


Exporter les résultats de JDepend au format CSV


Convertion Word ou OpenOffice en PDF


Ligne de commande avec Apache CLI


Interopérabilité Java / PHP


Détecter la présence d&#8217;une JRE


Monitoring de Java par SNMP


Sérialisation des types énumérés


Tutoriel, partie 1


Tutoriel, partie 2





XML et Services Web



Pile des techniques JAX


Marshalling de Map avec JAX-B


Service web JAX-WS avec CXF et Spring


Rediriger les traces de CXF vers Log4J





Java EE



Zone HTML wysiwyg


Gestion des sessions http


Sécurité des EJB avec JAAS


Business Delegate





JSF



Choix des librairies JSF


Utiliser les convertisseurs


Gérer les validations de saisie


Ouvrir une popup depuis une page JSF


Super classe pour les backing beans


Utiliser la bibliothèque Tomahawk


Formulaire multiligne avec un Data Table


Utiliser Shale Validator avec Facelets


Techniques sur les Value Change Listener


Utiliser une action &lt;i&gt;immediate&lt;/i&gt;


Balise &lt;c:set&gt; dans une page JSF





CDI



Scopes avec Weld SE


Tests avec Weld SE





Spring



Sécurité d&#8217;une application Web avec Acegi


Authentification silencieuse avec Acegi et NTLM





JBoss



Installation de JBoss 5


Installation de JBoss 4


Déploiement d&#8217;applications


Développement de MBean et de service


RMI sur http


JBoss derrière un pare-feu


Développement d&#8217;un module JAAS


Monitoring JMX


Sécurisation des accès JMX


Proxy JDBC (P6Spy)


DefaultDS


SecurityManager


Logging dans JBoss AS 4 et 5


Logging dans JBoss AS 6





Glassfish



Installation


Connecter Glassfish à un serveur Jackrabbit


Installer une datasource





WebLogic



Installation





Jonas



Portage de Weblogic vers Jonas


Gestion des mots de passe







</description>
          <pubDate>2021-11-30T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Archives</link>
          <guid isPermaLink="true">https://www.jtips.info/Archives</guid>
        </item>
      
    
      
        <item>
          <title>Intercepter les signaux `kill`</title>
          <description>
Intercepter l&#8217;arrêt de la JVM


Avec l&#8217;objet Runtime, il est possible d&#8217;ajouter des tâches exécutées au moment de l&#8217;arrêt de la JVM.



Runtime.getRuntime()
       .addShutdownHook(
            new Thread(() -&gt; System.out.println("Cleaning before shutdown"))
       );





Les signaux Linux


Pour arrêter sagement un process :



$ kill 1234
ou
$ kill -TERM 1234



Pour simuler un CTRL+C :



$ kill -INT 1234
ou
$ kill -2 1234



Quelques signaux :




HUP (1) : hangup


INT (2) : terminal interrupt (CTRL+C)


QUIT (3) : terminal quit, with core dump


KILL (9) : kill (cannot be caught)


TERM (15) : termination (default)


&#8230;&#8203;




La plupart de ces signaux provoquent l&#8217;arrêt du process, avec éventuellement un core dump (QUIT, ILL, TRAP,&#8230;&#8203;).
Ce comportement par défaut peut être redéfini par le process.
Seul le signal KILL ne peut pas être intercepté.


La liste complète des signaux peut être obtenue par la commande kill -l.




Les signaux Linux en Java


Le signal QUIT est intercepté par le JVM pour générer un thread dump.


Il est possible de définir ses propres inteceptions avec la classe sun.misc.Signal.



Signal.handle(
        new Signal("INT"),
        this::handleINT
    );



Attention, le comportement est très différent des shutdown hooks.
En effet, ici on n&#8217;ajoute pas d&#8217;action mais on remplace l&#8217;action par défaut.
Par conséquent, si on veut que le process s&#8217;arrête, il faut le lui demander explicitement : System.exit(0).


Comme spécifié plus haut, KILL ne peut pas être intercepté.
Si on essaie, ça déclenche une exception.



java.lang.IllegalArgumentException: Signal already used by VM or OS: SIGKILL
	at java.base/jdk.internal.misc.Signal.handle(Signal.java:172)
	at jdk.unsupported/sun.misc.Signal.handle(Signal.java:157)
	at ...



Il est aussi possible d&#8217;envoyer un signal au process courant, à condition que celui-ci le gère :



Signal.raise(new Signal("HUP"));










Attention, la classe Signal n&#8217;est pas dans les packages standards.
Elle est dans le module jdk.unsupported ce qui signifie qu&#8217;elle peut disparaitre.


Elle existe depuis le JDK 1.2, et est toujours utilisable dans le JDK 17.
Son support n&#8217;a été ajouté que tardivement dans GraalVM, en version 21.1.








Avec Docker


A l&#8217;intérieur du conteneur, la principale subtilité vient du fait que l&#8217;application est souvent sur le process numéro 1 et qu&#8217;on ne peut pas lui adresser de signal KILL.
Les autres signaux fonctionnent bien.


Depuis l&#8217;hôte, on peut transmettre un signal par la commande docker kill.
Le signal par défaut, si on ne spécifie par --signal, est KILL.



$ docker kill --signal=INT &lt;container-name&gt;





Références




Wikipedia - Signal (informatique)


Docker - docker kill


GraalVM release note - Java on Truffle




</description>
          <pubDate>2021-11-30T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/Signal</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/Signal</guid>
        </item>
      
    
      
        <item>
          <title>MailHog</title>
          <description>
MailHog est un serveur SMTP.
On peut lui envoyer des e-mails mais il ne les transmet jamais aux destinataires.
Il les garde en mémoire et les restitue via une interface Web.


On l&#8217;utilise généralement en environnement de développement ou en test d&#8217;intégration.


Docker


Le projet a démarré en 2014.
Il est fait en Go.
Sa maintenance est au ralenti, plein de tickets sont ouverts et des MR sont soumises.
A suivre : fork ?


Les auteurs ont publié une image Docker : mailhog/mailhog.
Mais elle n&#8217;est pas mise à jour.
Il existe plein d&#8217;alternatives comme druidfi/mailhog qui est mise à jour régulièrement et est plus petite (8 Mo, from scratch) que l&#8217;originale (400 Mo).



docker run --publish 127.0.0.1:2025:1025 --publish 127.0.0.1:8025:8025 --detach druidfi/mailhog



Le port 1025 est utilisé pour l&#8217;envoi de messages en SMTP.
Le port 8025 est utilisé pour consulter les messages via l&#8217;interface Web.


Docker Compose


  smtp:
    image: druidfi/mailhog
    ports:
      - "127.0.0.1:2025:1025"
      - "127.0.0.1:8025:8025"






JUnit / Testcontainer


Ça peut aussi se démarrer avec Testcontainer pour des tests d&#8217;intégration automatisés.



  GenericContainer&lt;?&gt; smtp = new GenericContainer&lt;&gt;("druidfi/mailhog")
                                        .withExposedPorts(1025, 8025);
  smtp.start();

  int smtpPort = smtp.getMappedPort(1025);





API


MailHog expose ses services en API Web.
Les principales opérations sont les suivantes :




Lire tous les messages : GET /api/v2/messages


Supprimer tous les messages : DELETE /api/v1/messages


Lire un message : GET /api/v1/messages/{id}


Supprimer un message : DELETE /api/v1/messages/{id}




Il y a un piège dans l&#8217;API.
Elle produit du contenu au format JSON, pour lequel on attend habituellement un type Mime "application/json".
Or MailHog déclare un header "text/json", qui n&#8217;est pas compris nativement par tous les clients.


Par exemple, avec RestTemplate de Spring Framework, il faut déclarer ce type spécifiquement.



    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setSupportedMediaTypes(List.of(new MediaType("text", "json")));

    RestTemplate restTemplate = new RestTemplate(new SimpleClientHttpRequestFactory());
    restTemplate.setMessageConverters(List.of(converter));





Avancé


Stockage des messages

Par défaut, les messages sont stockés en mémoire uniquement.
MailHog peut être paramétré pour rendre les messages persistants, grâce au paramètre -storage ou à la variable d&#8217;environnement MH_STORAGE.



Stockage en fichiers


docker run --name mailhog --rm --detach                                   \
           --publish 127.0.0.1:2025:1025 --publish 127.0.0.1:8025:8025    \
           mailhog/mailhog -storage=maildir -maildir-path=/var/mail



Le stockage en répertoire ne fonctionne pas avec l&#8217;image druidfi/mailhog car elle est construite from scratch.



Stockage MongoDB 4.x (docker)


docker run --network mail --name mh-mongo                \
           --env MONGO_INITDB_ROOT_USERNAME=mongoadmin   \
           --env MONGO_INITDB_ROOT_PASSWORD=secret       \
           --rm --detach mongo:4

docker run --network mail --name mailhog --rm --detach                    \
           --publish 127.0.0.1:2025:1025 --publish 127.0.0.1:8025:8025    \
           mailhog/mailhog -storage=mongodb -mongo-uri=mh-mongo



Ça ne marche pas avec un MongoDB version 5 avec authentification.



Stockage MongoDB 4.x (clever cloud)


# Information à récupérer dans la console de l'add-on
username=kfif5tflgdf4lskfme23
password=lhy5cGkEsVB65r8gt6r
db=bpa2demglxd23se

host=$db-mongodb.services.clever-cloud.com
docker run --name mailhog --rm --detach                                          \
           --publish 127.0.0.1:2025:1025 --publish 127.0.0.1:8025:8025           \
           mailhog/mailhog -storage=mongodb                                      \
                           -mongo-uri=$username:$password@$host/$db -mongo-db=$db



J&#8217;ai testé avec une base MongoDB de Dev (gratuite), et ça fonctionne sans problème.
C&#8217;est un vieux add-on, en version 4.0.3.


Il semble que MailHog ne sache pas utiliser MongoDB Atlas, mais je ne sais pas pourquoi.
En tout cas, j&#8217;ai testé avec un cluster gratuit, en version 5.0, et ça ne fonctionne pas.
L&#8217;erreur rencontrée est Error connecting to MongoDB: no reachable servers.



Envoi réel des e-mails

L&#8217;introduction que MailHog ne transmettait jamais les messages aux destinataires.
C&#8217;est vrai dans la configuration par défaut, mais ça peut aussi être modifié pour qu&#8217;il fonctionne en relais, grâce au paramètre -outgoing-smtp ou à la variable d&#8217;environnement MH_OUTGOING_SMTP.


La première étape est de définir un fichier de configuration en JSON.



{
    "ovh": {
        "name": "ovh",
        "host": "smtp.ovh.net",
        "port": "25",
        "email": "noreply@jtips.info",
        "username": "mail@jtips.info",
        "password": "fjzlgre36vqz",
        "mechanism": "PLAIN"
    }
}









la valeur de "name" doit être identique à la clé ("ovh").





Puis on rend le fichier disponible depuis le conteneur, via un volume.



docker run --name mailhog --rm --detach                                          \
           --publish 127.0.0.1:2025:1025 --publish 127.0.0.1:8025:8025           \
           --volume $(pwd)/outgoing.json:/conf/outgoing.json \
           druidfi/mailhog -outgoing-smtp=/conf/outgoing.json



Ensuite, on peut libérer (release) les messages depuis l&#8217;interface graphique.
L&#8217;API peut aussi être utilisée avec un POST sur /api/v1/messages/{message-id}}/release, en passant le nom du serveur SMTP à utiliser {"name":"ovh"}.



id=$(curl -s http://localhost:8025/api/v2/messages | jq --raw-output .items[0].ID)
curl http://localhost:8025/api/v1/messages/$id/release -X POST --data '{"name": "ovh"}'




</description>
          <pubDate>2021-11-23T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Mail/MailHog</link>
          <guid isPermaLink="true">https://www.jtips.info/Mail/MailHog</guid>
        </item>
      
    
      
        <item>
          <title>FakeSMTP</title>
          <description>
FakeSMTP est un serveur SMTP.
On peut lui envoyer des e-mails mais il ne les transmet jamais aux destinataires.
Il les stocke dans un répertoire local sous forme de fichiers .eml.


On l&#8217;utilise généralement en environnement de développement ou en test d&#8217;intégration.


Docker


Il existe plusieurs logiciels qui répondent au nom de FakeSMTP.
Celui que j&#8217;utilise est développé par Gautier Mechling, en Java.


Le développement a commencé en 2011, jusqu&#8217;en 2015.
Depuis, la maintenance est ralentie.
Mais je ne pense pas que ça pose de problème.
Il tourne bien sur un JDK 11, preuve qu&#8217;il vieillit bien.


Image Docker

Avant j&#8217;utilisais l&#8217;image munkyboy/fakesmtp, mais elle fonctionne avec un JDK 7 et n&#8217;a jamais été mise à jour.
Depuis, je suis passé sur ghusta/fakesmtp dont la dernière version tourne sur un JDK 11.



docker run --publish 127.0.0.1:2025:25 --detach ghusta/fakesmtp









Le conteneur fonctionne en root, mais je n&#8217;ai pas trouvé d&#8217;équivalent avec un user.






Volume

Pour accéder aux fichiers .eml des messages, il faut monter un volume en répertoire local.



docker run --publish 127.0.0.1:2025:25 --volume /tmp/smtp:/var/mail --detach ghusta/fakesmtp




Docker Compose


  smtp:
    image: ghusta/fakesmtp
    ports:
      - "127.0.0.1:2025:25"
    volumes:
      - "/tmp/smtp:/var/mail"






Dans les tests


Ça peut aussi se démarrer avec Testcontainer pour des tests d&#8217;intégration automatisés.



  GenericContainer&lt;?&gt; smtp = new GenericContainer&lt;&gt;("ghusta/fakesmtp")
                                        .withExposedPorts(25);
  smtp.start();

  int smtpPort = smtp.getMappedPort(25);





Lecture des messages


Pour lire les fichiers eml, on peut utiliser l&#8217;API blank">Jakarta Mail, anciennement _Java Mail.
La principipale classe est MimeMessage, elle permet d&#8217;accéder aux données et métadonnées d&#8217;un message.


Pour avoir un tel objet, il faut ouvrir un IntputStream sur le fichier .eml et le passer en paramètre du contructeur.



    InputStream is = Files.newInputStream(emlPath);
    Session session = Session.getDefaultInstance(new Properties());
    MimeMessage message = new MimeMessage(session, is);
    // ...





MimeMessage



parse()


getFrom(): Address[]


getReplyTo(): Address[]


getRecipients(Message.RecipientType type): Address[]


getSubject(): String


getContent(): Object







L&#8217;API standard n&#8217;étant pas très amicale, je préfère encapsuler l&#8217;objet dans un EmailWrapper :




plutôt que de retourner des Address, je manipule des String,


les méthodes sont susceptibles de lever des MessagingException qui sont checked,


le contenu peut être multipart qui doit être décortiqué.




Apache Commons Mail a une classe MimeMessageParser qui facilite aussi la manipulation de MimeMessage.
Cette classe a quand même un gros défaut, ses méthodes sont susceptibles de lever une Exception, sans autre précision.




MimeMessageParser



parse()


getFrom(): String


getReplyTo(): String


getTo(): List&lt;Address&gt;


getCc(): List&lt;Address&gt;


getBcc(): List&lt;Address&gt;


getSubject(): String


getHtmlContent(): String


getPlainContent(): String







Spring Framework a une classe MimeMessageHelper qui facilite aussi la manipulation de MimeMessage, mais qui est beaucoup plus orientée sur l&#8217;écriture d&#8217;un message que sur sa lecture.


</description>
          <pubDate>2021-11-07T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Mail/FakeSMTP</link>
          <guid isPermaLink="true">https://www.jtips.info/Mail/FakeSMTP</guid>
        </item>
      
    
      
        <item>
          <title>Sauvegardes avec MS SqlServer</title>
          <description>
SqlServer avec Docker





Dump





Restauration


Pour demander le contenu



RESTORE FILELISTONLY
    FROM DISK = '/var/opt/mssql/backup/mydb.bak'




RESTORE DATABASE MyDB
    FROM DISK = '/var/opt/mssql/backup/mydb.bak'
    WITH PARTIAL,
    MOVE 'MyDB' TO '/var/opt/mssql/data/MyDB.mdf',
    MOVE 'MyDB_log' TO '/var/opt/mssql/data/MyDB_log.ldf'



Gérer l&#8217;accès à la base de données



CREATE LOGIN rtower WITH PASSWORD = 'rqMep4TebukMPH#Q', DEFAULT_DATABASE = Monitoring;
GRANT CONNECT SQL TO rtower
USE Monitoring
DROP USER rtower
CREATE USER rtower FOR LOGIN rtower
GRANT CONNECT ON DATABASE::Monitoring TO rtower
GRANT SELECT, EXECUTE on SCHEMA::dbo TO rtower





Restauration Partielle


Dans la backup que je devais restaurer, il y avait un fichier FILESTREAM :



RESTORE DATABASE MyDB
    FILEGROUP = 'PRIMARY'
    FROM DISK = '/var/opt/mssql/backup/mydb.bak'
    WITH PARTIAL,
    MOVE 'MyDB' TO '/var/opt/mssql/data/MyDB.mdf',
    MOVE 'MyDB_log' TO '/var/opt/mssql/data/MyDB_log.ldf'



&#8658; erreur à ignorer


</description>
          <pubDate>2021-07-02T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/SqlServer/backup</link>
          <guid isPermaLink="true">https://www.jtips.info/SqlServer/backup</guid>
        </item>
      
    
      
        <item>
          <title>Liste des timezones en Javascript</title>
          <description>
En Java


OK, c&#8217;est hors sujet, mais si on admet pouvoir gérer cette liste coté backend, en java, c&#8217;est assez simple :



Set&lt;String&gt; timezoneIds = ZoneId.getAvailableZoneIds();



Au moment de la rédaction de la page, l&#8217;ensemble est constitué de 600 zones.


Les ids sont les codes IANA (UTC, Europe/Paris, Antarctica/Troll,&#8230;&#8203;).




Avec Moment.JS


Il faut ajouter le modules moment-timezone.



timezoneIds = moment.tz.names();



Comme pour la variante Java, on récupère une liste de codes IANA, au nombre de 593.
En réalité, il y a 19 différences entre les deux listes.


Cette solution fonctionne bien, mais elle a 2 défauts :




Moment.JS est en fin de vie.


La définition complète des fuseaux horaires aloudri beaucoup l&#8217;application.






Pur JavaScript


La liste des fuseaux horaires peut varier d&#8217;une implémentation à l&#8217;autre.
Le seul fuseau qui doit obligatoirement être supporté est UTC.


Généralement, les codes de la base IANA sont supportés.
Malheureusement, il n&#8217;y a pas d&#8217;API standard pour récupérer une telle liste.


</description>
          <pubDate>2021-05-25T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JavaScript/Timezone</link>
          <guid isPermaLink="true">https://www.jtips.info/JavaScript/Timezone</guid>
        </item>
      
    
      
        <item>
          <title>Internationalisation en JavaScript</title>
          <description>
Seules les API standards de JavaScript sont abordées dans cette page.


Locale du navigateur


La locale préférée :



const preferredLocale = navigator.language;



La liste des locales configurées (expérimental, mais largement supporté) :



const preferredLocales = navigator.languages;
if (preferredLocales) {
    const preferredLocale = preferredLocales[0];
}









Ne marche pas avec IE, mais on s&#8217;en fout.







Dates


Le type principal pour manipuler des dates et heures est Date.


Cet objet est universel.
Au moment de sa convertion en texte, la locale et le fuseau horaire interviennent.


Pour la conversion en texte, on utilise la classe DateTimeFormat.



let date = new Date(Date.UTC(2000, 11, 31));
let locale = 'fr-FR';
let format = { year: 'numeric', month: '2-digit', day: '2-digit' };

let textContent = new Intl.DateTimeFormat(locale, format).format(date);   // 31/12/2000




let date = new Date(Date.UTC(2000, 11, 31));
let locale = 'en';
let format = { dateStyle: 'medium' };

let textContent = new Intl.DateTimeFormat(locale, format).format(date);   // Dec 31, 2000





Durées


Pour la mise en forme des durées, on utilise la classe RelativeTimeFormat.




Nombres


Pour la conversion de nombres en texte, on utilise la classe NumberFormat.



let number = 1234.567;
let locale = 'fr';

let textContent = new Intl.NumberFormat(locale).format(number);      // 1 234,567




let number = 1234.567;
let locale = 'en';
let format ={ style: 'currency', currency: 'EUR' };

let textContent = new Intl.NumberFormat(locale, format).format(number);    // €1,234.57





Listes


Pour la mise en forme des énumérations, on utilise la classe ListFormat.



let list = ['aaa', 'bbb', 'ccc'];;
let locale = 'en';
let format = { type: 'disjunction' };

let textContent = new Intl.ListFormat(locale, format).format(number);    // aaa, bbb, or ccc





Pluriels


Pour les formes plurielles, on utilise la classe PluralRules.
Ses cas d&#8217;usage sont moins évidents.



let locale = 'en';

let one = new Intl.PluralRules(locale).select(1);    // one
let one = new Intl.PluralRules(locale).select(2);    // other





Exemples




https://jsfiddle.net/sewatech/62zdasLx/




</description>
          <pubDate>2021-05-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JavaScript/Intl</link>
          <guid isPermaLink="true">https://www.jtips.info/JavaScript/Intl</guid>
        </item>
      
    
      
        <item>
          <title>Keycloak et certificats</title>
          <description>
Le but : démarrer Keycloak avec Docker, pour permettre l&#8217;authentification par certificat.


Pour ça, il faut passer par le port TLS et initialiser les certificats.


Générer les certificats



BASE=pwd

rm -rf $BASE/keycloak-tls
rm -rf $BASE/certs

mkdir -p $BASE/certs

cd certs

GREEN='\033[0;32m'
NC='\033[0m'

printf "${GREEN} Generating keycloak CA for https ${NC}\n"
openssl genrsa -out $BASE/certs/ca-keycloak.key 2048
openssl rsa -in $BASE/certs/ca-keycloak.key -out $BASE/certs/ca-keycloak.key
openssl req -new -x509 -sha256 -days 365 -key $BASE/certs/ca-keycloak.key -out $BASE/certs/ca-keycloak.cer -subj "/CN=keycloak-ca/O=sewatech"

printf "${GREEN} Generating keycloak CA for mtls ${NC}\n"
openssl genrsa -out $BASE/certs/ca-client.key 2048
openssl rsa -in $BASE/certs/ca-client.key -out $BASE/certs/ca-client.key
openssl req -new -x509 -sha256 -days 365 -key $BASE/certs/ca-client.key -out $BASE/certs/ca-client.cer -subj "/CN=client-ca/O=sewatech"

printf "${GREEN} Generating keycloak certificate https ${NC}\n"
openssl genrsa -out $BASE/certs/keycloak-server.key 2048
openssl rsa -in $BASE/certs/keycloak-server.key -out $BASE/certs/keycloak-server.key
openssl req -new -key $BASE/certs/keycloak-server.key -sha256 -out $BASE/certs/keycloak-server.csr -subj "/CN=localhost/O=sewatech"
openssl x509 -req -days 365 -sha256 -in $BASE/certs/keycloak-server.csr -CA $BASE/certs/ca-keycloak.cer -CAkey $BASE/certs/ca-keycloak.key \
             -set_serial 1 -out $BASE/certs/keycloak-server.cer

printf "${GREEN} Generating keycloak certificate for mtls ${NC}\n"
openssl genrsa -out $BASE/certs/sewatech-client.key 2048
openssl rsa -in $BASE/certs/sewatech-client.key -out $BASE/certs/sewatech-client.key
openssl req -new -key $BASE/certs/sewatech-client.key -out $BASE/certs/sewatech-client.csr -subj "/CN=swx509user/O=sewatech"
openssl x509 -req -days 365 -sha256 -in $BASE/certs/sewatech-client.csr -CA $BASE/certs/ca-client.cer -CAkey $BASE/certs/ca-client.key \
             -set_serial 2 -out $BASE/certs/sewatech-client.cer

cd $BASE

mkdir $BASE/keycloak-tls

printf "${GREEN} Building https bundles ${NC}\n"
cp $BASE/certs/keycloak-server.cer $BASE/keycloak-tls/tls.crt
cp $BASE/certs/keycloak-server.key $BASE/keycloak-tls/tls.key
echo "" &gt;&gt; $BASE/keycloak-tls/tls.crt
cat $BASE/certs/ca-keycloak.cer &gt;&gt; $BASE/keycloak-tls/tls.crt

printf "${GREEN} Building mtls server side bundles ${NC}\n"
cp $BASE/certs/ca-client.key $BASE/keycloak-tls/ca-client.bundle
echo "" &gt;&gt; $BASE/keycloak-tls/ca-client.bundle
cat $BASE/certs/ca-client.cer &gt;&gt; $BASE/keycloak-tls/ca-client.bundle

cd $BASE

printf "${GREEN} Building mtls client side bundles ${NC}\n"
cp $BASE/certs/keycloak-server.cer $BASE/keycloak-tls/server.pem
echo "" &gt;&gt; $BASE/keycloak-tls/server.pem
cat $BASE/certs/ca-keycloak.cer &gt;&gt; $BASE/keycloak-tls/server.pem

cp $BASE/certs/sewatech-client.cer $BASE/keycloak-tls/client.pem
echo "" &gt;&gt; $BASE/keycloak-tls/client.pem
cat $BASE/certs/ca-client.cer &gt;&gt; $BASE/keycloak-tls/client.pem

openssl pkcs12 -export -clcerts -in $BASE/keycloak-tls/client.pem -inkey $BASE/certs/sewatech-client.key \
               -out $BASE/keycloak-tls/client.p12 -password pass:sewatech

cd $BASE





Démarrer Keycloak



docker run --publish 8883:8443 \
           --env KEYCLOAK_USER=admin --env KEYCLOAK_PASSWORD=admin \
           --env X509_CA_BUNDLE=/etc/x509/https/ca-client.bundle \
           --volume $(pwd)/keycloak-tls:/etc/x509/https \
           --name kc-example --rm \
           --detach jboss/keycloak:12.0.4





Références


Ceci devrait permettre de faire fonctionner les scripts d&#8217;administration REST.


</description>
          <pubDate>2021-03-30T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Keycloak/tls</link>
          <guid isPermaLink="true">https://www.jtips.info/Keycloak/tls</guid>
        </item>
      
    
      
        <item>
          <title>Spring Security avec OAuth 2 et OpenID Connect</title>
          <description>
Application Web


Pour une application à l&#8217;ancienne, on utilise le flux Authorization Code Grant,
qui implique un User Agent, un Client et l'Authorization Server.


Dépendances


&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-oauth2-client&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
&lt;/dependency&gt;




WebSecurityConfigurerAdapter


@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/echo/**").authenticated()
                .antMatchers("/").permitAll()
            .and()
                .oauth2Login();
    }

}




Configuration YAML

4 providers prédéfinis




Google


Github


Facebook


Okta




Provider maison (exemple avec Keycloak)



spring:
  security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: example-client
            client-secret: example-secret
            authorization-grant-type: authorization_code
            redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
            client-authentication-method: basic
            client-name: Keycloak
            scope:
              - openid
              - profile
              - email
              - address
              - phone
        provider:
          keycloak:
            authorization-uri: http://localhost:8888/auth/realms/example-realm/protocol/openid-connect/auth
            token-uri: http://localhost:8888/auth/realms/example-realm/protocol/openid-connect/token
            jwk-set-uri: http://localhost:8888/auth/realms/example-realm/protocol/openid-connect/certs




Configuration par code

Un peu plus de code, et un peu moins de YAML, en remplacement de gros application.yml précédent.



example:
  keycloak:
    client-id: example-client
    client-secret: example-secret
    base-url: http://localhost:8888
    realm: example-realm




@Configuration
public class KeycloakConfig {

    @Value("${example.keycloak.client-id}")
    private String clientId;

    @Value("${example.keycloak.client-secret}")
    private String clientSecret;

    @Value("${example.keycloak.base-url}")
    private String baseUrl;

    @Value("${example.keycloak.realm}")
    private String realm;

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(this.keycloakClientRegistration());
    }

    private ClientRegistration keycloakClientRegistration() {
        return ClientRegistration.withRegistrationId("keycloak")
                .clientId(clientId)
                .clientSecret(clientSecret)
                .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
                .scope("openid", "profile", "email", "address", "phone")
                .clientName("Keycloak")
                .authorizationUri(baseUrl + "/auth/realms/" + realm + "/protocol/openid-connect/auth")
                .tokenUri(baseUrl + "/auth/realms/" + realm + "/protocol/openid-connect/token")
                .jwkSetUri(baseUrl + "/auth/realms/" + realm + "/protocol/openid-connect/certs")
                .build();
    }

}




Username


  private String getUsername() {
      Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
      return ((DefaultOidcUser)principal).getPreferredUsername();
  }






Application SPA


Pour une application plus moderne en Single Page Application, le serveur Spring joue le rôle de Resource Server.


Dépendances


&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-oauth2-resource-server&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
&lt;/dependency&gt;




WebSecurityConfigurerAdapter


@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/echo/**").authenticated()
                .antMatchers("/").permitAll()
            .and()
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    }

}




Configuration YAML


spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8888/auth/realms/example-realm




Username


  private String getUsername() {
      Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
      return ((Jwt)principal).getClaimAsString("preferred_username");
  }






Tests


Cette configuration peut être testée avec Keycloak et Postman.


</description>
          <pubDate>2021-03-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/security-oauth2</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/security-oauth2</guid>
        </item>
      
    
      
        <item>
          <title>OAuth 2 avec Postman</title>
          <description>
Authentification avec un mot de passe


Requête POST sur https://&lt;host&gt;:&lt;port&gt;/auth/realms/&lt;realm&gt;/protocol/openid-connect/token avec un body x-www-form-urlencoded :



username:swuser
password:swpwd
grant_type:password
client_id:example-client
client_secret:example-secret





Authentification avec un certificat


Requête POST sur https://&lt;host&gt;:&lt;port&gt;/auth/realms/&lt;realm&gt;/protocol/openid-connect/token avec un body x-www-form-urlencoded :



grant_type:password
client_id:example-client
client_secret:example-secret



Et configuration du certificat dans settings, onglet Certificates.
Associer &lt;host&gt;:&lt;port&gt; avec un certificat p12 ou crt/key.




Enregistrer le token


Pour utiliser le token automatiquement dans les requêtes ultérieures, dans l&#8217;onglet Tests :



if (pm.response.code &lt; 300) {
  pm.collectionVariables.set("access_token", pm.response.json().access_token);
  pm.collectionVariables.set("refresh_token", pm.response.json().refresh_token);
}





Utiliser le token


Dans l&#8217;onglet Authorization, sélectionner Bearer Token avec la valeur {{access_token}}.




Rafraichir le token


Requête POST sur https://&lt;host&gt;:&lt;port&gt;/auth/realms/&lt;realm&gt;/protocol/openid-connect/token avec un body x-www-form-urlencoded :



refresh_token:{{refresh_token}}
grant_type:refresh_token
client_id:example-client
client_secret:example-secret



</description>
          <pubDate>2021-03-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Postman/oauth2</link>
          <guid isPermaLink="true">https://www.jtips.info/Postman/oauth2</guid>
        </item>
      
    
      
        <item>
          <title>Keycloak, administration REST</title>
          <description>
Prérequis


Démarrer Keycloak (testé avec la version 20)



docker run --publish 8888:8080 \
           --env KEYCLOAK_ADMIN=admin --env KEYCLOAK_ADMIN_PASSWORD=admin \
           --name kc-example --rm \
           --detach keycloak/keycloak:20.0 start-dev





Initialisation


Initialisation d&#8217;un Realm avec un Client et un User.



base_url=http://localhost:8888

# Authenticate and get token
token=$(curl -s -d "client_id=admin-cli" -d "username=admin" -d "password=admin" -d "grant_type=password" \
             "$base_url/realms/master/protocol/openid-connect/token" \
          | jq -r .access_token)

# Create realm
curl -H "Authorization: bearer $token" -H "Content-Type: application/json" \
     -d '{"realm":"sewatech", "enabled":true}' \
     $base_url/admin/realms

# Create private client (with secret)
curl -H "Authorization: bearer $token" -H "Content-Type: application/json"  \
     -d '{"clientId":"private-jtips", "enabled":true, "standardFlowEnabled":true,  \
          "directAccessGrantsEnabled":true, "rootUrl":"http://localhost:4200",  \
          "redirectUris":["http://localhost:4200/*"], "secret":"example-secret"}'  \
     $base_url/admin/realms/sewatech/clients
# Create public client (without secret)
curl -H "Authorization: bearer $token" -H "Content-Type: application/json"  \
     -d '{"clientId":"public-jtips", "enabled":true, "standardFlowEnabled":true,  \
          "directAccessGrantsEnabled":true, "rootUrl":"http://localhost:4200",  \
          "redirectUris":["http://localhost:4200/*"], "publicClient":true}'  \
     $base_url/admin/realms/sewatech/clients

# Create user with password
curl -H "Authorization: bearer $token" -H "Content-Type: application/json"  \
     -d '{"username":"jtips", "enabled":true,  \
          "credentials": [{"type":"password","value":"jtipspwd","temporary":false}]}'  \
          $base_url/admin/realms/sewatech/users





Identifiants


Pour retrouver l&#8217;id du client:



clientId=$(curl -s -H "Authorization: bearer $token" -H "Content-Type: application/json" \
                $base_url/admin/realms/sewatech/clients?clientId=public-jtips \
           | jq -r .[0].id)





Direct flow


Le direct flow est activé par défaut.
On peut l&#8217;utiliser dans Postman ou en ligne de commande.








curl -d "client_id=public-jtips" -d "grant_type=password" \
     -d "username=jtips" -d "password=jtipspwd" \
     $base_url/realms/sewatech/protocol/openid-connect/token



Ce flow peut être désactivé au niveau du client.



client=$(curl -s -H "Authorization: bearer $token"  $base_url/admin/realms/sewatech/clients/$clientId  \
          | jq -r '.directAccessGrantsEnabled=false')
curl -s -H "Authorization: bearer $token" -H "Content-Type: application/json" -X PUT  \
     -d "$client" $base_url/admin/realms/sewatech/clients/$clientId





X.509 direct flow


Configuration du direct flow (utilisé depuis postman) pour authentification par un certificat X.509.


Attention, il y a des prérequis sur les certificats et sur le démarrage de keycloak.



# base_url et token sont initialisé comme ci-dessus

# X509 direct flow
curl --silent --header "Authorization: bearer $token" --header "Content-Type: application/json" \
     --data '{"alias":"x509 direct grant", "providerId": "basic-flow", "topLevel": true, \
              "authenticationExecutions": [{    \
              "authenticator\": "direct-grant-auth-x509-username",  \
              "requirement": "REQUIRED",  \
              "priority": 0, \
              "userSetupAllowed": false, \
              "autheticatorFlow": false \
              }]}'    \
     $base_url/admin/realms/sewatech/authentication/flows

curl --silent --header "Authorization: bearer $token" --header "Content-Type: application/json" \
      --data '{"provider":"direct-grant-auth-x509-username"}' \
     $base_url/admin/realms/sewatech/authentication/flows/x509%20direct%20grant/executions/execution

curl -X PUT --silent --header "Authorization: bearer $token" --header "Content-Type: application/json" \
     --data '{"directGrantFlow": "x509 direct grant"}' \
     $base_url/admin/realms/sewatech

execution_id=$(curl --silent --header "Authorization: bearer $token" --header "Content-Type: application/json" \
                    $base_url/admin/realms/sewatech/authentication/flows/x509%20direct%20grant/executions \
               | jq -r .[0].id)

...



SubjectDN

L&#8217;attribut CN du SubjectDN sert de username.



...

curl --silent --header "Authorization: bearer $token" --header "Content-Type: application/json" \
     --data '{"config": {"x509-cert-auth.mapping-source-selection": "Subject's Common Name", \
                         "x509-cert-auth.canonical-dn-enabled": "true", \
                         "x509-cert-auth.user-attribute-name": "usercertificate", \
                         "x509-cert-auth.confirmation-page-disallowed": true}, \
                         "alias":"X509"}' \
     $base_url/admin/realms/sewatech/authentication/executions/$execution_id/config




Serial et IssuerDN

Dans cet exemple, la correspondance entre le certificat et l&#8217;utilisateur est faite par le n° de série et le DN de l&#8217;émetteur.



...

curl --silent --header "Authorization: bearer $token" --header "Content-Type: application/json"  \
     --data '{"config": {"x509-cert-auth.mapping-source-selection":"Certificate Serial Number and IssuerDN", \
                         "x509-cert-auth.mapper-selection":"Custom Attribute Mapper",
                         "x509-cert-auth.mapper-selection.user-attribute-name":"SerialNumber##IssuerDN", \
                         "x509-cert-auth.canonical-dn-enabled":true, \
                         "x509-cert-auth.user-attribute-name":"usercertificate", \
                         "x509-cert-auth.confirmation-page-disallowed":true}, \
              "alias":"X509serial"}' \
     $base_url/admin/realms/sewatech/authentication/executions/$execution_id/config



Pour être trouvé, l&#8217;utilisateur doit avoir par 2 attributs personnalisés : SerialNumber et IssuerDN.



# Create user with Serial and IssuerDN
curl --silent --header "Authorization: bearer $token" --header "Content-Type: application/json"  \
     --data '{"username":"swserialuser", "enabled":true, \
              "attributes":{"SerialNumber":["2"],"IssuerDN":["o=sewatech,cn=client-ca"]}}'  \
     $base_url/admin/realms/sewatech/users



L&#8217;IssuerDN est en minuscules parce qu&#8217;on a activé le format canonical.





Références




Keycloak Admin REST API


Create a Keycloak Realm Using Admin REST API


Resetting password of a keycloak user using Rest Service




</description>
          <pubDate>2021-03-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Keycloak/rest-admin</link>
          <guid isPermaLink="true">https://www.jtips.info/Keycloak/rest-admin</guid>
        </item>
      
    
      
        <item>
          <title>JavaSE 7 - opérateur en diamant</title>
          <description>
Cette nouvelle notation permet d&#8217;alléger le code lorsqu&#8217;on instancie une classe avec generic. Le cas classique est celui des collections :



       List&lt;MyClass&gt; maListeOld = new ArrayList&lt;MyClass&gt;();



Le type contenu dans la liste est répété entre la déclaration et l&#8217;instanciation. L&#8217;opérateur en diamant évite cette redondance :



       List&lt;MyClass&gt; maListeNew = new ArrayList&lt;&gt;();



Les esprits chagrins prétendent que ça ne sert à rien puisque leur IDE préféré leur évite de réécrire le contenu. Mon avis est que c&#8217;est encore un cas où l&#8217;IDE servait à combler une lacune du langage, comme c&#8217;est souvent le cas.


Cette évolution est légère mais sera très souvent utile.
</description>
          <pubDate>2019-03-05T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java7/Diamant</link>
          <guid isPermaLink="true">https://www.jtips.info/Java7/Diamant</guid>
        </item>
      
    
      
        <item>
          <title>HTTP/2 avec Node.js</title>
          <description>
Le support d&#8217;HTTP/2 a été introduit avec Node.js 8.x.
Je ne sais exactement depuis quelle version mineure, en tout cas avec Node.js 8.6, il est présent et marqué comme expérimental.


Le module http2 devrait être stable pour Node.js 9.


Pour les versions précédentes, le support d&#8217;HTTP/2 peut exister grâce à des modules tiers.


HTTP/2 par ALPN



const http2 = require('http2');
const fs = require('fs');

const server = http2
  .createSecureServer(
    {
      key: fs.readFileSync('resources/node.key'),
      cert: fs.readFileSync('resources/node.crt')
    },
    (request, response) =&gt; {
      response.end('Hello World');
    }
  )
  .listen(8002);





HTTP/2 avec Express.js


En théorie, ça devrait fonctionner avec le bout de code ci-dessous.



const http2 = require('http2');
const fs = require('fs');




const app = express();
app.get('*', (req, res) =&gt; {
  res
    .status(200)
    .json({message: 'Hello'})
});

const server = http2
  .createSecureServer(
    {
      key: fs.readFileSync('resources/node.key'),
      cert: fs.readFileSync('resources/node.crt')
    },
    app
  )
  .listen(8002);



Mais, avec Node.js 8. et Express.js 4.16, ça ne fonctionne pas, il y a une erreur à la première requête.


Il semble qu&#8217;il faille attendre la version 5 d&#8217;Express.js.
C&#8217;est un échec avec Express.js 5.0.0-alpha.6


|}


</description>
          <pubDate>2017-10-01T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Node/http2</link>
          <guid isPermaLink="true">https://www.jtips.info/Node/http2</guid>
        </item>
      
    
      
        <item>
          <title>HTTP/2 avec Vert.x</title>
          <description>
Support d&#8217;ALPN


Pour Vert.X 3.3 et 3.4




Avec l&#8217;extension ALPN de Jetty (en Java 8)


Avec OpenSSL






Code


Pour activer HTTP/2, il faut d&#8217;abord un serveur HTTP avec TLS, sur lequel on active ALPN.



HttpServerOptions h2Options = new HttpServerOptions()
               .setSsl(true)
               .setKeyStoreOptions(
                       new JksOptions()
                               .setValue(Buffer.buffer(getKeyStore()))
                               .setPassword(KEYSTORE_PASSWORD))
               .setUseAlpn(true);





OpenSSL


Le code ci-dessus produit une erreur si on utilise JSSE, l&#8217;implémentation Java de TLS.
Par contre, ça fonctionne si on passe en OpenSSL.
Et pour ça, il faut ajouter l&#8217;extension BoringSSL de Netty.



&lt;dependency&gt;
    &lt;groupId&gt;io.netty&lt;/groupId&gt;
    &lt;artifactId&gt;netty-tcnative-boringssl-static&lt;/artifactId&gt;
    &lt;version&gt;1.1.33.Fork26&lt;/version&gt;
    &lt;scope&gt;runtime&lt;/scope&gt;
&lt;/dependency&gt;



Remarque : ça fonctionne avec Vert.x 3.4 (donc Netty 4.1) + Netty tcnative 1.1, par contre, ça ne marche pas avec Netty tcnative 2.0.




JSSE


Comme pour Tomcat, HTTP/2 peut fonctionner en JSSE avec Java 8 à condition d&#8217;ajouter l&#8217;extension ALPN de Jetty au bootclasspath.



java -Xbootclasspath/p:alpn-boot-8.1.11.v20170118.jar ...



</description>
          <pubDate>2017-09-30T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Vertx/http2</link>
          <guid isPermaLink="true">https://www.jtips.info/Vertx/http2</guid>
        </item>
      
    
      
        <item>
          <title>HTTP/2 avec WildFly</title>
          <description>
La possibilité d&#8217;activer HTTP/2 a été ajoutée dans la version 9 de WildFly.


Depuis WildFly 10.1


A partir de WildFly 10.1, il n&#8217;est plus nécessaire d&#8217;ajouter l&#8217;extension ALPN, même avec un JDK 8.
Il suffit d&#8217;activer HTTP/2, ce qui est fait par défaut.



[host:9990 /] /subsystem=undertow/server=default-server/https-listener=https        \
                    :write-attribute(name=enable-http2, value=true)





JDK 8, WildFly 9 ou 10.0


Tout d&#8217;abord, pour un JDK 8, il faut ajouter l&#8217;extension ALPN de Jetty dans le bootclasspath, en ajouter la ligne ci-dessous dans bin/standalone.conf.



JAVA_OPTS="$JAVA_OPTS -Xbootclasspath/p:alpn-boot.jar"



Ensuite, il faut ajouter un listener HTTPS.


Enfin, on active HTTP/2 sur ce listener.



[host:9990 /] /subsystem=undertow/server=default-server/https-listener=https        \
                    :write-attribute(name=enable-http2, value=true)





Versions d&#8217;Undertow


Pour connaitre les détails de la marche à suivre, il faut noter la version d&#8217;Undertow embarqué.




WildFly  8.2 &#8658; Undertow 1.1.8 (pas de HTTP/2)


WildFly  9.0 &#8658; Undertow 1.2.9 (HTTP/2 avec l&#8217;extension ALPN)


WildFly 10.0 &#8658; Undertow 1.3.15 (HTTP/2 avec l&#8217;extension ALPN)


WildFly 10.1 &#8658; Undertow 1.4.0 (HTTP/2 &lt;b&gt;sans&lt;/b&gt; l&#8217;extension ALPN)


WildFly 11.0 &#8658; Undertow 1.4.xx (HTTP/2 &lt;b&gt;sans&lt;/b&gt; l&#8217;extension ALPN)




</description>
          <pubDate>2017-09-29T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WildFly/HTTP2</link>
          <guid isPermaLink="true">https://www.jtips.info/WildFly/HTTP2</guid>
        </item>
      
    
      
        <item>
          <title>HTTP/2 avec Apache httpd</title>
          <description>


Activer le module mod_http2





LoadModule http2_module modules/mod_http2.so





Activer TLS / SSL


Activer le potocole HTTP/2





Protocols h2 http/1.1



Cette directive peut être placée au niveau global ou dans un virtual host SSL.


HTTP 2 est disponible depuis Apache 2.4.17.
</description>
          <pubDate>2017-09-29T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Apache/HTTP2</link>
          <guid isPermaLink="true">https://www.jtips.info/Apache/HTTP2</guid>
        </item>
      
    
      
        <item>
          <title>HTTP/2 avec Undertow</title>
          <description>
Undertow est un serveur Web Java avec lequel il est facile de démarrer, même avec SSL.


Il est tout aussi simple d&#8217;activer HTTP/2.
Du moins dans son mode le plus classique, avec ALPN.


HTTP/2 avec ALPN


Le prérequis d&#8217;ALPN, c&#8217;est d&#8217;avoir du TLS 1.2.


Ensuite, il suffit d&#8217;ajouter l&#8217;option ENABLE_HTTP2.



       Undertow http2Server = Undertow.builder()
               .addHttpsListener(8002, "localhost", buildSslContext())
               .setServerOption(UndertowOptions.ENABLE_HTTP2, true)
               .setHandler(Start::hello)
               .build();
       http2Server.start();



Jusqu&#8217;à Undertow 1.3.x, avec Java 8, il fallait l&#8217;extension ALPN de Jetty pour que HTTP/2 fonctionne.
Sans cela, une exception était lancée à l&#8217;initialisation du listener.


Depuis Undertow 1.4, la négociation ALPN a été redéveloppée et l&#8217;extension n&#8217;est plus nécessaire.
On peut faire du HTTP/2 avec Java 8, sans modifier le bootclasspath.


</description>
          <pubDate>2017-09-28T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Undertow/http2</link>
          <guid isPermaLink="true">https://www.jtips.info/Undertow/http2</guid>
        </item>
      
    
      
        <item>
          <title>TLS/SSL avec Undertow</title>
          <description>
Undertow est un serveur Web Java avec lequel il est facile de démarrer.


Le support de SSL apporte un léger surplus de compléxité.
Juste quelques lignes de code.


Certificat


Undertow utilise la classe java.security.Keystore.
C&#8217;est donc elle qui impose les formats supportés.


C&#8217;est donc naturellement que Undertow supporte des certificats de type




JKS, générés avec l&#8217;outil keytool du JDK


PKCS12, créés avec OpenSSL




Dans l&#8217;exemple ci-dessous, j&#8217;ai utilisé un certificat PKCS12 généré comme ceci :



openssl req -newkey RSA:2048 -nodes -keyout undertow.key -x509 -days 365 -out undertow.crt
openssl pkcs12 -inkey undertow.key -in undertow.crt -export -out undertow.pfx
rm undertow.key &amp;amp;&amp;amp; rm undertow.crt



J&#8217;ai ensuite placé le fichier undertow.pfx dans le répertoire resources de mon projet afin qu&#8217;il se retrouve à la racine du fichier jar construit.




Démarrage


Au démarrage, au lieu du HttpListener, il faut ajouter un HttpsListener avec un context SSL qui contient le certificat.



   public static void main(final String[] args) throws Exception {
       ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

       KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
       keyStore.load(
               classLoader.getResourceAsStream(KEYSTORE_FILE),
               KEYSTORE_PASSWORD.toCharArray());
       KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
       keyManagerFactory.init(keyStore, KEYSTORE_PASSWORD.toCharArray());

       SSLContext sslContext = SSLContext.getInstance(TLS_VERSION);
       sslContext.init(keyManagerFactory.getKeyManagers(), null, null);

       Undertow sslServer = Undertow.builder()
               .addHttpsListener(8001, "localhost", sslContext)
               .setHandler(Start::hello)
               .build();
       sslServer.start();
   }



</description>
          <pubDate>2017-09-28T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Undertow/SSL</link>
          <guid isPermaLink="true">https://www.jtips.info/Undertow/SSL</guid>
        </item>
      
    
      
        <item>
          <title>Démarrer avec Undertow</title>
          <description>
Undertow est le serveur Web / Servlet embarqué dans WildFly.


Il peut aussi être utilisé en autonome.


Dépendances


On utilise Undertow de manière programmatique. Donc avant de coder, il faut commencer par déclarer des dépendances, avec Maven par exemple.


La dépendance principale est io.undertow:undertow-core.
On peut développer son serveur Web avec elle seule.


La documentation propose de compléter avec  io.undertow:undertow-servlet et io.undertow:undertow-websockets-jsr.




Démarrage


Tout se passe dans un méthode main().



   public static void main(final String[] args) throws Exception {
       Undertow simpleServer = Undertow.builder()
               .addHttpListener(8000, "localhost")
               .setHandler(Start::hello)
               .build();
       simpleServer.start();
   }

   private static void hello(HttpServerExchange exchange) {
       exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
       exchange.getResponseSender().send("Hello World");
   }



</description>
          <pubDate>2017-09-28T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Undertow/HTTP</link>
          <guid isPermaLink="true">https://www.jtips.info/Undertow/HTTP</guid>
        </item>
      
    
      
        <item>
          <title>HTTP/2 avec Tomcat</title>
          <description>
Le support de HTTP/2 a été ajouté dans Tomcat 9, et backporté dans Tomcat 8.5.


Ça a été implémenté avec quelques contraintes :




SSL/TLS avec ALPN


Connecteur OpenSSL uniquement pour Java 8


Connecteur JSSE à partir de Java 9




SSL/TLS


Tout d&#8217;abord, il faut configurer un connecteur avec SSL/TLS.


Il est très fortement conseillé d&#8217;utiliser la version la plus récente de TLS, car les navigateurs peuvent refuser la connexion HTTP/2 si TLS est ancien.
En 2017, Firefox 55 et Chrome 61 n&#8217;acceptent de se connecter qu&#8217;en TLS 1.2 ou plus récent.




Upgrade et ALPN


On modifie notre connecteur TLS pour y configurer l&#8217;upgrade vers HTTP/2.



   &lt;Connector port="8543" SSLEnabled="true"&gt;
       &lt;UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" &gt;
       ...
   &lt;/Connector&gt;



Si notre client (navigateur) et notre plateforme supportent ALPN, alors les communications se feront en HTTP/2.


ALPN et Java

Le support d&#8217;ALPN a été ajouté dans Java 9.
Tomcat se basant dessus pour ses connecteurs JSSE (pur Java), ces derniers ne pourront pas faire de HTTP/2 en Java 8.


La configuration suivante ne fonctionne donc qu&#8217;à partir de Java 9 :



   &lt;Connector port="8443" SSLEnabled="true"
              protocol="org.apache.coyote.http11.Http11NioProtocol"
              sslImplementationName="org.apache.tomcat.util.net.jsse.JSSEImplementation"&gt;
       &lt;UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" &gt;
       &lt;SSLHostConfig&gt;
           &lt;Certificate certificateKeystoreFile="conf/tomcat.pfx" certificateKeystorePassword="tomcat"&gt;
       &lt;/SSLHostConfig&gt;
   &lt;/Connector&gt;



D&#8217;ailleurs, en démarrant en Java 8, on peut lire ça dans les logs :



31-Sep-2017 16:64:00.666 SEVERE [main] org.apache.coyote.http11.AbstractHttp11Protocol.configureUpgradeProtocol
The upgrade handler [org.apache.coyote.http2.Http2Protocol] for [h2] only supports upgrade via ALPN but has
been configured for the ["https-jsse-nio-8443"] connector that does not support ALPN.



Alors qu&#8217;en Java 9 :



31-Sep-2017 16:64:00.666 INFO [main] org.apache.coyote.http11.AbstractHttp11Protocol.configureUpgradeProtocol
The ["https-jsse-nio-8443"] connector has been configured to support negotiation to [h2] via ALPN




ALPN et OpenSSL

En Java 8, on ne peut utiliser qu&#8217;un connecteur avec un SSL géré en mode OpenSSL.
Ça signifie qu&#8217;il faut installer Tomcat Native avec un connecteur APR ou NIO+OpenSSL.



   &lt;Connector port="8443" SSLEnabled="true"
              protocol="org.apache.coyote.http11.Http11NioProtocol"
              sslImplementationName="org.apache.tomcat.util.net.openssl.OpenSSLImplementation"&gt;
       &lt;UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" &gt;
       &lt;SSLHostConfig&gt;
           &lt;Certificate certificateKeystoreFile="conf/tomcat.pfx" certificateKeystorePassword="tomcat"&gt;
       &lt;/SSLHostConfig&gt;
   &lt;/Connector&gt;



Et si c&#8217;est bien configuré, on devrait voir le même log :



31-Sep-2017 16:64:00.666 INFO [main] org.apache.coyote.http11.AbstractHttp11Protocol.configureUpgradeProtocol
The ["https-openssl-apr-8443"] connector has been configured to support negotiation to [h2] via ALPN






Références




Apache Tomcat 9 Configuration Reference - HTTP/2 Support


Gist - Tomcat 9 configuration with HTTP/2


Docker example, with Tomcat 10 and JDK 11




</description>
          <pubDate>2017-09-28T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Tomcat/HTTP2</link>
          <guid isPermaLink="true">https://www.jtips.info/Tomcat/HTTP2</guid>
        </item>
      
    
      
        <item>
          <title>WildFly remoting</title>
          <description>
Dans WildFly, on appelle remoting la technique qui permet de fait d&#8217;appeler des EJB ou du JMS depuis un client distant.


Depuis WildFly 8, on utilise un protocole spécifique qui s&#8217;appelle http-remoting.
Il utilise le port HTTP (8080) et exploite la technique de l&#8217;UPGRADE.


Le remoting est activé par défaut, mais sans sécurité.
Il peut donc être utile d&#8217;ajouter deux aspects de sécurité : l&#8217;authentification et le transport SSL.


La configuration décrite ci-dessous a été testée avec WildFly 10.1.
Il est probable qu&#8217;elle ne marche plus à partir de WidlFly 11.


Client


Dépendances (EJB)

Sans outil de gestion de dépendance (Maven, Gradle,&#8230;&#8203;), la façon la plus simple d&#8217;avoir les bonnes classes coté client est d&#8217;utiliser le fichier $JBOSS_HOME/bin/client/jboss-client.jar.


Avec Maven, on peut déclarer la dépendance suivante :



 &lt;dependency&gt;
     &lt;groupId&gt;org.jboss&lt;/groupId&gt;
     &lt;artifactId&gt;jboss-ejb-client&lt;/artifactId&gt;
     &lt;version&gt;2.1.8.Final&lt;/version&gt;
 &lt;/dependency&gt;




Properties

L&#8217;accès au registre JNDI distant est configuré par un fichier jndi.properties.



# jndi.properties
java.naming.factory.url.pkgs=org.jboss.ejb.client.naming



Et l&#8217;accès aux EJB est configuré dans une autre fichier jboss-ejb-client.properties.



#jboss-ejb-client.properties
endpoint.name=client-endpoint
remote.connections=default
remote.connection.default.host=127.0.0.1
remote.connection.default.port=8080



Ces deux fichiers peuvent être remplacés par quelques lignes de code



Properties clientProperties = new Properties();
clientProperties.put("remote.connections", "default");
clientProperties.put("remote.connection.default.port", "8080");
clientProperties.put("remote.connection.default.host", "127.0.0.1");

EJBClientConfiguration ejbClientConfiguration = new PropertiesBasedEJBClientConfiguration(clientProperties);
ContextSelector&lt;EJBClientContext&gt; contextSelector = new ConfigBasedEJBClientContextSelector(ejbClientConfiguration);
EJBClientContext.setSelector(contextSelector);

Properties jndiProperties = new Properties();
jndiProperties.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
namingContext = new InitialContext(jndiProperties);



Cette première étape devrait permettre d&#8217;appeler des EJB distant, mais sans aucune sécurité.





Authentification


Client


endpoint.name=client-endpoint
remote.connections=default
remote.connection.default.host=127.0.0.1
remote.connection.default.port=8080

remote.connection.default.username=alexis
remote.connection.default.password=hassler

remote.connection.default.connect.options.org.xnio.Options.SASL_POLICY_NOPLAINTEXT=false
remote.connection.default.connect.options.org.xnio.Options.SASL_DISALLOWED_MECHANISMS=JBOSS-LOCAL-USER




Serveur


/core-service=management/security-realm=RemotingRealm:add
/core-service=management/security-realm=RemotingRealm/authentication=jaas:add(name=sw-domain)

/subsystem=remoting/http-connector=remoting-connector:add(connector-ref=default, security-realm=RemotingRealm)






SSL


Client


endpoint.name=client-endpoint
remote.connections=default
remote.connection.default.protocol=https-remoting   # Remoting on HTTPS
remote.connection.default.host=127.0.0.1
remote.connection.default.port=8443                 # HTTPS port
remote.connection.default.connect.options.org.xnio.Options.SASL_POLICY_NOPLAINTEXT=false
remote.connection.default.connect.options.org.xnio.Options.SASL_DISALLOWED_MECHANISMS=JBOSS-LOCAL-USER

remote.connection.default.username=alexis
remote.connection.default.password=hassler




Serveur


/core-service=management/security-realm=RemotingRealm:add
/core-service=management/security-realm=RemotingRealm/authentication=jaas:add(name=sw-domain)

/subsystem=remoting/http-connector=ssl-remoting-connector:add(connector-ref=https, security-realm=RemotingRealm)




</description>
          <pubDate>2017-09-07T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WildFly/Remoting</link>
          <guid isPermaLink="true">https://www.jtips.info/WildFly/Remoting</guid>
        </item>
      
    
      
        <item>
          <title>Code bloquant dans Vert.x</title>
          <description>
Le credo de Vert.X, c&#8217;est :


Don&#8217;t block the event loop


En clair, il ne faut pas appeler de méthode bloquante dans un verticle classique.
Mais il propose quelques solutions pour exécuter quand même du code bloquant.


Code bloquant


Le problème, c&#8217;est qu&#8217;on trouve souvent du code bloquant dans des librairies tierces :




JDBC


I/O




Pour faire mes tests, j&#8217;ai juste utilisé Thread.sleep().



   private String sleep(long duration) {
       try {
           System.out.println(String.format("Start sleeping for %s ms", duration));
           Thread.sleep(duration);
           return "End sleeping";
       } catch (InterruptedException e) {
           e.printStackTrace();
           return "Problem while sleeping";
       }
   }





Délai


Il y a deux délais qui entrent en jeu.




max event loop execute time (ns) : c&#8217;est le délai principal ; si l&#8217;event loop n&#8217;a pas rendu la main après ce délai, une alerte est loguée,


blocked thread check period (ms) : c&#8217;est l&#8217;intervalle de vérification




Par défaut, le premier est à deux secondes et le deuxième à une seconde.
Donc on est certain d&#8217;avoir une alerte si on dépasse les trois secondes de bloquage, et on peut éventuellement en avoir une entre deux et trois secondes.


Ces délais peuvent être modifiés au démarrage de Vert.X.



   public static void main(String[] args) {
       VertxOptions options = new VertxOptions();

       options.setBlockedThreadCheckInterval(500L);  // 0.5 s
       options.setMaxEventLoopExecuteTime(500_000L); // 0.5 s

       Vertx vertx = Vertx.vertx(options);
       ....
   }



La modification de l&#8217;intervalle de vérification est particulièrement utile en debug (cf. mon blog).




executeBlocking


La façon la plus simple d&#8217;exécuter du code bloquant est de l&#8217;enrober dans un vertx.executeBlocking(&#8230;&#8203;).
Cette méthode prend en premier paramètre la lambda bloquante à exécuter.
Le deuxième paramètre est un callback appelé lorsque la première lambda est terminée.


Pour marquer la fin du traitement, il faut appeler event.complete().
L&#8217;objet passé en paramètre est récupéré dans le result du callback.



   private void sleepBlocking(long duration) {
       vertx.executeBlocking(event -&amp;gt; event.complete(sleep(duration)),
                             event -&amp;gt; System.out.println(event.result().toString()));
   }



Cet appel peut se faire depuis n&#8217;importe quel verticle.
La lambda est exécutée sur un worker-thread.
Lorsqu&#8217;on appelle plusieurs executeBlocking(&#8230;&#8203;) à la suite, dans le même contexte, il sont exécutés sur le même thread, de façon séquentielle.
En ajoutant false en 2° paramètre, on demande à ce que l&#8217;exécution ne respecte pas l&#8217;ordre, et dans ce cas les lambdas
sont appelées en parallèle, sur des threads séparés.


Attention quand même à la durée de blocage.
Le temps d&#8217;exécution d&#8217;une tâche doit être inférieur au paramètre maxWorkerExecuteTime qui vaut une minute par défaut.
Et ça aussi, ça peut se configurer.



   public static void main(String[] args) {
       ....
       options.setMaxEventLoopExecuteTime(500_000L); // 0.5 s
       ....
   }



La vérification de durée de traitement est faite par le BlockedThreadChecker, comme pour l'event loop.
Elle est aussi sensible à la modification de son intervalle de vérification.




Worker Verticle


Un worker verticle est un verticle qui fonctionne sur son propre thread, issu du worker thread pool.



   public static void main(String[] args) {
       ...
       vertx.deployVerticle(new DatabaseVerticle(),
                            new DeploymentOptions().setWorker(true));
       ...
   }





Threads Pool


Les threads utilisés font partie d&#8217;un pool créé au démarrage de Vert.X.
La taille du pool est fixée au démarrage avec options.setWorkerPoolSize(&#8230;&#8203;) ; la taille par défaut est 20.


On peut aussi faire travailler un pool de threads spécifique.
On instancie un WorkerExecutor pour ça, et on c&#8217;est lui qui fera l'`executeBlocking(&#8230;&#8203;)`.



   private void sleepBlocking(long duration) {
       WorkerExecutor executor = vertx.createSharedWorkerExecutor("toto");
       executor.executeBlocking(
               event -&amp;gt; event.complete(sleep(duration)),
               event -&amp;gt; System.out.println(event.result().toString()));
   }



Attention, ça ne marche pas si on est déjà sur un worker thread.
Par exemple, si on est dans un worker verticle, on restera sur le pool par défaut.


</description>
          <pubDate>2017-05-17T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Vertx/Blocking</link>
          <guid isPermaLink="true">https://www.jtips.info/Vertx/Blocking</guid>
        </item>
      
    
      
        <item>
          <title>JSON dans Vert.X</title>
          <description>
Vert.x core fournit quelques classes pour faire du JSON.
Il les utilise dans ses APIs.
C&#8217;est Jackson qui est utilisé de façon cachée.


Json


La classe io.vertx.core.json.Json est essentiellement un wrapper pour ObjectMapper de Jackson.
Elle est là pour nous cacher et simplifier l&#8217;utilisation de l&#8217;ObjectMapper, avec des méthodes statiques.


Pour passer d&#8217;un contenu json à un objet Java :



String jsonString = "{\"name\":\"Alexis\"}";
Person person = Json.decodeValue(jsonString, Person.class);



Il existe aussi une version joliment formattée.


Et pour le sens inverse :



String jsonPerson = Json.encode(person);





JsonObject


La classe io.vertx.core.json.JsonObject permet de charger du contenu json dans un objet non typé, qui ressemble à une map.



JsonObject jsonObject = new JsonObject(jsonString);
String name = jsonObject.getString("name");



On a donc des méthodes getXxx() pour récupérer les valeurs et des méthodes put() pour en modifier ou en ajouter.


Et pour transformer en json, on encode(), avec aussi une variante prettily :



jsonObject.put("Age", 42);
String result = jsonObject.encode());





JsonArray


La classe io.vertx.core.json.JsonArray est l&#8217;équivalent de JsonObject pour des tableaux.



String jsonArrayString = "[\"Alexis\", \"Alice\"]";

JsonArray jsonArray = new JsonArray(jsonArrayString);
String first = jsonArray.getString(0));

jsonArray.add("Bob");
String result = jsonArray.encode());



</description>
          <pubDate>2017-05-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Vertx/json</link>
          <guid isPermaLink="true">https://www.jtips.info/Vertx/json</guid>
        </item>
      
    
      
        <item>
          <title>Verticle, le composant Vert.x</title>
          <description>
L&#8217;unité de travail de Vert.x est le verticle.


Verticle


Concrètement, c&#8217;est une classe qui hérite de AbstractVerticle.



package fr.sewatech.vertx;

import io.vertx.core.AbstractVerticle;

public class SimpleVerticle extends AbstractVerticle {
    @Override
   public void start() throws Exception {
       ...
   }
}





Démarrage


La façon la plus simple de démarrer une application vert.x, est de faire une méthode main.


Dans cette méthode on crée une instance de Vertx et on lui transmet une instance de notre Verticle.



   public static void main(String[] args) {
       Vertx vertx = Vertx.vertx();
       vertx.deployVerticle(new SimpleVerticle());
   }



</description>
          <pubDate>2017-05-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Vertx/Verticle</link>
          <guid isPermaLink="true">https://www.jtips.info/Vertx/Verticle</guid>
        </item>
      
    
      
        <item>
          <title>Références Vert.x</title>
          <description>
Références Web




Site Web


Documentation officielle


Plugin Maven






Bibliographie




Mini-book Reactive Microservices in Java (registration required)






Audio et Vidéos


Podcast




Castcodeurs : Interview sur Vert.x avec Julien Viet et Clément Escoffier




Présentations




Applications réactives avec Eclipse Vert.x par Julien Ponge et Julien Viet, à Devoxx France 2017, en français (2h50)


Microservices réactifs avec Eclipse Vert.x et Kubernetes par Clément Escoffier, à Devoxx France 2017, en français (40min)


Chaîne youtube d&#8217;Eclipse Vert.x




</description>
          <pubDate>2017-05-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Vertx/References</link>
          <guid isPermaLink="true">https://www.jtips.info/Vertx/References</guid>
        </item>
      
    
      
        <item>
          <title>Vert.x avec Maven</title>
          <description>
Dépendances


On peut utiliser Maven pour déclarer les dépendances vers les librairies de Vert.x.


Par exemple :



    &lt;dependency&gt;
        &lt;groupId&gt;io.vertx&lt;/groupId&gt;
        &lt;artifactId&gt;vertx-core&lt;/artifactId&gt;
        &lt;version&gt;${vertx.version}&lt;/version&gt;
    &lt;/dependency&gt;



Quelques autres dépendances classiques :




io.vertx:vertx-web pour les applications Web


io.vertx:vertx-hazelcast pour un cluster


&#8230;&#8203; (à compléter)




Toutes les versions des dépendances sont décrites dans un bom.



 &lt;dependencyManagement&gt;
   &lt;dependencies&gt;
     &lt;dependency&gt;
       &lt;groupId&gt;io.vertx&lt;/groupId&gt;
       &lt;artifactId&gt;vertx-dependencies&lt;/artifactId&gt;
       &lt;version&gt;${vertx.version}&lt;/version&gt;
       &lt;type&gt;pom&lt;/type&gt;
       &lt;scope&gt;import&lt;/scope&gt;
     &lt;/dependency&gt;
   &lt;/dependencies&gt;
 &lt;/dependencyManagement&gt;





Plug-in Reactiverse


On peut pousser l&#8217;utilisation de Maven avec le plug-in Plugin Maven par Reactiverse io.reactiverse:vertx-maven-plugin. On peut utiliser celui-ci dès la création du projet.


Pour créer un projet vide :



$ mvn -DprojectGroupId=fr.sewatech.formation       \
      -DprojectArtifactId=vertx-mvn                \
      io.reactiverse:vertx-maven-plugin:1.0.24:setup



On peut aussi demander au plugin de créer un Verticle et/ou d&#8217;ajouter des dépendances Vert.X.



$ mvn -Ddependencies=web,jmx                       \
      -Dverticle=fr.sewatech.vertx.MvnVerticle     \
      io.reactiverse:vertx-maven-plugin:1.0.24:setup



Attention, l&#8217;ajout des dépendances ne fonctionne que si le le fichier pom.xml ne contient pas déjà le plugin.
Et il y a un risque d&#8217;avoir des dépendances dupliquées.


Le plug-in intervient aussi dans le packaging de l&#8217;application en produisant un fat-jar, c&#8217;est-à-dire un fichier jar contenant l&#8217;application et toutes ses dépendances.



$ mvn compile vertx:package



ou simplement, grâce à l&#8217;intégration du plugin dans le cycle de vie maven :



$ mvn package



Ce mode de packaging permet de lancer très simplement l&#8217;application.



$ java -jar target/vertx-mvn-1.0-SNAPSHOT.jar start



Il est aussi possible de lancer l&#8217;application avec le plugin.



$ mvn vertx:run



L&#8217;intérêt de ce mode de lancement est sa capacité de relancer l&#8217;application pour prendre en compte les modifications.


Malheureusement, 'mvn vertx:debug' ne supporte pas le rechargement.




Vert.X starter


Le projet Vert.X fournit aussi un projet d&#8217;exemple à partir duquel on peut démarrer.



$ curl http://vertx.io/assets/starter-scripts/create-vertx-project-maven.sh -o vertx-create-maven-project.sh &amp;&amp; \
  bash vertx-create-maven-project.sh



Le projet produit aussi un fat-jar grâce au plug-in Shade.



$ mvn package



Il permet aussi de lancer directement l&#8217;application, grâce au plug-in exec.



$ mvn exec:run



</description>
          <pubDate>2017-05-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Vertx/Maven</link>
          <guid isPermaLink="true">https://www.jtips.info/Vertx/Maven</guid>
        </item>
      
    
      
        <item>
          <title>Application Web avec Vert.x</title>
          <description>
Serveur Web


On peut débuter une application Web avec vertx-core, mais on se rend rapidement compte qu&#8217;il est plus pratique de démarrer avec vertx-web


Après avoir créé notre verticle, on peut y démarrer un serveur Web.



   @Override
   public void start() throws Exception {
       vertx.createHttpServer()
               .requestHandler(request -&gt; request.response().end("Hello World\n"))
               .listen(8001);
   }



Ce serveur va traiter toutes les requêtes qui arrivent sur le port 8001 avec le même résultat.



$ curl http://localhost:8001





Routeur


Pour traiter les requêtes de façon différenciée, selon l&#8217;URL ou la méthode, on peut utiliser le routeur.



   @Override
   public void start() throws Exception {
       Router router = Router.router(vertx);

       ...

       vertx.createHttpServer()
               .requestHandler(router::accept)
               .listen(8001);
   }



Maintenant qu&#8217;on a le routeur, on va pouvoir déclarer les routes&#8230;&#8203; (TBD)




Routes simples


Commençons par déclarer des routes simplement par leur URL.
Les routes sont évaluées dans l&#8217;ordre de déclaration, et pour la première dont l&#8217;URL correspond, le handler est appelé.



   @Override
   public void start() throws Exception {
       ...
       router.route("/hello")
             .handler(routingContext -&gt; routingContext.response().end("Hello World"));
       router.route("/hi")
             .handler(routingContext -&gt; routingContext.response().end("Hi everybody"));
   }



Au niveau du handler, la seule différence avec l&#8217;exemple basique c&#8217;est qu&#8217;on ne travaille plus avec l&#8217;objet request, mais avec un RoutingContext.



$ curl http://localhost:8001/hello



Pour les requêtes avec paramètres, j&#8217;utilise une méthode plutôt qu&#8217;une lambda, pour des raisons de lisibilité.



   @Override
   public void start() throws Exception {
       ...
       router.get("/hello/:name")
               .handler(this::hello);
       ...
   }

   private void hello(RoutingContext routingContext) {
       String name = routingContext.request().getParam("name");
       routingContext.response().end("Hello " + name);
   }



On notera que par défaut les paramètres de requête et le paramètres de chemin sont exploités de la mmême façon.



$ curl http://localhost:8001/hello/Alexis



ou



$ curl http://localhost:8001/hello/who?name=Alexis





Post


Pour avoir accès au corp des requêtes POST, il faut avoir ajouté un body handler.



   @Override
   public void start() throws Exception {
       ...
       router.route()
           .handler(BodyHandler.create());
       router.post("/update")
           .handler(this::update);
       ...
   }

   private void update(RoutingContext routingContext) {
       routingContext.getBodyAsJson()
               .stream()
               . ...;
       routingContext.response().end("Done");
   }



On peut récupérer le body sous la forme d&#8217;un objet JSON ou d&#8217;un Buffer.


Sans la ligne avec le BodyHandler, le body sera toujours null.



$ curl --data '{"name":"Alexis"}' http://localhost:8001/update










Habituellement, une seule route est exécutée, or là, la route "/hello" est exécutée après celle qui contient le body handler.
C&#8217;est juste parce que le body handler ne construit pas de response, mais fait appel à la route suivante.








Formulaire


D&#8217;un point de vue HTTP, les données d&#8217;un formulaire sont représentées par le type MIME "multipart/form-data".
Ce type de données est traitée par le BodyHandler, mais au lieu de les mettre dans l&#8217;objet routingContext.getBody(), elles sont mises à disposition dans la routingContext.request().formAttributes().



   @Override
   public void start() throws Exception {
       ...
       router.post("/form")
           .handler(this::form);
       ...
   }

   private void form(RoutingContext routingContext) {
       MultiMap attributes = routingContext.request().formAttributes();
       ...
       routingContext.response().end("Done");
   }



Les attributs sont dans une multi-map ce qui permet d&#8217;avoir plusieurs valeurs pour la même clé.



$ curl --form "name=Alexis" http://localhost:8001/form





Upload


L&#8217;upload est juste un POST "multipart/form-data" particulier.


Il est aussi pris en charge par le BodyHandler qui stocke chaque fichier uploadé dans un répertoire
et qui nous fourni des métdonnées sous la forme d&#8217;objets FileUpload.



   private void upload(RoutingContext routingContext) {
       routingContext.fileUploads()
               .stream()
               .map(FileUpload::uploadedFileName)
               .forEach(System.out::println);
       routingContext.response().end();
   }



Chaque objet FileUpload nous donne le chemin du fichier, le nom du fichier coté client, le nom de l&#8217;attribut de
formulaire, la taille du fichier et le type MIME de son contenu.



$ curl -F "image=@cute-kitty.jpg" -v http://localhost:8001/upload





Routes intermédiaires


En général, lorsqu&#8217;on associe un handler à une route, c&#8217;est pour produire une réponse.
Ce n&#8217;est pas le cas avec le BodyHandler qui est là pour préparer des données pour d&#8217;autres handlers.


On peut aussi implémenter des handlers intermédiaires.
Pour cela, au lieu de produire une réponse, il faut demander au routeur de passer à la route suivante.



   @Override
   public void start() throws Exception {
       ...
       router.route()
             .handler(routingContext -&gt; {
                         routingContext.put("default-name", "nobody");
                         routingContext.next();
                       });
       ...
   }



Dans cette exemple, j&#8217;ajoute une donnée qui sera utilisable par les handlers suivants par routingContext.get("default-name") ou dans la map routingContext.data().


</description>
          <pubDate>2017-05-13T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Vertx/Web</link>
          <guid isPermaLink="true">https://www.jtips.info/Vertx/Web</guid>
        </item>
      
    
      
        <item>
          <title>ANTLR/JavaScript</title>
          <description>
ANTLR


ANTLR est une bibliothèque qui permet de générer du code d&#8217;analyse de documents texte à partir d&#8217;une grammaire. Jusqu&#8217;à la version 3, le code était généré uniquement pour le langage Java. Depuis la version 4, d&#8217;autres cibles sont disponibles : JavaScript, C#, Python et bientôt C++.


Dans cet article, je vais montrer comment mettre en oeuvre un parser avec JavaScript.


Le code source est disponible sur GitHub




Création du projet


ANTLR est une API Java. J&#8217;utilise donc un IDE Java pour créer mon projet. Je choisi par ailleurs d&#8217;utiliser Gradle pour la construction du projet.


Voici la structure du projet Gradle généré par mon IDE :







Je modifie ensuite le fichier de construction 'build.gradle'. L&#8217;élément important dans ce script est l&#8217;argument '-Dlanguage=JavaScript'.



plugins {
   id 'antlr'
}

repositories {
   mavenCentral()
}

group 'icodem'
version '1.0-SNAPSHOT'
description = 'CSV ANTLR4-based parser'

dependencies {
   antlr "org.antlr:antlr4:4.5.1"
}

generateGrammarSource {
   group "${project.name}"
   description 'Generates the parser code based on the grammar'

   arguments += ["-visitor", "-Dlanguage=JavaScript"]

   copy {
       from 'build/generated-src/antlr/main'
       into 'src/main/resources'
       include '/CSV*.js'
       include '/CSV*.tokens'
   }
}

clean {
   delete fileTree('src/main/resources') {
       include '/CSV*.js'
       include '/CSV*.tokens'
   }
}

wrapper {
   gradleVersion = '2.5'
}





La grammaire


Je prends une grammaire dans le repo GitHub de ANTLR et je place le fichier correspondant dans le répertoire 'src/main/antlr' :



grammar CSV;

document        : hdr row+ EOF;

hdr : row ;

row : field (',' field)* '\r'? '\n' ;

field : TEXT | STRING | ;

TEXT : ~[,\n\r"]+ ;
STRING : '"' ('""'|~'"')* '"' ;








On peut donc maintenant générer les fichiers JavaScript pour notre analyseur via la commande Gradle 'generateGrammarSource'. Les fichiers générés sont copiés dans le répertoire 'src/main/resources'.









Installer le runtime ANTLR


Le runtime ANTLR est l&#8217;ensemble des fichiers JavaScript qui permettent l&#8217;exécution de notre analyseur JavaScript constitué des fichiers générés précedemment. Il est téléchargeable depuis le site de ANTLR. Il faut prendre la version de runtime qui correspondant à la version utilisée pour la génération, i.e. dans notre exemple antlr-javascript-runtime-4.5.1.zip.


On place ensuite le runtime dans notre projet dans le répertoire 'src/main/resources'.







Il faut aussi ajouter le fichier 'require.js' que je place pour ma part dans le sous-répertoire 'lib'. A noter que ce script ne fait pas partie du runtime ANTLR. Il faut donc le télécharger depuis le site GitHub de ANTLR.









Exécution dans un navigateur


L&#8217;exécution est effectué dans une page HTML. Les points importants :




Charger le script 'require.js' avec la balise '&lt;script&gt;'


Charger le runtime ANTLR avec 'require()'


Charger les fichiers de scripts de notre analyseur avec 'require()'










&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
   &lt;meta charset="UTF-8"&gt;
   &lt;title&gt;Parsing CSV avec ANTLR 4&lt;/title&gt;
   &lt;script src="lib/require.js"&gt;&lt;/script&gt;
&lt;/head&gt;

&lt;body&gt;
    &lt;p id="display"/&gt;
&lt;/body&gt;

&lt;script&gt;
   // chargement du runtime ANTLR
   var antlr4 = require('antlr4/index');

   // chargement de l'analyseur CSV
   var CSVLexer = require('./CSVLexer');
   var CSVParser = require('./CSVParser');

   // chaîne à analyser
   var input = "Nom,Prénom,Age\nLampion,Émile,29\nLarosière,Jean,48\n";

   // préparation des objets pour l'analyse
   var chars = new antlr4.InputStream(input);
   var lexer = new CSVLexer.CSVLexer(chars);
   var tokens  = new antlr4.CommonTokenStream(lexer);
   var parser = new CSVParser.CSVParser(tokens);
   parser.buildParseTrees = true;

   // invocation de l'analyse
   var tree = parser.document();

   // affichage du résultat
   var display = document.getElementById("display");
   display.innerHTML = tree.toStringTree(null, parser);

   &lt;/script&gt;

&lt;/html&gt;





Exécution dans une JVM


On peut exécuter notre analyseur JavaScript dans une JVM à l&#8217;aide du moteur de script JavaScript de la JVM. Avec Java 8, il s&#8217;agit de Nashorn.


Pour la mise en oeuvre, nous utilisons une classe Java, un script JS et une version adapté de 'require' pour Nashorn :







Le script JS fournit une fonction de parsing. Autrement dit, cette fonction invoque l&#8217;analyseur JS comme nous le faisions dans le cas d&#8217;une exécution dans une page web.


Fichier 'script.js' :



function parseCSV(input) {
    // chargement du script 'require' spécifique nashorn
    load('src/main/resources/lib/require-nashorn.js');
    require.basePath = 'src/main/resources/';

    // chargement du runtime ANTLR
    var antlr4 = require('antlr4/index');

    // chargement de l'analyseur CSV
    var CSVLexer = require('./CSVLexer');
    var CSVParser = require('./CSVParser');

    // chaîne à analyser
    var input = "Nom,Prénom,Age\nLampion,Émile,29\nLarosière,Jean,48\n"

    // préparation des objets pour l'analyse
    var chars = new antlr4.InputStream(input);
    var lexer = new CSVLexer.CSVLexer(chars);
    var tokens  = new antlr4.CommonTokenStream(lexer);
    var parser = new CSVParser.CSVParser(tokens);
    parser.buildParseTrees = true;

    // invocation de l'analyse
    var tree = parser.document();

    var result = tree.toStringTree(null, parser);
    return result
}



La classe Java instancie le moteur JS Nashorn puis sollicite le précédent script.


Fichier 'Main.java' :



public class Main {

   public static void main(String[] args) throws Exception {
       ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
       Bindings globalScope = engine.getBindings(ScriptContext.GLOBAL_SCOPE);
       globalScope.put("global", globalScope);
       globalScope.put("window", "");// just to avoid fs.js module loading in FileStream.js

       engine.eval("load('src/main/resources/script.js')");

       Invocable invocable = (Invocable) engine;

       String input = "Nom,Prénom,Age\nLampion,Émile,29\nLarosière,Jean,48\n";
       Object result = invocable.invokeFunction("parseCSV", input);

       System.out.println(result);

   }
}



Le fichier 'require.js' disponible avec ANTLR n&#8217;est pas compatible pour une exécution avec Nashorn (du moins, je ne suis pas arrivé à le faire fonctionner). J&#8217;ai donc adapté le script de ANTLR à Nashorn.


Fichier 'require-nashorn.js' :



// NOTE The loadModule parameter points to the function, which prepares the
//      environment for each module and runs its code. Scroll down to the end of
//      the file to see the function definition.
(function(loadModule) { 'use strict';

// INFO Java objects
   var Paths = Java.type("java.nio.file.Paths");
   var Files = Java.type("java.nio.file.Files");
   var StandardCharsets = Java.type("java.nio.charset.StandardCharsets");
   var Collectors = Java.type("java.util.stream.Collectors");

// INFO Current module descriptors
//      pwd[0] contains the descriptor of the currently loaded module,
//      pwd[1] contains the descriptor its parent module and so on.

   var pwd = Array();

// INFO Module cache
//      Contains getter functions for the exports objects of all the loaded
//      modules. The getter for the module 'mymod' is name '$name' to prevent
//      collisions with predefined object properties (see note below).
//      As long as a module has not been loaded the getter is either undefined
//      or contains the module code as a function (in case the module has been
//      pre-loaded in a bundle).

   var cache = new Object();

// INFO Module getter
//      Takes a module identifier, resolves it and gets the module code via
//      Java NIO API. If this was successful the code and
//      some environment variables are passed to the load function. The return
//      value is the module's exports object. If the cache already
//      contains an object for the module id, this object is returned directly.

   function require(identifier) {

       var descriptor = resolve(identifier);
       var cacheid = '$'+descriptor.id;
       if (cache[cacheid]) {
           return cache[cacheid];
       }

       var content = Files.lines(descriptor.path, StandardCharsets.UTF_8)
           .collect(Collectors.joining("\n"));
       loadModule(descriptor, cache, pwd, content);
       return cache[cacheid];

   }

// INFO Module resolver
//      Takes a module identifier and resolves it to a module id and path. Both
//      values are returned as a module descriptor, which can be passed to
//      fetch to load a module.

   function resolve(identifier) {

       var basePath = Paths.get(require.basePath);
       var parentPath = basePath;
       if (pwd.length &gt; 0) {
           parentPath = pwd[0].path.getParent();
       }
       var scriptPath = parentPath.resolve(identifier + ".js").normalize();

       // build id corresponding to script
       var subpath = scriptPath.subpath(basePath.getNameCount(), scriptPath.getNameCount());
       var id = subpath.toString();

       return {'id': id, 'path': scriptPath};
   }


// INFO Exporting require to global scope
   global.require = require;

})(

// INFO Module loader
//      Takes the module descriptor, the global variables and the module code,
//      sets up the module environment, defines the module getter in the cache
//      and evaluates the module code.

   function (module) {
       var exports = new Object();
       Object.defineProperty(module, 'exports', {'get':function(){return exports;},'set':function(e){exports=e;}});
       arguments[2].unshift(module);
       Object.defineProperty(arguments[1], '$'+module.id, {'get':function(){return exports;}});
       try {
           eval(arguments[3]);
       } catch (e) {
           print("* eval ERROR " + module.path + " : " + e.message);
       }

       arguments[2].shift();
   }

);



</description>
          <pubDate>2015-09-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JavaScript/ANTLR</link>
          <guid isPermaLink="true">https://www.jtips.info/JavaScript/ANTLR</guid>
        </item>
      
    
      
        <item>
          <title>Installation de PostgreSQL</title>
          <description>
L&#8217;installation de PostgreSQL sur Debian m&#8217;a réservé quelques surprises&#8230;&#8203; Plus précisément, il y a quelques différences entre la doc qui traite surtout de l&#8217;installation depuis les sources et l&#8217;installation par les dépôts. C&#8217;est donc pour ça que j&#8217;ai fait cette page.


Debian


Installation

Debian propose une version par défaut de PostgreSQL, mais le fournisseur de la base de données propose ses propres dépôts, permettant de choisir sa version.


J&#8217;ai installé PostgreSQL 9.3 sur Debian 8 (Jessie) :



touch /etc/apt/sources.list.d/pgdg.list
echo "deb link:http://apt.postgresql.org/pub/repos/apt/[http://apt.postgresql.org/pub/repos/apt/] jessie-pgdg main" &gt; /etc/apt/sources.list.d/pgdg.list
wget --quiet -O - link:https://www.postgresql.org/media/keys/ACCC4CF8.asc[https://www.postgresql.org/media/keys/ACCC4CF8.asc] | sudo apt-key add -
sudo apt-get update
sudo apt-get install postgresql-9.3



Le package client est installé automatiquement. Par contre, la base ne peut pas encore être démarrée, il faut d&#8217;abord initialiser sont cluster (répertoire de données).



Cluster

Pour cela il ne faut pas utiliser directement initdb, mais pg_createcluster. Dans un premier temps, la commande a échoué à cause d&#8217;une incohérence dans ma configuration le locale. J&#8217;ai donc du la réinitialiser au préalable (l&#8217;option --locale aurait peut-être été suffisante).



dpkg-reconfigure locales




pg_createcluster 9.3 main



Le cluster est maintenant créé dans /var/lib/postgresql/9.3/main/, avec ses fichiers de configuration dans /etc/postgresql/9.3/main/. Les autres commandes pour les clusters sont pg_lsclusters (liste des clusters), pg_dropcluster et pg_ctlcluster.


Il reste à démarrer la base (sur le port 5432) :



/etc/init.d/postgresql start




User et Database

On utilise le client en ligne de commande psql :



su -postgres
psql



On crée ensuite l&#8217;utilisateur avec son mot de passe et sa base de données :



postgres=# CREATE USER myuser WITH PASSWORD 'mypwd';
postgres=# CREATE DATABASE mydb OWNER myuser;



Dorénavant, je peux me connecter à cette base de données :



psql -h 127.0.0.1 -d mydb -U myuser



Pour pouvoir se connecter sans préciser le host, il faut modifier la ligne concernant la connexion local dans le fichier /etc/postgresql/9.3/main/pg_hba.conf, et passer sa connexion de peer (utilisation des credentials système) à md5 (utilisation d&#8217;un mot de passe) :



# TYPE  DATABASE        USER            ADDRESS                 METHOD
local   all             all                                     peer



devient



# TYPE  DATABASE        USER            ADDRESS                 METHOD
local   all             all                                     md5



Dans la configuration par défaut, aucune connexion distante n&#8217;est acceptée. Il est possible de les autoriser, ou d&#8217;utiliser plus simplement un tunnel SSH :



 ssh -L 65432:localhost:5432 -f sysuser@myserver -N




</description>
          <pubDate>2015-07-22T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/PostgreSQL/Install</link>
          <guid isPermaLink="true">https://www.jtips.info/PostgreSQL/Install</guid>
        </item>
      
    
      
        <item>
          <title>Crypto/AES</title>
          <description>
Générer une clé :



       SecretKey key = KeyGenerator.getInstance("AES").generateKey();



Lire une clé (texte) :



       byte[] keyAsBytes = "0123456789ABCDEF".getBytes();
       SecretKey key = new SecretKeySpec(keyAsBytes, 0, keyAsBytes.length, "AES");



Chiffrer du texte :



       String text = "Hello"
       Cipher aesCipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
       aesCipher.init(Cipher.ENCRYPT_MODE, key);
       byte[] cipherBytes = aesCipher.doFinal(text.getBytes());



Déchiffrer :



       Cipher aesCipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
       aesCipher.init(Cipher.DECRYPT_MODE, key);
       String text = new String(aesCipher.doFinal(cipherBytes));

</description>
          <pubDate>2015-07-17T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Crypto/AES</link>
          <guid isPermaLink="true">https://www.jtips.info/Crypto/AES</guid>
        </item>
      
    
      
        <item>
          <title>JMX Remote</title>
          <description>
Par accès remote, on entend surtout accès externe, par opposition à l&#8217;accès intra-VM au MBean server.
Cet accès est celui utilisé par des outils comme JConsole, VisualVM ou JDK Mission Control.
Il existe deux cas d&#8217;accès externes : local ou distant.


Accès local


L&#8217;accès local se fait par l'Attach API.
Cette technique ne nécessite aucune configuration pour être utilisée (depuis Java 6) :
on lance une application java, puis on peut y connecter notre outil (jconsole par exemple).
La connexion initiale se fait par un échange entre les process, puis une socket est ouverte sur un port aléatoire pour le reste de la communication.
Le dialogue se fait donc par le réseau par l&#8217;interface locale.


Il est possible de désactiver l'Attach API, et donc l&#8217;accès local, avec le flag -XX:+DisableAttachMechanism.
Il semble qu&#8217;il soit impossible de désactiver l&#8217;accès local si on active l&#8217;accès distant.


L&#8217;accès local est aussi désactivé si on désactive les données de performances avec -XX:-UsePerfData et reste désactivé même si on active l&#8217;accès distant.




Accès distant


Pour activer l&#8217;accès distant, il faut ouvrir un port d&#8217;accès, avec le flag com.sun.management.jmxremote.port=&lt;port&gt;.
Une fois cet accès activé, se posent des questions concernant la sécurité (authentification et SSL) et la configuration réseau.


Authentification

La solution la plus simple est de désactiver l&#8217;authentification avec com.sun.management.jmxremote.authenticate=false.
Simple pour le développement, mais risqué en production.


Pour un accès sécurisé, on a deux fichiers properties : celui des mots de passe et celui des droits d&#8217;accès.
Par défaut ces fichiers sont dans &lt;JDK_HOME&gt;/conf/management, ce qui n&#8217;est pas très pratique.
On changera leur localisation avec com.sun.management.jmxremote.password.file=&lt;jmxremote.password&gt; et com.sun.management.jmxremote.access.file=&lt;jmxremote.access&gt;.


Le fichier password contient des couples username password :



alexis h@ssl3r
guest s3cr3t



Les permissions doivent être limitées à l&#8217;utilisateur qui lance l&#8217;application :



~$ chmod 600 jmxremote.password



Le fichier access contient des couples username permission :



alexis readwrite
guest readonly



Pour un utilisateur en readwrite, on peut affiner les droits, en ajouter les permissions de suppression (unregister) de MBeans, et de création (create) de MBeans, limités à certains packages ou certaines classes.



alexis readwrite unregister create fr.sewatech.management.*




TLS/SSL

Là aussi, la solution la plus simple est de désactiver le TLS avec com.sun.management.jmxremote.ssl=false.
Et là aussi, ça peut être risqué en production.


Pour sécuriser les communications, il faut donc générer une pair de clé dans un keystore.



~$ keytool -genkeypair -keystore conf/jmxremote.p12 -storepass javapwd                \
                      -alias jmxremote -keypass javapwd -dname "cn=www.sewatech.fr"



Puis on utilise ce keystore au démarrage de l&#8217;application, avec les propriétés javax.net.ssl.keyStore et javax.net.ssl.keyStorePassword.



~$ java -Djavax.net.ssl.keyStore=conf/jmxremote.p12 -Djavax.net.ssl.keyStorePassword=javapwd ...



Du coté du client JMX, on exporte la clé publique, auto-signée, qu&#8217;on importe dans un truststore.



~$ keytool -certreq    -keystore conf/jmxremote.p12 -storepass javapwd                       \
                       -alias jmxremote -keypass javapwd -file temp/jmxremote.cer
~$ keytool -gencert    -keystore conf/jmxremote.p12 -storepass javapwd                       \
                       -alias jmxremote -keypass javapwd -infile temp/jmxremote.cer          \
                       -outfile temp/jmxremote.cert
~$ keytool -importcert -keystore temp/jmxremote.p12 -storepass jmxkey                        \
                       -alias jconsole  -keypass jmxkey  -file temp/jmxremote.cert



Puis pour lancer le client JMX, il faut les propriétés javax.net.ssl.trustStore et javax.net.ssl.trustStorePassword :



~$ jconsole -J-Djavax.net.ssl.trustStore=temp/jmxremote.cert   \
            -J-Djavax.net.ssl.trustStorePassword=jmxkey




Ports

Lorsqu&#8217;on démarre une application avec un accès JMX distant, trois ports sont ouverts :




le port registry, qu&#8217;on a spécifié avec -Dcom.sun.management.jmxremote.port,


le port server, qui est choisi aléatoirement et qui sert à la communication en accès distant,


le port attach, qui sert à la communication en accès local.




Ce port server aléatoire peut poser des problèmes s&#8217;il faut passer par un firewall.
Pour contourner ce problème, il est possible de fixer la valeur de port avec com.sun.management.jmxremote.rmi.port=&lt;port&gt;.
On peut même utilser la même valeur que pour le port registry.



-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.rmi.port=9999



Le port attach est aussi aléatoire, mais ça ne pose pas de problème puisqu&#8217;il ne sert qu&#8217;en accès local.



Adresse IP

Par défaut, les trois ports sont en binding sur toutes les interfaces réseau.



~$ ss -pln | grep &lt;pid&gt;

tcp   LISTEN 0   50  :9999   *:  users:(("java",pid=131652,fd=13))
tcp   LISTEN 0   50  :9998   *:  users:(("java",pid=131652,fd=11))
tcp   LISTEN 0   50  :38613  *:  users:(("java",pid=131652,fd=15))



Avec -Dcom.sun.management.jmxremote.host=localhost, les ports registry et server passent en binding sur l&#8217;interface locale.



~$ ss -pln | grep &lt;pid&gt;

tcp   LISTEN 0   50  [::ffff:127.0.0.1]:9999  :  users:(("java",pid=131652,fd=13))
tcp   LISTEN 0   50  [::ffff:127.0.0.1]:9998  :  users:(("java",pid=131652,fd=11))
tcp   LISTEN 0   50  :38613                  *:  users:(("java",pid=131652,fd=15))




Host name

C&#8217;est une bizarrerie du protocole, mais la connexion avec le port de registry et la communication via le port server ne se font pas forcément avec la même adresse IP et le même hostname.
La connexion se fait sur l&#8217;adresse demandée par le client puis le serveur envoie son hostname et son port server au client pour établir la connxion de communication.
Le hostname envoyé est le même que celui rendu par la commande hostname.


Ça peut poser problème losque cette commande renvoie un nom privé, connu uniquement de la machine elle-même, alors que les clients se connectent avec un hostname public.
Pour contourner ce problème, on peut forcer l&#8217;adresse qui sera utilisée pour la communication avec java.rmi.server.hostname=&lt;public-hostname&gt;.





WildFly


Concernant les vieux JBoss, le sujet est déjà traité dans la page dédiée au monitoring JMX dans JBoss 4 ou 5.
A partir de JBoss AS 7, ou de JBoss EAP 6, le choses ont radicalement changé.
Ces changements sont toujours valables dans WildFly.


Le changement est radical, puisqu&#8217;on n&#8217;utilise plus rien de l&#8217;accès JMX remote.
A la place des ports cités ci-dessus, on réutilise le port d&#8217;administration (9990), avec un protocole maison.
D&#8217;un point de vue réseau, ça simplifie les choses, mais ça impose des adaptations du coté du client JMX.


Tout d&#8217;abord, le client doit enrichir son classpath avec des fichiers jar supplémentaires.
Cette opération est faite par le script bin/jconsole.sh, qui peut être adapté pour un autre client.



~$ jconsole -J-Djava.class.path=$JAVA_HOME/lib/jconsole.jar:$JAVA_HOME/lib/tools.jar;$JBOSS_HOME/bin/client/jboss-cli-client.jar



ou



~$ visualvm -cp:a $JBOSS_HOME/bin/client/jboss-cli-client.jar



Ensuite, comme le protocole de communication n&#8217;est pas du RMI standard, l&#8217;adresse de connexion est plus compliquée :




service:jmx:remote+http://myserver:9990




L&#8217;authentification est la même que pour l&#8217;accès par jboss-cli ou par la console web.




Synthèse


Avec toutes les propriétés qu&#8217;on vient de voir, la ligne de commande pour démarrer une application peut être longue, avec beaucoup de -D.
Par exemple, pour démarrer Tomcat avec JMX en mode développement, je mets ça dans mon fichier setenv.sh :



# Network
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=9999"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.rmi.port=9999"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.host=localhost"
# TLS
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"
# Authentication
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=false"



Et pour démarrer Tomcat avec JMX en mode sécurisé, mon fichier bin/setenv.sh devient :



JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.config.file=conf/jmxremote.properties"
# Cette propriété n'est pas dans la famille com.sun.management, elle ne va donc pas dans le fichier
JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=vaurion.jtips.info"



J&#8217;ai extrait la configuration spécifique à jmxremote dans un fichier jmxremote.properties.



# conf/jmxremote.properties
# Réseau
com.sun.management.jmxremote.port=9999
com.sun.management.jmxremote.rmi.port=9998
# TLS
com.sun.management.jmxremote.ssl.config.file=conf/jmxremote-tls.properties
# Authentication
com.sun.management.jmxremote.password.file=conf/jmxremote-pwd.properties
com.sun.management.jmxremote.access.file=conf/jmxremote-access.properties




# conf/jmxremote-tls.properties
javax.net.ssl.keyStore=conf/jmxremote.p12
javax.net.ssl.keyStorePassword=javapwd



</description>
          <pubDate>2015-06-25T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JMX/Remote</link>
          <guid isPermaLink="true">https://www.jtips.info/JMX/Remote</guid>
        </item>
      
    
      
        <item>
          <title>Installer et configurer mod_cluster</title>
          <description>
Installation (Apache Web Server 2.2)


Télécharger mod_cluster sur http://mod-cluster.jboss.org/downloads/


Sur la page de téléchargement, sont mélangés trois types de distributions :




java bundles : inutile pour WildFly puisque cette partie y est déjà intégrée


mod_cluster native bundles with httpd : c&#8217;est un Apache Web Server avec le module mod_cluster intégré ; pourquoi pas mais ce n&#8217;est pas le sujet ici


mod_cluster modules for httpd : c&#8217;est le module mod_cluster dans sa plus simple forme




Nous allons donc utiliser le module mod_cluster que nous intégrerons dans un Apache Web Server déjà installé. Avant de télécharger, vérifiez bien lequel correspond à votre couple OS / httpd ; pour mod_cluster 12.x, seul Apache Web Server 2.2 est supporté ; ça ne fonctionne pas avec Apache 2.4 (testé avec mod_cluster 1.2.6) . L&#8217;archive contient 4 fichiers .so qu&#8217;on copie dans le répertoire des modules de Apache Web Server.


Il faut charger les quatre modules (fichier httpd.conf) :



LoadModule slotmem_module modules/mod_slotmem.so
LoadModule manager_module modules/mod_manager.so
LoadModule proxy_cluster_module modules/mod_proxy_cluster.so
LoadModule advertise_module modules/mod_advertise.so



De plus, mod_proxy doit être activé, avec le transport AJP :



LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_ajp_module modules/mod_proxy_ajp.so





Installation (Apache Web Server 2.4)


Les distributions binaires de mod_cluster 1.2 fonctionnent uniquement avec Apache Web Server 2.2. Si vous voulez utiliser Apache Web Server 2.4, vous devrez compiler mod_cluster vous-même. L&#8217;exemple ci-dessous fonctionne avec Ubuntu 14.04.


On commence par installer Apache et quelques prérequis :



apt-get update
apt-get install -y autoconf libtool apache2 git
apt-get remove apache2-threaded-dev
apt-get install apache2-prefork-dev



Puis on récupère le code source :



git clone link:https://github.com/modcluster/mod_cluster.git[https://github.com/modcluster/mod_cluster.git]
cd mod_cluster



Puis on compile les modules un à un :



cd native/advertise
./buildconf; ./configure --with-apxs=/usr/bin/apxs; make; cp *.so /usr/lib/apache2/modules/
echo "LoadModule advertise_module /usr/lib/apache2/modules/mod_advertise.so" &gt;&gt; /etc/apache2/mods-available/proxy_cluster.load




cd ../manager
./buildconf; ./configure --with-apxs=/usr/bin/apxs; make; cp *.so /usr/lib/apache2/modules/
echo "LoadModule manager_module /usr/lib/apache2/modules/mod_manager.so" &gt;&gt; /etc/apache2/mods-available/proxy_cluster.load




cd ../mod_proxy_cluster
./buildconf; ./configure --with-apxs=/usr/bin/apxs; make; cp *.so /usr/lib/apache2/modules/
echo "LoadModule proxy_cluster_module /usr/lib/apache2/modules/mod_proxy_cluster.so" &gt;&gt; /etc/apache2/mods-available/proxy_cluster.load




cd ../mod_cluster_slotmem
./buildconf; ./configure --with-apxs=/usr/bin/apxs; make; cp *.so /usr/lib/apache2/modules/
echo "LoadModule slotmem_module /usr/lib/apache2/modules/mod_cluster_slotmem.so" &gt;&gt; /etc/apache2/mods-available/proxy_cluster.load



On charge les modules :



ln -s /etc/apache2/mods-available/proxy.load /etc/apache2/mods-enabled/
ln -s /etc/apache2/mods-available/proxy_ajp.load /etc/apache2/mods-enabled/
ln -s /etc/apache2/mods-available/proxy_cluster.load /etc/apache2/mods-enabled/



Et comme avec Apache 2.2, mod_proxy doit être activé, avec le transport AJP :



LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_ajp_module modules/mod_proxy_ajp.so





Configuration


Le but est de faire communiquer le mod_cluster avec WildFly. On va le faire sur le port 6666. Dans l&#8217;exemple ci-dessous, WildFly et Apache Web Server sont sur la même machine et communiquent en localhost.


On va placer la configuration dans un fichier httpd-cluster.conf, qui devra être en include dans le fichier de configuration principal httpd.conf.



Listen 127.0.0.1:6666
&lt;VirtualHost 127.0.0.1:6666&gt;

  &lt;Location /&gt;
    Order deny,allow
    Deny from all
    Allow from 127.0.0.1
  &lt;/Location&gt;

  KeepAliveTimeout 60
  MaxKeepAliveRequests 0

  ManagerBalancerName mycluster
  ServerAdvertise On
  EnableMCPMReceive

&lt;/VirtualHost&gt;
MemManagerFile /var/log/apache2/



La dernière ligne est utile dans certaines configuration d&#8217;Apache pour le stockage de certains fichiers, comme la mémoire partagée ou de locks. Par défaut, ces fichiers sont placés dans $server_root/logs/ ; par exemple, sur Ubuntu 14.04, Apache ne démarre pas parce que ce répertoire est /etc/apache2/logs/ et qu&#8217;il n&#8217;existe pas.


Activer le gestionnaire (pour Apache 2.2)



&lt;Location /cluster-manager&gt;
  SetHandler mod_cluster-manager
  Order deny,allow
  Deny from all
  Allow from 127.0.0.1
&lt;/Location&gt;



Activer le gestionnaire (pour Apache 2.4)



&lt;Location /cluster-manager&gt;
  SetHandler mod_cluster-manager
  Require ip 127.0.0.1
&lt;/Location&gt;





WildFly


Le module mod_cluster peut être utilisé avec JBoss AS, en version 7 ou plus ancienne, ou avec Tomcat. Avec WildFly 8, la configuration est très simple. Il suffit de le lancer dans un profil HA, avec un binding réseau plus large que celui par défaut. En standalone, on peut le lancer comme ça :



bin/standalone.sh -c standalone-ha.xml -b 0.0.0.0





Vérifications


Si tout va bien, on devrait voir apparaître une trace de ce type dans les logs de WildFly :



08:21:56,960 INFO  [org.jboss.modcluster] (UndertowEventHandlerAdapter - 1) MODCLUSTER000012: default-server connector will use /127.0.0.1



Et en consultant la page du cluster-manager (http://localhost/cluster-manager), on devrait voir apparaître le node dans la liste.


</description>
          <pubDate>2015-04-25T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Apache/mod_cluster</link>
          <guid isPermaLink="true">https://www.jtips.info/Apache/mod_cluster</guid>
        </item>
      
    
      
        <item>
          <title>Monitoring de Java par SNMP</title>
          <description>
Le support natif de SNMP est arrivé avec le JDK 5 d&#8217;Oracle, en même temps que les autres outils de gestion de la JVM comme jconsole, jps ou jstat. Cet article a été rédigé (et testé) avec le JRE/JDK 8 d&#8217;Oracle, mais il devrait être applicable à toutes les versions depuis la 5, à quelques détails près. En revanche, il ne s&#8217;applique pas à OpenJDK qui n&#8217;a pas le support de SNMP.


D&#8217;autres solutions, plus souples, peuvent être utilisées pour publier des informations depuis Java en SNMP, mais elles ne sont pas traitées dans cet article. Seule la technique de publication native à la JVM d&#8217;Oracle est abordée.


Configuration Java


Options de la JVM

La JVM d&#8217;Oracle offre la possibilité de publier des informations sur son fonctionnement par SNMP (en version 2c). Le support de SNMP est activé avec des propriétés système qui ressemblent beaucoup à celles pour JMX.




Définir le port





-Dcom.sun.management.snmp.port=8161





Définir le binding IP





-Dcom.sun.management.snmp.interface=0.0.0.0





Désactiver l&#8217;utilisation d&#8217;ACL





-Dcom.sun.management.snmp.acl=false





OU définir le fichier ACL





-Dcom.sun.management.snmp.acl.file=snmp.acl



Le fichier ACL contient une portion acl et une portion trap. Dans la portion acl, on définit d&#8217;abord les communities qui serviront aux clients pour se connecter. Pour chaque community, on donne un droit d&#8217;accès : read-only ou read-write. Enfin, on précise les managers, sous la forme d&#8217;une liste  de hostnames, d&#8217;adresses IP ou de masques IP, qui auront le droit d&#8217;accéder.



acl = {
 {
   communities = monitor, tomcat
   access = read-only
   managers = localhost
 }
 {
   communities = admin
   access = read-write
   managers = localhost
 }
}



Ce fichier ne doit être accessible que par l&#8217;utilisateur qui exécute le process Java, en lecture et éventuellement en écriture :



chmod 400 snmp.acl



Avec ces options, le process Java ouvre le port 8161, en UDP. Vous pouvez le vérifier avec netstat :



netstat -pln | grep 8161



Attention, tout ce qui a été décrit ici ne marche pas avec OpenJDK. La partie SNMP du JDK d&#8217;Oracle n&#8217;a pas été portée dans OpenJDK.



Intégration dans Tomcat

Dans Tomcat, on utilise le fichier $CATALINA_HOME/bin/setenv.sh pour renseigner les propriétés système, en les mettant dans la variable d&#8217;environnement CATALINA_OPTS.


Ce fichier peut prendre la forme suivante :



CATALINA_OPTS="-Dcom.sun.management.snmp.port=8161 -Dcom.sun.management.snmp.acl.file=$CATALINA_HOME/conf/snmp.acl"






Données accessibles


Informations publiées

Le fichier MIB Java définit de façon statique les informations modifiables et celles qui sont consultables. Les informations qui me semblent les plus intéressantes sont les suivantes :




jvmMemoryHeapMaxSize : mémoire heap maximale (-Xmx)


jvmMemoryHeapInitSize : mémoire heap initiale (-Xms)


jvmMemoryHeapCommitted : mémoire heap allouée


jvmMemoryHeapUsed : mémoire heap utilisée


jvmMemoryNonHeapMaxSize : mémoire non-heap maximale


jvmMemoryNonHeapInitSize : mémoire non-heap initiale


jvmMemoryNonHeapCommitted : mémoire non-heap allouée


jvmMemoryNonHeapUsed : mémoire non-heap utilisée


jvmMemGCCount.3 et jvmMemGCCount.4 : nombre de collections


jvmMemGCTimeMs.3 et jvmMemGCTimeMs.4 : temps pris par les collections


jvmMemManagerName.3 et jvmMemManagerName.4 : noms des collecteurs (les suffixes 1 et 2 sont utilisés pour le CodeCache et le Metaspace)


jvmThreadCount : nombre de threads


jvmRTUptimeMs : uptime




On peut aussi avoir des informations sur les différentes zones de la heap (jvmMemPoolName.N, jvmMemPoolInitSize.N, jvmMemPoolUsed.N, jvmMemPoolCommitted.N, jvmMemPoolMaxSize.N), sur les threads (jvmThreadInstName.N, jvmThreadInstCpuTimeNs.N, jvmThreadInstLockName.N,&#8230;&#8203;).



Informations modifiables

Pour savoir quelles données sont modifiables, il faut faire une recherche sur "read-write" dans le MIB. On trouve par exemple la verbosité du GC (jvmMemoryGCVerboseLevel) ou le flag de monitoring de la CPU (jvmThreadCpuTimeMonitoring).





Comment tester ?


Pour interroger la JVM et lui demander ses informations SNMP, j&#8217;ai utilisé les outils Net-SNMP en ligne de commande sur Ubuntu. Pour qu&#8217;ils fonctionnent correctement, il faut les installer et ajouter les fichiers de définitions MIB qui ne sont pas installés par défaut.



sudo apt-get install snmp snmp-mibs-downloader
sudo wget --directory-prefix=/usr/share/snmp/mibs/ http://docs.oracle.com/javase/8/docs/jre/api/management/JVM-MANAGEMENT-MIB.mib



Tester avec snmpwalk

On peut maintenant interroger notre process pour avoir toutes les informations :



snmpwalk -v2c -c tomcat localhost:8161 .



Les deux options passées ici sont obligatoires : -v pour la version de SNMP (2c, ici) et -c pour la community.


Les informations ne sont pas très lisibles car cette commande utilise des identifiants numériques (OID) pour chaque objet, comme par exemple iso.3.6.1.4.1.42.2.145.3.163.1.1.2.110.1.21.2. Pour que ce soit plus lisible et que snmpwalk affiche des identifiants textuels (comme jvmMemGCCount.3) il faut indiquer le MIB à snmpwalk :



snmpwalk -v2c -m JVM-MANAGEMENT-MIB -c tomcat localhost:8161 .



Pour interroger une ressource précise, on remplace le '.' par un identifiant d&#8217;objet, sous forme numérique ou textuelle.



snmpwalk -v2c -m JVM-MANAGEMENT-MIB -c tomcat localhost:8161 jvmThreadDaemonCount



ou



snmpwalk -v2c -c tomcat localhost:8161 JVM-MANAGEMENT-MIB::jvmThreadDaemonCount



Cette dernière commande donne un résultat qui doit ressemble à ça :



JVM-MANAGEMENT-MIB::jvmThreadDaemonCount.0 = Gauge32: 12



On retrouve dans cette réponse l&#8217;OID, préfixé par le MIB, avec le type de la valeur (Gauge32) et la valeur elle-même. On peut affiner cette sortie avec l&#8217;option -O pour éliminer l&#8217;identifiant (-Ov), le type (-OQ) ou l&#8217;unité (-OU). La commande suivante affiche uniquement la valeur :



snmpwalk -v2c -OvUQ -c tomcat localhost:8161 JVM-MANAGEMENT-MIB::jvmThreadDaemonCount




Tester avec snmpget

snmpget a une syntaxe et un fonctionnement très proches de snmpwalk. La différence entre les deux réside dans la capacité à obtenir une liste d&#8217;objets. Par exemple les arguments passés à la JVM ont tous un OID qui commence par .1.3.6.1.4.1.42.2.145.3.163.1.1.4.20.1.2. Avec snmpwalk, en interrogeant ce nœud, on les obtient tous.



snmpwalk -v2c -m JVM-MANAGEMENT-MIB -c tomcat localhost:8161 .1.3.6.1.4.1.42.2.145.3.163.1.1.4.20.1.2



En revanche, on ne peut pas utiliser snmpget pour ce nœud. Il ne fonctionne que sur les objets de terminaison.



snmpget -v2c -m JVM-MANAGEMENT-MIB -c tomcat localhost:8161 .1.3.6.1.4.1.42.2.145.3.163.1.1.4.20.1.2.1
snmpget -v2c -m JVM-MANAGEMENT-MIB -c tomcat localhost:8161 .1.3.6.1.4.1.42.2.145.3.163.1.1.4.20.1.2.2
snmpget -v2c -m JVM-MANAGEMENT-MIB -c tomcat localhost:8161 .1.3.6.1.4.1.42.2.145.3.163.1.1.4.20.1.2.3
...




Tester avec snmpset

Pour que la commande snmpset fonctionne, il faut l&#8217;utiliser avec une community qui a des droits read-write (admin dans l&#8217;exemple d&#8217;ACL ci-dessus). La syntaxe de snmpset ressemble à celle de snmpget, à la différence prêt qu&#8217;après l&#8217;OID, on précise le type de valeur et la valeur qu&#8217;on veut enregistrer.


Par exemple, pour activer les traces des garbage collectors :



snmpset -v2c -m JVM-MANAGEMENT-MIB -c admin localhost:8161 jvmMemoryGCVerboseLevel.0 i 2




</description>
          <pubDate>2014-10-07T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/snmp</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/snmp</guid>
        </item>
      
    
      
        <item>
          <title>Commandes pratiques pour Docker</title>
          <description>
Cette page reprend quelques commandes Docker plutôt usuelles qui ne se trouvent pas directement dans la documentation officielle.
</description>
          <pubDate>2014-08-10T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Docker/Commandes</link>
          <guid isPermaLink="true">https://www.jtips.info/Docker/Commandes</guid>
        </item>
      
    
      
        <item>
          <title>Installer et configurer mod_jk</title>
          <description>
Le module mod_jk est développé dans le cadre du projet Tomcat et est conçu pour faire communiquer un serveur Web Apache avec une instance de Tomcat, avec le protocole AJP. Il peut être aussi être utilisé pour faire communiquer Apache Web Server avec d&#8217;autres serveurs d&#8217;applications Java, comme WildFly, JBoss AS ou Glassfish.


Installation


Contrairement à mod_proxy, son concurrent, ce module n&#8217;est pas inclus dans les distributions Apache.
Il doit donc être installé indépendamment.


Installer le module signifie placer le fichier mod_jk.so dans le répertoire des modules de Apache Web Server.
L&#8217;emplacement de ce répertoire dépend de la distribution qu&#8217;on utilise.



cp mod_jk.so $httpd_home/modules/



Puis il faut déclarer ce module dans httpd.conf.



LoadModule jk_module modules/mod_jk.so



Windows

Le binaire pour Windows peut être téléchargé depuis le site des connecteurs Tomcat.



Linux

Le projet Tomcat ne fournit pas de binaire pour Linux.
Il faut donc le compiler soi-même ou éventuellement utiliser les packages natifs.


Pour Ubuntu (testé avec la version 14.04):



sudo apt-get install libapache2-mod-jk




Mac OS X

Il n&#8217;y a pas de distribution binaire pour Mac OS X.
Il faut donc recompiler mod_jk à partir du code source.


Le script ci-dessous a été testé sous Mac OS X 10.9, avec Xcode installé préalablement, pour mod_jk 1.2.40.



sudo ln -s /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain /Applications/Xcode.app/Contents/Developer/Toolchains/OSX10.9.xctoolchain
wget http://mir2.ovh.net/ftp.apache.org/dist/tomcat/tomcat-connectors/jk/tomcat-connectors-1.2.40-src.tar.gz
tar -zxf tomcat-connectors-1.2.40-src.tar.gz
cd tomcat-connectors-1.2.40-src/native
./configure CFLAGS='-arch x86_64' APXSLDFLAGS='-arch x86_64' --with-apxs=/usr/sbin/apxs
sudo make
sudo make install



Après ça, le binaire est /usr/libexec/apache2/mod_jk.so.


Attention, ce script ne fonctionne qu&#8217;avec l&#8217;instance Apache fournie avec Mac OS.
Si vous utilisez une instance d&#8217;Apache installée par homebrew, il faudra pointer sur le bon apxs:



./configure CFLAGS='-arch x86_64' APXSLDFLAGS='-arch x86_64' --with-apxs=/usr/local/sbin/apxs



Ou si vous utilisez XAMPP, le make ci-dessus ne fonctionne pas.
Ce script ci-dessous fonctionne (avec XAMPP 1.8.3 / Apache 2.4):



./configure CFLAGS='-arch x86_64' APXSLDFLAGS='-arch x86_64' --with-apxs=/Applications/XAMPP/xamppfiles/bin/apxs
cd apache-2.0/
make -f Makefile.apxs install



Attention, si vous faites compiler pour plusieurs cibles, sur la même machine, pensez à nettoyer entre chaque build:



sudo make clean






Configuration


httpd-jkconf[httpd-jk.conf

Pour configurer mod_jk, on travaille dans httpd.conf ou dans un fichier (httpd-jk.conf) qui est inclus dans httpd.conf.
Là aussi, l&#8217;organisation des inclusions dépend de la distribution utilisée.
Certaines distributions incluent tous les fichiers d&#8217;un répertoire prédéfini (extra, mods-enabled,&#8230;&#8203;) ou font des inclusions fichier par fichier.


Dans ce fichier httpd-jk.conf, on fait pointer le module sur le fichier qui configure les workers, on définit les logs et quelques autres paramètres.



   JkWorkersFile extra/workers.properties
   JkLogFile logs/mod_jk.log
   JkLogLevel info
   JkShmFile logs/mod_jk.shm
   ...



Ensuite, on associe des URLs à des workers avec des points de montage JK.



   ...
   JkMount /swmsg tomcat1
   JkMount /swmsg/* tomcat1
   ...



Enfin, on peut définir les URLs des pages d&#8217;administration.



   ...
   &lt;Location /jk-status&gt;
       JkMount jk-status
       Order deny,allow
       Deny from all
       Allow from 127.0.0.1
   &lt;/Location&gt;
   &lt;Location /jk-manager&gt;
       JkMount jk-manager
       Order deny,allow
       Deny from all
       Allow from 127.0.0.1
   &lt;/Location&gt;




workersproperties[workers.properties

Ce fichier permet de définir les workers, c&#8217;est à dire les cibles associées au module.
Il y a trois types de cibles: les workers concrets (instances Tomcat, Wildfly, JBoss,&#8230;&#8203;), les workers d&#8217;administration (status) et les workers virtuels (lb).


La page de suivi peut être en lecture seule ou en lecture/écriture.
Il est d&#8217;ailleurs possible de définir plusieurs workers de type status, chacun aura sa propre URL de montage.



worker.jk-status.type=status
worker.jk-status.read_only=true

worker.jk-status.type=manager
worker.jk-status.read_only=false



Dans le fichier httpd-jk.conf, un point de montage utilise le worker tomcat1.
Il doit donc être défini dans workers.properties, comme un worker de type ajp13, qui accède à une instance Tomcat locale ou distante.



worker.tomcat1.host=127.0.0.1
worker.tomcat1.port=8009
worker.tomcat1.type=ajp13



Enfin, en début de fichier, on fait la liste des workers qui pourront être montés:



worker.list=jk-status,jkmanager,tomcat1






Load balancing


On configure le load balancer dans le fichier workers.properties.
Le balancer est là pour envoyer les requêtes entre différents workers concrets (de type ajp13).



worker.balancer.type=lb
worker.balancer.balance_workers=instance1,instance2

worker.instance1.host=127.0.0.1
worker.instance1.port=8009
worker.instance1.type=ajp13

worker.instance2.host=127.0.0.1
worker.instance2.port=8109
worker.instance2.type=ajp13



Ensuite, on monte le balancer sur l&#8217;URL de l&#8217;application, dans le fichier httpd-jk.conf.



JkMount /swmsg-web balancer
JkMount /swmsg-web/* balancer



Les requêtes seront alors réparties sur les deux instances.


</description>
          <pubDate>2014-07-09T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Apache/mod_jk</link>
          <guid isPermaLink="true">https://www.jtips.info/Apache/mod_jk</guid>
        </item>
      
    
      
        <item>
          <title>Affinité de session avec WildFly et Apache</title>
          <description>
Lorsqu&#8217;on installe un cluster WildFly, l&#8217;accès Web aux différentes instances se fait via un intermédiaire qui est souvent Apache Web Server, avec un module dédié.
Ce module peut être mod_jk, mod_proxy ou mod_cluster.
A partir du moment où une application Web déployée dans le cluster utilise les sessions, elle devient stateful.
Dans ce cas, il est conseillé de mettre en place l&#8217;affinité de session.


L&#8217;affinité de session est le mécanisme qui consiste à faire en sorte que si une session a été démarrée sur une instance pour un utilisateur, cet utilisateur sera toujours renvoyé vers la même instance.
Pour mettre en place cette affinité, il faut généralement configurer WildFly et le module Apache.


Dans les exemples ci-dessous, on déploiera une application swmsg.war sur deux instances sur la même machines. Apache Web Server sera aussi sur la même machine.


WildFly + mod_proxy


Les instances de WildFly peuvent être démarrées dans un n&#8217;importe quel profil, HA ou pas. Ensuite, il faut préciser le node name de l&#8217;instance. Par défaut, c&#8217;est le hostname.



$ bin/standalone.sh -Djboss.node.name=instance1



Pour le second serveur, il faut en plus faire un décalage de ports pour éviter les conflits.



$ bin/standalone.sh -Djboss.node.name=instance2 -Djboss.socket.binding.port-offset=100



Théoriquement, l&#8217;application qu&#8217;on déploie doit être compatible avec le clustering.
Pour cela, il faut que dans le fichier WEB-INF/web.xml, il y ait l&#8217;élément &lt;distributable/&gt;.



&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0"&gt;
   &lt;distributable/&gt;
   ...
&lt;/web-app&gt;



J&#8217;ai dit théoriquement car j&#8217;ai constaté que le suffixe était ajouté même sans que l&#8217;application n&#8217;est pas déclarée &lt;distributable/&gt;.


Enfin, on configure mod_proxy de Apache.
Le fichier exact dans lequel on configure ce module dépend fortement de la distribution et de l&#8217;installation de Apache Web Server.
Pour savoir dans quel fichier et / ou répertoire travailler, relisez la documentation de votre installation.
Dans tous les cas, ce fichier de configuration devra contenir la portion de configuration suivante :



&lt;Proxy balancer://mycluster&gt;
  BalancerMember http://localhost:8080 route=instance1
  BalancerMember http://localhost:8180 route=instance2
&lt;/Proxy&gt;
&lt;Location /swmsg&gt;
  ProxyPass balancer://mycluster/swmsg stickysession=JSESSIONID
&lt;/Location&gt;



Avec cette configuration, chaque WildFly ajoutera son node name en suffixe du cookie de session et quand mod_proxy verra passer un cookie finissant par instance1, il le renverra vers le premier membre.


PS : avec JBoss AS 7, cette configuration fonctionne aussi, ainsi que la propriété -DjvmRoute qui a disparu en WildFly 8.




WildFly + mod_proxy (alternative)


Cette solution a l&#8217;avantage sur la première de pouvoir être utilisée pour un reverse proxy avec n&#8217;importe quelle cible,
WildFly, Glassfish, ou des cibles non-java.
Elle peut même être utilisée dans un environnement hétérogène.
Cette solution générique utilise un cookie de route indépendant de celui de session, dans le mod_proxy.


Dans cet exemple, on crée un cookie ROUTEID qui est utilisé pour



Header add Set-Cookie "ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED
&lt;Proxy balancer://mycluster&gt;
  BalancerMember http://localhost:8080 route=alexis1
  BalancerMember http://localhost:8180 route=alexis2
&lt;/Proxy&gt;
&lt;Location /swmsg-web&gt;
  ProxyPass balancer://mycluster/swmsg-web stickysession=ROUTEID
&lt;/Location&gt;



Il n&#8217;y a rien à faire du coté de WildFly.




WildFly + mod_jk


Le load balancer du mod_jk est en affinité de session, par défaut.
Il n&#8217;y a donc pas grand chose à faire&#8230;&#8203; à part ouvrir le port AJP dans WildFly et configurer le worker lb dans mod_jk.


Commençons par WildFly. Dans la configuration initiale, le port AJP n&#8217;est pas ouvert.
Il faut donc ajouter le listener AJP à Undertow.
On peut par exemple utiliser jboss-cli :



/subsystem=undertow/server=default-server/ajp-listener=default-ajp:add(socket-binding=ajp)



A faire pour chaque instance WildFly.
En utilisant un outil comme netstat, on constate que le port 8009 est ouvert (au décalage de port près).


Du coté de Apache Web Server, on doit installer le module mod_jk et configurer le load balancer.
Même si on n&#8217;a rien configuré pour l&#8217;affinité de session, elle est activée par défaut.


</description>
          <pubDate>2014-06-29T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WildFly/StickySession</link>
          <guid isPermaLink="true">https://www.jtips.info/WildFly/StickySession</guid>
        </item>
      
    
      
        <item>
          <title>MQTT/Mosquitto</title>
          <description>
Mosquitto est le plus connu des brokers MQTT. Il est Open Source et a été transféré dans la fondation Eclipse.


Installation


Mosquitto peut être installé sous Windows, Linux ou MacOS.


Ubuntu

Sous Ubuntu 13.04, Mosquitto peut directement être installé par apt-get, sans ajouter de repository supplémentaire :



sudo apt-get install mosquitto



Mosquitto est installé en service. Il est automatiquement démarré au lancement de la machine.


Dans ce cas, le fichier de configuration est /etc/mosquitto/mosquitto.conf



MacOS

Sous MacOS, j&#8217;ai utilisé homebrew :



brew install mosquitto



Mosquitto n&#8217;est pas automatiquement installé en service, mais comme Homebrew est plutôt bien fait (pas pour tout, mais là oui), il nous explique comment le faire :



ln -sfv /usr/local/opt/mosquitto/*.plist ~/Library/LaunchAgents
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mosquitto.plist



Pour démarrer Mosquitto en mode console sans service :



mosquitto -d -c /usr/local/etc/mosquitto/mosquitto.conf



Dans ce cas, le fichier de configuration est /usr/local/etc/mosquitto/mosquitto.conf. Si on ne précise pas le fichier de configuration avec l&#8217;option -c, aucun fichier n&#8217;est utilisé et ce sont les valeurs par défaut qui sont utilisées.



Windows

Sous Windows, je ne l&#8217;ai jamais installé, et je ne le ferai que sous la contrainte.





Configuration


La configuration se fait dans un fichier .conf qu&#8217;on spécifie au lancement de mosquitto (cf. ci-dessus).


</description>
          <pubDate>2014-04-04T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/MQTT/Mosquitto</link>
          <guid isPermaLink="true">https://www.jtips.info/MQTT/Mosquitto</guid>
        </item>
      
    
      
        <item>
          <title>MQTT/Mosca</title>
          <description>
Mosca est un broker MQTT qui fonctionne sur Node.js.
Il peut être utilisé comme serveur autonome ou embarqué dans une application.


Installation


Le premier prérequis est l&#8217;installation de Node.js. Après ça, on peut utiliser l&#8217;utilitaire npm:



npm install mosca bunyan -g





Configuration



</description>
          <pubDate>2014-04-04T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/MQTT/Mosca</link>
          <guid isPermaLink="true">https://www.jtips.info/MQTT/Mosca</guid>
        </item>
      
    
      
        <item>
          <title>Maven/Test</title>
          <description>
Lorsqu&#8217;on parle de tests, on peut distinguer plusieurs ensembles. Ici on se concentre ceux qui concernent directement le développeur, soit les tests unitaires et les tests d&#8217;intégration.


Définitions


Test Unitaire

Dans un test unitaires, on vérifie le comportement d&#8217;une méthode, ou d&#8217;une classe, indépendamment de son environnement. Pour qu&#8217;un test soit réellement unitaire, il faut isoler la classe, ce qui se fait avec des Mock Objects.


Ce sont les tests les plus répandus. Pour pratiquement chaque classe qui sera déployée, on fait une classe de test unitaire. Et ces tests peuvent être exécutés très souvent parce qu&#8217;ils sont rapides.



Test d&#8217;Intégration

Dans un test d&#8217;intégration, on teste une partie du logiciel dans son environnement. La définition est vague. De ce fait, cet ensemble englobe plusieurs sortes de tests : tests de classes, tests d&#8217;interfaces graphiques,&#8230;&#8203;


Ces tests sont plus lents à s&#8217;exécuter, on les joue donc moins systématiquement.





Maven


Surefire

Par défaut dans Maven, les tests sont exécutés par le plugin surefire. Celui-ci exécute les classes du répertoire src/test/java qui commencent ou finissent par "Test" et celle qui finissent par "TestCase". Surefire s&#8217;exécute dans la phase de test, entre la compilation et le packaging. Surefire est activé par défaut, il n&#8217;y a donc rien à faire pour l&#8217;utiliser.


Ce plugin est donc parfait pour les tests unitaires.



Failsafe

Failsafe est un fork de Surefire adapté pour les tests d&#8217;intégration. Il s&#8217;exécute dans la phase integration-test, donc après le packaging, et exécute par défaut les classes qui commencent ou finissent par "IT" et celle qui finissent par "ITCase". L&#8217;autre différence avec surefire, c&#8217;est qu&#8217;il prend en compte une phase de préparation (pre-integration-test) et une phase de nettoyage (post-integration-test). Ces phases servent typiquement à démarrer et arrêter un DB, un serveur Web, un serveur mail&#8230;&#8203;



Surefire pour les tests d&#8217;intégration

Avec surefire, on peut ré-inclure les classes *IT. Dans ce cas, les tests unitaires et d&#8217;intégration s&#8217;exécuteront dans phase de test.





Initialisation


Pour mieux gérer l&#8217;initialisation des tests, et la rendre indépendante de Maven, il y a aussi des solutions. On peut gérer des ressources (web server, mail server) sous forme d&#8217;attributs statiques d&#8217;une classe dédiée et leur initialisation est déclenchée par les tests plutôt que par maven. Pour que les ressources soient instanciées une fois pour toutes, on exécute les tests dans une suite, et c&#8217;est la suite qui initialise les ressources.


Pour que les tests puissent rester indépendants, chaque test vérifie que les ressources dont il a besoin sont initialisées.




Références


Livres

Integration Testing from the Trenches de Nicolas Fränkel



</description>
          <pubDate>2014-04-02T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Maven/Test</link>
          <guid isPermaLink="true">https://www.jtips.info/Maven/Test</guid>
        </item>
      
    
      
        <item>
          <title>Références WildFly</title>
          <description>
Quelques sites qui parlent de WildFly :




https://www.mastertheboss.com/




Quelques sites qui parlent de JBoss AS 7 et/ou d&#8217;anciennes versions de WildFly :




https://jaitechwriteups.blogspot.com/search/label/wildfly


http://badr-elhouari.blogspot.com/search/label/Jboss%20AS7


</description>
          <pubDate>2014-02-12T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WildFly/References</link>
          <guid isPermaLink="true">https://www.jtips.info/WildFly/References</guid>
        </item>
      
    
      
        <item>
          <title>Démarrer WildFly</title>
          <description>
Les scripts fonctionnent sur un système normal (Linux, MacOS,&#8230;&#8203;).
Si vous utilisez une système d&#8217;exploitation exotique (Windows), il faut remplacer les extensions .sh par .bat et les extensions .conf par .conf.bat.


Démarrage


Mode autonome

Démarrer WildFly en mode autonome, dans le profil par défaut :



$ bin/standalone.sh



Pour changer de profil, il y a plusieurs possibilités :



# standalone.conf
JAVA_OPTS="$JAVA_OPTS -Djboss.server.default.config=standalone-full.xml"



ou



$ bin/standalone.sh --server-config=standalone-full.xml



ou encore plus simplement



$ bin/standalone.sh -c standalone-full.xml




Mode haute disponibilité

Il existe deux exemples de configuration en mode haute disponibilité (ou plus simplement en mode maître - esclave) qui permet à une instance de reprendre là où s&#8217;est arrêtée l&#8217;autre.
Il faudra par conte lancer les deux instances avec un décalage de ports afin d&#8217;éviter les conflits ainsi que le nom du noeud.



$ bin/standalone.sh -c standalone-full-ha.xml -Djboss.node.name=nodeA
$ bin/standalone.sh -c standalone-full-ha.xml -Djboss.socket.binding.port-offset=500 -Djboss.node.name=nodeB



Dans l&#8217;exemple ci-dessus on a décalé les ports du second noeud de 500, ainsi le port http 8080 se retrouve en 8580.



Mode domaine

Démarrer WildFly en mode domaine, dans le profil par défaut :



$ bin/domain.sh



Pour changer de profil en domaine, il y a faut modifier le fichier domain.xml.





Références




Getting Started Guide




</description>
          <pubDate>2014-02-12T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WildFly/GettingStarted</link>
          <guid isPermaLink="true">https://www.jtips.info/WildFly/GettingStarted</guid>
        </item>
      
    
      
        <item>
          <title>MySQL/metrics</title>
          <description>
Métriques sur les tables : nombre de lignes, taille de données et des indexes.



SELECT concat(table_schema,'.',table_name),
       table_rows Rows,
       round(data_length/(1024*1024),2) "Data (MB)",
       round(index_length/(1024*1024),2) "Index (MB)",
       round((data_length+index_length)/(1024*1024),2) "Total size (MB)"
FROM information_schema.TABLES
ORDER BY data_length+index_length DESC;



Cumul



SELECT table_schema,
       round(sum(data_length)/(1024*1024),0) "Data (MB)",
       round(sum(index_length)/(1024*1024),0) "Index (MB)",
       round(sum(data_length+index_length)/(1024*1024),0) "Total size (MB)"
FROM information_schema.tables
GROUP BY table_schema;



Pour avoir le nombre de requêtes total ou par seconde :



mysql&gt; status



Renvoie



Threads: 14  Questions: 65212688  Slow queries: 26  Opens: 10849  Flush tables: 159  Open tables: 60  Queries per second avg: 15.048



Autre solution, avec une requête :



SELECT uptime.variable_value,
       queries.variable_value,
       round(queries.variable_value/uptime.variable_value,2)
FROM information_schema.SESSION_STATUS as queries,
     information_schema.SESSION_STATUS as uptime
WHERE uptime.variable_name='UPTIME'
  AND queries.variable_name='QUERIES';

</description>
          <pubDate>2013-11-19T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/MySQL/metrics</link>
          <guid isPermaLink="true">https://www.jtips.info/MySQL/metrics</guid>
        </item>
      
    
      
        <item>
          <title>Java/OOME</title>
          <description>
Différentes messages associé aux OutOfMemoryError :




Heap Space : impossible de créer un nouvel objet dans la mémoire heap


Perm Space : impossible de créer un nouvel objet dans la mémoire perm


GC overhead limit exceeded : une collection (parallèle ou concurrente) dure trop longtemps ; généralement dû à une saturation de la heap ; cette erreur peut être supprimée avec l&#8217;argument -XX:-UseGCOverheadLimit (cf. http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html#par_gc.oom)


</description>
          <pubDate>2013-10-01T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/OOME</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/OOME</guid>
        </item>
      
    
      
        <item>
          <title>JMS dans WildFly</title>
          <description>
Dans WildFly, les fonctionnalités de messaging JMS sont prises en charge par ActiveMQ Artemis.
De ce fait, la configuration du subsystem messaging-activemq a pas mal de similitudes avec celle d&#8217;un serveur ActiveMQ Artemis autonome.


La fonctionnalité de messaging n&#8217;est pas présente dans la configuration par défaut de WildFly.
Pour l&#8217;avoir, il faut utiliser le fichier de configuration standalone-full.xml ou le profil full, en mode domaine.


Il est aussi possible d&#8217;ajouter la fonctionnalité de messaging au profil par défaut en ajoutant l&#8217;extension org.wildfly.extension.messaging-activemq, le subsystem urn:jboss:domain:messaging-activemq.


Toute la configuration JMS se fait dans ce subsystem messaging-activemq.
Plus précisément, elle se fait dans son élément enfant &lt;server&gt;.


Destinations


Le terme destination est générique aux queues et topics.
Les destinations sont dans des éléments &lt;jms-queue&gt; et &lt;jms-topic&gt;.
Pour chaque destination, la configuration minimal est constituée d&#8217;un nom de destination et d&#8217;au moins une entrée JNDI.



&lt;server name="default"&gt;
  &lt;!-- ... --&gt;
  &lt;jms-queue name="SWq" entries="java:/jms/queue/SwQueue"/&gt;
  &lt;jms-topic name="SWt" entries="java:/jms/topic/SwTopic"/&gt;
  &lt;!-- ... --&gt;
&lt;/server&gt;



La queue et le topic peuvent aussi être déclarés par jboss-cli :



[host:9990 /] /subsystem=messaging-activemq/server=default/jms-queue=SWq:add(entries=[java:/jms/queue/SwQueue])

[host:9990 /] /subsystem=messaging/hornetq-server=default/jms-topic=SWt:add(entries=[java:/jms/topic/SwTopic])



On notera que la queue et le topic configurés ci-dessus ne sont pas accessible à distance puisqu&#8217;ils n&#8217;ont pas d&#8217;entrée JNDI dans l&#8217;espace de nommage java:jboss/exported/.




Sécurité


Accès distant

Pour accéder à une destination, un client externe à WildFly passe par JNDI via un accès distant.
Cet accès est sécurisé par l'`ApplicationRealm`.



Permissions par destinations

L&#8217;ajout de permissions à une destination ou un ensemble de destinations se fait par des security-settings.



   &lt;security-settings&gt;
      &lt;security-setting match="jms.queue.SWq"&gt;
          &lt;role name="swuser" consume="true" send="true" /&gt;
      &lt;/security-setting&gt;
   &lt;/security-settings&gt;



Un security-setting avec match="#" s&#8217;applique à toutes les destinations.
Un security-setting avec match="jms.queue.SWq" ne s&#8217;applique qu&#8217;à la queue JMS dont le nom est SWq.


La liste des permissions est la suivante :




send


consume


createDurableQueue


deleteDurableQueue


createNonDurableQueue


deleteNonDurableQueue


manage




La même configuration par cli :



[host:9990 /] /subsystem=messaging-activemq/server=default/security-setting="jms.queue.SWq":add
[host:9990 /] /subsystem=messaging-activemq/server=default/security-setting="jms.queue.SWq"/role=swuser  \
                    :add(send=true, consume=true)



Attention, l&#8217;authentification se fait par l&#8217;appel de la méthode connectionFactory.getConnection(username, password).
Il n&#8217;y a aucune propagation automatique lorsqu&#8217;on appelle connectionFactory.getConnection().





Destinations de gestion


Dans les destinations de gestion, je regroupe la dead letter queue qui reçoit les messages qui n&#8217;ont pas pu être consommés et l'expiry queue qui reçoit les messages dont la date d&#8217;expiration est dépassée.



  &lt;address-setting name="#"
                   dead-letter-address="jms.queue.DLQ"
                   expiry-address="jms.queue.ExpiryQueue" /&gt;



Expiry Queue

La file d&#8217;expiration reçoit les messages qui ont expiré.
Cela peut être dû à une durée de vie limité fixée par le producteur, avant l&#8217;envoi :



producer.setTimeToLive(10000L);



L&#8217;expiration peut aussi être provoquée par un script jboss-cli, pour tous les messages d&#8217;une destination :



[host:9990 /] /subsystem=messaging-activemq/server=default/jms-queue=SwQueue:expire-messages



Pour un unique message d&#8217;une destination, identifié par son JMSMessageID :



[host:9990 /] /subsystem=messaging-activemq/server=default/jms-queue=SwQueue                      \
                    :expire-message(message-id=ID:f0671793-faec-11e2-9d0d-2d43143133bb)




Dead Letter Queue

La file de rebuts reçoit les messages qui n&#8217;ont pas pu être délivrés, ou plus précisément ceux qui n&#8217;ont pas pu être livrés après un nombre d&#8217;échecs égal au paramètre max-delivery-attempts.
C&#8217;est le cas par exemple pour des messages consommés dans le cadre de transactions annulées (rollback).


La valeur par défaut de ce paramètre est 10, mais il peut être modifié :



[host:9990 /] /subsystem=messaging-activemq/server=default/address-setting=#                      \
                    :write-attribute(name=max-delivery-attempts, value=5)



Il est aussi possible de forcer l&#8217;envoi de tous les messages d&#8217;une destination dans la DLQ :



[host:9990 /] /subsystem=messaging-activemq/server=default/jms-queue=SWq                          \
                    :send-messages-to-dead-letter-address()



Pour un unique message d&#8217;une destination, identifié par son JMSMessageID :



[host:9990 /] /subsystem=messaging-activemq/hornetq=default/jms-queue=SWq                         \
                    :send-message-to-dead-letter-address(message-id=ID:6c28635a-faef-11e2-9d0d)




</description>
          <pubDate>2013-07-26T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WildFly/JMS</link>
          <guid isPermaLink="true">https://www.jtips.info/WildFly/JMS</guid>
        </item>
      
    
      
        <item>
          <title>Protéger les mots de passe de WildFly dans un caveau</title>
          <description>
La technique de Vault permet de ne plus afficher les mots de passe, ou autres chaînes de caractères sensibles, dans les fichiers de configuration de WildFly.


Cette fonctionnalité a été introduite en JBoss AS 7.1, puis a été abandonnée dans elytron.
De ce fait, elle n&#8217;est plus utilisable dans WildFly 25.


Les commandes décrites dans cette page ont été testées avec OpenJDK 11 et WildFly 23.


Mise en oeuvre


keystore

Pour créer le keystore, on utilise l&#8217;outil keytool fourni avec le JDK.
On crée le magasin et on y inclut directement l&#8217;alias vault qu&#8217;on utilisera plus tard.



~# keytool -genseckey -storetype jceks -keyalg AES -keysize 128               \
           -keystore standalone/configuration/vault.ks -storepass vaultpwd    \
           -alias vault -keypass vaultpwd




vault

Le script vault.sh peut être utilisé de façon interactive ou avec des paramètres.
Le mode non interactif, avec paramètres, n&#8217;est pas documenté.
Mais vault.sh --help apporte les informations nécessaires.


Pour initialiser notre caveau, il faut indiquer




les informations sur le keystore (fichier, alias du caveau, mot de passe),


le répertoire qui contiendra les fichiers chiffrés,


les informations de salage du mot de passe (chaine et nombre d&#8217;itérations).




On y stocke directement des informations sensibles, avec le bloc, le nom et la valeur de l&#8217;attribut.



~# bin/vault.sh --keystore standalone/configuration/vault.ks                  \
                --keystore-password vaultpwd                                  \
                --alias vault --enc-dir standalone/data/vault                 \
                --iteration 12 --salt azertyui                                \
                --vault-block SewaDS --attribute password --sec-attr sapwd



Le première parte du résultat est le contenu du caveau, avec la chaine à utiliser à la place du mot de passe :



********************************************
Vault Block:SewaDS
Attribute Name:password
Configuration should be done as follows:
VAULT::SewaDS::password::1
********************************************



La seconde partie du résultat est la commande CLI à passer.



configuration

Cette configuration de caveau peut maintenant être intégrée.



[9990 /] /core-service=vault                                                  \
            :add(vault-options=                                               \
                    [("KEYSTORE_URL"=&gt;"${jboss.server.config.dir}/vault.ks"), \
                     ("KEYSTORE_PASSWORD"=&gt;"MASK-3TYfSWtwSqg4vC2Y56enLw"),    \
                     ("KEYSTORE_ALIAS"=&gt;"vault"),                             \
                     ("SALT"=&gt;"azertyui"),                                    \
                     ("ITERATION_COUNT"=&gt;"12"),                               \
                     ("ENC_FILE_DIR"=&gt;"${jboss.server.data.dir}/vault/")])




utilisation

On peut maintenant utiliser le mot de passe stocké dans le caveau avec la chaine générée, qui commence par VAULT.
Dans mon cas, je voulais l&#8217;utiliser pour une datasource dont la configuration ressemblait à ça :



&lt;datasource ...&gt;
  ...
  &lt;security&gt;
    &lt;user-name&gt;sa&lt;/user-name&gt;
    &lt;password&gt;sa&lt;/password&gt;
  &lt;/security&gt;
&lt;/datasource&gt;



La nouvelle configuration devient :



&lt;datasource ...&gt;
  ...
  &lt;security&gt;
    &lt;user-name&gt;sa&lt;/user-name&gt;
    &lt;password&gt;${VAULT::SewaDS::password::1}&lt;/password&gt;
  &lt;/security&gt;
&lt;/datasource&gt;




</description>
          <pubDate>2013-07-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WildFly/Vault</link>
          <guid isPermaLink="true">https://www.jtips.info/WildFly/Vault</guid>
        </item>
      
    
      
        <item>
          <title>Propriétés système dans WildFly</title>
          <description>
Cette page récapitule les propriétés système les plus courantes.


Il est possible de lire la liste complète des propriétés système depuis jboss-cli :app-name:



/core-service=platform-mbean/type=runtime:read-attribute(name=system-properties)



Standalone


Les propriétés système sont passées au démarrage de WildFly en les mettant dans la variable d&#8217;environnement JAVA_OPTS.
Le fichier standalone.conf est l&#8217;endroit idéal pour ça.


jboss.server.default.config

Fichier de configuration principal



  JAVA_OPTS="$JAVA_OPTS -Djboss.server.default.config=standalone-ha.xml"



La valeur par défaut est ${jboss.server.config.dir}/standalone.xml



Répertoires



jboss.home.dir


jboss.server.base.dir : ${jboss.home.dir}/standalone


jboss.server.config.dir : ${jboss.server.base.dir}/configuration


jboss.server.data.dir : ${jboss.server.base.dir}/data


jboss.server.log.dir : ${jboss.server.base.dir}/log


jboss.server.temp.dir : ${jboss.server.base.dir}/tmp





jboss.bind.address

Adresse IP sur laquelle l&#8217;interface publique de WildFly doit se brancher ;
concerne l&#8217;accès aux applications



  JAVA_OPTS="$JAVA_OPTS -Djboss.bind.address=0.0.0.0"



La valeur par défaut est 127.0.0.1, ce qui signifie que WildFly n&#8217;est accessible qu&#8217;en local.
En mettant 0.0.0.0, WildFly est accessible en local et à distance, via toutes les interfaces réseau du serveur.


Autres adresses IP :




jboss.bind.address.management : pour les accès de management


jboss.bind.address.unsecure : les accès IIOP





jboss.socket.binding.port-offset

Décalage des ports d&#8217;écoute par rapport aux valeurs indiquées dans le fichier de configuration



  JAVA_OPTS="$JAVA_OPTS -Djboss.socket.binding.port-offset=100"



La valeur par défaut est 0.


Certains ports ont leur propre variable :




jboss.http.port


jboss.https.port


jboss.ajp.port


jboss.management.http.port


jboss.management.https.port





jboss.node.name

Nom de l&#8217;instance JBoss dans un cluster



  JAVA_OPTS="$JAVA_OPTS -Djboss.node.name=node0"



La valeur par défaut est le hostname fourni par le système d&#8217;exploitation.


Cette propriété sert aussi de valeur par défaut à l'`instance-id`, utilisé pour l&#8217;affinité de session (en remplacement de jvmRoute).





Domain


Fichiers de configuration

Fichier host :



JAVA_OPTS="$JAVA_OPTS -Djboss.host.default.config=host-slave.xml"



La variante courte est --host-config.


Fichier domain :



JAVA_OPTS="$JAVA_OPTS -Djboss.domain.default.config=domain-bis.xml"




Répertoires



jboss.home.dir


jboss.domain.base.dir : ${jboss.home.dir}/domain


jboss.domain.config.dir : ${jboss.server.base.dir}/configuration


jboss.domain.data.dir : ${jboss.server.base.dir}/data


jboss.domain.log.dir : ${jboss.server.base.dir}/log


jboss.domain.temp.dir : ${jboss.server.base.dir}/tmp







Références




WildFly Admin Guide - System properties




</description>
          <pubDate>2013-06-29T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WildFly/SystemProperty</link>
          <guid isPermaLink="true">https://www.jtips.info/WildFly/SystemProperty</guid>
        </item>
      
    
      
        <item>
          <title>Administration WildFly en ligne de commande</title>
          <description>
L&#8217;outil CLI permet d&#8217;effectuer toutes les tâches d&#8217;administration de WildFly en ligne de commande.










Certaines commandes sont trop longues pour être affichées sur une seule ligne ;
dans ce cas, je les ai affichées sur plusieurs lignes, avec '\' comme signe de continuation.







Premiers pas


Connexion

Pour se connecter au serveur WildFly local, démarré sans port-offset :



~# $JBOSS_HOME/bin/jboss-cli.sh --connect



En réalité, cette commande connecte jboss-cli au serveur définit comme serveur par défaut dans $JBOSS_HOME/bin/jboss-cli.xml.


Pour se connecter à un autre serveur WildFly, avec un port-offset à 100 :



~# $JBOSS_HOME/bin/jboss-cli.sh --connect --controller=myserver:10090



Pour démarrer en mode non-interactif, et exécuter un script :



~# $JBOSS_HOME/bin/jboss-cli.sh --connect --file=myscript.cli




Vocabulaire

Avec CLI, on travaille avec des commandes, des ressources (ou noeuds), des attributs et opérations sur ces ressources.



Aide sur une commande

Pour obtenir de l&#8217;aide sur une commande, il faut taper la commande puis --help. Par exemple :



[host:9990 /] deploy --help



Attention, ça marche bien pour les commandes, mais pas les opérations.



Aide sur une opération

Pour avoir de l&#8217;aide sur une opération, il faut utiliser la commande read-operation, en précisant le noeud et l&#8217;opération.



[host:9990 /] read-operation --node=/core-service=vault add






Commandes en standalone (management)


Cette partie concerne la configuration des accès aux outils de gestions (jboss-cli, web console, APIs).


Accès local

L&#8217;accès en localhost avec jboss-cli peut se faire sans authentification.
Si cette possibilité vous gène, vous pouvez la désactiver :



[host:9990 /] /subsystem=elytron/security-domain=ManagementDomain:list-remove(name=realms, index=1)
[host:9990 /] reload



Après ça, même en localhost, CLI demandera une authentification.


Dans les anciens WildFly, avant elytron :



[host:9990 /] /core-service=management/security-realm=ManagementRealm/authentication=local:remove
[host:9990 /] reload




Gestion des droits

Pour activer le système d&#8217;autorisations RBAC :



[host:9990 /] /core-service=management/access=authorization:write-attribute(name=provider,value=rbac)



Pour revenir au fonctionnement par défaut :



[host:9990 /] /core-service=management/access=authorization:write-attribute(name=provider,value=simple)



Dans ce fonctionnement, pour qu&#8217;un utilisateur ait le droit d&#8217;accéder aux outils de management, il faut lui associer un de rôles prédéfinis.


Il est possible d&#8217;associer un rôle à tous les utilisateurs.



[host:9990 /] /core-service=management/access=authorization/role-mapping=Monitor:add(include-all=true)



On peut aussi associer explicitement des utilisateurs à un rôle.



[host:9990 /] /core-service=management/access=authorization/role-mapping=Administrator/include=alexis       \
                  :add(type=USER, name=alexis)



Enfin, on peut associer des groupes d&#8217;utilisateurs à un rôle.



[host:9990 /] /core-service=management/access=authorization/role-mapping=Administrator/include=app-admin    \
                  :add(type=USER, name=app-admin)



Dans la configuration initiale, les groupes d&#8217;utilisateurs sont définis dans le fichier standalone/configuration/mgmt-groups.properties.



Authentification LDAP

La première étape est de connecter WildFly à l&#8217;annuaire LDAP.



[host:9990 /] /core-service=management/ldap-connection=LdapConnection                             \
                :add(url="ldap://localhost:1389",search-dn="cn=Root ",search-credential="rootpwd")



Pour la deuxième étape, on configure l&#8217;authentification LDAP.



[host:9990 /] /core-service=management/security-realm=LdapRealm:add()
[host:9990 /] /core-service=management/security-realm=LdapRealm/authentication=ldap:add(     \
                  base-dn="ou=admin,ou=people,dc=sewatech,dc=fr",                            \
                  username-attribute="cn",                                                   \
                  connection="LdapConnection")
[host:9990 /] /core-service=management/management-interface=http-interface                   \
                  :write-attribute(name=security-realm,value=LdapRealm)



Maintenant, la liste des utilisateurs ayant accès au management n&#8217;est plus dans le fichier properties, mais dans l&#8217;annuaire LDAP.



Autorisations LDAP

Si on veut affiner les autorisations, on passe en mode RBAC et on peut associer les rôles WildFly aux groupes du LDAP.



[host:9990 /] batch
[host:9990 /] /core-service=management/security-realm=LdapRealm/authorization=ldap:add(connection="LdapConnection")
[host:9990 /] /core-service=management/security-realm=LdapRealm/authorization=ldap/group-search=group-to-principal    \
                  :add(group-name=SIMPLE, group-name-attribute=cn,                                                    \
                       base-dn="ou=admin,ou=groups,dc=sewatech,dc=fr", recursive=true, search-by=DISTINGUISHED_NAME,  \
                       principal-attribute=uniqueMember)
[host:9990 /] run-batch



Ensuite, il faut établir une correspondance entre groupes et rôles.



[host:9990 /] /core-service=management/access=authorization/role-mapping=Administrator:add
[host:9990 /] /core-service=management/access=authorization/role-mapping=Administrator/include=app-admin              \
                  :add(type=GROUP, realm=LdapRealm)




Communications TLS

La première étape est de créer la pair de clés :



~# keytool -genkeypair -alias jboss -keypass jbosskey                                        \
                       -keystore jboss.ks -storepass jbosskey                                \
                       -keyalg RSA -dname "cn=localhost"



Ensuite, on créé le server-identity qui utilise cette clé :



[host:9990 /] /core-service=management/security-realm=ManagementRealm/server-identity=ssl    \
                  :add(alias=jboss,                                                          \
                       key-password=jbosskey,                                                \
                       keystore-password=jbosskey,                                           \
                       keystore-path=jboss.ks,                                               \
                       keystore-provider=JKS,                                                \
                       keystore-relative-to=jboss.server.config.dir)



Enfin, on ouvre l&#8217;accès sécurisé à l&#8217;interface de management :



[host:9990 /] /core-service=management/management-interface=http-interface                   \
                  :write-attribute(name=secure-socket-binding,                               \
                                   value=management-https)









Ces commandes ne sont plus utiles pour utiliser un certificat auto-signé.
WildFly peut générer son propre keystore au premier accès TLS.








Commandes en standalone (profile)


Cette partie concerne la configuration des subsystems de WildFly.


Authentification (legacy)

Créer un security domain avec des fichiers properties (pratique pour le dev) :



[host:9990 /] batch
[host:9990 /] /subsystem=security/security-domain=sw-xxx:add(cache-type=default)
[host:9990 /] /subsystem=security/security-domain=sw-xxx/authentication=classic:add()
[host:9990 /] /subsystem=security/security-domain=sw-xxx/authentication=classic/login-module=UsersRoles       \
                  :add(code=UsersRoles, flag=required,                                                        \
                       module-options={"usersProperties"=&gt;"${jboss.server.config.dir}/sw-users.properties",   \
                                       "rolesProperties"=&gt;"${jboss.server.config.dir}/sw-roles.properties"})
[host:9990 /] run-batch



Les fichiers sw-users.properties et sw-roles.properties doivent être placés dans le répertoire standalone/configuration.


Créer un security domain avec un annuaire LDAP :



[host:9990 /] batch
[host:9990 /] /subsystem=security/security-domain=sw-xxx:add(cache-type=default)
[host:9990 /] /subsystem=security/security-domain=sw-xxx/authentication=classic:add()
[host:9990 /] /subsystem=security/security-domain=sw-xxx/authentication=classic/login-module=LdapExtended   \
                  :add(code=LdapExtended, flag=required,                                                    \
                       module-options={"java.naming.provider.url"=&gt;"ldap://127.0.0.1:1338/",                \
                                       "bindDN"=&gt;"cn=Sewatech", "bindCredential"=&gt;"aa",                     \
                                       "baseCtxDN"=&gt;"ou=people,dc=sewatech,dc=fr",                          \
                                       "baseFilter"=&gt;"(uid={0})",                                           \
                                       "rolesCtxDN"=&gt;"ou=groups,dc=sewatech,dc=fr",                         \
                                       "roleFilter"=&gt;"(uniqueMember={1})", "roleAttributeID"=&gt;"cn"})
[host:9990 /] run-batch



Créer un security domain en identité sécurisée (pour une utilisation depuis une DataSource) :



[host:9990 /] batch
[host:9990 /] /subsystem=security/security-domain=ds-xxx:add(cache-type=default)
[host:9990 /] /subsystem=security/security-domain=ds-xxx/authentication=classic:add()
[host:9990 /] /subsystem=security/security-domain=ds-xxx/authentication=classic/login-module=SecureIdentity        \
                  :add(code=SecureIdentity, flag=required,                                                         \
                       module-options={"principal"=&gt;"sax","userName"=&gt;;"sa","password"=&gt;;"9fdd42c2a7390d3"})
[host:9990 /] run-batch



Le mot de passe chiffré est obtenu avec le script bash suivant :



~# export MODULES_HOME=$JBOSS_HOME/modules/system/layers/base
~# java -cp $MODULES_HOME/org/picketbox/main/*.jar org.picketbox.datasource.security.SecureIdentityLoginModule sa









Ce paragraphe est qualifié de legacy car le sous-système /subsystem=security a progressivement été remplacé par le sous-système /subsystem=elytron.
Les deux ont cohabité entre WildFly 11 et 24, puis seul elytron a été conservé dans WildFly 25.






Communications TLS

La clé TLS peut être la même que pour le management, ou générée de la même façon.


Ensuite, on crée un connector sur le port 8443, qui utilise la clé (placée dans le répertoire configuration de mon WildFly).



[host:9990 /] /core-service=management/security-realm=ApplicationRealm/server-identity=ssl   \
                  :add(alias=server, key-password=password,                                  \
                       keystore-password=password, keystore-path=application.keystore,       \
                       keystore-provider=JKS, keystore-relative-to=jboss.server.config.dir)

[host:9990 /] /subsystem=undertow/server=default-server/https-listener=https                 \
                  :add(socket-binding=https, security-realm=ApplicationRealm)




Communications HTTP/2

Les communications HTTP/2 passent par un listener HTTPS.



[host:9990 /] /subsystem=undertow/server=default-server/https-listener=https                 \
                  :write-attribute(name=enable-http2, value=true)




Datasource

Il y a deux syntaxes possibles pour manipuler les datasources.
Soit on applique des opérations sur le noeud correspondant à la datasource, dans le sous-système /subsystem=datasources, soit on utilise la commande datasource.


Créer une datasource, avec une nom d&#8217;utilisateur et un mot de passe :



[host:9990 /] data-source add --name=MyDS2 --jndi-name="java:jboss/datasources/MyDS2"        \
                              --connection-url="jdbc:mysql://localhost:3306/test"            \
                              --driver-name=h2 --user-name="sa" --password="sa"



ou



[host:9990 /] /subsystem=datasources/data-source=MyDS                                        \
                    :add(jndi-name="java:jboss/datasources/MyDS",                            \
                         connection-url="jdbc:mysql://localhost:3306/test",                  \
                         driver-name="mysql", user-name="sa", password="sa" )



Créer une datasource rattachée à un domaine de sécurité qui fournit le nom d&#8217;utilisateur et le mot de passe
(cf. plus haut, identité sécurisée en mode legacy) :



[host:9990 /] data-source add --name=MyDS2 --jndi-name="java:jboss/datasources/MyDS2"        \
                --connection-url="jdbc:mysql://localhost:3306/test"                          \
                --driver-name=h2 --security-domain="ds-xxx"



La syntaxe avec l&#8217;opération :add, plutôt que la commande datasource add peut aussi être utilisée.


Connaitre la liste des datasources



[host:9990 /] ls /subsystem=datasources/data-source



Activer les statistiques d&#8217;une datasource :



[host:9990 /] /subsystem=datasources/data-source=MyDS                                        \
                    :write-attribute(name=statistics-enabled, value=true)



Lire l&#8217;état du pool de connexion d&#8217;une datasource :



[host:9990 /] /subsystem=datasources/data-source=MyDS/statistics=pool                        \
                    :read-resource(include-runtime=true)




Déploiement

Déployer une application :



[host:9990 /] deploy ~/to-bo-deployed/my-web-app.war



Retirer une application :



[host:9990 /] undeploy my-web-app.war




Messaging

Attention, pour que le messaging fonctionne, il faut avoir démarré WildFly en profil full ou ajouté le subsystem messaging à votre profil.


Ajouter une queue JMS :



[host:9990 /] /subsystem=messaging-activemq/server=default/jms-queue=SWq                     \
                  :add(entries=[java:/queue/SwQueue])



Ajouter une entrée JNDI à un connection factory :



[host:9990 /] /subsystem=messaging-activemq/server=default/connection-factory=RemoteConnectionFactory   \
                  :add-jndi(jndi-binding=java:jboss/exported/jms/ConnectionFactory)



Ajouter une entrée JNDI à une destination :



[host:9990 /] /subsystem=messaging-activemq/server=default/jms-queue=SWq                     \
                  :add-jndi(jndi-binding=java:jboss/exported/jms/SWq)



Désactiver la sécurité JMS :



[host:9990 /] /subsystem=messaging-activemq/server=default                                   \
                  :write-attribute(name=security-enabled, value=false)



Associer un domaine de sécurité à HornetQ :



[host:9990 /] /subsystem=messaging-activemq/server=default                                   \
                  :write-attribute(name=security-domain, value=hornetq)




Logging

Modifier le niveau de logs global :



[host:9990 /] /subsystem=logging/root-logger=ROOT:write-attribute(name=level, value=WARN)









Toutes les opérations spécifiques sur root-logger=ROOT ont été dépréciées.
root-logger=ROOT doit maintenant être traité comme les autres loggers.





Ajouter un handler :



[host:9990 /] /subsystem=logging/periodic-rotating-file-handler=SWFILE                            \
                  :add(file={path=swmsgx.log, relative-to=jboss.server.log.dir},                  \
                       suffix=yyyy-MM-dd,                                                         \
                       formatter="%d %-5p [%c] (%t) %s%e%n")



Ajouter un logger :



[host:9990 /] /subsystem=logging/logger=fr.sewatech:add(level=TRACE)



Associer un logger à un handler :



[host:9990 /] /subsystem=logging/logger=fr.sewatech:add-handler(name=SWFILE)



Récupérer la liste des fichiers de log :



[host:9990 /] ls /subsystem=logging/log-file



Lire un fichier de log complet :



[host:9990 /] /subsystem=logging/log-file=server.log:read-log-file(lines=-1, skip=0)



Suivre un fichier de log (tail) :



[host:9990 /] /subsystem=logging/log-file=server.log:read-log-file(tail=true, lines=10, skip=0)



Désactiver la détection par le logging d&#8217;une configuration embarquée dans l&#8217;application :



[host:9990 /] /subsystem=logging:write-attribute(name=use-deployment-logging-config, value=false)



Désactiver les dépendances implicites vers les modules de logging :



[host:9990 /] /subsystem=logging:write-attribute(name=add-logging-api-dependencies,value=false)




Access Log

Ajouter les logs d&#8217;accès Web :



[host:9990 /] /subsystem=undertow/server=default-server/host=default-host/setting=access-log:add



On peut aussi modifier le pattern des logs, en faisant attention aux espaces et doubles-quotes :



[host:9990 /] cd /subsystem=undertow/server=default-server/host=default-host/setting=access-log
[host:9990 /] :write-attribute(name=pattern, value="%h %l %u %t \"%r\" %s %b %D")



Les logs d&#8217;accès ne sont écrits qu&#8217;à partir du prochain redémarrage (ou rechargement).



Développement (JSP)

En phase de développement, on a pris l&#8217;habitude avec Jetty ou Tomcat de pouvoir modifier à chaud les ressources Web.
Ceci est désactivé par défaut dans WildFly et peut être réactivé avec la commande



[host:9990 /] /subsystem=undertow/servlet-container=default/setting=jsp:write-attribute(name=development, value=true)




Compression HTTP

Pour activer la compression des flux HTTP, il faut ajouter le filtre gzip et le configurer pour qu&#8217;il ne compresse que les flux de type texte.



[host:9990 /] /subsystem=undertow/configuration=filter/gzip=gz:add
[host:9990 /] /subsystem=undertow/server=default-server/host=default-host/filter-ref=gz                                      \
                   :add(predicate=                                                                                           \
                         "exists['%{o,Content-Type}']                                                                        \
                          and regex[pattern='(?:application/javascript|text/css|text/html|text/xml|application/json)(;.*)?', \
                                   value=%{o,Content-Type},                                                                  \
                                   full-match=true]")



Il faut aussi ajouter le header Vary pour informer les intermédiaires comme des reverse proxies.



[host:9990 /] /subsystem=undertow/configuration=filter/response-header=vary:add(header-name=Vary, \
                                               header-value=Accept-Encoding)
[host:9990 /] /subsystem=undertow/server=default-server/host=default-host/filter-ref=vary:add




Supprimer IIOP

Pour déployer une application qui utilise JMS, il faut le subsystem messaging-activemq, et pour ça le plus simple est de démarrer en profil full.


Malheureusement le profil full active aussi IIOP qui est rarement utile et qui ouvre un port supplémentaire.
Le script suivant permet de supprimer IIOP du profil full.



[host:9990 /] /subsystem=ejb3/service=iiop:remove
[host:9990 /] /subsystem=iiop-openjdk:remove

[host:9990 /] /extension=org.wildfly.iiop-openjdk:remove

[host:9990 /] /socket-binding-group=standard-sockets/socket-binding=iiop:remove
[host:9990 /] /socket-binding-group=standard-sockets/socket-binding=iiop-ssl:remove
[host:9990 /] reload
[host:9990 /] /interface=unsecure:remove




Remoting sécurisé (legacy)

Il y a deux aspects à traiter pour sécuriser les connexions distantes aux EJB ou à JMS :
l&#8217;authentification et la confidentialité du transport.


La validation des identités est traitée par un secuurity domain et l&#8217;échange des identités est traitée par un realm, par défaut ApplicationRealm.


Pour que l'authentification fonctionne correctement, il faut reconfigurer l'`ApplicationRealm` en remplaçant l&#8217;élement authentication par un &lt;jaas&gt;.
Plutôt que de modifier ApplicationRealm, on peut aussi en créer un nouveau.



[host:9990 /] /core-service=management/security-realm=RemotingRealm:add
[host:9990 /] /core-service=management/security-realm=RemotingRealm/authentication=jaas:add(name=sw-domain)



Ensuite, il faut reconfigurer le subsystem remoting en modifiant l&#8217;http-connector existant ou en créant un nouveau :



[host:9990 /] /subsystem=remoting/http-connector=http-remoting-connector              \
                         :add(connector-ref=default, security-realm=RemotingRealm)



Par défaut, les accès remote se font avec le protocole remoting+http.
Ce protocole passe par le port HTTP par défaut, avec un UPGRADE.


Pour sécuriser les communications, on peut basculer en remoting+https.



[host:9990 /] /subsystem=remoting/http-connector=http-remoting-connector:write-attribute(name=connector-ref, value=https)



Evidemment, cette sécurisation peut être mise en place dès la création du connector,



[host:9990 /] /subsystem=remoting/http-connector=http-remoting-connector:add(connector-ref=https, security-realm=RemotingRealm)



La référence sur le connector https implique qu&#8217;il y ait un https-listener de ce nom dans le subsystem undertow.
C&#8217;est le cas par défaut depuis WildFly 10.1.





Commandes en domaine


Subsystems et profiles

La principale différence par rapport au fonctionnement en standalone, c&#8217;est que les subsystems ne sont plus directement à la racine, mais dans un profile.


Par exemple, pour une queue JMS, dans le profile full :



[host:9990 /] /profile=full/subsystem=messaging-activemq/server=default/jms-queue=SWq:add(entries=[java:/queue/SwQueue])




Serveurs

Démarrer un serveur (server-one) :



[host:9990 /] /host=local/server-config=server-one:start



Arrêter un serveur :



[host:9990 /] /host=local/server-config=server-one:stop




Applications

Déployer une application sur tous les serveurs du domaine :



[host:9990 /] deploy my-app.war --all-server-groups



Déployer une application sur certains groupes de serveurs :



[host:9990 /] deploy my-app.war --server-groups=server-group-one,server-group-two



Retirer une application de tous les serveurs du domaine :



[host:9990 /] undeploy my-app.war --all-relevant-server-groups



Retirer une application de certains groupes de serveurs :



[host:9990 /] undeploy my-app.war --server-groups=server-group-one --keep-content






Personnalisation


Il existe deux possibilités d&#8217;enrichir les commandes de jboss-cli, soit avec la commande alias, soit avec la commande command.


commande

Avec command, il est possible de créer de nouvelles commandes mappées avec des opérations sur des noeuds.



alias

Avec 'alias_, il est possible de créer donner des noms simples et expressif à des commandes complexes. Sa syntaxe est proche de l*alias*_* système.*


Par exemple, une fois l&#8217;alias deploy-app défini comme ci-dessous, il servira à déployer l&#8217;application myapplication.war dans le groupe server-group-one :



[host:9990 /] alias deploy-app="deploy ~/to-be-deployed/myapplication.war --server-groups=server-group-one"



Pour afficher un alias, on appelle :



[host:9990 /] alias deploy-app



Et l&#8217;appel de alias sans argument affiche la liste des alias.





Références




Les anciennes commandes, pour JBoss AS 7 et EAP 6


Documentation WildFly : CLI Recipes


Æsh, l&#8217;outil utilisé pour la console


Wiki JBoss : Format of a Detyped Operation Request


Wiki JBoss : Command-line Operation Request Format




</description>
          <pubDate>2013-06-26T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WildFly/CLI</link>
          <guid isPermaLink="true">https://www.jtips.info/WildFly/CLI</guid>
        </item>
      
    
      
        <item>
          <title>AngularJS/tutorial</title>
          <description>
Les quelques notes de cet article indiquent comment procéder pour démarrer simplement avec le tutoriel Angular, sans Git.


Installation des outils


Pour commencer, il faut installer Node.js qui est nécessaire pour l&#8217;exécution des tests. Node.js est aussi utilisé comme serveur web pour l&#8217;application.


On ajoute ensuite les modules nécessaires pour les tests :



$ npm -g install jasmine-node
$ npm -g install testacular



Remarques :




les commandes sont exécutées en mode console


$ représente le prompt de la console


npm = Node.js Package Manager


l&#8217;option -g permet d&#8217;installer les modules dans un répertoire global, i.e. pour tous les utilisateurs (cette option est recommandée, sinon les outils seront installés uniquement pour votre projet)


Jasmine est un framework de test de code JavaScript écrit en Ruby


jasmine-node est la version Jasmine pour Node.js &#8658; pas la peine d&#8217;installer Ruby


Testacular est le moteur d&#8217;exécution des tests JavaScript


sous MacOS X, ces commandes sont exécutées avec sudo (probablement sous Linux aussi)




En ce qui concerne le serveur web qui nous permettra de visualiser les pages développées, le tutoriel d&#8217;Angular suggère d&#8217;installer le script web-server.js du projet corrigé angular-phonecat.
Je préfère pour ma part installer le module http-server de Node.js., la manip étant plus simple :



$ npm -g install http-server



Quelques commandes Node.js utiles :




npm -g ls pour lister les modules du répertoire global


npm -g rm nom_du_module pour supprimer un module






Corrigé de l&#8217;application angular-phonecat


Quand on progresse dans le tutorial, le code utilise des fichiers css, des bibliothèques JavaScript et des images.
Il vaut mieux télécharger le projet angular-phonecat afin d&#8217;avoir les éléments sous la main le moment venu.




Structure initiale du projet


Avant de commencer le tutoriel, créer l&#8217;arborescence suivante :




projet


app


config


test






Étapes du tutoriel


Step 1 - Static Template

Créer le fichier index.html dans le répertoire app.




projet


app


index.html


config


test




Le répertoire projet représente l&#8217;espace de travail.


En mode console dans le répertoire projet, démarrer le serveur web avec le module http-server de Node.js sur le port 8000 :



$ http-server . -p 8000



Remarque : le caractère "." représente le répertoire courant


La page index.html est accessible depuis l&#8217;url http://localhost:8000/app/index.html.
Le serveur peut être arrêté avec un ctrl C.



Step 2 - Angular Templates

Fichiers et répertoires à copier / créer :




copier le fichier angular.js depuis projet corrigé dans le répertoire app/lib/angular


créer le fichier controllers.js dans le répertoire js


créer le répertoire test/unit


créer le fichier controllersSpec.js dans le répertoire test/unit


projet


app


index.html


js


controllers.js


lib


angular


angular.js


lib


config


testacular.conf.js


test


unit


controllersSpec.js




Créer le fichier de configuration pour les tests unitaires :




sous le répertoire projet, exécuter la commande testacular init


testing framework = jasmine


files to test = test/**/*Spec.js


autres options = valeurs proposées par défaut


déplacer le fichier de configuration testacular.conf.js dans le répertoire config


modifier le basePath du fichier testacular.conf.js : basePath = '../';


démarrer le serveur de test dans une seconde console, depuis le répertoire config avec la commande testacular start (le serveur peut être arrêté avec un ctrl C)





Step 3 - Filtering Repeaters

Dans cette étape, on apprend à utiliser les repeaters, les filters et à faire des tests end-to-end.


Fichiers et répertoires à copier / créer :




depuis le projet corrigé, copier img et css dans le répertoire app


créer le répertoire e2e dans le répertoire test


créer le fichier scenarios.js dans le répertoire e2e


copier le fichier runner.html dans le répertoire e2e depuis le projet corrigé


créer le répertoire lib/angular dans le répertoire test


copier le fichier angular-scenario.js dans le répertoire test/lib/angular depuis le projet corrigé :


projet


app


index.html


css


app.css


bootstrap-responsive.css


bootstrap-responsive.min.css


bootstrap.css


bootstrap.min.css


img


un ensemble d&#8217;images&#8230;&#8203;


js


controllers.js


lib


angular


angular.js


config


test


e2e


runner.html


scenarios.js


lib


angular


angular-scenario.js


unit


controllersSpec.js




La page d&#8217;exécution des tests est accessible depuis l&#8217;url http://localhost:8000/test/e2e/runner.html



Step 5 - XHRs and Dependency Injection

Dans cette étape des objets mock sont créés pour les tests end-to-end. Il faut donc ajouter la dépendance angular-mocks :




copier le fichier angular-mocks.js dans le répertoire test/lib/angular du projet


ajouter angular-mocks.js dans la liste des fichiers de testacular.conf.js (attention à l&#8217;odre de déclaration des fichiers) :





 files = [
   JASMINE,
   JASMINE_ADAPTER,
   'app/lib/angular/angular.js',
   'test/lib/angular/angular-mocks.js',
   'app/js//*.js',
   'test//*Spec.js'
 ];



Nous avons aussi besoin des fichiers json qui sont dans le répertoire phones :




projet


app


index.html


css


app.css


bootstrap-responsive.css


bootstrap-responsive.min.css


bootstrap.css


bootstrap.min.css


img


un ensemble d&#8217;images&#8230;&#8203;


js


controllers.js


lib


angular


angular.js


phones


config


test


e2e


runner.html


scenarios.js


lib


angular


angular-scenario.js


angular-mocks.js


unit


controllersSpec.js




Tests unitaires : commentaires sur la syntaxe

À la première lecture, il n&#8217;est pas évident de comprendre la syntaxe de l&#8217;injection du service $http.
Tout d&#8217;abord, un rappel sur le mécanisme d&#8217;injection :


Angular se fonde sur le nom des arguments passés au contructeur du contrôleur : ces noms doivent correspondre aux noms de services déclarés auprès d&#8217;Angular pour que l&#8217;injection soit réalisée.
Dans le cas du contrôleur PhoneListCtrl, les objets $scope et $http sont injectés : il s&#8217;agit de services Angular prédéfinis (cf $http et $scope).



function PhoneListCtrl($scope, $http) {...}



Revenons maintenant au code du test unitaire.


1- Pourquoi le service $httpBackend est injecté dans le beforeEach et non pas le service $http ?


Tout simplement parce que le service $http délègue de façon sous-jacente les traitements au service $httpBackend et Angular fournit un objet de leurre pour $httpBackend, et non pas pour $http, pour des raisons d&#8217;organisation du code j&#8217;imagine.
On modifie donc le comportement à l&#8217;intérieur du moteur, pas le moteur lui-même.


2- OK, je fais un objet de leurre pour $httpBackend, mais comment l&#8217;objet $http manipulé dans le contrôleur connait-il mon objet mock ?


En fait, le service $httpBackend est injecté dans le service $http dans tous les scénarii d&#8217;exécution : applicatif ou tests unitaires. Donc, encore une fois, en modifiant $httpBackend, on modifie indirectement $http.


3- Très bien, l&#8217;objet de leurre, c&#8217;est $httpBackend, alors pourquoi on injecte $httpBackend dans le beforeEach, et non pas $httpBackend lui-même ?


On peut effectivement injecter $httpBackend directement, et tout fonctionne parfaitement.
Attention alors de bien renommer la variable $httpBackend du code de test, comme par exemple dans le code ci-dessous :



 var scope, ctrl, httpBackend;

 beforeEach(inject(function($httpBackend, $rootScope, $controller) {
   httpBackend = $httpBackend;
   httpBackend.expectGET('phones/phones.json').
         respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);

   scope = $rootScope.$new();
   ctrl = $controller(PhoneListCtrl, {$scope: scope});
 }));

 it('should create "phones" model with 2 phones fetched from xhr', function() {
   expect(scope.phones).toBeUndefined();
   httpBackend.flush();

   expect(scope.phones).toEqual([{name: 'Nexus S'},
                                 {name: 'Motorola DROID'}]);
 });



Dans le tutoriel d&#8217;Angular, il a été choisi de déclarer la variable $httpBackend, probablement pour être explicite sur la nature de l&#8217;objet (i.e. c&#8217;est le service Angular $httpBackend).
Dans le beforeEach, si on continue à utiliser $httpBackend dans la fonction d&#8217;injection, alors cela conduit à l&#8217;affectation $httpBackend = $httpBackend&#8230;&#8203;
Il faut donc utiliser un autre nom pour l&#8217;argument passé à la fonction d&#8217;injection : $httpBackend.
Cela fonctionne parfaitement, car les caractères "" sont retirés du nom par l&#8217;injecteur d&#8217;Angular, lors de la recherche du service (cf code source injector) &#8658; le nom $httpBackend_ = le nom $httpBackend.


4- Ça marche. Une dernière question : pourquoi le service $http n&#8217;est-il pas fournit au constructeur du contrôleur dans le beforeEach ?


D&#8217;une part, on n&#8217;utilise pas $http dans le code de test pour les raisons indiquées ci-dessus.
D&#8217;autre part, le service $controller qui instancie PhoneListCtrl procèdera à l&#8217;injection des services qui ne sont pas explicitement déclarés : PhoneListCtrl va donc bien recevoir $http.


C&#8217;est équivalent au code suivant, qui passe explicitement le service $http :



 var scope, ctrl, httpBackend;

 beforeEach(inject(function($httpBackend, $rootScope, $controller, $http) {
   httpBackend = $httpBackend;
   httpBackend.expectGET('phones/phones.json').
         respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);

   scope = $rootScope.$new();
   ctrl = $controller(PhoneListCtrl, {$scope: scope, $http: $http});
 }));





Step 7 - Routing &amp; Multiple Views



ajouter le fichier app.js dans le répertoire projet/app/js


créer le répertoire partials dans projet/app et ajouter les fichiers phone-list.html et phone-detail.html


projet


app


index.html


css


app.css


bootstrap-responsive.css


bootstrap-responsive.min.css


bootstrap.css


bootstrap.min.css


img


un ensemble d&#8217;images&#8230;&#8203;


js


app.js


controllers.js


lib


angular


angular.js


partials


phone-detail.html


phone-list.html


phones


config


test


e2e


runner.html


scenarios.js


lib


angular


angular-scenario.js


angular-mocks.js


unit


controllersSpec.js





Step 9 - Filters



créer le fichier filters.js dans le répertoire projet/app/js


créer le fichier filtersSpec.js dans le répertoire test/unit


projet


app


index.html


css


app.css


bootstrap-responsive.css


bootstrap-responsive.min.css


bootstrap.css


bootstrap.min.css


img


un ensemble d&#8217;images&#8230;&#8203;


js


app.js


controllers.js


filters.js


lib


angular


angular.js


partials


phone-detail.html


phone-list.html


phones


config


test


e2e


runner.html


scenarios.js


lib


angular


angular-scenario.js


angular-mocks.js


unit


controllersSpec.js


filtersSpec.js





Step 10 - Event Handlers

Dans la description des tests unitaires pour le contrôleur PhoneDetailCtrl, il faut ajouter la propriété images, sinon le test provoque une erreur dans le contrôleur sur la ligne $scope.mainImageUrl = data.images[0]; :



 describe('PhoneDetailCtrl', function(){
   var scope, $httpBackend, ctrl;

   beforeEach(inject(function($httpBackend, $rootScope, $routeParams, $controller) {
     $httpBackend = $httpBackend;
     $httpBackend.expectGET('phones/xyz.json')
                 .respond({name:'phone xyz', images: ['image/url1.png', 'image/url2.png']});

     $routeParams.phoneId = 'xyz';
     scope = $rootScope.$new();
     ctrl = $controller(PhoneDetailCtrl, {$scope: scope});
   }));


   it('should fetch phone detail', function() {
     expect(scope.phone).toBeUndefined();
     $httpBackend.flush();

     expect(scope.phone).toEqual({name:'phone xyz', images: ['image/url1.png', 'image/url2.png']});
   });
 });




Step 11 - REST and Custom Services



créer le fichier services.js dans le répertoire projet/app/js


copier le fichier angular-resources.js du corrigé, dans le répertoire projet/app/lib/angular


ajouter app/lib/angular/angular-resource.js à la liste des fichiers dans testacular.conf.js


remplacer la vérification expect(scope.phone).toEqualData({}) par expect(scope.phone).toBeUndefined() dans controllersSpec.js :





   it('should fetch phone detail', function() {
     expect(scope.phone).toBeUndefined();
     $httpBackend.flush();

     expect(scope.phone).toEqualData(xyzPhoneData());
   });





projet


app


index.html


css


app.css


bootstrap-responsive.css


bootstrap-responsive.min.css


bootstrap.css


bootstrap.min.css


img


un ensemble d&#8217;images&#8230;&#8203;


js


app.js


controllers.js


filters.js


services.js


lib


angular


angular.js


angular-resources.js


partials


phone-detail.html


phone-list.html


phones


config


test


e2e


runner.html


scenarios.js


lib


angular


angular-scenario.js


angular-mocks.js


unit


controllersSpec.js


filtersSpec.js





</description>
          <pubDate>2013-01-26T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JavaScript/AngularJS</link>
          <guid isPermaLink="true">https://www.jtips.info/JavaScript/AngularJS</guid>
        </item>
      
    
      
        <item>
          <title>JBoss Modules</title>
          <description>
JBoss Modules est le système de gestion des modules intégré à WildFly.
Il est plus simple qu&#8217;OSGi et utilisable dans WildFly ou dans une application autonome dès 2012.


Cette page peut vous intéresser pour construire des modules pour WildFly ou pour développer des applications à base de modules.


Structure d&#8217;un module


Un module peut être un fichier jar avec une description dans le fichier manifest, un peu comme pour OSGi, ou dans un répertoire avec une description XML.


La forme utilisée dans WildFly est celle du répertoire : par exemple, le module org.sewatech.exemple sera dans un répertoire org/sewatech/exemple.
Dans ce répertoire, on aura un sous-répertoire par slot.
Un slot correspond à une version et le slot par défaut est dans le répertoire main.
Le contenu du slot par défaut du module org.sewatech.exemple est donc dans le répertoire org/sewatech/exemple/main.


Ce répertoire contient au moins un fichier module.xml qui décrit le contenu du module, sous forme de ressources (fichiers jar) et ses dépendances.



 &lt;?xml version="1.0" encoding="UTF-8"?&gt;
 &lt;module xmlns="urn:jboss:module:1.1" name="org.sewatech.this.one"&gt;
   &lt;resources&gt;
     &lt;resource-root path="first.jar"/&gt;
     &lt;resource-root path="second.jar"/&gt;
   &lt;/resources&gt;

   &lt;dependencies&gt;
     &lt;module name="org.sewatech.other" /&gt;
   &lt;/dependencies&gt;
 &lt;/module&gt;



Les ressources sont des fichiers jar tout à fait standards, sans aucune référence à JBoss Modules.
L&#8217;association entre le fichier jar et la notion de module est faite exclusivement dans le fichier module.xml.


Module de démarrage

Pour développer une application autonome, en dehors de WildFly, il faut une classe avec une méthode main qu&#8217;on va déclarer comme classe de démarrage du module.



 &lt;?xml version="1.0" encoding="UTF-8"?&gt;
 &lt;module xmlns="urn:jboss:module:1.1" name="org.sewatech.start"&gt;
   &lt;main-class name="org.sewatech.start.Main"/&gt;
   ...
 &lt;/module&gt;



Ensuite, il faut démarrer Java avec jboss-modules, en indiquant que ce module est utilisé au démarrage.



 java -jar jboss-modules.jar -mp modules org.sewatech.module.start



L&#8217;option -mp permet de donner le répertoire racine des modules.



Export

Par défaut, un module exporte tout le contenu de ses ressources aux modules qui vont l&#8217;utiliser.
Pour limiter l&#8217;accès à une API, et interdire l&#8217;accès à des classes d&#8217;implémentation, le module peut exporter uniquement certains packages.



 &lt;?xml version="1.0" encoding="UTF-8"?&gt;
 &lt;module xmlns="urn:jboss:module:1.1" name="org.sewatech.other"&gt;
   &lt;exports&gt;
   &lt;/exports&gt;
   ...
 &lt;/module&gt;






Dépendances


On doit définir explicitement les dépendances pour utiliser des classes externes au module.
Seules les classes de packages java.* sont utilisables directement.
Pour toutes les autres classes, même celles du JDK ou celles dans le classpath doivent être déclarées.
C&#8217;est le cas, par exemple pour JAXB ou l&#8217;API module. Dans ce cas, on parle de dépendances system.


Dépendances systèmes

Une dépendance système fait référence à un ou plusieurs package présents dans le classpath.



 &lt;dependencies&gt;
   &lt;system&gt;
     &lt;paths&gt;
       &lt;path name="org/jboss/modules"/&gt;
       ...
     &lt;/paths&gt;
   &lt;/system&gt;
 &lt;/dependencies&gt;



Dans les exemples cités précédemment, les dépendances minimales sont sur les packages javax.xml.bind et com.sun.xml.internal.bind.v2, pour les classes, et javax/xml/bind/annotation pour les annotations.
Les erreurs pour les classes sont assez parlante (java.lang.ClassNotFoundException) alors que pour les annotations, c&#8217;est un peu plus flou : " SAXException2: unable to marshal &#8230;&#8203; ".
Ceci est plus dû au fonctionnement de ces annotations qu&#8217;à celui des modules.
En effet, lorsqu&#8217;un annotation ne peut pas être chargée par le classloader, elle est tout simplement ignorée.



 &lt;dependencies&gt;
   &lt;system&gt;
     &lt;paths&gt;
       &lt;path name="javax/xml/bind"/&gt;
       &lt;path name="javax/xml/bind/annotation" /&gt;
       &lt;path name="com/sun/xml/internal/bind/v2"/&gt;
     &lt;/paths&gt;
   &lt;/system&gt;
 &lt;/dependencies&gt;



On voit bien qu&#8217;il peut être fastidieux de devoir déclarer toutes ces dépendances.
Cette tâche peut être simplifiée en utilisant les modules fournis dans WildFly.
Le module javax.api centralise les dépendances vers les packages javax.* de Java SE et sun.jdk vers les packages com.sun.* et sun.* fournis avec la JVM d&#8217;Oracle.
On notera l&#8217;attribut export="true" dans ces deux modules&#8230;&#8203;



Export

Lorsqu&#8217;un module A déclare une dépendance vers un autre module B, il peut en utiliser toutes les classes et interfaces.
En revanche, la dépendance n&#8217;étant pas transitive, il ne peut pas utiliser les classes des modules d&#8217;un troisième module, même si B a déclaré une dépendance vers lui.
Si la transitivité est utile pour un module, elle doit être déclarée explicitement.
C&#8217;est ce qui est fait par exemple dans le module javax.api que j&#8217;ai cité plus haut : ce module dépend de ressources system, qu&#8217;il exporte aux autres modules :



   &lt;dependencies&gt;
     &lt;system export="true"&gt;
       &lt;paths&gt;
           &lt;path name="javax/accessibility"/&gt;
           ...
       &lt;/paths&gt;
     &lt;/system&gt;
   &lt;/dependencies&gt;



De la même façon, on peut exporter un module, totalement ou partiellement.
Pour un import total, on ajoute l&#8217;attribut export="true" au module en dépendance :



   &lt;dependencies&gt;
     &lt;module export="true" name="org.sewatech.other" /&gt;
     ...
   &lt;/dependencies&gt;



Pour un export partiel, on ajoute un sous-élément &lt;export&gt; dans le module en dépendance pour inclure ou exclure des packages :



   &lt;dependencies&gt;
     &lt;module name="org.sewatech.other"&gt;
       &lt;export&gt;
         &lt;include-set&gt;
           &lt;path name="org.sewatech.other.api"/&gt;
         &lt;/include-set&gt;
       &lt;/export&gt;
     &lt;/module&gt;
     ...
   &lt;/dependencies&gt;



ou



   &lt;dependencies&gt;
     &lt;module name="org.sewatech.other"&gt;
       &lt;export&gt;
         &lt;exclude-set&gt;
           &lt;path name="org.sewatech.other.impl"/&gt;
         &lt;/exclude-set&gt;
       &lt;/export&gt;
     &lt;/module&gt;
     ...
   &lt;/dependencies&gt;




</description>
          <pubDate>2012-12-08T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WildFly/Modules</link>
          <guid isPermaLink="true">https://www.jtips.info/WildFly/Modules</guid>
        </item>
      
    
      
        <item>
          <title>Java/OutOfMemoryError</title>
          <description>
En cours de rédaction&#8230;&#8203;


hotspot




Managed Heap


Perm Space


Thread : unable to create new native thread


Direct ByteBuffer : Allocated 1953546760 bytes of native memory before running out






j9




Managed Heap


Thread : Failed to fork OS thread


Direct ByteBuffer : Unable to allocate 1048576 bytes of direct memory after 5 retries






Références




http://www.ibm.com/developerworks/linux/library/j-nativememory-linux/




</description>
          <pubDate>2012-04-28T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/OutOfMemoryError</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/OutOfMemoryError</guid>
        </item>
      
    
      
        <item>
          <title>Accéder à une queue JMS distante dans JBoss AS 7</title>
          <description>
Pour accéder à une destination JMS (queue ou topic), il faut d&#8217;abord accéder au registre JNDI, lui demander la ConnectionFactory et la destination, puis se connecter au serveur et envoyer ou consommer les messages.



jndiContext = new InitialContext();
connectionFactory = (ConnectionFactory) jndiContext.lookup(factoryName);
queue = (Queue) jndiContext.lookup(queueName);
...



JNDI


La configuration JNDI est la suivante (notez que le serveur JNDI est sécurisé) :



#jndi.properties
java.naming.factory.initial=org.jboss.naming.remote.client.InitialContextFactory
java.naming.provider.url=remote://localhost:4447

java.naming.security.principal=alexis
java.naming.security.credentials=hassler





JMS


Le nom de la fabrique est jms/RemoteConnectionFactory. Ce nom correspond au nom exporté java:jboss/exported/jms/RemoteConnectionFactory, configuré dans le fichier standalone-full.xml.



&lt;connection-factory name="RemoteConnectionFactory"&gt;
    &lt;connectors&gt;
        &lt;connector-ref connector-name="netty"/&gt;
    &lt;/connectors&gt;
    &lt;entries&gt;
        &lt;entry name="RemoteConnectionFactory"/&gt;
        &lt;entry name="java:jboss/exported/jms/RemoteConnectionFactory"/&gt;
    &lt;/entries&gt;
&lt;/connection-factory&gt;



Le nom de la queue suit la même logique ; il faut qu&#8217;il soit exporté.



&lt;jms-queue name="SWq"&gt;
    &lt;entry name="queue/SWq"/&gt;
    &lt;entry name="java:jboss/exported/queue/SWq"/&gt;
&lt;/jms-queue&gt;



Enfin, JMS est sécurisé par défaut dans JBoss AS 7.1, il faut donc que le client s&#8217;authentifie aussi lors de l&#8217;établissement de la connexion JMS. Réutiliser les mêmes informations d&#8217;authentification que pour JNDI.



connection = connectionFactory.createConnection(jndiEnvironment.get(Context.SECURITY_PRINCIPAL), jndiEnvironment.get(Context.SECURITY_CREDENTIALS));



</description>
          <pubDate>2012-03-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss7/RemoteJMS</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss7/RemoteJMS</guid>
        </item>
      
    
      
        <item>
          <title>Accéder à un EJB distant dans JBoss AS 7</title>
          <description>
Dans JBoss AS 7, beaucoup de choses ont changé, y compris la façon dont le registre JNDI est organisé et dont un client peut accéder à un EJB distant. Voici un résumé de la façon de procéder dans JBoss AS 7.1 (la première version à supporter pleinement le remoting).


Avant JBoss AS 7


Avec les versions précédentes de JBoss, pour accéder à un EJB distant on instanciait un contexte de nommage à qui on demandait de nous donner un proxy sur l&#8217;EJB.



namingContext = new InitialContext();
messageServiceEjb = (MessageService) namingContext.lookup("swmsg-app/MessageServiceBean/remote");
messageService.getMessage(id);



Pour que ceci fonctionne, on avait besoin d&#8217;un fichier jndi.properties dans le classpath du client.



#jndi.properties
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory
java.naming.provider.url=jnp://localhost:1099
java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces



Pour que les communications passent entre le client et le serveur, il fallait que le serveur puisse recevoir des connexions sur les ports 1099, 1098 et 4446.




Avec le client EJB de JBoss AS 7


Dans JBoss AS 7, le fonctionnement a radicalement changé, même si le code ressemble beaucoup. En effet, on instancie toujours un contexte initial et on demande une référence à l&#8217;EJB distant.



namingContext = new InitialContext();
messageServiceEjb = (MessageService) namingContext.lookup("ejb:swmsg-app/swmsg-ejb3//MessageServiceBean!fr.sewatech.formation.appserv.service.MessageService");
messageService.getMessage(id);



En fait, seul le nom JNDI de l&#8217;EJB a changé. Il a même changé de façon assez radicale puisqu&#8217;on notera ce préfixe (ou namespace) "ejb:". Ce namespace n&#8217;existe pas en tant que tel dans le registre JNDI du serveur ; il sert au client JNDI à adresser sa demande au client EJB de JBoss. De ce fait, le fichier jndi.properties a bien changé, il n&#8217;y a plus besoin de NamingContextFactory, ni d&#8217;url du serveur.



#jndi.properties
java.naming.factory.url.pkgs=org.jboss.ejb.client.naming



Les informations sur le serveur sont à présent dans le fichier jboss-ejb-client.properties, lui aussi dans le classpath.



#jboss-ejb-client.properties
endpoint.name=client-endpoint
remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=false

remote.connections=default

remote.connection.default.host=127.0.0.1
remote.connection.default.port=4447
remote.connection.default.username=alexis
remote.connection.default.password=hassler



Le port 4447 est celui du composant remoting, c&#8217;est le seul utilisé pour la communication depuis le client. Et comme, dans JBoss AS 7.1, JNDI est sécurisé par défaut. Il faut donc ajouter un utilisateur de type Application, avec la commande add-user.


Ce fonctionnement est un hybride entre l&#8217;accès par JNDI et l&#8217;accès direct à l&#8217;EJB. En utilisant l&#8217;API de client EJB de JBoss, on pourrait même se passer de JNDI.




Comme avant, par JNDI


Le fonctionnement classique par JNDI a été conservé pour faciliter la transition vers JBoss AS 7. Il semble que ce mode classique pourrait ne pas être conservé dans l&#8217;avenir.


Dans ce mode, on utilise le nom JNDI exporté de l&#8217;EJB.



namingContext = new InitialContext();
messageServiceEjb = (MessageService) namingContext.lookup("swmsg-app/swmsg-ejb3/MessageServiceBean!fr.sewatech.formation.appserv.service.MessageService");
messageService.getMessage(id);



Ce nom exporté correspond au nom "java:jboss/exported/&#8230;&#8203;".


Le fichier jndi.properties devient



#jndi.properties
java.naming.factory.initial=org.jboss.naming.remote.client.InitialContextFactory
java.naming.provider.url=remote://localhost:4447
java.naming.security.principal=alexis
java.naming.security.credentials=hassler
remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=false





Références


Il y a une page dédiée à ce sujet dans la documentation de JBoss AS 7.1.


</description>
          <pubDate>2012-03-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss7/RemoteEJB</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss7/RemoteEJB</guid>
        </item>
      
    
      
        <item>
          <title>WildFly en domaine</title>
          <description>
L&#8217;objectif est de mettre en place un domaine WildFly, constitué d&#8217;un maître (appelé master) et un esclave (appelé host202).


Principe


Avec WildFly, nous avons le choix entre un mode autonome (standalone) ou en domaine (domain).
Le mode standalone est similaire à ce que nous connaissions dans les anciennes versions de JBoss, avec un serveur démarré et administré individuellement.
Le mode domain permet de démarrer plusieurs instances avec une seule commande et d&#8217;administrer les instances sur plusieurs machines de façon centralisée.


Dans le mode domain, il y a 3 types d&#8217;acteurs :




des serveurs WildFly sur lesquels sont déployées les applications,


un host controller par machine, chacun contrôlant les serveurs WildFly sur cette machine,


un domain controller qui pilote l&#8217;ensemble de l&#8217;environnement.









Sur chaque machine, il faut démarrer un host controller (ou un domain controller), avec la commande



~# $JBOSS_HOME/bin/domain.sh



En démarrant ainsi, la configuration se fait dans les fichiers domain/configuration/host.xml et domain.xml.
Le fichier host.xml contient le paramétrage du contrôleur et le domain.xml configure les serveurs WildFly.




Configuration


Contrôleur de domaine / d&#8217;hôte

Ce qui différencie un contrôleur de domaine d&#8217;un contrôleur d&#8217;hôte c&#8217;est l&#8217;élément &lt;domain-controller&gt; dans host.xml.


Un contrôleur de domain se contrôle localement :



&lt;domain-controller&gt;
  &lt;local/&gt;
&lt;/domain-controller&gt;



Un contrôleur d&#8217;hôte est contrôlé par un contrôleur de domaine distant :



&lt;domain-controller&gt;
  &lt;remote host="192.168.0.101" port="9990" protocol="remote+http" /&gt;
&lt;/domain-controller&gt;



Chaque contrôleur doit avoir un nom unique dans l&#8217;environnement.
On utilise souvent master pour le contrôleur de domaine et on change le nom des contrôleurs d&#8217;hôte :



&lt;host name="host202" xmlns="urn:jboss:domain:18.0"&gt;
  [...]
&lt;/host&gt;




Serveurs WildFly

Dans la terminologie des domaines WildFly, un serveur est une instance de WildFly.
Ces serveurs sont démarrés par leurs contrôleurs ;
chaque serveur est déclaré dans le fichier host.xml.



&lt;servers&gt;
  &lt;server name="server-one" group="main-server-group"&gt;
  &lt;/server&gt;
  &lt;server name="server-two" group="main-server-group"&gt;
  &lt;/server&gt;
&lt;/servers&gt;



Le groupe de serveur qui est spécifié ici regroupe des serveurs qui ont le même profil de configuration, le même groupe de sockets et les mêmes applications.
La configuration de chaque groupe de serveur est dans domain.xml.







Sur chaque hôte, chaque groupe fait référence à un profil qui est un assemblage de sous-systèmes, comme le logging, les datasources, web, ejb3,&#8230;&#8203;
Ce profil représente la configuration des instances.



Authentification (legacy)

Le contrôleur secondaire se connecte au contrôleur de domaine via son interface de gestion.
Il doit donc en respecter les contraintes de sécurité, comme l&#8217;authentification.


Pour ça, la connexion &lt;remote&gt; doit spécifier le username et le security-realm dans lequel le mot de passe est stocké.
Si le username n&#8217;est pas renseigné, c&#8217;est le name du host qui est utilisé.



&lt;domain-controller&gt;
  &lt;remote host="192.168.0.101" port="9990" protocol="remote+http"
          username="slave" security-realm="SlaveRealm"/&gt;
&lt;/domain-controller&gt;



Le mot de passe est stocké en base64 sous la forme d&#8217;un élément de &lt;server-identities&gt;.



&lt;security-realm name="SlaveRealm"&gt;
  &lt;server-identities&gt;
    &lt;secret value="c2xhdmVfdXNlcl9wYXNzd29yZA==" /&gt;
  &lt;/server-identities&gt;
&lt;/security-realm&gt;









Ce paragraphe legacy ne s&#8217;applique que jusqu&#8217;à WildFly 24.
En effet, l&#8217;ancien subsystem de sécurité a été retiré de WildFly 25.






Authentification (elytron)

Avec elytron, la connexion &lt;remote&gt; ne spécifie ni username ni security-realm, mais un contexte d&#8217;authentification.



&lt;domain-controller&gt;
  &lt;remote host="192.168.0.101" port="9990" protocol="remote+http"
          authentication-context="hcAuthContext"/&gt;
&lt;/domain-controller&gt;



Le mot de passe et le nom d&#8217;utilisateur sont stockés dans une configuration d&#8217;authentification d'elytron.
La relation entre le contexte et cette configuration est faite par l&#8217;intermédiaire de règles.
Et bien que ça concerne un subsystem, c&#8217;est dans le fichier host.xml.



&lt;subsystem xmlns="urn:wildfly:elytron:14.0"&gt;
  &lt;authentication-client&gt;

    &lt;authentication-configuration name="hostAuthConfig"
                                  authentication-name="slave"
                                  realm="ManagementRealm"&gt;
      &lt;credential-reference clear-text="slave_user_password"/&gt;
    &lt;/authentication-configuration&gt;

    &lt;authentication-context name="hcAuthContext"&gt;
      &lt;match-rule match-host="192.168.0.101"
                  authentication-configuration="hostAuthConfig"/&gt;
    &lt;/authentication-context&gt;

  &lt;/authentication-client&gt;
  ...
&lt;/subsystem&gt;



Comme toujours, la configuration avec elytron est complexe, mais avec beaucoup de souplesse.


Le mot de passe est en clair ici.
On peut aussi le masquer ou externaliser son stockage dans un credential store.



Découverte du contrôleur de domaine

Dans les fichiers host.xml fournis avec les versions récentes de WildFly, les coordonnées du contrôleur de domaine ne sont pas directement stockées sur l&#8217;élément &lt;remote&gt;, mais dans des options de découverte.
Cette façon de faire permet de spécifier plusieurs contrôleurs distants.
Ça permet de démarrer avec un autre contrôleur si le premier est en panne.



&lt;domain-controller&gt;
  &lt;remote authentication-context="hcAuthContext"&gt;
    &lt;discovery-options&gt;
      &lt;static-discovery name="primary" protocol="remote+http"
                        host="192.168.0.101" port="9990"/&gt;
      &lt;static-discovery name="secondary" protocol="remote+http"
                        host="192.168.0.102" port="9990"/&gt;
    &lt;/discovery-options&gt;
  &lt;/remote&gt;
&lt;/domain-controller&gt;



L&#8217;exemple ci-dessus utilise un contexte d&#8217;authentification d'elytron, mais cette façon de faire est compatible avec la sécurité legacy.


Avec ces options de découverte, il est aussi possible de remplacer la communication réseau par du stockage S2.
Je n&#8217;ai pas trouvé du documentation WildFly sur le sujet, mais la documentation JBoss EAP, Deploying JBoss EAP on Amazon Web Services l&#8217;explique bien et s&#8217;applique à WildFly.





Mise en oeuvre (sécurisée)


Ce paragraphe est un exemple de mise en oeuvre d&#8217;un domaine WildFly 25.
L&#8217;environnement est simplifié, avec deux machines ; le maître est sur le serveur 192.168.1.201 et l&#8217;esclave sur le serveur 192.168.1.202.







Configuration du master

Le fichier host.xml par défaut convient bien pour cela.
Pour simplifier l&#8217;environnement, je n&#8217;associe qu&#8217;un seul serveur au master.
Il faut surtout ajouter un login pour l&#8217;esclave afin que celui-ci puisse s&#8217;authentifier auprès du master et se connecter à son interface de management.



~# $JBOSS_HOME/bin/add-user.sh host202 hostpwd



Le master peut maintenant être démarré, en prenant soin de lier l&#8217;interface de management à une adresse publique.
Le choix de lier les interfaces applicatives à une adresse publique est secondaire ici (-b 0.0.0.0).



~# $JBOSS_HOME/bin/domain.sh -bmanagement 0.0.0.0



Pour démarrer un contrôleur de domaine sans serveur, on peut démarrer avec host-master.xml.



~# $JBOSS_HOME/bin/domain.sh -bmanagement 0.0.0.0 -Djboss.host.default.config=host-master.xml




Configuration du slave

Pour cela, le pont de départ serait plutôt host-slave.xml.
En effet, il faut en priorité spécifier un domain-controller distant,et lui associer un realm qui contient un server-identity.



&lt;domain-controller&gt;
  &lt;remote host="${jboss.domain.master.address}"
          port="${jboss.domain.master.port:9990}"
          protocol="${jboss.domain.master.protocol:remote+http}"
          authentication-context="hcAuthContext"/&gt;
&lt;/domain-controller&gt;



Le mot de passe est stockée au niveau de la configuration d&#8217;authentification d'elytron (fichier host-slave.xml).
Par acquis de conscience, on va le stocker dans un format masqué.



~# bin/elytron-tool.sh mask --salt fzekalzg --iteration 7 --secret hostpwd
MASK-DZlF8QP1ioV;fzekalzg;7




&lt;subsystem xmlns="urn:wildfly:elytron:14.0"&gt;
  &lt;authentication-client&gt;
    &lt;authentication-configuration name="hostAuthConfig" realm="ManagementRealm"
                                  authentication-name="host202"&gt;
      &lt;credential-reference clear-text="MASK-DZlF8QP1ioV;fzekalzg;7"/&gt;
    &lt;/authentication-configuration&gt;
    &lt;authentication-context name="hcAuthContext"&gt;
      &lt;match-rule match-host="${jboss.domain.master.address}"
                  authentication-configuration="hostAuthConfig"/&gt;
    &lt;/authentication-context&gt;
  &lt;/authentication-client&gt;
  ...
&lt;/subsystem&gt;



La configuration est prête, on peut donc démarrer l&#8217;esclave.



~# $JBOSS_HOME/bin/domain.sh -Djboss.domain.master.address=192.168.1.201 -b 0.0.0.0






Références




WildFly Admin Guide - Domain Setup


WildFly Client Configuration - &lt;authentication-client /&gt;


Red Hat - Deploying JBoss EAP on Amazon Web Services


Octopus deploy - WildFly S3 Domain Discovery




</description>
          <pubDate>2012-03-02T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WildFly/Domain</link>
          <guid isPermaLink="true">https://www.jtips.info/WildFly/Domain</guid>
        </item>
      
    
      
        <item>
          <title>Connexion de JConsole à JBoss AS 7 sur Amazon EC2</title>
          <description>
L&#8217;objectif est d&#8217;établir une connexion entre jconsole et JBoss AS 7 déployé sur une instance Amazon EC2.


Environnement


L&#8217;environnement dans lequel j&#8217;ai mis en place la solution est le suivant :




JBoss AS 7.1 ou 7.0


Amazon EC 2 t1.micro instance, Amazon Linux


IP publique fixe (Elastic IP), avec un nom DNS personnalisé (aws1.sewatech.net)


Client jconsole (JDK 6) sur MacOS X, derrière un pare-feu











Configuration de JBoss


Dans JBoss AS 7.1, les accès distants à JMX sont désactivés. L&#8217;activation de l&#8217;accès distant se fait en deux parties.


Activation des connecteurs

Tout d&#8217;abord, il faut associer des connecteurs au sous-système JMX, en changeant, dans le fichier standalone/configuration/standalone.xml, la portion :



&lt;subsystem xmlns="urn:jboss:domain:jmx:1.1" show-model="true"/&gt;



en :



&lt;subsystem xmlns="urn:jboss:domain:jmx:1.1" show-model="true"&gt;
    &lt;jmx-connector registry-binding="jmx-connector-registry"
                   server-binding="jmx-connector-server"/&gt;
&lt;/subsystem&gt;



Cette opération est inutile avec les versions 7.0.



Connexion distante

Ensuite, il faut mettre ces connecteurs à l&#8217;écoute de connexions distantes.
Par défaut, ils n&#8217;acceptent que les connexions locales.
Ceci peut se faire grâce à un paramètre au lancement de JBoss (-Djboss.bind.address.management=0.0.0.0).
J&#8217;ai préféré modifier la configuration de l&#8217;interface réseau management utilisée par les connecteurs JMX.



&lt;interfaces&gt;
    &lt;interface name="management"&gt;
        &lt;inet-address value="0.0.0.0"/&gt;
    &lt;/interface&gt;
    ...
&lt;/interfaces&gt;



Dans cette partie de la configuration, il n&#8217;y a aucune spécificité liée à Amazon EC2. La première spécificité est due au firewall et à la translation d&#8217;adresse pour accéder au serveur. Le nom (public) utilisé pour atteindre le serveur n&#8217;est pas celui du serveur lui-même, appelé nom privé. Il en est de même pour les adresses IP : une IP publique et une IP privée.





Avec JBoss AS 7.1.0.CR1


Attention : cette partie est obsolète. La technique présentée ici n&#8217;est valable qu&#8217;en version 7.1.0.CR1. Elle ne fonctionnait pas dans les versions précédentes et ne fonctionne plus à partir de la 7.1.0.Final.


Pour que la translation d&#8217;adresse ne pause pas de problème à l&#8217;accès JMX, il faut ajouter un paramètre au lancement de JBoss. En fin du fichier bin/standalone.conf, j&#8217;ai ajouté la ligne suivante :



JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=aws1.sewatech.net"



Cette option ne fonctionne qu&#8217;à partir de la version 7.1.0.CR1 ; cf. AS7-3120.


Configuration du firewall Amazon

JBoss utilise deux ports fixes pour JMX : 1090, pour la connexion initiale et 1091 pour la communication ultérieure. Il faut donc autoriser le client à accéder à ces ports.



ec2-authorize quicklaunch-1 -P tcp -p 1090-1091 -s xxx.yyy.zzz.www/32




JConsole

Pour finir, on peur démarrer jconsole normalement.



jconsole aws.sewatech.net:1090






Avec JBoss AS 7.0


Ceci est aussi valable avec la version AS 7.1.beta1, avant que le ticket AS7-3120 ne soit résolu.


Configuration du client

L&#8217;accès direct au serveur n&#8217;est pas possible. Il faut mettre en place un proxy SOCKS.



ssh -vfND 9999 -i .ec2/aws1.pem ec2-user@aws1.sewatech.net




Configuration du firewall Amazon

Dernière étape, il faut autoriser la connexion au serveur. Du fait du proxy SOCKS, il faut autoriser le serveur lui-même à accéder au port.



ec2-authorize quicklaunch-1 -P tcp -p 1090 -s 107.20.184.81/32



C&#8217;est l&#8217;adresse IP publique qui est spécifiée ici.



JConsole

On démarre JConsole en lui indiquant le proxy SOCKS.



jconsole -J-DsocksProxyHost=localhost -J-DsocksProxyPort=9999 aws1.sewatech.net:1090



Il y a un peu de latence au démarrage, mais ça fonctionne.



</description>
          <pubDate>2011-12-17T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss7/JMX-EC2</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss7/JMX-EC2</guid>
        </item>
      
    
      
        <item>
          <title>JBoss/Migration</title>
          <description>
L&#8217;objectif de cet article n&#8217;est pas de fournir un guide de migration (pour cela, cf la documentation JBoss), mais plutôt de partager une expérience de migration vers JBoss 7.0.2.


Répertoire de déploiement


L&#8217;application que je migre me sert de support pour les exercices d&#8217;une formation JPA. Je n&#8217;ai donc pas besoin d&#8217;une configuration JBoss évoluée et je garde la configuration par défaut proposée par Eclipse, à savoir "standalone".




Configuration de la data source


La configuration des data sources est assez différentes des versions antérieures de JBoss.


Pour commencer, il faut installer le driver JDBC, dans mon cas celui de MySQL. Deux possibilités : soit on installe le driver dans le répertoire de déploiement, soit on l&#8217;installe comme module. J&#8217;opte pour la seconde option, bien que la première solution soit plus simple.
A cette fin, il faut copier le driver mysql-connector-java-5.1.18-bin.jar dans le répertoire AS7_HOME/modules/com/mysql/main (le répertoire AS7_HOME/modules/com existe déjà, il faut créer le sous-répertoire mysql/main).
Dans ce répertoire on ajoute aussi le fichier module.xml :



	&lt;?xml version="1.0" encoding="UTF-8"?&gt;
	&lt;module xmlns="urn:jboss:module:1.0" name="com.mysql"&gt;
 	  &lt;resources&gt;
		&lt;resource-root path="mysql-connector-java-5.1.18-bin.jar"/&gt;
	  &lt;/resources&gt;
	  &lt;dependencies&gt;
		&lt;module name="javax.api"/&gt;
		&lt;module name="javax.transaction.api"/&gt;
	  &lt;/dependencies&gt;
	&lt;/module&gt;



Dans un deuxième temps, on déclare la data source dans le fichier standalone.xml situé dans le répertoire AS7_HOME/standalone/configuration.


On ajoute la déclaration de la data source dans l&#8217;élément &lt;datasources&gt; existant (par défaut, une datasource H2 est déjà configurée) :



	&lt;datasource jndi-name="java:jboss/datasources/LibrairieDS" pool-name="LibrairieDS" enabled="true" jta="true" use-java-context="true" use-ccm="true"&gt;
		&lt;connection-url&gt;
			jdbc:mysql://localhost:3306/librairie
		&lt;/connection-url&gt;
		&lt;driver&gt;
			com.mysql
		&lt;/driver&gt;
		&lt;transaction-isolation&gt;
			TRANSACTION_READ_COMMITTED
		&lt;/transaction-isolation&gt;
 		&lt;pool&gt;
			&lt;prefill&gt;
				false
			&lt;/prefill&gt;
			&lt;use-strict-min&gt;
				false
			&lt;/use-strict-min&gt;
			&lt;flush-strategy&gt;
				FailingConnectionOnly
			&lt;/flush-strategy&gt;
		&lt;/pool&gt;
 		&lt;security&gt;
 			&lt;user-name&gt;
 				myuser
 			&lt;/user-name&gt;
 			&lt;password&gt;
 				mypassword
 			&lt;/password&gt;
  		&lt;/security&gt;
  	&lt;/datasource&gt;



Il faut aussi déclarer le driver MySQL dans l&#8217;élément &lt;drivers&gt; existant (juste en dessous de &lt;datasources&gt;) :



	&lt;driver name="com.mysql" module="com.mysql"&gt;
		&lt;xa-datasource-class&gt;
			com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
		&lt;/xa-datasource-class&gt;
	&lt;/driver&gt;





Le fichier persistence.xml


Le nom de la data source déclarée a évolué pour être en conformité avec les standards de nommage JEE6 (avec JBoss 6, j&#8217;utilisais le nom jdbc/LibrairieDS).
Je dois donc changer ce nom dans le fichier de configuration JPA :



	&lt;jta-data-source&gt;java:jboss/datasources/LibrairieDS&lt;/jta-data-source&gt;





Lookup entity manager


Je profite de cette migration pour adapter aux normes JEE 6 les noms de référence au PU démarré automatiquement par le serveur (ajout du préfixe "persistence" dans le nom).
Je modifie donc le fichier web.xml :


Entity manager factory :



	&lt;persistence-unit-ref&gt;
		&lt;persistence-unit-ref-name&gt;persistence/librairie-emf&lt;/persistence-unit-ref-name&gt;
		&lt;persistence-unit-name&gt;librairie&lt;/persistence-unit-name&gt;
	&lt;/persistence-unit-ref&gt;



Entity manager :



	&lt;persistence-context-ref&gt;
		&lt;persistence-context-ref-name&gt;persistence/librairie-em&lt;/persistence-context-ref-name&gt;
		&lt;persistence-unit-name&gt;librairie&lt;/persistence-unit-name&gt;
	&lt;/persistence-context-ref&gt;



Lookup entity manager factory :



	InitialContext ctx = new InitialContext();
	EntityManagerFactory emf = (EntityManagerFactory) ctx.lookup("java:comp/env/persistence/librairie-emf");



Lookup entity manager :



	InitialContext ctx = new InitialContext();
	EntityManager em = (EntityManager) ctx.lookup("java:comp/env/persistence/librairie-em");





Désactivation Envers


JBoss 7.0.2 fonctionne avec Hibernate 4.0 CR2 qui active le framework Envers par défaut, même si l&#8217;applicatif ne l&#8217;utilise pas (pas d&#8217;annotation Envers dans mon code).
J&#8217;avais donc une erreur au déploiement de l&#8217;application, du fait des tables spécifiques Envers manquantes dans ma base de données.
Je désactive donc Envers dans persistence.xml en ajoutant la propriété suivante :



 &lt;property name="hibernate.listeners.envers.autoRegister" value="false"/&gt;





Commons logging


Afin d&#8217;éviter un ClassNotFoundException sur les classes de commons-logging lors du déploiement, j&#8217;ajoute la ligne suivante dans le fichier MANIFEST.MF des modules concernés :



 Dependencies: org.apache.commons.logging





Modification de l&#8217;EAR


Dans l&#8217;application que j&#8217;utilise à des fins de TP, je déploie des composants de service métier de différentes façons. Dans un premier temps, ces composants sont simplement instanciés dans la couche cliente (un backing bean JSF).
Dans un deuxième temps, les composants sont déployés sous la forme d&#8217;EJB. Avec JBoss 6, le jar qui contient ces composants pouvait être placé à la racine de l&#8217;EAR, quelle que soit l&#8217;option retenue (EJB ou pas EJB). Ce n&#8217;est pas conforme aux standards quant à la structure de l&#8217;EAR, mais JBoss 6 le tolère, et c&#8217;était bien pratique.
Avec JBoss 7, plus de tolérance : les jar EJB doivent être placés à la racine de l&#8217;EAR, et les jar non EJB doivent être situés dans le sous répertoire lib.


</description>
          <pubDate>2011-11-28T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/Migration</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/Migration</guid>
        </item>
      
    
      
        <item>
          <title>JBoss/Build</title>
          <description>
Rédaction inachevée&#8230;&#8203;


La façon de compiler et construire JBoss AS dépend grandement de sa version.
Les plus récents sont hébergés sur GitHub et construit avec Maven alors qu&#8217;auparavant, c&#8217;était Subversion et Ant, voire un savant mélange de Ant et de Maven.
La documentation sur le sujet est un peu éparpillée, mais le meilleur point d&#8217;entrée est probablement sur le wiki de JBoss.


JBoss AS 7



git clone https://github.com/jbossas/jboss-as.git
cd jboss-as/
export MAVEN_OPTS=-Xmx256m
mvn clean install



Je ne connais pas les prérequis en terme de version de Maven, mais je l&#8217;ai fait fonctionner avec Maven 3.0.3.




JBoss AS 5 / 6


La version 6 de JBoss AS est une évolution de la version 5.
Elles sont basées sur la même architecture et utilisent le même référentiel de code.
Le choix de la version est donc lié aux tags.
J&#8217;ai restreint le périmètre à JBoss AS 5 et 6 (+ EAP), mais on pourrait remonter à JBoss AS 2.2 !


Pour la version en cours, on utilise donc le trunk :



svn co http://anonsvn.jboss.org/repos/jbossas/trunk/ .
export JAVA_HOME=...
./build.sh



Le JBossAS nouvellement constitué est dans build/target.
Pour le démarrer :



cd build/target/jboss-6.1.1-SNAPSHOT/
bin/run.sh



Pour compiler une version spécifique, on utilise les tags.


JBoss AS 6.0


svn co http://anonsvn.jboss.org/repos/jbossas/tags/6.0.0.Final/ .
export JAVA_HOME=...
./build.sh



puis pour le lancer



cd build/target/jboss-6.0.0.Final/
bin/run.sh




JBoss AS 5.1

Dans JBoss AS 5, les choses se compliquent.
Le build utilise du Ant, qui lui-même utilise des portions de Maven.
On ne peut pas lancer le build depuis la racine.
Il fallait utiliser le répertoire build.
Enfin, les repositories JBoss inscrits dans le pom.xml ne sont plus accessibles ; ensuite, certains artefacts ont été dépréciés ; il faut donc le mettre à jour.



svn co http://anonsvn.jboss.org/repos/jbossas/tags/JBoss_5_1_0_GA/ .
cd build
./build.sh



Remarque :




Ça ne fonctionne pas avec Ant 1.8, il faut revenir à Ant 1.7.





JBoss EAP 5.1


svn co http://anonsvn.jboss.org/repos/jbossas/tags/JBPAPP_5_1_1_GA/ .
cd build
./build.sh






Glassfish 3


La façon de construire Glassfish 3 est très bien expliqué sur le wiki de Sun.
Oui oui, c&#8217;est toujours sun.com.
Le code source est hébergé dans un dépot Subversion et le build se fait avec Maven.



svn checkout https://svn.java.net/svn/glassfish~svn/trunk/main .
export MAVEN_OPTS=-Xmx1024m
mvn clean install



Remarque :




le Xmx à 512m préconisé n&#8217;était pas suffisant. Ça suffit peut-être sur une VM en 32bits.
Le plus compliqué dans l&#8217;affaire, du moins pour le néophyte, est de trouver la localisation du Glassfish compilé :





cd /Users/alexis/Projet/sw-oracle/gf3/appserver/distributions/glassfish/target/stage/glassfish3/glassfish
bin/asadmin start-domain



</description>
          <pubDate>2011-10-28T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/Build</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/Build</guid>
        </item>
      
    
      
        <item>
          <title>Logs et statitistiques des requêtes Hibernate</title>
          <description>
show_sql


Pour activer l&#8217;affichage des requêtes SQL générées par Hibernate, on connait le classique show_sql, qui peut être configurée dans le fichier persistence.xml.



&lt;property name="hibernate.show_sql"&gt;true&lt;/property&gt;



Exemple:



Hibernate: select s1_0.id,s1_0.date_debut,s1_0.date_fin,m1_0.id,m1_0.code,m1_0.duree,m1_0.titre,m1_1.taux_pratique from session
s1_0 join cours m1_0 on m1_0.id=s1_0.matiere_id where m1_0.titre like ? order by s1_0.date_debut



Cette option est pratique, mais a quelques défauts importants :




Les requêtes sont affichées dans la sortie standard, ce serait mieux de les avoir dans les logs configurés.


Il manque la valeur des paramètres passés.


Les requêtes sont peu lisibles car sur une ligne.




Pour ce dernier point, on peut avoir une meilleure mise en forme des requêtes.
Il faut activer l&#8217;option format_sql.



 &lt;property name="hibernate.format_sql"&gt;true&lt;/property&gt;



Exemple:



Hibernate:
    select
        s1_0.id,
        s1_0.date_debut,
        s1_0.date_fin,
        m1_0.id,
        m1_0.code,
        m1_0.duree,
        m1_0.titre
      from
          session s1_0
      join
          cours m1_0
          on m1_0.id=s1_0.matiere_id
      where
          m1_0.titre like ?
      order by
          s1_0.date_debut





Loggers


Pour envoyer les logs dans Log4J ou Logback, il faut désactiver l&#8217;option show_sql et activer le logger org.hibernate.SQL en debug.
Les informations sur les paramètres sont envoyées au niveau trace dans le logger org.hibernate.type.descriptor.sql pour Hibernate 5 (ou anvant) et dans le logger org.hibernate.orm.jdbc.bind pour Hibernate 6.



  &lt;logger name="org.hibernate.SQL" level="DEBUG" /&gt;
  &lt;logger name="org.hibernate.orm.jdbc.bind" level="TRACE"/&gt;



Exemple:



01:23:45.666 DEBUG o.h.SQL select s1_0.id,s1_0.date_debut,s1_0.date_fin,m1_0.id,m1_0.code,m1_0.duree,m1_0.titre,m1_1.taux_pratique
from session s1_0 join cours m1_0 on m1_0.id=s1_0.matiere_id where m1_0.titre like ? order by s1_0.date_debut
01:23:45.666 TRACE o.h.orm.jdbc.bind: binding parameter (1:VARCHAR) &lt;- [%Cre%]









L&#8217;option format_sql fonctionne aussi avec le logger comme avec l&#8217;option show_sql.







Slow queries


La durée limite pour le déclenchement du log est configuré par la propriété hibernate.log_slow_query.
Sa valeur par défaut est 0, ce qui désactive la fonctionnalité.


Le temps d&#8217;exécution ne prend en compte ni le temps de préparation, ni le temps de traitement du résultat.



  &lt;property name="hibernate.log_slow_query" value="20" /&gt;



Exemple:



01:23:45.666 INFO o.h.SQL_SLOW SlowQuery: 128 milliseconds.
SQL: 'select s1_0.id,s1_0.date_debut,s1_0.date_fin,m1_0.id,m1_0.code, m1_0.duree,m1_0.titre,m1_1.taux_pratique from session s1_0
join cours m1_0 on m1_0.id=s1_0.matiere_id where m1_0.titre like ? order by s1_0.date_debut'









Jusqu&#8217;à la version 6.2, il fallait utiliser hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS. hibernate.log_slow_query est la nouvelle propriété depuis la version 6.3.
L&#8217;ancien nom reste utilisable pour le moment.







Cache L2



 &lt;logger name="org.hibernate.orm.cache" level="DEBUG"/&gt;



Exemple:



01:23:45.666 DEBUG o.h.orm.cache Checking cached query results in region: default-query-results-region
01:23:45.666 DEBUG o.h.orm.cache Query results were not found in cache
...
01:23:45.666 DEBUG o.h.orm.cache Caching query results in region: default-query-results-region; timestamp=70175456585277
...
01:23:45.666 DEBUG o.h.orm.cache Checking cached query results in region: default-query-results-region
01:23:45.666 DEBUG o.h.orm.cache Returning cached query results





Statistiques


Hibernate peut produire des statiques pour chaque requête ou par session.


Pour les statistiques des requêtes, il suffit d&#8217;activer le logger au niveau choisi (TRACE ou DEBUG).
Certaines des informations peuvent être redondantes avec les logs des paragraphes précédents, comme pour le cache L2.



 &lt;logger name="org.hibernate.stat" level="TRACE"/&gt;



Exemple:



01:23:45.666 DEBUG o.h.s.i.StatisticsImpl Statistics#queryCacheMiss( [CRITERIA] select ... from session,
default-query-results-region )
01:23:45.666 TRACE o.h.s.i.StatisticsImpl Statistics#queryCachePut( [CRITERIA] select ... from session,
default-query-results-region )
01:23:45.666 DEBUG o.h.s.i.StatisticsImpl HHH000117: HQL: [CRITERIA] select ... from session, time: 3ms, rows: 18
...
01:23:45.666 TRACE o.h.s.i.StatisticsImpl Statistics#queryCacheHit( [CRITERIA] select ... from session,
default-query-results-region )



Pour les statistiques de sessions,



 &lt;property name="hibernate.generate_statistics"&gt;true&lt;/property&gt;



Exemple:



01:23:45.666 INFO  o.h.e.i.StatisticalLoggingSessionEventListener : Session Metrics {
    38374 nanoseconds spent acquiring 1 JDBC connections;
    0 nanoseconds spent releasing 0 JDBC connections;
    493919 nanoseconds spent preparing 1 JDBC statements;
    1179672 nanoseconds spent executing 1 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
    0 nanoseconds spent performing 0 L2C puts;
    0 nanoseconds spent performing 0 L2C hits;
    0 nanoseconds spent performing 0 L2C misses;
    0 nanoseconds spent executing 0 flushes (flushing a total of 0 entities and 0 collections);
    5190 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections)
}





Hibernate avec Spring Boot


Quand on utilise Spring Boot, tout ceci peut être configuré dans le fichier application.yml.



spring:
  jpa:
    properties:
      hibernate:
        format_sql: true
        log_slow_query: 20
        generate_statistics: true

logging:
  level:
    "org.hibernate.SQL": DEBUG
    "org.hibernate.orm.jdbc.bind": TRACE
    "org.hibernate.orm.cache": DEBUG
    "org.hibernate.stat": TRACE



</description>
          <pubDate>2011-10-13T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Hibernate/Log</link>
          <guid isPermaLink="true">https://www.jtips.info/Hibernate/Log</guid>
        </item>
      
    
      
        <item>
          <title>JavaSE 7 - NIO2 - File System</title>
          <description>
Java 7 : découverte de NIO2 pour l&#8217;accès aux fichiers (rédaction inachevée)


Sous ce chapeau de New I/O se cachent plusieurs fonctionnalités bien distinctes. On peut principalement dissocier les classes d&#8217;accès au système de fichiers d&#8217;une part et les entrées / sorties asynchrones d&#8217;autre part. Seule la partie file system est traitée dans cette page.


java.io.File


Cette classe existe depuis Java 1.0 et est le moyen prévu pour parcourir le système de fichiers et de manipuler fichiers et répertoires. Cette classe a largement évolué depuis sa version initiale, en particulier pour Java 1.2, puis dans une moindre mesure pour Java 1.4 et Java 6.


Il est possible de créer un objet File, représentant un fichier ou répertoire à partir d&#8217;une chaîne de caractères ou d&#8217;un objet URI. Le format chemin peut être absolu ou relatif, mais est évidemment dépendant du système d&#8217;exploitation.


La classe java.io.File était jusqu&#8217;à maintenant le point central pour accéder au système de fichiers en Java. En Java SE 7, les fonctionnalités de cette classe sont reprises et enrichies dans d&#8217;autres classes du package java.nio.file. La classe java.io.File n&#8217;est pas dépréciée pour autant, mais son usage est déconseillé, au profit des nouvelles implémentations.




Manipulation de chemins


Un chemin d&#8217;accès peut être absolu ou relatif, son format dépend de l&#8217;OS. Les techniques pour définir le chemin d&#8217;un java.io.File était la chaîne de caractères ou l&#8217;URI. En JavaSE 7, on utilise l&#8217;interface java.nio.file.Path. Un objet Path est immuable, comme pour java.io.File. Un objet Path n&#8217;est pas directement associé à un fichier ou un répertoire, il peut exister sans fichier ou répertoire associé ; pour accéder aux fichiers associés il faut utiliser la classe java.nio.file.Files.


Comme Path est une interface, il faut utiliser la classe java.nio.file.Paths pour en créer des instances, à partir d&#8217;une chaîne de caractères ou d&#8217;une URI.



       Path myPath = Paths.get("mydir");



Les méthodes de Path permettent de décortiquer un chemin (nombre de niveaux, décomposition du chemin) et de naviguer dans les répertoires parents (accès au parent, accès à la racine), mais pas de naviguer dans le contenu.


Pour accéder à des sous-répertoires d&#8217;un chemin, on peut directement les passer à la création du Path, sans se préoccuper du séparateur.



       Path jarPath = Paths.get("dist", "Java7Examples.jar");



On peut aussi résoudre les sous-répertoires depuis l&#8217;objet Path, en donnant un sous-répertoire ou un sous-chemin.



       Path jarPath = projectPath.resolve("dist").resolve("Java7Examples.jar");



ou



       Path jarPath = projectPath.resolve(Paths.get("dist","Java7Examples.jar"));



Path est aussi capable d&#8217;extraire un chemin relatif par rapport à un autre chemin.



       Path jarPath = Paths.get("dist", "Java7Examples.jar")
                           .toAbsolutePath();;
       // =&gt; /home/alexis/NetBeansProjects/Java7Examples/dist/Java7Examples.jar
       Path homePath = Paths.get(System.getProperty("user.home"));
       // =&gt; /home/alexis
       System.out.println(homePath.relativize(jarPath));
       // =&gt; NetBeansProjects/Java7Examples/dist/Java7Examples.jar



Les méthodes equals, compareTo, startsWith et endsWith permettent de faire des comparaisons entre chemins.


La méthode normalize permet de faire le ménage dans les '.' et '..' qui auraient été utilisés pour la construction du chemin.



       Path myPath = Paths.get(".", "src");
       Path myPath = Paths.get("src", "..", ".", "src");
       System.out.println(myPath.toAbsolutePath());
       // =&gt; /home/alexis/NetBeansProjects/Java7Examples/src/.././src
       System.out.println(myPath.normalize().toAbsolutePath());
       // =&gt; /home/alexis/NetBeansProjects/Java7Examples/src



La méthode toRealPath permet de résoudre les liens symboliques.



       Path docPath = Paths.get("nio-doc");
       System.out.println(docPath.toAbsolutePath());
       // =&gt; /home/alexis/NetBeansProjects/Java7Examples/nio-doc
       System.out.println(docPath.toRealPath());
       // =&gt; /home/alexis/Téléchargements/nio



L’interopérabilité avec l&#8217;ancienne classe java.io.File a été conservée avec la possibilité de créer un Path depuis un File (myFile.toPath()) et réciproquement (myPath.toFile()).




Accès aux fichiers


Manipuler des chemins, c&#8217;est bien, mais l&#8217;objectif est généralement d&#8217;accéder aux fichiers, soit de façon externe pour les déplacer, copier, renommer ou pour en manipuler les métadonnées, soit pour lire ou modifier le contenu. Jusqu&#8217;à présent, l&#8217;accès externe était assuré par la classe File alors que le contenu était accédé par des flux (stream), éventuellement encapsulés dans des readers ou writers.


L&#8217;accès externe est aujourd&#8217;hui assuré par la classe utilitaire java.nio.file.Files, qui ne contient que des méthodes statiques.


On trouve les méthodes de création (createFile, createDirectory, createLink), suppression (delete), déplacement (move) et de copie (copie) qui travaillent avec des objets Path. Files fournit la nature du chemin : fichier (isRegularFile), répertoire (isDirectory), lien symbolique (isSymbolicLink). Files fournit aussi des informations sur le fichier : taille (size), propriétaire (getOwner), lisible (isReadable), modifiable (isWritable), caché (isHidden), exécutable (isExecutable), type MIME de son contenu (probeContentType).


On notera avec grand plaisir le support des liens symboliques : créer, parcourir,&#8230;&#8203;


L&#8217;accès aux méta-données a été amélioré. Certaines sont directement fournies par Files (heure de modification, propriétaire), d&#8217;autres sont accessibles via les attributs de fichier. Ces attributs peuvent être vus sous différentes formes : basique qui est compatible avec tous les systèmes, dos qui est valable uniquement sous Windows ou posix qui peut être utilisé sur les systèmes d&#8217;exploitation sérieux.
La vue basique n&#8217;a que peu d&#8217;intérêt puisque la plupart de ses informations sont directement disponibles via la classe Files. Son seul intérêt réside dans les performances car il peut économiser plusieurs accès successifs à un fichier



       BasicFileAttributes jarBasicAttrs = Files.readAttributes(jarPath, BasicFileAttributes.class);
       System.out.println("creationTime is " + jarBasicAttrs.creationTime());
       System.out.println("lastAccessTime is " + jarBasicAttrs.lastAccessTime());



Pour Posix, la plus-value se situe dans les permissions.




Parcourir les répertoires


Parcourir un répertoire est un peu moins intuitif mais plus puissant qu&#8217;avec java.io.File. En effet, on doit utiliser une ressource de type java.nio.file.DirectoryStream ; l&#8217;utilisation du try-with-resources est bienvenue.



       Path homePath = Paths.get(System.getProperty("user.home"));
       try (DirectoryStream&lt;Path&gt; stream = Files.newDirectoryStream(homePath)) {
           for (Path entry : stream) {
               System.out.println(entry);
           }
       }
       // =&gt; /home/alexis/Bureau
       // =&gt; /home/alexis/NetBeansProjects
       // =&gt; /home/alexis/Images
       // =&gt; /home/alexis/Téléchargements
       // =&gt; /home/alexis/.m2
       // =&gt; /home/alexis/.gnome2
       // =&gt; ...



La méthode peut aussi recevoir un paramètre optionnel qui permet de filtrer le résultat avec la syntaxe glob ou avec des expressions régulières.



       try (DirectoryStream&lt;Path&gt; stream = Files.newDirectoryStream(homePath, "*.{java,xml}")) {
           for (Path entry : stream) {
               System.out.println(entry);
           }
       }



En fait, la vrai puissance de cette méthode est dans sa capacité à parcourir de gros répertoires, contrairement à java.io.File qui construit d&#8217;abord un tableau avant de le mettre à disposition.


Pour aller plus loin dans la puissance, on peut utiliser la méthode walkFileTree. Celle-ci permet de parcourir toute une structure hiérarchique de répertoire à partir d&#8217;une racine, en utilisant un visiteur. Par exemple, pour rechercher des fichiers source en java, nous pouvons parcourir l&#8217;arbre de répertoires à partir de la racine du projet, et détecter les fichiers *.java.


Nous commençons par écrire un visiteur. Celui-ci doit implémenter l&#8217;interface java.nio.file.FileVisitor, éventuellement en héritant de java.nio.file.SimpleFileVisitor. L&#8217;évaluation du nom du fichier pourrait se faire par comparaison de chaînes de caractère mais gagne en élégance si on utilise un PathMatcher, avec une syntaxe glob ou regexp, comme pour le DirectoryStream.



   private static class SourceFileVisitor extends SimpleFileVisitor&lt;Path&gt; {
       private static final Path ROOT = Paths.get("");
       private static final PathMatcher SOURCE_MATCHER = ROOT.getFileSystem().getPathMatcher("glob:*/.java");

       @Override
       public FileVisitResult visitFile(Path path, BasicFileAttributes attr) {
           if (SOURCE_MATCHER.matches(path)) {
               System.out.println(path);
           }
           return FileVisitResult.CONTINUE;
       }
   }



Ensuite, l&#8217;utilisation de ce visiteur se fait simplement par l&#8217;appel de la méthode walkFileTree.



       Files.walkFileTree(Paths.get(""), new SourceFileVisitor());
       // =&gt; src/org/sewatech/java7/nio2/UsingFileStore.java
       // =&gt; src/org/sewatech/java7/nio2/SymLinkSupport.java
       // =&gt; src/org/sewatech/java7/nio2/Directories.java
       // =&gt; src/org/sewatech/java7/nio2/MetadataAttributes.java





Lire un fichier, écrire dans un fichier


Pour lire séquentiellement le contenu d&#8217;un fichier, il fallait jusqu&#8217;à maintenant instancier un flux de type FileReader ou FileInputStream et l&#8217;associer avec des flux de filtre, comme un BufferedReader. Avec NIO2, ça n&#8217;a pas changé&#8230;&#8203; Seule différence, la classe java.nio.file.Files fournit des méthodes utilitaires pour nous simplifier le travail.


Ainsi pour parcourir le contenu d&#8217;un fichier texte, on peut utiliser Files.newBufferedReader.



       Charset UTF8 = Charset.forName("UTF-8");

       Path sourcePath = Paths.get("src/org/sewatech/java7/nio2/UsingFileStore.java");
       try (BufferedReader reader = Files.newBufferedReader(sourcePath, UTF8)) {
           String line = null;
           while ((line = reader.readLine()) != null) {
               System.out.println(line);
           }
       }



Évidemment, il existe l&#8217;équivalent pour écrire dans un fichier.



       Path sourcePath = Paths.get("nothing.txt");
       String content = "line 1\nline 2\nline 3\n...";
       try (BufferedWriter writer = Files.newBufferedWriter(sourcePath, UTF8)) {
           writer.write(content);
       }



La classe Files offre aussi des méthodes pour obtenir un flux sans buffer (newInputStream et newOutputStream) ainsi que des canaux NIO pour des accès directs (newByteChannel).


Enfin, pour les petits fichiers Files nous simplifie la vie en nous fournissant directement le contenu binaire (readAllBytes) ou textuel (readAllLines).



       Path sourcePath = Paths.get("src/org/sewatech/java7/nio2/UsingFileStore.java");
       List&lt;String&gt; lines = Files.readAllLines(sourcePath, UTF8);
       for (String line : lines) {
           System.out.println(line);
       }





Liens symboliques


Comme nous l&#8217;avons déjà vu, la méthode toRealPath de Path permet de résoudre les liens symboliques dans les chemins.



       Path docRealPath = docPath.toRealPath();
       // =&gt; /home/alexis/Documents/nio



Le support des liens symboliques est aussi assuré dans la classe Files. Pour la suppression d&#8217;un lien, c&#8217;est la même méthode delete que pour les fichiers qui est utilisé.



       Files.delete(docPath);



On peut vérifier que le répertoire réel existe toujours.



       System.out.println("docRealPath exists : " + Files.exists(docRealPath));
       // =&gt; false



Pour la création du lien, on utilise la méthode createSymbolicLink.



       Files.createSymbolicLink(docPath, docRealPath);
       System.out.println("docRealPath exists : " + Files.exists(docRealPath));
       // =&gt; false



La méthode isSymbolicLink permet de vérifier si le chemin point sur un lien symbolique.



       System.out.println("docPath is a sym link : " + Files.isSymbolicLink(docPath));



Enfin, la méthode readSymbolicLink donne le même résultat que path.toRealPath dans le cas des liens symboliques et renvoie une NotLinkException sinon.



       System.out.println("Real path : " + Files.readSymbolicLink(docPath));
       // =&gt; /home/alexis/Documents/nio





Permissions Posix


L&#8217;information de sécurité la plus simple à obtenir est l&#8217;utilisateur propriétaire du fichier. Cette information est accessible directement par la classe Files.



       UserPrincipal owner = Files.getOwner(manifestPath);
       System.out.println(owner);
       // =&gt; alexis



Pour connaître le groupe d&#8217;appartenance du fichier, il faut passer par les attributs de méta-données. Ceux-ci fournissent un autre moyen de récupérer l&#8217;utilisateur propriétaire.



       PosixFileAttributes attributes = Files.readAttributes(manifestPath, PosixFileAttributes.class);
       UserPrincipal owner = attributes.owner();
       GroupPrincipal group = attributes.group();
       System.out.println("Le fichier appartient à " + owner + ":" + group);



L&#8217;information détaillée des permission peut être récupérée par le même objet d&#8217;attributs (méthode permissions) ou directement via la classe Files (getPosixFilePermissions)



       Set&lt;PosixFilePermission&gt; permissions = attributes.permissions();
       System.out.println(PosixFilePermissions.toString(permissions));
       // =&gt; rw-r--r--



Ces permissions peuvent aussi être modifiées, à condition que notre process en ait la permission, bien sûr.



       Set&lt;PosixFilePermission&gt; permissions = PosixFilePermissions.fromString("rw-rw-r--");
       Files.setPosixFilePermissions(manifestPath, permissions);



Cette façon de procéder est parfaite pour les développeurs habitués à Linux ou Unix. Les autres préféreront probablement manipuler les permissions une à une. Ils auront l&#8217;avantage d&#8217;utiliser un type énuméré, plus rigoureux qu&#8217;une simple chaîne de caractères.



       PosixFilePermission[] permissionsArray = {PosixFilePermission.OWNER_READ,
                                                 PosixFilePermission.OWNER_WRITE,
                                                 PosixFilePermission.GROUP_READ,
                                                 PosixFilePermission.OTHERS_READ};
       Set&lt;PosixFilePermission&gt; newPermissions = new HashSet&lt;&gt;(Arrays.asList(permissionsArray));
       Files.setPosixFilePermissions(manifestPath, newPermissions);



Cette façon de procéder peut être pratique pour ajouter ou supprimer une permission.



       Set&lt;PosixFilePermission&gt; permissions = attributes.permissions();
       permissions.add(PosixFilePermission.OTHERS_EXECUTE);
       permissions.remove(PosixFilePermission.OTHERS_READ);
       Files.setPosixFilePermissions(path, permissions);





Partitions et disques


Une machine peut avoir accès à plusieurs stockages, comme des volumes ou des partitions. Ces stockages sont représentés dans NIO2 par des java.nio.file.FileStore.


On peut retrouver le stockage pour un chemin.



       Path homePath = Paths.get(System.getProperty("user.home"));
       FileStore homeStore = Files.getFileStore(homePath);
       System.out.println(homeStore.name());
       // =&gt; rootfs



A partir de là, on peut savoir si celui-ci supporte les attributs Posix ou Dos. On peut aussi retrouver les informations de taille, d&#8217;occupation et de place disponible qu&#8217;on avait déjà sur java.io.File.



       System.out.println("Support Posix : " + homeStore.supportsFileAttributeView(PosixFileAttributeView.class));
       // =&gt; true
       System.out.println("Taille (Go) : " + formatter.format(homeStore.getTotalSpace()/1048576));
       // =&gt; Taille (Go) : 11 536
       System.out.println("Espace disponible (Go) : " + homeStore.getUsableSpace()/1048576);
       // =&gt; Espace disponible (Go) : 880
       System.out.println("Espace inoccupé (Go) : " + homeStore.getUnallocatedSpace()/1048576);
       // =&gt; Espace inoccupé (Go) : 1466



Il est possible de récupérer l&#8217;ensemble des stockages d&#8217;un système.



       Iterable&lt;FileStore&gt; fileStores = FileSystems.getDefault().getFileStores();
       for (FileStore fileStore : fileStores) {
           System.out.print(fileStore);
           System.out.println(", type : " + fileStore.type());
       }
       // =&gt; / (/dev/sda1), type : ext4
       // =&gt; /proc (proc), type : proc
       // =&gt; ...
       // =&gt; /media/sf_stockage (stockage), type : vboxsf



En revanche, je n&#8217;ai pas trouvé de moyen de récupérer le point de montage du store. Cette information est affichée dans le toString, mais n&#8217;est pas disponible dans l&#8217;interface publique.




Notification de changement


L&#8217;objet central du dispositif de surveillance et de notification est de type java.nio.file.WatchService et s&#8217;obtient à partir du FileSystem.



       WatchService watcher = FileSystems.getDefault().newWatchService();



un WatchService est capable de surveiller n&#8217;importe quel objet Watchable, comme un Path. Il suffit d&#8217;effectuer l&#8217;enregistrement en précisant quels événements doivent être surveillés (constantes de StandardWatchEventKinds).



       Path sourcePath = Paths.get("nothing.txt");
       WatchKey key = sourcePath.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);



L&#8217;annulation de la surveillance d&#8217;un fichier se fait en annulant l&#8217;objet WatchKey et l&#8217;annulation de toutes les surveillances set fait en clôturant le WatchService.



       key.cancel();
       // ou
       watcher.close();



Entre-temps, l&#8217;attente d&#8217;événements peut se faire avec la méthode take qui attend indéfiniment ou avec la méthode poll qui attend pendant une durée limitée, voire pas du tout. Les événements sont consignés sur la clé, qui doit être réinitialisée après traitement des événements.



       watcher.take();
       for (WatchEvent&lt;?&gt; event : key.pollEvents()) {
           System.out.println(event.context() + " - " + event.kind());
       }
       key.reset();





Conclusion


Tout d&#8217;abord, je dois avouer que lorsque j&#8217;ai commencer la visite de NIO2, je ne m&#8217;attendais pas à tant de nouveautés, d&#8217;autant que ce sujet n&#8217;a pas fait beaucoup de bruit à la sortie de JavaSE 7.


Pour résumer, on peut lister les nouveautés comme ceci :




Les fonctionnalités de classe java.io.File sont reproduites et réparties dans les classes Path, Files, FileStore et FileSystem du package java.nio.file. Les responsabilités sont ainsi mieux organisées.


Le support des spécificités des systèmes de fichiers Posix est apporté, en particulier les permissions et les liens symboliques.


Le support des notifications a été ajouté.




Quelques références pour approfondir le sujet :




http://download.oracle.com/javase/tutorial/essential/io/fileio.html


http://openjdk.java.net/projects/nio/


https://github.com/hasalex/Java7-examples, pour le code source des exemples




</description>
          <pubDate>2011-08-16T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java7/NIO2-FileSystem</link>
          <guid isPermaLink="true">https://www.jtips.info/Java7/NIO2-FileSystem</guid>
        </item>
      
    
      
        <item>
          <title>JavaSE 7 - try-with-resources</title>
          <description>
Nouveautés Java 7 : fermeture automatique des ressources


Dans plusieurs APIs de Java, il est nécessaire de clore les ressources utilisées. C&#8217;est le cas pour JDBC ou IO et plus généralement pour celle utilisant des ressources externes. Pour éviter la fuite de ressources, il est nécessaire de mettre l&#8217;appel de la méthode de clôture dans un bloc finally. Si on ajoute à cela la gestion des exceptions, généralement validées (checked), on obtient un code très peu lisible, avec beaucoup de code qui n&#8217;a rien à voir avec l&#8217;objectif fonctionnel.


Dans l&#8217;exemple ci-dessous, on ouvre un fichier texte, qu&#8217;on lit ligne par ligne.



       BufferedReader br;
       try {
           br = new BufferedReader(new FileReader("."));
           try {
               System.out.println(br.readLine());
           } catch (IOException e) {
               System.err.println("Problème de lecture du fichier");
           } finally {
               try {
                   br.close();
               } catch (IOException ex) {
               }
           }
       } catch (FileNotFoundException ex) {
           System.err.println("Fichier non trouvé");
       }



Dans cette portion, on commence par ouvrir un flux de lecture sur le fichier et gérer l&#8217;exception d&#8217;absence de fichier. Puis on lit le fichier ligne à ligne, ce qui peut provoquer une exception d&#8217;entrée-sortie. On clôt le flux dans le finally, ce qui peut provoquer une exception d&#8217;entrée-sortie qui ne doit pas être traitée sous peine de masquer l&#8217;exception de lecture. Ça fait beaucoup de code pour lire un fichier texte&#8230;&#8203;


Le try-with-resource permet de déclarer la ressource à clore dans le try, ce qui en simplifie grandement la structure.



       try (BufferedReader br = new BufferedReader(new FileReader("."))) {
           System.out.println(br.readLine());
       } catch (IOException e) {
           System.err.println("Problème de lecture du fichier");
       }



Cette notation peut être utilisée pour n&#8217;importe quelle classe qui implémente la nouvelle interface java.lang.AutoCloseable.
</description>
          <pubDate>2011-08-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java7/TryWithResource</link>
          <guid isPermaLink="true">https://www.jtips.info/Java7/TryWithResource</guid>
        </item>
      
    
      
        <item>
          <title>JavaSE 7 - Strings in switch statements</title>
          <description>
Nouveautés JavaSE 7 : support des String dans l&#8217;instruction switch


Dans un premier temps, seuls les types entiers (byte, short, int, long, char) pouvaient être utilisés dans les switch, sous forme littérale ou par l&#8217;intermédiaire de constantes.



       switch (month) {
           case Calendar.DECEMBER:
           case Calendar.JANUARY:
           case Calendar.FEBRUARY:
               season = Season.WINTER;
               break;
           ...
       }



Avec JavaSE 5, le switch a été étendu aux types énumérés.



   enum Season {
       SPRING, SUMMER, FALL, WINTER;
   }




       switch (season) {
           case WINTER:
               headgear = "woolly hat";
               break;
           ...
       }



La version 7 apporte maintenant le support du type String.



       switch (headgear) {
           case "none" :
               //...
               break;
       }

</description>
          <pubDate>2011-08-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java7/StringInSwitch</link>
          <guid isPermaLink="true">https://www.jtips.info/Java7/StringInSwitch</guid>
        </item>
      
    
      
        <item>
          <title>JavaSE 7 - multi-catch</title>
          <description>
Jusqu&#8217;à maintenant, dans la structure try-catch, chaque catch ne pouvait traiter qu&#8217;un seul type d&#8217;exception. Donc c&#8217;est un traitement spécifique pour chaque type d&#8217;exception attrapée.



       try {
           Class.forName("org.sewatech.examples.java7.MyClass").newInstance();
           //...
       } catch (ClassNotFoundException e) {
           System.err.printf("Problème de création de mon objet (%s)\n", e);
       } catch (InstantiationException e) {
           System.err.printf("Problème de création de mon objet (%s)\n", e);
       } catch (IllegalAccessException e) {
           System.err.printf("Problème de création de mon objet (%s)\n", e);
       }



On peut certes jouer avec l&#8217;héritage entre classes d&#8217;exception, mais on arrive rapidement à quelque chose comme catch(Exception ex). Sans grand intérêt.



       try {
           Class.forName("org.sewatech.examples.java7.MyClass").newInstance();
           //...
       } catch (Exception e) {
           System.err.printf("Problème de création de mon objet (%s)\n", e);
       }



Pour éviter ce tout-ou-rien, Java SE 7 permet maintenant de traiter plusieurs types d&#8217;exceptions, séparés par un 'pipe', pour chaque catch.



       try {
           Class.forName("org.sewatech.examples.java7.MyClass").newInstance();
           //...
       } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
           System.err.printf("Problème de création de mon objet (%s)\n", e);
       }



Au passage, le rethrow est mieux géré,  avec une meilleure inférence.


Très pratique, cette nouveauté ne devrait pas être tant utilisée que cela ; la faute aux frameworks comme hibernate, JPA, Spring, CDI,&#8230;&#8203; qui nous aident à mieux gérer les exceptions et, surtout, à en séparer le traitement du code métier.
</description>
          <pubDate>2011-08-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java7/MultiCatch</link>
          <guid isPermaLink="true">https://www.jtips.info/Java7/MultiCatch</guid>
        </item>
      
    
      
        <item>
          <title>JavaSE 7 - valeurs littérales formatées</title>
          <description>
Cette nouveauté paraît anecdotique, mais peut faciliter la lecture de code qui manipule des grandes valeurs littérales numériques.


Par exemple, que vaut 2365876245 ? Deux cent millions, deux milliards, vingt milliards ? La même valeur est plus lisible si elle est écrite 2_365_876_245.


Ainsi,



       long val = 2365876245L;



peut maintenant s&#8217;écrire



       long val = 2_365_876_245L;



Autre nouveauté, les valeurs littérales peuvent être écrites en binaire. Jusqu&#8217;à maintenant, le décimal, l&#8217;octal et l’hexadécimal étaient supportés. Pour écrire une valeur en binaire, il faut la préfixer par 0b.



       int binaryValue = 0b011100101;



Pour rappel, le préfixe pour l&#8217;hexadécimal est 0x et celui pour l&#8217;octal est 0. Ainsi, la valeur 229 peut s&#8217;écrire sous les quatre formes suivantes :



       int decimalValue = 229;
       int binaryValue = 0B011100101;
       int hexaValue = 0xe5;
       int octalValue = 0345;

</description>
          <pubDate>2011-08-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java7/Literal</link>
          <guid isPermaLink="true">https://www.jtips.info/Java7/Literal</guid>
        </item>
      
    
      
        <item>
          <title>Sécurité JMX sous JBoss (script)</title>
          <description>
Pour facilité de le travail pour la sécurisation des accès JMX, je propose un script shell qui automatise les tâches :




Activation du scurity-constraint dans JMX-Console


Activation de l&#8217;intercepteur d&#8217;authentification dans JMX-Invoker


Suppression des mots de passe par défaut





# JMX console
sed -i "s/&lt;security-constraint&gt;/--&gt;&lt;security-constraint&gt;/g"               \
       $JBOSS_HOME/server/default/deploy/jmx-console.war/WEB-INF/web.xml
sed -i "s/&lt;\/security-constraint&gt;/&lt;\/security-constraint&gt;&lt;\!--/g"         \
       $JBOSS_HOME/server/default/deploy/jmx-console.war/WEB-INF/web.xml

sed -i "s/&lt;security-domain&gt;/--&gt;&lt;security-domain&gt;/g"                       \
       $JBOSS_HOME/server/default/deploy/jmx-console.war/WEB-INF/jboss-web.xml
sed -i "s/&lt;\/security-domain&gt;/&lt;\/security-domain&gt;&lt;\!--/g"                 \
       $JBOSS_HOME/server/default/deploy/jmx-console.war/WEB-INF/jboss-web.xml

# JMX invoker
sed -i "s/&lt;descriptors&gt;/--&gt;&lt;descriptors&gt;/"                                      \
       $JBOSS_HOME/server/default/deploy/jmx-invoker-service.xml
sed -i "s/&lt;\/descriptors&gt;/&lt;\/descriptors&gt;&lt;\!--/"                                \
       $JBOSS_HOME/server/default/deploy/jmx-invoker-service.xml

# Modification du mot de passe
password=newpwd
sed -i "s/=admin/$password/"                                              \
       $JBOSS_HOME/server/default/conf/props/jmx-console-users.properties



Ce script a été testé sur JBoss AS 4.x et 5.x. Il est inutile sur JBoss EAP puisque celui-ci est sécurisé par défaut.
</description>
          <pubDate>2011-05-23T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/SecureJMX/Script</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/SecureJMX/Script</guid>
        </item>
      
    
      
        <item>
          <title>Java User Groups de France</title>
          <description>
Les JUGs ont fleuri en France et alentours au début des années 2010.
Vous en trouverez probablement un proche de chez vous.


Carte des JUGs


J&#8217;avais initialement publié cette carte sur mon blog, puis je l&#8217;ai transférée ici pour en faciliter les mises à jour.


Je me suis basé sur l&#8217;ancienne carte officielle et sur la liste des JUGs de Jean-Michel Doudoux.







Vous pouvez utiliser cette librement et je suis preneur de toute amélioration : oubli, logo plus récent ou de meilleur qualité, repositionnement.




Java User Groups


En France










































Juste à coté de la France











&#160;
&#160;





</description>
          <pubDate>2011-04-06T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JavaUserGroups</link>
          <guid isPermaLink="true">https://www.jtips.info/JavaUserGroups</guid>
        </item>
      
    
      
        <item>
          <title>Logging dans JBoss AS 6</title>
          <description>
A partir de JBoss AS 6, le serveur n&#8217;utilise plus Log4J pour ses logs internes, mais sa propre solution de logging : JBoss LogManager. Cette solution est une extension de l&#8217;API java.util.logging. Par conséquent, ses concepts sont très proches de ceux de Log4J, avec quelques petites améliorations.


Comme j.u.l et Log4J, JBoss LogManager utilise des Loggers, pour lesquels on configure le niveau (Level) de sortie. Enfin, on configure les éléments de sortie, appelés ici Handlers. Tout ceci se configure dans un fichier jboss-logging.xml dans le répertoire deploy de JBoss.


Configuration


La configuration se fait par un fichier jboss-logging.xml dans le répertoire deploy du profil démarré (default, all,&#8230;&#8203;). Le contenu de ce fichier n&#8217;est pas très éloigné de celui d&#8217;un fichier de configuration de Log4J, avec un format un peu différent.


Dans la première partie, on configure les handlers, puis les loggers pour finir avec le root-logger. Par défaut, les traces sont envoyées vers la sortie standard (console-handler) et vers le fichier log/server.log, à partir du niveau INFO. Comme pour JBoss AS 5, le répertoire de log peut être modifié avec l&#8217;option -Djboss.server.log.dir, ainsi que le niveau avec l&#8217;option -Djboss.server.log.threshold.




Loggers


Logger

Pour chaque logger, on spécifie le niveau et, éventuellement, de handlers. En somme, rien de bien différent de Log4J. On retrouve aussi la même notion d&#8217;additivity.



  &lt;logger category="org.jboss.system.server.Server"&gt;
     &lt;level name="INFO"/&gt;
     &lt;handlers&gt;
        &lt;handler-ref name="CONSOLE"/&gt;
     &lt;/handlers&gt;
  &lt;/logger&gt;




Root logger

On retrouve aussi le logger racine.



  &lt;root-logger&gt;
     &lt;level name="${jboss.server.log.threshold:INFO}"/&gt;
     &lt;handlers&gt;
        &lt;handler-ref name="CONSOLE"/&gt;
        &lt;handler-ref name="FILE"/&gt;
     &lt;/handlers&gt;
  &lt;/root-logger&gt;



Avec cette configuration, l&#8217;option -Djboss.server.log.threshold passée au lancement de JBoss permet de modifier le niveau de sortie global.



Additivité

La notion d&#8217;additivity de Log4J se retrouve sous forme d&#8217;un attribut use-parent-handlers.



  &lt;logger category="org.jboss.bootstrap" use-parent-handlers="false"&gt;
     &lt;level name="INFO"/&gt;
     &lt;handlers&gt;
        &lt;handler-ref name="CONSOLE"/&gt;
     &lt;/handlers&gt;
  &lt;/logger&gt;






Handlers


Les handlers sont les sorties pour les traces. On retrouve ici le même vocabulaire que pour java.util.logging, mais les techniques ressemblent à celles de Log4J : console, fichier avec rotation périodique, fichier avec rotation sur la taille,&#8230;&#8203; La grande différence avec Log4J est l&#8217;utilisation de balises typées.


Console

Pour envoyer les traces dans la sortie standard, on utilise la balise &lt;console-handler&gt;. On peut limiter les traces à un niveau minimum. Les possibilités de mise en forme sont détaillées plus bas.



  &lt;console-handler name="CONSOLE" autoflush="true" target="System.out"&gt;
     &lt;level name="INFO"/&gt;
     &lt;formatter&gt;
        &lt;pattern-formatter pattern="..."/&gt;
     &lt;/formatter&gt;
  &lt;/console-handler&gt;




Fichier à rotation périodique

Pour envoyer les traces dans un fichier et assurer une rotation périodique, on utilise la balise &lt;periodic-rotating-file-handler&gt; et on précise le nom du fichier, le mode ajout / remplacement et le suffixe. Comme pour Log4J, le suffixe est ajouté aux fichiers sauvegardé après la rotation ; il sert aussi à spécifier la période de rotation. Par défaut, la rotation est quotidienne.



  &lt;periodic-rotating-file-handler
        file-name="${jboss.server.log.dir}/server.log" suffix=".yyyy-MM-dd"
        name="FILE" autoflush="true" append="true"&gt;
     &lt;formatter&gt;
        &lt;pattern-formatter pattern="..."/&gt;
     &lt;/formatter&gt;
  &lt;/periodic-rotating-file-handler&gt;




Fichier à rotation sur la taille

Pour envoyer les traces dans un fichier et assurer une rotation périodique, on utilise la balise &lt;size-rotating-file-handler&gt; et on précise la taille de chaque fichier et le nombre de fichiers conservés.



  &lt;size-rotating-file-handler
        file-name="${jboss.server.log.dir}/server.log" rotate-size="500k" max-backup-index="5"
        name="FILE" autoflush="true" append="true"&gt;
     &lt;formatter&gt;
        &lt;pattern-formatter pattern="..."/&gt;
     &lt;/formatter&gt;
  &lt;/size-rotating-file-handler&gt;




Sortie asynchrone

Pour que les traces sortent sans bloquer le programme, on les fait passer par un handler asynchrone, via la balise &lt;async-handler&gt;. On lui associe ensuite les sorties concrètes.



  &lt;async-handler name="ASYNC"&gt;
     &lt;sub-handlers&gt;
        &lt;handler-ref name="FILE"/&gt;
        &lt;handler-ref name="CONSOLE"/&gt;
     &lt;/sub-handlers&gt;
  &lt;/async-handler&gt;




Gestion des erreurs

Dans chaque handler, on trouve généralement un gestionnaire d&#8217;erreur de type &lt;only-once/&gt; qui évite le bégaiement en cas d&#8217;erreur de logging.



  &lt;foo-handler name="FOO"&gt;
     &lt;error-manager&gt;
        &lt;only-once/&gt;
     &lt;/error-manager&gt;
     ...
  &lt;/foo-handler&gt;




Autres handlers

Il est possible d&#8217;intégrer n&#8217;importe quel handler qu respecte l&#8217;API j.u.l.



  &lt;handler name="FOO" class="org.sewatech.logging.handler.MyHandler"&gt;
     ...
  &lt;/handler&gt;




Appenders Log4J

Enfin, JBoss LogManager est capable de bénéficier de toute la richesse de son ancêtre, car il propose un handler qui redirige vers un appender Log4J.



  &lt;log4j-appender name="SMTP" class="org.apache.log4j.net.SMTPAppender"&gt;
     &lt;properties&gt;
        &lt;property name="to"&gt;admin@admin.sewatech.net&lt;/property&gt;
        &lt;property name="from"&gt;jboss@admin.sewatech.net&lt;/property&gt;
        &lt;property name="subject"&gt;JBoss AS 6 Sever Errors&lt;/property&gt;
        &lt;property name="SMTPHost"&gt;smtp.sewatech.net&lt;/property&gt;
        &lt;property name="bufferSize"&gt;10&lt;/property&gt;
     &lt;/properties&gt;

     &lt;formatter&gt;
        &lt;pattern-formatter pattern="..."/&gt;
     &lt;/formatter&gt;
  &lt;/log4j-appender&gt;



On peut ainsi envoyer les traces vers une base de données, un sujet JMS, le réseau,&#8230;&#8203;





Formatter


Conversions

JBoss LogManager supporte à peut prêt les mêmes formats que le PatternLayout de Log4J. On retrouve la même façon de spécifier le format, avec les mêmes caractères de conversion :




c = nom du logger


d = date éventuellement formattée selon SimpleDateFormat


F = nom du fichier source


C = nom de la classe


M = nom de la méthode


L = n° de ligne


l = localisation : fichier source  + classe + méthode + n° de ligne


m = message avec stacktrace


p = niveau


r = durée depuis le démarrage


t = nom du thread


x = NDC


X = MDC


n = séparateur de ligne




Par exemple, %d %-5p [%c] (%t) %m%n est compatible avec JBoss LogManager et Log4J.


JBoss LogManager a introduit quelques conversion supplémentaires, en particulier pour mieux afficher les exceptions :




s = message sans stacktrace


e = stacktrace simple


E = stacktrace étendue




Dans le mode étendu, LogManager essaie d&#8217;extraire le nom de l&#8217;archive et la version d&#8217;implémentation ou de spécification correspondant à chaque ligne du stack trace. Cela peut faciliter certains diagnostics, mais peut aussi dégrader les performances en cas de nombreuses stack traces .


Ainsi, le format ` "%d %-5p [%c] (%t) %m%n" ` peut avantageusement être remplacé par ` "%d %-5p [%c] (%t) %s%E%n" ` ou ` "%d %-5p [%c] (%t) %s%e%n" `.


Il me reste encore un caractère à étudier :




k = clé de ressource
=== Segments




Les conversions %c et %C sont à segments, c&#8217;est-à-dire qu&#8217;elles sont constituées de mots séparés par des caractères '.'. Pour ce type de conversion, on  peut sélectionner les n derniers mots : ` "%c{1}" `.



Justification

Toutes les conversions sont justifiable. Plus précisément, elles peuvent être justifiée, tronquées et alignées. Là aussi, la façon de spécifier le format ressemble énormément à Log4J : ` %20.30c ` ou ` %-20.30c `. La bonne nouvelle, c&#8217;est que la façon de tronquer est différente de celle de Log4J. En effet, avec ` %.30m `, LogManager conserve le début du message alors que Log4J en conservait la fin.





Filtres


Comme dans java.util.logging, il est possible de filtrer les logs au niveau des handlers ou des loggers (bien que pour ces derniers, je n&#8217;ai pas réussi à les faire fonctionner).


JBoss LogManager fournit une série de filtres, avec les balises qui correspondent.




accept


deny


match : correspond





  &lt;filter&gt;
     &lt;match pattern="Started"/&gt;
  &lt;/filter&gt;





level





  &lt;filter&gt;
     &lt;level level="INFO"/&gt;
  &lt;/filter&gt;





level-range





  &lt;filter&gt;
     &lt;level-range min-level="DEBUG" max-level="WARN"/&gt;
  &lt;/filter&gt;



Il fournit aussi des filtres pour combiner d&#8217;autres filtres.




not


all


any




Enfin, il fournit des filtres de transformation.




replace


change-level






Utilisation autonome


JBoss LogManager peut être utilisé comme logger pour n&#8217;importe quelle application. Pour cela, il faut configurer l&#8217;application pour que celui-ci remplace le LogManager de java.util.logging. La configuration se fait alors dans un fichier logging.properties standard. Celui contenu dans bin/run.jar de JBoss AS 6 peut servir d&#8217;exemple.




Conclusion


JBoss AS 6 n&#8217;utilise plus Apache Log4J, mais JBoss LogManager. Les concepts sont les mêmes, mais la configuration change légèrement. JBoss LogManager peut utiliser Log4J, mais n&#8217;en dépend que de façon optionnelle.


Pour plus de détails, le meilleur endroit, au moment ou j&#8217;écris ces lignes, est le schéma du fichier (/deployers/jboss-logging.deployer/logging-service-metadata.jar/META-INF/schema).


</description>
          <pubDate>2011-03-01T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/LogManager</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/LogManager</guid>
        </item>
      
    
      
        <item>
          <title>WeldSE/Test/Arquillian-Pom</title>
          <description></description>
          <pubDate>2011-01-30T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WeldSE/Test/Arquillian-Pom</link>
          <guid isPermaLink="true">https://www.jtips.info/WeldSE/Test/Arquillian-Pom</guid>
        </item>
      
    
      
        <item>
          <title>Tests de beans CDI avec WeldSE et Arquillian</title>
          <description>
La question des tests unitaires se pose de façon répétitive, dès lors qu&#8217;on développe avec un modèle de composants basé sur un conteneur. Si CDI est prévu pour fonctionner dans JavaEE, le projet Weld fournit l&#8217;utilitaire Weld SE pour utiliser CDI en environnement JavaSE.


Les projets Weld et Seam 3 ne fournissent pas beaucoup d&#8217;aide pour les tests unitaires de CDI parce que leur stratégie de test pour tous les modèles de composants de JavaEE passe par Arquillian.


Dans cet article, nous verrons quelques techniques pour tester des composants CDI avec jUnit, grâce à Weld SE.


Before / After


Pour tester un composant CDI, il faut que Weld SE soit démarré au préalable, puis il faut demander l&#8217;instance du composant au conteneur.


Cette portion de code peut donc être intégrée dans une méthode d&#8217;initialisation de la classe de test, sans oublier une méthode de fin de tests pour arrêter Weld.



 public class TotoServiceTest {

     TotoService service;
     Weld weld;

     @BeforeClass
     public void initializeWeld() {
         weld = new Weld();
         WeldContainer container = weld.initialize();
         service = container.instance().select(TotoService.class).get();
     }

     @AfterClass
     public void shutdownWeld() {
         weld.shutdown();
     }

     ...
 }





Rule


Ce code peut être factorisé dans une Rule pour être utilisé depuis n&#8217;importe quelle classe de test.



 public class WeldSeRule extends ExternalResource {

     private Weld weld;
     private WeldContainer container;

     @Override
     protected void before() throws Throwable {
         weld = new Weld();
         container = weld.initialize();
     }

     @Override
     protected void after() {
         weld.shutdown();
     }

     public &lt;T&gt; T getBean(Class&lt;T&gt; testedClass) {
         return container.instance().select(testedClass).get();
     }
 }



Une fois cette Rule créée, on doit la déclarer dans chaque classe de test et appeler la méthode getBean() dans une méthode @Before.



public class TotoServiceTest {
   @Rule
   public WeldSeRule weld = new WeldSeRule();

   TotoService service;

   @Before
   public void init() {
       service = weld.getBean(TotoService.class);
   }
   ...
}



Tout ça reste encore assez fastidieux, et la valeur ajoutée par rapport aux simples @BeforeClass et @AfterClass sont faibles. Il faudrait, pour que ce soit avantageux, injecter automatiquement l&#8217;objet à tester.




Runner


Cette troisième technique est utilisée avec Spring qui fournit un runner permettant à notre classe de test de devenir elle-même un bean géré et donc de supporter l&#8217;injection et toutes les autres fonctionnalités du framework. Un tel runner a existé dans le trunk de Weld, mais a été supprimé. Je suppose que pour faire quelque chose d&#8217;exhaustif, l&#8217;effort était important et que l&#8217;équipe de développement a préféré porter son effort sur Arquillian. Mon ambition ici est beaucoup plus raisonnable : je veux juste démarrer Weld SE puis tester une instance de ma classe de test gérée par Weld.


Mon runner est donc une classe qui hérite du runner par défaut, puis je redéfinis la méthode run pour ajouter le démarrage et l&#8217;arrêt de Weld SE. Enfin, je redéfinis la méthode createTest() pour lui faire retourner une instance créée dans Weld.



 public class WeldRunner extends BlockJUnit4ClassRunner {

     private Weld weld;
     private WeldContainer container;

     public WeldRunner(Class&lt;?&gt; klass) throws InitializationError {
         super(klass);
     }

     @Override
     public void run(RunNotifier notifier) {
         initializeWeld();
         super.run(notifier);
         shutdownWeld();
     }

     @Override
     protected Object createTest() throws Exception {
         return container.instance().select(getTestClass().getJavaClass()).get();
     }

     private void initializeWeld() {
         weld = new Weld();
         container = weld.initialize();
     }

     private void shutdownWeld() {
         weld.shutdown();
     }
 }



Il me suffit ensuite de lancer mes tests avec ce runner et de demander l&#8217;injection des objets à tester.



 @RunWith(WeldRunner.class)
 public class TotoServiceTest {

     @Inject
     TotoService service;

     @Test
     public void trouveParCleShouldWorkWithValidId() {
         Long id = 1L;
         Toto toto = service.trouveParCle(id);
         assertNotNull("Le toto n'a pas été trouvé.", toto);
         assertEquals("Le toto n'a pas la bonne clé.", id, toto.id);
     }
     ...
 }



Ce runner est certainement à améliorer, mais il fournit déjà le service que je lui demandais : initialiser mon environnement CDI sans polluer ma classe de test.




Arquillian


Arquillian est un outil développé par Red Hat / JBoss  pour tester des composants gérés (CDI, EJB, JPA, JSF,&#8230;&#8203;) dans leurs conteneurs. L&#8217;outil se compose des éléments suivants :




un Runner jUnit et des annotations associées,


des librairies pour embarquer les conteneurs,


Shrinkwrap, un outil pour empaqueter les classes à tester.




Au moment (janvier 2011) où j&#8217;écris ces lignes, Arquillian est en version alpha. Ceci se ressent avec des problèmes de compatibilité. Par exemple, la version alpha4 du conteneur Weld est compatible avec Weld 1.1.Beta1, mais pas avec la version finale. La situation devrait être rétablie avec les versions beta. En attendant, il faut utiliser une variante du conteneur fournie dans le projet Weld.


Par ailleurs, les versions alpha ne sont livrée que via Maven, et uniquement dans le repository public de JBoss.


Pour le développement, j&#8217;ai besoin des librairies suivantes :




org.jboss.weld:weld-core:1.1.0.Final, org.jboss.weld:weld-api:1.1.0.Final


org.jboss.spec:jboss-javaee-6.0:1.0.0.Final


org.slf4j:slf4j-log4j12:1.5.10, log4j:log4j:1.2.16




Pour la phase de test :




junit:junit:4.8.2


org.jboss.arquillian:arquillian-junit:1.0.0.Alpha4


org.jboss.weld.arquillian.container:arquillian-weld-ee-embedded-1.1:1.1.0.Final




Cette dernière librairie devra être remplacée par org.jboss.arquillian.container:arquillian-weld-ee-embedded-1.1, lorsque le projet aura atteint un meilleur niveau de stabilité. Au final, j&#8217;ai utilisé ce fichier pom.xml.


Dans cet environnement, je peux développer mon test. Je dois tout d&#8217;abord utiliser le Runner Arquillian. Ensuite, je dois préparer l&#8217;archive qui sera déployée dans le conteneur. Enfin, j&#8217;injecte le bean à tester et je développe les cas de test.



 @RunWith(Arquillian.class)
 public class TotoServiceTest {

     @Deployment
     public static Archive&lt;?&gt; createDeployment() {
         return ShrinkWrap
                    .create(JavaArchive.class, "toto.jar")
                    .addClasses(TotoService.class)
                    .addManifestResource(EmptyAsset.INSTANCE, "beans.xml");
     }

     @Inject
     TotoService service;

     ...
 }



Le reste de la classe de test est tout à fait similaire à un test unitaire traditionnel.




Mock


Enfin, pour pouvoir tester correctement et de façon unitaire, il faut pouvoir introduire des objets de mock.


La meilleure façon de produire un objet de mock avec CDI est de développer une méthode annotée en @Produces directement dans notre classe de test. Le problème, c&#8217;est que ce bean va entrer en conflit avec le vrai bean et rompt la règle de Higlander : "There can be only one". La solution passe donc par la principe des alternatives.


Notre producteur de mock devient une alternative. Attention, c&#8217;est la classe qui contient la méthode de production qui doit être une alternative car la méthode ne peut pas être activée. Problème de granularité qui pourrait être corrigé dans un avenir proche.



 @Alternative
 public class TotoDaoMockProducer {
     @Produces TotoDao mockDao() {
         return mock(TotoDao.class);
     }
 }



Il ne reste plus qu&#8217;à activer cette alternative, en déclarant le stéréotype alternatif dans le beans.xml de test.



 &lt;alternatives&gt;
     &lt;class&gt;org.sewatech.cdi.test.TotoDaoMockProducer&lt;/class&gt;
 &lt;/alternatives&gt;



Le principal problème est d&#8217;activer cette alternative dynamiquement, sans avoir à modifier manuellement le fichier beans.xml. Une des sources de ce problème vient aussi du fonctionnement un peu trop automatique de CDI : tous les beans trouvés sont référencés et même, tous les fichiers beans.xml sont chargés.


La spécification CDI 1.0 ne prévoit malheureusement pas de technique de configuration dynamique, par programmation. Cette fonctionnalité a été recensée dans les évolutions envisageables pour CDI 1.1. Il est peut-être possible d&#8217;arriver au même résultat en attaquant le moteur Weld, mais je n&#8217;ai pas réussi à le faire.


L&#8217;autre solution passe par Arquillian. Le fichier beans.xml peut être généré via ShrinkWrap, ce qui apporte la souplesse qu&#8217;on attendait.



 @Deployment
 public static Archive&lt;?&gt; createDeployment() {
     String testBeansXml = "&lt;beans xmlns=\"link:http://java.sun.com/xml/ns/javaee[http://java.sun.com/xml/ns/javaee]\""
                                + "xmlns:xsi=\"link:http://www.w3.org/2001/XMLSchema-instance[http://www.w3.org/2001/XMLSchema-instance]\""
                                + "xsi:schemaLocation=\"link:http://java.sun.com/xml/ns/javaee[http://java.sun.com/xml/ns/javaee] link:http://java.sun.com/xml/ns/javaee/beans_1_0.xsd[http://java.sun.com/xml/ns/javaee/beans_1_0.xsd]\"&gt;"
                             + "&lt;alternatives&gt;&lt;class&gt;org.sewatech.cdi.service.TotoDaoMockProducer&lt;/class&gt;&lt;/alternatives&gt;"
                         + "&lt;/beans&gt;";
         return ShrinkWrap
                    .create(JavaArchive.class, "toto.jar")
                    .addPackage(TotoService.class.getPackage())
                    .addPackage(TotoDao.class.getPackage())
                    .addPackage(ConnectionManager.class.getPackage())
                    .addManifestResource(new ByteArrayAsset(testBeansXml.getBytes()), "test/beans.xml")
                    .addManifestResource("META-INF/beans.xml", "beans.xml");
     }
 }



Dans cet exemple, nous utilisons le fichier beans.xml classique et un fichier test/beans.xml construit dynamiquement.


La solution est cocasse : on utilise un outil dont le but est de faciliter les tests d&#8217;intégration pour exécuter des tests unitaires.


</description>
          <pubDate>2011-01-29T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WeldSE/Test</link>
          <guid isPermaLink="true">https://www.jtips.info/WeldSE/Test</guid>
        </item>
      
    
      
        <item>
          <title>JBoss/HornetQ</title>
          <description>
EN COURS DE REDACTION


HornetQ est le 3° moteur JMS de JBoss. Jusqu&#8217;à JBoss AS 4, on avait eu JBossMQ qui était de qualité médiocre. A partir de JBoss AS 4.2, il est possible de remplacer JBossMQ par JBoss Messaging. Celui-ci est fourni en standard avec JBoss AS 5.x. A partir de JBoss AS 6, c&#8217;est HornetQ qui est installé. Ce dernier est une vrai révolution par rapport à ses prédécesseurs ; il surclasse tous ses concurrents en performances.


Le sujet de cet article est de montrer comment utiliser HornetQ dans les versions 4 et 5 de JBoss AS.


Installer HornetQ


Il faut commencer par télécharger la version de JBoss AS que vous souhaitez utiliser : 4.2.3 ou 5.1.0. La procédure marchera aussi avec des versions plus anciennes, mais celles-ci sont de moindre qualité. On décompresse l&#8217;archive téléchargée ; pour ma part, je mets le répertoire décompressé dans /opt/java/


Ensuite, on télécharge HornetQ. A l&#8217;heure où j&#8217;écris l&#8217;article, j&#8217;utilise la version 2.1.2. Je décompresse aussi l&#8217;archive dans /opt/java/.


Ensuite, il faut renseigner la variable d&#8217;environnement JBOSS_HOME et exécuter le script de build placé dans le répertoire d&#8217;HornetQ correspondant à la version de JBoss AS utilisée.



 export JBOSS_HOME=/opt/java/jboss-5.1.0.GA
 cd /opt/java/hornetq-2.1.2.Final/config/jboss-as-5
 sh build.sh



Le script m&#8217;aura créé deux nouveaux profils dans JBoss : all-with-hornetq et default-with-hornetq. Le premier changement visible dans ces nouveaux profils est le remplacement du répertoire messaging par un répertoire hornetq.sar.




Configurer HornetQ


Destinations

La première tâche de configuration est de créer nos Queues et Topics. Cette tâche se fait dans le fichier deploy/hornetq.sar/hornetq-jms.xml.



  &lt;queue name="MyQ"&gt;
     &lt;entry name="/queue/MyQueue"/&gt;
  &lt;/queue&gt;

  &lt;topic name="MyT"&gt;
     &lt;entry name="/queue/MyTopic"/&gt;
  &lt;/topic&gt;



On apprécie ici la sobriété de la syntaxe qui tranche avec les versions précédentes. Par contre, on peut regretter de ne pas pouvoir déployer dans un fichier séparé, ce qui était très pratique pour l&#8217;automatisation du déploiement.



ConnectionFactory



Sécurité



Cluster



Connecteur

Netty,&#8230;&#8203;
-Dhornetq.remoting.netty.port
-Dhornetq.remoting.netty.batch.port
-Dhornetq.server-id


Binding de port ?



</description>
          <pubDate>2011-01-22T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/HornetQ</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/HornetQ</guid>
        </item>
      
    
      
        <item>
          <title>JUnit/Theory</title>
          <description>
L&#8217;annotation @Theory est utilisée en remplacement de l&#8217;annotation @Test.
Elle est utilisée pour des méthodes de test avec arguments et utilise les valeurs des champs annotés @DataPoint ou @DataPoints pour créer des combinaisons d&#8217;arguments.
</description>
          <pubDate>2011-01-12T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JUnit/Theory</link>
          <guid isPermaLink="true">https://www.jtips.info/JUnit/Theory</guid>
        </item>
      
    
      
        <item>
          <title>CDI - Portées avec WeldSE</title>
          <description>
Portées CDI


Les portées définies dans CDI sont




@RequestScoped : 1 instance rattachée à la requête HTTP


@SessionScoped : 1 instance rattachée à la session HTTP


@ConversationScoped : portée intermédiaire entre request et session


@ApplicationScoped : 1 instance unique pour l&#8217;application (ie le classloader)




De plus, il est possible de définir ses propres portées en créant des annotations marquées @ScopeType.




Portées Weld SE


En environnement SE, il n&#8217;y a pas de portée Request, Session ou Conversation pour les beans CDI ; ces portées ne sont présentes qu&#8217;avec l&#8217;API servlet.


Pour certains tests, si on veut se passer d&#8217;Arquillian, il peut être pratique de simuler ces portées. Je ne suis évidemment pas le premier à être confronté à ce problème ; ainsi, on peut trouver comment faire avec Weld 1.0. Mais ça ne fonctionne plus avec Weld 1.1, la classe AbstractThreadLocalMapContext a disparu. J&#8217;ai donc dû pousser les investigations, ce qui m&#8217;a mené au code suivant, qui a l&#8217;air de fonctionner avec Weld 1.1.0 CR1 et avec la version finale.



package org.jboss.weld.manager; // required for visibility to BeanManagerImpl#getContexts()

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;

import javax.enterprise.context.ConversationScoped;
import javax.enterprise.context.RequestScoped;
import javax.enterprise.context.SessionScoped;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterDeploymentValidation;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.Extension;

import org.jboss.weld.context.AbstractBoundContext;
import org.jboss.weld.context.bound.MutableBoundRequest;

public class WeldServletScopesSupportForSe implements Extension {

    public void afterDeployment(@Observes AfterDeploymentValidation event, BeanManager beanManager) {
        Map&lt;String, Object&gt; sessionMap = new HashMap&lt;String, Object&gt;();
        activateContext(beanManager, SessionScoped.class, sessionMap);

        Map&lt;String, Object&gt; requestMap = new HashMap&lt;String, Object&gt;();
        activateContext(beanManager, RequestScoped.class, requestMap);

        activateContext(beanManager, ConversationScoped.class, new MutableBoundRequest(requestMap, sessionMap));
    }

    private &lt; S &gt; void activateContext(BeanManager beanManager, Class&lt;? extends Annotation&gt; cls, S storage) {
        BeanManagerImpl beanManagerImpl = (BeanManagerImpl) beanManager;
        AbstractBoundContext&lt; S &gt; context = (AbstractBoundContext&lt; S &gt;) beanManagerImpl.getContexts().get(cls).get(0);

        context.associate(storage);
        context.activate();
    }
}



Évidemment, ce problème ne se pose que pour des tests unitaires standards et est résolu si on utilise Arquillian avec le conteneur Weld-EE.


</description>
          <pubDate>2011-01-02T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WeldSE/Scopes</link>
          <guid isPermaLink="true">https://www.jtips.info/WeldSE/Scopes</guid>
        </item>
      
    
      
        <item>
          <title>Tomcat et mod_proxy</title>
          <description>
On peut installer Tomcat derrière un serveur frontal pour plusieurs raisons.
Cela peut être pour donner plus de souplesse à l&#8217;environnement de déploiement, pour augmenter la sécurité ou pour factoriser des ressources statiques.
Quelle que soit la raison, l&#8217;utilisation d&#8217;Apache Httpd est un grand classique, que ce soit avec le mod_proxy ou avec le mod_jk.


Dans cette article, je présente les premiers pas pour installer Tomcat 6 derrière Apache Httpd 2.2 avec le mod_proxy.
Là encore, il reste un choix pour le protocole, puisque mod_proxy supporte, entre autres, les protocoles HTTP et AJP.


Premiers pas


Pour aller vite, voici l&#8217;installation la plus sommaire pour faire fonctionner cet environnement.
Dans l&#8217;exemple ci-dessous, j&#8217;utilise le protocole AJP, mais l&#8217;exemple pourrait très facilement se traduire pour le protocole HTTP.


Configuration Tomcat

Pour que Tomcat supporte le protocole AJP, il lui faut un connecteur correspondant.
Les connecteurs se configurent dans le fichier &lt;catalina_base&gt;/conf/server.xml. La portion de configuration ci-dessous est présente par défaut à l&#8217;installation de Tomcat :



    &lt;Connector port="8009" protocol="AJP/1.3" /&gt;



Cette ligne suffit à faire en sorte que Tomcat supporte le protocole AJP sur le port 8009.



Configuration Apache

La façon de configurer Apache Httpd dépend de la façon dont il a été installé et, pour Linux, de la distribution utilisée.
Le principal fichier de configuration est &lt;httpd_base&gt;/conf/httpd.conf, mais il est fréquent que des éléments de configuration soient répartis dans d&#8217;autres fichiers et importés dans la configuration globale avec la directive Include.



Configuration Apache sous Debian / Ubuntu

Sous Debian, la pratique pour configurer un nouveau module est de placer un fichier avec l&#8217;extension load pour configurer le chargement du module et un fichier avec l&#8217;extension conf pour la configuration à proprement parler.


Pour le mod_proxy, on aura donc un fichier proxy.load et un fichier proxy_ajp.load, pour le chargement du mod_proxy et de sa couche de transport par AJP. Ces fichiers existent dans le répertoire /etc/apache2/mods-available/, et sont effectivement utilisés lorsqu&#8217;on mets un lien symbolique depuis /etc/apache2/mods_enabled/.



 ln -s /etc/apache2/mods-available/proxy.load /etc/apache2/mods_enabled/
 ln -s /etc/apache2/mods-available/proxy_ajp.load /etc/apache2/mods_enabled/



Ensuite, pour la configuration du proxy (en mode reverse), on crée un fichier /etc/apache2/mods-available/proxy-swmsg.conf (le nom importe peu, ici swmsg est le nom de mon application), et on fera un lien symbolique dans /etc/apache2/mods-enabled/.



 touch /etc/apache2/mods-available/proxy-swmsg.conf
 ln -s /etc/apache2/mods-available/proxy-swmsg.conf /etc/apache2/mods_enabled/




Configuration Apache sous Windows

Windows n&#8217;étant plus mon système d&#8217;eploitation habituel, il risque d&#8217;y avoir quelques incohérences dans ma description, mais j&#8217;essaie quand même, en me basant sur une installation standard d&#8217;Apache Httpd.


Le fichier de configuration principal est C:\Program Files\Apache Software Foundation\Apache2.2\conf\httpd.conf et les fichiers secondaires sont dans C:\Program Files\Apache Software Foundation\Apache2.2\conf\extra\.
Pour activer le mod_proxy et sa couche de transport AJP, je vais dans le fichier httpd.conf et je décommente les deux lignes suivantes :



 LoadModule proxy_module modules/mod_proxy.so
 LoadModule proxy_ajp_module modules/mod_proxy_ajp.so



Puis je crée un fichier proxy-swmsg.conf dans le répertoire extra, que je référence en fin de fichier httpd.conf par la directive Include.



 Include conf/extra/proxy-swmsg.conf




Configuration mod_proxy

Cette portion de configuration est dans le fichier proxy-swmsg.conf qui aura été créé au préalable par une des procédures ci-dessus.



 &lt;Proxy /swmsg&gt;
   ProxyPass ajp://localhost:8009/swmsg
   ProxyPassReverse ajp://localhost:8009/swmsg
 &lt;/Proxy&gt;



Après redémarrage de Apache, les requêtes sur http://localhost/swmsg, ou http://myserver/swmsg seront traitées par Tomcat.





Configuration avancée


La configuration présentée dans les premiers pas est très souvent insuffisante. TODO&#8230;&#8203;


</description>
          <pubDate>2010-05-06T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Tomcat/Mod_proxy</link>
          <guid isPermaLink="true">https://www.jtips.info/Tomcat/Mod_proxy</guid>
        </item>
      
    
      
        <item>
          <title>JPA avec Spring</title>
          <description>
Dans cet article, nous allons décrire le développement d&#8217;une couche DAO[1] avec Spring et JPA[2].


Implémentation des DAO


JpaDaoSupport et JpaTemplate

Une première solution consistait à créer une classe DAO qui hérite de la classe JpaDaoSupport de Spring.


Cette solution a disparu depuis Spring Framework 4.



Ecrire des DAO "Plain JPA"

La seconde solution consiste à écrire des DAO qui n&#8217;utilisent pas les classes Spring.
Cette approche est intéressante car d&#8217;une part, elle semblera plus naturelle à un développeur JPA, et d&#8217;autre part, elle permet la configuration par annotation.


Avec cette solution, Spring injecte directement une instance "entity manager" dans notre DAO en exploitant l&#8217;annotation JPA @PersistenceContext.



@Repository
public class ProductDao {

  @PersistenceContext
  private EntityManager em;

  public List&lt;Product&gt; findByTitleLike(String title) {
    return em.createQuery(
            "select p from Product as p where p.title like :title",
            Product.class)
        .setParameter("title", title + '%')
        .getResultList();
  }
  ...
}












Bien que Spring injecte un "entity manager", nous n&#8217;aurons à déclarer du point de vue de Spring uniquement un bean de type EntityManagerFactory, pas EntityManager.


Spring permet d&#8217;injecter un EntityManagerFactory, notamment grâce à l&#8217;annotation @PersistenceUnit, mais les DAO doivent alors se charger de récupérer l&#8217;entity manager, ce qui alourdit le code











Déclarer l&#8217;entity manager factory


Pour déclarer l&#8217;entity manager factory, il faut d&#8217;abord considérer l&#8217;environnement d&#8217;exécution de notre code, à savoir :




Application standalone (cas d&#8217;une application Spring Boot, batch, test JUnit,&#8230;&#8203;)


Serveur Tomcat


Serveur Java EE / Jakarta EE (Glassfish, JBoss / WildFly,&#8230;&#8203;)




Cas d&#8217;une application standalone

Caractéristiques de l&#8217;environnement 




La connexion n&#8217;est pas gérée nativement par un pool : d&#8217;où l&#8217;utilisation du bean dataSource de type DataSource


Il n&#8217;existe pas nativement un conteneur JPA, permettant entre autres d&#8217;instancier l&#8217;entity manager factory : d&#8217;où le bean de type LocalContainerEntityManagerFactoryBean




Fichier XML de configuration Spring

&lt;beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
                https://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/jee
                https://www.springframework.org/schema/jee/spring-jee.xsd
            http://www.springframework.org/schema/context
                https://www.springframework.org/schema/context/spring-context.xsd"&gt;

    &lt;context:annotation-config/&gt;
    &lt;context:component-scan base-package="info.jtips.spring"/&gt;

    &lt;bean id="dataSource"
          class="org.springframework.jdbc.datasource.DriverManagerDataSource"&gt;
        &lt;property name="driverClassName" value="org.postgresql.Driver" /&gt;
        &lt;property name="url" value="jdbc:postgresql://localhost:5432/jtips"/&gt;
        &lt;property name="username" value="jtips" /&gt;
        &lt;property name="password" value="jtipspwd" /&gt;
    &lt;/bean&gt;

    &lt;bean id="emf"
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"&gt;
        &lt;property name="dataSource" ref="dataSource" /&gt;
    &lt;/bean&gt;

&lt;/beans&gt;




Application Tomcat

Caractéristiques de l&#8217;environnement :




La connexion est gérée par un pool de connexion déclaré sous Tomcat : d&#8217;où la récupération du bean dataSource par lookup JNDI.


Il n&#8217;existe pas nativement un conteneur JPA, permettant entre autres d&#8217;instancier l&#8217;entity manager factory : d&#8217;où le bean de type LocalContainerEntityManagerFactoryBean.




Fichier XML de configuration Spring 

&lt;beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
                https://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/jee
                https://www.springframework.org/schema/jee/spring-jee.xsd
            http://www.springframework.org/schema/context
                https://www.springframework.org/schema/context/spring-context.xsd"&gt;

    &lt;context:annotation-config/&gt;
    &lt;context:component-scan base-package="info.jtips.spring"/&gt;

    &lt;jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/LibrairieDS"/&gt;

    &lt;bean id="emf"
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"&gt;
        &lt;property name="dataSource" ref="dataSource" /&gt;
    &lt;/bean&gt;

&lt;/beans&gt;




Application Java EE / Jakarta EE

Caractéristiques de l&#8217;environnement :




La connexion est gérée par un pool de connexion déclaré sous le serveur d&#8217;application : d&#8217;où la récupération du bean dataSource par lookup JNDI.


Le serveur intègre nativement un conteneur JPA permettant l&#8217;instanciation de l&#8217;entity manager factory : on ne déclare donc pas l&#8217;entity manager factory avec Spring.




Fichier XML de configuration Spring

&lt;beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
                https://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/jee
                https://www.springframework.org/schema/jee/spring-jee.xsd
            http://www.springframework.org/schema/context
                https://www.springframework.org/schema/context/spring-context.xsd"&gt;

    &lt;context:annotation-config/&gt;
    &lt;context:component-scan base-package="info.jtips.spring"/&gt;

    &lt;jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/LibrairieDS"/&gt;

&lt;/beans&gt;






Configuration JPA


Il s&#8217;agit de renseigner le fichier persistence.xml placé dans le répertoire META-INF de l&#8217;archive de déploiement.
Encore une fois, la configuration dépend de l&#8217;environnement d&#8217;exécution.


Cas d&#8217;une application standalone

Cet environnement d&#8217;exécution ne fournit pas de gestionnaire transationnel JTA[3].
On positionne donc l&#8217;attribut transation-type à RESOURCE_LOCAL.


La data source est pour sa part spécifiée dans la configuration Spring.
Par ailleurs, comme on utilise un conteneur Spring / JPA (le bean LocalContainerEntityManagerFactoryBean), il faut préciser l&#8217;implémentation JPA à utiliser via l&#8217;élément provider.


Fichier persistence.xml

&lt;persistence&gt;
  &lt;persistence-unit name="jtips" transaction-type="RESOURCE_LOCAL"&gt;
    &lt;provider&gt;org.hibernate.jpa.HibernatePersistenceProvider&lt;/provider&gt;
  &lt;/persistence-unit&gt;
&lt;/persistence&gt;




Application Tomcat

La configuration est identique au cas d&#8217;une application standalone.


Fichier persistence.xml

&lt;persistence&gt;
  &lt;persistence-unit name="jtips" transaction-type="RESOURCE_LOCAL"&gt;
    &lt;provider&gt;org.hibernate.jpa.HibernatePersistenceProvider&lt;/provider&gt;
  &lt;/persistence-unit&gt;
&lt;/persistence&gt;




Application Java EE / Jakarta EE

Les serveurs d&#8217;application Java EE fournissent un gestionnaire transationnel de type JTA.
On positionne donc l&#8217;attribut transation-type à JTA et on utilise l&#8217;élément jta-data-source pour déclarer la data source.
Par ailleurs, les serveurs Java EE sont livrés avec une implémentation JPA.
Il n&#8217;est donc pas nécessaire de spécifier le provider JPA.


Fichier persistence.xml

&lt;persistence&gt;
  &lt;persistence-unit name="jtips" transaction-type="JTA"&gt;

    &lt;jta-data-source&gt;java:/jdbc/LibrairieDS&lt;/jta-data-source&gt;

  &lt;/persistence-unit&gt;
&lt;/persistence&gt;






Références




Exemples de code, avec Spring 5.3, Spring Boot 2.6 et Hibernate 5.6








1. Data Access Object


2. Java Persistence API


3. Java Transaction API

</description>
          <pubDate>2010-04-12T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/JPA</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/JPA</guid>
        </item>
      
    
      
        <item>
          <title>Logging dans JBoss</title>
          <description>
JBoss AS utilise Log4J pour ses propres traces.
Une vieille légende dit qu&#8217;il est impossible de gérer correctement les traces applicatives avec Log4J, et que celles-ci se retrouvent rejetée dans les traces du serveur.
Le but de cet article est de clarifier cette situation et d&#8217;apporter quelques réponses.


Les exemples de configuration de cet article ont été réalisés avec JBoss AS 5.1, mais les principes s&#8217;appliquent aux versions précédentes.
À partir de JBoss AS 6, Log4J a été abandonné au profit d&#8217;une solution interne, JBoss Log Manager.


Log4J dans JBoss


Log4J est présent sous deux formes dans JBoss.
Il y a des traces du démarrage qui sortent dans la console et dans le fichier boot.log, puis les traces du serveur qui sortent aussi dans la console et dans le fichier server.log.


Traces de démarrage

Les traces qui sortent au démarrage de JBoss ne sont pas les plus importantes et sont rarement reconfigurées.
Toutefois, si vous voulez en modifier le sortie ou le niveau, ça se passe dans run.jar, le fichier log4j.properties.


Cette configuration est active jusqu&#8217;à ce que le service de logging de JBoss démarre.



Service de logging

Une fois le service de logging démarré, c&#8217;est la configuration de conf/jboss-log4j.xml qui utilisée.
C&#8217;est dans ce fichier qu&#8217;on fait l&#8217;essentiel du travail de configuration.


Le réflexe avant une mise en production est de désactiver le ConsoleAppender et de n&#8217;associer que le FileAppender principal au root logger.



  &lt;root&gt;
     &lt;priority value="${jboss.server.log.threshold}"/&gt;
     &lt;appender-ref ref="FILE"/&gt;
  &lt;/root&gt;



Le reste de la configuration est très correcte en JBoss 5:




append = true, pour que le fichier de trace ne soit pas écrasé en cas de redémarrage ou de rechargement de la configuration,
*%t dans le ConversionPattern, pour avoir le nom des threads dans les traces,


le niveau par défaut est à INFO pour le root logger,


les loggers les plus verbeux sont limités au niveau WARN.




Tous ces points étaient critiquables dans le versions précédentes de JBoss et c&#8217;est avec plaisir que j&#8217;ai vu ces évolutions.
Ça fait du travail en moins pour la mise en production.
La seule critique que je pourrais formuler concerne l&#8217;excès de commentaires.
Certes, c&#8217;est utile pour les exemples, mais je conseille de les supprimer avant la mise en production, afin d&#8217;avoir un fichier plus lisible.





Log4J dans les applications


La façon dont est chargé Log4J dans une application dépend des pratiques de développeurs.
En particulier, il faut savoir s&#8217;ils ont chargé la configuration explicitement, en indiquant l&#8217;URL du fichier de configuration.


Configuration par défaut

Pour un déploiement en war, si aucun chargement n&#8217;est fait explicitement, le fichier log4j.jar doit être dans WEB-INF/lib et le fichier log4j.xml doit être dans WEB-INF/classes.


Pour un déploiement en ear, ces fichiers ne doivent pas être dans le war, mais uniquement dans l&#8217;ear, dans son répertoire lib pour le jar et directement à la racine pour log4j.xml.
Dans ce cas, la configuration locale n&#8217;est prise en compte que si l&#8217;isolation du classloader est activée pour l&#8217;ear, ce qui n&#8217;est pas le cas par défaut.
Cette situation a été à l&#8217;origine de la légende citée en introduction.


La configuration locale de Log4J ne doit absolument pas avoir de ConsoleAppender car cela enverrait des traces dans la sortie standard qui est interceptée par le service de logging de JBoss pour être ressortie dans un logger STDOUT, au niveau INFO.
Le résultat serait de voir des traces étranges, avec des informations en double (date et heure) et pas forcément pertinentes (niveau).



Configuration explicite

TODO



Isolation de Classloader

Dans tous les cas, pour que la configuration locale puisse être prise en compte, il faut que les classes locale de Log4J soient chargée en priorité sur les classes installées dans JBoss.
Ça ne pose pas de problème pour un war, mais dans le cas de déploiement en ear, la meilleure façon de faire est d&#8217;activer l&#8217;isolation, dans le fichier deployers/ear-deployer-jboss-beans.xml:



 &lt;bean name="JBossAppParsingDeployer" class="org.jboss.deployment.JBossAppParsingDeployer"&gt;
   ...
   &lt;property name="callByValue"&gt;trueproperty&gt;
 &lt;/bean&gt;
 &lt;bean name="EARClassLoaderDeployer" class="org.jboss.deployment.EarClassLoaderDeployer"&gt;
   &lt;property name="isolated"&gt;true&lt;/property&gt;
 &lt;/bean&gt;



En plus de nous faciliter la tâche pour Log4J, ce changement de configuration nous rapproche du comportement standard attendu par un serveur d&#8217;applications JavaEE&#8230;&#8203;





Solution mixte


Le wiki de JBoss propose une solution mixte: pas besoin de mettre Log4J dans l&#8217;application pour envoyer ses traces dans un fichier à part.
L&#8217;idée, c&#8217;est de configurer Log4J au niveau global en utilisant un filtre sur l&#8217;origine des traces, non pas par rapport aux loggers, mais au fichier de déploiement.
Ce filtre est fourni par JBoss, via le wiki pour les anciennes versions, directement dans JBoss à partir de la version 6, et via une mise à jour de JBoss Logging pour la version 5.1.
J&#8217;ai fait l&#8217;essai dans cette version.


J&#8217;ai donc récupéré les fichiers jboss-logging-spi.jar et jboss-logging-log4j.jar, et si ils n&#8217;ont pas exactement ces noms, il faut les renommer et les mettre dans le répertoire bin de JBoss. Ceci est nécessaire pour avoir le nouveau filtre TCLMCFilter. J&#8217;ai ensuite créé mon appender dans jboss-log4j.xml qui fera sortir les traces issues de swmsg-app.ear.



 &lt;appender name="SWFILE" class="org.jboss.logging.appender.DailyRollingFileAppender"&gt;
   &lt;errorHandler class="org.jboss.logging.util.OnlyOnceErrorHandler"/&gt;
   &lt;param name="File" value="${jboss.server.log.dir}/swmsg.log"/&gt;
   &lt;param name="Append" value="true"/&gt;
   &lt;param name="DatePattern" value="'.'yyyy-MM-dd"/&gt;
   &lt;layout class="org.apache.log4j.PatternLayout"&gt;
     &lt;param name="ConversionPattern" value="%d%-5p [%c] (%t)%m%n"/&gt;
   &lt;/layout&gt;

   &lt;filter class="org.jboss.logging.filter.TCLMCFilter"&gt;
     &lt;param name="AcceptOnMatch" value="true"/&gt;
     &lt;param name="DeployURL" value="swmsg-app.ear"/&gt;
   &lt;/filter&gt;
   &lt;filter class="org.apache.log4j.varia.DenyAllFilter"&gt;&lt;/filter&gt;
 &lt;/appender&gt;



Puis j&#8217;ai ajouté ce appender au root logger.



 &lt;root&gt;
   &lt;priority value="${jboss.server.log.threshold}"/&gt;
   &lt;appender-ref ref="FILE"/&gt;
   &lt;appender-ref ref="SWFILE"/&gt;
 &lt;/root&gt;



Le résultat, c&#8217;est de voir un nouveau fichier log/swmsg.log avec les traces de mon application.


Par la suite, j&#8217;ai voulu ajouter un AsyncAppender pour essayer de réduire les temps de traitement des traces, mais ça a été un échec.



 &lt;appender name="ASYNC" class="org.apache.log4j.AsyncAppender"&gt;
   &lt;appender-ref ref="FILE"/&gt;
   &lt;appender-ref ref="SWFILE"/&gt;
 &lt;/appender&gt;

 &lt;root&gt;
   &lt;priority value="${jboss.server.log.threshold}"/&gt;
   &lt;appender-ref ref="ASYNC"/&gt;
 &lt;/root&gt;



Dans cette configuration, je n&#8217;ai plus de trace dans swmsg.log :-(
J&#8217;ai posté dans le forum de JBoss, si vous avez une solution vous pouvez la soumettre là-bas. J&#8217;ai aussi ouvert un bug; à suivre&#8230;&#8203;


Je n&#8217;ai utilisé que la technique la plus simple et la plus pratique à mettre en place.
Ce n&#8217;est pas la seule, et le wiki de JBoss nous en propose d&#8217;autres, comme celle du Repository Selector.


</description>
          <pubDate>2010-03-10T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/Logging</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/Logging</guid>
        </item>
      
    
      
        <item>
          <title>Livres</title>
          <description>
Un peu de lecture&#8230;&#8203;


Maven


Le projet Maven propose un page de références bibliographiques.


Maven: The Definitive Guide

Il a été écrit par Sonatype, la société de référence en ce qui concerne Maven. Il a été scindé en deux livres : Maven by Example et Maven: The Complete Reference. L&#8217;ensemble du livre est consultable gratuitement en HTML ou en PDF.


Personnellement, j&#8217;ai surtout apprécié la seconde partie, vraiment complète et fouillée.
Pour la première partie, j&#8217;ai préféré #Apache_Maven


A noter que Sonatype publie d&#8217;autres livres que je n&#8217;ai pas lus :




Maven Handbook


Repository Management with Nexus


Developing with Eclipse and Maven




Les éditions papier de tous ces livres peuvent être achetés à prix très compétitif sur lulu.com


On notera aussi que plusieurs de ces auteurs ont partacipé précédemment à Better Builds with Maven, lui aussi consultable gratuitement en ligne.



Maven: le Guide Ultime

C&#8217;est la traduction française du précédent. Pour l&#8217;instant (mars 2010), il est encore d&#8217;un seul tenant, mais il devrait aussi être scindé. La traduction est très respectueuse de l&#8217;original, ce qui rend son style un peu trop anglo-saxon, mais évite les mauvaises surprises à la lecture.



Apache Maven

http://www.pearson.fr/Resources/Titles/27440100730370/Images/27440100730370M.gif


Il ne se sont pas foulés pour le titre&#8230;&#8203; Mais c&#8217;est à peu près la seule critique que je ferais au livre. Déjà, c&#8217;est un livre directement écrit en français, par des français, dans un style très agréable. J&#8217;ai vraiment pris du plaisir à le lire.


Je pense que c&#8217;est le livre idéal pour se lancer dans Maven. #Maven: The Definitive Guide sera la référence parfaite pour approfondir des points précis.





Gestion des versions


3 outils dominent le marché sur ce sujet : l&#8217;antique CVS, la classique Subversion et le moderne GIT. Pour chacun des outils, on trouve à la fois des livres électroniques en accès libre, parfois en français, et des livres traditionnels édités sur papier.


GIT

Git est un logiciel de gestion de versions décentralisé, créé par Linus Torvalds, et distribué sous licence GNU GPL v2.




Pro Git - professional version control


Git Community Book : en français, au format HTML ou PDF





Subversion



Gestion de versions avec Subversion


en version papier anglaise, et sa 2nd édition traite de Subversion 1.5,


en version papier française, seule la première édition a été commercialisée, sous le titre Gestion de projets avec Subversion,


la version électronique anglaise est la plus à jour, traitant de Subversion 1.6 (en mars 2010),


la version électronique française est un cran en retard.




J&#8217;ai lu la première édition, en français. Cette lecture m&#8217;a été utile au moment où j&#8217;ai commencé à mettre en place Subversion ; ses explications sur le fonctionnement de l&#8217;outil et sur sa logique d&#8217;organisation m&#8217;ont bien servi. Par contre, la lecture de ce livre n&#8217;ai pas spécialement plaisante. Je le rangerais dans la catégorie des livres de référence qu&#8217;il est utile de consulter à l&#8217;occasion.





Méthodes agiles




Kanban and Scrum - making the most of both, par Henrik Kniberg et Mattias Skarin, cécembre 2009


Kanban et Scrum, tirer le meilleur des deux est la traduction du livre précédent




</description>
          <pubDate>2010-03-02T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Livres</link>
          <guid isPermaLink="true">https://www.jtips.info/Livres</guid>
        </item>
      
    
      
        <item>
          <title>Développer avec le SDK de Google App Engine</title>
          <description>
Pour commencer à développer un application pour Google App Engine, je pense qu&#8217;il est préférable d&#8217;utiliser un IDE avec le bon plug-in. Cependant, pour comprendre les mécanismes sous-jacents, l&#8217;utilisation en ligne de commande est toujours très instructive.


Installation du SDK


L&#8217;environnement de développement est basé sur un JDK, en version 5 ou 6, et du SDK de Google App Engine. Celui-ci fournit l&#8217;API spécifique à GAE et tout ce qui est nécessaire pour disposer de l&#8217;API et des classes spécifiques et pour simuler l&#8217;exécution locale d&#8217;une application GAE.


L&#8217;installation du SDK est assez simple : j&#8217;ai téléchargé l&#8217;archive ZIP pour Java, que j&#8217;ai décompressée, dans le répertoire de mon choix (/opt/java).




Structure de l&#8217;application


Pour exécuter mon application en local, il faut qu&#8217;elle soit dans un répertoire, avec une structure de répertoire similaire à une archive war standard, avec un répertoire WEB-INF et son fichier web.xml.
Il faut toutefois ajouter le fichier spécifique à GAE, appengine-web.xml.



   mywar/
       .jsp
       css/.css
       js/.js
       WEB-INF/
           web.xml
           appengine-web.xml
           lib/.jar
           classes/... (classes compilées)
           appengine-generated/... (généré par le SDK)



Remarque : le répertoire est appengine-generated est créé par le SDK, il est inutile de le créer soi-même.




Configuration de l&#8217;application


Le fichier appengine-web.xml est nécessaire pour lancer l&#8217;application. Ce fichier contient au moins une description succincte de l&#8217;application : son identifiant et sa version.
Attention, il n&#8217;est pas question d&#8217;utiliser les numéros de versions traditionnels x.y.z, car le '.' est interdit ; seuls sont autorisés les chiffres, lettres (sans accents) et le caractère '-'.



 &lt;?xml version="1.0" encoding="utf-8"?&gt;
 &lt;appengine-web-app xmlns="http://appengine.google.com/ns/1.0"&gt;
     &lt;application&gt;myapp&lt;/application&gt;
     &lt;version&gt;1-alpha&lt;/version&gt;
 &lt;/appengine-web-app&gt;



Le fichier web.xml est aussi obligatoire, même si on n&#8217;y met rien ; eh non, ce n&#8217;est pas encore du JavaEE 6.



 &lt;?xml version="1.0" encoding="utf-8"?&gt;
 &lt;web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5"&gt;
 &lt;/web-app&gt;





Tester l&#8217;application


Le SDK fournit des scripts permettant de lancer un serveur Web local et d&#8217;y déployer notre application :



/opt/java/gae-java-sdk/bin/dev_appserver.sh mywar/



L&#8217;application est alors accessible à l&#8217;adresse http://localhost:8080/.


Si on a une autre application est déjà à l&#8217;écoute sur le port 8080, il est possible de lancer l&#8217;application sur un autre port.



/opt/java/gae-java-sdk/bin/dev_appserver.sh --port=8180 mywar/





Déployer l&#8217;application


Avant de pouvoir déployer l&#8217;application, il faut évidemment être enregistré et avoir créé une application.
Avec un compte, on peut en créer 10.
Au moment de la création, on renseigne un identifiant et une description.
L&#8217;identifiant doit faire entre 6 et 30 caractères et servira à construire l&#8217;URL : http://myapp.appspot.com.
L&#8217;identifiant doit être reporté dans le fichier appengine-web.xml, dans l&#8217;élément &lt;application&gt; La description fait entre 4 et 30 caractères ; on pourra noter que les caractères spéciaux sont interdits, y compris les caractères accentués.


Tout est prêt pour envoyer notre application sur le serveur, via le script appcfg.sh.



/opt/java/gae-java-sdk/bin/appcfg.sh update mywar/



Le script demande la saisie du mail qui a été utilisé pour créer le compte GAE, ainsi que le mot de passe.
Une fois déployée, l&#8217;application est accessible à l&#8217;URL link:`\http://myapp.appspot.com`.
A chaque update, GAE vérifie si la version existe, auquel cas il la met à jour, ou si il s&#8217;agit d&#8217;une nouvelle version.
La première version envoyée est automatiquement la version par défaut, mais pour promouvoir les versions suivantes, il faut passer par la console de gestion.
Chaque version a sa propre URL http://1-alpha.myapp.appspot.com, par exemple.




Conclusion


On le voit ici, on peut commencer à développer une application pour Google App Engine avec un JDK, le SDK et un éditeur de texte.
Les commandes utilisées dans cette page sont des scripts shell prévus pour Linux ou pour MacOS ; des commandes équivalentes, en .cmd sont fournies pour Windows.


Les phases préliminaires d&#8217;enregistrement du compte et de l&#8217;application sont les mêmes que pour le développement plus civilisé, avec un IDE comme Eclipse ou Netbeans.
Reste maintenant à étudier toutes les spécificités du développement Google App Engine et de faire les bons choix.
D&#8217;autres pages de JTips présentent (ou présenteront) les solutions que j&#8217;aurais adoptées, et je consignerai les raisons de mes choix, et mes impressions dans des billets sur Google App Engine de mon blog.


</description>
          <pubDate>2010-02-08T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Cloud/GoogleAppEngine-SDK</link>
          <guid isPermaLink="true">https://www.jtips.info/Cloud/GoogleAppEngine-SDK</guid>
        </item>
      
    
      
        <item>
          <title>Tutoriel Java 2</title>
          <description>
Présentation


Cet article consistue la deuxième partie du tutoriel Java, dont l&#8217;objectif est de réaliser une application "Todo List" en langage Java avec une interface graphique écrite en Swing.
Dans la première partie, nous avons développé les classes model et dao.
Nous allons maintenant écrire les classes service, qui vont notamment mettre en oeuvre les règles de gestion.




Création des classes de service métier


Nous allons continuer à travailler dans le projet TodoListService créé sous NetBeans dans la première partie du tutoriel.
Pour rappel, ce projet va contenir à terme toute la couche métier de l&#8217;application.


Spécification du service

Commençons donc par créer l&#8217;interface TodoListService que nous plaçons dans le package info.jtips.todolist.service :



package info.jtips.todolist.service;

import info.jtips.todolist.model.Tache;
import info.jtips.todolist.model.Utilisateur;
import java.util.List;

public interface TodoListService {
   Utilisateur verifierLogin(String id, String pwd);
   void enregistrerUtilisateur(Utilisateur u);
   Tache lireTache(int id);
   List&lt;Tache&gt; lireListeTaches();
   void sauvegarderTache(Tache t);
   void supprimerTache(Tache t);
}



Cette interface représente le contrat du service métier, c&#8217;est à dire qu&#8217;elle se contente d&#8217;énoncer la liste des méthodes qui doivent être disponibles dans un service de ce type.
Elle ne dit pas comment les méthodes doivent être mises en oeuvre, car cela est de la responsabilité des classes d&#8217;implémentation (voir un peu plus loin).


Par ailleurs, nous ajoutons une classe d&#8217;exception personnalisée dans le même package, car les méthodes du service métier seront susceptibles de lever un exception si une règle de gestion échoue :



package info.jtips.todolist.service;

public class TodoListException extends RuntimeException {

   public TodoListException(Throwable cause) {
       super(cause);
   }

   public TodoListException(String message, Throwable cause) {
       super(message, cause);
   }

   public TodoListException(String message) {
       super(message);
   }

   public TodoListException() {
   }

}










j&#8217;ai choisi d&#8217;hériter de la classe RuntimeException, plutôt que de la classe Exception.
Ce choix est discutable, mais il apporte de la simplicité au développement pour les raisons suivantes :




syntaxe try..catch pas imposée


rollback automatique des transactions lorsque les services métier sont hébergés par un conteneur Spring ou EJB (ce sujet n&#8217;est pas couvert dans cet article)









Implémentation du service

Ajoutons maintenant la classe d&#8217;implémentation de l&#8217;interface définie précédemment, à savoir la classe TodoListServiceImpl placée dans le package info.jtips.todolist.service.impl.
Cette implémentation s&#8217;appuie sur la couche DAO pour interroger et mettre à jour la base de données.
Les méthodes du service vont donc utiliser des instances de UtilisateurDao et TacheDao pour effectuer le requêtage SQL.


Comme les DAO sont utilisées dans toutes les méthodes du service, nous les déclarons en champs d&#8217;instance (en fait, on peut procéder de cette façon car les DAO sont dites stateless, c&#8217;est à dire qu&#8217;elles ne possèdent pas d&#8217;état propre à un objet client).


Dans le code ci-dessous, examiner les règles de gestion et notamment l&#8217;utilisation de l&#8217;exception personnalisée TodoListException :



package info.jtips.todolist.service.impl;

import info.jtips.todolist.dao.TacheDao;
import info.jtips.todolist.dao.UtilisateurDao;
import info.jtips.todolist.dao.impl.TacheDaoImpl;
import info.jtips.todolist.dao.impl.UtilisateurDaoImpl;
import info.jtips.todolist.model.Tache;
import info.jtips.todolist.model.Utilisateur;
import info.jtips.todolist.service.TodoListException;
import info.jtips.todolist.service.TodoListService;
import java.util.List;

public class TodoListServiceImpl implements TodoListService {

   private UtilisateurDao utilisateurDao = new UtilisateurDaoImpl();
   private TacheDao tacheDao = new TacheDaoImpl();

   /**
    * Cette méthode permet de vérifier un couple "identifiant/mot de passe"
    * issue d'un écran de login. Si l'utilisateur correspondant à l'identifiant
    * ou si le mot de passe est incorrect, alors une exception est levée
    */
   public Utilisateur verifierLogin(String id, String pwd) {
       Utilisateur u = utilisateurDao.findById(id);
       if (u != null &amp;amp;&amp;amp; pwd != null &amp;amp;&amp;amp; pwd.equals(u.getMotDePasse())) {
           return u;// connexion OK
       }
       throw new TodoListException("Echec de l'authentification");
   }

   /**
    * Un utilisateur peut être enregistré si son identifiant n'est pas déjà utilisé.
    * Si l'identifiant est déjà utilisé, une exception est levée.
    */
   public void enregistrerUtilisateur(Utilisateur u) {
       // vérification login déjà utilisé
      Utilisateur utilisateurExistant = utilisateurDao.findById(u.getIdentifiant());
      if (utilisateurExistant != null) throw new TodoListException("Identifiant déjà utilisé");

      // ok, on peut enregistrer
      utilisateurDao.create(u);
   }

   /**
    * Dans cette implémentation, l'association "Utilisateur &lt;-&gt; Tache" est
    * initialisée uniquement côté "Tache", ie la propriété "utilisateur".
    * Côté "Utilisateur", la collection de tâches n'est pas initialisée.
    */
   public List&lt;Tache&gt; lireListeTaches() {
       List&lt;Tache&gt; taches = tacheDao.findAll();
       for (Tache t : taches) {
           Utilisateur u = utilisateurDao.findById(t.getUtilisateur().getIdentifiant());
           t.setUtilisateur(u);
       }
       return taches;
   }

   /**
    * Dans cette méthode, l'association "Utilisateur &lt;-&gt; Tache" est complètement
    * initialisée : ie la propriété "utilisateur" côté "Tache" et la propriété
    * "taches" coté "Utilisateur".
    */
   public Tache lireTache(int id) {
       Tache tache = tacheDao.findById(id);
       String idUtilisateur = tache.getUtilisateur().getIdentifiant();
       Utilisateur u = utilisateurDao.findById(idUtilisateur);
       List&lt;Tache&gt; taches = tacheDao.findByUtilisateur(u);
       for (Tache t : taches) {
           u.ajouterTache(t);
           if (t.getId() == id) tache = t;// évite d'avoir deux instances différentes
                                          // pour l'entité recherchée dans le graphe d'objet
       }
       tache.setUtilisateur(u);
       return tache;
   }

   /**
    * Cette méthode consiste à effectuer un INSERT s'il s'agit de sauvegarder
    * une nouvelle tâche (reconnue par id == -1). S'il s'agit d'une tâche
    * existante (reconnue par id != -1), alors un UPDATE est généré.
    */
   public void sauvegarderTache(Tache t) {
       if (t.getId() == -1) {
           tacheDao.create(t);
       }
       else {
           tacheDao.update(t);
       }
   }

   /**
    * Suppression de la tâche passée en paramètre
    */
   public void supprimerTache(Tache t) {
       tacheDao.delete(t);
   }

}






Tests unitaires


De même que pour la couche DAO, il convient de prévoir des tests unitaires afin de vérifier le bon fonctionnement de notre service métier.


La classe de test ci-dessous est similaire à celles que nous avons écrites pour les DAO. Remarquez cependant la méthode de test testEnregistrerUtilisateurExistant, pour laquelle nous paramétrons l&#8217;annotation @Test avec expected=TodoListException.class : cela signifie que le test est correct si l&#8217;exception TodoListException est levée par la méthode testée.



package info.jtips.todolist.service;

import info.jtips.todolist.model.Tache;
import info.jtips.todolist.model.Utilisateur;
import info.jtips.todolist.service.impl.TodoListServiceImpl;
import java.util.Date;
import java.util.List;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;

public class TodoListServiceTest {

   @Test
   public void testEnregistrerUtilisateur() {
       Utilisateur u = new Utilisateur();
       u.setIdentifiant("bb");
       u.setPrenom("Bugs");
       u.setNom("Bunny");
       u.setMotDePasse("mp");

       TodoListService service = new TodoListServiceImpl();
       service.enregistrerUtilisateur(u);

       System.out.println("Terminé");
   }

   @Test(expected=TodoListException.class)
   public void testEnregistrerUtilisateurExistant() {
       Utilisateur u = new Utilisateur();
       u.setIdentifiant("bb");
       u.setPrenom("Bugs");
       u.setNom("Bunny");
       u.setMotDePasse("mp");

       TodoListService service = new TodoListServiceImpl();
       service.enregistrerUtilisateur(u);

       System.out.println("Terminé");
   }

   @Test
   public void testVerifierLoginOK() {
       TodoListService service = new TodoListServiceImpl();
       Utilisateur u = service.verifierLogin("bb", "mp");

       assertNotNull(u);
       assertEquals("Bugs", u.getPrenom());
       assertEquals("Bunny", u.getNom());
   }

   @Test(expected=TodoListException.class)
   public void testVerifierLoginKO() {
       TodoListService service = new TodoListServiceImpl();
       Utilisateur u = service.verifierLogin("bb", "pas le bon mot de passe");
   }

   @Test
   public void testSauvegarderTache1() {// test INSERT
       Utilisateur u = new Utilisateur();
       u.setIdentifiant("bb");
       u.setPrenom("Bugs");
       u.setNom("Bunny");
       u.setMotDePasse("mp");

       Tache t1 = new Tache();
       t1.setLibelle("Une première tâche");
       t1.setDateFin(new Date());
       t1.setPriorite(1);
       u.ajouterTache(t1);

       Tache t2 = new Tache();
       t2.setLibelle("Une deuxième tâche");
       t2.setDateFin(new Date());
       t2.setPriorite(1);
       u.ajouterTache(t2);

       TodoListService service = new TodoListServiceImpl();
       service.sauvegarderTache(t1);
       service.sauvegarderTache(t2);
   }

   @Test
   public void testSauvegarderTache2() {// test UPDATE
       TodoListService service = new TodoListServiceImpl();
       Tache t = service.lireTache(2);
       t.setLibelle("Tâche modifiée");
       service.sauvegarderTache(t);

   }

   @Test
   public void testLireTache() {
       TodoListService service = new TodoListServiceImpl();
       Tache t = service.lireTache(1);
       assertEquals(1, t.getId());
       assertEquals("Une première tâche", t.getLibelle());
       assertNotNull(t.getUtilisateur());
       assertEquals("bb", t.getUtilisateur().getIdentifiant());
       assertEquals(2, t.getUtilisateur().getTaches().size());

   }

   @Test
   public void testLireListeTaches() {
       TodoListService service = new TodoListServiceImpl();
       List&lt;Tache&gt; taches = service.lireListeTaches();
       assertEquals(2, taches.size());
   }

   @Test
   public void testSupprimerTaches() {
       TodoListService service = new TodoListServiceImpl();
       Tache t = service.lireTache(1);
       service.supprimerTache(t);
       List&lt;Tache&gt; taches = service.lireListeTaches();
       assertEquals(1, taches.size());
   }

}





Conclusion


Notre couche métier est désormais terminée.
Elle peut être utilisée depuis une couche de présentation.







Notez cependant quelques imperfections :




Lors d&#8217;un appel de méthode sur le service métier, plusieurs connexions JDBC sont ouvertes du fait de l&#8217;invocation de plusieurs méthodes DAO (cf méthode lireTache()). Cette façon de procéder est coûteuse en ressources machine.


La gestion transactionnelle n&#8217;est pas gérée correctement.
En effet, nous nous basons actuellement sur l&#8217;auto-commit pour gérér les transactions.
Ce mécanisme opère au niveau des DAO, ie plusieurs commit ont lieu lors de l&#8217;appel à une méthode du service (cf méthode enregistrerUtilisateur().
La démarcation de la transaction devrait plutôt être située au niveau de la classe TodoListServiceImpl




Pour résoudre ces imperfections, plusieurs solutions existent :




Gérer l&#8217;ouverture / fermeture de la connexion JDBC, ainsi que l&#8217;appel aux commit et rollback à l&#8217;aide d&#8217;un thread local :
nous ne détaillerons pas cette approche ici, car elle nécessite l&#8217;écriture de code technique, qui peut être très avantageusement remplacé par une des deux solutions ci-dessous



Utiliser un conteneur EJB


Utiliser Spring Framework









Téléchargement du projet


Le projet NetBeans correspondant à la couche métier.




Suite du tutoriel


Bientôt un nouvel  article pour la partie présentation avec Swing&#8230;&#8203;


</description>
          <pubDate>2010-01-12T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/Tutoriel-2</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/Tutoriel-2</guid>
        </item>
      
    
      
        <item>
          <title>Tutoriel Java</title>
          <description>
Présentation du tutoriel


L&#8217;objectif de ce tutoriel est de réaliser une application "Todo List" en langage Java avec une interface graphique écrite en Swing. Ce tutoriel est destiné aux développeurs qui débutent en Java.


Caractéristiques techniques



Langages utilisés : Java et SQL


Outil de développement : NetBeans 6.8


Interface graphique : API Swing


Base de données : MySQL


Tests : JUnit





Description de l&#8217;application

Le but de cette application est de créer des tâches qui sont assignées à un utilisateur.


La première fonctionnalité consiste donc à identifier l&#8217;utilisateur via un écran de login. Cependant, la première fois que l&#8217;application est utilisée, il n&#8217;y a pas d&#8217;utilisateur enregistré en base de données. Il faut donc prévoir un écran qui permet l&#8217;enregistrement d&#8217;un utilisateur.
Une fois connecté, l&#8217;utilisateur pourra visualiser la liste de toutes les tâches enregistrées, y compris celles des autres utilisateurs. A partir de cette liste, il pourra aussi ajouter une nouvelle tâche ou modifier une tâche existante.



Organisation du développement

Nous commencerons par bâtir la couche métier de l&#8217;application, i.e. :




les classes d&#8217;entités


les classes DAO pour la persistance


les classes de services pour la mise en oeuvre des règles de gestion




Pour tester le code produit dans la couche métier, nous utiliserons JUnit.


Nous développerons ensuite l&#8217;interface graphique.



Installation

Pour pouvoir réaliser ce tutorial, vous devez installer :




Un JDK


NetBeans


MySQL




Vous devez par ailleurs télécharger le fichier mysql-connector-java-5.X.X-bin.jar, qui nous permettra de nous connecter à MySQL depuis Java.


Il faut ensuite passer le script SQL suivant pour créer la base de données :



create database todolist default character set latin1 collate latin1_swedish_ci;

use todolist;

grant all privileges on todolist.* to scott@127.0.0.1 identified by 'tiger';

create table utilisateur (
       identifiant varchar(10) not null,
       mot_de_passe varchar(10) not null,
       nom varchar(30) not null,
       prenom varchar(30) not null,
       primary key(identifiant)) type=InnoDB;

create table tache (
       id integer not null auto_increment,
       libelle varchar(100) not null,
       date_fin date not null,
       priorite int(11) not null,
       utilisateur_id varchar(10),
       primary key(id)) type=InnoDB;

alter table tache
  add constraint fk_tache_utilisateur foreign key (utilisateur_id) references utilisateur (identifiant);






Développement de la couche métier


Création du projet

Ouvrir Netbeans et aller dans le menu File &#8594; New Project. Sélectionner la catégorie Java, puis le type de projet Java Class Library.







Cliquer ensuite sur le bouton Next et renseigner le nom de projet TodoListService, et cliquer enfin sur le bouton Finish :








Création des classes d&#8217;entité

Une application Java est composée d&#8217;un ensemble de classes qui définissent chacunes une petite partie de l&#8217;application. A terme, l&#8217;application pourra comporter un grand nombre de classes.
A des fins de lisibilités, et aussi pour éviter des conflits de nommage, les classes ne doivent pas être toutes créées au même endroit. Il faut donc les organiser dans des packages, notion objet qui correspond à peu près à une arborescence de répertoires.
Commençons donc par créer un package pour nos classes d&#8217;entités :




Dans le projet TodoListService, sélectionner Source Packages, puis par clic droit, aller dans le menu New &#8594; Java Package&#8230;&#8203;.


Rentrer le nom de package info.jtips.model (attention de bien respecter la casse !)











Cliquer enfin sur Finish




Votre package doit maintenant apparaître dans votre projet comme ci-dessous :







Nous pouvons maintenant créer nos classes d&#8217;entités, qui correspondent plus ou moins aux tables définies en base de données. Les classes à créer sont les suivantes :




Utilisateur


Tache




Pour cela, sélectionner le package créé précédemment, et par clic droit, sélectionner le menu New &#8594; Java Class&#8230;&#8203; et entrer le nom de classe Utilisateur (encore une fois, faites bien attention à la casse !).







Cliquer sur le bouton Finish. Vous devriez obtenir ceci :







Notez que le nom du package est spécifié dans le code généré, ainsi bien sûr que le nom de la classe. Cette classe que nous venons de créer est aussi un nouveau type que nous pourrons utiliser par la suite.


Dans la classe Utilisateur, ajouter le code suivant qui consiste à définir les propriétés d&#8217;un utilisateur :



package info.jtips.model;

public class Utilisateur {
   private String identifiant;
   private String nom;
   private String prenom;
   private String motDePasse;

   public String getIdentifiant() {
       return identifiant;
   }

   public void setIdentifiant(String identifiant) {
       this.identifiant = identifiant;
   }

   public String getMotDePasse() {
       return motDePasse;
   }

   public void setMotDePasse(String motDePasse) {
       this.motDePasse = motDePasse;
   }

   public String getNom() {
       return nom;
   }

   public void setNom(String nom) {
       this.nom = nom;
   }

   public String getPrenom() {
       return prenom;
   }

   public void setPrenom(String prenom) {
       this.prenom = prenom;
   }

}



Chaque utilisateur est caractérisé par un identifiant, un nom, un prénom et un mot de passe.
Ces informations sont de type chaîne de caractères, soit String en syntaxe Java.
Par convention, pour définir une propriété au sens Java, il faut aussi ajouter les méthodes getters et setters.
Ces méthodes doivent respecter les règles de nommage qui apparaissent dans le code ci-dessus.


De la même façon, vous pouvez créer la classe Tache :



package info.jtips.model;

import java.util.Date;

public class Tache {
   private int id;
   private String libelle;
   private Date dateFin;
   private int priorite;

   public Date getDateFin() {
       return dateFin;
   }

   public void setDateFin(Date dateFin) {
       this.dateFin = dateFin;
   }

   public int getId() {
       return id;
   }

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

   public String getLibelle() {
       return libelle;
   }

   public void setLibelle(String libelle) {
       this.libelle = libelle;
   }

   public int getPriorite() {
       return priorite;
   }

   public void setPriorite(int priorite) {
       this.priorite = priorite;
   }
}



Remarques sur le code ci-dessus :




Les champs id et priorite sont de type entier, soit int en Java qui est un des 8 types primitifs (tout les autres types sont des classes)


Le champ dateFin est du type java.util.Date, d&#8217;où la ligne import.util.Date entre la déclaration du package et la déclaration de la classe (la classe String fait partie du package java.lang qui est le seul package Java ne nécessitant pas d&#8217;import)




Nous venons de définir les entités Utilisateur et Tache, ainsi que leur propriétés.
En fait, ces classes ne sont pas indépendantes, elles sont liées de la façon suivante :




Une tâche est assignée à un seul utilisateur


Un utilisateur possède plusieurs tâches




En notation UML, cela donne le schéma suivant :







Les relations entre classes qui apparaissent sur le schéma ci-dessus s&#8217;appellent associations. En Java, une association se code comme une propriété :




Association Tache &#8594; Utilisateur (association dite many-to-one)





package info.jtips.model;

import java.util.Date;

public class Tache {
   private int id;
   private String libelle;
   private Date dateFin;
   private int priorite;

   private Utilisateur utilisateur;

   public Utilisateur getUtilisateur() {
       return utilisateur;
   }

   public void setUtilisateur(Utilisateur utilisateur) {
       this.utilisateur = utilisateur;
   }
   // autres getters et setters...
}





Association Utilisateur &#8594; Tache (association dite one-to-many)





package info.jtips.model;

import java.util.List;

public class Utilisateur {
   private String identifiant;
   private String nom;
   private String prenom;
   private String motDePasse;

   private List&lt;Tache&gt; taches;

   public List&lt;Tache&gt; getTaches() {
       return taches;
   }

   public void setTaches(List&lt;Tache&gt; taches) {
       this.taches = taches;
   }
   // autres getters et setters...

}



Remarque sur le code ci-dessus :




Comme un utilisateur possède plusieurs tâches, on définit l&#8217;association à l&#8217;aide d&#8217;un objet conteneur de type java.util.List


Ce dernier type fait partie du package java.util, d&#8217;où l&#8217;import


La liste contient des objets de type Tache : c&#8217;est ce que signifie la déclaration List&lt;Tache&gt; (on appelle cela un générique)




Pour terminer avec les classes d&#8217;entités, nous allons coder une méthode nommée ajouterTache qui permet d&#8217;ajouter une tâche à un utilisateur :



package info.jtips.model;

import java.util.List;

public class Utilisateur {
   private String identifiant;
   private String nom;
   private String prenom;
   private String motDePasse;

   private List&lt;Tache&gt; taches;

   public void ajouterTache(Tache t) {
       if (taches == null) taches = new ArrayList&lt;Tache&gt;();
       taches.add(t);
       t.setUtilisateur(this);
   }

   // getters et setters...
}



Remarques sur le code de la méthode ajouterTache(Tache t) ci-dessus :




Déclaration de la méthode



La méthode prend en paramètre une variable de type Tache


Cette méthode ne retourne pas de résultat : d&#8217;où le type void pour le retour





1ère ligne de la méthode



Les variables ne sont pas des objets, elles "pointent" sur des objets (on parle aussi d'instance pour désigner un objet)


La variable taches pointe d&#8217;abord sur l&#8217;objet null, car on ne lui a pas affecté d&#8217;objet


L&#8217;opérateur new permet de créer (ou instancier) un objet


Dans la méthode ajouterTache(Tache t), avant d&#8217;ajouter la tâche à la liste, on vérifie si la liste est nulle et on l&#8217;instancie si c&#8217;est le cas (notez que l&#8217;instanciation ne se produira qu&#8217;une seule fois, i.e. lors du premier appel à la méthode ajouterTache(Tache t))





2ème ligne de la méthode



On ajoute l&#8217;objet passé en paramètre à la liste des tâches de l&#8217;utilisateur. Comme vous pouvez le constater, l&#8217;appel à une méthode est effectué via la notation pointée.





3ème ligne de la méthode



L&#8217;association est bi-directionnelle : la nouvelle tâche est connue de l&#8217;utilisateur, mais il faut aussi renseigner la tâche afin qu&#8217;elle connaisse l&#8217;utilisateur


L&#8217;utilisateur est renseigné auprès de la tâche par appel de la méthode setUtilisateur(Utilisateur u)


La variable implicite this référence l&#8217;instance en cours. En l&#8217;occurence il s&#8217;agit de l&#8217;utilisateur que l&#8217;on souhaite passer à la tâche








Création des classes DAO

Les DAO sont des classes d&#8217;accès aux données.
Il s&#8217;agit de classes qui fournissent des méthodes dont le but est d&#8217;exécuter des requêtes SQL.
De plus, ces méthodes sont chargées de la traduction des données tabulaires en objet et réciproquement.
Une bonne pratique pour mettre en oeuvre les DAO consiste à d&#8217;abord spécifier les méthodes qu&#8217;elles doivent mettre en oeuvre.
A cette fin, nous allons créer des types Java qui ne sont pas des classes, mais des interfaces (à la différence d&#8217;une classe, une interface n&#8217;est pas instanciable).


Dans votre projet NetBeans, créer le package info.jtips.dao et créer l&#8217;interface nommée UtilisateurDao (menu New &#8594; Java Interface&#8230;&#8203;).
Vous ajouterez aussi le code de spécification des méthodes qui figurent ci-dessous.



package info.jtips.dao;

import info.jtips.model.Utilisateur;

public interface UtilisateurDao {
   Utilisateur findById(String identifiant);
   void create(Utilisateur u);
}



Remarque : les méthodes créées ci-dessus ne possèdent pas de corps pour ajouter du code; nous avons simplement défini la signature des différentes méthodes


De la même façon, créer l&#8217;interface TacheDao :



package info.jtips.dao;

import info.jtips.model.Tache;
import java.util.List;

public interface TacheDao {
   List&lt;Tache&gt; findAll();
   Tache findById(int id);
   void create(Tache t);
   void update(Tache t);
   void delete(Tache t);
}



Pour continuer, ajoutons la classe UtilisateurDaoImpl dans le package info.jtips.dao.impl :



package info.jtips.dao.impl;

import info.jtips.dao.UtilisateurDao;
import info.jtips.model.Utilisateur;

public class UtilisateurDaoImpl implements UtilisateurDao {

   public Utilisateur findById(String identifiant) {
       return null;
   }

   public void create(Utilisateur u) {

   }

}



Cette classe réalise l&#8217;interface que nous avons définie précédemment :




C&#8217;est ce qu signifie le mot clé implements


La classe redéfinit les méthodes spécifiées par l&#8217;interface (cette fois-ci, nous pouvons ajouter du code dans les méthodes)




Voyons maintenant comment coder cette classe de telle sorte que l&#8217;on puisse mettre en oeuvre le requêtage SQL nécessaire. Nous allons pour cela utiliser l&#8217;API JDBC, qui consiste à :




établir une connexion sur la base de données


récupérer un statement qui permettra d&#8217;exécuter une requête SQL


récupérer un result set en cas de SELECT SQL, afin de lire les données retournées par la base




Commençons par créer une méthode pour établir la connexion sur la base de données (MySQL en l&#8217;occurence) :



package info.jtips.dao.impl;

import info.jtips.dao.UtilisateurDao;
import info.jtips.model.Utilisateur;
import java.sql.Connection;
import java.sql.DriverManager;

public class UtilisateurDaoImpl implements UtilisateurDao {

   public Utilisateur findById(String identifiant) {
       return null;
   }

   public void create(Utilisateur u) {

   }

   private Connection getConnexion() throws Exception {
       // Chargement du driver
       Class.forName("com.mysql.jdbc.Driver");

       // Obtention de la connexion
       String url = "jdbc:mysql://localhost:3306/todolist";
       Connection cx = DriverManager.getConnection(url, "scott", "tiger");
       return cx;
   }

}



Remarque sur le code de la méthode getConnexion() :




Toutes les classes JDBC (ici Connection et DriverManager) font partie du package java.sql


Déclaration de la méthode



C&#8217;est une méthode marquée private, c&#8217;est à dire que seule la classe UtilisateurDaoImpl peut l&#8217;invoquer (contrairement aux autres méthodes qui sont marquées public, et peuvent donc être invoquées depuis n&#8217;importe quelle autre classe)


Cette méthode est susceptible de générer une exception (par exemple si la base de données n&#8217;est pas démarrée) : d&#8217;où le throws Exception


La méthode retourne une instance de type Connection





1ère ligne de code



Permet de charger le driver correspondant à MySQL


Ce driver est fourni dans un fichier .jar que nous installerons par la suite





2ème ligne de code



Définition de l&#8217;URL de connexion sur la base de données MySQL nommée todolist





3ème ligne de code



On établit la connexion avec l&#8217;URL, l&#8217;identifiant scott et le mot de passe tiger





Dernière ligne de code



Le mot clé return permet de retourner l&#8217;instance de la connexion établie







Nous pouvons maintenant coder la méthode findById() :



package info.jtips.dao.impl;

import info.jtips.dao.UtilisateurDao;
import info.jtips.model.Utilisateur;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class UtilisateurDaoImpl implements UtilisateurDao {

   public Utilisateur findById(String identifiant) {
       Connection cx = null;
       PreparedStatement st = null;
       ResultSet rs = null;
       Utilisateur utilisateur = null;
       try {
           cx = getConnexion();
           st = cx.prepareStatement("select nom, prenom, mot_de_passe from utilisateur where identifiant = ?");
           st.setString(1, identifiant);
           rs = st.executeQuery();
           if (rs.next()) {
               String nom = rs.getString(1);
               String prenom = rs.getString(2);
               String motDePasse = rs.getString(3);

               utilisateur = new Utilisateur();
               utilisateur.setIdentifiant(identifiant);
               utilisateur.setNom(nom);
               utilisateur.setPrenom(prenom);
               utilisateur.setMotDePasse(motDePasse);
           }

       } catch (Exception e) {
           throw new RuntimeException(e);
       } finally {
           try { if (rs != null) rs.close(); } catch (SQLException sqle) {}
           try { if (st != null) st.close(); } catch (SQLException sqle) {}
           try { if (cx != null) cx.close(); } catch (SQLException sqle) {}
       }

       return utilisateur;
   }

   public void create(Utilisateur u) {}

   private Connection getConnexion() throws Exception {
       // Chargement du driver
       Class.forName("com.mysql.jdbc.Driver");

       // Obtention de la connexion
       String url = "jdbc:mysql://localhost:3306/todolist";
       Connection cx = DriverManager.getConnection(url, "scott", "tiger");
       return cx;
   }

}



Remarques sur le code de la méthode findById() :




La connexion JDBC est récupérée par appel à la méthode getConnexion()


A partir de la connexion, on récupère un objet de type java.sql.PreparedStatement


Le statement est initialisé avec une chaîne de requête SQL


La chaîne de requête SQL est paramétrée avec le caractère ? de façon à pouvoir rechercher l&#8217;utilisateur qui correspond à l&#8217;identifiant passé en paramètre de la méthode


Le paramètre identifiant est passé au statement via le code st.setString(1, identifiant) (le premier paramètre désigne la position du paramètre injecté dans la chaîne de requête, ici la valeur 1)


La requête est enfin exécutée suite à l&#8217;invocation de la méthode st.executeQuery()


Après l&#8217;exécution de la requête, on récupère un objet de type java.sql.ResultSet, puisqu&#8217;il s&#8217;agit d&#8217;une requête de lecture


Ce result set contient les données tabulaires lues dans la base de données, càd à priori plusieurs lignes, à l&#8217;instar d&#8217;une requête SQL SELECT


La méthode rs.next() permet de déplacer la position du result set sur le prochain enregistrement lu s&#8217;il existe, auquel cas la méthode retourne la valeur booléeene true


Dans notre cas, comme la recherche est effectuée sur la clé primaire, nous savons qu&#8217;il n&#8217;y aura qu&#8217;un seul résultat : il suffit donc d&#8217;itérer au plus une fois, d&#8217;où l&#8217;utilisation de la condition if (rs.next())


Pour chaque ligne lue, les valeurs de colonne sont récupérées à l&#8217;aide des méthodes rs.getXXX(index), où XXX correspond au type récupéré, et index à la position de la colonne dans la requête (on commence à partir de 1)


Les données tabulaire sont ensuite transformées en objet, dans notre cas en objet Utilisateur


D&#8217;où l&#8217;instanciation de l&#8217;utilisateur à retourner


Les setters de l&#8217;utilisateur sont invoqués pour passer les valeurs de propriétés récupérées depuis les result set


Le bloc try..catch..finally permet de gérer les exceptions


Le bloc try contient le code d&#8217;exécution normale


Le bloc catch est invoqué lorsqu&#8217;une erreur survient dans le bloc try


Dans notre exemple, nous propageons simplement l&#8217;exception à la méthode appelante sous la forme d&#8217;une RuntimeException (cette façon de faire est discutable, mais elle nous simplifie la tâche pour l&#8217;instant)


Le bloc finally est invoqué quelle que soit la situation : erreur ou pas


Dans notre cas, nous en profitons pour fermer les ressources ouvertes pour le traitement JDBC par invocation des méthodes close() sur les objets Connection, PreparedStatement et ResultSet)


Comme les méthodes close() génèrent elles aussi des exceptions, nous encapsulons le code de fermeture des ressources dans un bloc try..catch




Pour terminer le code de cette première DAO, il nous reste à compléter la méthode create(Utilisateur u). Contrairement à la méthode précédente, il s&#8217;agit d&#8217;une requête d&#8217;écriture; le code est donc légèrement différent :




Invocation de la méthode st.executeUpdate() plutôt que st.executeQuery()


Pas d&#8217;objet ResultSet à exploiter





   public void create(Utilisateur u) {
       Connection cx = null;
       PreparedStatement st = null;
       try {
           cx = getConnexion();
           st = cx.prepareStatement("insert into utilisateur (identifiant, nom, prenom, mot_de_passe) values (?, ?, ?, ?)");
           st.setString(1, u.getIdentifiant());
           st.setString(2, u.getNom());
           st.setString(3, u.getPrenom());
           st.setString(4, u.getMotDePasse());

           st.executeUpdate();
       } catch (Exception e) {
           throw new RuntimeException(e);
       } finally {
           try { if (st != null) st.close(); } catch (SQLException sqle) {}
           try { if (cx != null) cx.close(); } catch (SQLException sqle) {}
       }
   }



De la même façon, nous codons la classe DAO pour les tâches :



package info.jtips.dao.impl;

import info.jtips.dao.TacheDao;
import info.jtips.model.Tache;
import info.jtips.model.Utilisateur;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class TacheDaoImpl implements TacheDao {

   public List&lt;Tache&gt; findAll() {
       Connection cx = null;
       PreparedStatement st = null;
       ResultSet rs = null;
       List&lt;Tache&gt; taches = new ArrayList&lt;Tache&gt;();
       try {
           cx = getConnexion();
           st = cx.prepareStatement("select id, libelle, priorite, date_fin, utilisateur_id from tache");
           rs = st.executeQuery();
           while (rs.next()) {
               int id = rs.getInt(1);
               String libelle = rs.getString(2);
               int priorite = rs.getInt(3);
               Date dateFin = rs.getDate(4);
               String utilisateurId = rs.getString(5);

               Tache t = new Tache();
               t.setId(id);
               t.setLibelle(libelle);
               t.setDateFin(dateFin);
               t.setPriorite(priorite);

               Utilisateur u = new Utilisateur();
               u.setIdentifiant(utilisateurId);
               t.setUtilisateur(u);

               taches.add(t);
           }

       } catch (Exception e) {
           throw new RuntimeException(e);
       } finally {
           try { if (rs != null) rs.close(); } catch (SQLException sqle) {}
           try { if (st != null) st.close(); } catch (SQLException sqle) {}
           try { if (cx != null) cx.close(); } catch (SQLException sqle) {}
       }

       return taches;
   }

   public Tache findById(int id) {
       Connection cx = null;
       PreparedStatement st = null;
       ResultSet rs = null;
       Tache t = null;
       try {
           cx = getConnexion();
           st = cx.prepareStatement("select id, libelle, priorite, date_fin, utilisateur_id from tache where id = ?");
           st.setInt(1, id);
           rs = st.executeQuery();
           if (rs.next()) {
               String libelle = rs.getString(2);
               int priorite = rs.getInt(3);
               Date dateFin = rs.getDate(4);
               String utilisateurId = rs.getString(5);

               t = new Tache();
               t.setId(id);
               t.setLibelle(libelle);
               t.setDateFin(dateFin);
               t.setPriorite(priorite);

               Utilisateur u = new Utilisateur();
               u.setIdentifiant(utilisateurId);
               t.setUtilisateur(u);
           }

       } catch (Exception e) {
           throw new RuntimeException(e);
       } finally {
           try { if (rs != null) rs.close(); } catch (SQLException sqle) {}
           try { if (st != null) st.close(); } catch (SQLException sqle) {}
           try { if (cx != null) cx.close(); } catch (SQLException sqle) {}
       }

       return t;
   }

   public List&lt;Tache&gt; findByUtilisateur(Utilisateur u) {
       Connection cx = null;
       PreparedStatement st = null;
       ResultSet rs = null;
       List&lt;Tache&gt; taches = new ArrayList&lt;Tache&gt;();
       try {
           cx = getConnexion();
           st = cx.prepareStatement("select id, libelle, priorite, date_fin from tache where utilisateur_id = ?");
           st.setString(1, u.getIdentifiant());
           rs = st.executeQuery();
           while (rs.next()) {
               int id = rs.getInt(1);
               String libelle = rs.getString(2);
               int priorite = rs.getInt(3);
               Date dateFin = rs.getDate(4);

               Tache t = new Tache();
               t.setId(id);
               t.setLibelle(libelle);
               t.setDateFin(dateFin);
               t.setPriorite(priorite);
               t.setUtilisateur(u);

               taches.add(t);
           }

       } catch (Exception e) {
           throw new RuntimeException(e);
       } finally {
           try { if (rs != null) rs.close(); } catch (SQLException sqle) {}
           try { if (st != null) st.close(); } catch (SQLException sqle) {}
           try { if (cx != null) cx.close(); } catch (SQLException sqle) {}
       }

       return taches;
   }

   public void create(Tache t) {
       Connection cx = null;
       PreparedStatement st = null;
       try {
           cx = getConnexion();
           st = cx.prepareStatement("insert into tache (libelle, priorite, date_fin, utilisateur_id) values (?, ?, ?, ?)");
           st.setString(1, t.getLibelle());
           st.setInt(2, t.getPriorite());
           Date df = t.getDateFin();
           java.sql.Date dateFin = new java.sql.Date(df.getTime());
           st.setDate(3, dateFin);
           st.setString(4, t.getUtilisateur().getIdentifiant());

           st.executeUpdate();
       } catch (Exception e) {
           throw new RuntimeException(e);
       } finally {
           try { if (st != null) st.close(); } catch (SQLException sqle) {}
           try { if (cx != null) cx.close(); } catch (SQLException sqle) {}
       }
   }

   public void update(Tache t) {
       Connection cx = null;
       PreparedStatement st = null;
       try {
           cx = getConnexion();
           st = cx.prepareStatement("update tache set libelle = ?, priorite = ?, date_fin = ?, utilisateur_id = ? where id = ?");
           st.setString(1, t.getLibelle());
           st.setInt(2, t.getPriorite());
           st.setDate(3, new java.sql.Date(t.getDateFin().getTime()));
           st.setString(4, t.getUtilisateur().getIdentifiant());
           st.setInt(5, t.getId());

           st.executeUpdate();
       } catch (Exception e) {
           throw new RuntimeException(e);
       } finally {
           try { if (st != null) st.close(); } catch (SQLException sqle) {}
           try { if (cx != null) cx.close(); } catch (SQLException sqle) {}
       }
   }

   public void delete(Tache t) {
       Connection cx = null;
       PreparedStatement st = null;
       try {
           cx = getConnexion();
           st = cx.prepareStatement("delete from tache where id = ?");
           st.setInt(1, t.getId());

           st.executeUpdate();
       } catch (Exception e) {
           throw new RuntimeException(e);
       } finally {
           try { if (st != null) st.close(); } catch (SQLException sqle) {}
           try { if (cx != null) cx.close(); } catch (SQLException sqle) {}
       }
   }

   private Connection getConnexion() throws Exception {
       // Chargement du driver
       Class.forName("com.mysql.jdbc.Driver");

       // Obtention de la connexion
       String url = "jdbc:mysql://localhost:3306/todolist";
       Connection cx = DriverManager.getConnection(url, "scott", "tiger");
       return cx;
   }

}




Tester les DAO avec JUnit

Nos DAO sont prêtes, nous allons maintenant les tester avec JUnit. Avant toute chose, il faut installer le driver MySQL, faute de quoi, nous ne pourrons pas nous connecter à la base de données.
Pour cela, il suffit de sélectionner le menu Add JAR/Folder&#8230;&#8203; par clic droit sur le répertoire Librairies de votre projet :







Après la sélection du driver MySQL, vous devriez le voir apparaître dans votre explorateur de projet sous NetBeans :







Ajoutons maintenant la classe de test JUnit pour la DAO Utilisateur :




Dans le répertoire Test Packages de votre projet, ajouter le package info.jtips.dao


Sélectionner le menu New &#8594; Other&#8230;&#8203; par clic droit sur le package que vous venez de créer


Choisir le type JUnit Test dans la catégorie JUnit









Rentrer ensuite le nom classe de la classe de test :  UtilisateurDaoTest, puis sélectionner les cases à cocher comme ci-dessous :







Choisir enfin JUnit 4 comme librairie de test :







Le code généré par NetBeans est le suivant :



package info.jtips.dao;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;

public class UtilisateurDaoTest {

   public UtilisateurDaoTest() {
   }

   @BeforeClass
   public static void setUpClass() throws Exception {
   }

   @AfterClass
   public static void tearDownClass() throws Exception {
   }

}



Complétons cette classe afin de tester l&#8217;insertion d&#8217;un utilisateur en base de données en ajoutant la méthode testCreate() :



package info.jtips.dao;

import info.jtips.dao.impl.UtilisateurDaoImpl;
import info.jtips.model.Utilisateur;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;

public class UtilisateurDaoTest {

   public UtilisateurDaoTest() {
   }

   @BeforeClass
   public static void setUpClass() throws Exception {
   }

   @AfterClass
   public static void tearDownClass() throws Exception {
   }

   @Test
   public void testCreate() {
       Utilisateur u = new Utilisateur();
       u.setIdentifiant("bb");
       u.setPrenom("Bugs");
       u.setNom("Bunny");
       u.setMotDePasse("mp");

       UtilisateurDao dao = new UtilisateurDaoImpl();
       dao.create(u);

       System.out.println("Terminé");// permet d'afficher le message "Terminé" en console
   }

}



Remarque sur le code de la classe de test :




La méthode testCreate() permet de tester la méthode create() de la DAO utilisateur


Cette méthode pour être exécutée avec JUnit est annotée avec @Test


Bonne pratique de programmation Java pour le composant à tester :


Le type de la variable dao est UtilisateurDao, ie l&#8217;interface qui spécifie les méthodes de la DAO


L&#8217;objet pointé par dao est du type UtilisateurDaoImpl, ie la classe de réalisation de l&#8217;interface DAO




Pour exécuter ce code, sélectionner le menu Test File par clic droit sur la classe UtilisateurDaoTest. Vous devriez obtenir le rapport JUnit suivant :







Vous pouvez aussi vérifier en base de données la création du nouvel enregistrement.


Pour terminer la classe de test, nous ajoutons la méthode testFindById() :



package info.jtips.dao;

import info.jtips.dao.impl.UtilisateurDaoImpl;
import info.jtips.model.Utilisateur;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;

public class UtilisateurDaoTest {

   public UtilisateurDaoTest() {
   }

   @BeforeClass
   public static void setUpClass() throws Exception {
   }

   @AfterClass
   public static void tearDownClass() throws Exception {
   }

   @Test
   public void testCreate() {
       Utilisateur u = new Utilisateur();
       u.setIdentifiant("bb");
       u.setPrenom("Bugs");
       u.setNom("Bunny");
       u.setMotDePasse("mp");

       UtilisateurDao dao = new UtilisateurDaoImpl();
       dao.create(u);

       System.out.println("Terminé");// permet d'afficher le message "Terminé" en console
   }

   @Test
   public void testFindById() {
       UtilisateurDao dao = new UtilisateurDaoImpl();
       Utilisateur u = dao.findById("bb");
       assertEquals("bb", u.getIdentifiant());
       assertEquals("Bugs", u.getPrenom());
       assertEquals("Bunny", u.getNom());
       assertEquals("mp", u.getMotDePasse());
   }

}



Remarque sur le code de la méthode testFindById() :




Pour vérifier les résultat de la méthode, nous utilisons la méthode assertEquals()


Cette méthode consiste à comparer une valeur attendue (1er argument), à un résultat (2ème argument)


Si le résultat est égal à la valeur attendue, alors l&#8217;exécution se poursuit, sinon une exception est levée et la méthode de test est interrompue




Si vous relancez l&#8217;exécution du test, vous devriez obtenir le résultat suivant :







Nous constatons que le test de la méthode findById() a réussi (rapport vert), tandis que le test de la méthode create() a échoué (rapport rouge). C&#8217;est normal, puisque nous avons tenté de créer un nouvel enregistrement avec une clé primaire déjà présente en base (enregistrement créé lors de l&#8217;exécution précédente).


Voici enfin la classe de test pour la DAO des tâches  :



package info.jtips.dao;

import info.jtips.dao.impl.TacheDaoImpl;
import info.jtips.model.Tache;
import info.jtips.model.Utilisateur;
import java.util.Date;
import java.util.List;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;

public class TacheDaoTest {

   public TacheDaoTest() {
   }

   @BeforeClass
   public static void setUpClass() throws Exception {
   }

   @AfterClass
   public static void tearDownClass() throws Exception {
   }

   @Test
   public void testCreate() {
       Utilisateur u = new Utilisateur();
       u.setIdentifiant("bb");
       u.setPrenom("Bugs");
       u.setNom("Bunny");
       u.setMotDePasse("mp");

       Tache t1 = new Tache();
       t1.setLibelle("Une première tâche");
       t1.setDateFin(new Date());
       t1.setPriorite(1);
       u.ajouterTache(t1);

       Tache t2 = new Tache();
       t2.setLibelle("Une deuxième tâche");
       t2.setDateFin(new Date());
       t2.setPriorite(1);
       u.ajouterTache(t2);

       TacheDao dao = new TacheDaoImpl();
       dao.create(t1);
       dao.create(t2);

       System.out.println("Terminé");
   }

   @Test
   public void testFindById() {
       TacheDao dao = new TacheDaoImpl();
       Tache t = dao.findById(1);
       assertEquals(1, t.getId());
       assertEquals("Une première tâche", t.getLibelle());
       assertEquals("bb", t.getUtilisateur().getIdentifiant());
   }

   @Test
   public void testFindAll() {
       TacheDao dao = new TacheDaoImpl();
       List&lt;Tache&gt; taches = dao.findAll();
       assertEquals(2, taches.size());
   }

   @Test
   public void testUpdate() {
       TacheDao dao = new TacheDaoImpl();
       Tache t = dao.findById(2);
       t.setLibelle("Tâche modifiée");
       dao.update(t);
   }

   @Test
   public void testDelete() {
       TacheDao dao = new TacheDaoImpl();
       Tache t = dao.findById(1);
       dao.delete(t);
   }

}




Refactoring !

Après le développement des classes présentées ci-dessus, je me suis aperçu que je m&#8217;étais trompé dans les noms de package. En effet, il serait souhaitable d&#8217;ajouter le nom de l&#8217;application "TodoList" dans les noms de package. Nous allons donc utiliser la fonctionnalité de refactoring offerte par NetBeans.
Pour cela, sélectionner le package à renommer, puis par clic droit, naviguer vers le menu Refactor &#8594; Rename&#8230;&#8203; :







Nous renommons ainsi l&#8217;ensemble de nos packages, sans oublier les classes de test, suivant la règle de nommage info.jtips.todolist.xxx, ce qui donne au final :








Création des classes de service métier

Cf article Partie 2 : écrire une couche de service métier.



Téléchargement du projet

Le projet NetBeans correspondant à cette première partie.





Création des classes de présentation (Swing)


Bientôt un nouvel  article pour traiter le sujet&#8230;&#8203;


</description>
          <pubDate>2009-12-21T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/Tutoriel-1</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/Tutoriel-1</guid>
        </item>
      
    
      
        <item>
          <title>Spring Web Flow</title>
          <description>
Introduction


Qu&#8217;est-ce que Spring Web Flow ?

Spring Web Flow est un sous-projet de Spring Framework. Il permet de définir et d&#8217;exécuter des enchaînements de pages dans une application web. Il est utilisable de façon autonome, mais on peut aussi l&#8217;intégrer avec un MVC web :




Spring MVC, Struts et JSF pour SWF 1


Spring MVC et JSF pour SWF 2




Voici un exemple de web flow concernant la saisie d&#8217;une commande :







Dans la suite de cet article, nous verrons comment mettre en oeuvre SWF en version 2, d&#8217;une part avec Spring MVC et d&#8217;autre part avec JSF.



Description d&#8217;un Web Flow

Les web flows sont décrits avec un langage spécifique au format XML (Flow Definition Language, noté FDL par la suite). Comme nous pouvons le remarquer dans l&#8217;exemple ci-dessous, un enchaînement consiste en une série d&#8217;étapes appelées « états ».



&lt;flow&gt;

     &lt;view-state id="saisirProduit"&gt;
     &lt;/view-state&gt;

     &lt;view-state id="saisirEmail"&gt;
     &lt;/view-state&gt;

     &lt;action-state id="enregistrerCommande"&gt;
     &lt;/action-state&gt;
     ...
&lt;/flow&gt;



Remarque : Spring IDE (plugin pour Eclipse) permet de composer visuellement les web flows SWF (cf image ci-dessous). Je n&#8217;ai cependant pas réussi à l&#8217;utiliser avec Eclipse Galileo. Cette fonctionnalité n&#8217;est pas non plus disponible avec Spring STS, bien qu&#8217;un assistant de création pour SWF 2 soit proposé.










Principe de SWF


Le web flow représente une conversation avec un utilisateur unique. Il peut être représenté à l&#8217;aide d&#8217;un diagramme d&#8217;état. Voici un exemple :







Ce qui correspond en syntaxe SWF au code XML suivant :



&lt;flow&gt;
   &lt;view-state id="saisirProduit"&gt;
      ...
   &lt;/view-state&gt;
   &lt;view-state id="saisirEmail"&gt;
      ...
   &lt;/view-state&gt;
   &lt;action-state id="enregistrerCommande"&gt;
      ...
   &lt;/action-state&gt;
&lt;/flow&gt;



Pour passer d&#8217;un état à un autre, on génère des évènements, qui permettent de franchir des transitions. Par exemple, un clic sur le bouton «Terminer » génère un évènement
qui permet de passer à l&#8217;état « Enregistrer la commande ». En langage FLD, une transition est représentée avec l&#8217;élément &lt;transition&gt; :



&lt;view-state id="saisirEmail"&gt;
   &lt;transition on="terminer" to="enregistrerCommande" /&gt;
&lt;/view-state&gt;



Notion de View State

Comme son nom le laisse penser, un view state correspond à une page que l&#8217;on souhaite afficher. Quand on entre dans un view state, SWF affiche la page correspondant à l&#8217;état. Le web flow est alors en pause. Après un certain temps, l&#8217;utilisateur génère un événement pour reprendre l&#8217;exécution du web flow (submit).
En langage FLD, pour associer une page à un view state, on utilise l&#8217;attribut view :



&lt;view-state id="saisirProduit" view="/saisirproduit.jsp"&gt;
  &lt;transition on="suivant" to="saisirEmail" /&gt;
  &lt;transition on="ajouter" to="saisirProduit" /&gt;
&lt;/view-state&gt;



La plupart du temps, un view state est défini comme l&#8217;état de démarrage du web flow. Cet état de démarrage est spécifié avec l&#8217;attribut start-state.
Si start-state n&#8217;est pas défini, l&#8217;état de démarrage est le premier état trouvé dans la liste.



&lt;flow start-state="saisirProduit"&gt;
  &lt;view-state id="saisirProduit" view="/saisirproduit.jsp"&gt;
    &lt;transition on="suivant" to="saisirEmail" /&gt;
    &lt;transition on="ajouter" to="saisirProduit" /&gt;
  &lt;/view-state&gt;
  &lt;view-state id="saisirEmail"&gt;
    ...
  &lt;/view-state&gt;
  &lt;action-state id="enregistrerCommande"&gt;
    ...
  &lt;/action-state&gt;
&lt;/flow&gt;




Notion d&#8217;Action State

L&#8217;objectif d&#8217;un action state est d&#8217;exécuter du code non visuel (i.e. un action state ne produit pas l&#8217;affichage d&#8217;une page). On peut comparer un action state à la partie contrôleur d&#8217;un MVC.


Quand on entre dans un action state, une méthode d&#8217;action est exécutée via une EL définie à l&#8217;aide de l&#8217;élément &lt;evaluate&gt; :



&lt;action-state id="enregistrerCommande"&gt;
  &lt;evaluate expression="commandeAction.enregistrerCommande()"/&gt;
  &lt;transition on="ok" to="afficherRecap" /&gt;
  &lt;transition on="ko" to="afficherErreur" /&gt;
&lt;/action-state&gt;



Dans l&#8217;exemple ci-dessus, l&#8217;EL indique qu&#8217;il faut invoquer la méthode enregistrerCommande() sur le bean nommé commandeAction. Ce bean est tout simplement un bean Spring :



@Component("commandeAction")
public class CommandeAction {
   public String enregistrerCommande() {
    ...
    commandeService.createCommande(commande);
    return "ok";
   }
}



Il est possible de passer des variables aux méthodes d&#8217;action. Dans l&#8217;exemple ci-dessous, on créé une variable nommée commandeForm qui sera passé en paramètre de la méthode enregistrerCommande().




Web Flow





&lt;flow&gt;
   &lt;var name="commandeForm" class="org.librairie.web.CommandeForm"/&gt;
   ...
   &lt;action-state id="enregistrerCommande"&gt;
     &lt;evaluate expression="commandeAction.enregistrerCommande(commandeForm)"/&gt;
     &lt;transition on="ok" to="afficherRecap" /&gt;
     &lt;transition on="ko" to="afficherErreur" /&gt;
   &lt;/action-state&gt;
   ...
&lt;/flow&gt;



Remarque : par défaut, la variable est placée dans la portée flow scope.




Méthode Java





public String enregistrerCommande(CommandeForm form) {
 ...
}



Il existe aussi des variables implicites définies par SWF telle que flowRequestContext :



&lt;evaluate expression="action.enregistrerCommande(form, flowRequestContext)"/&gt;



Cette variable peut être utilisée par exemple dans le code de l&#8217;action Java pour placer un résultat dans le flow scope :



public String enregistrerCommande(CommandeForm form, RequestContext context) {
   ...
   commandeService.createCommande(commande);
   context.getFlowScope().put("commande", commande);
   ...
}



La page web affichée après l&#8217;exécution de l&#8217;action pourra alors accéder à la commande placée dans le flow scope.



Notion d&#8217;End State

Un end state permet de définir la page à afficher lorsque le web flow se termine. Lorsque le web flow passe dans un end state, les données liées à l&#8217;instance du web flow en cours ne sont plus accessibles.


En langage FLD, un end state se définit avec l&#8217;élément &#8230;&#8203; &lt;end-state&gt; :



&lt;flow&gt;
  ...
  &lt;end-state id="afficherRecap" view="/recap.jsp" /&gt;
&lt;/flow&gt;




Exécuter une action "on start"

Une action "on start" est invoquée au démarrage du web flow, afin d&#8217;initialiser des variables utilisées par la suite dans le web flow. Dans notre exemple, la liste des produits est chargée au démarrage du web flow afin d&#8217;alimenter la liste déroulante de la page "saisirProduit" :







Dans l&#8217;exemple suivant, l&#8217;action "on start" invoque une action qui retourne une liste de produits dont le résultat est placé dans le flow scope :



&lt;flow start-state="saisirProduit"&gt;
   &lt;on-start&gt;
       &lt;evaluate expression="commandeAction.findProduit()" result="flowScope.produits"/&gt;
   &lt;/on-start&gt;

   &lt;view-state id="saisirProduit" view="/saisirproduit.jsp"&gt;
      ...
   &lt;/view-state&gt;
   ...
 &lt;/flow&gt;




Exécuter une action "on render"

Le principe d&#8217;une action "on render" consiste à exécuter du code Java avant l&#8217;affichage de la vue. Voici donc une autre façon d&#8217;initialiser la liste des produits pour notre formulaire :



&lt;view-state id="saisirProduit" view="/saisirproduit.jsp"&gt;
   &lt;on-render&gt;
     &lt;evaluate expression="commandeAction.findProduit()" result="viewScope.produits"/&gt;
   &lt;/on-render&gt;
   &lt;transition on="ajouter" to="saisirEmail"/&gt;
&lt;/view-state&gt;



Remarque : le view scope utilisé dans le code ci-dessus n&#8217;a de sens qu&#8217;avec un view state


D&#8217;autres déclenchements d&#8217;action sont possibles dans un « view state » :




&lt;on-entry&gt; : action exécutée à l&#8217;entrée dans le « view state »


&lt;on-exit&gt; : action exécutée en sortie du « view state »





Configuration Spring

Pour utiliser SWF dans une application, il faut demander au conteneur Spring de démarrer le moteur SWF. Il suffit pour cela de configurer un fichier d&#8217;application context, en utilisant les balises SWF :



&lt;beans&gt;

    &lt;webflow:flow-executor id="flowExecutor"/&gt;

    &lt;webflow:flow-registry id="flowRegistry" flow-builder-services="flowBuilderServices"&gt;
       &lt;webflow:flow-location id="flowCommande" path="/WEB-INF/webflow-commande.xml"/&gt;
       &lt;webflow:flow-location id="flow2" path="/WEB-INF/flow2.xml"/&gt;
    &lt;/webflow:flow-registry&gt;
    ...
&lt;/beans&gt;






Intégration avec Spring MVC


Configuration Spring

Afin de pouvoir utiliser SWF avec Spring MVC, il faut compléter le fichier d&#8217;application context de Spring pour notre application. La configuration minimale impose la déclaration des beans suivants : « flow builder services », « flow handler adapter » et « flow handler mapping ».



&lt;beans&gt;
   ...
   &lt;webflow:flow-builder-services id="flowBuilderServices" /&gt;

   &lt;bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter"&gt;
      &lt;property name="flowExecutor" ref="flowExecutor" /&gt;
   &lt;/bean&gt;
   &lt;bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping"&gt;
      &lt;property name="flowRegistry" ref="flowRegistry"/&gt;
      &lt;property name="order" value="0"/&gt;
   &lt;/bean&gt;

&lt;/beans&gt;




Associer un view state et une JSP

Dans le fichier de description du web flow, l&#8217;attribut « view » permet de spécifier la JSP à laquelle un view state correspond :



&lt;view-state id="saisirProduit" view="/saisirproduit.jsp"&gt;
   ...
&lt;/view-state&gt;



On peut aussi exploiter le "view resolver" de Spring MVC (ce qui permet de ne pas mettre le nom des fichiers JSP dans le document de définition du web flow). A cette fin, il faut déclarer un "view factory creator" dans la configuration Spring :




applicationContext-webflow.xml





&lt;webflow:flow-builder-services id="flowBuilderServices" view-factory-creator="viewFactoryCreator"/&gt;
&lt;bean id="viewFactoryCreator" class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator"&gt;
   &lt;property name="viewResolvers" ref="viewResolver"/&gt;
&lt;/bean&gt;





webflow-commande.xml





&lt;view-state id="saisirProduit" view="saisirproduit"&gt;
   ...
&lt;/view-state&gt;




Ecriture des JSP : invoquer un web flow

L&#8217;url à utiliser dépend du flow id et de l&#8217;url pattern de la dispatcher servlet de Spring MVC.




web.xml





&lt;servlet-mapping&gt;
   &lt;servlet-name&gt;librairie&lt;/servlet-name&gt;
   &lt;url-pattern&gt;*.do&lt;/url-pattern&gt;
&lt;/servlet-mapping&gt;





applicationContext-webflow.xml





&lt;webflow:flow-location id="flowCommande" path="/WEB-INF/webflow-commande.xml"/&gt;





JSP





&lt;a href="flowCommande.do"&gt;Passer une Commande&lt;/a&gt;

&lt;form action="flowCommande.do"&gt;
   &lt;input type="submit" value="Passer une Commande"/&gt;
&lt;/form&gt;




Ecriture des JSP : émettre un évènement


&lt;form&gt;
   &lt;input type="submit" name="_eventId_suivant" value="Suivant"/&gt;
&lt;/form&gt;

&lt;form&gt;
   &lt;input type="submit" value="Suivant" /&gt;
   &lt;input type="hidden" name="_eventId" value="suivant" /&gt;
&lt;/form&gt;

&lt;a href="${flowExecutionUrl}&amp;amp;_eventId=suivant"&gt;Suivant&lt;/a&gt;




Ecriture des JSP : formulaires

Pour écrire le formulaire, on utilise les tags de Spring MVC. Ce formulaire est lié à un objet "model" instancié par le webflow, dont la classe contient les champs des différents formulaires du web flow.




saisirproduit.jsp





&lt;form:form modelAttribute="commandeForm"&gt;
   Produit  : &lt;form:select path="idProduit"&gt;...&lt;/form:select&gt;
   Quantité : &lt;form:input path="quantite" /&gt;
   &lt;input type="submit" name="_eventId_ajouter" value="Ajouter"/&gt;
&lt;/form:form&gt;





webflow-commande.xml





&lt;flow&gt;
   &lt;var name="commandeForm" class="org.librairie.web.CommandeForm"/&gt;
   ...
&lt;/flow&gt;





CommandeForm.java





public class CommandeForm implements Serializable {
     private String idProduit;
     private int quantite;
     private String email;

     // getters et setters
     ...
}




Traitement des formulaires

Les view states assurent le traitement des formulaires après le submit. Ils assurent en effet le transfert des données saisies par l&#8217;utilisateur dans l&#8217;objet "model".




saisirproduit.jsp





&lt;form:form modelAttribute="commandeForm"&gt;
   ...
&lt;/form:form&gt;





webflow-commande.xml





&lt;view-state id="saisirProduit" model="commandeForm" view="saisirproduit.jsp"&gt;
   &lt;binder&gt;
      &lt;binding property="idProduit"/&gt;
      &lt;binding property="quantite"/&gt;
   &lt;/binder&gt;
   &lt;transition on="ajouter" to="saisirEmail"/&gt;
&lt;/view-state&gt;






Intégration avec JSF


SWF permet d&#8217;intégrer JSF uniquement dans le cas où l&#8217;on utilise Facelets (i.e. pages .xhtml). Les backing beans et les règles de navigation sont pris en charge par SWF. Il n&#8217;y a donc pas de configuration dans faces-config.xml


Configuration web.xml

En plus de la configuration standard JSF + Facelets, il faut déclarer la servlet de Spring :



&lt;servlet&gt;
   &lt;servlet-name&gt;SpringServlet&lt;/servlet-name&gt;
   &lt;servlet-class&gt;
      org.springframework.web.servlet.DispatcherServlet
   &lt;/servlet-class&gt;
   &lt;init-param&gt;
      &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;
      &lt;param-value&gt;&lt;/param-value&gt;
   &lt;/init-param&gt;
   &lt;load-on-startup&gt;2&lt;/load-on-startup&gt;
&lt;/servlet&gt;

&lt;servlet-mapping&gt;
   &lt;servlet-name&gt;SpringServlet&lt;/servlet-name&gt;
   &lt;url-pattern&gt;/spring/*&lt;/url-pattern&gt;
&lt;/servlet-mapping&gt;




Configuration Spring

La configuration minimale impose la déclaration des beans suivants : "flow builder services", "flow handler adapter" et "flow handler mapping".




applicationContext-webflow.xml





&lt;beans&gt;
   ...
   &lt;faces:flow-builder-services id="flowBuilderServices"/&gt;
   &lt;bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter"&gt;
      &lt;property name="flowExecutor" ref="flowExecutor" /&gt;
   &lt;/bean&gt;
   &lt;bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping"&gt;
      &lt;property name="flowRegistry" ref="flowRegistry" /&gt;
      &lt;property name="defaultHandler"&gt;
	      &lt;bean class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/&gt;
	   &lt;/property&gt;
   &lt;/bean&gt;
&lt;/beans&gt;




Associer un view state et une page JSF

Dans le fichier de descrption du web flow, l&#8217;attribut « view » spécifie la page .xhtml à laquelle le view state correspond.




webflow-commande.xml





&lt;view-state id="saisirProduit" view="/saisirproduit.xhtml"&gt;
   ...
&lt;/view-state&gt;



Remarque : Contrairement à Spring MVC, il n&#8217;existe pas de "view resolver" simple pour s&#8217;affranchir de l&#8217;extension .xhtml dans l&#8217;attribut view



Pages XHTML : invoquer un web flow

L&#8217;url à utiliser dépend du flow id et de l&#8217;url pattern de la dispatcher servlet de Spring MVC.




applicationContext-webflow.xml





&lt;webflow:flow-location id="flowCommande" path="/WEB-INF/webflow-commande.xml"/&gt;





web.xml





&lt;servlet-mapping&gt;
   &lt;servlet-name&gt;SpringServlet&lt;/servlet-name&gt;
   &lt;url-pattern&gt;/spring/*&lt;/url-pattern&gt;
&lt;/servlet-mapping&gt;





Page xhtml





&lt;a href="${request.contextPath}/spring/flowCommande"&gt;Passer une Commande&lt;/a&gt;

&lt;form action="${request.contextPath}/spring/flowCommande"&gt;
   &lt;input type="submit" value="Passer une Commande"/&gt;
&lt;/form&gt;




Pages XHTML : émettre un évènement

L&#8217;attribut "action" des composants de commande permet de spécifier l&#8217;évènement à générer.




saisirproduit.xhtml





&lt;h:commandButton id="btnSuivant" value="Suivant" action="suivant"/&gt;

&lt;h:commandLink id="btnSuivant" value="Suivant" action="suivant"/&gt;





webflow-commande.xml





&lt;view-state id="saisirProduit" view="/saisirproduit.xhtml"&gt;
   ...
   &lt;transition on="suivant" to="saisirEmail"/&gt;
&lt;/view-state&gt;




Pages XHTML : formulaires



saisirproduit.xhtml





&lt;h:form&gt;
   &lt;h:outputLabel for="cboProduit" value="Produit : "/&gt;
   &lt;h:selectOneMenu id="cboProduit" value="{flowScope.commandeForm.idProduit}"&gt;
      &lt;f:selectItems value="{produits}"/&gt;
   &lt;/h:selectOneMenu&gt;
   &lt;h:outputLabel for="txtQuantite" value="Quantité : "/&gt;
   &lt;h:inputText id="txtQuantite" value="#{flowScope.commandeForm.quantite}"/&gt;
   &lt;h:commandButton value="Suivant" action="suivant"/&gt;
&lt;/h:form&gt;



Remarque : Les EL JSF ont accès au "flowScope" de SWF




webflow-commande.xml





&lt;flow&gt;
   &lt;var name="commandeForm" class="org.librairie.web.CommandeForm"/&gt;
   ...
&lt;/flow&gt;




</description>
          <pubDate>2009-12-19T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/WebFlow</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/WebFlow</guid>
        </item>
      
    
      
        <item>
          <title>Reverse proxy avec Apache httpd et mod_proxy</title>
          <description>
Toutes les informations consignées ci-dessous ont été testées avec Apache 2.4.52 et Tomcat 11.


Apache mod_proxy


Le module proxy peut fonctionner en forward ou reverse.
C&#8217;est cette seconde fonctionnalité qui nous intéresse ici.


Il permet de paramétrer des transferts de requêtes entre Apache et d&#8217;autres serveurs Web, comme Tomcat ou WildFly.
Cependant, ce module ne prend pas en charge le transport pour lequel on doit associer des modules de transport : proxy_http, proxy_ajp,&#8230;&#8203;




mod_proxy sous Debian


Sous Debian, l&#8217;activation du module proxy et de ses modules de transport passe par la création de liens symbolique dans le répertoire mods-enabled de la configuration d&#8217;Apache : fichiers .load pour le chargement des modules et fichiers .conf pour leur configuration.



$&gt; ln -s /etc/apache2/mods-available/proxy.conf /etc/apache2/mods-enabled/
$&gt; ln -s /etc/apache2/mods-available/proxy.load /etc/apache2/mods-enabled/
$&gt; ln -s /etc/apache2/mods-available/proxy_http.load /etc/apache2/mods-enabled/



L&#8217;activation des modules peut aussi se faire avec a2enmod, qui crée les liens symboliques.
La désactivation se fait avec a2dismod, qui supprime les liens symboliques, sans supprimer les fichiers.



$&gt; a2enmod proxy
$&gt; a2enmod proxy_http



Pour finir, les modifications seront prises en compte après le redémarrage d&#8217;Apache.



$&gt; systemctl restart apache2
# ou
$&gt; apache2ctl restart





Configuration en HTTP


Dans cet exemple, j&#8217;ai exposé via Apache une application app déployée dans Tomcat.
Tomcat et Apache sont sur le même serveur.


Le premier niveau de configuration est d&#8217;indiquer que les requêtes du contexte /manager doivent être passées à Tomcat.
L&#8217;URL dans la directive ProxyPass est sur le protocole http, ce qui nécessite le chargement du module proxy_http.


Le paramétrage se fait dans le fichier /etc/apache2/mods-enabled/proxy.conf.



# mods-available/proxy.conf
&lt;Location /app&gt;
    ProxyPass http://localhost:8080/app
&lt;/Location&gt;



Le problème dans cette configuration, c&#8217;est que les redirections sont mal traitées.
Si l&#8217;application fait des redirections avec des redirections, elle le fait avec les informations dont elle dispose, c&#8217;est-à-dire celle de Tomcat.
Apache doit donc réécrire ces URLs, grâce à la directive ProxyPassReverse.
Et pour que Apache conserve le header Host, on peut ajouter la directive ProxyPreserveHost.



# mods-available/proxy.conf
ProxyPreserveHost on
&lt;Location /app&gt;
    ProxyPass http://localhost:8080/app
    ProxyPassReverse http://localhost:8080/app
&lt;/Location&gt;



Enfin, pour que Tomcat ait les bonnes informations pour ses logs d&#8217;accès, il faut ajouter une RemoteIpValve qui transfert l&#8217;addresse du client du header X-Forwarded-For vers le champs RemoteAddr de l&#8217;API servlet.



&lt;!-- conf/server.xml --&gt;
&lt;Valve className="org.apache.catalina.valves.RemoteIpValve" /&gt;
&lt;Valve className="org.apache.catalina.valves.AccessLogValve"
       ...
       requestAttributesEnabled="true" /&gt;





Configuration avec TLS


Si on n&#8217;a pas confiance dans le réseau entre Apache et Tomcat, on peut établir une connexion TLS.
Pour ça, il faut que le module ssl soit actif.
Puis, on active le moteur SSL pour le module proxy avec SSLProxyEngine.


Si le certificat de Tomcat n&#8217;est pas valide, on peut désactiver les vérifications.


Enfin, on modifie l&#8217;adresse du ProxyPass pour y mettre une URL en https.



# mods-available/proxy.conf
SSLProxyEngine on

SSLProxyVerify none
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off

&lt;Location /app&gt;
    ProxyPass https://localhost:8443/app
    ProxyPassReverse https://localhost:8443/app
&lt;/Location&gt;





Configuration en HTTP/2


Le protocole HTTP/2 est pris en charge par le module proxy_http2.



$&gt; a2enmod proxy_ajp



Il supporte évidemment HTTP/2 sur TLS.



# mods-available/proxy.conf
SSLProxyEngine on
&lt;Location /app&gt;
    ProxyPass h2://localhost:8443/app
    ProxyPassReverse https://localhost:8443/app
&lt;/Location&gt;



Pour ça, le connecteur 8443 doit supporter l&#8217;upgrade vers HTTP/2.



&lt;!-- conf/server.xml --&gt;
&lt;Connector port="8443" protocole="HTTP/1.1" &gt;
    &lt;UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" /&gt;
    ...
&lt;/Connector&gt;



Il supporte aussi HTTP/2 clear text.



# mods-available/proxy.conf
&lt;Location /app&gt;
    ProxyPass h2c://localhost:8080/app
    ProxyPassReverse http://localhost:8080/app
&lt;/Location&gt;



Là aussi, le connecteur doit supporter l&#8217;upgrade vers HTTP/2.



&lt;!-- conf/server.xml --&gt;
&lt;Connector port="8080" protocole="HTTP/1.1"&gt;
    &lt;UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" /&gt;
    ...
&lt;/Connector&gt;





Configuration en AJP


Avec AJP, il faut un connecteur qui supporte ce protocole dans Tomcat.



&lt;!-- conf/server.xml --&gt;
&lt;Connector protocol="AJP/1.3"
           port="8009" redirectPort="443"
           secret="5ecr3t"/&gt;



Le secret peut être omis si la communication se fait dans un réseau sûr.
Dans cas, il faut mettre l&#8217;attribut secretRequired="false".


Du coté d&#8217;Apache, le module de transport proxy_ajp doit être activé.



$&gt; a2enmod proxy_ajp



Puis ProxyPass renvoie vers le port 8009, avec le schema ajp, à la place de 8080 et http.
Pour ce protocole, il n&#8217;y a pas besoin de la directive ProxyPreserveHost.



# mods-available/proxy.conf
&lt;Location /docs&gt;
    ProxyPass ajp://localhost:8009/app secret="5ecr3t"
    ProxyPassReverse ajp://localhost:8009/app
&lt;/Location&gt;





Prise en charge des ressources statiques


Pour que les ressources statiques soient prises en charge directement par Apache, il faut les placer dans le répertoire /var/www/html et configurer le module proxy pour qu&#8217;il ne transfère pas les requêtes à Tomcat.



# mods-available/proxy.conf
ProxyPreserveHost on
&lt;Location /app&gt;
    ProxyPass http://localhost:8080/app
    ProxyPassReverse http://localhost:8080/app
&lt;/Location&gt;

&lt;Location /app/css&gt;
    ProxyPass !
&lt;/Location&gt;
&lt;Location /app/img&gt;
    ProxyPass !
&lt;/Location&gt;



Ça peut aussi se faire, avec plus de souplesse, avec le moteur de réécriture.



RewriteEngine On

# Ressource statique interceptée
RewriteRule "^/app/css/?(.*)" "$0" [L]
RewriteRule "^/app/js/?(.*)" "$0" [L]
RewriteRule "^/app/images/?(.*)" "$0" [L]

# Le reste est envoyé au mod_proxy [P]
RewriteRule "^/app/(.*)" "http://localhost:8080/app/$1" [P]

&lt;Location /app&gt;
  ProxyPassReverse http://localhost:8080/app
&lt;/Location&gt;



</description>
          <pubDate>2009-11-20T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Apache/ReverseProxy</link>
          <guid isPermaLink="true">https://www.jtips.info/Apache/ReverseProxy</guid>
        </item>
      
    
      
        <item>
          <title>Clustering Tomcat</title>
          <description>
Avant d&#8217;entrer dans les détails, précisons la signification du terme cluster ici.
Pour faire simple, il s&#8217;agit du fonctionnement de l&#8217;élément &lt;Cluster&gt;.


Lorsqu&#8217;on met en place un cluster de Tomcat, il faut avant tout gérer la répartition des requêtes HTTP avec Nginx, HAproxy ou Apache Web Server.
Ensuite, pour assurer un bon niveau de tolérances aux pannes, il faut répliquer les sessions entre les différents noeuds du cluster.
Cette partie est assurée par cet élément &lt;Cluster&gt; dont nous allons voir le fonctionnement.







Mise en place avec Docker Compose


L&#8217;installation de Tomcat en cluster est théoriquement simple.
Il suffit d&#8217;ajouter un élément &lt;Cluster&gt; dans le &lt;Engine&gt; ou le &lt;Host&gt;.



&lt;!-- conf/server-simple.xml ---&gt;
&lt;Server port="-1"&gt;
  ...
  &lt;Engine name="Catalina" defaultHost="localhost"&gt;
    &lt;Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"&gt;
    ...
  &lt;/Engine&gt;
&lt;/Server&gt;



Pour tester ça, j&#8217;ai créé une image avec une application d&#8217;exemple (celle que j&#8217;utilise en formation) et un fichier de configuration.



FROM tomcat:11

ARG SERVER_XML_FILE=server.xml

COPY conf/$SERVER_XML_FILE conf/server.xml
COPY msg.war webapps/msg.war

ENV CATALINA_OPTS="-XX:MaxRAMPercentage=75"

EXPOSE 8080 4000




~$ build --tag jtips/tomcat:simple --build-arg=SERVER_XML_FILE=server-simple.xml ./tomcat



Ensuite, j&#8217;ai utilisé Docker Compose avec 3 instances de Tomcat et un Nginx pour la répartition des requêtes HTTP.








services:
  nginx:
    image: nginx
    ports:
      - 1080:80
    volumes:
      - ../nginx/default.conf:/etc/nginx/conf.d/default.conf
  tomcat1:
    image: jtips/tomcat:simple
  tomcat2:
    extends: tomcat1
  tomcat3:
    extends: tomcat1



Pour Nginx, on a ajouté le proxy_pass et l'`upstream` dans le fichier default.conf.



upstream tomcat {
  server tomcat1:8080;
  server tomcat2:8080;
  server tomcat3:8080;
}

server {
  location /msg {
    proxy_pass http://tomcat/msg;
  }
  ...
}



Après avoir démarré l&#8217;ensemble avec docker compose up, on peut accéder à l&#8217;application sur l&#8217;adresse http://localhost:1080, et à chaque requête, on passe d&#8217;un Tomcat à l&#8217;autre.


Lors de mes premiers essais, j&#8217;ai quand même eu un problème de temps de démarrage.
Une instance a démarré en 3 secondes alors que les autres ont mis 63 secondes.
Pour comprendre et résoudre le problème, il faut entrer un peu plus dans les détails.




Configuration détaillée


En déclarant le cluster avec cette simple ligne, il faut être conscient qu&#8217;on met en place plusieurs éléments dans leur configuration par défaut.







Cette architecture du composant cluster se retrouve lorsqu&#8217;on utilise une configuration détaillée du Cluster.




Gestionnaire de sessions


Le manager est le gestionnaire de sessions.
Dans une configuration simple de Tomcat, chaque application a une instance de StandardManager pour la gestion du cycle de vie de ses sessions.
Lorsqu&#8217;on passe en mode cluster, il est remplacé par un DeltaManager, qui est capable de créer des sessions locales et d&#8217;échanger des sessions avec ses pairs.


DeltaManager

Le problème de temps de démarrage décrit ci-dessus est dû au DeltaManager.


Lorsqu&#8217;un manager sait qu&#8217;un autre membre est présent dans le cluster, il lui envoye une demande de sessions.
S&#8217;il ne reçoit rien, il se met en attente jusqu&#8217;au timeout et retarde le démarrage.
C&#8217;est ce qu&#8217;il se passe avc notre configuration et apparait dans les logs ci-dessous.



tomcat2-1  | 12:00:13.182 INFO DeltaManager.getAllClusterSessions
    Manager [localhost#/msg], requesting session state from [MemberImpl[tcp://{172, 21, 0, 2}:4000].
    This operation will timeout if no session state has been received within [60] seconds.
tomcat1-1  | 12:00:13.264 WARNING DeltaManager.waitForSendAllSessions
    Manager [localhost#/msg]: In reply to the 'Get all session data' message sent at [11/20/25, 10:59 PM],
    a 'No matching context manager' message was received after [112] ms.
tomcat1-1  | 12:00:13.265 WARNING DeltaManager.getAllClusterSessions
    Manager [localhost#/msg]: Drop message [SESSION-GET-ALL] inside GET_ALL_SESSIONS
    sync phase start date [11:59 AM] message date [11:59 PM]



Dans notre configuration, comme les instances de Tomcat démarrent exactement en même temps, on doit éviter cette attente.
La solution rudimentaire consiste à démarrer les instances de Tomcat les unes après les autres, en déclarant un dépendance entre les services.



  tomcat1:
    image: jtips/tomcat
    ...
  tomcat2:
    extends: tomcat1
    depends_on: [tomcat1]
  tomcat3:
    extends: tomcat1
    depends_on: [tomcat2]



L&#8217;inconvénient, c&#8217;est que ça augmente le temps de démarrage de l&#8217;ensemble puisqu&#8217;il n&#8217;y a plus rien en parallèle.
Une autre solution consiste à désactiver le timeout de l&#8217;élément Manager, dans le Cluster.



  &lt;Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"&gt;
    &lt;Manager className="org.apache.catalina.ha.session.DeltaManager"
             stateTransferTimeout="0" /&gt;
  &lt;/Cluster&gt;









Le problème de timeout ne se produit que dans un seul contexte.
Pour le reproduire, il faut monter le fichier conf/server.xml en volume.
Dans la plupart des cas, ça fonctionne bien avec la configuration par défaut.






BackupManager

Une troisième solution serait de passer à un BackupManager au lieu du DeltaManager.
Avec lui, chaque session n&#8217;est répliquée que sur un seul autre noeud du cluster.
Et il n&#8217;essaie pas de récupérer d&#8217;autres sessions au démarrage.



  &lt;Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"&gt;
    &lt;Manager className="org.apache.catalina.ha.session.BackupManager" /&gt;
  &lt;/Cluster&gt;



Son terrain de prédilection est le cluster large, au delà de 4 instances, et la résolution du problème au démarrage n&#8217;est qu&#8217;un effet de bord.
Et il a quand même un point faible, comme chaque session n&#8217;a qu&#8217;un seul replica, si deux noeuds tombent en même temps, on perd des sessions.





Multicast vs TCP


Dans la configuration par défaut, les instances de Tomcat détectent la présence des autres en multicast.
Avec notre réseau local, ça fonctionne bien, mais sur un vrai réseau, les admins n&#8217;aiment pas trop ça parce que ça parle trop.
Nous allons donc modifier la configuration pour passer en connexions TCP/IP.


Les composants du GroupChannel sont concernés par cette modification, et plus particulièrement le Membership.
Il communique en multicast pour découvrir d&#8217;autres membres sur la même adresse et le même port de multicast (228.0.0.4:45564, par défaut).
Chaque instance envoie à cette adresse sa propre adresse IP et le port sur lequel écoute son Receiver (4000 par défaut).
Les communications entre chaque Sender et les Receiver des autres instances se font en TCP/IP.


Pour éviter le multicast, il faut donc changer le service de membership et basculer vers un fonctionnement statique.
Avec le StaticMembershipService, chaque instance est identifiée explicitement dans la configuration.



&lt;Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"&gt;
  &lt;Channel className="org.apache.catalina.tribes.group.GroupChannel"&gt;
    &lt;Membership className="org.apache.catalina.tribes.membership.StaticMembershipService"&gt;
      &lt;Member className="org.apache.catalina.tribes.membership.StaticMember"
              host="tomcat1" port="4000"
              uniqueId="{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}" /&gt;
      &lt;Member className="org.apache.catalina.tribes.membership.StaticMember"
              host="tomcat2" port="4000"
              uniqueId="{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2}" /&gt;
      &lt;Member className="org.apache.catalina.tribes.membership.StaticMember"
              host="tomcat3" port="4000"
              uniqueId="{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3}" /&gt;
    &lt;/Membership&gt;
  &lt;/Channel&gt;
&lt;/Cluster&gt;



Au passage, le démarrage est bien plus rapide, on passe d&#8217;un peu plus de 3 secondes à une grosse seconde.


Par contre, ça limite le fonctionnement dynamique pour l&#8217;ajout d&#8217;instance en cours de route.
Il semble toujours possible d&#8217;ajouter une instance sans modifier les instances existantes.
Pour ça, la nouvelle instance doit avoir la liste de tous les autres membres du cluster.
Mais cette configuration est instable, avec beaucoup d&#8217;événement memberDisappeared (toutes les 25 secondes).
Pour stabiliser la nouvelle topologie, il faut redémarrer tous les membres avec une liste complète.




Cluster dans le cloud


Aucune des solutions présentées (multicast et static) n&#8217;est satisfaisante dans un environnement dynamique comme un cloud.
Pour moderniser notre cluster Tomcat il faut utiliser CloudMembershipService, qui existe depuis Tomcat 9.
Il n&#8217;apparaît pas dans la documentation mais on trouve un peu d&#8217;information dans la javadoc.


Son comportement par défaut se base sur la résolution DNS.
Chaque instance Tomcat a sa propre adresse IP, mais elles partagent toutes un nom DNS commun.
C&#8217;est une configuration qu&#8217;on peut reproduire avec Docker Compose ou Kubernetes.


Commençons par la configuration Tomcat.
Dans l&#8217;extrait ci-dessous, on a précisé l&#8217;attribut membershipProviderClassName, mais ce n&#8217;était pas nécessaire car DNSMembershipProvider est la valeur par défaut.



&lt;Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"&gt;
  &lt;Channel className="org.apache.catalina.tribes.group.GroupChannel"&gt;
    &lt;Membership className="org.apache.catalina.tribes.membership.cloud.CloudMembershipService"
                membershipProviderClassName="org.apache.catalina.tribes.membership.cloud.DNSMembershipProvider"/&gt;
  &lt;/Channel&gt;
&lt;/Cluster&gt;



C&#8217;est sobre.
En réalité, on peut avoir besoin de plus d&#8217;information, mais ça se fait à la façon cloud, par des variables d&#8217;environnement.


Pour cet exemple, nous avons un environnement composé d&#8217;un Nginx et de 3 replicas identiques de Tomcat.








services:
  nginx:
    image: jtips/nginx
    ports:
      - 1080:80
    depends_on: [tomcat0]
  tomcat0:
    image: jtips/tomcat
    environment:
      DNS_MEMBERSHIP_SERVICE_NAME: "tomcat0"
    deploy:
      replicas: 3



La variable DNS_MEMBERSHIP_SERVICE_NAME est utilisée par le DNSMembershipProvider pour rechercher les instances par leur nom de service.
Sans cette variable, il recherche un service nommé tomcat.


La configuration de Tomcat ressemble à ça:



&lt;Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"&gt;
  &lt;Channel className="org.apache.catalina.tribes.group.GroupChannel"&gt;
    &lt;Membership className="org.apache.catalina.tribes.membership.cloud.CloudMembershipService"
                membershipProviderClassName="org.apache.catalina.tribes.membership.cloud.DNSMembershipProvider"/&gt;
  &lt;/Channel&gt;
&lt;/Cluster&gt;



Du coté de Nginx, on ne déclare plus qu"un server, la rotation sera assurée par le DNS de Docker.



upstream tomcat {
  server tomcat0:8080;
}





Cluster k8s


La configuration précédente fonctionne aussi avec Kubernetes.
Le répartiteur est géré par Ingress et les instances de Tomcat sont déployées dans un service, avec 3 replicas.







Avec Kubernetes, Tomcat a aussi le KubernetesMembershipProvider.
Il utilise l&#8217;API de Kubernetes, ce qui apporte plus de souplesse que la solution DNS.



  &lt;Membership className="org.apache.catalina.tribes.membership.cloud.CloudMembershipService"
              membershipProviderClassName="org.apache.catalina.tribes.membership.cloud.KubernetesMembershipProvider"/&gt;



Les instances de Tomcat peuvent être dans des services différents.
Elles sont sélectionnées par un label.
Par contre, toutes les instances de Tomcat doivent être dans le même namespace.







Le page Kubernetes détaille la configuration.




Sécurité


Afin de mitiger le risque de MITM et d&#8217;injection, on peut mettre en place le chiffrement des messages.



&lt;Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"&gt;
  ...

  &lt;Interceptor className="org.apache.catalina.tribes.group.interceptors.EncryptInterceptor"
                encryptionKey="AES is the way"/&gt;
  &lt;Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/&gt;
  &lt;Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor"/&gt;
&lt;/Cluster&gt;



Ceci ne réduit pas les risques d&#8217;attaque par DoS.








Il faudrait utiliser la technique de substitution par property source pour l&#8217;attribut encryptionKey, plutôt que le renseigner directement dans le fichier.







Affinité de session


En production, du moment que l&#8217;application est stateful, et même avec une bonne réplication de session, il vaut mieux utiliser l&#8217;affinité.


La technique historique est de déclarer une jvmRoute.
Elle est utilisée pour suffixer l&#8217;identifiant des sessions, afin que le répartiteur puisse réorienter chaque session sur l&#8217;instance qui l&#8217;a créée.



&lt;Engine name="Catalina" defaultHost="localhost" jvmRoute="${tomcat.route}"&gt;
  ...
&lt;/Engine&gt;



Dans cet exemple, on utilise les capacités de substitution de la configuration.
Il faut renseigner la propriété système tomcat.route.



  tomcat1:
    image: jtips/tomcat
    environment:
      CATALINA_OPTS: "-XX:MaxRAMPercentage=75 -Dtomcat.route=tct1"
  tomcat2:
    extends: tomcat1
    environment:
      CATALINA_OPTS: "-XX:MaxRAMPercentage=75 -Dtomcat.route=tct2"
  tomcat3:
    extends: tomcat1
    environment:
      CATALINA_OPTS: "-XX:MaxRAMPercentage=75 -Dtomcat.route=tct3"



Enfin, on exploite la route au niveau du répartiteur de requêtes.
Apache httpd, avec mod_proxy ou mod_jk, utilisait cette notion de route.
Avec Nginx, il faut l&#8217;extension jvm_route qui n&#8217;est pas implantée dans la version gratuite.
Et cette technique ne fonctionne plus si les instances de Tomcat sont des replicas d&#8217;un même service.


Nginx peut se passer de route avec ip_hash qui organise la répartition des requêtes en se basant sur le hash de l&#8217;adresse IP du client.



upstream tomcat {
  ip_hash;
  server tomcat1:8080;
  server tomcat2:8080;
  server tomcat3:8080;
}





Synthèse


La configuration par défaut fonctionne bien sur un petit réseau local, avec des instances de Tomcat qui ne démarrent pas strictement en même temps.


Le problème de timeout au démarrage a été résolu en configurant le DeltaManager avec stateTransferTimeout="0".


Le problème de réseau étendu a été résolu en désactivant la détection de membres par multicast, grâce au StaticMembershipService ou au CloudMembershipService.


La sécurité peut être renforcée grâce au EncryptInterceptor.


</description>
          <pubDate>2009-10-30T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Tomcat/Cluster</link>
          <guid isPermaLink="true">https://www.jtips.info/Tomcat/Cluster</guid>
        </item>
      
    
      
        <item>
          <title>Mémoire Java</title>
          <description>
La mémoire utilisée par un processus Java est organisée en plusieurs parties :




Heap : elle sert à l&#8217;instanciation des objets


Perm : on parle de l&#8217;espace des objets permanent, pour la définition des classes


Code cache : elle est utilisée pour le code machine généré par le compilateur JIT


Stack : chaque thread a son espace d&#8217;exécution




La taille totale du processus peut être calculé ainsi :
mémoire totale = heap + perm + code cache + (stack size x nb threads) + &#8230;&#8203;


Heap


TODO




Perm


TODO




Code cache


TODO




Stack


La taille du stack doit être suffisamment grande pour permettre l&#8217;exécution de méthodes complexes. Un stack size trop faible peut déboucher sur une erreur de type "java.lang.StackOverflowError". Attention toutefois, cette erreur peut être tout simplement due à une récursivité infinie, ou trop profonde. A l&#8217;opposé, un stack size trop grand peut déboucher sur une erreur de type "java.lang.OutOfMemoryError".


Les arguments qui permettent de modifier la taille du stack sont -Xss ou -XX:ThreadStackSize. Le rôle de chacun de ces arguments est relativement mal défini, et peut changer d&#8217;un système à l&#8217;autre.



# Utilisation de -Xss : il faut préciser l'unité
java -Xss512k MaClasse

# Utilisation de -XX:ThreadStackSize : en ko
java -XX:ThreadStackSize=512 MaClasse



La détermination du stack n&#8217;est pas toujours simple. Lorsqu&#8217;on consulte la documentation de Sun sur le sujet, on se rend compte que plusieurs paramètres peuvent entrer en ligne de compte, en particulier le système d&#8217;exploitation. Par exemple, sous Linux, il semblerait qu&#8217;il faille parfois modifier la taille de stack des threads au niveau du système.



 ulimit -s 2024



Quelques petits tests permettent de constater concrètement comment ces paramètres sont pris en compte. Le premier est une simple méthode recursive, qui s&#8217;appelle elle-même jusqu&#8217;à ce qu&#8217;un erreur java.lang.StackOverflowError survienne. Le programme intercepte l&#8217;erreur et affiche le nombre d&#8217;appels qui ont été effectués.


Lorsque je réalise ce test sous Windows 32bits, avec un JDK 6 de Sun, avec une machine de profil client (mono-processeur), j&#8217;obtiens les résultats suivants :



java EvaluateStack recursive
=&gt; 8506 calls

java -server EvaluateStack recursive
=&gt; 5811 calls

java -Xss320k EvaluateStack recursive
=&gt; 8506 calls

java -server -Xss320k EvaluateStack recursive
=&gt; 5811 calls

java  -server -Xss512k EvaluateStack recursive
=&gt; 9592 calls

java -server -Xss320k EvaluateStack recursive
=&gt; 14650 calls

java -XX:ThreadStackSize=512 EvaluateStack recursive
=&gt; 8506 calls

java -server -XX:ThreadStackSize=512 EvaluateStack recursive
=&gt; 5811 calls

java -Xss4096k EvaluateStack recursive
=&gt; 129340 calls



La première conclusion, c&#8217;est que la valeur par défaut du stack size est de 320ko, et que Hotspot client permet une récursivité plus importante. La seconde, c&#8217;est que l&#8217;argument -XX:ThreadStackSize semble inopérant. Une telle réponse n&#8217;étant pas satisfaisante, j&#8217;ai poussé le test en modifiant mon code. Dans la première version l&#8217;appel récursif était directement appelé depuis la méthode main, c&#8217;est-à-dire dans le thread principal ; après modification, l&#8217;appel récursif se fait dans un thread autonome, ce qui change significativement les résultats ;



java -XX:ThreadStackSize=512 EvaluateStack recursive
=&gt; 14636 calls

java -Xss512k EvaluateStack recursive
=&gt; 14636 calls



La nouvelle conclusion, c&#8217;est que -Xss affecte tous les threads, y compris le principal, et -XX:ThreadStackSize ne concerne pas le thread principal. Il semblerait que certaines versions de la JVM aient un argument spécifique pour celui-ci (-XX:MainThreadStackSize), ce qui n&#8217;est pas le cas pour mon environnement.


Les mêmes tests réalisés sur la même machine, mais avec un JDK 5 donne les résultats suivants :



java EvaluateStack recursive
=&gt; 6620 calls

java -Xss120k EvaluateStack recursive
=&gt; 6620 calls

java -Xss256k EvaluateStack recursive
=&gt; 31196 calls

java -Xss1024k EvaluateStack recursive
=&gt; 31196 calls



Par conséquent, les appels récursifs en JDK 5 semblent beaucoup moins gourmands ! Par contre, la valeur prise en compte semble stagner entre 256k et 1024k (même nombre de calls) ; au-delà de 1024k, les deux versions du JDK présentent les mêmes valeurs de calls. Autre différence notable, -Xss ne semble pas affecter le thread principal.


Enfin, dans un dernier test, j&#8217;ai évalué le nombre de threads que la JVM pouvait supporter avant d&#8217;émettre un java.lang.OutOfMemoryError. Pour cela, je démarre des threads en boucle, et chaque thread est mis en attente. Sans surpise, le nombre de threads supportés décroit lorsqu&#8217;on augmente le stack size.



java -Xss320k EvaluateStack threads
=&gt; Threads created = 5657

java -Xss1024k EvaluateStack threads
=&gt; Threads created = 1806



Il est intéressant de constater que ce nombre décroit lorsque la taille réservée au heap augmente. Ceci s&#8217;exmplique par le fait que les threads occupent la mémoire entre les espaces réservés (heap, perm, code cache,&#8230;&#8203;) et la taille maximale du processus.



java -Xmx512m -Xss1024k EvaluateStack threads
=&gt; Threads created = 1358



Le même test sous Linux Ubuntu 32bits; avec OpenJDK 6 nous donne des résultats très similaires en ce qui concerne la récursivité. En revanche, mon test de nombre de threads ne fonctionne pas, on arrive à un blocage de la machine (machine virtuelle avec peu de capacité) !




Récapitulatif


Pour spécifier la taille de chaque zone, on utilise les paramètres de lancement de Java suivant :









Espace
Maximum
Initial




Heap
-Xmx
-Xms


Perm
-XX:MaxPermSize
-XX:PermSize


Code cache
-XX:ReservedCodeCacheSize
-XX:InitialCodeCacheSize


Stack
-Xss ou -XX:ThreadStackSize
(taille fixe)




Une liste exhaustive des paramètres de la machine virtuelle de Sun peut être trouvée sur le site de HP (!) ou celui de Sun, avec une page pour les paramètres standards et une page pour les paramètres spécifiques.


</description>
          <pubDate>2009-10-23T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/M%C3%A9moire</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/M%C3%A9moire</guid>
        </item>
      
    
      
        <item>
          <title>Logging Log4J avec CXF</title>
          <description>
Dans sa configuration par défaut, Apache CXF utilise le système de trace du JDK (java.util.logging). Comme je l&#8217;ai déjà fait pour Tomcat, je préfère utiliser Log4J , voire LogBack lorsque c&#8217;est possible. Ici, nous verrons comment il est possible d&#8217;orienter facilement les traces de CXF vers Log4J.


Intégration par fichier XML


La première solution consiste à ajouter un fichier META-INF/cxf/org.apache.cxf.Logger dans un endroit accessible par un classloader. Cet endroit peut être la racine d&#8217;un fichier jar ou ear, ou le répertoire WEB-INF/classes/ d&#8217;un fichier war (ce qui donne un fichier WEB-INF/classes/META-INF/cxf/org.apache.cxf.Logger !). Ce fichier ne contient qu&#8217;une ligne qui contient le nom qualifié de la bonne classe de logger :



 # META-INF/cxf/org.apache.cxf.Logger
 org.apache.cxf.common.logging.Log4jLogger





Intégration par variable d&#8217;environnement


Une solution plus simple encore, et moins intrusive que la solution précédente, consiste à ajouter une variable d&#8217;environnement Java au lancement du programme qui utilise CXF, soit en paramètre du programme, soit, pour JBoss ou Tomcat, en variable JAVA_OPTS :



 JAVA_OPTS=-Dorg.apache.cxf.Logger=org.apache.cxf.common.logging.Log4jLogger



Le défaut de cette solution, c&#8217;est que la modification n&#8217;est pas intégrée à l&#8217;application elle-même, mais à un script elle-même, ce qui peut être gênant dans le cas d&#8217;une application JavaEE.


Remarque : cette solution a été testée avec CXF 2.


</description>
          <pubDate>2009-10-12T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/CXF/Log4J</link>
          <guid isPermaLink="true">https://www.jtips.info/CXF/Log4J</guid>
        </item>
      
    
      
        <item>
          <title>Script d&apos;installation de Tomcat</title>
          <description>
Plutôt que de reproduire manuellement la procédure d&#8217;installation de Tomcat, j&#8217;ai cherché à automatiser un maximum de tâches.
Le script qui en découle est encore sommaire et ne fournit pas une configuration définitive, et de toute façon par conforme à une configuration de production.
De plus, ce travail est ciblé pour mon système d&#8217;exploitation habituel qui est Linux Ubuntu, avec Tomcat 6.


Procédure d&#8217;installation


Installation de Java

Le premier pré-requis est la présence de Java. J&#8217;utilise la version 6 d&#8217;OpenJDK.
J&#8217;en profite aussi pour ajouter des outils associés à Java, comme la commande update-java-alternative qui peut être pratique pour détecter le répertoire d&#8217;installation du JDK.



# Installation de Java
apt-get -y install openjdk-6-jdk
apt-get -y install java-common



Cette portion pourrait être améliorée en vérifiant au préalable la présence d&#8217;un autre JDK 6.



Installation de Tomcat

Dans cette portion, on télécharge la distribution de Tomcat, puis on l&#8217;installe dans le répertoire /usr/java.
On crée en plus un lien symbolique, qui permet d&#8217;accéder au répertoire de Tomcat sans se préoccuper du numéro de version dans le nom du répertoire.



# Variables (racine du miroir et version de Tomcat)
MIRROR_HOME=link:http://mir2.ovh.net/ftp.apache.org/dist[http://mir2.ovh.net/ftp.apache.org/dist]
TOMCAT_VERSION=6.0.20

# Téléchargement et installation de tomcat
wget $MIRROR_HOME/tomcat/tomcat-6/v6.0.20/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz
mkdir /usr/java
tar -xvf apache-tomcat-$TOMCAT_VERSION.tar.gz -C /usr/java/
ln -s /usr/java/apache-tomcat-$TOMCAT_VERSION/ /usr/java/apache-tomcat




Installation en service

Pour l&#8217;installation de Tomcat en service, j&#8217;ai préparé un script de type init.d qu&#8217;il faut télécharger et installer.
Ce script est épuré et ne tient pas compte des bonnes pratiques de mise en production.


Pour le démarrage, il lance le script startup de Tomcat avec l&#8217;utilisateur java.
Pour l&#8217;arrêt, il lance le script shutdown.



#!/bin/bash
# Tomcat auto-start
#
# description: Démarrage automatique de Tomcat

export JAVA_HOME=/usr/lib/jvm/java-6-openjdk
export CATALINA_HOME=/usr/java/apache-tomcat
USER=java

# Fonction de démarrage
start()
{
  su -p -s /bin/sh - $USER $CATALINA_HOME/bin/startup.sh
}

# Fonction d'arrêt
stop()
{
  sh $CATALINA_HOME/bin/shutdown.sh
}

case $1 in
start)
  start
  ;;
stop)
  stop
  ;;
restart)
  stop
  sleep 1
  start
  ;;
esac
exit 0



Pour installer Tomcat, il suffit donc de copier ou télécharger ce script dans le répertoire bin de Tomcat et de faire un lien symbolique depuis /etc/init.d/tomcat.



SCRIPTS_HOME=https://gist.githubusercontent.com/hasalex/d1a3b7b1083c4f38399baa4ded6db243/raw/0371d33c331defe12de382e5e732167386c82e8e/
wget $SCRIPTS_HOME/tomcat-init-ubuntu.sh
mv tomcat-init-ubuntu.sh /usr/java/apache-tomcat/bin/init-ubuntu.sh
ln -s /usr/java/apache-tomcat/bin/init-ubuntu.sh /etc/init.d/tomcat
chmod 755 /etc/init.d/tomcat



Enfin, pour que ce script soit pris en compte automatiquement au démarrage et à l&#8217;arrêt du serveur, on ajoute des lien symboliques dans /etc/rcN.d.



ln -s /etc/init.d/tomcat /etc/rc1.d/K80tomcat
ln -s /etc/init.d/tomcat /etc/rc2.d/S80tomcat




Installation de Log4J

Je n&#8217;aime vraiment pas utiliser JULI, et même en développement, je préfère Log4J.
J&#8217;ai donc préparé un fichier de configuration élémentaire qu&#8217;il faut télécharger et installer dans le répertoire lib de Tomcat, en plus des fichiers jar de l&#8217;adaptateur Log4J.



# LOG4J
wget $MIRROR_HOME/logging/log4j/1.2.15/apache-log4j-1.2.15.tar.gz
tar -xvf apache-log4j-1.2.15.tar.gz apache-log4j-1.2.15/log4j-1.2.15.jar
mv apache-log4j-1.2.15/log4j-1.2.15.jar  /usr/java/apache-tomcat/lib/
rm -r apache-log4j-*
wget $MIRROR_HOME/tomcat/tomcat-6/v$TOMCAT_VERSION/bin/extras/tomcat-juli-adapters.jar
mv tomcat-juli-adapters.jar /usr/java/apache-tomcat/lib/
wget $MIRROR_HOME/tomcat/tomcat-6/v$TOMCAT_VERSION/bin/extras/tomcat-juli.jar
mv tomcat-juli.jar /usr/java/apache-tomcat/bin/
wget $SCRIPTS_HOME/tomcat-log4j.properties
mv tomcat-log4j.properties /usr/java/apache-tomcat/lib/log4j.properties






Script d&#8217;installation


Le script complet d&#8217;installation peut être téléchargé et utilisé librement.



wget https://gist.githubusercontent.com/hasalex/d1a3b7b1083c4f38399baa4ded6db243/raw/522fcad19e17a04622f1a6d98a4f15bd90e59ab1/tomcat-install.sh
chmod +x tomcat-install.sh
bash tomcat-install.sh



</description>
          <pubDate>2009-07-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Tomcat/Installation</link>
          <guid isPermaLink="true">https://www.jtips.info/Tomcat/Installation</guid>
        </item>
      
    
      
        <item>
          <title>Glassfish/Reseau</title>
          <description>
Port ouverts avec Glassfish 2


Les ports ouverts par défaut par Glassfish 2 sont :




4848 - Admin


8080 - HTTP, applications Web


7676 - JMS


3700 et 3920 - IIOP


8181 - HTTP sur SSL


3820 - IIOP sur SSL


8686 - JMX




</description>
          <pubDate>2009-06-17T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Glassfish/Reseau</link>
          <guid isPermaLink="true">https://www.jtips.info/Glassfish/Reseau</guid>
        </item>
      
    
      
        <item>
          <title>Gestion des mots de passe</title>
          <description>
L&#8217;objectif de cet article est d&#8217;exposer les réponses que j&#8217;ai trouvée à certains problèmes de configuration du service d&#8217;authentification de Jonas. Pour les principes généraux de cette configuration, je renverrais le lecteur vers la  documementation de Jonas.


jonas-realm.xml


Ce fichier sert à spécifier les techniques d&#8217;authentification utilisées par les applications déployées dans Jonas. Ces techniques peuvent être de plusieurs natures : stockage des users / passwords / roles dans une base de données, dans un annuaire LDAP, dans un fichier texte,&#8230;&#8203;


Dans l&#8217;exemple fourni, l&#8217;authentification se fait via un memoryrealm ; plusieurs exemples sont donnés en commentaire, en particulier un ldaprealm et un dsrealm.


Memory Realm

Le Memory Realm fourni est utilisé en particulier pour l&#8217;application d&#8217;administration, qui requiert le rôle jonas-admin. En étudiant rapidement ce realm, on constate que les utilisateurs tomcat et jadmin ont directement ce rôle, et que l&#8217;utilisateur jonas a aussi ce rôle, de par son appartenance au groupe jonas.


Il faut donc en conclure qu&#8217;il faut faire le ménage dans ces utilisateurs, avant toute mise en production, ou, au moins, changer les mots de passe. Encore faut-il savoir enregistrer correctement les mots de passe ; si vous souhaitez les écrire en clair, il n&#8217;y a aucun problème, mais si vous préférez les formats MD5 ou SHA, quelques problèmes apparaissent&#8230;&#8203;





Chiffrement des mots de passe


La documentation de Jonas nous indique juste qu&#8217;il est possible de stocker les mots de passe chiffrés en MD5 ou SHA. Le fichier jonas-realm.xml nous fourni un exemple de chaque chiffrement, pour le même mot de passe (jonas).



 &lt;user name="jadmin" password="{MD5}nF3dVBB3NPfRgzWlJFwoaw==" roles="jonas-admin" /&gt;
 &lt;user name="jonas" password="SHA:NaLG+uYfgHeqth+qQBlyKr8FCTw=" groups="jonas" /&gt;



En essayant d&#8217;appliquer une digestion MD5 ou SHA sur le texte "jonas", on obtient les résultats suivants :



 ~$ echo jonas | md5sum
 ~$ 5314232b04d20b370a37a6b784276635

 ~$ echo jonas | sha1sum
 ~$ b436e3b7cca6ca173669b9b17ca7338623712c4a



Il apparait clairement que les résultats ne corresopondent pas à l&#8217;exemple&#8230;&#8203;


Chiffrement SHA avec htpasswd

Pour chiffrer un mot de passe en SHA, il est possible d&#8217;utiliser la commande htpasswd, utiliser pour les mots de passe d&#8217;Apache HTTP Server.



 ~$ htpasswd -nbs ""  jonas
 ~$ :{SHA}NaLG+uYfgHeqth+qQBlyKr8FCTw=



La documentation de Apache précise que cette commande utilise SHA1 (sur 160 bits) avec un encodage base64.



Chiffrement MD5

La commande htpasswd ne fonctionne pas pour la digestion MD5 car l&#8217;algorithme utilisé par Apache n&#8217;est pas standard. Il a donc fallu approfondir les recherches.


Par analogie avec le traitement par SHA, j&#8217;ai essayé de combiner MD5 avec un encodage base64, en utilisant md5sum, ce qui n&#8217;a pas donné de résultat plus satisfaisant.



 ~$ echo 5314232b04d20b370a37a6b784276635 | base64
 ~$ NTMxNDIzMmIwNGQyMGIzNzBhMzdhNmI3ODQyNzY2MzUK



Ma première erreur a été d&#8217;utiliser simplement la commande echo ; en effet, celle-ci ajoute un retour à la ligne à la fin du texte. La bonne commande aurait été avec l&#8217;argument -n.


La deuxième erreur a été d&#8217;utiliser md5sum qui traite les résultats sous forme de chaîne de caractères. Si cette commande est capable de lire du contenu binaire (argument -b), il n&#8217;existe pas d&#8217;argument pour modifier le format de sortie. J&#8217;ai donc décidé de basculer vers openssl qui offre cette possibilité.


Le résultat de ces nouvelles tentatives est le suivant :



 ~$ echo -n jonas | openssl md5 -binary | base64
 ~$ nF3dVBB3NPfRgzWlJFwoaw==



Le compte est bon ! Il suffit d&#8217;ajouter le préfixe "{MD5}".



Chiffrement SHA avec OpenSSL

Pour avoir un traitement homogène entre SHA et MD5, il est aussi possible d&#8217;utiliser la commande sha1 d&#8217;OpenSSL, avec les mêmes arguments :



 ~$ echo -n jonas | openssl sha1 -binary | base64
 ~$ NaLG+uYfgHeqth+qQBlyKr8FCTw=



Là aussi le résultat est bon, au préfixe {SHA} prêt.



Script de chiffrement

Il devient alors facile de créer un script, basé sur OpenSSL.



 #!/bin/bash
 case "$1" in
   md5)
     prefix="{MD5}"
     algo="md5"
     ;;
   sha)
     prefix="SHA:"
     algo="sha1"
     ;;
   *)
     echo $1 : "Algo non géré"
     exit 1
     ;;
 esac

 echo -n $2 | openssl $algo -binary | base64




Chiffrement avec Java

Le même script aurait aussi facilement pu être écrit sous forme d&#8217;une classe Java.


En effet, le chiffrement SHA1, avec encodage base64 se traite par la ligne de code suivante :



 "SHA:" + new sun.misc.BASE64Encoder().encode(java.security.MessageDigest.getInstance("SHA1").digest(password.getBytes()))



Et l&#8217;équivalent pour MD5 est :



 "{MD5}" + new sun.misc.BASE64Encoder().encode(java.security.MessageDigest.getInstance("MD5").digest(password.getBytes()));



En assemblant tout cela, on obtient la classe suivante :



 package fr.sewatech.utils;

 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;

 import sun.misc.BASE64Encoder;

 public class JonasSecurity {
   public static void main(String[] args) {
     if (args.length &lt; 2) {
       help();
     }

     String algo = args[0].toLowerCase();
     String prefix = null;
     String password = args[1];
     if ("md5".equals(algo)) {
       prefix = "{MD5}";
     } else if ("sha".equals(algo)) {
       prefix = "SHA:";
       algo = "sha1";
     } else {
       help();
     }

     try {
       System.out.println(prefix
           + new BASE64Encoder().encode(MessageDigest
               .getInstance(algo).digest(password.getBytes())));
     } catch (NoSuchAlgorithmException e) {
       help();
     }
   }

   private static void help() {
     System.out
         .println("Il faut 2 argument : le premier est l'algorithme (md5 ou sha), et le deuxième est le mot de passe à chiffrer.");
     System.exit(1);
   }
 }






Conclusion


Le chiffrement et l&#8217;encodage des mots de passe pour Jonas présente quelques pièges qui peuvent prendre un peu de temps. Le point principal, si on veut travailler avec les utilitaires de Linux, est de passer en mode binaire.


La façon de stocker les données d&#8217;authentification et de chiffrer les mots de passe est identique entre Jonas 4 et Jonas 5, et les techniques présentées ont été testées avec Jonas 4.8, Jonas 4.10 et Jonas 5.1 RC2.


</description>
          <pubDate>2009-05-17T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Jonas/Security</link>
          <guid isPermaLink="true">https://www.jtips.info/Jonas/Security</guid>
        </item>
      
    
      
        <item>
          <title>Outils des technologies sémantiques</title>
          <description>
Les technologies sémantiques sont en pleine effervescence, avec de nombreux outils.
Pour s&#8217;y retrouver entre leurs fonctionnalités, rien ne vaut un petit récapitulatif.


Requêtes SPARQL


Twinkle

Twinkle est un outil de requêtes SPARQL.


Il fonctionne avec Java 5+ et utilise ARQ de Jena.
Il se présente sous forme d&#8217;interface graphique Swing.



Joseki



ARQ

ARQ est le moteur de requêtes SPARQL intégré à Jena. Il peut être utilisé en programmation Java ou en ligne de commande.


Exemple d&#8217;utilisation d&#8217;ARQ (SELECT) :



 export ARQROOT=/usr/java/jena-2.5.6
 $ARQROOT/bin/sparql --query=map.rq --data=link:http://rdf.freebase.com/rdf/en.paris[http://rdf.freebase.com/rdf/en.paris] --results=XML



Cet exemple interroge la page sur Paris dans freebase, avec le requête enregistrée dans map.rq, et restitue un résultat au format SPARQL/XML.


Pour un CONSTRUCT, le format de sortie RDF/XML est plus adapté, et la redirection dans un fichier RDF facilite son exploitation par d&#8217;autres outils.



 export ARQROOT=/usr/java/jena-2.5.6
 $ARQROOT/bin/sparql --query=map-construct.rq --data=link:http://rdf.freebase.com/rdf/en.paris[http://rdf.freebase.com/rdf/en.paris] --results=RDF &gt; ../out.rdf






Parser RDF


W3C

L&#8217;outil de validation en ligne du W3C permet de vérifier si un contenu est bien conforme aux spécifications.



Mindswap RDF Converter

Cet outil en ligne permet de convertir des ressources RDF entre les formats RDF/XML, N3 et NTriple.



RDF-Parser

RDF-Parser est un parser RDF en javascript. Il est pratique pour des petits exemples, mais supporte mal les gros volumes.


De toute façon, dans une architecture sérieuse, le parsing RDF directement dans le navigateur ne me semble pas être une bonne solution&#8230;&#8203;



Jena

Jena est un framework sémantique en Java, open source, réalisé par les labs d&#8217;HP.
Il est capable de faire du parsing RDF, RDFS, OWL, de gérer des requêtes SPARQL, via ARQ, et peut jouer le rôle de moteur d&#8217;inférence ou en intégrer un autre, comme Pellet.
Je considère Jena comme la Rolls des parsers, en Java.


En revanche, l&#8217;accès en ligne de commande est relativement pauvre.
Seule la programmation Java permet d&#8217;exploiter la richesse de Jane.





Reasonner


Pellet



Autres

Racer, FACT++,&#8230;&#8203;



</description>
          <pubDate>2009-05-06T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Semantic/Outils</link>
          <guid isPermaLink="true">https://www.jtips.info/Semantic/Outils</guid>
        </item>
      
    
      
        <item>
          <title>Apache Commons CLI</title>
          <description>
Développer une application en ligne de commande avec Apache Commons CLI


Ou comment gérer les paramètres passés au programme&#8230;&#8203;


Problématique


Développer une application en ligne de commande en Java ne pose pas de problème en soit.
En effet, il suffit de développer une méthode main et de traiter les arguments reçus dans la variable args.



 public static void main(String[] args) {
   String premiereOption = args[0];
   ...
 }



Dans une application de ce type, les arguments servent à paramétrer le comportement du programme et à injecter des valeurs à utiliser dans le traitement.
Le problème, c&#8217;est que cette façon de procéder ne permet pas facilement de gérer des passages d&#8217;arguments nommés, optionnels ou obligatoire, à la Posix ou à la GNU.
Il est bien possible de passer des propriétés, par l&#8217;option -D, mais là encore, le coté verbeux peut rebuter plus d&#8217;un utilisateur.


On notera que passer des arguments au programme n&#8217;est pas la seule façon de procéder.
On peut aussi utiliser des fichiers properties, exploiter les possibilités de l&#8217;API Preference de Java ou utiliser des variables d&#8217;environnement.




Formats d&#8217;arguments


Options et données

Tout d&#8217;abord, il faut bien distinguer dans la pratique le passage d&#8217;option et le passage de données en argument.
Une donnée serait par exemple le nom d&#8217;un fichier, alors que l&#8217;option serait par exemple -f pour indiquer qu&#8217;il faut prendre en compte un fichier.
On voit ici que l&#8217;option n&#8217;est qu&#8217;un flag transmis à l&#8217;application.


Généralement, la donnée est passée de façon brute alors que l&#8217;option est préfixée, par un tiret ou un double tiret, sous Linux, ou par un slash sous Windows.
Les données et options peuvent être associées dans une liste d&#8217;arguments, en les juxtaposant ou en les séparant par un signe prédéfini, comme =, par exemple.


Dans ce premier exemple, la donnée foo.tar.gz concerne l&#8217;option f uniquement parce qu&#8217;elle lui succède dans la liste des arguments :



~# tar -f foo.tar.gz



Dans ce deuxième exemple, le fait que la donnée foo.tar.gz concerne l&#8217;option file est indiqué par le signe =.
On notera que le choix de ce signe dépend uniquement d&#8217;une convention et qu&#8217;il pourrait être replacé par :, &gt;, ou tout autre signe.



~# tar --file=foo.tar.gz




Propriétés Java

Les propriétés Java sont préfixées par les deux caractère "-D" et utilisent le séparateur "=" entre l&#8217;option et la valeur.
L&#8217;option est souvent constituée d&#8217;un ensemble de mots séparés par des points (".") passés au format -Djtips.doit=true.
Ce type d&#8217;argument est géré automatique lorsqu&#8217;il est passé à la JVM, mais pas quand il est passé au programme.


Avec ce premier format, l&#8217;argument peut être lu dans les system properties.



~# java -Djtips.doit=true info.jtips.commons.CLI




 public static void main(String[] args) {
   System.out.println(System.getProperty("jtips.doit"));
 }



Par contre, avec ce format-ci, l&#8217;argument est passé dans le tableau args et doit être traité manuellement.



~# java info.jtips.commons.CLI -Djtips.doit=true




 public static void main(String[] args) {
   System.out.println(args[0]);
 }




Options Posix

Le format d&#8217;option Posix est court, sur un caractère, avec un tiret ('-') devant chaque option ou devant un ensemble d&#8217;option.
Par exemple, pour la commande tar, chaque lettre derrière le tiret correspond à une option :



~# tar -zxvlf foo.tar.gz




Options GNU

Le format d&#8217;option GNU est plus long, mais plus lisible pour un humain.
Chaque option est précédée par un double tiret et est écrite sous forme d&#8217;un mot ou d&#8217;une suite de motes séparés par des tirets simples.



~# tar --gzip --verbose --extract --check-links --file=foo.tar.gz






Apache Commons CLI


L&#8217;utilitaire Commons CLI a été développer pour faciliter le travail du développeur.
Il se charge de vérifier la liste des arguments, nous aide à présenter un message d&#8217;erreur en cas de problème, puis nous assiste dans la lecture des options et des valeurs associées.


Créer les options

Avant de créer les options, il faut instancier leur conteneur, de type Options.



 Options options = new Options();



Puis chaque option peut être créé directement en appelant la méthode addOption(&#8230;&#8203;), dont il existe trois variantes.



 // Ajoute une option -a, sans argument, avec une description
 options.addOption("a", false, "Option a");

 // Ajoute une option -b, ou --blabla, avec un argument, avec une description
 options.addOption("b", "blabla", true, "Option a");



La troisième variante prend un objet Option en paramètre.
Celui-ci doit être instancié via la méthode Option.builder().
Une partie de sa configuration peut se faire avec le builder, le reste de la configuration se fait ultérieurement sur l&#8217;option.
Cette façon de procéder est plus verbeuse, mais apporte plus de souplesse.



 // Création de l'option -o, ou --out-file, avec un argument et obligatoire
 Option outFileOption = Option.builder()
            .option("o")
            .longOpt("out-file")
            .desc("URL du fichier cible")
            .required()
            .hasArg()
            .build();

 // L'option est ajoutée à la liste
 options.addOption(outFileOption);




Analyser les options et arguments

L&#8217;analyse des options et arguments transmis se fait par un CommandLineParser.
Commons CLI en proposait trois implémentations qui se différenciaient essentiellement par leur traitement des options longues.
Depuis la version 1.3, ça a été simplifié au profit d&#8217;une classe unique DefaultParser.







La méthode parse() renvoie un objet de type CommandLine qui nous permettra de lire toutes les options et tous les arguments.



CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);




Lecture des options et arguments

Le plus facile reste à faire : lire les options et arguments&#8230;&#8203;
Pour cela, on utilise l&#8217;objet de type CommandLine produit par le parser, et on lui demande les options, avec le méthode hasOption(&#8230;&#8203;) ou getOptions(&#8230;&#8203;), puis les valeurs avec la méthode getOptionValue(&#8230;&#8203;) de CommandLine ou getValue() de Option.



 // Option obligatoire, je ne teste donc pas sa présence
 outFile = cmd.getOptionValue('o');

 // Option facultative, je teste sa présence
 if (cmd.hasOption('a')) {
   aValue = cmd.getOptionValue('a');
 }
 // Option facultative, je passe une valeur par défaut
 bValue = cmd.getOptionValue('b', "Valeur par défaut pour b") ;




Erreurs de validation

En cas d&#8217;erreur lors de l&#8217;analyse des arguments, une ParseException se produit et fournit un message d&#8217;erreur qui indique quelles options posent problème.



 System.err.println("Parsing failed : " + e.getMessage());



La ParseException a cependant le défaut de ne proposer qu&#8217;un message en anglais, sans donner de détail sur les options incriminées.
Pour avoir plus de détails, il faut accéder aux sous-classes.









MissingOptionException est levée lorsqu&#8217;il manque une ou plusieurs options obligatoires.
La méthode getMissingOptions() permet d&#8217;avoir la liste des options manquantes.


MissingArgumentException est levée lorsqu&#8217;il manque un argument à une option.
La méthode getOption() permet de connaître l&#8217;option qui nécessite un argument.


UnrecognizedOptionException est levée lorsqu&#8217;une option non prévue est passée.
La méthode getOption() permet de connaître l&#8217;option inconnue.


AlreadySelectedException est levée lorsque plusieurs options d&#8217;un même groupe exclusif sont passées.
La méthode getOption() permet de connaître l&#8217;option qui a déclenché l&#8217;exception et getOptionGroup() permet de connaître le groupe..


AmbiguousOptionException est levée lorsqu&#8217;on utilise un nom d&#8217;option partiel, qui peut correspondre à plusieurs options.
La mémthode getMatchingOptions() permet de connaître les options qui correspondent.





Afficher l&#8217;aide

La classe HelpFormatter permet d&#8217;afficher un texte d&#8217;aide qui liste les options et arguments attendus, ainsi que le texte d&#8217;accompagnement.



 HelpFormatter formatter = new HelpFormatter();
 formatter.printHelp( "CLI", options );



Cette aide est affichée, de façon classique, sur les options --help ou --usage, et, éventuellement, quand l&#8217;utilisateur fait une erreur de saisie.





Conclusion


Cet exemple a été réalisé avec Apache Commons CLI 1.5.


Références :




Apache Commons CLI


Exemple de code




</description>
          <pubDate>2009-05-05T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/ApacheCommons/CLI</link>
          <guid isPermaLink="true">https://www.jtips.info/ApacheCommons/CLI</guid>
        </item>
      
    
      
        <item>
          <title>Portage J2EE vers Jonas 4</title>
          <description>
Portage d&#8217;une application J2EE 1.4 de Weblogic 9.2 vers Jonas 4.10


Cet article reprend les tâches que j&#8217;ai dues mettre en oeuvre pour le portage d&#8217;une application J2EE, dans le cadre d&#8217;un projet.
Il ne recherche en aucun cas l&#8217;exhaustivité.


Architecture de l&#8217;application


Le portage concetne une application Web, à base de JSP et de servlets, avec des EJBs session et MDB.
La couche Web communique avec les EJBs, et les EJBs communiquent entre eux.
De plus, les EJBs accèdent à une base de données relationnelle en JDBC, via une DataSource.
L&#8217;application utilise le framework de traces log4j.


On notera que certaines bizareries ont été constatées dans l&#8217;application, comme l&#8217;accès aux EJB ou à la datasource via leurs noms JNDI, sans untilistion 'ejb-ref ou de resource-ref.
Ceci nous oblige donc à respecter scrupuleusement les noms JNDI utilisés.
Pour tous les EJBs sessions, les interfaces local et remote sont fournies, alors que seul l&#8217;accès local est nécessaire.




Déploiement


L&#8217;application est déployée sous forme d&#8217;un fichier ear, contenant un fichier war et un fichier jar d&#8217;EJBs, ainsi qu&#8217;un fichier jar de librairie externe à l&#8217;ear.


Les fichiers jar de librairies (log4j, driver JDBC,&#8230;&#8203;) sont déposées dans le répertoire lib/ext/ de Jonas.
Le fichier ear est déposé dans le répertoire apps/autoload/ de Jonas, ou dans apps/.
Dans ce dernier cas, il faut déclarer le fichier ear dans la configuration conf/jonas.properties :



jonas.service.ear.descriptors	myapp.ear





Couche Web


Le portage de la partie Web de l&#8217;application n&#8217;a demandé aucun effort !
Le fichier war a pu être déployé sans aucune modification.




Couche métier : EJBs


Pour les EJBs, la tâche principale consiste en la traduction du fichier spécifique weblogic-ejb-jar.xml en fichier jonas-ejb-jar.xml. Le format de ce fichier doit correspondre à une version récente de Jonas, afin de profiter de certaines évolutions indispensables, comme la spécification du nom JNDI pour l&#8217;accès local.



 &lt;jonas-ejb-jar
      xmlns="http://www.objectweb.org/jonas/ns"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.objectweb.org/jonas/ns
                          http://jonas.ow2.org/ns/jonas-ejb-jar_4_8.xsd"&gt;
 &lt;/jonas-ejb-jar&gt;



EJB Session

Pour chaque EJB session, un élément &lt;jonas-session&gt; doit être ajouté.
Cet élément contient le nom JNDI remote et, depuis Jonas 4.8, le nom JNDI local, ainsi que le dimensionnement du pool d&#8217;instances.




Paramétrage Weblogic

 &lt;weblogic-enterprise-bean&gt;
   &lt;ejb-name&gt;MySession&lt;/ejb-name&gt;
   &lt;stateless-session-descriptor&gt;
     &lt;pool&gt;
       &lt;max-beans-in-free-pool&gt;50&lt;/max-beans-in-free-pool&gt;
       &lt;initial-beans-in-free-pool&gt;10&lt;/initial-beans-in-free-pool&gt;
     &lt;/pool&gt;
   &lt;/stateless-session-descriptor&gt;
   &lt;enable-call-by-reference&gt;true&lt;/enable-call-by-reference&gt;
   &lt;jndi-name&gt;MySession&lt;/jndi-name&gt;
   &lt;local-jndi-name&gt;MySessionLocal&lt;/local-jndi-name&gt;
 &lt;/weblogic-enterprise-bean&gt;



Paramétrage Jonas

 &lt;jonas-session&gt;
   &lt;ejb-name&gt;MySession&lt;/ejb-name&gt;
   &lt;jndi-name&gt;MySession&lt;/jndi-name&gt;
   &lt;jndi-local-name&gt;MySessionLocal&lt;/jndi-local-name&gt;
   &lt;min-pool-size&gt;10&lt;/min-pool-size&gt;
   &lt;max-cache-size&gt;50&lt;/max-cache-size&gt;
 &lt;/jonas-session&gt;





Il faut noter que Weblogic propose un nombre plus important de paramètres spécifiques que Jonas, et que certains d&#8217;entre eux ne peuvent pas être traduits. Parmi ceux-là, on peut citer &lt;enable-call-by-reference&gt;.



EJB MDB

Pour chaque EJB MDB, un élément &lt;jonas-message-driven&gt; doit être ajouté.
Cet élément contient le nom JNDI de la queue ou du topic auquel se branche le bean, ainsi que le dimensionnement du pool d&#8217;instances.


Paramétrage Weblogic

 &lt;weblogic-enterprise-bean&gt;
   &lt;ejb-name&gt;MyMDB&lt;/ejb-name&gt;
   &lt;message-driven-descriptor&gt;
     &lt;pool&gt;
       &lt;max-beans-in-free-pool&gt;50&lt;/max-beans-in-free-pool&gt;
       &lt;initial-beans-in-free-pool&gt;10&lt;/initial-beans-in-free-pool&gt;
     &lt;/pool&gt;
     &lt;destination-jndi-name&gt;jms/MyQueue&lt;/destination-jndi-name&gt;
   &lt;/message-driven-descriptor&gt;
   &lt;dispatch-policy&gt;IRWorkManager&lt;/dispatch-policy&gt;
 &lt;/weblogic-enterprise-bean&gt;



Paramétrage Jonas

 &lt;jonas-message-driven&gt;
   &lt;ejb-name&gt;MyMDB&lt;/ejb-name&gt;
   &lt;jonas-message-driven-destination&gt;
     &lt;jndi-name&gt;jms/MyQueue&lt;/jndi-name&gt;
   &lt;/jonas-message-driven-destination&gt;
   &lt;min-pool-size&gt;10&lt;/min-pool-size&gt;
   &lt;max-cache-size&gt;50&lt;/max-cache-size&gt;
 &lt;/jonas-message-driven&gt;



Ici aussi, certains paramètres ne peuvent pas être traduits, comme &lt;dispatch-policy&gt;, qui fait référence à un &lt;work-manager&gt;, dans le même fichier.
Ces paramètres permettent de gérer des pools de threads spécifiques.
A ma connaissance, le seul paramétrage équivalent dans jonas est au niveau global, dans conf/jonas.properties, sous la clé jonas.service.ejb.mdbthreadpoolsize.



 jonas.service.ejb.mdbthreadpoolsize	10



Par ailleurs, nous avons du modifier des éléments dans le fichier ejb-jar.xml. La première concerne la durabilité de souscription du MDB, qui était spécifiée alors que le type de destination est Queue ! Ces données sont incompatibles, mais Weblogic les accepte ; Jonas les refuse et affiche un message d&#8217;erreur.


Paramétrage Weblogic

 &lt;message-driven &gt;
   &lt;ejb-name&gt;MyMDB&lt;/ejb-name&gt;
   &lt;message-driven-destination&gt;
     &lt;destination-type&gt;javax.jms.Queue&lt;/destination-type&gt;
     &lt;subscription-durability&gt;NonDurable&lt;/subscription-durability&gt;
   &lt;/message-driven-destination&gt;
 &lt;/message-driven&gt;



Paramétrage Jonas

 &lt;message-driven &gt;
   &lt;ejb-name&gt;MyMDB&lt;/ejb-name&gt;
   &lt;message-driven-destination&gt;
     &lt;destination-type&gt;javax.jms.Queue&lt;/destination-type&gt;
   &lt;/message-driven-destination&gt;
 &lt;/message-driven&gt;



Les développeurs de l&#8217;application on eu la très bonne idée d&#8217;externaliser, sous forme de &lt;env-entry&gt;, le nom JNDI de la connexion factory pour accéder aux queues.
Un &lt;resource-ref&gt; aurait été plus adapté, mais ne nous plaignons pas&#8230;&#8203;
La valeur sous Weblogic est weblogic/jms/ConnectionFactory, alors que pour Jonas, elle est CF, pour une ConnectionFactory, ou QCF, pour une QueueConnectionFactory.
L&#8217;analyse du code nous a montré que c&#8217;est la deuxième fabrique qui est utilisée.



 &lt;session &gt;
   &lt;ejb-name&gt;MySession&lt;/ejb-name&gt;
   &lt;env-entry&gt;
     &lt;env-entry-name&gt;QUEUE_CONNECTION_FACTORY_NAME&lt;/env-entry-name&gt;
     &lt;env-entry-type&gt;java.lang.String&lt;/env-entry-type&gt;
     &lt;env-entry-value&gt;QCF&lt;/env-entry-value&gt;
   &lt;/env-entry&gt;
 &lt;/session&gt;



Si le nom de la fabrique n&#8217;avait pas été externalisé, il aurait fallu créer une entrée de registre JNDI avec le même nom que sous Weblogic.
Dans certains cas limités, c&#8217;est possible en ajoutant un élément &lt;ConnectionFactory&gt; dans conf/joramAdmin.xml ;
la principale limitation de vient du fait qu&#8217;il est impossible de créer des sous-contextes (ou répertoires).
Il aurait donc fallu créer un composant qui fait un lookup sur "CF" et qui fait un bind sur le nouveau nom.





Autres composants


Datasource

Comme pour les EJBs, l&#8217;utilisation du nom global de la DataSource nous impose de lui conservé son nom d&#8217;origine (jdbc/MyDS).
Nous avons donc créé un fichier MyDB.properties dans le répertoire conf/ de Jonas, puis référencé ce fichier dans jonas.properties.



 jonas.service.dbm.datasources    MyDB



Ce fichier contient le paramétrage de la DataSource ; nous l&#8217;avons créé par copie d&#8217;un fichier existant.



 datasource.name			jdbc/MyDS
 datasource.url			jdbc:mysql://localhost/mydb
 ...




Log4J

La localisation du fichier de configuration de Log4J est spécifiée directement dans le code, sans externalisation !
Heureusement, le chemin est relatif, par rapport au script de lancement de Jonas.
On retrouve donc ici l&#8217;intérêt de ne pas lancer Jonas directement depuis les sous-répertoires de bin, mais de créer ses propres scripts.


Il y aurait beaucoup à dire sur l&#8217;utilisation qui a été faite de Log4J.
On constate une fois de plus que sa puissance ne peut pas être correctement exploitée en production à cause d&#8217;un excès de zèle des développeurs.
Mais je m&#8217;éloigne du sujet&#8230;&#8203;





Portage sous Jonas 5


La transition vers Jonas 5 n&#8217;a pas posé de problème supplémentaire.
Seuls les noms des répertoires de déploiement ont dus être adaptés.




Conclusion


Le portage de l&#8217;application a finalement été facile à réaliser ; nous n&#8217;avons rencontré aucun point de blocage, malgré un respect lointain des préconisation de J2EE par les développeurs :




pas de resource-ref, ni de ejb-ref,


utilisation d&#8217;une QueueConnectionFactory au lieu d&#8217;une ConnectionFactory,


utilisation étrange de Log4J.




</description>
          <pubDate>2009-04-26T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Jonas/PortageWeblogic</link>
          <guid isPermaLink="true">https://www.jtips.info/Jonas/PortageWeblogic</guid>
        </item>
      
    
      
        <item>
          <title>JAX-WS/CXF et Spring</title>
          <description>
Le projet CXF


CXF est un projet Apache qui est issu de la fusion entre XFire et Celtix. Il s&#8217;agit d&#8217;une implémentation JAX-WS qui s&#8217;intègre facilement avec Spring Framework. Il est donc plutôt orienté "Contract Last", mais il permet cependant de développer suivant l&#8217;approche "Contract First".


Il implémente des standards WS et Java :




SOAP, WS-I Basic Profile, WS-Security&#8230;&#8203;


JAX-WS, JAX-WSA, JSR-181, SAAJ




Par ailleurs, il propose plusieurs types de transport (HTTP, JMS&#8230;&#8203;), ainsi qu&#8217;un choix entre plusieurs frameworks de Data Binding (JAXB, Aegis&#8230;&#8203;).


Dans cet article, nous traiterons uniquement de l&#8217;approche "Contract Last", i.e. le fichier WSDL sera généré à partir des classes Java annotées.




Principe de communication client / service









Processus de développement


Partie serveur

Pour écrire la partie serveur du service web, les étapes de développement sont les suivantes :




Fournir une interface + une classe d&#8217;implémentation qui seront annotées avec les annotations de l&#8217;API JAX-WS


Configurer Spring pour déclarer le bean de service web


Configurer la Servlet CXF dans le fichier web.xml




L&#8217;interface Java du service doit être annotée avec @WebService :



@WebService
public interface CatalogueService {

  Produit findProduitById(Long id);
  ...
}



Cette configuration peut être enrichie afin de contrôler plus précisément les éléments du fichier WSDL qui sera généré, notamment à l&#8217;aide des attributs de @WebService et de l&#8217;annotation @WebMethod :



@WebService(name="Catalogue",
            targetNamespace="http://www.librairie.org")
public interface CatalogueService {

  @WebMethod(operationName="getProduit")
  Produit findProduitById(@WebParam(name="id") Long id);
  ...
}



La classe d&#8217;implémentation doit pour sa part être annotée avec @WebService de façon à préciser l&#8217;interface qui définit le service web :



@WebService(endpointInterface="org.librairie.service.CatalogueService")
public class CatalogueServiceImpl implements CatalogueService {
  private ProduitDao produitDao;

  @Override
  public Produit findProduitById(Long id) {
    Produit produit = produitDao.findById(id);
    return produit;
  }
  ...
}



Au niveau de la configuration Spring, il faut :




importer les fichiers d&#8217;application context de CXF


déclarer le bean de service web avec le schéma jaxws fourni par CXF


déclarer les dépendances du bean de service web




Voici donc le fichier d&#8217;application context pour notre service web :



&lt;beans xmlns="http://www.springframework.org/schema/beans"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:jaxws="http://cxf.apache.org/jaxws"
          xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
                              http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.0.xsd
                              http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd"&gt;

  &lt;import resource="classpath:META-INF/cxf/cxf.xml" /&gt;
  &lt;import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" /&gt;
  &lt;import resource="classpath:META-INF/cxf/cxf-servlet.xml" /&gt;

  &lt;bean id="catalogueService"
        class="org.librairie.service.CatalogueServiceImpl"&gt;
    &lt;property name="produitDao" ref="produitDao"/&gt;
  &lt;/bean&gt;

  &lt;jaxws:endpoint id="catalogueServiceEndpoint"
                  implementor="#catalogueService"
                  address="/CatalogueService" /&gt;
&lt;/beans&gt;



Dans l&#8217;exemple ci-dessus, &lt;jaxws:endpoint&gt; permet de créer le bean de web service avec l&#8217;identifiant catalogueServiceEndpoint. L&#8217;attribut implementor est utilisé pour injecter un bean Spring dans le bean de web service. A noter que le nom du bean doit être préfixé avec le caractère #.


On configure enfin le fichier web.xml afin de déclarer la servlet responsable du chargement de l&#8217;application context et du traitement des requêtes HTTP destinées aux endpoints :



&lt;servlet&gt;
  &lt;servlet-name&gt;CXFServlet&lt;/servlet-name&gt;
  &lt;servlet-class&gt;
    org.apache.cxf.transport.servlet.CXFServlet
  &lt;/servlet-class&gt;
  &lt;load-on-startup&gt;1&lt;/load-on-startup&gt;
&lt;/servlet&gt;

&lt;servlet-mapping&gt;
  &lt;servlet-name&gt;CXFServlet&lt;/servlet-name&gt;
  &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
&lt;/servlet-mapping&gt;



Remarque : Si le fichier Spring est nommé cxf.xml et placé dans le classpath, alors la servlet CXF charge automatiquement l&#8217;application context. Sinon, il faut effectuer le chargement de l&#8217;application context via le context loader listener de Spring :



&lt;context-param&gt;
  &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;
  &lt;param-value&gt;/WEB-INF/applicationContext-cxf.xml&lt;/param-value&gt;
&lt;/context-param&gt;

&lt;listener&gt;
  &lt;listener-class&gt;
    org.springframework.web.context.ContextLoaderListener
  &lt;/listener-class&gt;
&lt;/listener&gt;




Partie client

Côté client, deux approches sont possibles :




Si l&#8217;interface Java du service est disponible,  il suffit de créer un Factory Bean JAX-WS au niveau de la configuration Spring


Si on dispose uniquement du fichier WSDL, un outil permet de générer des stubs à partir du fichier WSDL




Quand on dispose de l&#8217;interface Java du service, la configuration Spring du bean client est effectué à l&#8217;aide d&#8217;un bean JaxWsProxyFactoryBean fourni par CXF :



&lt;bean id="catalogue"
      class="org.librairie.service.CatalogueService"
      factory-bean="clientFactory" factory-method="create"/&gt;

&lt;bean id="clientFactory"
      class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean"&gt;
  &lt;property name="serviceClass"
            value="org.librairie.service.CatalogueService"/&gt;
  &lt;property name="address"
            value="http://server/myapp/CatalogueService"/&gt;
&lt;/bean&gt;



Pour invoquer le service web depuis le code Java, il suffit alors d&#8217;instancier le conteneur Spring et de récupérer le bean client :



String[] paths = {"applicationContext-cxf-client.xml"};
ApplicationContext ctx = new ClassPathXmlApplicationContext(paths);

CatalogueService service = (CatalogueService)ctx.getBean("catalogue");
Produit produit = service.findProduitById(3);




</description>
          <pubDate>2009-03-07T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/JAX-WS</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/JAX-WS</guid>
        </item>
      
    
      
        <item>
          <title>Tomcat en production</title>
          <description>
Petit récapitulatif des opérations à réaliser pour la mise en production d&#8217;un serveur Tomcat 5.5 ou 6.0 (mise à jour en cours pour Tomcat 7 et 8).


Installation


Machine virtuelle

Il faut installer un JDK récent. Celui d&#8217;Oracle devrait convenir, ou éventuellement OpenJDK.



Tomcat


Même sous Linux, on installe Tomcat depuis l'archive téléchargée sur leur site.
L'installation par paquet rpm, apt-get ou autre amène parfois des surprises.
(cf. link:http://www.developpez.net/forums/f262/java/developpement-web-java/tomcat/[forum developpez.net])




Log4J

&lt;strike&gt;Je préfère largement Log4J au Java Logging API. Je fais donc le remplacement.&lt;/strike&gt;



JULI n'est pas terrible, mais il s'est largement amélioré.
Par exemple, depuis Tomcat 8, il sait écrire chaque trace sur une seule ligne, et de façon asynchrone. Je le conserve donc, en simplifiant sa link:#Traces[configuration].






Script de démarrage



&lt;strike&gt;Pour commencer, il peut être pratique de renseigner CATALINA_HOME, pour pouvoir l'utiliser dans la suite du script.&lt;/strike&gt;




Tomcat est démarré avec son propre user, et surtout pas _root_.
Comme j'utilise un Apache frontal, je n'ai pas besoin d'écouter sur le port 80.



Profile JVM et Mémoire

La JVM se lance avec les options par défaut, ce qui est rarement la meilleure de solutions.




Ajouter l&#8217;option -server


Ajouter les options -Xms et -Xmx





JAVA_OPTS="$JAVA_OPTS -server -Xms512m -Xmx512m"



Pour faire générer un dump de mémoire en cas de OutOfMemoryError :



JAVA_OPTS="$JAVA_OPTS -XX:-HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$CATALINA_HOME/logs"




Garbage Collector


Il n'y généralement pas d'optimisation a priori du ramasse-miettes.
Son paramétrage se fait après diagnostic d'un problème.




En revanche, l'existence de collections explicites dans le code peut être une source de lenteur, il faut donc les désactiver.




JAVA_OPTS="$JAVA_OPTS -XX:+DisableExplicitGC"




JMX

&lt;strike&gt;En Java 5, on ajoute la propriété permettant de monitorer avec jConsole ou tout autre client JMX, comme MC4J ou jManage.&lt;/strike&gt;



&lt;strike&gt;JAVA_OPTS="$JAVA_OPTS "-Dcom.sun.management.jmxremote&lt;/strike&gt;




Si le serveur n'a pas de couche graphique, on va permettre l'accès à distance.
Cet accès est sécurisé et nécessite une authentification dont les données sont stockées dans des fichiers properties, avec un fichier qui contient les couples user/password et un fichier qui associe les rôles aux users.




JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.rmi.port=1098
                      -Dcom.sun.management.jmxremote.access.file=$CATALINA_HOME/conf/jmx.access
                      -Dcom.sun.management.jmxremote.password.file=$CATALINA_HOME/conf/jmx.password"



&lt;strike&gt;Avec Java 6, l&#8217;accès JMX local est automatique ouvert, sans avoir à spécifier de variable supplémentaire. Cependant, en l&#8217;absence de la variable -Dcom.sun.management.jmxremote, Tomcat n&#8217;enregistrera pas ses propres MBeans ; on ne pourra donc monitorer que les données de la JVM, comme l&#8217;utilisation de la mémoire, mais pas les datasources ou connecteurs.&lt;/strike&gt;


En conclusion, à partir de Java 5, on ajoute systématiquement la variable -Dcom.sun.management.jmxremote.


La page JMX/Remote donne des explications plus détaillées sur cet accès.



AWT - Display


Si Tomcat est lancé sur un serveur sans display, des erreurs peuvent se produire au démarrage.
Cela est du à l'utilisation de composants AWT, qui, bien que pouvant ne pas être graphiques, essaient de se rattacher à un display.



Pour éviter cette tentative de rattachement, il faut ajouter la variable -Djava.awt.headless :



JAVA_OPTS="$JAVA_OPTS -Djava.awt.headless=true"






Applications fournies


Activer TLS

A partir du moment ou je veux utiliser les applications de gestion, comme le Manager, je passe par du TLS.



Sécurité du Manager

Il faut à la fois créer un user dans tomcat-users.xml et forcer l&#8217;utilisation de SSL.


Il vaut mieux stocker la version chiffrée du mot de passe :



&gt; bin/digest.sh -a sha-512 sewatech
sewatech:6f9341ffb9...552c8a0e4




&lt;!-- conf/tomcat-users.xml --&gt;
&lt;role rolename="manager" /&gt;
&lt;user username="alexis" password="6f9341ffb9...552c8a0e4" roles="manager-gui" /&gt;




&lt;!-- conf/server.xml --&gt;
&lt;Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase" digest="sha-512"/&gt;



Et pour forcer le SSL, on ajouter une user-data-constraint aux contraintes de sécurité existantes dans le fichier WEB-INF/web.xml du manager



 &lt;!-- WEB-INF/web.xml --&gt;
 &lt;security-constraint&gt;
   &lt;web-resource-collection&gt;
     &lt;web-resource-name&gt;HTML Manager interface (for humans)&lt;/web-resource-name&gt;
     &lt;url-pattern&gt;/html/*&lt;/url-pattern&gt;
   &lt;/web-resource-collection&gt;
   &lt;auth-constraint&gt;
      &lt;role-name&gt;manager-gui&lt;/role-name&gt;
   &lt;/auth-constraint&gt;

   &lt;user-data-constraint&gt;
      &lt;transport-guarantee&gt;CONFIDENTIAL&lt;/transport-guarantee&gt;
   &lt;/user-data-constraint&gt;

 &lt;/security-constraint&gt;



La même portion doit être ajoutée à chaque security-constraint.



Ménage

Je retire les applications inutiles :




docs


examples


host-manager




Pour ROOT, j&#8217;en vide le contenu pour le remplacer par une simple page index.html de redirection.



&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;meta http-equiv="REFRESH" content="0;URL=/myapp/"&gt;
    &lt;title&gt;Redirecting...&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
  &lt;/body&gt;
&lt;/html&gt;




Applications d&#8217;admin

&lt;strike&gt;Je n&#8217;installe pas l&#8217;application admin, de toute façon elle ne fonctionne plus correctement dans Tomcat 6.&lt;/strike&gt;


J&#8217;ajoute (et sécurise) les applications suivantes :




Jolokia pour accéder aux informations JMX au format JSON


Psi Probe, le successeur de Lambda Probe





Applications métier

J&#8217;installe les applications métier + DataSource (+ Realm).


Je précompile les JSP (avec &lt;strike&gt;lambda probe&lt;/strike&gt; Psi Probe)





Configuration


Ménage

Pour y voir un peu plus clair, je retire tous les commentaires dans le fichier server.xml.



Connector


SSL, mais je l'ai déjà dit.
J'ouvre le connecteur AJP, pour installer Apache en frontal.
Éventuellement, si AJP ne convient pas (par choix des administrateurs réseau), je crée un nouveau connecteur sur le 8082.



Chaque connecteur utilise son propre pool de thread, car je n&#8217;ai pas encore bien vu l&#8217;intérêt du pool partagé (Executor de Tomcat 6).



DataSource


Je crée les DataSources nécessaires dans GlobalNamingResources.
Les jar sont placés dans libsw, déclaré comme autre répertoire de librairies (pour ne pas mélanger avec les jar d'origine).




Jasper

Désactiver le mode development



Sécurité

Utiliser les valves d&#8217;accès, au moins pour manager


Je n&#8217;active pas le SecurityManager, bien que ça pourrait être une bonne chose !



Traces

Log4J + valves de traces à la mode Apache + sewatrace



</description>
          <pubDate>2009-03-06T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Tomcat/Production</link>
          <guid isPermaLink="true">https://www.jtips.info/Tomcat/Production</guid>
        </item>
      
    
      
        <item>
          <title>Log4J dans Tomcat</title>
          <description>
Dans sa configuration par défaut, Tomcat utilise le système de traces du JDK, appelé Java Logging API, ou JUL. Pour pallier aux faiblesses de ce système, Tomcat intègre une sur-couche appelée JULI , qui permet par exemple d&#8217;isoler la configuration d&#8217;une application Web, ou d&#8217;avoir plusieurs fichiers de traces.


On peut s&#8217;étonner de ce choix car Tomcat est développé par la fondation Apache qui héberge un projet logging, dont l&#8217;outil Log4J est la référence en matière de traces ! Heureusement, le concepteurs de Tomcat nous ont laissé la possibilité de remplacer JUL / JULI par Log4J, et de retrouver toutes les possibilités en terme de appenders, de layout et de filtres.


Installer Log4J


La procédure pour installer Log4J dans Tomcat diffère un peu entre Tomcat 5.5 et Tomcat 6.


Tomcat 5.5

Dans un premier temps, il faut télécharger Log4J et Apache Commons Logging.


Ensuite, il faut placer les fichier log4j-1.2.x.jar et commons-logging.jar dans le répertoire commons/lib de Tomcat.


Enfin, le fichier de paramétrage, log4j.properties ou log4j.xml, doit être placé dans le répertoire commons/classes de Tomcat. Si vous préférez un autre emplacement, comme le répertoire conf, par exemple, il faut l&#8217;indiquer à Tomcat avec la propriété -Dlog4j.configuration :



 export JAVA_OPTS="-Dlog4j.configuration=$CATALINA_HOME/conf/log4j.xml"



Cette procédure devrait suffire à remplacer JUL par Log4J.


La procédure peut être affinée en modifiant le script catalina.sh (.bat) :



 # Set Log4J if it is present
 if [ -r "$CATALINA_BASE"/conf/log4j.xml ]; then
   JAVA_OPTS="$JAVA_OPTS "-Dlog4j.configuration="file:$CATALINA_BASE/conf/log4j.xml"
 else
   if [ -r "$CATALINA_BASE"/conf/log4j.properties ]; then
     JAVA_OPTS="$JAVA_OPTS "-Dlog4j.configuration="file:$CATALINA_BASE/conf/log4j.properties"
   fi
 fi




Tomcat 6.0

La procédure est légèrement plus complexe pour la version 6 de Tomcat.


Le début ne change pas beaucoup, avec le téléchargement de Log4J et le placement du jar dans le répertoire lib de Tomcat (la structure des répertoires de librairies a été simplifiée). Il n&#8217;y a pas besoin de Commons Logging.


Ensuite, la documentation nous dit de générer des fichiers avec Ant et le script extra.xml, à partir du code source de Tomcat. J&#8217;ai trouvé plus simple de télécharger les fichiers depuis les archives de Tomcat ! Pour les versions les plus récentes (6.0.13), les fichiers extra sont livrés dans le répertoire bin/extra du site.


J&#8217;ai donc téléchargé tomcat-juli.jar, que j&#8217;ai mis dans bin/ de Tomcat, en remplacement du fichier existant, et tomcat-juli-adapters.jar que j&#8217;ai mis dans lib.


Le fichier de paramètre se positionne dans le répertoire lib, ou à l&#8217;aide de la propriété -Dlog4j.configuration.





Paramétrer Log4J


Paramétrage simple

La documentation de Tomcat propose un fichier de configuration simplifié qui peut faire l&#8217;affaire, à condition de réduire le volume, et donc de remplacer le niveau DEBUG par INFO.



 log4j.rootLogger=INFO, TOMCAT
 log4j.appender.TOMCAT=org.apache.log4j.RollingFileAppender
 log4j.appender.TOMCAT.File=${catalina.home}/logs/tomcat.log
 log4j.appender.TOMCAT.MaxFileSize=10MB
 log4j.appender.TOMCAT.MaxBackupIndex=10
 log4j.appender.TOMCAT.layout=org.apache.log4j.PatternLayout
 log4j.appender.TOMCAT.layout.ConversionPattern=%p %t %c - %m%n




Paramétrage à l&#8217;identique

Pour ceux qui se sont attachés aux traces générées par JUL, avec plusieurs fichiers, il est possible de reproduire le même fonctionnement avec Log4J. Il est assez simple d&#8217;obtenir un résultat approchant avec des DailyRollingFileAppender et un PatternLayout paramétrés comme ceci :



log4j.appender.CATALINA=org.apache.log4j.DailyRollingFileAppender
log4j.appender.CATALINA.File=${catalina.base}/logs/catalina.log
log4j.appender.CATALINA.DatePattern=.yyyy-MM-dd
log4j.appender.CATALINA.Threshold=DEBUG
log4j.appender.CATALINA.layout=org.apache.log4j.PatternLayout
log4j.appender.CATALINA.layout.ConversionPattern=%d{DATE} %C %M%n%5p %m%n



Des petites différences subsistent, dans le nom des fichiers et dans le format des dates. Ce dernier peut encore être affiné, au risque de réduire les performances :



log4j.appender.CATALINA.layout.ConversionPattern=%d{d MMM yyyy hh:mm:ss} %C %M%n%5p %m%n



Pour avoir exactement les mêmes noms de fichiers, il faut utiliser le RollingFileAppender des composants extra. Après téléchargement, on place le fichier apache-log4j-extras-1.0.jar dans le répertoire des librairies de Tomcat (lib/ ou common/lib/), puis on reprend la configuration au début, en XML, car ce appender est incompatible avec la configuration en fichier properties. Il faut aussi utiliser Log4J 1.2.15 (ou +), qui assure une bonne prise en compte des balises XML des composants extra.


En utilisant des appenders definis comme ci-dessous, on obtient un résultat très proche de ce que JUL produit. La seule différence est un espace en début de ligne !



 &lt;appender name="CATALINA" class="org.apache.log4j.rolling.RollingFileAppender"&gt;
   &lt;param name="threshold" value="DEBUG"/&gt;
   &lt;rollingPolicy class="org.apache.log4j.rolling.TimeBasedRollingPolicy"&gt;
     &lt;param name="FileNamePattern" value="${catalina.base}/logs/catalina.%d{yyyy-MM-dd}.log"/&gt;
   &lt;/rollingPolicy&gt;

   &lt;layout class="org.apache.log4j.PatternLayout"&gt;
     &lt;param name="ConversionPattern" value="%d{d MMM yyyy hh:mm:ss} %C %M%n%5p %m%n"/&gt;
   &lt;/layout&gt;
 &lt;/appender&gt;




Paramétrage complet

Le paramétrage suivant reproduit au plus près le paramétrage par défaut de JUL, dans Tomcat 6. J&#8217;insiste sur le fait que cette configuration n&#8217;est pas idéale dans Log4J et que pour un environnement de production, il est préférable de ne pas utiliser les conversions %C et %M. De même, il est faut privilégier les formats de dates prédéfinis, comme %d{DATE}.



&lt;?xml version="1.0" encoding="UTF-8" ?&gt;
&lt;!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"&gt;
&lt;log4j:configuration xmlns:log4j="link:http://jakarta.apache.org/log4j/[http://jakarta.apache.org/log4j/]"&gt;

  &lt;appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender"&gt;
    &lt;param name="threshold" value="DEBUG"/&gt;
    &lt;layout class="org.apache.log4j.PatternLayout"&gt;
      &lt;param name="ConversionPattern" value="%d{d MMM yyyy hh:mm:ss} %C %M%n%5p %m%n"/&gt;
    &lt;/layout&gt;
  &lt;/appender&gt;

  &lt;appender name="CATALINA" class="org.apache.log4j.rolling.RollingFileAppender"&gt;
    &lt;param name="threshold" value="DEBUG"/&gt;
    &lt;rollingPolicy class="org.apache.log4j.rolling.TimeBasedRollingPolicy"&gt;
      &lt;param name="FileNamePattern" value="${catalina.base}/logs/catalina.%d{yyyy-MM-dd}.log"/&gt;
    &lt;/rollingPolicy&gt;

    &lt;layout class="org.apache.log4j.PatternLayout"&gt;
      &lt;param name="ConversionPattern" value="%d{d MMM yyyy hh:mm:ss} %C %M%n%5p %m%n"/&gt;
    &lt;/layout&gt;
  &lt;/appender&gt;

  &lt;appender name="LOCALHOST" class="org.apache.log4j.rolling.RollingFileAppender"&gt;
    &lt;param name="threshold" value="DEBUG"/&gt;
    &lt;rollingPolicy class="org.apache.log4j.rolling.TimeBasedRollingPolicy"&gt;
      &lt;param name="FileNamePattern" value="${catalina.base}/logs/localhost.%d{yyyy-MM-dd}.log"/&gt;
    &lt;/rollingPolicy&gt;

    &lt;layout class="org.apache.log4j.PatternLayout"&gt;
      &lt;param name="ConversionPattern" value="%d{d MMM yyyy hh:mm:ss} %C %M%n%5p %m%n"/&gt;
    &lt;/layout&gt;
  &lt;/appender&gt;

  &lt;appender name="MANAGER" class="org.apache.log4j.rolling.RollingFileAppender"&gt;
    &lt;param name="threshold" value="DEBUG"/&gt;
    &lt;rollingPolicy class="org.apache.log4j.rolling.TimeBasedRollingPolicy"&gt;
      &lt;param name="FileNamePattern" value="${catalina.base}/logs/manager.%d{yyyy-MM-dd}.log"/&gt;
    &lt;/rollingPolicy&gt;

    &lt;layout class="org.apache.log4j.PatternLayout"&gt;
      &lt;param name="ConversionPattern" value="%d{d MMM yyyy hh:mm:ss} %C %M%n%5p %m%n"/&gt;
    &lt;/layout&gt;
  &lt;/appender&gt;

  &lt;appender name="ADMIN" class="org.apache.log4j.rolling.RollingFileAppender"&gt;
    &lt;param name="threshold" value="DEBUG"/&gt;
    &lt;rollingPolicy class="org.apache.log4j.rolling.TimeBasedRollingPolicy"&gt;
      &lt;param name="FileNamePattern" value="${catalina.base}/logs/admin.%d{yyyy-MM-dd}.log"/&gt;
    &lt;/rollingPolicy&gt;

    &lt;layout class="org.apache.log4j.PatternLayout"&gt;
      &lt;param name="ConversionPattern" value="%d{d MMM yyyy hh:mm:ss} %C %M%n%5p %m%n"/&gt;
    &lt;/layout&gt;
  &lt;/appender&gt;

  &lt;appender name="HOST-MANAGER" class="org.apache.log4j.rolling.RollingFileAppender"&gt;
    &lt;param name="threshold" value="DEBUG"/&gt;
    &lt;rollingPolicy class="org.apache.log4j.rolling.TimeBasedRollingPolicy"&gt;
      &lt;param name="FileNamePattern" value="${catalina.base}/logs/host-manager.%d{yyyy-MM-dd}.log"/&gt;
    &lt;/rollingPolicy&gt;

    &lt;layout class="org.apache.log4j.PatternLayout"&gt;
      &lt;param name="ConversionPattern" value="%d{d MMM yyyy hh:mm:ss} %C %M%n%5p %m%n"/&gt;
    &lt;/layout&gt;
  &lt;/appender&gt;

  &lt;logger name="org.apache.catalina.core.ContainerBase.[Catalina].[localhost]" additivity="false"&gt;
    &lt;level value="INFO"/&gt;
    &lt;appender-ref ref="LOCALHOST"/&gt;
  &lt;/logger&gt;

  &lt;logger name="org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager]" additivity="false"&gt;
    &lt;level value="INFO"/&gt;
    &lt;appender-ref ref="MANAGER"/&gt;
  &lt;/logger&gt;

  &lt;logger name="org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/admin]" additivity="false"&gt;
    &lt;level value="INFO"/&gt;
    &lt;appender-ref ref="ADMIN"/&gt;
  &lt;/logger&gt;

  &lt;logger name="org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager]" additivity="false"&gt;
    &lt;level value="INFO"/&gt;
    &lt;appender-ref ref="HOST-MANAGER"/&gt;
  &lt;/logger&gt;

  &lt;root&gt;
    &lt;level value="INFO"/&gt;
    &lt;appender-ref ref="CONSOLE" /&gt;
    &lt;appender-ref ref="CATALINA"/&gt;
  &lt;/root&gt;
&lt;/log4j:configuration&gt;






Log4J et SecurityManager


Si on démarre Tomcat en mode security, Log4J ne peut pas être initialisé à cause d&#8217;un manque de permission.



 ./startup.sh -security



Il faut donc modifier le fichier conf/catalina.policy et l&#8217;adapter aux besoin de Log4J. La configuration diffère légèrement entre une configuration par fichier properties et celle en fichier xml.


Configuration par properties

La modification de permissions porte sur le fichier tomcat-juli.jar. Il faut avant tout que ce fichier puisse lire le fichier de configuration de Log4J et les propriétés de type "log4j.*". Le reste des permissions dépend de cette configuration ; si, de façon classique, les traces sont envoyés dans des fichiers du répertoire logs, il faut prévoir les permissions correspondantes.



grant codeBase "file:${catalina.home}/bin/tomcat-juli.jar" {
  permission java.util.PropertyPermission "", "read";
  permission java.io.FilePermission "${catalina.base}${file.separator}lib${file.separator}log4j.properties", "read";
  permission java.io.FilePermission "${catalina.base}${file.separator}logs", "read, write";
  permission java.io.FilePermission "${catalina.base}${file.separator}logs${file.separator}", "read, write";
};




Configuration par XML

Le changement de nom du fichier ne suffit pas tout à fait pour que cela fonctionne. Si le fichier est bien pris en compte, et les traces bien générées, il reste des alertes au moment de l&#8217;initialisation de Log4J.



grant codeBase "file:${catalina.home}/bin/tomcat-juli.jar" {
  permission java.io.FilePermission "${catalina.base}${file.separator}lib${file.separator}log4j.xml", "read";
  permission java.io.FilePermission "${catalina.base}${file.separator}logs", "read, write";
  permission java.io.FilePermission "${catalina.base}${file.separator}logs${file.separator}*", "read, write";
};



L&#8217;ajout de la permission suivante permet un bon fonctionnement, même si elle est un peu radicale :



  permission java.io.FilePermission "&lt;&lt;ALL FILES&gt;&gt;", "read";




</description>
          <pubDate>2009-03-06T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Tomcat/Log4J</link>
          <guid isPermaLink="true">https://www.jtips.info/Tomcat/Log4J</guid>
        </item>
      
    
      
        <item>
          <title>Spring Security</title>
          <description>
Introduction


Le module Acegi a été rebaptisé en version 2 par Spring Security. Outre ce renommage, cette nouvelle version apporte des schémas XML qui simplifient considérablement la configuration.


Spring Security permet de gérer l&#8217;accès aux ressources d&#8217;une application Java. Ces ressources peuvent être des pages web, mais aussi des objets de services métier. Toute ressource sollicitée par un appelant est rendue accessible si, d&#8217;une part, l&#8217;appelant s&#8217;est identifié, et si d&#8217;autre part, il possède les droits nécessaires (des rôles dans le vocabulaire Spring Security).


Dans cet article, nous traiterons le cas de la sécurisation des pages web.




Principe de Spring Security


Les requêtes HTTP sont interceptées par un filtre de servlet qui délègue à un bean Spring les traitements de vérification d&#8217;accès aux pages web.







Ce bean met en oeuvre une chaîne de filtres. Chacun des filtres est un bean auquel est attribué une tâche précise :




Intégration dans la session HTTP des informations de sécurité contenues dans la requête


Vérification de l&#8217;identité de l&#8217;appelant et affichage d&#8217;une invite de connexion si nécessaire


Vérification des droits d&#8217;accès à la ressource sollicitée
*&#8230;&#8203;









Certains filtres sont obligatoires, d&#8217;autres optionnels. La chaîne de filtres est largement configurable, ce qui permet de personnaliser au mieux la gestion de la sécurité dans les applications web. Spring Security offre ainsi les fonctionnalités suivantes :




Authentification anonyme


Fonction Remember Me


Gestion NTLM


Intégration avec un serveur LDAP ou un serveur CAS


Gestion des certificats X509






Installer Spring Security


Les fichiers jar

La distribution de Spring Security fournit une bonne vingtaine de fichiers jar. Evidemment, tous ne sont pas nécessaires pour démarrer un projet. Pour les exemples qui suivent, les fichiers suivants sont suffisants :




spring-security-core-2.X.X.jar


spring-security-taglibs-2.X.X.jar





Configurer le fichier web.xml

Il suffit de déclarer le filtre de servlet de DelegatingFilterProxy pour activer le module de sécurité :



&lt;filter&gt;
  &lt;filter-name&gt;springSecurityFilterChain&lt;/filter-name&gt;
  &lt;filter-class&gt;
    org.springframework.web.filter.DelegatingFilterProxy
  &lt;/filter-class&gt;
&lt;/filter&gt;

&lt;filter-mapping&gt;
  &lt;filter-name&gt;springSecurityFilterChain&lt;/filter-name&gt;
  &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
&lt;/filter-mapping&gt;




Schéma XML

Le module Spring Security est livré avec un schéma XML spécifique. Celui-ci doit être spécifié dans les documents de configuration Spring :



&lt;beans xmlns="link:http://www.springframework.org/schema/beans[http://www.springframework.org/schema/beans]"
       xmlns:xsi="link:http://www.w3.org/2001/XMLSchema-instance[http://www.w3.org/2001/XMLSchema-instance]"
       xmlns:security="link:http://www.springframework.org/schema/security[http://www.springframework.org/schema/security]"
       xsi:schemaLocation="link:http://www.springframework.org/schema/beans[http://www.springframework.org/schema/beans] link:http://www.springframework.org/schema/beans/spring-beans-2.0.xsd[http://www.springframework.org/schema/beans/spring-beans-2.0.xsd]
                           link:http://www.springframework.org/schema/security[http://www.springframework.org/schema/security] link:http://www.springframework.org/schema/security/spring-security-2.0.4.xsd[http://www.springframework.org/schema/security/spring-security-2.0.4.xsd]"&gt;




Une configuration de base

Voici un exemple de configuration minimale de la chaîne de filtres :



&lt;security:authentication-provider&gt;
  &lt;security:user-service&gt;
    &lt;security:user name="admin" password="admin"
                   authorities="ROLE_ADMIN,ROLE_USER" /&gt;
    &lt;security:user name="guest" password="guest"
                   authorities="ROLE_USER" /&gt;
  &lt;/security:user-service&gt;
&lt;/security:authentication-provider&gt;

&lt;security:http&gt;
  &lt;security:intercept-url pattern="/editerproduit.do" access="ROLE_ADMIN"/&gt;
  &lt;security:intercept-url pattern="/**" access="ROLE_USER"/&gt;

  &lt;security:http-basic/&gt;
&lt;/security:http&gt;



Cet exemple est complet et tout à fait fonctionnel. Comparé à la version 1 d&#8217;Acegi, cela paraît surprenant car le volume de code est bien moindre, mais tout y est. Explications des éléments de cette configuration :




La base de données utilisateur est définie en dur dans le fichier de configuration. Pour chaque utilisateur, on spécifie l&#8217;identifiant, le mot de passe et les rôles qui lui sont attribués avec l&#8217;élément user.


On définit aussi les règles d&#8217;accès aux pages de l&#8217;application à l&#8217;aide de l&#8217;élément intercept-url. Il s&#8217;agit simplement d&#8217;associer un pattern d&#8217;URL à un ou plusieurs rôles (séparation avec une virgule). A noter que les patterns d&#8217;URL sont traités dans l&#8217;ordre de déclaration : si /** était déclaré en premier, les autres patterns ne seraient pas vérifiés.


Le dernier élément http-basic permet d&#8217;afficher une invite de connexion avec une popup pour demander l&#8217;identification de l&#8217;utilisateur :









Dans notre exemple, toutes les pages sont filtrées. La demande d&#8217;identification se produit donc quelle que soit la page sollicitée. Une fois l&#8217;utilisateur identifié, les informations de sécurité le concernant sont stockées en session HTTP dans le security context. L&#8217;identification n&#8217;est par conséquent pas demandée une nouvelle fois lorsque l&#8217;on navigue vers d&#8217;autres pages. Si l&#8217;utilisateur tente de naviguer vers une page non autorisée (par exemple editerproduit.do pour l&#8217;utilisateur guest), Spring Security renvoie une erreur 403 au navigateur web :







Si l&#8217;identification de l&#8217;utilisateur est incorrecte (identifiant ou mot de passe), Spring Security renvoie une erreur 401 au navigateur web :










Authentification par formulaire


Déclarer l&#8217;authentification par formulaire

Pour activer l&#8217;authentification par formulaire, il suffit de remplacer l&#8217;élément &lt;http-basic&gt; par l&#8217;élément &lt;form-login&gt; :



&lt;security:http&gt;
  &lt;security:intercept-url pattern="/editerproduit.do" access="ROLE_ADMIN"/&gt;
  &lt;security:intercept-url pattern="/**" access="ROLE_USER"/&gt;

  &lt;security:form-login/&gt;
&lt;/security:http&gt;



Quand &lt;form-login&gt; n&#8217;est pas paramétré, Spring Security génère une page de formulaire par défaut :








Page de login personnalisée

Evidemment, il est souhaitable de spécifier une page de login personnalisée à l&#8217;aide de l&#8217;attribut login-page :



&lt;security:http&gt;
  &lt;security:intercept-url pattern="/login.jsp" filters="none"/&gt;
  &lt;security:intercept-url pattern="/editerproduit.do" access="ROLE_ADMIN"/&gt;
  &lt;security:intercept-url pattern="/**" access="ROLE_USER"/&gt;

  &lt;security:form-login login-page="/login.jsp"/&gt;
&lt;/security:http&gt;



A noter aussi l&#8217;ajout d&#8217;une règle d&#8217;interception avec filters="none" afin que la page de login ne soit pas filtrée par Spring Security. Sans cette règle, l&#8217;application tombe dans une boucle infinie qui tente d&#8217;afficher la page login.jsp, mais qui nécessiterait aussi d&#8217;être authentifiée (suivant la dernière règle avec le pattern /**).


Par ailleurs, la page de login doit respecter certaines règles :




action &#8658; j_spring_security_check


nom input identifiant &#8658; j_username


nom input mot de passe &#8658; j_password





&lt;form method="post" action="j_spring_security_check"&gt;
  Identifiant  :&lt;input name="j_username" value="" type="text" /&gt;
  Mot de passe :&lt;input name="j_password" type="password" /&gt;
  &lt;input value="Valider" type="submit" /&gt;
&lt;/form&gt;



Nous disposons dorénavant de notre page personnalisée pour l&#8217;authentification :








Page d&#8217;échec d&#8217;authentification

L&#8217;attribut authentication-failure-url permet de spécifier une page indiquant un échec de l&#8217;authentification (par défaut, on boucle sur la page de login). De même que pour la page de login, il faut penser à déclarer une règle d&#8217;interception afin de ne pas filtrer la page d&#8217;erreur.



&lt;security:http&gt;
  &lt;security:intercept-url pattern="/login.jsp" filters="none"/&gt;
  &lt;security:intercept-url pattern="/login-failure.jsp" filters="none"/&gt;
  &lt;security:intercept-url pattern="/editerproduit.do" access="ROLE_ADMIN"/&gt;
  &lt;security:intercept-url pattern="/**" access="ROLE_USER"/&gt;

  &lt;security:form-login login-page="/login.jsp"
                      authentication-failure-url="/login-failure.jsp"/&gt;
&lt;/security:http&gt;



Quand l&#8217;utilisateur saisit un identifiant ou un mot de passe incorrect, la page login-failure.jsp est alors affichée :








URL post login

Quand une URL est sollicitée par l&#8217;utilisateur, la page de login est affichée suivant le rôle nécessaire à l&#8217;accès de la page. Une fois l&#8217;authentification effectuée, Spring Security renvoie la page initialement demandée :







Si la première page demandée par l&#8217;utilisateur est la page de login elle-même, alors Spring Security redirige la requête vers la page du welcome file list après l&#8217;authentification. Il est cependant possible de redéfinir la page post login à l&#8217;aide de l&#8217;attribut default-target-url :



&lt;security:form-login login-page="/login.jsp"
                     default-target-url="/accueil.jsp"/&gt;



Ce qui donne :







Par ailleurs, si on souhaite forcer l&#8217;affichage de la default-target-url quelle que soit l&#8217;URL initialement sollicitée par l&#8217;utilisateur, il suffit de positionner l&#8217;attribut always-use-default-target à true :



&lt;security:form-login login-page="/login.jsp"
                     default-target-url="/accueil.jsp"
                     always-use-default-target="true"/&gt;











Page indiquant un accès non-autorisé


Quand un utilisateur authentifié tente d&#8217;accéder à une ressource non-autorisée, Spring Security renvoie par défaut une erreur HTTP 403. A la place, on peut demander l&#8217;affichage d&#8217;une page d&#8217;erreur personnalisée à l&#8217;aide de l&#8217;attribut access-denied-page :



&lt;security:http access-denied-page="/denied.jsp"&gt;
  &lt;security:intercept-url pattern="/login.jsp" filters="none"/&gt;
  &lt;security:intercept-url pattern="/editerproduit.do" access="ROLE_ADMIN"/&gt;
  &lt;security:intercept-url pattern="/**" access="ROLE_USER"/&gt;

  &lt;security:form-login login-page="/login.jsp"/&gt;
&lt;/security:http&gt;










Gestion du logout


Le logout est piloté par l&#8217;invocation de l&#8217;URL /j_spring_security_logout. Il provoque l&#8217;invalidation de la session HTTP et la redirection vers la page racine de l&#8217;application.


Pour l&#8217;activer, il suffit d&#8217;ajouter l&#8217;élément &lt;logout&gt; dans la configuration Spring :



&lt;security:http&gt;
  &lt;security:intercept-url pattern="/login.jsp" filters="none"/&gt;
  &lt;security:intercept-url pattern="/editerproduit.do" access="ROLE_ADMIN"/&gt;
  &lt;security:intercept-url pattern="/**" access="ROLE_USER"/&gt;

  &lt;security:form-login login-page="/login.jsp"/&gt;
  &lt;security:logout/&gt;
&lt;/security:http&gt;



Une JSP fournit le lien de déconnexion :



&lt;a href="j_spring_security_logout"&gt;Déconnexion&lt;/a&gt;



Il est possible de personnaliser le logout :




Attribut logout-success-url : page affichée après le logout


Attribut logout-url : pour redéfinir j_spring_security_logout


Attribut invalidate-session : true par défaut





&lt;security:logout logout-success-url="/login.jsp"
                 logout-url="/doLogout"
                 invalidate-session="false"/&gt;





Authentification anonyme


L&#8217;authentification anonyme permet de créer un utilisateur sans identification explicite. Cette authentification automatique se produit au premier accès HTTP sur l&#8217;application. Les caractéristiques de cet utilisateur sont les suivantes :




Identifiant : anonymousUser


Rôle : ROLE_ANONYMOUS




On peut ainsi donner l&#8217;accès à certaines ressources à tout utilisateur non authentifié :




Page d&#8217;accueil


Page de login


Images


Fichiers CSS et JavaScript


&#8230;&#8203;




Pour activer ce mode d&#8217;authentification, on utilise l&#8217;élément &lt;anonymous/&gt; :



&lt;security:http&gt;
  &lt;security:intercept-url pattern="/login.jsp"
                          access="ROLE_ANONYMOUS"/&gt;
  &lt;security:intercept-url pattern="//*.js"
                          access="ROLE_ANONYMOUS"/&gt;
  &lt;security:intercept-url pattern="/" access="ROLE_USER"/&gt;
  &lt;security:form-login login-page="/login.jsp"/&gt;

  &lt;security:anonymous /&gt;
&lt;/security:http&gt;



Une fois l&#8217;utilisateur authentifié explicitement, anonymousUser est remplacé dans le security context par ce nouvel utilisateur. Il convient donc d&#8217;affecter le rôle anonyme à tous les utilisateurs pour ne pas filtrer l&#8217;accès aux ressources accessibles au rôle anonyme :



&lt;security:user name="admin" password="admin"
               authorities="ROLE_ADMIN,ROLE_USER,ROLE_ANONYMOUS" /&gt;
&lt;security:user name="guest" password="guest"
               authorities="ROLE_USER,ROLE_ANONYMOUS" /&gt;



Une autre solution consiste à ajouter les rôles nécessaires dans les règles d&#8217;interception des requêtes HTTP :



&lt;security:intercept-url pattern="/login.jsp"
                        access="ROLE_ANONYMOUS,ROLE_ADMIN,ROLE_USER"/&gt;
&lt;security:intercept-url pattern="/*/.js"
                        access="ROLE_ANONYMOUS,ROLE_ADMIN,ROLE_USER"/&gt;



Par ailleurs, on peut redéfinir l&#8217;identifiant et le rôle de l&#8217;utilisateur anonyme :



&lt;security:anonymous username="anonyme"
                    granted-authority="ROLE_ANONYME"/&gt;





Fonction remember-me


La fonction "remember-me" permet à un site de se rappeler de l&#8217;identité d&#8217;un principal entre deux sessions. Pour l&#8217;activer, il suffit d&#8217;ajouter l&#8217;élément &lt;remember-me&gt; dans le fichier de configuration Spring :



&lt;security:http&gt;
  &lt;security:intercept-url pattern="/login.jsp" filters="none"/&gt;
  &lt;security:intercept-url pattern="/**" access="ROLE_USER"/&gt;

  &lt;security:form-login login-page="/login.jsp"/&gt;

  &lt;security:remember-me /&gt;
&lt;/security:http&gt;



A noter que cette fonction nécessite une authentification par formulaire. Le formulaire doit par ailleurs contenir le paramètre _spring_security_remember_me :



&lt;form method="post" action="j_spring_security_check"&gt;
  Identifiant :
  &lt;input name="j_username" value="" type="text" /&gt;
  Mot de passe :
  &lt;input name="j_password" type="password" /&gt;
  Remember me ?
  &lt;input type="checkbox" name="_spring_security_remember_me"&gt;
  &lt;input value="Valider" type="submit" /&gt;
&lt;/form&gt;








Par défaut, le jeton "remember-me" est stocké dans une cookie et contient les informations suivantes :




Identifiant


Mot de passe


Date d&#8217;expiration


Clé privé




Bien que l&#8217;identifiant et le mot de passe soient cryptés dans le cookie, cette solution présente une faille de sécurité car ces informations sont exposées sur le réseau. Une alternative consiste alors à stocker les informations du jeton "remember-me" dans une base de données. Il suffit pour cela de préciser une data source à l&#8217;aide de l&#8217;attribut data-source-ref (cet attribut référence un bean Spring de type DataSource) :



&lt;security:http&gt;
  &lt;security:intercept-url pattern="/login.jsp" filters="none"/&gt;
  &lt;security:intercept-url pattern="/**" access="ROLE_USER"/&gt;

  &lt;security:form-login login-page="/login.jsp"/&gt;

  &lt;security:remember-me data-source-ref="dataSource"/&gt;
&lt;/security:http&gt;



Spring Security stocke alors le jeton dans la table persistent_logins dont voici le script de création :



create table persistent_logins
             (username varchar(64) not null,
              series varchar(64) primary key,
              token varchar(64) not null,
              last_used timestamp not null)





Configuration auto-config


L&#8217;attribut auto-config permet de simplifier la configuration de Spring Security au maximum. Elle inclut par défaut les fonctionnalités suivantes :




Authentication par formulaire généré par Spring


Authentication anonyme activé


Fonctions logout et remember-me activées




Il suffit alors de fournir une règle d&#8217;interception d&#8217;URL pour activer Spring Security :



&lt;security:http auto-config="true"&gt;
  &lt;security:intercept-url pattern="/**" access="ROLE_USER" /&gt;
&lt;/security:http&gt;



Les éléments de configuration peuvent être redéfinis selon les besoins :



&lt;security:http auto-config="true"&gt;
  &lt;security:intercept-url pattern="/login.jsp" filters="none"/&gt;
  &lt;security:intercept-url pattern="/**" access="ROLE_USER" /&gt;
  &lt;security:form-login login-page="/login.jsp"/&gt;
&lt;/security:http&gt;





Authentication provider


Crypter les mots de passe

Les mots de passe peuvent être cryptés à l&#8217;aide de l&#8217;élément password-encoder. Plusieurs algorithmes de cryptage sont disponibles en standards :




plaintext


sha


md5


md4


&#8230;&#8203;





&lt;security:authentication-provider&gt;
  &lt;security:password-encoder hash="sha"/&gt;
  &lt;security:user-service&gt;
    &lt;security:user name="admin"
                   password="d033e22ae348aeb5660fc2140aec35850c4da997"
                   authorities="ROLE_ADMIN,ROLE_USER" /&gt;
    &lt;security:user name="guest"
                   password="35675e68f4b5af7b995d9205ad0fc43842f16450"
                   authorities="ROLE_USER,ROLE_ANONYMOUS" /&gt;
  &lt;/security:user-service&gt;
&lt;/security:authentication-provider&gt;



Afin de renforcer la sécurité, on peut préciser une valeur "salt" à l&#8217;algorithme de cryptage via l&#8217;élément salt-source.


La valeur "salt" peut être unique pour tous les utilisateurs (attribut system-wide) :



&lt;security:password-encoder hash="sha"&gt;
  &lt;security:salt-source system-wide="ma valeur"/&gt;
&lt;/security:password-encoder&gt;



La valeur "salt" peut aussi être générée pour chaque utilisateur (attribut user-property) à partir d&#8217;une propriété de l&#8217;objet UserDetails (cette stratégie renforce encore plus la sécurité) :



&lt;security:password-encoder hash="sha"&gt;
  &lt;security:salt-source user-property="username"/&gt;
&lt;/security:password-encoder&gt;



Note : UserDetails est une interface qui définit les informations d&#8217;identification pour chaque utilisateur :







Enfin, Spring Security fournit des classes qui permettent de crypter les mots de passe. Voici un exemple pour l&#8217;algorithme sha :



ShaPasswordEncoder encoder = new ShaPasswordEncoder();
String cryptedPassword = encoder.encodePassword(password, saltValue));




Provider JDBC

Les informations d&#8217;authentification et d&#8217;habilitation peuvent être stockées en base de données :



&lt;security:authentication-provider&gt;
  &lt;security:jdbc-user-service data-source-ref="dataSource" /&gt;
&lt;/security:authentication-provider&gt;



L&#8217;attribut data-source-ref permettant de référencer un bean Spring de type DataSource.


Quand on déclare un service de type jdbc-user-service, Spring utilise une classe DAO qui respecte le schéma SQL suivant :



CREATE TABLE users (
  username VARCHAR(50) NOT NULL PRIMARY KEY,
  password VARCHAR(50) NOT NULL,
  enabled BIT NOT NULL
);

CREATE TABLE authorities (
  username VARCHAR(50) NOT NULL,
  authority VARCHAR(50) NOT NULL
);

ALTER TABLE authorities ADD CONSTRAINT fk_authorities_users foreign key (username) REFERENCES users(username);



Il est possible de personnaliser les requêtes SQL de ce service JDBC si les noms des tables et colonnes de la base de données "utilisateurs" sont différents du schéma ci-dessus :



&lt;security:authentication-provider&gt;
  &lt;security:jdbc-user-service data-source-ref="dataSource"
                              users-by-username-query="SELECT identifiant,motdepasse,actif FROM utilisateurs WHERE identifiant = ?"
                              authorities-by-username-query="SELECT identifiant,role FROM roles WHERE identifiant = ?"/&gt;
&lt;/security:authentication-provider&gt;



Si le schéma de la base de données "utilisateurs" ne permet pas d&#8217;appliquer ces requêtes, il est alors nécessaire de créer un service spécifique par implémentation de l&#8217;interface UserDetailsService.





Utiliser la taglib de Spring Security


L&#8217;objectif de cette taglib est de permettre :




d&#8217;afficher des informations concernant l&#8217;utilisateur connecté


d&#8217;afficher / cacher des fragments de JSP en fonction des rôles de l&#8217;utilisateur connecté




Déclaration de la taglib dans une JSP :



&lt;%@ taglib prefix="sec" uri="link:http://www.springframework.org/security/tags[http://www.springframework.org/security/tags]" %&gt;



Le tag "authentication"

Il permet d&#8217;afficher une propriété de l&#8217;objet Authentication (interface de Spring Security) :



Utilisateur : &lt;sec:authentication property="principal.username"/&gt;



Ainsi que le montre l&#8217;exemple ci-dessus, l&#8217;attribut property peut référencer une propriété composée. En l&#8217;occurence, on affiche l&#8217;identifiant de l&#8217;utilisateur :








Le tag "authorize"

Il permet d&#8217;afficher / cacher des fragments de JSP en fonction des rôles de l&#8217;utilisateur connecté.


Attributs du tag :




ifAllGranted : teste si l&#8217;utilisateur a tous les rôles indiqués


ifAnyGranted : teste si l&#8217;utilisateur a un des rôles indiqués


ifNotGranted : teste si l&#8217;utilisateur n&#8217;a aucun des rôles indiqués




Dans l&#8217;exemple ci-dessous, on affiche l&#8217;identifiant de l&#8217;utilisateur s&#8217;il possède le rôle ROLE_USER :



&lt;sec:authorize ifAllGranted="ROLE_USER"&gt;
  Utilisateur : &lt;sec:authentication property="principal.username"/&gt;
&lt;/sec:authorize&gt;




</description>
          <pubDate>2009-02-28T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/Security</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/Security</guid>
        </item>
      
    
      
        <item>
          <title>XML/JAX</title>
          <description>
XML est un format de communication et de stockage universel, utilisable sur tous les systèmes d&#8217;exploitation et manipulable avec tous les langages.
De ce fait, Java se doit d&#8217;apporter un bon support d&#8217;XML.


Il faut avouer que, pendant plusieurs années, le support d&#8217;XML par Java était avant tout le fait de projets indépendants, comme Apache Xerces ou Xalan, et que la mise en œuvre des services Web s&#8217;apparentait à un parcours du combattant.
En tout cas, les aspects pratiques étaient souvent exclus du langage Java, ce qui le rendait moins séduisant, pour XML et les Web Services, que son concurrent .NET.


Avec les APIs JAX (Java API for XML), Sun a repris la main sur ces aspects et réunifie les techniques de manipulation XML avec Java.


Principales APIs JAX


JAXP

JAXP est l&#8217;acronyme pour Java API for XML Processing.
JAXP regroupe les techniques de validation et de parcours (parsing) de documents XML :




DOM : Document Object Model


SAX : Simple API for XML


StAX : Streaming API for XML, qui s&#8217;appuie sur des principes similaires à SAX




Les parties DOM et SAX de JAXP sont inclus dans le JDK depuis la version 1.4.
StAX n&#8217;y est inclus que depuis la version 6.



JAXB

JAXB signifie Java Architecture for XML Binding.
Il permet de mettre en place des transformations en objets Java et documents XML en paramétrant les correspondances à l&#8217;aide d&#8217;annotations.


Les classes et interfaces de cette API sont dans le package javax.xml.bind.



JAX-WS

JAX-WS est la Java API for XML Web Services.
Il remplace JAX-RPC pour la mise en oeuvre de services Web.


La principale avancée de JAX-WS est la possibilité d&#8217;utiliser les annotations pour la déclaration et la configuration des services, ce qui permet à Java de rattraper un retard de plusieurs années par rapport à son concurrent .NET.


Les classes et interfaces de cette API sont dans le package javax.xml.ws.


Java Web Services Metadata (JSR 181) est dans le package javax.jws.



JAX-RS

JAX pour services REST.





Autres APIs XML


SAAJ (SOAP with Attachments API for Java) est dans le package javax.xml.soap.


WSIT (Web Services Interoperability Technologies) est une sur-couche à JAX-WS.


XWS-Security = XML and Web Services Security




Anciennes API




JAX-RPC = Java API for XML-based RPC, est remplacé par JAX-WS


JAXM = Java API for XML Messaging


JAXR = Java API for XML Registries




</description>
          <pubDate>2009-02-20T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/XML/JAX</link>
          <guid isPermaLink="true">https://www.jtips.info/XML/JAX</guid>
        </item>
      
    
      
        <item>
          <title>Java/DetectJre</title>
          <description>
Qui n&#8217;a jamais été confronté au problème de mise à jour des scripts ?
On développe ou adapte des script Windows ou shell pour lancer nos programmes java, que ce soit pour des applications standard ou des serveurs d&#8217;applications, mais la portabilité de ces scripts se trouve limité par la localisation de la JRE ou du JDK.


Les techniques présentées ci-dessous concernent les JDK et JRE de Sun.


Techniques générales


Java dans le path

Les solutions les plus simples auxquelles on pense immédiatement, consistent à lancer l&#8217;exécutable java ou javac, en lui demandant la version.
Ceci fonctionne très bien si l&#8217;exécutable en question a été mis dans le path.



java -version



Affiche



Java(TM) SE Runtime Environment (build 1.6.0_10-b33)
Java HotSpot(TM) Client VM (build 11.0-b15, mixed mode, sharing)



On constate que c&#8217;est un JRE, version 6 de Sun (cf. HotSpot).



javac -version



Affiche



Eclipse Java Compiler v_774_R33x, 3.3.1, Copyright IBM Corp 2000, 2007. All rights reserved.



On constate que le compilateur par défaut est celui d&#8217;Eclipse.


Pour retrouver le répertoire d&#8217;installation, sous Linux, il faut remonter les liens symboliques de façon récursive.



#!/bin/sh
javac_path=which javac
while [ -L $javac_path ]
do
  javac_ln=ls -l $javac_path
  javac_path=echo $javac_ln | sed -e 's/.* -&gt; //'
done
echo $javac_path




Variable JAVA_HOME

Cette variable est demandée par beaucoup de programmes.
Les scripts qui sont présentés ici servent souvent à renseigner une telle variable.
Cependant, si elle est renseignée au niveau du système, celà nous facilite le travail.
A condition toutefois que la version soit la bonne.


sous Linux :



$JAVA_HOME/bin/java -version   # pour un JRE
$JAVA_HOME/bin/javac -version  # pour un JDK



sous Windows :



%JAVA_HOME%\bin\java -version   # pour un JRE
%JAVA_HOME%\bin\javac -version  # pour un JDK






Techniques Windows


Sous Windows, les JRE et JDK de Sun s&#8217;inscrivent dans la base de registre au moment de l&#8217;installation.
Il suffit donc de savoir chercher au bon endroit pour trouver l&#8217;information nécessaire.


Script

Le script ci-dessous recherche un JDK v6 et positionne la variable JAVA_HOME dessus.



set exp1=reg query "HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Development Kit\1.6" /v JavaHome
set exp2=findstr /I /L /C:"REG_SZ"
for /f "tokens=1,2,3" %%a in ('%exp1%^|%exp2%') do set JAVA_HOME=%%c
rem echo %JAVA_HOME%




Table de registre

L&#8217;organisation du registre est la suivante :




Les JDK sont enregistrés dans le répertoire HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Development Kit, avec 1 sous-répertoire par version majeure et 1 sous-répertoire par numéro d&#8217;update.
La clé CurrentVersion à la racine de ce répertoire indique la version par défaut.


Les JRE sont enregistrés dans le répertoire HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment, avec la même organisation que pour les JDK : sous-répertoires et clé CurrentVersion.




Par exemple, sur mon poste, j&#8217;ai 2 JDK et 1 JRE, ce qui donne ceci


HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\




Java Development Kit



CurrentVersion = 1.6





1.5



JavaHome = C:\DevJava\jdk1.5.0_16





1.5.0_16



JavaHome = C:\DevJava\jdk1.5.0_16





1.6



JavaHome = C:\DevJava\jdk1.6.0_11





1.6.0_11



JavaHome = C:\DevJava\jdk1.6.0_11





Java Runtime Environment



CurrentVersion = 1.6





1.6



JavaHome = C:\Program Files\Java\jre6





1.6.0_11



JavaHome = C:\Program Files\Java\jre6







On notera que c&#8217;est encore l&#8217;ancienne numérotation des versions que est utilisée : 1.6 au lieu de 6.


On peut encore trouver quelques autres informations, concernant Web Start et le Java Plug-in.





Techniques Linux


Pour détecter l&#8217;installation d&#8217;un JDK ou d&#8217;une JRE sous Linux, on peut interroger le système d&#8217;installation par dépôts, avec les utilitaires rpm ou apt-get, ou utiliser des commandes spécifiques, donnant la liste des alternatives connues du système pour Java.


Alternatives Java

Sous Ubuntu / Debian, la commande update-java-alternatives donne une liste des JRE et JDK installés dans /usr/lib/jvm/.
Pour limiter la recherche aux JRE, on ajoute l&#8217;argument --jre.



update-java-alternatives -l
update-java-alternatives --jre -l



Cette commande fait partie du paquet java-common, qu&#8217;il faut avoir installé au préalable.


En associant cette commande avec grep et awk, on arrive à extraire le chemin d&#8217;installation d&#8217;une JRE ou d&#8217;un JDK :



export JAVA_HOME=update-java-alternatives -l | grep java-6-openjdk | awk '{ print $3 }'



&#8658; /usr/lib/jvm/java-6-openjdk


La même commande permet de forcer la version de java par défaut.



sudo update-java-alternatives -s java-6-openjdk



Sous RedHat, l&#8217;équivalent est la commande alternatives.



alternatives --display java




Paquets Ubuntu / Debian

Installation de Java

Ces systèmes utilisent apt-get. Pour poser les bonnes questions à l&#8217;utilitaire, il faut connaître comment est construit le nom des paquets. Sur Ubuntu, les JDK ont des noms du type sun-java5-jdk, et les JRE, sun-java5-jre.


L&#8217;installation d&#8217;un JDK 5 se fait par la commande suivante :



 sudo apt-get install sun-java5-jdk



L&#8217;installation d&#8217;une JRE 5 se fait par la commande suivante :



 sudo apt-get install sun-java5-jre



Cette règle est mise à mal si on utilise d&#8217;autres JDK ou JRE, comme ceux de Oracle/BEA ou d&#8217;IBM, voire OpenJDK. Les scripts devront être adaptés.



Détection de Java

Dans ces conditions, les commandes suivantes détectent les JRE qui sont installées :



dpkg -l | grep '^ii' | awk '{print $2}' | grep '^sun-java[1-9]-jre'
dpkg -l | grep '^ii' | awk '{print $2}' | grep '^sun-java[1-9]-bin'



Alors que celle-ci détecte les JDK :



dpkg -l | grep '^ii' | awk '{print $2}' | grep '^sun-java[1-9]-jdk'




Recherche des répertoires d&#8217;installation

Un script peut alors utiliser ces informations pour rechercher les répertoires d&#8217;installation des JDK ou JRE, en limitant aux versions 5 et 6.



#!/bin/sh

# 1° étape : rechercher les paquets installés
for jvm in dpkg -l | grep '^ii' | awk '{print $2}' | grep '^sun-java[5-6]-jdk' # remplacer jdk par bin pour chercher les jre
do
   # 2° étape : rechercher où est installé l'exécutable javac
   javac_allpath=sudo dpkg -L $jvm | grep 'bin/javac$' # remplacer javac par java pour chercher les jre
   # 3° étape : parcourir les exécutables trouvés
   for javac_path in $javac_allpath
   do
      # On élimine les liens symboliques
      if [ ! -L $javac_path ]
      then
         javabin_path=dirname $javac_path
         echo dirname $javabin_path
      fi
   done
done





</description>
          <pubDate>2009-02-19T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/DetectJre</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/DetectJre</guid>
        </item>
      
    
      
        <item>
          <title>Map avec JAX-B</title>
          <description>
JAXB - Marshalling d&#8217;une Map


Tout d&#8217;abord, je dois préciser que le titre n&#8217;est pas tout à fait exact : je ne vais pas mapper une Map, mais une HashMap. La technique présentée ici peut s&#8217;appliquer à n&#8217;importe quel type de Map, mais je n&#8217;ai pas réussi à la faire fonctionner en déclarant mon champ avec l&#8217;interface Map.


Présentation des classes


Les lecteurs qui auront déjà suivi une de mes formations reconnaîtront mon sujet de prédilection&#8230;&#8203; Je vais donc utiliser une classe Cours, qui est un JavaBean avec trois propriétés (champ + get + set).



 package fr.sewatech.formation.universite.xml.data;

 public class Cours {
   private String code;
   private String nom;
   private int duree;

   public Cours() {
   }
   public Cours(String code, String nom, int duree) {
     this.code = code;
     this.nom = nom;
     this.duree = duree;
   }
   // Méthodes get + set, non présentées ici pour gagner de la place
   @Override
   public String toString() {
     return code + " - " + nom + (duree == 0 ? "" : " - " + duree);
   }
 }



Je vais ensuite utiliser une classe Etudiant qui va avoir un champ de type HashMap&lt;String, Cours&gt;.



 package fr.sewatech.formation.universite.xml.data;

 import java.util.HashMap;

 public class Etudiant {

   private HashMap&lt;String, Cours&gt; allCours;

   public Etudiant() {
   }

   public Etudiant(int capacite) {
     allCours = new HashMap&lt;String, Cours&gt;();
   }

   public String toString() {
     StringBuilder resultat = new StringBuilder("Liste des cours de l'étudiant :");
     for (Object unCours : allCours.values()) {
       resultat.append("\n\t").append(unCours);
     }
     return resultat.toString();
   }
 }



On remarquera que les deux classes ont un constructeur vide, ce qui est imposé par JAX-B, que tous les champs de Cours ont des méthodes get et set, mais pas le champ allCours de la classe Etudiant.




Marshalling simple


Commençons tout d&#8217;abord à faire le mapping de la classe Cours. Ce sera très rapide, puisque cette classe respecte la convention JavaBean ; on peut donc l&#8217;utiliser pour le marshall et le unmarshall sans aucun ajout.



 package fr.sewatech.formation.universite.xml;

 import java.io.File;
 import javax.xml.bind.JAXB;
 import fr.sewatech.formation.universite.xml.data.Cours;

 public class CoursXML {

   public void save(Cours cours) {
     JAXB.marshal(cours, getXmlFile(cours.getCode()));
   }

   public Cours load(String code) {
     return JAXB.unmarshal(getXmlFile(code), Cours.class);
   }

   private File getXmlFile(String code) {
     File directory = new File("xml");
     if (! directory.exists()) {
       directory.mkdir();
     }
     return new File(String.format("xml/cours-%s.xml", code));
   }
 }



Le marshalling donnera un fichier de ce type :



 &lt;?xml version="1.0" encoding="UTF-8" standalone="yes"?&gt;
 &lt;cours&gt;
   AA
   &lt;duree&gt;1&lt;/duree&gt;
   &lt;nom&gt;Aaaa&lt;/nom&gt;
 &lt;/cours&gt;



Les annotations permettent d&#8217;affiner le fichier généré, en terme de noms et de structure.




Marshalling simple de Map


Si on essaie de faire la même opération avec la classe Etudiant, l&#8217;élément racine etudiant généré est vide, car il n&#8217;y a ni getter, ni setter. La solution la plus rapide pour faire générer du contenu est de changer le XmlAccessorType, pour que le marshalling s&#8217;appuie sur les champs au lieu des propriétés. Dans ce cas, il n&#8217;y a plus besoin de get et set.



 @XmlRootElement
 @XmlAccessorType(XmlAccessType.FIELD)
 public class Etudiant {
   private HashMap&lt;String, Cours&gt; allCours;
   //...
 }



Cette façon de faire fonctionne, et fonctionnerait même si le champ était de type Map au lieu de HasMap, mais sans possibilité d&#8217;affiner le résultat.


Si j&#8217;ajoute l&#8217;annotation @XMLElement sur un champ de type Map, une exception est générée (IllegalAnnotationsException), et si j&#8217;ajoute l&#8217;annotation @XMLElement sur un champ de type HashMap, l&#8217;élément généré est vide.



 &lt;?xml version="1.0" encoding="UTF-8" standalone="yes"?&gt;
 &lt;etudiant&gt;
   &lt;allCours/&gt;
 &lt;/etudiant&gt;





Marshalling complexe de Map


Pour résoudre le problème j&#8217;ai utilisé la technique des adaptateurs, avec la balise @XmlJavaTypeAdapter. Le principe est de confier à un adaptateur la transformation entre un objet que JAX-B n&#8217;est pas capable de gérer et un objet gérable. Pour une map, on peut envisager une transformation vers une liste de Entry. Là encore, une classe comme AbstractMap.SimpleEntry ne peut pas être utilisée car il lui manque le constructeur vide.


J&#8217;ai donc développer une classe SwMap qui contient une liste de SwMapEntry. Cette classe est conçue pour être compatible avec JAX-B, et réutilisable dans d&#8217;autres contextes grâce aux generics. SwMap implémente Iterable pour des raisons pratiques.



 @XmlAccessorType(XmlAccessType.FIELD)
 @XmlRootElement
 public class SwMap&lt;K, V&gt; implements Iterable&lt;SwMapEntry&lt;K, V&gt;&gt; {
   @XmlElement(name = "entry", required = true)
   private final ArrayList&lt;SwMapEntry&lt;K, V&gt;&gt; list = new ArrayList&lt;SwMapEntry&lt;K, V&gt;&gt;();

   public ArrayList&lt;SwMapEntry&lt;K, V&gt;&gt; getList() {
     return this.list;
   }
   public void put(K key, V value) {
     list.add(new SwMapEntry&lt;K, V&gt;(key, value));
   }
   public int size() {
     return list.size();
   }
   @Override
   public Iterator&lt;SwMapEntry&lt;K, V&gt;&gt; iterator() {
     return list.iterator();
   }
 }




 @XmlAccessorType(XmlAccessType.FIELD)
 @XmlRootElement
 public class SwMapEntry&lt;K, V&gt; {
   @XmlElement(name="key")
   private K key;
   @XmlElement
   private V value;

   public SwMapEntry() {
   }
   public SwMapEntry(K key, V value) {
       this.key = key;
       this.value = value;
   }

   public K getKey() {
       return key;
   }
   public V getValue() {
       return value;
   }
 }



Enfin, j&#8217;ai pu développer la classe d&#8217;adaptation entre Map et SwMap, là aussi avec les generics.



 public class SwMapAdapter&lt;K, V&gt; extends XmlAdapter&lt;SwMap&lt;K, V&gt;, Map&lt;K, V&gt;&gt; {

   @Override
   public SwMap&lt;K, V&gt; marshal(Map&lt;K, V&gt; value) throws Exception {
     SwMap&lt;K, V&gt; set = new SwMap&lt;K, V&gt;();
     for (Entry&lt;K, V&gt; entry : value.entrySet()) {
       set.put(entry.getKey(), entry.getValue());
     }
     return set;
   }

   @Override
   public Map&lt;K, V&gt; unmarshal(SwMap&lt;K, V&gt; value) throws Exception {
     HashMap&lt;K, V&gt; map = new HashMap&lt;K, V&gt;(value.size());
     for (SwMapEntry&lt;K, V&gt; entry : value) {
       map.put(entry.getKey(), entry.getValue());
     }
     return map;
   }
 }



Grâce à ces classes totalement génériques, je peux paramétrer le marshalling de mon champs de type HashMap.



 @XmlRootElement(name="student")
 @XmlAccessorType(XmlAccessType.FIELD)
 public class Etudiant {

   @XmlElement(name="courses")
   @XmlJavaTypeAdapter(SwMapAdapter.class)
   private HashMap&lt;String, Cours&gt; allCours;

   //...
 }





Conclusion


En attendant que JAX-B supporte correctement les classes qui implémentent Map, cette technique s&#8217;adaptera aux différents types de Map.


Peut-être existe-t-il une technique s&#8217;appuyant uniquement sur les annotations et peut-être existe-t-il une façon de faire avec l&#8217;interface Map, mais je n&#8217;ai pas trouvé&#8230;&#8203; Dans ce cas, le première chose à améliorer serait la documentation de JAX-B !


Les exemples ci-dessus ont été développés avec JAX-B 2.1, intégré à JavaSE 6.


</description>
          <pubDate>2009-01-23T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/XML/JAXB-Map</link>
          <guid isPermaLink="true">https://www.jtips.info/XML/JAXB-Map</guid>
        </item>
      
    
      
        <item>
          <title>Subversion/Client</title>
          <description>
Clients Subversion


Windows

TortoiseSVN



Linux

Je n&#8217;ai pas encore trouvé de client aussi pratique que pour Windows. Je pratique donc régulièrement la ligne de commande, mais attention, cela peut être long.





Astuces


Supprimer .svn

Il peut être utile de supprimer tous les répertoires .svn d&#8217;une arborescence. Cela peut se faire en ligne de commande.


Sous Windows, en se plaçant à la racine de la copie de travail :



 for /r . %f in (.svn) do rd /s /q "%f"



Sous Linux (pas encore testé) :



 find -name .svn -print0 | xargs -0 rm -rf




</description>
          <pubDate>2009-01-23T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Subversion/Client</link>
          <guid isPermaLink="true">https://www.jtips.info/Subversion/Client</guid>
        </item>
      
    
      
        <item>
          <title>Compresser un répertoire en fichier zip</title>
          <description>
Comment compresser un répertoire dans un fichier zip ?


Lire le contenu d&#8217;un répertoire


Un répertoire, comme un fichier, est représenté par une instance de java.io.File.
On peut parcourir l&#8217;ensemble de ses fichiers avec la méthode listFiles(), en excluant les répertoires.



File directory = new File(...);
File[] allFiles = directory.listFiles(File::isFile);
for (File file : allFiles) {
  //...
}



Pour lire le contenu d&#8217;un fichier, on utilise un FileInputStream, puis on lit chaque byte par byte ou, pour optimiser, par paquets de bytes.



FileInputStream inputStream = new FileInputStream(file);
int data;
while ((data = inputStream.read()) &gt;= 0) {
  //...
}





Ecrire un fichier Zip


Pour écrire un fichier Zip, il faut assembler un FileOutputStream à un ZipOutputStream.



File zipFile = new File(directory.getName() + ".zip");
FileOutputStream fOut = new FileOutputStream(zipFile);
ZipOutputStream zOut = new ZipOutputStream(fOut);



Pour chaque fichier, on doit créer une entrée du fichier zip, avant d&#8217;écrire des bytes, puis de fermer l&#8217;entrée.



// Déclaration de la première entrée de l'archive
zOut.putNextEntry(new ZipEntry(file.getName()));
int data;
while ((data = inputStream.read()) &amp;gt;= 0) {
  zOut.write(data);
}
zOut.closeEntry();





Exemple complet


Si on assemble tout ces éléments, on peut faire fonctionner cet exemple, pour un répertoire à 1 seul niveau de profondeur.



package fr.sewatech.formation.io;

import java.io.*;
import java.util.zip.*;

public class ZipDirExample {
  public static void main(String[] args) throws IOException {
    System.out.println(writeZip(new File("nio")));
  }

  public static File writeZip(File directory) throws IOException {
    // Si ce n'est pas un répertoire...
    File[] allFiles = directory.listFiles(File::isFile);
    if (!directory.isDirectory() || allFiles == null) {
      return null;
    }

    File zipFile = new File(directory.getName() + ".zip");
    try (ZipOutputStream zOut
            = new ZipOutputStream(new FileOutputStream(zipFile))) {
      for (File file : allFiles) {
        // Flux de lecture du fichier
        FileInputStream inputStream = new FileInputStream(file);

        // Déclaration de la première entrée de l'archive
        zOut.putNextEntry(new ZipEntry(file.getName()));
        int data;
        while ((data = inputStream.read()) &gt;= 0) {
          zOut.write(data);
        }
        zOut.closeEntry();
      }
      return zipFile;
    }
  }
}





Conclusion


Avec un peu de récursivité, on peut traiter le cas d&#8217;un répertoire contenant d&#8217;autres répertoires&#8230;&#8203;


</description>
          <pubDate>2009-01-23T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/DirToZip</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/DirToZip</guid>
        </item>
      
    
      
        <item>
          <title>Lazy loading avec Hibernate</title>
          <description>
Objectif du lazy loading


Pour comprendre pourquoi Hibernate utilise le lazy loading, il faut commencer par étudier le mécanisme du chargement immédiat. A cette fin, prenons l&#8217;exemple du diagramme de classe ci-dessous :





Remarquez que les associations sont toutes bi-directionnelles (approche préconisée par Hibernate, notamment afin de simplifier le requêtage HQL).


Quand on parle de chargement immédiat, il faut raisonner au niveau d&#8217;une association, pour un sens de navigation.
Dans notre exemple, nous supposons donc que toutes les associations sont paramétrées en mode de chargement immédiat (lazy=false) :



&lt;class name="Produit" table="PRODUIT"&gt;
  ...
  &lt;many-to-one name="categorie"
              column="CATEGORIE_ID"
              class="Categorie"
              lazy="false"/&gt;
  &lt;set name="auteurs" table="PRODUIT_AUTEUR" lazy="false"&gt;
    &lt;key column="PRODUIT_ID"/&gt;
    &lt;many-to-many class="Auteur" column="AUTEUR_ID"/&gt;
  &lt;/set&gt;
&lt;/class&gt;

&lt;class name="Categorie" table="CATEGORIE"&gt;
  ...
  &lt;set name="produits" lazy="false"&gt;
    &lt;key column="CATEGORIE_ID"/&gt;
    &lt;one-to-many class="Produit"/&gt;
  &lt;/set&gt;
&lt;/class&gt;

&lt;class name="Auteur" table="AUTEUR"&gt;
  ...
  &lt;set name="produits" table="PRODUIT_AUTEUR" lazy="false"&gt;
    &lt;key column="AUTEUR_ID"/&gt;
    &lt;many-to-many class="Produit" column="PRODUIT_ID"/&gt;
  &lt;/set&gt;
&lt;/class&gt;



Regardons maintenant ce qui se passe lorsqu&#8217;on effectue la lecture d&#8217;un enregistrement, un produit par exemple :





Puisque l&#8217;on cherche à lire un produit à partir de son identifiant, Hibernate génère la requête SQL suivante :



select * from produit where id = ?;



Et comme on est en chargement immédiat de ----Produit---- vers ----Categorie----, Hibernate génère aussi un ----SELECT---- sur la table ----CATEGORIE---- :



select * from categorie where id = ?;



Mais l&#8217;association ----Categorie &#8594; Produit---- est aussi en chargement immédiat, et donc Hibernate génère en plus la requête suivante, puisque la catégorie associée au produit a été chargée :



select * from produit where categorie_id = ?;



Etendons maintenant ce raisonnement à l&#8217;association many-to-many ----Produit &lt;&#8594; Auteur----. Pour chaque produit chargé [1], Hibernate effectue un ----SELECT---- vers la table des auteurs :



select produit_auteur.*,auteur.* from produit_auteur left outer join auteur on produit_auteur.auteur_id=auteur.id where produit_auteur.produit_id=?



Et pour chaque auteur chargé, Hibernate génère un ----SELECT---- vers les produits associés :



select produit_auteur.*,produit.* from produit_auteur left outer join produit on produit_auteur.produit_id=produit.id where produit_auteur.auteur_id=?



Et si parmi les produits associés aux auteurs, certains n&#8217;étaient pas encore chargés, Hibernate relance à nouveau des requêtes vers la table ----CATEGORIE----, puis ----AUTEUR---- pour chacun de ces produits&#8230;&#8203;


Vous l&#8217;avez compris : le chargement immédiat est une très mauvaise stratégie car elle implique l&#8217;exécution de nombreuses requêtes SQL et l&#8217;instanciation d&#8217;un graphe d&#8217;objets conséquent, alors que nous souhaitions lire le contenu d&#8217;un seul produit (pas les objets associés) ! Cette solution entraîne évidemment des dégradations importantes des performances de l&#8217;application.


L&#8217;objectif du lazy loading est de pallier ce problème, en minimisant le nombre de requêtes générées en fonction des besoins applicatifs, tout en tenant compte des associations.




Principe du lazy loading


Le lazy loading concerne le chargement des associations, et dans certains cas le chargement des entités. Dans les paragraphes suivants, nous allons détailler le principe du lazy loading dans le cas des associations many-to-one, one-to-many.
Nous examinerons aussi comment fonctionne la méthode Session.load() vis-à-vis du lazy loading.


Nous supposons dorénavant que toutes les associations et toutes les classes sont paramétrées en chargement lazy (lazy=true ou lazy=proxy), ce qui correspond en fait au paramétrage par défaut avec Hibernate 3 (le mode par défaut avec Hibernate 2 est le chargement immédiat).


Voici les fichiers de mapping correspondant à notre exemple :



&lt;class name="Produit" table="PRODUIT" lazy="true"&gt;
  ...
  &lt;many-to-one name="categorie"
              column="CATEGORIE_ID"
              class="Categorie"
              lazy="proxy"/&gt;
  &lt;set name="auteurs" table="PRODUIT_AUTEUR" lazy="true"&gt;
    &lt;key column="PRODUIT_ID"/&gt;
    &lt;many-to-many class="Auteur" column="AUTEUR_ID"/&gt;
  &lt;/set&gt;
&lt;/class&gt;

&lt;class name="Categorie" table="CATEGORIE" lazy="true"&gt;
  ...
  &lt;set name="produits" lazy="true"&gt;
    &lt;key column="CATEGORIE_ID"/&gt;
    &lt;one-to-many class="Produit"/&gt;
  &lt;/set&gt;
&lt;/class&gt;

&lt;class name="Auteur" table="AUTEUR" lazy="true"&gt;
  ...
  &lt;set name="produits" table="PRODUIT_AUTEUR" lazy="true"&gt;
    &lt;key column="AUTEUR_ID"/&gt;
    &lt;many-to-many class="Produit" column="PRODUIT_ID"/&gt;
  &lt;/set&gt;
&lt;/class&gt;



Les associations many-to-one

En mode lazy, suite à la lecture d&#8217;une entité, Hibernate ne charge pas les associations, i.e. Hibernate ne génère pas de requête SQL correspondant à ces associations.
En revanche les instances des objets associés existent quand les foreign key ne sont pas nulles : en effet, si les champs d&#8217;instance correspondant aux associations étaient ----null----, cela ne reflèterait pas la réalité des données en base.


Exemple : un produit est rattaché à une catégorie, et donc, lors du ----SELECT---- sur le produit recherché, la foreign key ----CATEGORIE_ID---- est récupérée (voir le ----SELECT---- ci-dessous) et doit être stockée dans un objet côté Java, afin de ne pas perdre cette information &#8658; le champ ----categorie---- dans l&#8217;instance de ----Produit---- ne doit pas être ----null----.



select id, code, description, categorie_id from produit where id = ?



En fait, l&#8217;instance de catégorie est renseignée incomplètement : seul son identifiant est renseigné (avec la foreign key), les autres champs sont pour l&#8217;instant ----null----. La figure ci-dessous illustre ce fonctionnement :







La problématique est alors la suivante : quand l&#8217;application accède au contenu de la catégorie (par exemple ----produit.getCategorie().getCode()----), il ne faut pas retourner une valeur nulle (données incohérentes).


La technique utilisée par Hibernate consiste alors à générer un proxy : l&#8217;objet ----categorie---- instancié par Hibernate n&#8217;est pas strictement du type ----Categorie----, il s&#8217;agit d&#8217;une sous-classe de ----Categorie---- (dans la figure ci-dessus, j&#8217;ai nommé cette sous classe ----Categorie$Enhanced.class---- : ce nom n&#8217;est pas exact, mais ce n&#8217;est pas très important, car dans notre code, nous ne devons jamais faire apparaître explicitement ces types).
Intérêt du proxy : il permet de détecter le premier accès au contenu de l&#8217;objet catégorie et de générer un SELECT sur la table CATEGORIE à ce moment là :



select * from categorie where id = ?



Suite au ----SELECT----, tous les champs de la catégorie sont renseignés, avant le retour de la méthode ----categorie.getCode()----. Cette méthode retourne alors la valeur du code de la catégorie telle qu&#8217;elle est en base de données.
Evidemment, si un autre accès au contenu de la catégorie est effectué (----categorie.getXXX()----), Hibernate ne génère pas à nouveau la requête sur la table ----CATEGORIE----, puisque l&#8217;état de l&#8217;objet est déjà renseigné. La figure ci-dessous illustre ce fonctionnement :








Les associations one-to-many

Le principe du lazy loading est similaire à ce que nous venons de voir avec les associations many-to-one.
La différence, c&#8217;est que dans ce cas Hibernate n&#8217;utilise pas de proxy.
Cela est tout à fait normal : dans le code Java, une association many est représentée par une collection.
Cet objet est défini par une des interfaces génériques de Java (----Collection----, ----List----, ----Set----, ----Map----), et Hibernate peut donc fournir sa propre implémentation[2] afin de mettre en oeuvre le code d&#8217;interception, lors du premier accès au contenu de l&#8217;association (exemple : dans le cas d&#8217;un ----Set----, Hibernate instancie un ----PersistentSet----).







Lors du premier accès au contenu de la collection (exemple : ----categorie.getProduits().iterator()----), Hibernate déclenche un ----SELECT---- vers la table correspondant aux objets associés (dans notre cas, la table ----PRODUIT----) :



select * from produit where categorie_id = ?



Au retour du ----SELECT----, Hibernate possède donc toutes les informations nécessaires pour instancier et renseigner complètement l&#8217;état des objets ----Produit---- associés à la catégorie (les produits ne sont pas des proxys).
Ces objets sont en outre ajoutés dans la collection ----Categorie.produits----.








La méthode Session.load()

Le lazy loading est aussi utilisé lorsque l&#8217;on effectue la lecture d&#8217;une entité à partir de son identifiant, via la méthode ----Session.load()----.
L&#8217;objet recherché avec ----Session.load()---- est instancié, mais la requête SQL correspondante n&#8217;est pas générée tout de suite.
Donc comme dans le cas des associations many-to-one, Hibernate renseigne incomplètement l&#8217;état de l&#8217;objet (son identifiant essentiellement) &#8658; un proxy est utilisé.







Et comme précédemment, lors du premier accès au contenu de l&#8217;objet, Hibernate génère un SELECT et initialise complètement l&#8217;état de l&#8217;objet :










L&#8217;exception LazyInitializationException


La technique de rechargement retardée fonctionne lorsque la session Hibernate est ouverte.
En effet, si l&#8217;on tente d&#8217;accéder à un proxy qui n&#8217;est pas encore initialisé, alors que la session est fermée (i.e. la connexion à la base de données est fermée), Hibernate ne peut pas générer de requête SQL : il en informe le code client par l&#8217;émission d&#8217;une LazyIntializationException.







La question que l&#8217;on se pose dans ce cas est alors la suivante : comment éviter ces exceptions ?
On pourrait par exemple paramétrer nos associations en mode de chargement immédiat :
évidemment, ce n&#8217;est pas la bonne réponse (cf premier paragraphe).


Une autre technique consiste à utiliser un design pattern nommé Open Session in View.
Cette technique est applicable uniquement en environnement web.
Il s&#8217;agit en fait d&#8217;installer un filtre de servlet qui ouvre la session Hibernate à la réception de la requête HTTP, et qui la ferme juste avant de renvoyer la réponse HTTP. Ainsi, quand les JSP tentent d&#8217;afficher le contenu des objets proxy non initialisés, Hibernate peut générer les requêtes SQL qui permettront d&#8217;initialiser ces proxys, puisque la session est encore ouverte.
Mais ce pattern comporte un inconvénient majeur : le code de la couche métier n&#8217;est pas portable, puisqu&#8217;il doit être nécessairement exécuté en environnement web.
En effet, si l&#8217;on souhaite utiliser notre code dans des composants distribués (RMI, Web Service&#8230;&#8203;), on retrouvera les LazyInitializationException !


Voici une dernière approche : il s&#8217;agit d&#8217;identifier la profondeur d&#8217;initialisation des graphes d&#8217;objets nécessaire en retour de la couche de service métier, afin de répondre au besoin des couches clientes. Une fois que l&#8217;on sait exactement ce que l&#8217;on doit récupérer, il suffit d&#8217;initialiser ces graphes d&#8217;objets dans les services métiers avec la technique adaptée (lecture fetchée, navigation&#8230;&#8203;).
Avantage : on élimine les LazyInitializationException et le code de la couche métier est portable dans d&#8217;autres environnement que les applications web.






1. n&#8217;oubliez pas qu&#8217;il n&#8217;y a plus qu&#8217;un seul produit chargé, mais plusieurs du fait du chargement immédiat de ----Categorie---- vers ----Produit----


2. c&#8217;est notamment pour cette raison que les côtés many doivent être déclarés avec les interfaces des collections, et non pas des types concrets (et de toute façon, c&#8217;est une bonne pratique !)

</description>
          <pubDate>2009-01-22T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Hibernate/Lazy_loading</link>
          <guid isPermaLink="true">https://www.jtips.info/Hibernate/Lazy_loading</guid>
        </item>
      
    
      
        <item>
          <title>Calcul avec BigDecimal</title>
          <description>
Dans un l&#8217;article sur float et double, nous avions vu qu&#8217;une suite de calculs assez simples, mais avec des valeurs initiales bien choisies, pouvait aboutir à des résultats étonnants.
Pour avoir de meilleurs résultats, il est préférable d&#8217;utiliser la classe java.math.BigDecimal.


BigDecimal / double


Le calcul divergeant avec double était le suivant :



 double b = 4095.1;
 double a = b + 1;
 double x = 1;

 for (int index = 1; index &lt;= 9; index++) {
   x = (a * x) - b;
   System.out.printf("%01d =&gt; %.6f\n", index, x);
 }



Avec des BigDecimal, cela donnerait ceci :



 BigDecimal b = new BigDecimal(4095.1);
 BigDecimal a = b.add(new BigDecimal(1));
 BigDecimal x = new BigDecimal(1);

 for (int index = 1; index &lt;= 9; index++) {
   x.multiply(a).subtract(b);
   System.out.printf("%01d =&gt; %.6f\n", index, x);
 }



Le résultat du calcul ne diverge plus du tout :



 1 =&gt; 1,000000
 2 =&gt; 1,000000
 ...
 9 =&gt; 1,000000
 ...
 999 =&gt; 1,000000





Calculs avec BigDecimal


Par contre, les possibilités de calcul sont plus réduites avec BigDecimal, car la classe java.util.Math ne supporte pas ce type.
Pour les calculs élémentaires, les méthodes sont directement dans BigDecimal :




add


subtract


multiply


divide, pour ce dernier, plusieurs versions, avec des modes d&#8217;arrondi sont proposés




Des méthodes de calculs un peu plus complexes sont aussi présents :




abs pour la valeur absolue


min et max


pow élève la valeur à une certaine puissance


pow calcule la racine carrée




</description>
          <pubDate>2009-01-07T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/BigDecimal</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/BigDecimal</guid>
        </item>
      
    
      
        <item>
          <title>PHP/QuercusTomcat</title>
          <description>
Apache Tomcat est un serveur d&#8217;applications Web plus répandu que Resin.
Il est donc intéressant de tester l&#8217;utilisation de Quercus avec Tomcat.


Installation


Pour démarrer, il faut un environnement Tomcat qui fonctionne, avec Tomcat 5 ou 6 et un JDK 5 ou 6.
Il faut aussi télécharger Quercus Standalone sur le site de Caucho.


Pour installer Quercus, il faut décompresser le fichier war téléchargé (quercus-3.2.1.war) sous forme de répertoire (quercus, par exemple), puis copier ce répertoire dans répertoire webapps de Tomcat.
Les pages PHP pourront être ajoutées dans ce répertoire ; elles seront appelée par l&#8217;URL http://localhost:8080/quercus/nom-de-la-page.php.


Cette procédure peut s&#8217;appliquer à n&#8217;importe quel serveur d&#8217;applications qui supporte le déploiement en mode répertoire (JBoss, Glassfish,&#8230;&#8203;).
Pour les serveurs qui imposent le mode archive, il faut décompresser l&#8217;archive de Quercus, ajouter les fichiers php, puis reconstituer l&#8217;archive.




Ajout de PHP dans une application


Pour ajouter le support de PHP à une application existante ou en cours de développement, il faut surtout ajouter la servlet « Quercus Servlet ».
Pour cela, il faut prendre les fichiers quercus.jar et resin-util.jar dans le répertoire WEB-INF/lib de l&#8217;archive et de les ajouter dans le même répertoire de notre application. Ensuite, il faut déclarer la servlet dans le fichier web.xml du répertoire WEB-INF.
Enfin, dans le même fichier, il faut déclarer la prise en charge des fichiers php par cette servlet.



 &lt;servlet&gt;
   &lt;servlet-name&gt;Quercus Servlet&lt;/servlet-name&gt;
   &lt;servlet-class&gt;com.caucho.quercus.servlet.QuercusServlet&lt;/servlet-class&gt;
 &lt;/servlet&gt;
 &lt;servlet-mapping&gt;
   &lt;servlet-name&gt;Quercus Servlet&lt;/servlet-name&gt;
   &lt;url-pattern&gt;*.php&lt;/url-pattern&gt;
 &lt;/servlet-mapping&gt;





Ajout de PHP dans toutes les applications


Dans Resin, toutes les applications supportent PHP. Pour avoir le même comportement dans Tomcat, il faut placer les fichiers quercus.jar et resin-util.jar dans le répertoire lib de Tomcat, comme pour la procédure précédente.
La différence, c&#8217;est que nous n&#8217;allons déclarer la servlet de Quercus qu&#8217;une seule fois, au niveau global, dans le fichier web.xml du répertoire conf de Tomcat.


Attention, dans le cas d&#8217;une déclaration globale, il ne faut plus déclarer localement la servlet car cela créerait une conflit.


</description>
          <pubDate>2008-12-30T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/PHP/QuercusTomcat</link>
          <guid isPermaLink="true">https://www.jtips.info/PHP/QuercusTomcat</guid>
        </item>
      
    
      
        <item>
          <title>Introduction à Quercus</title>
          <description>
Quercus est un logiciel open source, développé par la société Caucho, permettant de faire fonctionner une application PHP dans une machine virtuelle Java.


Il peut être utilisé dans Resin ou dans un autre serveur d&#8217;applications comme Tomcat.


Présentation de Quercus


Société Caucho

Quercus a été développé par la société Caucho, connue des habitués de Java pour leur serveur d&#8217;applications Resin et auteur des protocoles de communication Burlap et Hessian, basés sur HTTP, avec du contenu XML ou binaire. Tous ces produits sont distribués sous licence open source, avec des versions dites professionnelles, sous licence commerciale.


Resin est un serveur d&#8217;applications Java, destiné au déploiement d&#8217;applications Web. De ce fait, il est en concurrence directe avec Apache Tomcat, mais aussi avec tous les serveurs d&#8217;applications JavaEE, comme IBM Websphere, Oracle Weblogic ou RedHat JBoss. Resin est le premier serveur d&#8217;applications Java capable de supporter le déploiement d&#8217;applications PHP, en mode in-process, grâce à l&#8217;intégration de Caucho. Glassfish, le serveur d&#8217;applications JavaEE open source de Sun, lui emboite le pas puisqu&#8217;il embarque aussi Quercus pour la version 3.



Mode de fonctionnement

Dans toutes les solutions concurrentes, les pages PHP et les classes Java sont exécutées dans des processus séparés et communiquent via les couches du réseau. A l&#8217;opposé, avec Quercus, le PHP est exécuté au sein du même processus que Java, ce qui économise toutes les communications inter-processus.







Dans la version open source, Quercus interprète le code PHP, alors que la version professionnelle est capable de la compiler en byte-code Java, ce qui améliore encore les performances. Quercus est donc une implémentation du moteur PHP, indépendante de celle fournie par php.net.


Pour valider le fonctionnement, Caucho a testé un certain nombre d&#8217;applications PHP classiques, comme Typo3, MediaWiki, Drupal ou WordPress. Les résultats semblent probants, puisque sur certains tests, Quercus est quatre fois plus performant que le moteur PHP traditionnel.



Avantages

Outre l&#8217;interopérabilité des langages, les avantages de Quercus sont à chercher du coté des performances, ainsi que de certains services fournis par Java, comme la gestion des connexions aux bases de données par pools, grâce aux datasources.





Utilisation de Quercus


Quercus est intégré dans Resin. Il suffit donc d&#8217;installer ce dernier pour pouvoir l&#8217;utiliser.


Pouvoir déployer des pages PHP dans un serveur d&#8217;applications Java ne présente, en tant que tel, que peu d&#8217;intérêt. Cet exercice devient vraiment productif si le code PHP et le code Java peuvent interagir facilement, et si l&#8217;application PHP peut exploiter les ressources du serveur d&#8217;applications.


Connexion aux bases de données

En java, les connexions aux bases de données relationnelles se font via les drivers Java et les datasources. Toute connexion se fait en utilisant une API standard qui s&#8217;appelle JDBC, et le code Java est strictement identique pour les différents types de bases de données (Oracle, MySql, Microsoft SqlServer&#8230;&#8203;). Pour les applications Web, les connexions sont gérées au sein de pools et partagées entre les différents utilisateurs, via des composants appelés DataSource. Ces composants sont paramétrés dans le serveur d&#8217;applications et mis à disposition des applications déployées via le registre JNDI.







mysql_connect

Pour accéder à une base de données MySql, le développeur PHP utilise la foncion mysql_connect, en lui passant les paramètres de connexion (host, username, password et dbname). La même fonction peut être utilisée pour établir une connexion via une datasource ; dans ce cas, seul le nom de la datasource est passée en paramètre. Pour que cela fonctionne, il faut que la datasource ait été créée dans le serveur d&#8217;applications.



 // Connection traditionnelle, en PHP
 mysql_connect($host, $username, $password, $dbname);

 // Connection via une datasource
 mysql_connect("java:comp/env/jdbc/sewa-ds");



Par configuration, dans le fichier WEB-INF/resin-web.xml, il est possible d&#8217;ignorer les paramètres et d&#8217;imposer une base de données. Ceci peut être fort utile dans le cadre du portage d&#8217;une application PHP traditionnelle vers Quercus.



 &lt;web-app xmlns="link:http://caucho.com/ns/resin[http://caucho.com/ns/resin]"&gt;
   &lt;database jndi-name="jdbc/mysql"&gt;
     &lt;driver type="org.gjt.mm.mysql.Driver"&gt;
       &lt;url&gt;jdbc:mysql://localhost:3306/test&lt;/url&gt;
       &lt;user&gt;&lt;/user&gt;
       &lt;password&gt;&lt;/password&gt;
     &lt;/driver&gt;
   &lt;/database&gt;
 &lt;/web-app&gt;




Classe PDO

La classe PDO (Portable Data Object) permet de fournir des connexions à des bases de données, via des datasources, sans se préoccuper de leur type. Pour utiliser une connexion gérée par une datasource, il faut instancier un objet de type PDO en lui passant le nom complet de la datasource.



 $connection = new PDO("java:comp/env/jdbc/my-database");





Appel de classe Java

Classes standard

Il est possible d&#8217;instancier une classe Java standard, il faut utiliser la classe Java : $a = new Java("java.util.Date", 123);. Il est possible de gérer des imports de nom, qui allègent la notation au moment de l&#8217;instanciation : import java.util.Date;. Dans ce cas, l&#8217;instanciation est encore plus simple et ressemble à l&#8217;instanciation d&#8217;une classe PHP : $a = new Date(123);


Les classes utilisables sont accessible via le système de classloaders de Resin. Elles doivent être dans une des localisations suivantes :




WEB-INF/classes/ de l&#8217;application


WEB-INF/lib/*.jar de l&#8217;application


lib/*.jar de Resin




Il est possible de mélanger des classes PHP et des classes Java dans le répertoire WEB-INF/classes/.



Méthodes

L&#8217;appel de méthode peut se faire de façon classique. L&#8217;accès aux propriétés peut être fait de façon simplifiée, sans utiliser le get ou le set explicitement.


Les méthodes static sont accessible via la classe java_class : $class = java_class("java.lang.System"); puis $in = $class&#8594;in; ou directement sur la classe, si un import a été fait au préalable : $calendar = Calendar::getInstance();.


Lorsque de méthodes sont surchargées, le nombre d&#8217;arguments est prédominant. Par contre, si plusieurs méthodes ne sont différenciées que par le type des arguments, l&#8217;aspect non typé peut poser problème.



Fonctions

Les méthodes des classes qui héritent de AbstractQuercusModule sont exposées sous forme de fonctions PHP.




</description>
          <pubDate>2008-12-30T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/PHP/Quercus</link>
          <guid isPermaLink="true">https://www.jtips.info/PHP/Quercus</guid>
        </item>
      
    
      
        <item>
          <title>J2EE</title>
          <description>
Un serveur d&#8217;applications JavaEE permet de déployer des applications Web développées en Java, ainsi que des composants EJB.
  Il leurs fournit divers services, comme un registre de composant JNDI, des pools de connexions aux bases de données, appelées DataSource, de la messagerie inter-applications, une gestion des transactions,&#8230;&#8203;







DataSource


La datasource est le service le plus utilisé. Il permet aux applications de se connecter aux bases de données via un poll de connexions.







</description>
          <pubDate>2008-12-30T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/J2EE/index</link>
          <guid isPermaLink="true">https://www.jtips.info/J2EE/index</guid>
        </item>
      
    
      
        <item>
          <title>Installation JBoss AS 5</title>
          <description>
JBoss 5.0 est sorti fin 2008. Il est certifié JavaEE 5 et son architecture a été totalement revue. C&#8217;est ce dernier point qui explique l&#8217;important retard par rapport à ses concurrents. Une version 5.1 est sortie peu de temps après, en mai 2009.


Prérequis


Comme pour JBoss 4.2.3, JBoss 5.x peut fonctionner avec un JDK 5 ou 6. Une distribution spécifique est fournie pour chaque JDK, mais il est aussi possible de modifier un JBoss installé pour migrer de JDK.




Nouveautés de JBoss 5.0


Répertoires de déploiement

La première nouveauté qui saute aux yeux, lorsqu&#8217;on installe JBoss est la présence d&#8217;un nouveau répertoire dans les configurations. Ce nouveau répertoire, deployers, sert à déployer les services de déploiement. Un premier effort en ce sens avait déjà été fait avec la création de services spécifiques (fichiers *.deployer ou *-deployer.xml), et l&#8217;accentuation de cette séparation va dans le sens d&#8217;une meilleure organisation des déploiements.


Dans JBoss 4, il était aussi possible de créer des répertoires de déploiement séparés, technique qu&#8217;on utilisait généralement pour séparer les déploiements d&#8217;applications des déploiements de services JBoss.


Certainement moins visible, mais tout aussi important, la possibilité de paramétrer des scanner autonomes apporte une souplesse supplémentaire dans la gestion de ces répertoires de déploiement. Ces nouveaux scanners s&#8217;appuient sur le système VFS.



Configurations

On retrouve les configurations traditionnelles que sont default, all et minimal. On retrouve la configuration standard des versions certifiées. Celle-ci nous rappelle que JBoss est certifié dans une configuration qui n&#8217;est pas celle que nous utilisons habituellement, ce qui peut poser des problèmes de gestion de librairies ou de configuration (log4j, par exemple).


La vraie nouveauté est la configuration web, qui reprend les principaux services nécessaires au déploiement d&#8217;applications Web.



Librairies

Un nouveau répertoire lib a été ajouté, pour y mettre les librairies jar communes aux différentes configurations.


On se trouve donc avec trois niveaux de librairies :




&lt;boss_root&gt;/lib, auquel il ne faut pas toucher


&lt;boss_root&gt;/common/lib, qui contient les librairies communes à toutes les configurations


&lt;jboss_config&gt;/lib, qui contient les librairies spécifiques à une configuration





Service Windows

Pour installer JBoss 4.2, la technique la plus classique était d&#8217;utiliser les composants JBoss Native, avec son script service.bat. Ce dernier a été intégré à JBoss 5.


Par conséquent, l&#8217;installation de JBoss 5 est directement supportée dans la distribution classique.



JMX

Bien que le noyau de JBoss 5 ne soit plus le microkernel JMX, mais un microcontainer AOP, tous les composants déployés sont toujours accessibles via JMX. Le script twiddle et la jmx-console existent toujours, et les utilitaires mc4j et jManage continuent de fonctionner.


La nouveauté, ici, est le lifting de jmx-console. Il est vrai que cette dernière avait un aspect vieillot, voire rebutant, qui a fait fuir plusieurs administrateurs. La console est un peu plus joli, mais son fonctionnement reste strictement identique.


Pour une console plus riche, on se regrettera Embedded JOPR, qui n&#8217;est pas compatible avec JBoss 5.0, mais dont la version 1.2 est intégrée à JBoss 5.1.





Nouveautés de JBoss 5.1


Cette version est une évolution mineure de JBoss. Elle ne modifie en rien la structure interne, mais ajoute quelques fonctionnalités.


Console d&#8217;administration

JBoss se dote enfin d&#8217;une console d&#8217;administration présentable ! Avec l&#8217;intégration de Embedded JOPR dans la distribution standard, les railleries des opposants à JBoss pourront être mises en veilleuse.



Web Beans

JBoss 5.1 apporte une version préliminaire (qui est aussi la RI) de la JSR-299. Cette spécification, dont le nom initial était Web Beans s&#8217;appelle maintenant Java Contexts and Dependency Injection. Elle a pour objectif de faciliter l&#8217;injection de composants entre frameworks.



</description>
          <pubDate>2008-12-16T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/Installation-AS5</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/Installation-AS5</guid>
        </item>
      
    
      
        <item>
          <title>Cache de second niveau d&apos;Hibernate</title>
          <description>
Les différents caches d&#8217;Hibernate


Hibernate fonctionne avec deux niveaux de cache.


Le cache de premier niveau est lié à la session Hibernate, et les objets qui sont cachés ne sont visibles que pour une seule transaction.
Quand la session est fermée, le cache n&#8217;a plus d&#8217;existence.
Ce cache ne peut pas être désactivé.


Le cache de second niveau est quant à lui lié à la session factory d&#8217;Hibernate.
La portée de ce cache est la JVM, voire le cluster, où l&#8217;application est déployée.
Les objets cachés sont donc visibles depuis l&#8217;ensemble des transactions. Par défaut, ce cache n&#8217;est pas activé.
Une configuration est donc nécessaire, afin de l&#8217;activer.


Il existe enfin un cache de requête qui permet de cacher les résultats des requêtes exécutées.




Principe du cache de second niveau


Le cache de second niveau permet de cacher des entités, ainsi que des associations de type collection.
Il convient donc de sélectionner les classes et les collections à cacher, puis de configurer les fichiers de mapping correspondant.


Gestion du cache des entités

La configuration consiste à ajouter l&#8217;élément &lt;cache&gt; dans le fichier de mapping, juste avant l&#8217;élément &lt;id&gt;.
Il convient aussi de choisir une stratégie transactionnelle, sur laquelle nous reviendrons plus tard (attribut usage).



 &lt;class name="Produit" table="PRODUIT"&gt;
   &lt;cache usage="read-only"/&gt;
   &lt;id name="id" column="ID"&gt;
     &lt;generator class="identity"/&gt;
   &lt;/id&gt;
   &lt;property name="code" column="CODE"/&gt;
   &lt;property name="libelle" column="LIBELLE"/&gt;
   &lt;many-to-one name="categorie" column="CATEGORIE_ID" class="Categorie"/&gt;
 &lt;/class&gt;



L&#8217;exemple ci-dessus permet de cacher les entités de type Produit lorsqu&#8217;elles sont chargées depuis la base de données.
En fait, ce ne sont pas les instances Java issues du requêtage qui sont placées directement dans le cache, mais une copie de ces instances.
En effet, Hibernate utilise un mécanisme de sérialisation rapide pour la mise en cache d&#8217;un objet, afin d&#8217;éviter que plusieurs transactions ne possèdent une référence sur le même objet, ce qui occasionnerait des problèmes d&#8217;accès concurrents.
Par ailleurs, une fois placées en cache, les entités sont récupérées du cache à partir de leur identifiant.
Ce dernier est donc utilisé comme index lors de l&#8217;ajout d&#8217;une entité dans le cache.


Prenons l&#8217;exemple de 3 produits récupérés dans une transaction T1 :



+--------------------------------+
|             Cache des Produits |
+--------------------------------+
|  7 -&gt; [ "P007", "Tomate",  9 ] |
| 10 -&gt; [ "P010", "Celeri",  9 ] |
| 12 -&gt; [ "P012", "Pomme" , 13 ] |
+--------------------------------+



Remarque : l&#8217;association many-to-one Produit &#8594; Categorie est cachée sous la forme de l&#8217;identifiant de la catégorie, pas l&#8217;objet Categorie associé (dernière colonne : id=9 &#8658; catégorie "Légume" / id=13 &#8658; catégorie "Fruit").


Ces produits n&#8217;étaient pas présents dans le cache avant T1, et donc 3 select ont été exécutés (on suppose que T1 effectue un requêtage par identifiant avec session.get() par exemple) :



select * from produit where id = 7;
select * from produit where id = 10;
select * from produit where id = 12;



Si, pour chaque produit récupéré, T1 parcourt l&#8217;association Produit &#8594; Categorie (par exemple invoquant produit.getCategorie().getLibelle()), deux requêtes supplémentaires sont générées sur la table CATEGORIE.



select * from categorie where id = 9;
select * from categorie where id = 13;



Mais les deux instances de catégorie ne sont pas placées en cache de second niveau, puisque nous ne l&#8217;avons pas demandé dans le mapping :



 &lt;class name="Categorie" table="CATEGORIE"&gt;
   &lt;id name="id" column="ID"&gt;
     &lt;generator class="identity"/&gt;
   &lt;/id&gt;
   &lt;property name="code" column="CODE"/&gt;
   &lt;property name="libelle" column="LIBELLE"/&gt;
   &lt;set name="produits"&gt;
     &lt;key column="CATEGORIE_ID"/&gt;
     &lt;one-to-many class="Produit"/&gt;
 &lt;/class&gt;



Lorsqu&#8217;une seconde transaction T2 charge un des produits placés en cache lors de T1 (avec la méthode session.get() par exemple), aucun select sur la table PRODUIT n&#8217;est généré. En revanche, si T2 parcourt l&#8217;association Produit &#8594; Categorie pour le produit P010, une requête est générée sur la table CATEGORIE :



select * from categorie where id = 9;




Gestion du cache des associations many

La configuration du cache des collections est effectuée dans le fichier de mapping, à l&#8217;aide de l&#8217;élément &lt;cache&gt;.
Dans l&#8217;exemple ci-dessous, nous demandons à cacher la collection de produits associée à une catégorie :



 &lt;class name="Categorie" table="CATEGORIE"&gt;
   &lt;id name="id" column="ID"&gt;
     &lt;generator class="identity"/&gt;
   &lt;/id&gt;
   &lt;property name="code" column="CODE"/&gt;
   &lt;property name="libelle" column="LIBELLE"/&gt;
   &lt;set name="produits"&gt;
     &lt;cache usage="read-only"/&gt;
     &lt;key column="CATEGORIE_ID"/&gt;
     &lt;one-to-many class="Produit"/&gt;
 &lt;/class&gt;



Ainsi, chaque fois qu&#8217;une association many est parcourue, Hibernate ajoutera en cache la liste des identifiants des entités associées (et non pas les objets associés), indexée par l&#8217;identifiant de l&#8217;objet parent.
Les objets associés sont mis en cache uniquement si le cache d&#8217;entité est défini pour ces objets.


Dans l&#8217;exemple ci-dessous, la catégorie "Légume" (id=9) et la catégorie "Fruit" (id=13) sont chargées depuis la base de données par une transaction T1.
Suite à l&#8217;accès à la collection de produits pour chaque catégorie, les associations sont placées en cache (id=7 &#8658; "Tomate" et id=10 &#8658; "Celeri" pour la catégorie "Légume" (id=9) / id=12 &#8658; "Pomme" pour la catégorie "Fruit" (id=13)).



+----------------------------------------------+
| Cache des associations Categorie -&gt; Produits |
+----------------------------------------------+
|  9 -&gt; [ 7, 10 ]                              |
| 13 -&gt; [ 12 ]                                 |
+----------------------------------------------+



Les requêtes exécutées par T1 sont les suivantes:



select * from categorie where id = 9;
select * from categorie where id = 13;
select * from produit where categorie_id = 9;
select * from produit where categorie_id = 13;



Si une nouvelle transaction T2 charge la catégorie "Légume" (id=9) et accède aux produits associés, les requêtes suivantes sont exécutées :


1) Si cache d&#8217;entité pour Produit



select * from categorie where id = 9; (il n'y a pas de cache d'entité pour Categorie)



2) Si pas de cache d&#8217;entité pour Produit



select * from categorie where id = 9; (il n'y a pas de cache d'entité pour Categorie)
select * from produit where id = 7;
select * from produit where id = 10;



3) Si pas de cache d&#8217;association Categorie &#8594; Produit



select * from categorie where id = 9;
select * from produit where categorie_id = 9;



Conclusion : si on utilise un cache d&#8217;association, il est important de vérifier que le cache d&#8217;entité est activé pour les objets associés, sinon on risque de générer plus de requêtes que sans cache !





Les méthodes Hibernate qui exploitent le cache de second niveau


Session.load() et Session.get()

Les méthodes load() et get() interrogent toujours le cache avant d&#8217;émettre une requête vers la base de données.



Query.list() et Criteria.list()

La méthode list() n&#8217;utilise pas le cache de second niveau pour les objets sélectionnés par la requête.
En revanche, Hibernate interroge le cache en ce qui concerne les objets associés.
Par ailleurs, les objets sélectionnés sont placés dans le cache après l&#8217;exécution de la requête.



Query.iterate()

La méthode iterate() charge les instances une à une à partir de leur identifiant.
Par conséquent, cette méthode interroge toujours le cache pour les objets sélectionnés, ainsi que pour les associations.





Les stratégies transactionnelles


Il existe quatre stratégies transactionnelles possibles pour le cache de second niveau (read-only, read-write, nonstrict-read-write, transactionnal).


La stratégie read-only

Cette stratégie permet de cacher des données en lecture seule.
Si l&#8217;application tente de modifier des données read-only, une exception UnsupportedOperationException est levée par Hibernate.
Par ailleurs, l&#8217;accès aux données du cache est synchronisé.




Pour les environnements single node : EHCache et OSCache.


Pour les environnements cluster : SwarmCache et JBossCache





La stratégie read-write

Cette stratégie permet de cacher des données en lecture / écriture. Lors d&#8217;opérations de mise à jour, les modifications sont répercutées dans le cache.
Par ailleurs, l&#8217;accès aux données du cache étant synchronisé, les transactions sont assurées de lire les versions les plus à jour des données "committées" (ce niveau transactionnel se comporte comme un niveau d&#8217;isolation read committed).
Généralement, cette stratégie ne convient pas aux environnements en cluster, car aucun des cache providers cités dans la documentation Hibernate ne gère le verrouillage des données dans un tel environnement.
Il semble cependant que le produit Tangosol Coherence gère correctement la stratégie read-write au sein d&#8217;un cluster (note: Tangosol Coherence est désormais un produit Oracle).




Pour les environnements single node : EHCache et OSCache.


Pour les environnements cluster : Oracle Coherence





La stratégie nonstrict-read-write

Cette stratégie permet de cacher des données qui sont modifiées occasionnellement.
L&#8217;accès aux données n&#8217;est pas synchronisé, ce qui en fait la stratégie la plus rapide.
Cependant, il n&#8217;est pas garanti que les transactions récupèrent les données les plus à jour, suite à des modifications.








Lors de mise à jour, les données sont simplement évincées du cache.
Ce principe permet de synchroniser facilement les caches des différents noeuds d&#8217;un cluster par simple notification.







Pour les environnements single node : EHCache et OSCache.


Pour les environnements cluster : SwarmCache







Principe du cache des requêtes


Pour utiliser le cache de requête, il faut d&#8217;abord l&#8217;activer au niveau de la configuration Hibernate :



 &lt;property name="hibernate.cache.use_query_cache"&gt;true&lt;/property&gt;



Ensuite, on choisit de cacher les résultats d&#8217;une requête au moment de la création de la requête via la méthode setCacheable() sur un objet Query ou Criteria.



 Query query = session.createQuery("from Produit p where p.prix &gt; :prix")
                      .setFloat("prix", 10f)
                      .setCacheable(true);
 List produits = query.list();



Après la première exécution de la requête, Hibernate cache les identifiants des entités récupérées par la requête (et non pas les entités elles-mêmes), les données cachées étant indexées par la chaîne de requêtes + les paramètres injectés.



+----------------------------------------------------------------+
|                                             Cache des requêtes |
+----------------------------------------------------------------+
| [ "from Produit p where p.prix &gt; :prix", 10 ] -&gt; [ 5, 12, 19 ] |
+----------------------------------------------------------------+





Activation du cache provider


Pour utiliser le cache de second niveau, il faut sélectionner un cache provider dans le fichier de configuration Hibernate. L&#8217;exemple suivante montre comment activer EHCache :



 &lt;property name="hibernate.cache.provider_class"&gt;
   org.hibernate.cache.EhCacheProvider
 &lt;/property&gt;



</description>
          <pubDate>2008-10-31T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Hibernate/Cache</link>
          <guid isPermaLink="true">https://www.jtips.info/Hibernate/Cache</guid>
        </item>
      
    
      
        <item>
          <title>API Filter d&apos;Hibernate</title>
          <description>
Filtrer une association de type many


Hibernate donne la possibilité de filtrer des associations de type collection. Cette alternative aux requêtes HQL classiques permet notamment de contourner certains problèmes de requêtage dûs aux associations unidirectionnelles. Prenons l&#8217;exemple d&#8217;une catégorie associée à un ensemble de produits. L&#8217;association est unidirectionnelle, i.e. une catégorie "voit" les produits auxquels elle est associée, mais les produits ne "voient" pas la catégorie.



public class Categorie implements Serializable {
  private Long id;
  private String code;
  private String libelle;
  private Set&lt;Produit&gt; produits;

  // getters et setters
  ...
}
public class Produit implements Serializable {
  private Long id;
  private String libelle;
  private float prix;

  // getters et setters
  ...
}



Avec ce type d&#8217;association, il est impossible de lire les produits d&#8217;une catégorie via la requête HQL suivante puisque la propriété categorie n&#8217;existe pas dans la classe Produit :



 String hql = "from Produit p where p.categorie = :cat";
 Query query = session.createQuery(hql);
 List resultats = query.list(); =&gt; ECHEC



En revanche, nous pouvons utiliser un filtre de collection (méthode createFilter()) pour obtenir le résultat souhaité :



 Categorie cat = (Categorie)session.load(Categorie.class, new Long(1));
 Query  query = session.createFilter(cat.getProduits(), "");
 List&lt;Produit&gt; produits = query.list();
 for (Produit produit : produits) {
   System.out.println("Produit: code=" + produit.getCode() + ",titre=" + produit.getTitre());
 }





Appliquer des restrictions au filtre


Le deuxième paramètre de la méthode createFilter() permet d&#8217;ajouter des restrictions à la requête générée. Ces restrictions sont exprimées sous la forme de fragment de HQL :



 Categorie cat = (Categorie)session.load(Categorie.class, new Long(1));
 Query  query = session.createFilter(cat.getProduits(), "where this.prix &gt; :par_prix")
                       .setFloat("par_prix", 40f);
 List&lt;Produit&gt; produits = query.list();
 for (Produit produit : produits) {
   System.out.println("Produit: code=" + produit.getCode() + ",titre=" + produit.getTitre());
 }



Remarque : le mot clé this permet de faire référence à l&#8217;instance de produit en cours.




La clause select


La requête peut retourner une sélection de propriétés grâce à la clause select :



 Categorie cat = (Categorie)session.load(Categorie.class, new Long(1));
 Query  query = session.createFilter(cat.getProduits(), "select this.titre where this.prix &gt; :par_prix")
                       .setFloat("par_prix", 40f);
 List&lt;String&gt; titres = query.list();
 for (String titre : titres) {
   System.out.println("Titre produit: " + titre);
 }



Remarque : la clause from n&#8217;est pas nécessaire.


</description>
          <pubDate>2008-10-30T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Hibernate/Filter</link>
          <guid isPermaLink="true">https://www.jtips.info/Hibernate/Filter</guid>
        </item>
      
    
      
        <item>
          <title>Monitoring Java</title>
          <description>
Cet article présente succinctement quelques outils de monitoring et de dépannage pour Java.


Outils du JDK


Depuis le JDK 5, java intègre des outils de monitoring et des [outils de dépannage http://java.sun.com/javase/6/docs/technotes/tools/index.html#troubleshoot].


JConsole

JConsole est une console graphique qui regroupe un nombre important d&#8217;informations sur le fonctionnement d&#8217;un machine virtuelle.


Plus d&#8217;informations :




Using JConsole to Monitor Applications





jps

jps donne la iste des programmes java en exécution.



jstat

jstat fournit des statistiques détaillées sur le fonctionnement de la machine virtuelle :




Compilation hotspot : -compiler, -printcompilation


Chargement des classes : -class


Utilisation du garbage collection et des zones mémoire : -gc,&#8230;&#8203;





jstatd

jstatd permet à jps et jstat d&#8217;accéder à un serveur distant.



jstack

jstack affiche la piles d&#8217;appels de chaque thread d&#8217;une JVM. Il permet de détecter des deadlocks.



jmap

jmap donne des informations d&#8217;utilisation de la mémoire :




Types des objets en mémoire (-histo)


Objets partagés


Mémoire heap (-heap)


Objets en attente de finalisation (-finalizerinfo)





jinfo

jinfo donne les propriétés système et les paramètres de lancement d&#8217;une JVM.



jsadebugd

http://java.sun.com/javase/6/docs/technotes/tools/share/jsadebugd.html





Outils JBoss


Les consoles fournies avec JBossAS 4 sont riches en informations, mais particulièrement peu ergonomiques. Depuis fin 2008, JBoss propose JOPR, qui est la version open source de JBoss Operation Network.


</description>
          <pubDate>2008-10-27T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Monitoring/Outils</link>
          <guid isPermaLink="true">https://www.jtips.info/Monitoring/Outils</guid>
        </item>
      
    
      
        <item>
          <title>Semantic/RDF</title>
          <description>
Resource Definition Framework


Références


RDF - Général



RDF / XML



Spécifications W3C de la syntaxe RDF


Validateur du W3C





N3



Sprécicifations W3C


N3 Primer





N-Triple



&lt;a class="external autonumber" href="1&lt;/a&gt;







Transformation




Conversion de RDF - N3 - N-Triples


Conversion de RDF en HTML





&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;xsl:stylesheet version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
        xmlns:dc="http://purl.org/dc/elements/1.1/"
        xmlns:foo="http://purl.org/rss/1.0/"&gt;
   &lt;xsl:output method="html"/&gt;
   &lt;xsl:template match="/"&gt;
       &lt;xsl:apply-templates select="/rdf:RDF/foo:channel"/&gt;
   &lt;/xsl:template&gt;
   &lt;xsl:template match="/rdf:RDF/foo:channel"&gt;
       &lt; h3 &gt;&lt;xsl:value-of select="foo:title"/&gt;&lt;/ h3 &gt;
       &lt; p &gt;&lt;xsl:value-of select="foo:description"/&gt;&lt;/ p &gt;
       &lt; ul &gt;
           &lt;xsl:apply-templates select="/rdf:RDF/foo:item"/&gt;
       &lt;/ ul &gt;
   &lt;/xsl:template&gt;
   &lt;xsl:template match="/rdf:RDF/foo:item"&gt;
       &lt; li &gt;
           &lt; a href="{foo:link}" title="{substring(dc:date, 0, 11)}" &gt;&lt;xsl:value-of select="foo:title"/&gt;&lt;/ a &gt;
           &lt; p&gt;&lt;xsl:value-of select="foo:description" disable-output-escaping="yes" /&gt;&lt;/p &gt;
       &lt;/li&gt;
   &lt;/xsl:template&gt;
&lt;/xsl:stylesheet&gt;





Applications


Wiki Sémantique



http://semantic-mediawiki.org/





</description>
          <pubDate>2008-10-10T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Semantic/RDF</link>
          <guid isPermaLink="true">https://www.jtips.info/Semantic/RDF</guid>
        </item>
      
    
      
        <item>
          <title>Semantic/Jena</title>
          <description>
Jena est un framework sémantique open source, qui fonctionne avec Java. Il a été réalisé par HP.


Ses fonctionnalités sont les suivantes :




Stockage de triples


Lecture et écriture RDF aux formats RDF/XML, N3 et N-Triples


API OWL


Moteur de requêtes SPARQL




Dans les API, il y a un moteur de génération de classes Java à partir d&#8217;une ontologie OWL, DAML ou RDFS.


Autres logiciels :




Joseki


</description>
          <pubDate>2008-10-10T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Semantic/Jena</link>
          <guid isPermaLink="true">https://www.jtips.info/Semantic/Jena</guid>
        </item>
      
    
      
        <item>
          <title>Semantic/Ecran</title>
          <description>
XUL de Mozilla s&#8217;appuie beaucoup sur le format RDF comme source de données.
On peut concevoir des templates pôur faire des écrans mappés sur des sources RDF.


On peut aussi appliquer du XSLT sur du RDF/XML.




RDF Twig


link: Rx4RDF


</description>
          <pubDate>2008-10-10T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Semantic/Ecran</link>
          <guid isPermaLink="true">https://www.jtips.info/Semantic/Ecran</guid>
        </item>
      
    
      
        <item>
          <title>Semantic/Topbraid</title>
          <description>
Installation de Topraid Composer


La procédure décrite ici concerne la version d&#8217;évaluation.


Sous Windows

Un exécutable  d&#8217;installation est fourni sur le site de la société TopQuadrant



Sous Linux

Aucun fichier d&#8217;installation n&#8217;est fourni. Il faut donc faire une installation manuelle. Ceci n&#8217;est pas très compliqué puique Topraid Composer est un plug-in d&#8217;eclipse.


Il faut donc installer Eclipse (3.4, pour mon cas).


Ensuite, via l'Update Manager d&#8217;Eclipse, on installe le plug-in : menu Help / Softare Update&#8230;&#8203;, puis onglet Available Software, et ajout du site http://www.topbraidcomposer.org/update. N&#8217;ayant pas encore étudié le détail des fonctionnalités, j&#8217;ai cocher la case TopBraid Composer Maestro Edition, qui est la version la plus complète.





Démarrage


Le travail s&#8217;effectue dans un projet simple (menu New Project, puis General / Project), dans la perspective Topraid.


</description>
          <pubDate>2008-10-07T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Semantic/Topbraid</link>
          <guid isPermaLink="true">https://www.jtips.info/Semantic/Topbraid</guid>
        </item>
      
    
      
        <item>
          <title>Isolation des transactions</title>
          <description>
L&#8217;isolation des transactions est une fonctionnalité des bases de données paramétrable depuis JDBC, et donc depuis les frameworks de mapping O/R comme Hibernate, Toplink ou JPA.


Le standard SQL définit 4 niveaux d&#8217;isolation, permettant de choisir lesquels des 3 risques de lecture (sale, non reproductible ou fantôme) doivent être évités.


Risques encourus


La lecture sale (dirty read) est le risque le plus grave, puisque dans c&#8217;est le cas où il est possible de lire des données mises à jour par une transaction concurrente, mais non validées.
Si elles n&#8217;ont pas encore été validées, cela signifie peut-être que la transaction concurrente est en train de mettre à jour d&#8217;autres informations, nécessaire pour atteindre une bonne cohérence.
Dirty Read signifie donc incohérence potentielle.
De plus, en cas de rollback, ces informations risquent de disparaître dans un délai réduit.


La lecture non reproductible est le risque de lire des données différentes si on exécute deux fois la même requête de sélection dans une même requête.
La différence de résultat est due à une transaction concurrente qui aurait modifié des informations sur des lignes sélectionnées, et les aurait validées (commit).


La lecture fantôme est le risque de travailler sur un ensemble qui a évolué (lignes en plus ou en moins), à cause d&#8217;une transaction concurrente.




Modes d&#8217;isolation


Le mode d&#8217;isolation Uncommited Read est le plus laxiste, puisqu&#8217;il n&#8217;empêche aucun des risques cités !
On l&#8217;utilise rarement, à cause du risque d&#8217;incohérence sur les données.


Le mode Commited Read est le mode par défaut de la plupart des bases de données.
Du fait qu&#8217;il nous prémunisse contre le risque de Dirty Read, ce mode est souvent considéré comme un bon compromis pour les performances.


En mode Repeatable Read, on ne peut pas lire des données qui ont été modifiées mais pas encore validées par d&#8217;autres transactions, et on ne peut pas modifier les données lues par une transaction active.


Serializable est le mode le plus strict, car il assure une total cohérence des données au sein d&#8217;un transaction, et une complète isolation des transactions entre elles.
Dans ce mode, deux transactions qui agissent sur les mêmes tables ne peuvent pas être exécutées de façon concurrente.
C&#8217;est donc aussi un mode très pénalisant pour les performances&#8230;&#8203;
Il représentait le mode par défaut dans d&#8217;anciennes versions de bases ;
on parlait aussi de verrouillage au niveau table, par opposition au niveau ligne du Commited Read.


Ces quatre modes sont définis par la norme SQL.
Certaines bases peuvent en proposer d&#8217;autres qui ne seront pas supportés par JDBC.




Modes et bases de données




Uncommited Read : SqlServer


Commited Read : Oracle, PostgeSQL, SqlServer ; c&#8217;est le mode par défaut pour toutes ces bases.


Repeatable Read : SqlServer


Serializable : Oracle, PostgeSQL, SqlServer






Isolation avec JDBC


La modification du niveau d&#8217;isolation se fait sur l&#8217;objet de connexion.



 Class.forName("com.mysql.jdbc.Driver")
 Connection connexion = DriverManager.getConnection("jdbc:mysql://localhost/test", "root", "");
 connexion.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
 ...



L&#8217;interface java.sql.Connection fournit 5 constantes, pour choisir entre les 4 modes.
Il y en a une par mode, plus TRANSACTION_NONE, qui désactive les transactions.


La modification du mode d&#8217;isolation se fait généralement au moment de la connexion, ou, en tout cas, lorsqu&#8217;aucune transaction n&#8217;est en cours pour cette connexion.




Isolation avec JPA


Avec JPA, dans le cas où on n&#8217;utilise pas de datasource, le fichier persistence.xml définit les paramètres de connexion à la base de données.



&lt;persistence&gt;
  &lt;persistence-unit name="librairie" transaction-type="RESOURCE_LOCAL"&gt;
     ...
    &lt;properties&gt;
      &lt;property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"/&gt;
      &lt;property name="hibernate.connection.url" value="jdbc:mysql://localhost/mabase"/&gt;
      &lt;property name="hibernate.connection.username" value="root"/&gt;
      &lt;property name="hibernate.connection.password" value=""/&gt;

      &lt;property name="hibernate.connection.isolation" value="2"/&gt;
    &lt;/properties&gt;
  &lt;/persistence-unit&gt;
&lt;/persistence&gt;



Pour spécifier correctement le mode d&#8217;isolation, on doit connaître les valeurs des constantes JDBC :




TRANSACTION_NONE = 0


TRANSACTION_READ_COMMITTED = 2


TRANSACTION_READ_UNCOMMITTED = 1


TRANSACTION_REPEATABLE_READ = 4


TRANSACTION_SERIALIZABLE = 8




Dans le cas où on utilise une datasource, c&#8217;est dans son paramétrage, au niveau du serveur d&#8217;application, que cette information doit être définie.




Isolation avec Hibernate


La configuration d&#8217;isolation avec Hibernate se fait dans le fichier hibernate.cfg.xml, si on n&#8217;utilise pas de datasource.



&lt;hibernate-configuration&gt;
  &lt;session-factory&gt;
    &lt;property name="hibernate.connection.driver_class"&gt;com.mysql.jdbc.Driver&lt;/property&gt;
    &lt;property name="hibernate.connection.url"&gt;jdbc:mysql://localhost/mabase&lt;/property&gt;
    &lt;property name="hibernate.connection.username"&gt;root&lt;/property&gt;
    &lt;property name="hibernate.connection.password"&gt;&lt;/property&gt;
    &lt;property name="hibernate.dialect"&gt;org.hibernate.dialect.MySQLDialect&lt;/property&gt;

    &lt;property name="hibernate.connection.isolation"&gt;2&lt;/property&gt;
     ...
  &lt;/session-factory&gt;
&lt;/hibernate-configuration&gt;



Les valeurs pour la propriété d&#8217;isolation sont les mêmes que pour JPA.




Références


Je trouve que la doc de Microsoft TransactSQL est assez bonne sur le sujet.
On peut aussi se référer à la JavaDoc de l&#8217;interface Connection.


</description>
          <pubDate>2008-10-05T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/IsolationTransaction</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/IsolationTransaction</guid>
        </item>
      
    
      
        <item>
          <title>Glassfish/Datasource</title>
          <description>
Pour installer une datasource dans un serveur Glassfish 2, il faut d&#8217;abord installer le driver de la base de données, créer le pool de connexion, puis déclarer ce pool comme une ressource JNDI. Nous voyons cette procédure pour le cas d&#8217;une base MySql, mais elle s&#8217;applique à toute autre base.


Driver


Il suffit de placer le fichier d&#8217;archive jar du driver (mysql-connector-java-5.x.x-bin.jar, pour MySql) dans le répertoire bin de Glassfish.




Pool de connexion


Le pool de connexion doit être créé via la console d&#8217;administration, à l&#8217;adresse http://localhost:4848/.
Cette adresse est valable pour un glassfish local, installé avec une configuration par défaut.


Les paramètres d&#8217;authentification par défaut sont :




login = admin


password = adminadmin




Créer le pool

Dans le menu, à gauche, on ouvre les tâches "Resources" puis "JDBC" et, enfin, "Connection Pools". A ce niveau, on clique sur le bouton "New" pour arriver dans l&#8217;écran de saisie des informations générales du pool (étape 1/2). Pour mon exemple, je saisis les informations suivantes :




Name: LibrairiePool


Resource Type: javax.sql.Datasource


Database Vendor: MySql




Le bouton "Next" nous envoie vers la deuxième étape, qui est l&#8217;écran des informations détaillées. Cet écran est constitué de plusieurs zones :




General Settings: rappel des informations de la première étape et spécification de la classe de Datasource.


Pool Settings: paramétrage du pool, avec les tailles mini et maxi


Connection Validation: activation des fonctionnalité de fail-over sur les connexions


Transaction: paramétrage de la gestion transactionnelle, en particulier de l&#8217;isolation


Additional Properties: paramètre de connexion, dont


url=jdbc:mysql://localhost:3306/librairie


user=root


password=rootpwd




Remarques :




Le pool ne supporte pas que le mot de passe soit vide.


Ces propriétés ne sont pas celles proposées par défaut.




Après avoir saisi ces données, on peut cliquer sur "Finish" pour retourner à la liste des pools.



Tester le pool

A partir de la liste des pools, on peut revenir aux informations détaillées d&#8217;un pool. En haut de cet écran, un bouton "Ping" permet de tester la connexion.





Ressource JNDI


Pour le rendre accessible, il faut déclarer une ressource JNDI branchée sur le pool nouvellement créé. Cela se fait aussi dans la console d&#8217;administration, en passant par le menu "Resources" / "JDBC" / "JDBC Resources"




JNDI Name: jdbc/LibrairieDS


Pool Name: sélectionner "LibrairiePool"


Description: bla bla


Status: cocher "Enabled"




Puis on clique sur OK et la datasource est prête à l&#8217;emploi.


</description>
          <pubDate>2008-09-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Glassfish/Datasource</link>
          <guid isPermaLink="true">https://www.jtips.info/Glassfish/Datasource</guid>
        </item>
      
    
      
        <item>
          <title>Intégration Java / PHP</title>
          <description>
Quelles sont les solutions pour mettre en place une intégration entre PHP et Java ?


Ces notes ont servi de base pour la rédaction d&#8217;un article paru le numéro 6/2008 de PHP Solutions de novembre 2008.


Objectif d&#8217;intégration


Pourquoi vouloir intégrer Java et PHP ?


Intégrer un progiciel

Pour intégrer un progiciel écrit en PHP dans un environnement Java, il faut développer des modules dans un langage, capables d&#8217;interagir avec l&#8217;autre langage.



Fusionner deux systèmes d&#8217;information

Lors de la fusion de 2 sociétés, un système d&#8217;information peut être choisi au dépend de l&#8217;autre, ou les deux systèmes peuvent être fusionnés.
Dans ce cas, il est peut probable que les langages choisis initialement soit les mêmes.
Il faut donc mettre en place des modules d&#8217;intégration capable de s&#8217;adresser aux logiciels, quels que soient leurs langages.



Application multi-langages

Les progrès des techniques d&#8217;interopérabilité permettent aujourd&#8217;hui de concevoir des architectures multi-langages dont l&#8217;objectif n&#8217;est plus d&#8217;intégrer un existant, mais d&#8217;exploiter chaque langage pour ce à quoi il est le plus apte :




PHP pour les pages Web


Java pour les couches métier (avec Spring ou EJB) et pour la persistence (avec JPA ou Hibernate)












Solutions


WebService : SOAP et REST

Les solutions à base de Services Web sont adaptées à la plupart des langages modernes : PHP, Java, C# et .NET, C++,&#8230;&#8203;
Elles sont souvent utilisées dans les architectures orientée services (SOA).


SOAP est le protocole standard pour ce type de solution.
REST est une altenative moins standard, mais plus simple.








Pont Java-PHP

La communication entre Java et PHP s&#8217;appuie partiellement sur les mêmes techniques que pour les Web Services : HTTP et XML.
Par contre, les flux XML sont plus compacts et une partie des communications passe par des sockets de plus bas niveau.


Il existe deux solutions sur le marché :




celle de Zend Technologies, vendue sous licence commerciale avec le Zend Platform ES,


PHP/Java Bridge, une solution open source, sous licence LGPL, hébergée sur la plate-forme SourceForge.net










Intégration in-process

Plutôt que d&#8217;établir un pont entre deux processus, l&#8217;intégration in-process permet d&#8217;embarquer du PHP dans une machine virtuelle Java.
En fait, le moteur PHP traditionnel est remplacé par un moteur écrit en Java.


Il existe deux solutions sur le marché :




Caucho Quercus est un moteur PHP5 développé en Java et distribué sous licence GPL.


link:Project Zero, le projet open source initié par IBM, et qui sert de base aux développements de WebSphere sMash, propose des fonctionnalités du même type.












Quel choix ?




Services : environnements multi-langages, avec des besoins d&#8217;intégration importants et architectures orientées services


Pont : appels simples de classes Java depuis des pages PHP, dans le cadre de développements d&#8217;applications mixtes ou de modules d&#8217;intégration en PHP


In-process : applications mixtes PHP / Java, mais manque de recul sur la solution




</description>
          <pubDate>2008-09-05T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/PHP/IntegrationJava</link>
          <guid isPermaLink="true">https://www.jtips.info/PHP/IntegrationJava</guid>
        </item>
      
    
      
        <item>
          <title>Java / PHP Bridge</title>
          <description>
Le PHP/Java Bridge de sourceforge.net permet d&#8217;intégrer Java et PHP dans les deux sens : il permet d&#8217;appeler des classes Java depuis PHP, mais aussi d&#8217;appeler des scripts PHP depuis Java. Pour ce sens, le bridge fournit même deux techniques : appel en CGI, ou via la nouvelle technique d&#8217;appel de script de Java 6, référencée sous la JSR-223. On notera que le pont peut aussi fonctionner entre PHP et .NET ou Mono.


Ces notes ont servi de base pour la rédaction d&#8217;un article paru le numéro 6/2008 de PHP Solutions de novembre 2008.


Installation


L&#8217;environnement que je décris ci-dessous n&#8217;est qu&#8217;un exemple d&#8217;environnement compatible avec la version 5.2 du pont. Il a été testé sous Linux Ubuntu 8.04 et sous Window XP SP2 et SP3.


Environnement PHP :




Apache Web Server 2.2


PHP 5.2


allow_url_include = On dans php.ini




Environnement Java :




Java JDK 6


Apache Tomcat 6.0




Ensuite, il faut déployer JavaBridge.war dans Tomcat. Tomcat étant par défaut en mode de déploiement automatique, le déploiement se fait par copie du fichier dans son répertoire webapps/.




Intégration par CGI


On ne peut pas réellement parler d&#8217;intégration. Cette technique permet surtout de déployer des pages PHP dans Tomcat, sans avoir recours à Apache ou IIS.









Java Scripting API


La Java Scripting API, parfois connue sous le nom de sa spécification, JSR-223, définit une API permettant d&#8217;utiliser des langages de script dans une application Java. Le langage JavaScript est nativement supporté par la machine virtuelle qui intègre l&#8217;interpréteur Rhino. Le pont peut servir d&#8217;interpreteur PHP pour cette API.


Il fournit cinq modes d&#8217;intégration :




php simple, qui exécute simplement un script,





   manager.getEngineByName("php")





php interactif, qui exécute une série de lignes de code et qui permet de récupérer un résultat dans Java,





   manager.getEngineByName("php-interactive")





php invocable, qui permet d&#8217;appeler une fonction PHP et de récupérer son retour,





   manager.getEngineByName("php-invocable")





un mode Web, qui permet de partager un contexte et des informations de session,


un mode Web invocable, qui fait la synthèse de deux derniers (c&#8217;est mon préféré).





 // Affichage du titre
 out.println ("Essai de JSR-223 - mode Web invocable
");
 // Récupération de l'interpréteur de PHP via la méthode de fabrique
 ScriptEngine engine = EngineFactory.getInvocablePhpScriptEngine (this, application, request, response);
 // Connexion des sorties Java et PHP
 engine.getContext ().setWriter (out);
 // Déclaration de la fonction hello
 engine.eval ("&lt;?php function hello ($who) {return 'Hello '.$who;}; ?&gt;");
 // Appel de la fonction hello et affichage du résultat
 out.println (((Invocable) engine).invokeFunction ("hello", new Object[] {"world"}));
 // Réinitialisation de l'interpréteur
 engine.eval ((java.io.Reader)null);





Pont PHP vers Java


Appeler une classe standard


 &lt;?php
   // Fichier des fonctions et classes PHP du pont
   require_once ("http://localhost:8080/JavaBridge/java/Java.inc");

   // Instanciation de la classe Java StringBuilder via la classe PHP Java
   $buf=new Java ("java.lang.StringBuilder");
   // Appel de la méthode append sur l'objet Java StringBuilder
   $buf-&gt;append ("Hello");
   $buf-&gt;append (" ");
   $buf-&gt;append ("World");
   // Appel de la méthode toString sur l'objet Java StringBuilder et affichage du résultat
   echo ($buf-&gt;toString ());
 ?&gt;



A chaque appel de la page, une requête est envoyée vers Tomcat, à la servlet PhpJavaServlet de l&#8217;application JavaBridge, puis les échanges se font ensuite sur un port spécifique (entre 9267 et 9367)








Appeler une classe personnalisée

Pour que PHP puisse utiliser des classes métier, il faut en plus que l&#8217;application puisse avoir accès aux archives jar qui contiennent leurs définitions. L&#8217;accès peut être donné côté PHP, par la fonction java_autoload, ou côté Java. Si une seule archive est utilisée, la première solution est plus pratique, et si plusieurs archives sont nécessaires, ce qui est le cas le plus fréquent, il est plus pratique de gérer leur mise à disposition du côté de Java.



 &lt;?php
   require_once ("http://localhost:8080/JavaBridge/java/Java.inc");
   // Chargement de l'archive java copiée localement
   java_autoload ("java/university.jar") ;

   // Instanciation d'un objet Course
   $course=new Java ("fr.sewatech.university.model.Course", "mm-uml", "Analyse et conception avec UML");
   // Affichage du résultat
   echo ($course-&gt;toString ());
 ?&gt;




Appeler un bean Spring

En effet, depuis plusieurs années, de nombreuses applications Java ont été développées sur ce type de framework. Que ce soit Spring ou ses concurrents, ils ont pour point commun de gérer des composants, en les instanciant et en les mettant à disposition via des noms logiques plutôt que par des noms de classes.


Pour appeler des services métier intégrés à Spring, notre application PHP doit d&#8217;abord accéder au contexte Spring avant d&#8217;accéder aux beans.



 &lt;?php
   require_once("http://localhost:8080/UniBridge/java/Java.inc");

   // Instanciation du contexte applicatif de Spring paramétré dans le fichier application-context.xml
   $ctx = new Java(
       "org.springframework.context.support.ClassPathXmlApplicationContext",
       "application-context.xml");
   // Demande d'une référence au bean courseService inscrit dans Spring
   $service = $ctx-&gt;getBean("courseService");
   // Utilisation du bean
   $course=$service-&gt;findById(1);
   echo ($course-&gt;toString());
 ?&gt;



Il faut toutefois faire attention à la gestion des librairies dépendantes. En effet, lorsque nous utilisons des composants métier intégrés à Spring, nous devons accéder aux archives métier et à des archives techniques (spring.jar, commons-beanutils.jar, commons-collections.jar,&#8230;&#8203;). Nous avons trois possibilités pour rendre celles-ci accessibles :




les ajouter à la fonction java_autoload, ce qui peut être fastidieux,


les ajouter au répertoire lib/ de Tomcat, ce qui les rend accessibles à toutes les applications déployées, mais peut provoquer des conflits avec des versions utilisées dans d&#8217;autres applications,


les ajouter au répertoire WEB-INF/lib de l&#8217;application JavaBridge, ce qui constitue la meilleure option si une seule application Java est utilisée depuis PHP.




Pour éviter les risques de conflit lorsque plusieurs applications Java sont utilisées, on peut mettre en place une quatrième technique, en intégrant le contenu de JavaBridge à chaque application Java. Chacune embarque donc son propre pont ; de plus, toutes les classes Java nécessaires à PHP sont déjà chargées par le pont, ce qui nous dispense d&#8217;appeler la fonction java_autoload. Seul le require_once doit être adapté pour charger celui de l&#8217;application Java accédée. Cette technique a été utilisée dans l&#8217;exemple précédent, ce qui explique l&#8217;absence de la fonction  java_autoload.


Pour ajouter le pont à une application, il faut :




ajouter les fichiers JavaBridge.jar et php-servlet.jar, dans le répertoire WEB_INF/lib de l&#8217;application,


ajouter le répertoire java et son contenu à la racine de l&#8217;application,


prendre, dans le fichier WEB-INF/web.xml, les deux portions qui concernent la servlet PhpJavaServlet (éléments &lt;servlet&gt; et &lt;servlet-mapping&gt;) et les ajouter dans le fichier équivalent de notre application.





</description>
          <pubDate>2008-09-05T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/PHP/Bridge</link>
          <guid isPermaLink="true">https://www.jtips.info/PHP/Bridge</guid>
        </item>
      
    
      
        <item>
          <title>Transactions avec Hibernate et Spring</title>
          <description>
L&#8217;architecture classique d&#8217;une application java ou java EE est organisée en trois couches :




La couche de présentation qui est en charge de l&#8217;affichage, de recevoir la saisie des informations et de la cinématique des écrans.


La couche de services qui contient les traitements métier


La couche d&#8217;accès aux données, ou DAO, qui contient toutes les requêtes JDBC ou la manipulation des sessions hibernate.




Dans une implémentation propre de cette architecture, ces couches sont strictement disjointes.
Une des clés de ce type d&#8217;architecture est la gestion des transactions : elle doit être implémentée au niveau services, sans pour autant tenir compte de la technologie utilisée au sein de la couche DAO (Hibernate, JPA, JDBC,&#8230;&#8203;).





Ce "Tip" présente les façons traditionnelles de gérer les transactions avec Spring et Hibernate, mais aussi un technique alternative, adaptée au projets en cours d&#8217;intégration de Spring.
Le cas des transactions JTA est laissé de coté.
Il a été testé avec Spring 2.5, JUnit 4.4 et Hibernate 3.2, sous Eclipse 3.3.


Transactions Hibernate


Le démarrage et la manipulation des transactions se fait par l&#8217;intermédiaire de l&#8217;objet de session Hibernate.
Plusieurs requêtes peuvent donc participer à une même transaction si elles utilisent la même session.


Partage de session

Pour utiliser un session partagée, les classes de DAO doivent utiliser le même objet de SessionFactory, en le mettant par exemple en champs statique d&#8217;une classe utilitaire.



 public class HibernateUtil {
   private static SessionFactory sessionFactory;

   public static SessionFactory getSessionFactory() {
     if (sessionFactory == null)
       throw new IllegalStateException("Le sessionFactory n'a pas été initialisé.");
     return sessionFactory;
   }

   public static void initialize() {
     Configuration config = new Configuration();
     ...
     sessionFactory = config.buildSessionFactory();
   }
 }



La session est ensuite récupérée par la méthode getCurrentSession().



   Session currentSession = HibernateUtil.getSessionFactory().getCurrentSession();



Dans ce fonctionnement, Hibernate cherche à stocker la session dans le thread courant.
Pour permettre ce fonctionnement, la configuration (fichier hibernate.cfg.xml) doit inclure la propriété suivante :



 &lt;hibernate-configuration&gt;
   ...
   &lt;property name="hibernate.transaction.factory_class"&gt;
     org.hibernate.transaction.JDBCTransactionFactory
   &lt;/property&gt;
   &lt;property name="hibernate.current_session_context_class"&gt;
     thread
   &lt;/property&gt;
 &lt;/hibernate-configuration&gt;









La propriété hibernate.transaction.factory_class n&#8217;est pas obligatoire, car nous l&#8217;avons renseignée ici avec sa valeur par défaut.






Démarrage et validation de transaction

L&#8217;objet de session fournit des méthodes pour démarrer une transaction et pour récupérer une transaction en cours :



   // Démarrer une nouvelle transaction
   Transaction transaction = currentSession.beginTransaction()

   // Récupérer la transaction courante
   Transaction transaction = currentSession.getTransaction()



La validation ou l&#8217;annulation de la transaction se fait sur l&#8217;objet Transaction.



   // Validation de la transaction
   transaction.commit();

   // Annulation de la transaction
   transaction.rollback();



Dans ce mode de fonctionnement, la validation de la transaction (commit) déclenche le flush et ferme la session courante.
La portion de code suivante provoquera donc une exception :



   // Récupérer la session courante
   Session currentSession = HibernateUtil.getSessionFactory().getCurrentSession();
   // Récupérer la transaction courante
   Transaction transaction = currentSession.getTransaction()
   // Validation de la transaction
   transaction.commit();
   // Clôture de la session
   session.close();



Cette portion de code déclenche une exception org.hibernate.SessionException avec le message "Session was already closed".



Intégration dans l&#8217;architecture

Dans la technique présentée ici, on constate que toute la gestion des transactions passe par la manipulation de classes spécifiques à Hibernate.
Hors ceci est en contradiction avec le principe d&#8217;isolation des couches :
la couche de service ne devrait pas connaître la technique de persistance utilisée par la couche DAO.


Il est donc nécessaire d&#8217;ajouter dans notre architecture des couches d&#8217;abstraction pour mieux gérer les transactions.
Ces couches peuvent être fournis par les fonctionnalités d&#8217;inversion de contrôle (ou IoC) et d&#8217;aspects (AOP) de certains frameworks classiques comme Spring.





Intégration Spring / Hibernate


Dans le schéma présenté en introduction, les composants de DAO sont intégrés à Spring, ce qui peut faciliter la gestion de la session et des transactions.


Bean de SessionFactory

La façon traditionnelle d&#8217;intégrer des services Hibernate dans Spring est de déclarer un bean de type SessionFactory, par lequel passera toute création de session Hibernate.
Ce bean remplace totalement le fichier hibernate.cfg.xml.



 &lt;bean id="sessionFactory"
   class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"&gt;
   &lt;property name="dataSource" ref="dataSource" /&gt;
   &lt;property name="hibernateProperties"&gt;
     &lt;props&gt;
       &lt;prop key="hibernate.dialect"&gt;
         org.hibernate.dialect.HSQLDialect
       &lt;/prop&gt;
     &lt;/props&gt;
   &lt;/property&gt;
   &lt;property name="annotatedClasses"&gt;
     &lt;list&gt;
       &lt;value&gt;fr.sewatech.data.university.CourseData&lt;/value&gt;
     &lt;/list&gt;
   &lt;/property&gt;
 &lt;/bean&gt;




Bean de TransactionManager

Pour que Spring puisse gérer des transactions Hibernate, on ajoute un bean spécialisé dans cette tâche.



   &lt;bean id="txManager"
         class="org.springframework.orm.hibernate3.HibernateTransactionManager"&gt;
     &lt;property name="sessionFactory" ref="sessionFactory" /&gt;
   &lt;/bean&gt;




Transactions par programmation

En accédant au TransactionManager, il est possible de démarrer et de conclure des transactions depuis la classe de test.
Ces transactions engloberons naturellement les requêtes soumises à la base par le composant DAO.



 @Service("courseService")
 public class CourseServiceImpl implements CourseService {
   @Autowired // Injection  du transactionManager
   private PlatformTransactionManager transactionManager;

   public void justeFaisLe(CourseData course) {
     // Ouverture de la transaction, avec les paramètres par défaut (PROPAGATION_REQUIRED)
     TransactionStatus status = transactionManager.getTransaction(null);
     try {
       ...
       // Validation de la transaction
       transactionManager.commit(status);
     } catch (Exception e) {
       transactionManager.rollback(status);
     }
   }
   ...
 }




Transactions par AOP

C&#8217;est probablement ma technique préférée, car elle ne nécessite aucune modification dans le code source.
Son principal défaut est qu&#8217;elle se base sur des conventions de développement relativement fortes, sur le nommage des classes, packages et méthodes.


La partie &lt;aop:config&gt; paramètre les classes qui seront prises en compte et la partie &lt;tx:advice&gt; précise la statégie de transaction en fonction des méthodes.



 &lt;beans ...&gt;
   &lt;bean id="txManager"
         class="org.springframework.orm.hibernate3.HibernateTransactionManager"&gt;
     &lt;property name="sessionFactory" ref="sessionFactory" /&gt;
   &lt;/bean&gt;

   &lt;tx:advice id="serviceTxAdvice" transaction-manager="txManager"&gt;
     &lt;tx:attributes&gt;
       &lt;tx:method name="find*" propagation="REQUIRED" read-only="true" /&gt;
       &lt;tx:method name="*" propagation="REQUIRED" /&gt;
     &lt;/tx:attributes&gt;
   &lt;/tx:advice&gt;

   &lt;aop:config&gt;
     &lt;aop:pointcut id="serviceMethods"
         expression="execution(* fr.sewatech.university.service.Service.(..))" /&gt;
     &lt;aop:advisor advice-ref="serviceTxAdvice" pointcut-ref="serviceMethods" /&gt;
   &lt;/aop:config&gt;
 &lt;/beans&gt;









Dans la partie &lt;aop:config&gt;, l&#8217;expression peut désigner les classes ou les interfaces des services.






Transactions par annotation

Depuis Spring 2.0, avec la JVM 5, des annotations de transaction sont disponibles.
Un article traite déjà des annotations avec Spring 2.0 et Spring 2.5, et en particulier des annotations de transaction.





Techniques alternatives


Dans certains projets, les techniques traditionnelles ne peuvent pas être implémentées facilement à cause d&#8217;un historique sans Spring et d&#8217;erreurs de programmation antérieures.


Configuration hibernate.cfg.xml

Dans l&#8217;exemple de configuration précédente, le bean de SessionFactory remplace totalement le fichier de configuration hibernate.cfg.xml.
Dans le cadre d&#8217;une intégration progressive de Spring, il peut être intéressant de brancher le SessionFactory sur un fichier hibernate.cfg.xml existant :



	&lt;bean id="sessionFactory"
		class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"&gt;
		&lt;property name="configLocation"&gt;
			&lt;value&gt;hibernate.cfg.xml&lt;/value&gt;
		&lt;/property&gt;
	&lt;/bean&gt;



Le problème de cette technique est que la gestion automatique des transactions par Spring ne fonctionne plus.
Un contournement est de modifier le fichier de configuration d&#8217;Hibernate :



 &lt;hibernate-configuration&gt;
 	&lt;session-factory&gt;
     ...
 		&lt;property name="hibernate.current_session_context_class"&gt;
 			org.springframework.orm.hibernate3.SpringSessionContext
 		&lt;/property&gt;
     ...
 	&lt;/session-factory&gt;
 &lt;/hibernate-configuration&gt;



Un nouveau problème apparaît alors : Les sessions et transactions ne fonctionnent plus hors de Spring !
Ce qui est très gênant pour une intégration progressive.


Pour contourner ce nouveau problème, il faut initialiser la synchronisation entre Spring et les threads :



 if (! TransactionSynchronizationManager.isSynchronizationActive())
   TransactionSynchronizationManager.initSynchronization();



L&#8217;initialisation doit être faite une seule fois par thread, sous peine d&#8217;une exception de type java.lang.IllegalStateException, avec le message "Cannot activate transaction synchronization - already active".
C&#8217;est pour éviter celà que je fais un test préalable.


Grâce à cette technique, les mêmes classes de DAO peuvent être utilisées par des services dans Spring, utilisant donc le TransactionManager de Spring, et par des services externes à Spring, manipulant directement des sessions et transactions via les classes d&#8217;Hibernate.








Cette technique n&#8217;est généralement pas préconisée ; elle est utile dans une période de transition.






</description>
          <pubDate>2008-07-16T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Hibernate/Spring-Transactions</link>
          <guid isPermaLink="true">https://www.jtips.info/Hibernate/Spring-Transactions</guid>
        </item>
      
    
      
        <item>
          <title>Tests automatisés avec JUnit et Spring</title>
          <description>
Dans une architecture raisonnable, la couche de services qui contient les traitements métier et la couche d&#8217;accès aux données, ou DAO[1], qui contient toutes les requêtes JDBC[2] ou la manipulation des sessions hibernate, sont strictement disjoints.


Dans cette architecture, les transactions sont gérées par la couche de services.
Avec Spring Framework, ces transactions peuvent être gérées de façon automatique, à l&#8217;aide d&#8217;annotations ou par programmation, avec ou sans Spring Boot.







Test des beans de services


Dans cette architecture, tester les beans de service est la tâche la plus importante.
Pour ça, la classe de test doit pouvoir accéder au bean dans son contexte Spring.
Mais ça ne pose pas vraiment de problème puisqu&#8217;aucun prérequis n&#8217;est exigé, du point de vue transactionnel.


Initialisation par programmation

La classe de test peut initialiser elle-même le contexte Spring, et récupérer le bean à tester, dans une méthode annotée @BeforeAll.



@TestInstance(Lifecycle.PER_CLASS)
class ProductServiceTest {

  ProductService service;

  @BeforeAll
  void init() {
    ClassPathXmlApplicationContext context
            = new ClassPathXmlApplicationContext("application-config.xml");
    service = context.getBean(ProductService.class);
  }

   ...
 }



La suite du test se déroule de façon traditionnelle : une méthode de test, annotée par @Test, par méthode à tester, ou à peu près&#8230;&#8203;



public class CourseServiceTest {
  ...

  @Test
  void getAll_should_work() {
    // GIVeN

    // WHeN
    List&lt;Product&gt; products = service.getAll();

    // THeN
    assertThat(products).isEmpty();
  }

  ...
 }




Initialisation par annotation

Plutôt que de gérer les initialisations manuellement, il est possible de demander à Spring d&#8217;injecter les beans nécessaires.


La première étape est l&#8217;annotation de la classe pour que le test transite par Spring, avec le bon fichier de contexte.



@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations={"/application-config.xml"})
public class ProductServiceTest {
  ...
}



La seconde opération est d&#8217;injecter le bean à tester, avec l&#8217;annotation @Autowired ou avec l&#8217;annotation @Resource.



  @Autowired
  ProductService service;



La fin est identique à la technique traditionnelle : développer les méthodes de test.



Initialisation par annotation avec Spring Boot

Si vous développez avec Spring Boot, l&#8217;extension JUnit peut être remplacée par l&#8217;annotation @SpringBootTest.
Le reste du test reste identique.



@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ProductServiceBootTest {

  @Autowired
  ProductService service;

  ...
}






Test des beans de DAO


Le test des beans de DAO pose un problème supplémentaire : celui des transactions.
En effet, lorsqu&#8217;une méthode de DAO est appelée, un transaction doit être démarrée, puis doit être validée ou annulée après l&#8217;appel de la méthode.


Cette gestion de transaction doit donc être prise en charge par la classe de test.


Transactions par programmation

En accédant au bean TransactionManager, il est possible de démarrer et de conclure des transactions depuis la classe de test.
Ces transactions engloberont naturellement les requêtes soumises à la base par le composant DAO.



@TestInstance(Lifecycle.PER_CLASS)
public class ProductDaoTest {

  PlatformTransactionManager transactionManager;
  ProductDao productDao;

  @BeforeAll
  void initAll() throws Exception {
    ...
    transactionManager = context.getBean(PlatformTransactionManager.class);
  }

  @Test
  void findByTitle_should_work() {
    // GIVeN

    // WHeN
    TransactionStatus status = transactionManager.getTransaction(null);
    List&lt;Product&gt; products = productDao.findByTitleLike("TEST");
    transactionManager.commit(status);

    // THeN
    ...
  }

  ...
}



J&#8217;aime bien améliorer la lisibilité de cette forme en créant une méthode utilitaire runInTransaction(&#8230;&#8203;), avec une expression lambda en paramètre.



@TestInstance(Lifecycle.PER_CLASS)
public class ProductDaoTest {

  ...

  &lt;T&gt; T runInTransaction(Supplier&lt;T&gt; supplier) {
    TransactionStatus status = transactionManager.getTransaction(null);
    T result = supplier.get();
    transactionManager.commit(status);
    return result;
  }

  @Test
  void findByTitle_should_work() {
    // GIVeN

    // WHeN
    List&lt;Product&gt; products = runInTransaction(
      () -&gt; productDao.findByTitleLike("TEST")
    );

    // THeN
    ...
  }

  ...
}



Cette technique de manipulation de transaction fonctionnait déjà avec Spring 1.x !
Pour la méthode runInTransaction(&#8230;&#8203;), il a fallu attendre le JDK 8.



Transactions par annotation

Pour que la classe de test puisse démarrer et valider les transactions, il faut ajouter l&#8217;annotation @Transactional.



@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations={"/application-config.xml"})
@Transactional
public class ProductDaoTest {
  ...
}



Les tests peuvent se faire comme pour une classe de service : injection du bean (@Autowired ou @Resource) puis méthodes de test.


Cette technique de manipulation de transaction a été une des innovations de Spring 2.5, qui a été utilisée pour la rédaction initiale de cette page en 2008.
Elle reste compatible avec Spring Boot.



@SpringBootTest
@Transactional
class ProductDaoBootTest {
  ...
}










Lorsqu&#8217;une méthode de test est transactionnelle, les méthodes @BeforeEach et @AfterEach s&#8217;exécutent dans la même transaction.
Pour avoir l&#8217;équivalent en dehors de la transaction, il faut utiliser les annotations spécifiques à Spring @BeforeTransaction et @AfterTransaction.


Les méthodes @BeforeAll et @AfterAll s&#8217;exécutent en dehors de la transaction.









Références




Exemples de code, avec Spring 5.3, Spring Boot 2.6 et JUnit 5.8
(Version initiale rédigée et testée avec Spring 2.5, JUnit 4.4 et Hibernate 3.2)








1. Data Access Object


2. Java DataBase Connectivity

</description>
          <pubDate>2008-07-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/Test</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/Test</guid>
        </item>
      
    
      
        <item>
          <title>Flex/DataGrid</title>
          <description>
Les DataGrid permettent de représenter une liste de données dans plusieurs colonnes. Par défaut, une data grid permet de sélectionner une ligne (avec surlignage de la ligne sélectionnée), d&#8217;effectuer des opérations de tri sur chaque colonne (click sur le header), de redimensionner les colonnes.


Premier exemple


Une collection d&#8217;objets est fournie à la data grid via la propriété dataProvider. Chaque colonne est décrite en indiquant d&#8217;une part le texte du header,  et d&#8217;autre part la propriété de l&#8217;objet model à afficher.



&lt;mx:DataGrid dataProvider="{_acProduits}"&gt;
  &lt;mx:columns&gt;
    &lt;mx:DataGridColumn headerText="Code" dataField="code"/&gt;
    &lt;mx:DataGridColumn headerText="Libelle" dataField="libelle"/&gt;
    &lt;mx:DataGridColumn headerText="Prix" dataField="prix"/&gt;
  &lt;/mx:columns&gt;
&lt;/mx:DataGrid&gt;





Sélectionner une colonne


Il s&#8217;agit de redéfinir les comportements par défaut de la data grid de façon à pouvoir sélectionner une colonne lorsque l&#8217;on clique sur le header ou sur une cellule. A cette fin, il faut définir les propriétés suivantes sur la data grid :




selectable="false" de façon à éliminer le surlignage des lignes quand elles sont sélectionnées


useRollOver="false" de façon à éliminer le surlignage des lignes quand elles sont survolées par le curseur


headerRelease="colonneHandler1(event)" pour intercepter le clique sur le header d&#8217;une colonne


itemClick="colonneHandler2(event)" pour intercepter le clique sur une cellule





&lt;mx:DataGrid id="myTable" dataProvider="{_acProduits}" useRollOver="false" selectable="false"
             headerRelease="myHeaderHandler(event)" itemClick="myItemHandler(event)"&gt;
  &lt;mx:columns&gt;
    &lt;mx:DataGridColumn headerText="Code" dataField="code"/&gt;
    &lt;mx:DataGridColumn headerText="Libelle" dataField="libelle"/&gt;
    &lt;mx:DataGridColumn headerText="Prix" dataField="prix"/&gt;
  &lt;/mx:columns&gt;
&lt;/mx:DataGrid&gt;





Interception de l&#8217;évènement clic sur cellule




La couleur de fond de chaque colonne est affectée en fonction de la sélection.



private function myItemHandler(event:ListEvent):void {
  // colonne sélectionnée =&gt; gris
  for (var i:int = 0; i &lt; myTable.columns.length; i++) {
    var column:DataGridColumn = myTable.columns[i];
    if (i == event.columnIndex) {
      column.setStyle("backgroundColor", "#AAAAAA");
    } else {
      column.setStyle("backgroundColor", "#FFFFFF");
    }
  }
}





Interception de l&#8217;évènement clic sur header




La couleur de fond de chaque colonne est affectée en fonction de la sélection. Il faut aussi prendre soin de ne pas désactiver la fonction de tri au niveau des colonnes (sortable="false"), sinon l&#8217;évènement n&#8217;est pas généré. En revanche, pour empêcher le tri après emission de l&#8217;évènement,  il faut invoquer la méthode preventDefault() sur l&#8217;évènement.



private function myHeaderHandler(event:DataGridEvent):void {
  event.preventDefault();// pour éviter de déclencher le tri
  selectItem(event.columnIndex);
}



</description>
          <pubDate>2008-06-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Flex/DataGrid</link>
          <guid isPermaLink="true">https://www.jtips.info/Flex/DataGrid</guid>
        </item>
      
    
      
        <item>
          <title>Flex/AdvancedDataGrid</title>
          <description>
L&#8217;AdvancedDataGrid étend les possibilités du composant standard DataGrid, afin d&#8217;améliorer la visualisation des données. Les caractéristiques de ce composant sont les suivantes :




Tri multi-colonnes


Fonction de style sur les lignes et les colonnes


Affichage des données de façon hierarchique ou groupée


Groupe de colonnes


ItemRenderer avancé




Premier exemple


L&#8217;AdvancedDataGrid s&#8217;utilise comme un DataGrid. La différence se voit essentiellement à l&#8217;affichage, notamment au niveau du header avec une zone spécifique pour le tri. L&#8217;exemple ci-dessous affiche des produits (libellé en première colonne) et leur catégorie associée (code et libellé respectivement en deuxième et troisième colonne).



&lt;mx:AdvancedDataGrid designViewDataType="flat" dataProvider="{_acProduits}"&gt;
  &lt;mx:columns&gt;
    &lt;mx:AdvancedDataGridColumn headerText="Produit" dataField="libelle"/&gt;
    &lt;mx:AdvancedDataGridColumn headerText="Code catégorie" dataField="code"/&gt;
    &lt;mx:AdvancedDataGridColumn headerText="Libellé catégorie" dataField="libelle"/&gt;
  &lt;/mx:columns&gt;
&lt;/mx:AdvancedDataGrid&gt;





Colonnes groupées


L&#8217;idée des colonnes groupées consiste à créer un header commun pour des champs qui possèdent un lien entre eux. Reprenons ainsi l&#8217;exemple précédent de façon à grouper les colonnes relatives à la catégorie (notez que l&#8217;on définit maintenant les colonnes avec l&#8217;élément groupedColumns et non plus columns) :



&lt;mx:AdvancedDataGrid designViewDataType="flat" dataProvider="{_acData}"&gt;
  &lt;mx:groupedColumns&gt;
    &lt;mx:AdvancedDataGridColumn headerText="Produit" dataField="libelle"/&gt;
    &lt;mx:AdvancedDataGridColumnGroup headerText="Catégorie"&gt;
      &lt;mx:AdvancedDataGridColumn headerText="Code" dataField="code"/&gt;
      &lt;mx:AdvancedDataGridColumn headerText="Libellé" dataField="libelle"/&gt;
    &lt;/mx:AdvancedDataGridColumnGroup&gt;
  &lt;/mx:groupedColumns&gt;
&lt;/mx:AdvancedDataGrid&gt;





Supprimer la zone d&#8217;affichage du tri dans le header


L&#8217;AdvancedDataGrid utilise un header renderer par défaut qui affiche une zone pour le tri. Si on souhaite une table sans tri, cette zone devient gênante car elle prend de la place inutilement dans le header. Pour supprimer la zone, il suffit de redéfinir le header renderer de la table : pour cela, il faut d&#8217;une part créer un composant renderer (dans notre exemple un label), et d&#8217;autre part positionner l&#8217;attribut headerRenderer=classe_du_composant au niveau de la data grid.




Code du header renderer





public class SimpleHeaderRenderer extends Label
{
  public function SimpleHeaderRenderer()
  {
    setStyle('fontWeight', 'bold');
    setStyle('textAlign', 'center');
  }
}





Code d&#8217;affichage de la data grid





&lt;mx:AdvancedDataGrid designViewDataType="flat" dataProvider="{_acData}" headerRenderer="SimpleHeaderRenderer" sortableColumns="false"&gt;
  &lt;mx:groupedColumns&gt;
    &lt;mx:AdvancedDataGridColumn headerText="Produit" dataField="libelle"/&gt;
    &lt;mx:AdvancedDataGridColumnGroup headerText="Catégorie"&gt;
      &lt;mx:AdvancedDataGridColumn headerText="Code" dataField="code"/&gt;
      &lt;mx:AdvancedDataGridColumn headerText="Libellé" dataField="libelle"/&gt;
    &lt;/mx:AdvancedDataGridColumnGroup&gt;
  &lt;/mx:groupedColumns&gt;
&lt;/mx:AdvancedDataGrid&gt;



</description>
          <pubDate>2008-06-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Flex/AdvancedDataGrid</link>
          <guid isPermaLink="true">https://www.jtips.info/Flex/AdvancedDataGrid</guid>
        </item>
      
    
      
        <item>
          <title>Contextualisation de traces Log4J avec le MDC</title>
          <description>
Log4J propose un mécanisme de contextualisation des traces par le biais de NDC (Nested Diagnostic Context) et du MDC (Mapped Diagnostic Context). Ces 2 mécanismes se basent sur le même principe : on ajoute des informations, via des méthodes statiques ; ces informations étant ratachées au ThreadLocal. Le NDC empile les informations dans un stack, alors que MDC les enregistre sous forme de Map.


Sortie des informations


Les informations stockées dans le NDC peuvent être ajoutées dans des traces de type texte (console, fichier) grâce au caractère de conversion %x. Toutes les informations du NDC sont sorties en même temps. Pour le MDC, la caractère de conversion %X doit être associé à la clé d&#8217;une information stockée.



 &lt;param name="ConversionPattern" value="%d %-5p [%c] (%x) %m%n"/&gt;



ou



 &lt;param name="ConversionPattern" value="%d %-5p [%c] (%X{RemoteAddr} - %X{RequestURL}) %m%n"/&gt;





Enregistrement des informations


Dans une application Web, l&#8217;endroit le plus approprié pour enregistrer des informations dans le NDC ou le MDC est généralement un filtre. Par exemple, on peut déployer un filtre qui enregistre des informations sur la requête, comme l&#8217;URL de la requête, l&#8217;adresse IP du client ou le nom utilisé pour l&#8217;authentification.


Dans l&#8217;exemple ci-dessous, j&#8217;ai utilisé un MDC, qui me semble plus pratique et, surtout, plus modulaire. J&#8217;y ai enregistré les informations citées ci-dessus, ainsi que des paramètres d&#8217;initialisation présents dans le fichier web.xml.



 package fr.sewatech.util.web;

 import java.io.IOException;
 import java.util.;
 import javax.servlet.;
 import org.apache.commons.beanutils.PropertyUtils;
 import org.apache.commons.logging.;
 import org.apache.log4j.MDC;

 /
  * Filtre d'initialisation pour Log4J ; ajoute des informations dans le MDC
  * @author alexis
  *
  */
 public class Log4jFilter implements Filter {
 	private static Log logger = LogFactory.getLog(Log4jFilter.class);

 	private FilterConfig config;

 	/
 	 * Initialisation du filtre
 	 */
 	public void init(FilterConfig config) throws ServletException {
 		logger.debug("Création du filtre Log4jFilter");
 		this.config = config;
 	}

 	/
 	 * DEstruction du filtre
 	 */
 	public void destroy() {
 		logger.debug("Destruction du filtre Log4jFilter");
 	}

 	/
 	 * Execution du filtre ; enregistre les informations prevues
 	 */
 	public void doFilter(ServletRequest request, ServletResponse response,
 			FilterChain chain) throws IOException, ServletException {
 		try {
 			// Paramètres d'initialisation du filtre
 			Enumeration&lt;String&gt; initParameterNames = config.getInitParameterNames();
 			while (initParameterNames.hasMoreElements()) {
 				String name = (String) initParameterNames.nextElement();
 				MDC.put(name, config.getInitParameter(name));
 			}

 			putProperty(request, "RemoteAddr");
 			putProperty(request, "Locale");
 			putProperty(request, "PathInfo");
 			putProperty(request, "RequestURL");
 			putProperty(request, "ServletPath");
 			putProperty(request, "UserPrincipal");
 			putProperty(((HttpServletRequest) request).getSession(false), "SessionID");

 			chain.doFilter(request, response);
 		} finally {
 			Set&lt;String&gt; propertyNames = MDC.getContext().keySet();
 			for (String name : propertyNames) {
 				//MDC.remove(name);
 			}
 		}
 	}

 	/*
 	 * Enregistre une propriete d'un objet dans le MDC ; cet objet peut typiquement etre la requete ou la session
 	 * @param object
 	 * @param propertyName
 	 */
 	private void putProperty(Object object, String propertyName) {
 		try {
 			if (object != null) {
 				String name = propertyName.substring(0,1).toLowerCase() + propertyName.substring(1);
 				MDC.put(propertyName, PropertyUtils.getProperty(object, name));
 			}
 		} catch (Exception e) {
 		}
 	}
 }



Ce filtre doit être paramétré dans le fichier WEB-INF/web.xml de l&#8217;application.



 &lt;filter&gt;
   &lt;filter-name&gt;Log4jFilter&lt;/filter-name&gt;
   &lt;filter-class&gt;fr.sewatech.util.web.Log4jFilter&lt;/filter-class&gt;
   &lt;init-param&gt;
     &lt;param-name&gt;Application&lt;/param-name&gt;
     &lt;param-value&gt;Hello&lt;/param-value&gt;
   &lt;/init-param&gt;
 &lt;/filter&gt;
 &lt;filter-mapping&gt;
   &lt;filter-name&gt;Log4jFilter&lt;/filter-name&gt;
   &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
 &lt;/filter-mapping&gt;





Résultat


En associant ce filtre et la configuration du PatternLayout, les traces sortent avec les informations suivantes :



 01:38:48,328 INFO  [fr.sewatech.hello.web.PageFilter] (127.0.0.1 - Hello) Appel de /hi
 01:38:48,328 INFO  [fr.sewatech.hello.web.HelloServlet] (127.0.0.1 - Hello) HelloServlet.doGet()
 01:38:49,187 INFO  [fr.sewatech.hello.web.PageFilter] (127.0.0.1 - Hello) Appel de /hello-count.jsp



</description>
          <pubDate>2008-06-03T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Log4J/MDC</link>
          <guid isPermaLink="true">https://www.jtips.info/Log4J/MDC</guid>
        </item>
      
    
      
        <item>
          <title>JSF/ValueChangeListener</title>
          <description>
Les value change listeners sont invoqués en fin de phase PROCESS VALIDATION, si la local value du composant à l&#8217;origine de l&#8217;évènement a été modifiée depuis la dernière requête HTTP. Ces listeners sont souvent difficiles à exploiter, car les champs d&#8217;instance des backing beans sont mis à jour plus tard, pendant la phase UPDATE MODEL VALUES (invocation des setters dans les backing beans) : si le but de notre value change listener était de mettre à jour un champ du backing bean, ce dernier est alors écrasé. L&#8217;astuce suivante permet de relancer l&#8217;évènement value change event après l&#8217;invocation des setters des backing beans :



public void myValueChangeListener(ValueChangeEvent event) {
  PhaseId phaseId = event.getPhaseId();
  if (phaseId.equals(PhaseId.ANY_PHASE)) {
     event.setPhaseId(PhaseId.UPDATE_MODEL_VALUES);
     event.queue();
  } else if (phaseId.equals(PhaseId.UPDATE_MODEL_VALUES)) {
     // le champ d'instance myValue est mis à jour par
     // le listener et ne sera pas écrasé plus tard par
     // invocation du setter
     this.myValue = event.getNewValue();
  }
}

</description>
          <pubDate>2008-06-03T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JSF/ValueChangeListener</link>
          <guid isPermaLink="true">https://www.jtips.info/JSF/ValueChangeListener</guid>
        </item>
      
    
      
        <item>
          <title>Convertion PDF avec OOo et UNO</title>
          <description>
Il existe plusieurs types d&#8217;outils qui permettent de transformer un document Word ou OpenOffice en PDF :




OpenOffice : cette fonctionnalité est supportée de façon standard dans OpenOffice.


Imprimante PDF : ces outils s&#8217;intallent dans Windows ou Linux comme des imprimantes et génèrent du PDF à partir de n&#8217;importe quel document imprimable ; PdfCreator est le plus connu, pour Windows


Convertisseurs en ligne : certains sites proposent des services de conversion, mais il faut, pour les utiliser, uploader le document !




Toutes ces procédures sont manuelles, alors que mon but était de lancer les conversions en ligne de commande ou depuis une application java. En me basant sur la documentation et les exemples d&#8217;OpenOffice, j&#8217;ai développé cette classe qui utilise l&#8217;API UNO :



 package fr.sewatech.sewatoool.pdf;

 import java.io.File;
 import java.net.MalformedURLException;

 import com.sun.star.beans.PropertyValue;
 import com.sun.star.comp.helper.Bootstrap;
 import com.sun.star.frame.XComponentLoader;
 import com.sun.star.frame.XStorable;
 import com.sun.star.io.IOException;
 import com.sun.star.lang.IllegalArgumentException;
 import com.sun.star.lang.XMultiComponentFactory;
 import com.sun.star.uno.UnoRuntime;
 import com.sun.star.uno.XComponentContext;
 import com.sun.star.uri.ExternalUriReferenceTranslator;

 public class PdfConverter {

   public static void main(String[] args) {
     try {
       new PdfConverter().convert("D:\\Temp\\tmp.doc");
     } catch (Exception e) {
       e.printStackTrace();
     } finally {
       System.exit(0);
     }
   }

   private XComponentContext xContext;
   private Object component;
   private Object desktop;

   /
    * Convertit un fichier en PDF
    *
    * @param filename
    * @throws Exception
    */
   public void convert(String filename) throws Exception {
     startOpenOffice();
     String unoUrl = loadDocument(filename);
     generatePdf(unoUrl);
   }

   private void generatePdf(String unoUrl) throws IOException {
     XStorable xStorable = (XStorable) UnoRuntime.queryInterface(
     XStorable.class, component);

     PropertyValue[] convertProperties = new PropertyValue[2];
     convertProperties[0] = new PropertyValue();
     convertProperties[0].Name = "Overwrite";
     convertProperties[0].Value = true;

     convertProperties[1] = new PropertyValue();
     convertProperties[1].Name = "FilterName";
     convertProperties[1].Value = "writer_pdf_Export";

     xStorable.storeToURL(unoUrl.substring(0, unoUrl.lastIndexOf('.')) + ".pdf", convertProperties);
   }

   /
    * Chargement d'un fichier OpenOffice
    *
    * @param filename
    * @return
    * @throws MalformedURLException
    * @throws IOException
    * @throws IllegalArgumentException
    */
   private String loadDocument(String filename) throws MalformedURLException, IOException, IllegalArgumentException {
     XComponentLoader loader = (XComponentLoader) UnoRuntime.queryInterface(XComponentLoader.class, desktop);

     String unoUrl = formatUnoUrl(filename);

     // Value=true =&gt; pas d'interface graphique
     PropertyValue[] loadProperties = new PropertyValue[1];
     loadProperties[0] = new PropertyValue();
     loadProperties[0].Name = "Hidden";
     loadProperties[0].Value = true;

     component = loader.loadComponentFromURL(unoUrl, "_blank", 0,loadProperties);
     return unoUrl;
   }

   /
    * Démarrage d'OpenOffice
    *
    * @return Instance d'OOo avec contexte
    * @throws Exception
    */
   private Object startOpenOffice() throws Exception {
     xContext = Bootstrap.bootstrap();
     XMultiComponentFactory xMCF = xContext.getServiceManager();

     desktop = xMCF.createInstanceWithContext("com.sun.star.frame.Desktop", xContext);
     return desktop;
   }

   /
    * Formatte un chemin traditionnel en chemin compatible UNO
    *
    * @param filename Chemin du fichier, au format traditionnel
    * @return Chemin du fichier, au format UNO
    * @throws MalformedURLException
    */
   private String formatUnoUrl(String filename) throws MalformedURLException {
     String unoUrl = ExternalUriReferenceTranslator.create(xContext)
                                                   .translateToInternal(new File(filename).toURL().toExternalForm());
     return unoUrl;
   }
 }



Cette classe reste rudimentaire, mais est déjà fonctionnelle. Elle peut être intégrée dans une application ou, moyennant une petite adaptation du main, utilisée en ligne de commande.
</description>
          <pubDate>2008-05-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/OpenOffice/ConvertPDF</link>
          <guid isPermaLink="true">https://www.jtips.info/OpenOffice/ConvertPDF</guid>
        </item>
      
    
      
        <item>
          <title>Conversion de date</title>
          <description>
Pour formater une date en chaîne de caractère ou pour convertir une chaîne de caractère en date, on utilise la classe java.time.format.DateTimeFormatter, depuis le JDK 8.
Avec les JDK précédents, on utilisait java.text.DateFormat ou sa sous-classe java.text.SimpleDateFormat.


DateTimeFormatter.parse()


En choisissant, et imposant, le format de saisie de date, le code de parsing est simple.



public class DateFormatUtil {

  private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")

  public static TemporalAccessor parse(String dateSaisie) {
    if (dateSaisie != null) {
      TemporalAccessor date = formatter.parse(dateSaisie);
      return date;
    }
    return null;
  }
}





SimpleDateFormat.parse()


Dans une application, pour pouvoir faire les conversions, il est nécessaire d&#8217;imposer un format de saisie, qui correspond au pattern du SimpleDateFormat.



public class DateFormatUtil {

  private String dateFormat = "dd/MM/yyyy"

  public static Date parse(String dateSaisie) throws ParseException {
    if (dateSaisie != null) {
      SimpleDateFormat formatter = new SimpleDateFormat(dateFormat);
      Date date = formatter.parse(dateSaisie);
      return date;
    }
    return null;
  }
}









Contrairement à java.time.format.DateTimeFormatter, java.text.SimpleDateFormat n&#8217;est pas thread-safe.
C&#8217;est pourquoi je l&#8217;utilise en variable locale.







Multi-formats


Afin de rendre l&#8217;application plus conviviale, il faut donner une plus grande souplesse de saisie à l&#8217;utilisateur et supporter une série de patterns.
Avant d&#8217;en appliquer un, il faut rechercher celui qui est adapté à la saisie, ce qui peut se faire avec des expressions régulières.



public class DateFormatUtil {

  private static Map&lt;String , DateTimeFormatter&gt; regexToDatePattern
            = Map.of(
                  "(\\d{1})/(\\d{1})/(\\d{2})", DateTimeFormatter.ofPattern("d/M/yy"),
                  "(\\d{2})/(\\d{2})/(\\d{4})", DateTimeFormatter.ofPattern("dd/MM/yyyy"),
                  "(\\d{2})/(\\d{2})/(\\d{2})", DateTimeFormatter.ofPattern("dd/MM/yy"),
                  "(\\d{1})/(\\d{2})/(\\d{2})", DateTimeFormatter.ofPattern("d/MM/yy"),
                  "(\\d{2})/(\\d{1})/(\\d{2})", DateTimeFormatter.ofPattern("dd/M/yy"),
                  "(\\d{1})/(\\d{1})/(\\d{4})", DateTimeFormatter.ofPattern("d/M/yyyy"),
                  "(\\d{2})/(\\d{1})/(\\d{4})", DateTimeFormatter.ofPattern("dd/M/yyyy"),
                  "(\\d{1})/(\\d{2})/(\\d{4})", DateTimeFormatter.ofPattern("d/MM/yyyy")
              );

  public static TemporalAccessor parse(String dateSaisie) throws ParseException{
    if (dateSaisie != null) {
      return regexToDatePattern.entrySet().stream()
                    .filter(entry -&gt; dateSaisie.matches(entry.getKey()))
                    .findAny()
                    .map(entry -&gt; entry.getValue().parse(dateSaisie))
                    .orElse(null);

    }
    return null;
  }
}



Avant le JDK 8, on stockait le pattern dans la map, pour instancier un formatter à chaque besoin.



public class DateFormatUtil {

  private static Map&lt;String , String&gt; regexToDatePattern = new HashMap&lt;String, String&gt;();

  static {
    regexToDatePattern.put("(\\d{1})/(\\d{1})/(\\d{2})", "d/M/yy");
    regexToDatePattern.put("(\\d{2})/(\\d{2})/(\\d{4})", "dd/MM/yyyy");
    regexToDatePattern.put("(\\d{2})/(\\d{2})/(\\d{2})", "dd/MM/yy");
    regexToDatePattern.put("(\\d{1})/(\\d{2})/(\\d{2})", "d/MM/yy");
    regexToDatePattern.put("(\\d{2})/(\\d{1})/(\\d{2})", "dd/M/yy");
    regexToDatePattern.put("(\\d{1})/(\\d{1})/(\\d{4})", "d/M/yyyy");
    regexToDatePattern.put("(\\d{2})/(\\d{1})/(\\d{4})", "dd/M/yyyy");
    regexToDatePattern.put("(\\d{1})/(\\d{2})/(\\d{4})", "d/MM/yyyy");
  }

  public static Date parse(String dateSaisie) throws ParseException{
    if (dateSaisie != null) {
      for (Entry&lt;String, String&gt; entry : regexToDatePattern.entrySet()) {
        if (dateSaisie.matches(entry.getKey())) {
          SimpleDateFormat fmt = new SimpleDateFormat(entry.getValue());
          Date d = fmt.parse(dateSaisie);
          return d;
        }
      }
    }
    return null;
  }
}





Références




Oracle - Parsing and Formatting




</description>
          <pubDate>2008-05-06T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/DateFormat</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/DateFormat</guid>
        </item>
      
    
      
        <item>
          <title>Java avec OpenOffice sous Linux</title>
          <description>
Il y a quelques temps, j&#8217;ai développé un petit programme java qui génère une table des matières dans une présentation OOo Impress. Tout fonctionnait bien sous Windows, mais sur mon poste Ubuntu, j&#8217;avais systématiquement le message d&#8217;exception suivant :



com.sun.star.comp.helper.BootstrapException: no office executable found!
       at com.sun.star.comp.helper.Bootstrap.bootstrap(Bootstrap.java:253)
       at fr.sewatech.sewatoool.impress.helper.ImpressHelper.&lt;init&gt;(ImpressHelper.java:55)
       ... 1 more



Pourtant OpenOffice était bien installé sur la machine, et les jars du classpath étaient bien ceux du répertoire d&#8217;installation d&#8217;OOo. Et le problème se posait que je lance mon application depuis Eclipse ou en ligne de commande.


Ligne de commande


Pour information, voici le script que je lançais :



OFFICE_HOME="/usr/lib/openoffice/program/"
CLASSPATH="sewatoool.jar:$OFFICE_HOME/classes/juh.jar:$OFFICE_HOME/classes/jurt.jar:$OFFICE_HOME/classes/ridl.jar:$OFFICE_HOME/classes/unoil.jar"
java -cp $CLASSPATH fr.sewatech.sewatoool.impress.DocumentService toc



J&#8217;ai beaucoup cherché sur la toile, sans trouver de solution adaptée à mon cas. Cependant, en testant divers solutions, j&#8217;ai constaté qu&#8217;il fallait que je rajoute le répertoire program d&#8217;OOo dans le classpath. En fait, tout sous-répertoire de program ou tout fichier inclus dans un sous-repertoire, qui ne soit pas un jar valide, peut être ajouté.


Le script suivant fonctionne :



OFFICE_HOME="/usr/lib/openoffice/program/"
CLASSPATH="sewatoool.jar:$OFFICE_HOME/classes/juh.jar:$OFFICE_HOME/classes/jurt.jar:$OFFICE_HOME/classes/ridl.jar:$OFFICE_HOME/classes/unoil.jar:$OFFICE_HOME"
java -cp $CLASSPATH fr.sewatech.sewatoool.impress.DocumentService toc



Restait encore à résoudre le problème dans Eclipse.




Dans Eclipse


Pour mes développements, jai créé une "user library" qui rassemble les 4 jars nécessaires. Mais comme ces librairies ne peuvent pas contenir d'"external folder", j&#8217;ai du contourner le problème.


Ma première solution (ou plutôt bidouille !) consistait à créer un fichier jar bidon dans program/classes et à l&#8217;ajouter dans la "user library".


La deuxième solution, probablement meilleure, consiste à ajouter le répertoire program en tant qu'"external folder" dans la configuration de lancement de l&#8217;application. Il faut utiliser le bouton "Advanced" pour cela.







Cette solution a été testée avec Ubuntu 7.10, OpenOffice 2.3 et Eclipse 3.3.


La documentation de développement OpenOffice peut être trouvé sur leur wiki.




Avec OpenOffice 3


Dans la version 3 de OpenOffice, la structure de fichiers a un peu changé.



OFFICE_HOME="/usr/lib/openoffice"
CLASSPATH="sewatoool.jar:$OFFICE_HOME/URE/java/juh.jar:$OFFICE_HOME/URE/java/jurt.jar:$OFFICE_HOME/URE/java/ridl.jar:$OFFICE_HOME/Basis/program/classes/unoil.jar:$OFFICE_HOME/Basis/program"
java -cp $CLASSPATH fr.sewatech.sewatoool.impress.DocumentService toc



</description>
          <pubDate>2008-03-28T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/OpenOffice/Linux</link>
          <guid isPermaLink="true">https://www.jtips.info/OpenOffice/Linux</guid>
        </item>
      
    
      
        <item>
          <title>PMD RuleSet</title>
          <description>
PMD doit être utilisé avec un jeu de règles personnalisé, qui permet de concentrer l&#8217;analyse sur les règles les plus pertinentes.
Il est aussi possible, au lancement de PMD, de limiter l&#8217;analyse à une priorité minimale.


Pour mes audits, j&#8217;utilise généralement la ligne de commande suivante :



./pmd.sh ~/workspace/myproject/src csv sewatech-pmd.xml -reportfile sewatech.csv -minimumpriority 3



Cette commande génère un rapport au format CSV, avec la caractère ',' comme séparateur de champs.


J&#8217;utilise un jeu de règle de base (fichier sewatech-pmd.xml), valable pour PMD 4.2, que j&#8217;adapte en fonction des premières constatations :



 &lt;?xml version="1.0"?&gt;
 &lt;ruleset name="auditcode"
     xmlns="link:http://pmd.sf.net/ruleset/1.0.0[http://pmd.sf.net/ruleset/1.0.0]"
     xmlns:xsi="link:http://www.w3.org/2001/XMLSchema-instance[http://www.w3.org/2001/XMLSchema-instance]"
     xsi:schemaLocation="link:http://pmd.sf.net/ruleset/1.0.0[http://pmd.sf.net/ruleset/1.0.0] link:http://pmd.sf.net/ruleset_xml_schema.xsd[http://pmd.sf.net/ruleset_xml_schema.xsd]"
     xsi:noNamespaceSchemaLocation="link:http://pmd.sf.net/ruleset_xml_schema.xsd[http://pmd.sf.net/ruleset_xml_schema.xsd]"&gt;
   &lt;rule ref="rulesets/basic.xml"&gt;
    &lt;exclude name="UselessOverridingMethod"/&gt;
   &lt;/rule&gt;

   &lt;rule ref="rulesets/design.xml"&gt;
    &lt;exclude name="CloseResource"/&gt;
    &lt;exclude name="ImmutableField"/&gt;
    &lt;exclude name="UseSingleton"/&gt;
    &lt;exclude name="SimpleDateFormatNeedsLocale"/&gt;
    &lt;exclude name="UncommentedEmptyConstructor"/&gt;
    &lt;exclude name="UseLocaleWithCaseConversions"/&gt;
   &lt;/rule&gt;

   &lt;rule ref="rulesets/strings.xml"&gt;
    &lt;exclude name="AvoidDuplicateLiterals"/&gt;
   &lt;/rule&gt;

   &lt;rule ref="rulesets/naming.xml/LongVariable"&gt;
     &lt;properties&gt;
       &lt;property name="minimum" value="30"/&gt;
     &lt;/properties&gt;
   &lt;/rule&gt;
   &lt;rule ref="rulesets/naming.xml"&gt;
    &lt;exclude name="AbstractNaming"/&gt;
    &lt;exclude name="LongVariable"/&gt;
   &lt;/rule&gt;

   &lt;rule ref="rulesets/braces.xml" /&gt;
   &lt;rule ref="rulesets/clone.xml" &gt;
    &lt;exclude name="CloneThrowsCloneNotSupportedException"/&gt;
   &lt;/rule&gt;

   &lt;rule ref="rulesets/codesize.xml/ExcessivePublicCount"
          message="This class has a bunch of public methods and attributes (&gt;100)"&gt;
    &lt;properties&gt;
      &lt;property name="minimum" value="100"/&gt;
    &lt;/properties&gt;
   &lt;/rule&gt;
   &lt;rule ref="rulesets/codesize.xml/TooManyFields"
         message="Too many fields (&gt;30)"&gt;
    &lt;properties&gt;
      &lt;property name="minimum" value="30"/&gt;
    &lt;/properties&gt;
   &lt;/rule&gt;
   &lt;rule ref="rulesets/codesize.xml"&gt;
    &lt;exclude name="CyclomaticComplexity"/&gt;
    &lt;exclude name="ExcessivePublicCount"/&gt;
    &lt;exclude name="TooManyFields"/&gt;
    &lt;exclude name="NcssMethodCount"/&gt;
    &lt;exclude name="NcssTypeCount"/&gt;
    &lt;exclude name="NcssConstructorCount"/&gt;
   &lt;/rule&gt;

   &lt;rule ref="rulesets/optimizations.xml" &gt;
    &lt;exclude name="AvoidInstantiatingObjectsInLoops"/&gt;
    &lt;exclude name="LocalVariableCouldBeFinal"/&gt;
    &lt;exclude name="MethodArgumentCouldBeFinal"/&gt;
   &lt;/rule&gt;

   &lt;rule ref="rulesets/coupling.xml" &gt;
    &lt;exclude name="ExcessiveImports"/&gt;
   &lt;/rule&gt;

   &lt;rule ref="rulesets/finalizers.xml" /&gt;
   &lt;rule ref="rulesets/imports.xml" /&gt;
   &lt;rule ref="rulesets/javabeans.xml"&gt;
    &lt;exclude name="BeanMembersShouldSerialize"/&gt;
   &lt;/rule&gt;

   &lt;rule ref="rulesets/strictexception.xml" /&gt;

   &lt;rule ref="rulesets/sunsecure.xml"&gt;
    &lt;exclude name="ArrayIsStoredDirectly"/&gt;
    &lt;exclude name="MethodReturnsInternalArray"/&gt;
   &lt;/rule&gt;

   &lt;rule ref="rulesets/unusedcode.xml"&gt;
    &lt;exclude name="UnusedFormalParameter"/&gt;
   &lt;/rule&gt;

   &lt;rule ref="rulesets/logging-jakarta-commons.xml" /&gt;
   &lt;rule ref="rulesets/logging-java.xml" /&gt;

 &lt;/ruleset&gt;



J&#8217;utilise aussi PMD intégré à Eclipse via le plug-in pmd-eclipse. Dans ce cas, j&#8217;utilise un autre jeu de règles(fichier .pmd.xml), plus léger, avec uniquement des règles de priorité 1 et 2. Comme le plug-in utilise une version plus ancienne de PMD (pmd-eclipse 1.8 &#8658; PMD 3.4), je dois désactiver certaines règles.



 &lt;?xml version="1.0"?&gt;
 &lt;ruleset name="auditcode" xmlns="link:http://pmd.sf.net/ruleset/1.0.0[http://pmd.sf.net/ruleset/1.0.0]"
     xmlns:xsi="link:http://www.w3.org/2001/XMLSchema-instance[http://www.w3.org/2001/XMLSchema-instance]"
     xsi:schemaLocation="link:http://pmd.sf.net/ruleset/1.0.0[http://pmd.sf.net/ruleset/1.0.0] link:http://pmd.sf.net/ruleset_xml_schema.xsd[http://pmd.sf.net/ruleset_xml_schema.xsd]"
     xsi:noNamespaceSchemaLocation="link:http://pmd.sf.net/ruleset_xml_schema.xsd[http://pmd.sf.net/ruleset_xml_schema.xsd]"&gt;

   &lt;rule ref="rulesets/basic.xml/BooleanInstantiation" /&gt;
   &lt;rule ref="rulesets/basic.xml/DoubleCheckedLocking" /&gt;

   &lt;rule ref="rulesets/design.xml/AvoidReassigningParameters" /&gt;
   &lt;rule ref="rulesets/design.xml/ConstructorCallsOverridableMethod" /&gt;
   &lt;rule ref="rulesets/design.xml/EqualsNull" /&gt;
   &lt;rule ref="rulesets/design.xml/AbstractClassWithoutAbstractMethod" /&gt;

   &lt;rule ref="rulesets/strings.xml/StringInstantiation" /&gt;

   &lt;rule ref="rulesets/naming.xml/MethodNamingConventions" /&gt;
   &lt;rule ref="rulesets/naming.xml/ClassNamingConventions" /&gt;
   &lt;rule ref="rulesets/naming.xml/VariableNamingConventions" /&gt;
   &lt;rule ref="rulesets/naming.xml/SuspiciousEqualsMethodName" /&gt;

   &lt;rule ref="rulesets/strictexception.xml/AvoidThrowingRawExceptionTypes" /&gt;
   &lt;rule ref="rulesets/strictexception.xml/AvoidThrowingNullPointerException" /&gt;

   &lt;rule ref="rulesets/logging-java.xml/LoggerIsNotStaticFinal" /&gt;
   &lt;rule ref="rulesets/logging-java.xml/MoreThanOneLogger" /&gt;
   &lt;rule ref="rulesets/logging-java.xml/SystemPrintln" /&gt;

 &lt;/ruleset&gt;

</description>
          <pubDate>2008-03-26T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Qualite/PMD-RuleSet</link>
          <guid isPermaLink="true">https://www.jtips.info/Qualite/PMD-RuleSet</guid>
        </item>
      
    
      
        <item>
          <title>RichFaces/ScrollableDataTable</title>
          <description>
Rédaction inachevée&#8230;&#8203;


JBoss RichFaces 3.1.4


Description


Le composant graphique ScrollableDataTable est le tableau de données le plus complet de la librairie RichFaces.
Il permet d&#8217;afficher des données




Truc et astuces


Activer le tri par colonne

Pour que le tri fonctionne, il faut que la colonne ait un id, et que celui-ci corresponde à l&#8217;attribut de données.


Dans l&#8217;exemple ci-dessous, id="code" correspond à value="#{course.code}".



&lt;rich:column id="code" width="60px"&gt;
  &lt;f:facet name="header"&gt;
    &lt;h:outputText value="Code" /&gt;
  &lt;/f:facet&gt;
  &lt;h:outputText value="#{course.code}" /&gt;
&lt;/rich:column&gt;



Je n&#8217;ai pas testé le cas de données avec une profondeur plus importante.



Gérer une sélection


while(iterator.hasNext()) {
  Object key = iterator.next();
  table.setRowKey(key);
  if (table.isRowAvailable()) {
    Object o = table.getRowData();
  }
}




</description>
          <pubDate>2008-02-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JSF/RichFaces-ScrollableDataTable</link>
          <guid isPermaLink="true">https://www.jtips.info/JSF/RichFaces-ScrollableDataTable</guid>
        </item>
      
    
      
        <item>
          <title>Calculs avec virgules flottantes</title>
          <description>
L&#8217;article sur les calculs flottants souligne les limites des types float, quel que soit le langage utilisé.
La conclusion de cette démonstration est de garder des marges de manoeuvre conséquentes par rapport aux types utilisés.
Elle souligne aussi l&#8217;intérêt d&#8217;utiliser des types double plutôt que float.


Rappel Java


Pour manipuler des valeurs numériques, avec décimale, java nous propose les types float et double.
Le type float permet de gérer des valeurs entre -3.40x1038 et 3.40x1038, avec une valeur absolue minimale de 1.17x10-38.
Le type double est plus volumineux, puisqu&#8217;il prend en compte les nombres entre -1.80x10308 et 1.80x10308, avec une valeur absolue minimale de 2.22x10-308.


Le réflexe habituel est de se contenter de float lorsqu&#8217;on est dans la fourchette supportée, ce qui est le cas le plus courant, avec pour objectif louable d&#8217;économiser de la mémoire.
Ce réflexe va à l&#8217;encontre de la simplicité avec java puisque pour que le compilateur interprète un nombre à décimales comme un float, il faut le suffixer par f, sinon il sera considéré comme un double.



 float monNombre = 1.2;  // Ne compile pas car 1.2 est un double
 float monNombre = 1.2f; // Compile car 1.2f est un float





Calculs avec les float


Le risque qu&#8217;on court en essayant d&#8217;économiser de la mémoire est d&#8217;obtenir des résultats eronnés pour cause d&#8217;arrondis. Les erreurs de calculs peuvent être relativement importantes, et pour des valeurs bien inférieures au limites théoriques.


La classe de test unitaire suivante, exécutée dans jUnit 3.8, fonctionne sans failure :



 import junit.framework.TestCase;

 public class AdditionTest extends TestCase {
   public void testPlus() {
     float operande1 = 16777216;
     assertTrue(operande1 + 1.0f == operande1);
     assertTrue(++operande1 == operande1);
   }
 }



Dans cet exemple, additionner 1 à nombre, ou incrémenter ce nombre, est sans effet !!!


Si on retire le f en suffixe de 1.0, celui-ci devient un double et le calcul précédent donne un résultat plus conforme aux attentes. La valeur 16777216 n&#8217;est pas choisie au hasard puisque toutes les valeurs supérieures à celles-ci reproduisent l&#8217;anomalie.


Un exemple de calcul divergent peut être montré avec des multiplications :



 public void testFois() {
   float x = (3.10f * 2.30f) * 1.5f;
   float y = 3.10f * (2.30f * 1.5f);
   System.out.println( x );  // 10.695
   System.out.println( y );  // 10.694999
   assertTrue(x == y);
 }



L&#8217;assertion échoue ; l&#8217;ordre des multiplications a donc une importance !


Pour peu que ce calcul soit à objectif financier, les arrondis peuvent faire basculer le montant vers le centime inférieur.




Calculs avec les double


L&#8217;article cité en introduction nous montre un exemple de calcul avec double assez parlant.
Il fait des multiplications, additions et soustraction qui devraient toujours donner 1, mais qui diverge assez rapidement :



 double b = 4095.1;
 double a = b + 1;
 double x = 1;

 for (int index = 1; index &amp;lt;= 9; index++) {
   x = (a * x) - b;
   System.out.printf("%01d =&gt; %.6f\n", index, x);
 }



Le résultat de cette boucle est assez surprenant :



 1 =&gt; 1,000000
 2 =&gt; 1,000000
 3 =&gt; 1,000008
 4 =&gt; 1,031259
 5 =&gt; 129,040637
 6 =&gt; 524468,255009
 7 =&gt; 2148270324,241572
 8 =&gt; 8799530071030,805000
 9 =&gt; 36043755123945184,000000



Il est bien évident que le nombre 4095.1 n&#8217;est pas choisi au hasard, puisqu&#8217;en prenant d&#8217;autres nombres au hasard, on obtiendra systématiquement 1.0000.
Le plus étonnant est que la même boucle avec des float fonctionnera parfaitement.


Autre bizarrerie avec Double. Essayez ceci :



 Double.parseDouble("2.2250738585072012e-308")



Il ne reste plus qu&#8217;à espérer ne jamais tomber sur ce nombre dans un programme.




Conclusions


La conclusion de ces démonstrations est que dans le cadre de calcul financiers ou d&#8217;autres calculs qui demandent une précision particulière, il est peut-être plus prudent de passer par des entiers ou des BigDecimal&#8230;&#8203;
Je ne parle évident pas du calcul scientifique dont les contraintes sont beaucoup plus poussées et que je laisse aux spécialistes.


Il faut noter que ces résultats ne sont pas liés au langage java, mais au fonctionnement par virgule flottante de nos processeurs. D&#8217;ailleurs, les exemples cités dans l&#8217;article de référence sont en C.


</description>
          <pubDate>2007-12-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/AddFloat</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/AddFloat</guid>
        </item>
      
    
      
        <item>
          <title>Property Configurer</title>
          <description>
Comment remplacer dynamiquement les propriétés d&#8217;un bean Spring ?


Remplacement de propriétés


La technique traditionnelle pour externaliser les valeurs de propriétés d&#8217;un bean est d&#8217;utiliser le post-processeur PropertyPlaceholderConfigurer.



&lt;bean id="dataSource"
      class="org.springframework.jdbc.datasource.DriverManagerDataSource"&gt;
  &lt;property name="driver"&gt;&lt;value&gt;${jdbc.driver}&lt;/value&gt;&lt;/property&gt;
  &lt;property name="url"&gt;&lt;value&gt;${jdbc.url}&lt;/value&gt;&lt;/property&gt;
  &lt;property name="user"&gt;&lt;value&gt;${jdbc.user}&lt;/value&gt;&lt;/property&gt;
  &lt;property name="password"&gt;&lt;value&gt;${jdbc.password}&lt;/value&gt;&lt;/property&gt;
&lt;/bean&gt;

&lt;bean id="placeholder"
      class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"&gt;
  &lt;property name="location"&gt;&lt;value&gt;jdbc.properties&lt;/value&gt;&lt;/property&gt;
&lt;/bean&gt;




# Fichier jdbc.properties
jdbc.driver=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://localhost
jdbc.user=sa
jdbc.password=





Redéfinition de propriétés


Il est possible de faire une manipulation similaire, sans la logique de placeholder, mais en écrasant les valeurs définies dans le bean, avec le PropertyOverrideConfigurer



&lt;bean id="dataSource"
      class="org.apache.commons.dbcp.BasicDataSource"
      destroy-method="close"&gt;
  &lt;property name="driverClassName" value="org.hsqldb.jdbcDriverx" /&gt;
  &lt;property name="url" value="jdbc:hsqldb:hsql://localhost/XXX" /&gt;
  &lt;property name="username" value="sax" /&gt;
  &lt;property name="password" value="xxx" /&gt;
&lt;/bean&gt;

&lt;bean id="placeholder"
      class="org.springframework.beans.factory.config.PropertyOverrideConfigurer"&gt;
  &lt;property name="location"&gt;
    &lt;value&gt;jdbc.properties&lt;/value&gt;
  &lt;/property&gt;
&lt;/bean&gt;



Les propriétés enregistrées dans le fichier font référence aux propriétés du bean sur la construction beanName.propertyName :



# Fichier jdbc.properties
dataSource.driverClassName=org.hsqldb.jdbcDriver
dataSource.url=jdbc:hsqldb:hsql://localhost
dataSource.username=sa
dataSource.password=





Chargement personnalisé de propriétés


Dans les deux cas, il est possible d&#8217;aller plus loin dans la personnalisation des propriétés, en utilisant un bean personnel de propriétés.



&lt;bean id="dsProperties"
      class="fr.sewatech.university.utils.DatasourceProperties" /&gt;

&lt;bean id="placeholder"
      class="org.springframework.beans.factory.config.PropertyOverrideConfigurer"&gt;
  &lt;property name="properties" ref="dsProperties"/&gt;
&lt;/bean&gt;



Dans ce cas, les propriétés ne sont plus forcément stockées en fichier, mais sont gérée dans le constructeur de notre classe DatasourceProperties.



// Fichier DatasourceProperties.java
package fr.sewatech.university.utils;

import java.util.Properties;

public class DatasourceProperties extends Properties {

  private static final long serialVersionUID = -2672136808054763735L;

  public DatasourceProperties() {
    this.put("datasource.driverClassName", "org.hsqldb.jdbcDriver");
    this.put("datasource.url", "jdbc:hsqldb:hsql://localhost");
    this.put("datasource.username", "sa");
    this.put("datasource.password", "");
  }
}



Dans l&#8217;exemple ci-dessus, les propriétés sont codées en dur dans le constructeur ; il est évident que nous pouvons remplacer cette portion de code par l&#8217;accès à un bean injecté, ou )ar un accès direct à une base de données.


</description>
          <pubDate>2007-12-11T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/PropertyConfigurer</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/PropertyConfigurer</guid>
        </item>
      
    
      
        <item>
          <title>Annotations avec Spring</title>
          <description>
Depuis le JDK 5, en 2004, les annotations permmettent d&#8217;attacher des méta-données aux classes, interfaces et membres.
Avec Spring Framework, elles permettent de paramétrer les composants métier, et de configurer les composants techniques.
</description>
          <pubDate>2007-12-11T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/Annotations</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/Annotations</guid>
        </item>
      
    
      
        <item>
          <title>Annotations contre XML avec Spring</title>
          <description>
ou comment les annotations ont simplifié le développement d&#8217;applications Spring


Aux débuts de Spring Framework, toute la configuration et toutes les méta-données des composants devaient être déclarées en XML.
Cette page explique comment le remplacement de l&#8217;XML par des annotations a amélioré le code.
Elle a une portée historique puisqu&#8217;elle reprend le contenu d&#8217;une page écrite en 2007, pour des techniques qui ont émergé entre 2005 et 2010.
</description>
          <pubDate>2007-12-11T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/Annotations-XML</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/Annotations-XML</guid>
        </item>
      
    
      
        <item>
          <title>JSF/Facelets</title>
          <description>
Cet article décrit comment intégrer les tags du framework Shale Validator avec Facelets. Il s&#8217;agit notamment d&#8217;indiquer à Facelets comment interpréter les tags &lt;s:commonsValidator&gt;, &lt;s:validatorVar&gt; et &lt;s:validatorScript&gt;. Par exemple:



  &lt;s:commonsValidator type="minlength" arg="#{bundle['login.login']}" server="true" client="true" minlength="4"/&gt;
  &lt;s:commonsValidator type="mask" arg="#{bundle['login.login']}" server="true" client="true"&gt;
    &lt;s:validatorVar name="mask" value="^[a-zA-Z0-9]*$"/&gt;
  &lt;/s:commonsValidator&gt;
  &lt;s:validatorScript functionName="validateForm"/&gt;



Handler pour CommonsValidator


Un handler spécifique facelets doit être redéfini afin de pouvoir prendre en compte les variables de commons validator renseignées avec le tag &lt;s:validatorVar&gt;.
L&#8217;exemple ci-dessous permet de traiter uniquement le cas de la variable "mask", mais la classe handler peut être facilement étendue afin de prendre en compte d&#8217;autres variables.
A noter que ce handler n&#8217;hérite pas de ValidatorHandler, car précisément, la méthode apply() (méthode finale) de ValidatorHandler ne propage pas le traitement aux handlers fils.
Le code ci-dessous est donc une copie de la classe ValidatorHandler qui permet de propager le traitement au handler du tag &lt;s:validatorVar&gt;.



 package fr.icodem.facelets.shale;

 public class CommonsValidatorHandler extends MetaTagHandler {

   //  validatorVar tag 
   // set mask variable on commons validator
   public static class MaskMetadata extends Metadata {
     public void applyMetadata(FaceletContext ctx, Object instance) {
       ValueExpression var = ctx.getVariableMapper().resolveVariable("mask");
       if (var != null) {
         String expression = var.getExpressionString();
         CommonsValidator v = (CommonsValidator)instance;
         v.setMask(expression);
       }
     }
   }

   private final TagAttribute binding;

   private String validatorId = "org.apache.shale.CommonsValidator";

   public CommonsValidatorHandler(TagConfig config) {
     super(config);
     this.binding = this.getAttribute("binding");
   }

   public final void apply(FaceletContext ctx, UIComponent parent)
          throws IOException, FacesException, FaceletException, ELException {

     if (parent == null || !(parent instanceof EditableValueHolder)) {
          throw new TagException(this.tag,
                   "Parent not an instance of EditableValueHolder: " + parent);
     }

     //  validatorVar tag 
     // get validator vars
     if (nextHandler != null) nextHandler.apply(ctx, parent);

     // only process if it's been created
     if (parent.getParent() == null) {
       // cast to a ValueHolder
       EditableValueHolder evh = (EditableValueHolder) parent;
       ValueExpression ve = null;
       Validator v = null;
       if (this.binding != null) {
         ve = this.binding.getValueExpression(ctx, Validator.class);
         v = (Validator) ve.getValue(ctx);
       }
       if (v == null) {
         v = this.createValidator(ctx);
         if (ve != null) {
           ve.setValue(ctx, v);
         }
       }
       if (v == null) {
         throw new TagException(this.tag, "No Validator was created");
       }
       this.setAttributes(ctx, v);
       evh.addValidator(v);
     }
   }

   protected Validator createValidator(FaceletContext ctx) {
     if (this.validatorId == null) {
       throw new TagException(
                   this.tag,
                   "Default behavior invoked of requiring a validator-id " +
                   "passed in the constructor, must override ValidateHandler(ValidatorConfig)");
       }
       return ctx.getFacesContext().getApplication().createValidator(
               this.validatorId);
   }

   //  commons validator tag 
   // create specific rule set
   @SuppressWarnings("unchecked")
   protected MetaRuleset createMetaRuleset(Class type) {
     MetaRuleset  metaRuleset = super.createMetaRuleset(type)
                                             .ignore("binding")
                                             .alias("minlength", "minLength")
                                             .alias("maxlength", "maxLength")
                                             .add(new MaskMetadata());

     return metaRuleset;
   }
 }





Handler pour ValidatorVar


Ce handler permet de placer dans le context de Facelets les variables renseignées avec le tag &lt;s:validatorVar&gt;.



 public class ValidatorVarHandler extends TagHandler {

   // possible attributes
   private final TagAttribute name;
   private final TagAttribute value;

   public ValidatorVarHandler(TagConfig config) {
     super(config);
     this.name = getRequiredAttribute("name");
     this.value = getRequiredAttribute("value");
   }

   @Override
   public void apply(FaceletContext ctx, UIComponent parent)
              throws IOException, FacesException, ELException {
     String nameStr = this.name.getValue(ctx);
     ValueExpression veObj = this.value.getValueExpression(ctx, Object.class);
     ctx.getVariableMapper().setVariable(nameStr, veObj);
   }
 }





Fichier taglib Facelets pour Shale


Les handlers définis précédemment sont utilisés dans le fichier shale.taglib.xml :



 &lt;?xml version="1.0" encoding="UTF-8"?&gt;
 &lt;!DOCTYPE facelet-taglib PUBLIC
   "-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
   "http://java.sun.com/dtd/facelet-taglib_1_0.dtd"&gt;

 &lt;facelet-taglib&gt;
   &lt;namespace&gt;http://shale.apache.org/core&lt;/namespace&gt;
     &lt;tag&gt;
       &lt;tag-name&gt;commonsValidator&lt;/tag-name&gt;
       &lt;handler-class&gt;org.librairie.web.CommonsValidatorHandler&lt;/handler-class&gt;
   &lt;/tag&gt;
   &lt;tag&gt;
     &lt;tag-name&gt;validatorScript&lt;/tag-name&gt;
     &lt;component&gt;
       &lt;component-type&gt;org.apache.shale.ValidatorScript&lt;/component-type&gt;
     &lt;/component&gt;
   &lt;/tag&gt;
   &lt;tag&gt;
     &lt;tag-name&gt;validatorVar&lt;/tag-name&gt;
     &lt;handler-class&gt;org.librairie.web.ValidatorVarHandler&lt;/handler-class&gt;
   &lt;/tag&gt;
 &lt;/facelet-taglib&gt;



</description>
          <pubDate>2007-11-28T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JSF/Facelets</link>
          <guid isPermaLink="true">https://www.jtips.info/JSF/Facelets</guid>
        </item>
      
    
      
        <item>
          <title>StackTrace</title>
          <description>
Pour afficher dans les fichiers de traces les appels des méthodes, à la manière des messages d&#8217;erreurs, il existe plusieurs solutions:




les méthodes de la classe java.lang.Throwable,


les méthodes de la classe java.lang.Thread,


les outils externes.




Classe Throwable


En cas d&#8217;erreur ou d&#8217;exception, on dispose d&#8217;une instance de Throwable, sur laquelle on peut appeler les méthodes printStackTrace() et getStackTrace().


Sortie directe

La méthode printStackTrace() envoie directement la pile d&#8217;appel de méthodes dans la sortie d&#8217;erreur (System.err), sans passer par la case Log4J.
Cela ne permet pas de gérer convenablement les sorties de traces, voire cela pourrait égarer certaines traces !
La méthode printStackTrace() est donc généralement déconseillées.
Cela peut d&#8217;ailleurs être une bonne idée de modifier le template d&#8217;IDE pour la commande "surround with try catch block".


On préfère passer l&#8217;objet Throwable aux méthodes warn ou error de log4j ;
celles-ci envoie la trace des appels dans tous les appenders paramétrés.



Chaîne d&#8217;appel

La méthode getStackTrace() permet de récupérer tous les éléments de trace, sous forme de tableau de StackTraceElement, et de les gérer manuellement, et plus intelligemment.


Il est aussi possible d&#8217;utiliser la méthode printStackTrace() avec un PrintStream ou un PrintWriter en paramètre.
Cela permet d&#8217;envoyer la pile dans un sortie spécifique ou de la récupérer sous forme de chaîne de caractères :



public class ThrowableUtil {
    public static String getStackTraceAsString (Throwable throwable) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        throwable.printStackTrace(pw);
        return sw.getBuffer().toString();
    }
}



Si vous utilisez déjà Apache Commons Lang, cette méthode y existe déjà, dans la classe ExceptionUtils.



String stacktrace = ExceptionUtils.getStackTrace(e);






Classe Thread


La classe Thread permet de récupérer les mêmes informations, mais en fonctionnement normal, sans Throwable.
Les méthodes équivalentes existent: Thread.dumpStack() et Thread.currentThread().getStackTrack().
Il existe une méthode qui fonctionne sur tous les threads: Thread.getAllStackTraces().


Les mêmes remarques s&#8217;imposent pour dumpStack(): les traces sortent directement dans la sortie standard (System.out), sans possibilité de les gérer.
Les méthodes getStackTrack() et getAllStackTrack() sont disponibles depuis le JDK 5.




Outils externes


JConsole fournit un moyen graphique de parcourir l&#8217;ensemble des threads, idem pour VisualVM et Mission Control.
jstack le fait en ligne de commande.
Avec un client JMX, on peut aussi exploiter les informations fournies par le MBean java.lang:Threading.


</description>
          <pubDate>2007-10-31T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/StackTrace</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/StackTrace</guid>
        </item>
      
    
      
        <item>
          <title>Qualite/outils</title>
          <description>
BROUILLON&#8230;&#8203;


Liste des outils de mesure qualité


Evalués


JDepend


PMD - Vérification statique du respect des règles de développement - à utiliser avec un jeu de règle personnalisé


CheckStyle - Vérification statique du respect des règles de développement




Non évalués


CMT++ - (payant)


CAST - (payant)


FindBugs - il serait fort pour identifier les problèmes de threads


JPF (Java Path Finder) - Analyse dynamique du code


Nasdanika - (open source)


Jalopy - Formattage de code


XRadar - Il embarque PMD, JDepend, JavaNCSS, CheckStyle,&#8230;&#8203;


JavaNCSS


metrics


Cobertura - Calcul de couverture du code par les tests, basé sur jCoverage


Crap4J - Comparaison entre la couverture du code par les tests et la complexité cyclomatique, selon la formule de CRAP




Articles


Synthèse


Référence sur les métriques de complexité (définit la complexité cyclomatique)


Chidamber &amp; Kemerer object-oriented metrics suite


Conventions de développement


</description>
          <pubDate>2007-09-12T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Qualite/outils</link>
          <guid isPermaLink="true">https://www.jtips.info/Qualite/outils</guid>
        </item>
      
    
      
        <item>
          <title>Format CSV avec JDepend</title>
          <description>
L&#8217;outil JDepend permet de calculer des métriques de qualité pour les packages d&#8217;une application. Il évalue les dépendances amont et aval de chaque package, identifie les dépendances circulaires et fait ressortir le niveau d&#8217;abstraction et d&#8217;instabilité de chaque package. JDepend est très facile à mettre en oeuvre ; par contre, l&#8217;analyse des métriques demande un bon niveau d&#8217;expérience en conception objet.


Les résultats de JDepend peuvent être sortis dans un écran dédié (version swingui), dans un fichier xml (version xmlui) ou dans un fichier texte (version textui). C&#8217;est cette dernière possibilité qui m&#8217;a intéressée car le fichier résultat contient une synthèse des métriques sous forme CSV. Le défaut, c&#8217;est que le séparateur de colonnes utilisé est la virgule, qui est aussi le séparateur décimal puisque mon poste est en français.


Pour contourner ce problème, j&#8217;ai redéveloppé une classe de lancement pour JDepend, dans laquelle, j&#8217;ai modifié le séparateur de colonnes. J&#8217;en ai profité pour retirer toutes les autres informations et utiliser le suffixe .csv.


Classe JDepend


L&#8217;outil JDepend propose une classe JDepend centrale (package framework), plus 1 classe par type de sortie. Etant plutôt fainéant, j&#8217;ai estimé qu&#8217;en héritant de la classe textui.JDepend, j&#8217;aurais un minimum de travail à fournir. J&#8217;ai donc développé une classe qui hérite de jdepend.textui.JDepend, qui a sa méthode main, et qui redéfinit quelques méthodes&#8230;&#8203;



 package fr.sewatech.design.dependency;

 public class CsvJDepend extends jdepend.textui.JDepend {

   public static void main(String args[]) {
     new CsvJDepend().instanceMain(args);
   }
   public CsvJDepend() {
     super();
   }

   public CsvJDepend(PrintWriter writer) {
     super(writer);
   }

   // ...
 }





Formatage des sorties


La classe jdepend.textui.JDepend implémente plusieurs méthodes préfixées par print, qui écrivent les différentes parties du fichier texte. Seule la partie summary m&#8217;intéresse, j&#8217;ai donc redéfinit toutes les autres méthodes print avec des méthodes vides. Je sais que ce n&#8217;est pas très design, mais je suis dans un contexte où je ne domine pas le contenu de la sur-classe, et surtout, c&#8217;est pratique !



   @Override
   protected void printHeader() {
   }

   @Override
   protected void printPackages(Collection packages) {
   }

   @Override
   protected void printCycles(Collection packages) {
   }

   @Override
   protected void printFooter() {
   }





Ecriture du CSV


Enfin, pour la partie summary, je me suis inspriré de la sur-classe, en modifiant le séparateur de colonnes.



   @Override
   protected void printSummary(Collection packages) {
     final String separator = ";";

     getWriter().println(
       "Name"+separator+" CC"+separator+" CA"+separator+" Ca"+separator+" Ce"+separator+" A"+separator+" I"+separator+" D"+separator+" V");

     Iterator i = packages.iterator();
     while (i.hasNext()) {
       JavaPackage jPackage = (JavaPackage) i.next();
       getWriter().print(jPackage.getName() + separator);
       getWriter().print(jPackage.getClassCount() + separator);
       getWriter().print(jPackage.getAbstractClassCount() + separator);
       getWriter().print(jPackage.afferentCoupling() + separator);
       getWriter().print(jPackage.efferentCoupling() + separator);
       getWriter().print(toFormattedString(jPackage.abstractness()) + separator);
       getWriter().print(toFormattedString(jPackage.instability()) + separator);
       getWriter().print(toFormattedString(jPackage.distance()) + separator);
       getWriter().println(jPackage.getVolatility());
     }
   }





Lancement de la classe


Les options de lancement sont les mêmes que pour l&#8217;outil jDepend standard, de même que le même fichier jdepend.properties est pris en compte.
Le lancement du nouvel outil se fait de la façon suivant :



 java fr.sewatech.design.dependency.CsvJDepend -file jdepend-report.csv D:\Projets\leader\workspace\novanet\novanet\WEB-INF\classes



</description>
          <pubDate>2007-09-11T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Qualite/JDepend-csv</link>
          <guid isPermaLink="true">https://www.jtips.info/Qualite/JDepend-csv</guid>
        </item>
      
    
      
        <item>
          <title>JSF/Tomahawk</title>
          <description>
Upload d&#8217;un fichier


Lorsque l&#8217;on utilise le composant &lt;t:inputFileUpload&gt;, il faut ajouter l&#8217;attribut enctype="multipart/form-data" au formulaire.
Si cet attribut est absent, le message suivant est généré EvaluationException: Exception while invoking expression #{myBean.save}.



&lt;h:form id="myForm" enctype="multipart/form-data"&gt;
  &lt;t:inputFileUpload id="txtImage"
                     accept="image/*"
                     value="#{myBean.imageFile}"
                     storage="file"
                     styleClass="fileUploadInput"
                     required="true"
                     maxlength="200000"/&gt;
  ...
  &lt;h:commandButton id="btnValider" value="Valider" actionListener="#{myBean.save}"/&gt;
&lt;/h:form&gt;





Extensions Filter


Certains composants Tomahawk nécessite l&#8217;utilisation du filtre d&#8217;extensions de MyFaces.
Ce filtre permet notamment à Tomahawk de télécharger des ressources associées aux composants (des images, des fichiers javascript&#8230;&#8203;), ressources qui sont stockées dans le fichier jar de Tomahawk. Par exemple, le composant Picklist utilise le fichier picklist.js qui est téléchargé via une URI du type /myapp/faces/myFacesExtensionResource/xxx/picklist.js.


A noter que le mapping du filtre ne correspond pas forcément au mapping de votre servlet.
Ainsi, si la servlet est mappée suivant le pattern *.jsf, le filtre doit tout de même intercepter /faces/myFacesExtensionResource/*.



&lt;filter-mapping&gt;
  &lt;filter-name&gt;extensionsFilter&lt;/filter-name&gt;
  &lt;url-pattern&gt;/faces/myFacesExtensionResource/*&lt;/url-pattern&gt;
&lt;/filter-mapping&gt;



</description>
          <pubDate>2007-06-21T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JSF/Tomahawk</link>
          <guid isPermaLink="true">https://www.jtips.info/JSF/Tomahawk</guid>
        </item>
      
    
      
        <item>
          <title>JSF/Data Table</title>
          <description>
La mise en oeuvre d&#8217;un formulaire multiligne avec une data table est très simple. Il suffit de lier le composant data table avec une liste, comme pour un affichage, d&#8217;utiliser des composants de saisie dans la définition des colonnes et d&#8217;inclure la data table dans un h:form. Les valeurs saisies seront alors injectées par JSF directement dans les objets de la liste.


Code JSP:



&lt;h:form&gt;
  &lt;h:dataTable value="#{testBean.myDataList}"  var="item"&gt;
    &lt;h:column&gt;
      &lt;f:facet name="header"&gt;
      &lt;h:outputText value="Valeur 1" /&gt;
      &lt;/f:facet&gt;
      &lt;h:inputText value="#{item.value1}" /&gt;
    &lt;/h:column&gt;
    &lt;h:column&gt;
      &lt;f:facet name="header"&gt;
      &lt;h:outputText value="Valeur 2" /&gt;
      &lt;/f:facet&gt;
      &lt;h:inputText value="#{item.value2}" /&gt;
    &lt;/h:column&gt;
  &lt;/h:dataTable&gt;
  &lt;h:commandButton actionListener="#{testBean.process}" value="Go !"/&gt;
&lt;/h:form&gt;



Backing bean:



public class TestBean {
  private List&lt;data&gt; myDataList = new ArrayList&lt;Data&gt;();
  public TestBean() {
    myDataList.add(new Data("id 1", "val 1"));
    myDataList.add(new Data("id 2", "val 2"));
  }
  public List&lt;Data&gt; getMyDataList() {
    return myDataList;
  }
  public void setMyDataList(List&lt;Data&gt; myDataList) {
    this.myDataList = myDataList;
  }
  public void process(ActionEvent event) {
    for (Data data : myDataList) {
      System.out.println("value1=" + data.getValue1() + ", value2=" + data.getValue2());
    }
  }
}
&lt;/data&gt;



Objet dans la liste:



public class Data {
  private String value1;
  private String value2;
  public Data(String value1, String value2) {
    this.value1 = value1;
    this.value2 = value2;
  }
  public String getValue1() {
    return value1;
  }
  public void setValue1(String value1) {
    this.value1 = value1;
  }
  public String getValue2() {
    return value2;
  }
  public void setValue2(String value2) {
    this.value2 = value2;
  }
}

</description>
          <pubDate>2007-06-21T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JSF/Data_Table</link>
          <guid isPermaLink="true">https://www.jtips.info/JSF/Data_Table</guid>
        </item>
      
    
      
        <item>
          <title>Glassfish/Jackrabbit</title>
          <description>
Cet article décrit comment déclarer une ressource de type javax.jcr.Repository dans l&#8217;annuaire JNDI de Glassfish. Il s&#8217;agit en l&#8217;occurrence de pointer vers un serveur Jackrabbit (deployment model 3). La version Jackrabbit utilisée est 1.3.




Copier les librairies nécessaires à un client Jackrabbit dans le répertoire GLASSFISH_HOME/lib


jcr-1.0.jar, jackrabbit-api-1.3.jar, jackrabbit-classloader-1.3.jar, jackrabbit-core-1.3.jar, jackrabbit-jcr-commons-1.3.jar, jackrabbit-jcr-rmi-1.3.jar, jackrabbit-text-extractors-1.3.jar, commons-collections-3.1.jar, slf4j-log4j12-1.0.jar, concurrent-1.3.4.jar, lucene-core-2.0.0.jar


Dans la console d&#8217;administration de Glassfish, sélectionner le menu Resources &#8594; JNDI &#8594; Custom Resources


Ajouter une ressource JNDI (bouton New&#8230;&#8203;)


JNDI Name = jcr/myRepository


Resource Type = javax.jcr.Repository


Factory Class = org.apache.jackrabbit.rmi.client.ClientRepositoryFactory




Il faut maintenant configurer l&#8217;application web pour pointer vers la ressource créée précédemment :


Fichier web.xml



&lt;resource-ref&gt;
  &lt;description&gt;Repository JackRabbit&lt;/description&gt;
  &lt;res-ref-name&gt;jcr/repository&lt;/res-ref-name&gt;
  &lt;res-type&gt;javax.jcr.Repository&lt;/res-type&gt;
  &lt;res-auth&gt;Container&lt;/res-auth&gt;
  &lt;res-sharing-scope&gt;Shareable&lt;/res-sharing-scope&gt;
&lt;/resource-ref&gt;



Fichier sun-web.xml



&lt;resource-ref&gt;
  &lt;res-ref-name&gt;jcr/repository&lt;/res-ref-name&gt;
  &lt;jndi-name&gt;jcr/myRepository&lt;/jndi-name&gt;
&lt;/resource-ref&gt;



Après redémarrage de Glassfish, la ressource JCR peut être récupérée via un lookup JNDI :



InitialContext ctx = new InitialContext();
Repository repository = (Repository) ctx.lookup("java:comp/env/jcr/repository");

</description>
          <pubDate>2007-06-20T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Glassfish/Jackrabbit</link>
          <guid isPermaLink="true">https://www.jtips.info/Glassfish/Jackrabbit</guid>
        </item>
      
    
      
        <item>
          <title>JSF/Backing Bean</title>
          <description>
Cet article décrit l&#8217;implémentation d&#8217;une super-classe pour le développement des backing beans.


Classe abstraite


Cette classe fournit un ensemble de méthodes utiles pour des opérations récurrentes effectuées dans un backing bean. Les backing beans de l&#8217;application devront hériter de cette classe abstraite afin de bénéficier des services fournis.



public class AbstractBackingBean implements Serializable {
  ...
}



Accès aux objets JSF

Il s&#8217;agit de pouvoir récupérer facilement les objets FacesContext, Application, ExternalContext, VariableResolver, ainsi que les objets déclarés dans le fichier de configuration JSF (méthode getVariable()).



protected FacesContext getFacesContext() {
  return FacesContext.getCurrentInstance();
}
protected Application getApplication() {
  return getFacesContext().getApplication();
}
protected ExternalContext getExternalContext() {
  return getFacesContext().getExternalContext();
}
protected VariableResolver getVariableResolver() {
  return getApplication().getVariableResolver();
}
protected Object getVariable(String name) {
  Object o = getVariableResolver().resolveVariable(getFacesContext(), name);
  return o;
}




Accès aux objets Java Web

Même si JSF masque les objets HttpRequest et HttpSession, il est parfois utile d&#8217;intervenir directement sur ces objets.



protected HttpSession getSession() {
  return getRequest().getSession();
}
protected HttpServletRequest getRequest() {
  HttpServletRequest request = (HttpServletRequest)getFacesContext().getExternalContext().getRequest();
  return request;
}
protected String getParameter(String name) {
  HttpServletRequest request = getRequest();
  return request.getParameter(name);
}
protected void addSessionAttribute(String name, Object o) {
  FacesContext facesContext = getFacesContext();
  StringBuilder sb = new StringBuilder();
  sb.append("#{sessionScope.").append(name).append("}");
  getApplication().createValueBinding(sb.toString()).setValue(facesContext, o);
}




Méthode de fabrication d&#8217;objets SelectItem

Lorsque l&#8217;on utilise des composants graphiques de type sélection (&lt;h:selectOneMenu, &lt;h:selectManyListBox&gt;&#8230;&#8203;), il faut transformer les listes d&#8217;objets récupérés depuis la couche métier, en listes d&#8217;objets de type SelectItem pour alimenter les valeurs sélectionnables des composants graphiques.
Voici une méthode qui permet d&#8217;automatiser cette tâche fastidieuse :



/**
 * @param original collection des objets issus de la couche métier
 * @param labelProperty nom de la propriété des objets métier utilisée comme libellé dans SelectItem
 * @param valueProperty nom de la propriété des objets métier utilisée comme valeur dans SelectItem
 */
protected List&lt;SelectItem&gt; buildSelectCollection(Collection&lt;?&gt; original, String labelProperty, String valueProperty) {
List&lt;SelectItem&gt; result = new ArrayList&lt;SelectItem&gt;();
  if (original != null) {
    try {
      for (Object obj : original) {
        String label = (String) PropertyUtils.getProperty(obj, labelProperty);
        Object value = PropertyUtils.getProperty(obj, valueProperty);
        SelectItem item = new SelectItem(value, label);
        result.add(item);
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  return result;
}



Voici une version plus élaborées, permettant de fabriquer le libellé des objets SelectItem à partir de plusieurs propriétés et d&#8217;un pattern de formattage:



/**
 * @param original collection des objets issus de la couche métier
 * @param valueProperty nom de la propriété des objets métier utilisée comme valeur dans SelectItem
 * @param pattern pattern de formattage utilisé pour la fabrication du libellé des SelectItem
 * @param labelProperties nom des propriétés des objets métier injectées dans le pattern de formattage
 */
protected List&lt;SelectItem&gt; buildSelectCollection(Collection&lt;?&gt; original, String valueProperty,
                                                    String pattern, String... labelProperties) {
  List&lt;SelectItem&gt; result = new ArrayList&lt;SelectItem&gt;();
  if (original != null) {
    try {
      for (Object obj : original) {
        Object[] params = new Object[labelProperties.length];
        for (int i = 0; i &lt; params.length; i++) {
          String prop = (String) PropertyUtils.getProperty(obj, labelProperties[i]);
          params[i] = prop;
        }
        String label = getLabel(pattern, params);
        Object value = PropertyUtils.getProperty(obj, valueProperty);
        SelectItem item = new SelectItem(value, label);
        result.add(item);
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  return result;
}
private String getLabel(String pattern, Object[] params) {
  String label = MessageFormat.format(pattern, params);
  return label;
}




Ajouter un message

Exemple de méthodes pour ajouter un message JSF.



protected void addMessage(Severity severity, String message) {
  getFacesContext().addMessage(null, new FacesMessage(severity, message, message));
}
protected void addMessage(Severity severity, String summary, String detail) {
  getFacesContext().addMessage(null, new FacesMessage(severity, summary, detail));
}
protected void addMessage(String clientid, Severity severity, String summary, String detail) {
  getFacesContext().addMessage(clientid, new FacesMessage(severity, summary, detail));
}
protected void addMessage(UIComponent component, Severity severity, String summary, String detail) {
  String clientid = component.getClientId(getFacesContext());
  getFacesContext().addMessage(clientid, new FacesMessage(severity, summary, detail));
}






Accès au numéro de phase JSF dans un backing bean


Il est parfois utile de pouvoir lire l&#8217;identifiant de la phase JSF en cours de traitement JSF.
Le pattern ci-dessous est mis en oeuvre à l&#8217;aide d&#8217;un listener JSF et d&#8217;un backing bean dédié.
Le principe est le suivant :




un phase listener écoute toutes les phases JSF, puis récupère un lien sur un backing bean qui possède un champ d&#8217;instance de type PhaseId


le listener injecte le PhaseId dans le backing bean


ce dernier est disponible auprès de tous les autres backing beans puisqu&#8217;il sera lu dans le backing bean abstrait, via le variable resolver




Mise en oeuvre :



public class PhaseBean implements Serializable {
  private transient PhaseId currentPhaseId;
  public PhaseId getCurrentPhaseId() {
    return currentPhaseId;
  }
  public void setCurrentPhaseId(PhaseId currentPhaseId) {
    this.currentPhaseId = currentPhaseId;
  }
}
public class BasePhaseListener implements PhaseListener {
  public void beforePhase(PhaseEvent event) {
    FacesContext ctx = event.getFacesContext();
    VariableResolver vr = ctx.getApplication().getVariableResolver();
    PhaseBean phaseBean = (PhaseBeanvr.resolveVariable(ctx, "phaseBean");
    phaseBean.setCurrentPhaseId(event.getPhaseId());
  }
  public PhaseId getPhaseId() {
    return PhaseId.ANY_PHASE;
  }
}
public class AbstractBackingBean implements Serializable {
  private PhaseBean phaseBean;
  public PhaseBean getPhaseBean() {
    if (phaseBean == null) {
      phaseBean = (PhaseBean)getVariable("phaseBean");
    }
    return phaseBean;
  }
  public PhaseId getPhaseId() {
    return getPhaseBean().getCurrentPhaseId();
  }
}





Reposter les Value Change Events


Suivant l&#8217;article sur les value change listener, on peut ajouter une méthode helper dans notre backing bean abstrait :



protected boolean repostEvent(ValueChangeEvent event) {
  PhaseId phaseId = event.getPhaseId();
  if (phaseId.equals(PhaseId.ANY_PHASE)) {
     event.setPhaseId(PhaseId.UPDATE_MODEL_VALUES);
     event.queue();
  } else if (phaseId.equals(PhaseId.UPDATE_MODEL_VALUES)) {
    return true;
  }
  return false;
}



Cette méthode peut être utilisée de la façon suivante dans un backing bean concret :



public boolean myValueChangeListener(ValueChangeEvent event) {
  if (!repostEvent(event)) {
     // le champ d'instance myValue est mis à jour par
     // le listener et ne sera pas écrasé plus tard par
     // invocation du setter
     this.myValue = event.getNewValue();
  }
}



</description>
          <pubDate>2007-06-03T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JSF/Backing_Bean</link>
          <guid isPermaLink="true">https://www.jtips.info/JSF/Backing_Bean</guid>
        </item>
      
    
      
        <item>
          <title>TLS/SSL avec Tomcat</title>
          <description>
TLS/SSL permet de sécuriser les communications réseau grâce à un chiffrement basé sur un échange de clés.







Pour arriver à ce but avec Tomcat, il faut configurer un connecteur avec un certificat.


conf/server.xml

  &lt;Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"&gt;
    &lt;SSLHostConfig&gt;
      &lt;Certificate .../&gt;
    &lt;/SSLHostConfig&gt;
  &lt;/Connector&gt;



Il y a plusieurs façon de faire avec Tomcat.
Dans un premier temps, il faut choisir l&#8217;implémentation du coté du connecteur, entre JSSE et OpenSSL.
Ensuite, on peut avoir le choix du format du certificat, entre JKS, PKCS#12 et PKCS#8/PEM.
Enfin, il faut choisir l&#8217;outil pour générer ces certificats, essentiellement entre keytool et openssl.


Le contenu de cette page a été rédigé et testé avec Tomcat 11.0.


Implémentation TLS


Lorsqu&#8217;on configure le connecteur, on doit dans tous les cas activer l&#8217;attribut SSLEnabled="true", et on peut choisir l&#8217;implémentation TLS entre JSSE et OpenSSL.


JSSE

L&#8217;implémentation TLS par défaut dépend de la présence de la librairie native de Tomcat.


Sans librairie native, JSSE, Java Secure Socket Extension, est utilisée.
Tout le traitement TLS est pris en charge par les classes du JDK, il n&#8217;y a pas de dépendance externe.


L&#8217;extrait de configuration ci-dessus est l&#8217;équivalent du suivant :


conf/server.xml

  &lt;Connector
      port="8443" protocol="HTTP/1.1" SSLEnabled="true"
      sslImplementationName="org.apache.tomcat.util.net.jsse.JSSEImplementation"&gt;
    ...
  &lt;/Connector&gt;




OpenSSL

Avec la librairie native, l&#8217;implémentation OpenSSL est utilisée.
Le traitement TLS est délégué aux librairies natives de Tomcat et d&#8217;OpenSSL.
Il faut donc qu&#8217;elles soient installées sur la machine.
Cette configuration remplace le connecteur APR (org.apache.coyote.http11.Http11AprProtocol) qui a existé jusqu&#8217;à Tomcat 10.0.


L&#8217;extrait de configuration ci-dessus est équivalement à :


conf/server.xml

  &lt;Connector
      port="8443" protocol="HTTP/1.1" SSLEnabled="true"
      sslImplementationName="org.apache.tomcat.util.net.openssl.OpenSSLImplementation"&gt;
    ...
  &lt;/Connector&gt;



Lorsque la librairie native est présente, on peut forcer l&#8217;utilisation de l&#8217;implémentation JSSE.
L&#8217;inverse n&#8217;est pas vrai puisque s&#8217;il n&#8217;y a pas de librairie native, on ne peut pas utiliser OpenSSL.



Installation de tcnative

Pour installer les librairies natives, on peut généralement installer certaines dépendances depuis les dépots, mais la librairie libtcnative doit être compilé pour bénéficier d&#8217;une version récente.


Par exemple pour Fedora, on peut utiliser le script suivant :


build-tcnative.sh

#!/bin/bash
# Script pour Fedora : installer et compiler Tomcat Native (APR + OpenSSL)
set -e

# Variables
TOMCAT_NATIVE_VERSION="2.0.9"
INSTALL_PREFIX="/usr/local/tomcat-native"
JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java))))

# Installer les dépendances
sudo dnf install -y gcc make wget tar apr-devel openssl-devel

# Créer un dossier temporaire pour la compilation
TMPDIR=$(mktemp -d)
cd $TMPDIR

# Télécharger les sources Tomcat Native
wget https://downloads.apache.org/tomcat/tomcat-connectors/native/${TOMCAT_NATIVE_VERSION}/source/tomcat-native-${TOMCAT_NATIVE_VERSION}-src.tar.gz
tar xzf tomcat-native-${TOMCAT_NATIVE_VERSION}-src.tar.gz
cd tomcat-native-${TOMCAT_NATIVE_VERSION}-src/native

# Configurer la compilation
./configure --with-ssl=/usr/include/openssl
            --with-apr=/usr/bin/apr-1-config
            --with-java-home=$JAVA_HOME
            --prefix=$INSTALL_PREFIX

# Compiler et installer
make
sudo make install

# Ajouter la librairie à LD_LIBRARY_PATH
echo "export LD_LIBRARY_PATH=\$LD_LIBRARY_PATH:$INSTALL_PREFIX/lib" | sudo tee /etc/profile.d/tomcat-native.sh
source /etc/profile.d/tomcat-native.sh

# Nettoyer le dossier temporaire
cd
rm -rf $TMPDIR

echo "Tomcat Native Library installée dans $INSTALL_PREFIX"



Pour Debian ou Ubuntu, le script est similaire excepté pour l&#8217;installation des dépendances.



sudo apt update
sudo apt install -y build-essential wget tar libapr1-dev libssl-dev pkg-config



Il faut aussi noter que le listener APR est nécessaire pour que la librairie native soit détectée et chargée.


conf/server.xml

  &lt;Listener className="org.apache.catalina.core.AprLifecycleListener" /&gt;



Si l&#8217;installation de tcnative a bien fonctionné et que le listener est bien configuré, on devrait avoir les messages suivants au démarrage de Tomcat:



...
00:00:00.120 INFO [main] org.apache.catalina.core.AprLifecycleListener.lifecycleEvent
        Loaded Apache Tomcat Native library [2.0.9] using APR version [1.7.6].
00:00:00.125 INFO [main] org.apache.catalina.core.AprLifecycleListener.initializeSSL
        OpenSSL successfully initialized [OpenSSL 3.5.4 30 Sep 2025]
...






Format du certificat


Type du fichier de clés

Le connecteur peut utiliser deux types de keystores, JKS et PKCS#12.
JKS signifie Java KeyStore et c&#8217;est un format interne au JDK.
Depuis le JDK 11, le format standard PKCS#12 doit être privilégié.


Ces deux formats ont des similitudes.
Ils se présentent sous forme de fichiers keystore, qui contiennent plusieurs entrées, chacune contenant une clé privée, une clé publique et un certificat.


conf/server.xml

  &lt;Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"&gt;
    &lt;SSLHostConfig&gt;
      &lt;Certificate certificateKeystoreFile="conf/sewa.p12"
                   certificateKeystorePassword="sewapwd" /&gt;
    &lt;/SSLHostConfig&gt;
  &lt;/Connector&gt;



Le chemin de certificateKeystoreFile est absolu ou relatif à la racine de Tomcat.
Comme on n&#8217;a pas renseigné certificateKeyAlias, le connecteur utilise la première entrée du fichier.


On peut aussi configurer le connecteur avec des fichiers séparés pour la clé privée et le certificat / clé publique.
Les fichiers sont alors au format PEM.


conf/server.xml

  &lt;Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"&gt;
    &lt;SSLHostConfig&gt;
        &lt;Certificate certificateFile="conf/sewa.cert"
                     certificateKeyFile="conf/sewa.key"
                     certificateKeyPassword="sewapwd" /&gt;
    &lt;/SSLHostConfig&gt;
  &lt;/Connector&gt;



Les mêmes formats de fichiers sont supportés par les implémentations JSSE et OpenSSL.



Type de clé

L&#8217;attribut type de l&#8217;élément &lt;Cetificate&gt; fait référence au type de clé.
Il n&#8217;est généralement pas nécessaire de le renseigner car détecté à la lecture.


Les types les plus couramment utilisés sont RSA ou EC, pour elliptic curve.





Outils


On a à notre disposition essentiellement deux outils pour générer les clés et certificats, il s&#8217;agit de keytool qui fait partie du JDK et du classique openssl.
Il y a d&#8217;autres outils, qui sont généralement basés sur l&#8217;un des deux précédents.


keytool

Le keystore est un fichier qui stocke un ensemble de couples de clés privée/publique, auxquels on attache des certificats.
Pour générer un keystore au format JKS ou PKCS#12, on utilise keytool, qui s&#8217;installe avec le JDK.



~$ keytool -genkeypair                                                \
           -keystore sewa.jks -storepass sewapwd -storetype JKS       \
           -alias sewatech-fr -keypass sewapwd -keyalg RSA            \
           -dname "cn=www.sewatech.fr, o=Sewatech, c=FR"



Cette commande va générer une clé pour le site www.sewatech.fr.
Si le fichier sewa.jks existe déjà, la nouvelle clé est ajoutée, sinon le fichier est créé pour cette première clé.


Sa variante pour PKCS#12 est très semblable, et comme c&#8217;est le format par défaut, on peut omettre le paramètre -storetype.
Et comme le format ne supporte pas les mots de passe sur les entrées, on peut aussi omettre -keypass.



~$ keytool -genkeypair                                                \
           -keystore sewa.p12 -storepass sewapwd                      \
           -alias sewatech-fr -keyalg RSA                             \
           -dname "cn=www.sewatech.fr, o=Sewatech, c=FR"



Pour générer une clé elliptic curve, il faut juste changer la valeur de keyalg : -keyalg EC.


En l&#8217;état, la clé est auto-signée, c&#8217;est-à-dire qu&#8217;aucune autorité, autre que l&#8217;auteur lui-même, n&#8217;a certifié la validité de la clé.
Les navigateurs refusent donc d&#8217;établir une connexion sécurisée avec le site qui produit la clé, sauf si on lui demande explicitement d&#8217;accepter, via une gestion des exceptions.
La solution pour rassurer le navigateur, est de faire certifier la clé par une autorité tierce.


La demande de certificat est un fichier au format PKCS#10 généré à partir de la clé.



~$ keytool -certreq                                                  \
           -keystore sewa.p12 -storepass sewapwd                     \
           -alias sewatech-fr -file sewatech.csr



Le fichier csr doit ensuite être envoyé à l&#8217;autorité de certification (CA) qui retournera un certificat X.509 ou une chaîne de certificats.
Un certificat est produit pour valider les informations du propriétaire de la clé ; ce certificat fait référence à un certificat parent qui valide les informations de l&#8217;autorité.
Ces certificats peuvent être attachés les uns aux autres à plusieurs niveaux, formant une chaîne.
Le certificat, ou la chaîne, sont au format PKCS#7 ou PEM, ces deux formats étant supportés par keytool.
Si l&#8217;autorité retourne le certificat sous un autre format, il faut le transformer à l&#8217;aide d&#8217;un outil comme OpenSSL.


En possession d&#8217;un certificat au bon format, nous pouvons à présent l&#8217;importer et l&#8217;attacher à notre clé.



~$ keytool -importcert                                             \
           -keystore sewa.p12 -storepass sewapwd                   \
           -alias sewatech-fr -file sewatech.cer



Si le nom de domaine correspond bien au CN de la clé et si l&#8217;autorité qui a certifié notre clé a été déclarée dans sa configuration, le navigateur acceptera de naviguer par TLS.



openssl

OpenSSL est l&#8217;outil pour créer des clés et certificat PKCS#8, encodés en PEM.
Comme pour les autres formats, on peut choisir le type, entre RSA et EC.



# Génération de la clé privée RSA, avec mot de passe
~$ openssl req -newkey rsa -keyout sewa.key -x509 -out sewa.cert   \
               -subj "/CN=sewatech.fr" -passout pass:sewapwd



ou



# Génération de la clé privée EC, avec mot de passe
~$ openssl req -newkey ec -keyout sewa.key -x509 -out sewa.cert    \
               -pkeyopt ec_paramgen_curve:prime256v1               \
               -subj "/CN=sewatech.fr" -passout pass:sewapwd



OpenSSL peut aussi créer des keystores au format PKCS#12, à partir des fichiers créés précédemment.
Et l&#8217;outil ne peut créer que des keystores avec une seule entrée.



# Génération du keystore PKCS#12, avec mot de passe
~$ openssl pkcs12 -inkey sewa.key -in sewa.cert                    \
                  -export -out sewa.p12 -passin pass:sewapwd       \
                  -password pass:sewapwd






Synthèse


Depuis Tomcat 10.1, il n&#8217;y a plus cette distinction au niveau du protocole du connecteur.
C&#8217;est au niveau de l&#8217;implémentation qu&#8217;on choisit entre JSSE ou OpenSSL.


Les deux implémentations supportent les formats JKS, PKCS#12 et PKCS#8/PEM.


L&#8217;outillage tourne autour de keytool pour JKS et PKCS#12, et d'`openssl` pour PKCS#12 et PKCS#8/PEM, en RSA ou EC.


</description>
          <pubDate>2007-04-25T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Tomcat/TLS</link>
          <guid isPermaLink="true">https://www.jtips.info/Tomcat/TLS</guid>
        </item>
      
    
      
        <item>
          <title>Envoi de mail par Log4J</title>
          <description>
Le SMTPAppender de Log4J permet d&#8217;envoyer des traces par mail.


Paramétrage


Fichier properties


log4j.appender.mail=org.apache.log4j.net.SMTPAppender
log4j.appender.mail.SMTPHost=smtp.mycompany.com
log4j.appender.mail.Threshold=error
log4j.appender.mail.BufferSize=10
log4j.appender.mail.Subject=Message de l'application
log4j.appender.mail.From=appli@mycompany.com
log4j.appender.mail.To=appli.admin@mycompany.com
log4j.appender.mail.layout=org.apache.log4j.PatternLayout
log4j.appender.mail.layout.ConversionPattern=%d{dd-MM-yy HH:mm:ss,SSS} [%t] %5p %c{1}%n%m%n




Fichier XML


TODO






Explication sur les paramètres




Threshold : comme pour les autres appenders, il permet de limiter le niveau des messages ; dans notre cas, seuls les messages ERROR et FATAL seront envoyés par mail, quel que soit le paramétrage des Loggers


BufferSize : c&#8217;est le nombre maximum de messages qui seront regroupés dans chaque mail ; le mail est réellement envoyé quand le buffer est plein ou quand un message de niveau ERROR ou FATAL est ajouté.




</description>
          <pubDate>2007-04-19T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Log4J/SMTPAppender</link>
          <guid isPermaLink="true">https://www.jtips.info/Log4J/SMTPAppender</guid>
        </item>
      
    
      
        <item>
          <title>JSF/Validation</title>
          <description>
JSF déclenche les validations des champs du formulaire lorsqu&#8217;on déclenche un submit et que l&#8217;attribut immediate n&#8217;est pas positionné à true.
Les validations sont généralement déclarées dans la page.


Pour certains composants, les validations sont déclenchées automatiquement, sans aucune déclaration de la part du développeur. C&#8217;est, en particulier,  le cas des listes de sélection. Les composants UISelectOne (sur-classe de HtmlSelectOneListbox, HtmlSelectOneMenu et HtmlSelectOneRadio) effectue une validation systématique dans laquelle la valeur saisie est comparée aux valeurs possibles dans la liste des SelectItem.


Cette validation utilise la méthode equals qui commence par comparer les types de objets. Il faut donc préserver une cohérence de type entre les types utilisés dans les beans et ceux utilisés dans les SelectItem.


Remarque :




Cette validation a été constatée avec MyFaces 1.1.5


Elle n&#8217;était pas déclenchée dans les versions antérieurs (bug ?)


</description>
          <pubDate>2007-02-26T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JSF/Validation</link>
          <guid isPermaLink="true">https://www.jtips.info/JSF/Validation</guid>
        </item>
      
    
      
        <item>
          <title>Zone de saisie HTML</title>
          <description>
Comment ajouter une zone de texte avec mise en forme dans une JSP ?


Problème


Il faudrait pouvoir faire de la mise en forme du texte saisi dans une aire de texte (multilignes) avec (au minimum) les fonctions suivantes :




bold/italique/souligné,


bullet, énumération,


caractères spéciaux,


&#8230;&#8203;






Solution


Cette fonction peut être intégrée sous la forme d&#8217;une balise (JSF) existante ou à développer. Il est possible aussi de personnaliser un textarea avec du javascript. Cette deuxième solution semble être la plus facile à mettre en oeuvre, car il existe plusieurs solutions prêtes à l&#8217;emploi.


Quelques pistes à étudier :




Apache Tomawhak inputHtml


TinyMCE


FCKeditor


Dojo


Xinha






Apache Tomawhak inputHtml


Cette solution semble a priori la plus simple à intégrer dès lors qu&#8217;on utilise JSF. Il suffit d&#8217;intégrer la librairie Tomawahk et d&#8217;utiliser la balise h:inputHtml.



&lt;%@ taglib uri="link:http://java.sun.com/jsf/html[http://java.sun.com/jsf/html]" prefix="h" %&gt;
&lt;%@ taglib uri="link:http://java.sun.com/jsf/core[http://java.sun.com/jsf/core]" prefix="f" %&gt;
&lt;%@ taglib uri="link:http://myfaces.apache.org/tomahawk[http://myfaces.apache.org/tomahawk]" prefix="t" %&gt;

&lt;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "link:http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd[http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd]"&gt;
&lt;html xmlns="link:http://www.w3.org/1999/xhtml[http://www.w3.org/1999/xhtml]"&gt;
&lt;head&gt;
  &lt;meta http-equiv="content-type" content="application/xhtml+xml; charset=utf-8" /&gt;
  &lt;title&gt;Exemple JSF avec Tomahawk&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;f:view&gt;
    &lt;h:form id="main"&gt;
      &lt;t:inputHtml id="text" value="{wysiwyg.htmlText}"&gt;&lt;/t:inputHtml&gt;
      &lt;h:commandButton value="OK" action="{wysiwyg.execute}"&gt;&lt;/h:commandButton&gt;
    &lt;/h:form&gt;
  &lt;/f:view&gt;
&lt;/body&gt;
&lt;/html&gt;



Cela fonctionne, mais on atteind les premières limites dès qu&#8217;on veut intégrer 2 zones formattables dans la même page. La 2° ne sera qu&#8217;une simple zone textarea !




TinyMCE


TinyMCE est une solution à base de javascript qui peut être intégrée avec n&#8217;importe quelle technique de dévelopement serveur. Pour java, TinyMCE peut être utilisé avec n&#8217;importe quel framework (struts, JSF,&#8230;&#8203;).


L&#8217;installation est simple, puisqu&#8217;il suffit de récupérer le répertoire tiny_mce dans l&#8217;archive zip ou tar.gz téléchargée, et de placer le répertoire dans notre application web (WebContent dans mon projet eclipse-wtp).


Ensuite, il suffit d&#8217;utiliser le script depuis les JSP ; l&#8217;initialisation se fait par un script synchrone en début de JSP :



&lt;script language="javascript"
        type="text/javascript"
        src="tiny_mce/tiny_mce.js"&gt;
&lt;/script&gt;
&lt;script language="javascript" type="text/javascript"&gt;
    tinyMCE.init({
        mode : "specific_textareas",
        theme : "simple"
    });
&lt;/script&gt;



Les options d&#8217;initialisation sont détaillées sur le site http://wiki.moxiecode.com/index.php/TinyMCE:Index. Puis chaque zone avec formattage est déclarée avec l&#8217;attribut mce_editable.



&lt;textarea id="elm1" rows="10" cols="40" mce_editable="true"&gt;&lt;/textarea&gt;



Ceci fonctionne lorsque la zone de texte est déclarée en HTML simple. Par contre, l&#8217;attribut mce_editable ne fonctionne pas avec JSF.


Dans JSF, il faut soit déclarer tous les textareas comme gérés par TinyMCE, soit déclarer explicitement la liste des zones (attention aux id !).



&lt;script language="javascript" type="text/javascript"&gt;
    tinyMCE.init({
        mode : "exact",
        elements : "main:text",
        theme : "simple"
    });
&lt;/script&gt;



Cette initialisation concernera uniquement la zone créée comme ceci :



&lt;h:form id="main"&gt;
    &lt;h:inputTextarea id="text" rows="10" cols="40" value="{wysiwyg.htmlText}"&gt;&lt;/h:inputTextarea&gt;
    &lt;h:commandButton value="OK" action="{wysiwyg.execute}"&gt;&lt;/h:commandButton&gt;
&lt;/h:form&gt;





Dojo Rich Text Editor


Dojo est une librairie javascript très populaire. Elle peut être utilisée avec tous les langages de programmation. Elle est progressivement intégrée à Tomawahk (sandbox, puis tomahawk 1.1.5).


L&#8217;intégration dans Tomawahk se fait par la balise d&#8217;initialisation.



 &lt;t:dojoInitializer require="dojo.widget.Editor" /&gt;



Ensuite, l&#8217;éditeur HTML est un textarea auquel on applique un style spécifique dojo :



 &lt;h:inputTextarea id="text" styleClass="dojo-Editor"
                  value="#{wysiwyg.htmlText}" /&gt;



Remarques :




Testé avec Tomawahk 1.1.5 snapshot (22/02/2007)


Génère des erreurs à l&#8217;initialisation


Semble difficile à personnaliser (dimensions, boutons)




</description>
          <pubDate>2007-02-22T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Web/InputHtml</link>
          <guid isPermaLink="true">https://www.jtips.info/Web/InputHtml</guid>
        </item>
      
    
      
        <item>
          <title>Popup avec JSF</title>
          <description>
Comment ouvrir une fenêtre de popup depuis une page JSF ?


Une application doit ouvrir une fenêtre indépendante en réaction au clic sur un command link. La fenêtre affiche des données liées aux données de la fenêtre appelante.
</description>
          <pubDate>2007-02-22T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JSF/popup</link>
          <guid isPermaLink="true">https://www.jtips.info/JSF/popup</guid>
        </item>
      
    
      
        <item>
          <title>Sessions HTTP</title>
          <description>
Sessions http


Les sessions http servent à stocker des objets qui restent disponibles tout au long de la navigation d&#8217;un utilisateur. Une session contient les beans JSF déclarés en scope session, mais aussi d&#8217;autres objets qui y auront été mis explicitement. La liaison entre l&#8217;utilisateur (ou plutôt son navigateur) et sa session est représentée par une valeur j_session_id, transmise au serveur via un cookie, dans l&#8217;URL ou dans le corps d&#8217;une requête POST.


Sérialisation de session

Les sessions peuvent faire l&#8217;objet d&#8217;entrées et sorties de la machine virtuelle lorsqu&#8217;on met en oeuvre des mécanismes de tolérance de panne ou de répartition de charge.


Par défaut, Tomcat sauvegarde les sessions dans un fichier sessions.ser afin de les restaurer lorsqu&#8217;on redémarre le serveur. Les sessions peuvent aussi transiter par le réseau lorsqu&#8217;on met Tomcat en cluster. Dans ces 2 cas, seuls les objets sérialisables pourront être enregistrés dans le fichier ou transmis par le réseau.


La sérialisation dans le fichier sessions.ser a été désactivée dans la configuration du projet :



 &lt;Context&gt;
   ...
   &lt;Manager pathname="" /&gt;
 &lt;/Context&gt;



Ces mécanismes imposent 2 bonnes pratiques :




Alléger la session : on ne place en session que les objets dont on a besoin tout au long de la navigation


Placer des objets sérialisables : on ne place en session que des objets sérialisables





Objets sérialisables

Pour être sérialisable, un objet doit avant tout implémenter l&#8217;interface Serializable.
Cependant, cela ne suffit pas, il faut aussi que tous ses champs répondent à au moins un des critères suivants :




type primitif (int, float,&#8230;&#8203;)


type sérialisable (String, Integer,&#8230;&#8203;, classe implémentant Serializable)


transient




Un champ déclaré transient sera exclu de la sérialisation. Il est particulièrement recommandé de déclarer ainsi les champs qui sont trop lourds (texte volumineux) ou les objets sans état.



</description>
          <pubDate>2007-02-22T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/J2EE/Session</link>
          <guid isPermaLink="true">https://www.jtips.info/J2EE/Session</guid>
        </item>
      
    
      
        <item>
          <title>Collections du JDK</title>
          <description>
L&#8217;API de collection est séparée en 2 types de fonctionnalités : les collections à proprement parler avec les interfaces Collection, List, Set et leurs sous interfaces, et les maps.
Les bonnes pratiques imposent de déclarer les variables avec une interface et de n&#8217;utiliser les classes que pour l&#8217;instanciation.


Classes et interfaces


Les couples classe / interface classiques sont les suivants :



 Collection&lt;String&gt; col = new ArrayList&lt;&gt;();
 List&lt;String&gt; list = new ArrayList&lt;&gt;();
 Set&lt;String&gt; set = new HashSet&lt;&gt;();
 Map&lt;String, String&gt; map = new HashMap&lt;&gt;();



L&#8217;utilisation de classes anciennes, comme Vector ou Hashtable, est généralement à proscrire.


Avec le JDK 11, la façon la plus simple d&#8217;instancier une collection est d&#8217;utiliser les nouvelles méthodes de fabrique.
Avec ces méthodes, on ne se préoccupe plus du type concret de la collection obtenu, il faut juste savoir qu&#8217;elle n&#8217;est pas modifiable.



 List&lt;String&gt; list = List.of("AZERTY", "QSDFGH", "WXCVBN");



Et ça fonctionne aussi pour créer une Map.



 Map&lt;String, String&gt; map = Map.of(
                                "A", "AZERTY",
                                "Q", "QSDFGH",
                                "W", "WXCVBN"
                              );





Boucles for each


Depuis le JDK 5, une nouvelle forme de boucle for, surnommée « for each » a été introduite.
Elle est beaucoup plus pratique que l&#8217;ancienne forme, mais présente des contraintes lorsqu&#8217;on veut manipuler la collection qu&#8217;on parcourt car l&#8217;itérateur n&#8217;est pas accessible.



 for (Type var : col) {
   col.remove(var);   // Ceci est interdit !!!
 }



Le problème rencontré est une ConcurrentAccessException lorsqu&#8217;on supprime un élément de la collection en cours d&#8217;itération.
L&#8217;ancienne boucle, du fait qu&#8217;elle sépare distinctement la collection de son itérateur, permet d&#8217;ajouter ou de retirer depuis le corps de la boucle de éléments de la collection.
Ceci fonctionne à condition de la faire via l&#8217;itérateur et non directement sur la collection.



 for (Iterator&lt;Type&gt; it = col.iterator();it.hasNext();) {
   Type var =  it.next();
   ...
   col.remove(var);   // Ceci est toujours interdit
   it.remove();       // Ceci est autorisé
   ...
 }





Tri


Le tri d&#8217;une liste se fait en passant la liste à trier à la méthode java.util.Collections.sort(list).
Cette méthode trie les objets de la liste selon le résultat de leurs méthodes compareTo().
Cela signifie donc que ces objets doivent implémenter l&#8217;interface Comparable.



Collections.sort(list);



Pour trier des listes d&#8217;objets qui n&#8217;implémente pas cette interface, on peut choisir d&#8217;utiliser un Comparator et en passer une instance à la méthode java.util.Collections.sort(list, comparator).




Stream et lambda


Avec le JDK 8, l&#8217;introduction de l&#8217;API Stream a changé par mal de choses dans la manipiulation des collections.



newCol = col.stream()
            .filter(...)
            .sort()
            .toList();



Pour les comparateurs, la notation lambda simplifie les choses, puisque Comparator est une interface fonctionnelle.



Comparator comp = (Person p1, Person p2) -&gt; p2.getAge() - p1.getAge();



Et le JDK 8 a introduit des méthodes qui facilite l&#8217;écriture de comparateurs.



Comparator comp = Comparator.comparing(Person::getAge).reversed();





Implémentations de List


Comparée à LinkedList, ArrayList presque toujours meilleure, quelle que soit la taille de la liste.
Le seul cas où LinkedList est éventuellement meilleure, c&#8217;est quand on modifie les éléments intermédiaires de la liste :
ajout ou suppression d&#8217;objets en début ou au milieu de la liste.


java.util.concurrent.CopyOnWriteArrayList est une variante thread-safe de java.util.ArrayList.
Chaque méthode de modification fait une copie du tableau interne.




Implémentations de Set


L&#8217;implémentation par défaut est HashSet.


Avec LinkedHashSet, les objets sont itérés dans l&#8217;ordre de leur ajout.


TreeSet implémente SortedSet qui, comme sont nom l&#8217;indique, stocke les objets triés dans leur ordre naturel ou selon un comparateur.


EnumSet est utilisé pour les types énumérés.
Il est meilleur pour ça parce qu&#8217;il ne passe pas par equals() mais ==.


Il y a deux implémentations thread-safe :




CopyOnWriteArraySet fait une copie de son tableau interne à chaque modification,


ConcurrentSkipListSet implémente SortedSet.




Il y a une troisième option, à partir d&#8217;un ConcurrentHashMap.



Set&lt;String&gt; set = ConcurrentHashMap.newKeySet();





Implémentations de Map


L&#8217;implémentation par défaut est HashMap.


Avec LinkedHashMap, les objets sont itérés dans l&#8217;ordre de leur ajout.


TreeMap implémente SortedMap qui, comme sont nom l&#8217;indique, stocke les clés triées dans leur ordre naturel ou selon un comparateur.


EnumMap est utilisé pour les clés énumérés.
IdentityHashMap compare aussi les clés par ==, ce qui est performant mais contraire au contrat de Map.


Les implémentations thread-safe :




ConcurrentHashMap est l&#8217;implémentation par défaut,


ConcurrentSkipListMap implémente SortedMap.






Librairies tierces




Eclipse Collections


Apache Commons Collections


Guava






Références




Oracle - Collections Framework Overview


Développons en Java - Les collections




</description>
          <pubDate>2007-02-22T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/Collection</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/Collection</guid>
        </item>
      
    
      
        <item>
          <title>Sécurité JMX sous JBoss</title>
          <description>
Les accès JMX (consoles, twiddle, RMI) permettent de manipuler les MBeans déployés dans JBoss. Celà leur confère une puissance sans limite vis-à-vis du serveur d&#8217;application : arrêt du serveur, déploiement d&#8217;applications, inspection des composants,&#8230;&#8203;


Il est donc absolument nécessaire de sécuriser tous les accès JMX au serveur.


Sécuriser les consoles


Les consoles JMX (jmx-console) et Web (web-console) sont des applications Web traditionnelles, elles doivent donc être sécurisées en tant que tel.
Les configurations fournies avec JBoss permettent d&#8217;activer l&#8217;authentification très rapidement puisque tout le paramétrage est présent dans les fichiers web.xml et jboss-web.xml, mais en commentaire.



  &lt;!-- web.xml --&gt;
  &lt;security-constraint&gt;
    &lt;web-resource-collection&gt;
      &lt;web-resource-name&gt;HtmlAdaptor&lt;/web-resource-name&gt;
      &lt;description&gt;An example security config that only allows users with the
        role JBossAdmin to access the HTML JMX console web application
      &lt;/description&gt;
      &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
      &lt;http-method&gt;GET&lt;/http-method&gt;
      &lt;http-method&gt;POST&lt;/http-method&gt;
    &lt;/web-resource-collection&gt;
    &lt;auth-constraint&gt;
      &lt;role-name&gt;JBossAdmin&lt;/role-name&gt;
    &lt;/auth-constraint&gt;
  &lt;/security-constraint&gt;

  &lt;login-config&gt;
     &lt;auth-method&gt;BASIC&lt;/auth-method&gt;
     &lt;realm-name&gt;JBoss JMX Console&lt;/realm-name&gt;
  &lt;/login-config&gt;

  &lt;security-role&gt;
     &lt;role-name&gt;JBossAdmin&lt;/role-name&gt;
  &lt;/security-role&gt;



Attention cependant, à utiliser le même "security-domain" pour les deux consoles, ce qui n&#8217;est pas le cas avec les exemples fournis. Il faut veiller aussi à changer les login et password par défaut dans les fichiers properties.



  &lt;!-- jboss-web.xml --&gt;
  &lt;security-domain&gt;java:/jaas/jmx-console&lt;/security-domain&gt;



La sécurité de ces applications peut être renforcée en activant l&#8217;accès SSL.




Sécuriser le JMX Invoker


Authentification

Les accès via RMI et twiddle passent par le jmx-invoker. Pour le sécuriser, il faut décommenter la partie proposée en fin du fichier jmx-invoker-service.xml :



  &lt;!-- deploy/jmx-invoker-service.xml --&gt;
  &lt;descriptors&gt;
    &lt;interceptors&gt;
      &lt;interceptor code="org.jboss.jmx.connector.invoker.AuthenticationInterceptor"
                   securityDomain="java:/jaas/jmx-console"/&gt;
    &lt;/interceptors&gt;
  &lt;/descriptors&gt;



Un fois cette partie activée, les accès RMI nécessitent du authentification via un ClientLoginModule ou via la connexion à JNDI. Les accès par script (twiddle ou shutdown) nécessitent de renseigner les arguments -u et -p.



Restrictions réseau

La sécurité des accès distants peut encore être renforcée par un filtrage au niveau système.


Le jmx-invoker utilise un protocole de communication RMI qui s&#8217;appuie sur un invoker jrmp, pooled, iiop ou http. En standard, c&#8217;est l&#8217;invoker jrmp qui est utilisé. Par conséquent, les accès twiddle ou RMI se font via le port 4444. Si on crée un nouvel invoker, sur un autre port, il est alors possible de faire passer uniquement les invocations JMX par ce nouvel invoker. Il reste alors à paramétrer le firewall système du serveur pour qu&#8217;il n&#8217;accepte que les adresses IP des postes d&#8217;administrateur sur ce port.


Pour créer un nouvel invoker, on duplique l&#8217;invoker jrmp, du fichier conf/jboss-service.xml, en changeant le port et le nom du MBean ; puis on modifie le ProxyFactory du JmxInvoker.



  &lt;!-- deploy/jmx-invoker-service.xml --&gt;
  &lt;mbean code="org.jboss.invocation.jrmp.server.JRMPProxyFactory"
     name="jboss.jmx:type=adaptor,name=Invoker,protocol=jrmp,service=proxyFactory"&gt;
     &lt;depends optional-attribute-name="InvokerName"&gt;jboss:service=invoker,type=admin&lt;/depends&gt;
  &lt;/mbean&gt;

  &lt;!-- RMI/JRMP invoker for Admin --&gt;
  &lt;mbean code="org.jboss.invocation.jrmp.server.JRMPInvoker"
     name="jboss:service=invoker,type=admin"&gt;
     &lt;attribute name="RMIObjectPort"&gt;4449&lt;/attribute&gt;
     &lt;attribute name="ServerAddress"&gt;${jboss.bind.address}&lt;/attribute&gt;
     &lt;depends&gt;jboss:service=TransactionManager&lt;/depends&gt;
  &lt;/mbean&gt;



Toutes ces choses peuvent d&#8217;automatiser par un petit script shell.


Testé avec JBoss 4.0.5


&lt;span class="error"&gt;Attention : la clé de tri par défaut « SecureJMX » écrase la précédente clé « JBoss/SecureJMX ».&lt;/span&gt;



</description>
          <pubDate>2007-02-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/SecureJMX</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/SecureJMX</guid>
        </item>
      
    
      
        <item>
          <title>Acegi/ntlm</title>
          <description>
Acegi permet par le biais de filtres de choisir le mode d&#8217;authentification et coupler plusieurs modes d&#8217;authentification en fallback.


Voici donc comment configurer Acegi pour utiliser l&#8217;authentification par le protocole NTLM. Cette fonction n&#8217;est pas supportée en standard dans Acegi, cependant il  est possible de trouver les composants nécessaires dans les forums du projet.


L&#8217;implémentation du filtre NTLMProcessingFilter retenue a été téléchargée à partir de cette URL : http://opensource.atlassian.com/projects/spring/secure/attachment/11937/20060906_Acegi_NTLM.zip


Essentiellement 4 classes  composent cette solution :




public class AbstractProcessingFilter implements Filter, InitializingBean, ApplicationEventPublisherAware, MessageSourceAware


public class NtlmProcessingFilter extends AbstractProcessingFilter


public class NtlmProcessingFilterEntryPoint implements AuthenticationEntryPoint


public class BeginNtlmHandshakeException extends AuthenticationException









Lorsque la chaine de filtre invoque la méthode doFilter de la classe NtlmProcessingFilter :


1-	la méthode requiresAuthentication vérifie si l&#8217;authentification est requise


2-	la méthode onPreAuthentication s&#8217;assure que le navigateur a fourni les informations NTLM


3-	la méthode attemptAuthentication vérifie les informations NTLM auprès du contrôleur de domaine


Scenario exceptionnel :


2a-	les informations NTLM ne sont pas présentes : une exception BeginNtlmHandshakeException est lancée.


2b-	Le filtre ExceptionTranslationFilter appelle la méthode commence(…) de NtlmProcessingFilterEntryPoint qui renvoie un code HTTP 401 en spécifiant le protocole NTLM


2c-	Le navigateur réeffectue sa requête en incluant les données NTLM


FILTRE PAR IP


Le filtre NTLM est souvent couplé à un filtre d&#8217;authentification par formulaire. Dans ce cas, tout utilisateur ne se trouvant pas dans un domaine NT voit d&#8217;abord apparaître 3 fois successivement une fenêtre lui demandant son login et son mot de passe NT, puis arrive sur le filtre d&#8217;authentification par formulaire..


Pour contourner ce problème, un attribut a été rajouté au filtre NLTM : subnetMasks


La classe NtlmProcessingFilterWithIp hérite donc de NtlmProcessingFilter en redéfinissant la méthode requiresAuthentication(…) :


Cette méthode vérifie si l&#8217;IP de la machine appelante appartient à l&#8217;un des sous-réseaux spécifiés par les masques de l&#8217;attribut subnetMasks. Le cas échéant elle retourne false, ce qui a pour effet de passer la main au filtre suivant.




CUSTOMEDITOR


La syntaxe pour la définition du nouvel attribut subnetMasks est la suivante



		&lt;property name="subnetMasks"&gt;
			&lt;value&gt;
				aaa.bbb.ccc.ddd
				eee.fff.ggg.hhh
				…
				// some comments
			&lt;/value&gt;
		&lt;/property&gt;



Spring permet d&#8217;associer pour un type donné d&#8217;attribut une implémentation héritant de PropertyEditorSupport qui se charge de transformer la chaine de caractère contenue dans le fichier de configuration Acegi en une instance du type de l&#8217;attribut.


Deux classes ont donc été créées :




ListIps.java


ListIpsEditor.java




La déclaration de ce "CustomEditor" est faite dans le fichier de configuration d&#8217;acegi:



	&lt;bean id="customEditorConfigurer"
	    class="org.springframework.beans.factory.config.CustomEditorConfigurer"&gt;
	  &lt;property name="customEditors"&gt;
	    &lt;map&gt;
	      &lt;entry key="ch.oosphere.acegisecurity.ui.ntlm.ListIps"&gt;
	        &lt;bean class="ch.oosphere.acegisecurity.ui.ntlm.ListIpsEditor" /&gt;
	      &lt;/entry&gt;
	    &lt;/map&gt;
	  &lt;/property&gt;
	&lt;/bean&gt;



</description>
          <pubDate>2006-12-19T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/acegi-ntlm</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/acegi-ntlm</guid>
        </item>
      
    
      
        <item>
          <title>Sécurité avec acegi</title>
          <description>
Paramétrage général


Le filtre principal se paramètre dans le fichier web.xml.



&lt;filter&gt;
  &lt;filter-name&gt;Acegi&lt;/filter-name&gt;
    &lt;filter-class&gt;org.acegisecurity.util.FilterToBeanProxy&lt;/filter-class&gt;
    &lt;init-param&gt;
      &lt;param-name&gt;targetClass&lt;/param-name&gt;
      &lt;param-value&gt;org.acegisecurity.util.FilterChainProxy&lt;/param-value&gt;
    &lt;/init-param&gt;
  &lt;/filter&gt;
  &lt;filter-mapping&gt;
    &lt;filter-name&gt;Acegi&lt;/filter-name&gt;
    &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
  &lt;/filter-mapping&gt;
&lt;/filter&gt;



Dans un fichier de contexte Spring (securityContext.xml, par exemple), on paramètre la chaîne de filtres.



 &lt;bean id="filterChainProxy"
   class="org.acegisecurity.util.FilterChainProxy"&gt;
   &lt;property name="filterInvocationDefinitionSource"&gt;
     &lt;value&gt;
       CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
       PATTERN_TYPE_APACHE_ANT
       /**=aFilter1,aFilter2,aFilter3
     &lt;/value&gt;
   &lt;/property&gt;
 &lt;/bean&gt;



Les filtres présents dans la liste seront appelés dans l&#8217;ordre de déclaration.


L&#8217;enchaînement classique de filtre est le suivant :




httpSessionContextIntegrationFilter : permet de stocker les informations d&#8217;authentification en session


xxxProcessingFilter : impose une technique d&#8217;authentification (BASIC, FORM,&#8230;&#8203;)


securityContextHolderAwareRequestFilter : permet de stocker les informations d&#8217;authentification dans la requête, ce qui permet d&#8217;utiliser l&#8217;API standard avec request.getUserPrincipal() et request.isUserInRole().


exceptionTranslationFilter : permet de gérer les erreurs de sécurité, comme une tentative d&#8217;accès avant le login


filterInvocationInterceptor : gère les autorisations






Authentification Web BASIC


httpSessionContextIntegrationFilter

La déclaration de ce filtre est simple et classique :



 &lt;bean id="httpSessionContextIntegrationFilter"
   class="org.acegisecurity.context.HttpSessionContextIntegrationFilter" /&gt;



Ce filtre est nécessaire dans les applications Web, pour éviter à l&#8217;utilisateur de saisir son login à chaque requête.



basicProcessingFilter

Le xxxProcessingFilter prend en charge l&#8217;authentification standard http « BASIC ». Le login et le mot de passe sont saisis dans une boite de dialogue du navigateur. Celui-ci se charge de transmettre les informations dans la requête. Les informations sont habituellement gardées en cache dans le navigateur.



 &lt;bean id="basicProcessingFilter"
   class="org.acegisecurity.ui.basicauth.BasicProcessingFilter"&gt;
   &lt;property name="authenticationManager"&gt;
     &lt;ref local="authenticationManager" /&gt;
   &lt;/property&gt;
   &lt;property name="authenticationEntryPoint"&gt;
     &lt;bean id="basicProcessingFilterEntryPoint"
       class="org.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint"&gt;
       &lt;property name="realmName"&gt;
         &lt;value&gt;Calcul Realm&lt;/value&gt;
       &lt;/property&gt;
     &lt;/bean&gt;
   &lt;/property&gt;
 &lt;/bean&gt;



Les propriétés à déclarer sont l&#8217;authenticationManager, qui indique la technique de stockage du login et du password, et l&#8217;authenticationEntryPoint, qui spécifie la technique d&#8217;authentification.


L&#8217;authenticationManager liste des providers, capable de lui fournir des couples user/password. Dans cet exemple, on utilise un provider simple qui stocke les couples dans un fichier properties.



 &lt;bean id="authenticationManager"
   class="org.acegisecurity.providers.ProviderManager"&gt;
   &lt;property name="providers"&gt;
     &lt;list&gt;
       &lt;bean
         class="org.acegisecurity.providers.dao.DaoAuthenticationProvider"&gt;
         &lt;property name="userDetailsService"&gt;
           &lt;bean id="userDetailsService"
             class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl"&gt;
             &lt;property name="userProperties"&gt;
               &lt;bean
                 class=
             "org.springframework.beans.factory.config.PropertiesFactoryBean"&gt;
                 &lt;property name="location"
                   value="/WEB-INF/users.properties" /&gt;
               &lt;/bean&gt;
             &lt;/property&gt;
           &lt;/bean&gt;
         &lt;/property&gt;
       &lt;/bean&gt;
     &lt;/list&gt;
   &lt;/property&gt;
 &lt;/bean&gt;



Le fichier properties a le format suivant :



alexis=hassler,ROLE_SUPERVISOR
scott=tiger,ROLE_USER




securityContextHolderAwareRequestFilter

Ce filtre stocke les informations de sécurité de spring dans la request. Pour ce faire, il utilise la technique des wrappers.



 &lt;bean id="securityContextHolderAwareRequestFilter"
   class="org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter"/&gt;




exceptionTranslationFilter

Ce filtre gère les exceptions, en particulier les erreurs de type « accès refusé ». En cas d&#8217;exception de ce type, la requête est orientée vers un authenticationEntryPoint (cf. xxxProcessingFilter).



 &lt;bean id="exceptionTranslationFilter"
   class="org.acegisecurity.ui.ExceptionTranslationFilter"&gt;
   &lt;property name="authenticationEntryPoint"&gt;
     &lt;bean
       class="org.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint"&gt;
       &lt;property name="realmName"&gt;
         &lt;value&gt;Calcul Realm&lt;/value&gt;
       &lt;/property&gt;
     &lt;/bean&gt;
   &lt;/property&gt;
   &lt;property name="accessDeniedHandler"&gt;
     &lt;bean
       class="org.acegisecurity.ui.AccessDeniedHandlerImpl"&gt;
       &lt;property name="errorPage" value="/accessDenied.jsp" /&gt;
     &lt;/bean&gt;
   &lt;/property&gt;
 &lt;/bean&gt;




filterInvocationInterceptor

Ce dernier filtre gère les autorisations. La propriété « authenticationManager » indique la technique de stockage des login et des rôles ; il peut être identique à celui du xxxProcessingFilter.



   &lt;property name="authenticationManager"
     ref="authenticationManager" /&gt;



La propriété « accessDecisionManager » permet de définir une liste de « voters ». Chacun d&#8217;entre eux accepte ou refuse l&#8217;accès ; la décision globale peut être accordée soit si 1 voter a accepté, soit à la majorité, soit à l&#8217;unanimité.



   &lt;property name="accessDecisionManager"&gt;
     &lt;bean class="org.acegisecurity.vote.AffirmativeBased"&gt;
       &lt;property name="allowIfAllAbstainDecisions"
         value="false" /&gt;
       &lt;property name="decisionVoters"&gt;
         &lt;list&gt;
           &lt;bean class="org.acegisecurity.vote.RoleVoter" /&gt;
         &lt;/list&gt;
       &lt;/property&gt;
     &lt;/bean&gt;
   &lt;/property&gt;



Enfin, la 3° propriété définit les rôles permettant d&#8217;accéder aux URLs.



   &lt;property name="objectDefinitionSource"&gt;
     &lt;value&gt;
       CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
       PATTERN_TYPE_APACHE_ANT
       /admin/=ROLE_ADMIN
       /*=ROLE_USER
     &lt;/value&gt;
   &lt;/property&gt;






Authentification Anonyme


L&#8217;authentification anonyme permet d&#8217;autoriser certaines ressources aux utilisateurs non authentifiés explicitement.
Il faut d&#8217;abord déclarer un filtre d&#8217;authentification anonyme qui crée l&#8217;utilisateur « anonymous », avec le rôle « ROLE_ANONYMOUS », ainsi qu&#8217;un anonymousAuthenticationProvider qui fait référence à la clé du filtre.



&lt;bean id="anonymousProcessingFilter"
      class="org.acegisecurity.providers.anonymous.AnonymousProcessingFilter"&gt;
  &lt;property name="key"&gt;
    &lt;value&gt;ano&lt;/value&gt;
  &lt;/property&gt;
  &lt;property name="userAttribute"&gt;
    &lt;value&gt;anonymousUser,ROLE_ANONYMOUS&lt;/value&gt;
  &lt;/property&gt;
&lt;/bean&gt;

&lt;bean id="anonymousAuthenticationProvider"
      class="org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider"&gt;
  &lt;property name="key"&gt;
    &lt;value&gt;ano&lt;/value&gt;
  &lt;/property&gt;
&lt;/bean&gt;



Enfin, les ressources autorisées aux utilisateurs anonymes sont associées au rôle ROLE_ANONYMOUS dans le filterInvocationInterceptor.



&lt;bean id="filterInvocationInterceptor"
      class="org.acegisecurity.intercept.web.FilterSecurityInterceptor"&gt;
  &lt;property name="authenticationManager"&gt;
    &lt;ref bean="authenticationManager"/&gt;
  &lt;/property&gt;
  &lt;property name="accessDecisionManager"&gt;
    &lt;ref local="accessDecisionManager"/&gt;
  &lt;/property&gt;
  &lt;property name="objectDefinitionSource"&gt;
    &lt;value&gt;
      CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
      PATTERN_TYPE_APACHE_ANT
      /index.jsp=ROLE_ANONYMOUS,ROLE_USER
      /acegilogin.jsp*=ROLE_ANONYMOUS,ROLE_USER
      /**=ROLE_USER
    &lt;/value&gt;
  &lt;/property&gt;
&lt;/bean&gt;



</description>
          <pubDate>2006-12-19T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Spring/Acegi</link>
          <guid isPermaLink="true">https://www.jtips.info/Spring/Acegi</guid>
        </item>
      
    
      
        <item>
          <title>JSF/Converters</title>
          <description>
Les données affichées dans une page JSF possèdent deux représentations :




Côté client (code HTML), elles existent sous la forme de chaînes de caractères


Côté serveur (dans les backing beans), les données peuvent en revanche être typées (entier, date&#8230;&#8203;)




Il est donc nécessaire de pouvoir convertir ces données depuis le modèle Java vers le modèle HTML (composants d&#8217;affichage et de saisie) et réciproquement (cela concerne les composants de saisie lors du submit d&#8217;un formulaire).


Convertisseurs standards


JSF est livré avec un ensemble de convertisseurs standards.


Convertir une valeur numérique

Le NumberConverter

Ce convertisseur donne la possibilité de convertir des numériques suivant trois types différents : number (type par défaut), currency ou percent (et non pas percentage comme indiqué dans certaine documentation). En fonction du type retenu, on peut appliquer un formatage particulier, comme par exemple :




Affichage d&#8217;un prix





&lt;h:inputText value="#{produitBean.prix}"&gt;
  &lt;f:convertNumber type="currency"
                   groupingUsed="true"
                   currencyCode="EUR"
                   currencySymbol="€"
                   maxIntegerDigits="5"
                   maxFractionDigits="2"/&gt;
&lt;/h:inputText&gt;





Affichage d&#8217;un pourcentage





&lt;h:outputText value="#{produitBean.tva}"&gt;
  &lt;f:convertNumber type="percent"
                   minFractionDigits="1"
                   maxFractionDigits="1"/&gt;
&lt;/h:outputText&gt;



Remarque : dans le cas de composants de saisie, ce convertisseur ne fonctionne qu&#8217;avec les types double et long. Pour les autres types (float, int, short&#8230;&#8203;), il faut utiliser le tag &lt;f:converter&gt;



Autres convertisseurs numériques

En lecture seule (composants d&#8217;affichage tel que &lt;h:outputText/&gt;), les types int,short et float étant compatibles avec les types long et double, il est possible d&#8217;utiliser le tag &lt;f:convertNumber/&gt;. En revanche, une IllegalArgumentException est levée avec les composants de saisie, car le ConvertNumber génère un long si la valeur saisie est un entier, et un double si la valeur saisie est un réel. Ces valeurs ne peuvent donc pas être injectées dans des variables dont les types sont de moindre précision (int, short, float). Il convient donc d&#8217;utiliser un autre converter dans ce cas. Il existe plusieurs possibilités pour utiliser le convertisseur souhaité :




Attribut converter





&lt;h:inputText value="#{produitBean.quantite}"
             converter="javax.faces.convert.IntegerConverter" /&gt;





Tag &lt;f:converter/&gt;





&lt;h:inputText value="#{produitBean.quantite}"
             converter="Integer" /&gt;



Cette solution nécessite d&#8217;incrire le convertisseur dans le fichier de configuration JSF :



&lt;converter&gt;
  &lt;converter-id&gt;Integer&lt;/converter-id&gt;
  &lt;converter-class&gt;javax.faces.convert.IntegerConverter&lt;/converter-class&gt;
&lt;/converter&gt;







Convertisseurs personnalisés


Il est possible d&#8217;étendre l&#8217;API afin d&#8217;ajouter ses propres convertisseurs.


</description>
          <pubDate>2006-11-24T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JSF/Converters</link>
          <guid isPermaLink="true">https://www.jtips.info/JSF/Converters</guid>
        </item>
      
    
      
        <item>
          <title>JSF/implementation</title>
          <description>
JSF est une spécification optionnelle de J2EE 1.4 et intégrée à JavaEE 5.


Plusieurs implémentations du standard sont disponibles, ainsi que des librairies d&#8217;extension.


Composants standard


Ce sont des implémentations de la spécification (JSR-127), avec les composants standard (taglib core et html).


Sun JSF RI



C&#8217;est l&#8217;implémentation de référence de Sun, disponible dans la toute dernière version de la spécification (fin 2006 = 1.2).





Apache MyFaces



C&#8217;est une implémentation très couramment utilisée. La version est généralement en décalage, puisque fin 2006, on n&#8217;avait encore que la 1.1.


Elle est intégrée à JBoss 4.0.


https://myfaces.apache.org/







Composants complémentaires


Remarque : 3 librairies chez Apache qui, pour l&#8217;instant ont des zones de recouvrement.


Apache Tomahawk



Extension de JSF compatible avec les implémentations standard


Certains composants remplacent les composants standard


Support de Struts Tiles





Apache Tobago



Framework JSF intégré, largement incompatible avec les autres extensions (incompatibilité de renderkit)





Oracle ADF



Composants en remplacement ou en ajout du standard


Plusieurs type de tableaux et de listes


Validateurs et convertisseur coté client (javascript)


Support des principes d&#8217;accessibilité


Rendu partiel de page


http://www.oracle.com/technology/products/adf/





Apache Trinidad



Version open source de ADF Faces


En cours d&#8217;intégration chez Apache







Composants AJAX


AJAX devrait être totalement intégré à JSF pour la version 2.0 (JavaEE 6).
En attendant, pour développer des applications et sites au rendu un peu dynamique, nous devons utiliser des extensions.


L&#8217;utilisation de frameworks AJAX autonomes de JSF est dangereuse et peu poser des problèmes de gestion de l&#8217;état des composants.
Par ailleurs, quelques librairies se contentent d&#8217;une intégration minimale d&#8217;AJAX, avec un champ de type "suggest".


Nous avons privilégié la fonction de rendu partiel de page.


Ajax4JSF



Ajouts spécifiquement AJAX


Facile d&#8217;utilisation pour les boutons, plus délicat pour d&#8217;autres événements


Le projet est passé sur la coupe de JBoss


https://en.wikipedia.org/wiki/Ajax4jsf





Rich Client Faces



Les premiers retours semblent prometteurs&#8230;&#8203;


Licence open source LGPL


Développé par la société Vedana





Sun DynaFaces



En cours de développement, premières versions early access (fin 2006)


https://www.oracle.com/technical-resources/articles/javaee/ajax-javaserverfaces.html





IceFaces



Redéfinit les composants standard de JSF, avec des attributs supplémentaires


Existe en version Open Source ou en version commerciale


Semble assez riche et bien documenté


Rendu partiel et validation partielle de formaulaire


Push : mise à jour de page initiées par le serveur


Drag &amp; drop et autres effets, grâce à l&#8217;intégration de script.aculo.us


https://icesoft.com/icefaces/





Exadel RichFaces / JBoss RichFaces



Initialement développé sous licence commerciale par la société Exadel


Annonce d&#8217;une licence Open Source via JBoss.org


https://richfaces.jboss.org/





</description>
          <pubDate>2006-11-12T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JSF/implementation</link>
          <guid isPermaLink="true">https://www.jtips.info/JSF/implementation</guid>
        </item>
      
    
      
        <item>
          <title>MBeans</title>
          <description>
Le composant essentiel de JBoss 4 est le MBean. Tous les composants des couches système de JBoss (deployer, invoker, manager,&#8230;&#8203;) sont implémentés sous forme de MBeans et déployés dans des services (fichiers .sar).


Voyons comment développer nos propres MBeans et déployer nos propres services.


Développement d&#8217;un MBean simple


Un MBean JBoss est constitué d&#8217;une interface de management et d&#8217;une classe d&#8217;implémentation.


Interface de management

L&#8217;interface de management est une interface qui




hérite de org.jboss.system.Service


OU hérite de org.jboss.system.ServiceMBean


OU reprend simplement les méthodes de org.jboss.system.Service (create, destroy, start, stop)





package fr.sewatech.jboss.service;

import org.jboss.system.Service;

public interface JobServiceMBean extends Service {
    String getJobName();
    void setJobName(String name);

    void doTheJob();
}



Les interfaces Services et ServiceMBean sont dans le fichier lib/jboss-system.jar.



Classe d&#8217;implémentation

La classe du MBean implémente l&#8217;interface de management.



package fr.sewatech.jboss.service;

import org.apache.log4j.Logger;

public class JobService implements JobServiceMBean {

    private static Logger logger = Logger.getLogger(JobService.class);

    private String jobName;

    public void doTheJob() {
        logger.info("The Job " + jobName + " is done.");
    }

    public String getJobName() {
        return jobName;
    }

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

    public void create() throws Exception {
        logger.info("Service created");
    }

    public void destroy() {
        logger.info("Service destroyed");
    }

    public void start() throws Exception {
        logger.info("Service started");
    }

    public void stop() {
        logger.info("Service stopped");
    }
}






Déploiement du service


Un service est un fichier d&#8217;archive ou répertoire avec une extension sar et contenant un fichier META-INF/jboss-service.xml.


Descripteur de déploiement

Ce fichier décrit les MBeans déployés avec le service ; il permet d&#8217;initialiser les propriétés des MBeans.



&lt;?xml version="1.0" encoding="UTF-8"?&gt;

&lt;server&gt;
  &lt;mbean code="fr.sewatech.jboss.service.JobService" name="sewatech:service=job"&gt;
    &lt;attribute name="JobName"&gt;Hello&lt;/attribute&gt;
  &lt;/mbean&gt;
&lt;/server&gt;




Déploiement

Le déploiement se fait comme pour tout autre fichier d&#8217;archive, par copie dans le répertoire deploy (ou équivalent) de la configuration.


Lors du déploiement, les méthodes create() et start() sont appelée (dans cet ordre). Lors d&#8217;un retrait (undeploy), les méthodes stop() et destroy() sont appelées.





Appel du MBean


Une fois le services déployé, les MBeans peuvent être appelé depuis du code java, la jmx-console et la commande twiddle.


jmx-console

Les MBeans déployés apparaissent dans la page de garde de la jmx-console. Le détail du MBean montre ses propriétés et ses opérations.







Cette page permet de modifier les propriétés en RW et d&#8217;invoquer les opérations.



twiddle

Le répertoire bin de JBoss contient les scripts de lancement (run.bat, run.sh) et d&#8217;arrêt (shutdown.bat, shutdown.sh), mais aussi un script d&#8217;accès à JMX (twiddle.bat, twiddle.sh).


L&#8217;appel d&#8217;un opération se fait par la commande invoke.



twiddle invoke "sewatech:service=job" doTheJob



La modification d&#8217;une propriété se fait par la commande set.



twiddle set "sewatech:service=job" JobName hello



La lecture d&#8217;une propriété se fait par la commande get.



twiddle get "sewatech:service=job" JobName



Cette commande peut être utilisée en local ou à distance, en précisant l&#8217;adresse de l&#8217;annuaire JNDI dans l&#8217;option -s.


Remarque : n&#8217;oubliez pas de sécuriser l&#8217;accès twiddle !



java distant / RMI

Il est possible d&#8217;accéder aux MBeans à distance depuis une classe java, en utilisant l&#8217;adaptateur RMI (RMIAdaptor).



package fr.sewatech.jboss.jmxinvoke;

import javax.management.Attribute;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.naming.InitialContext;

public class RMIAccess {
    public static void main(String[] args) throws Exception
    {
       InitialContext ic = new InitialContext();
       MBeanServerConnection server = (MBeanServerConnection) ic.lookup("jmx/invoker/RMIAdaptor");

       // Get the MBeanInfo for the JNDIView MBean
       ObjectName job = new ObjectName("sewatech:service=job");
       server.setAttribute(job, new Attribute("JobName", "hi"));
       server.invoke(job, "doTheJob", null, null);
   }
}




java local

Depuis un autre composant déployé dans JBoss (servlet, EJB, MBean), l&#8217;accès est plus simple et ne nécessite pas d&#8217;accès au contexte JNDI. Il suffit d&#8217;utiliser la factory JMX :



       javax.management.MBeanServer server = (javax.management.MBeanServer) javax.management.MBeanServerFactory.findMBeanServer(null).get(0);



ou d&#8217;utiliser les classes spécifiques à JBoss :



       javax.management.MBeanServer server = org.jboss.mx.util.MBeanServerLocator.locateJBoss();



Le reste du code (setAttribute, invoke) est similaire à l&#8217;accès RMI.



</description>
          <pubDate>2006-11-01T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/Service</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/Service</guid>
        </item>
      
    
      
        <item>
          <title>JBoss en mode sécurisé</title>
          <description>
Principe


Cas général

L&#8217;activation du SecurityManager au lancement d&#8217;un programme java permet de s&#8217;assurer que celui-ci ne fera que des actions autorisées par l&#8217;administrateur. L&#8217;exemple le plus grossier est l&#8217;interdiction dans un composant JEE d&#8217;une commande  comme System.exit();. &lt;code&gt;. Et ne pensez pas que ça n&#8217;arrive jamais, il suffit d&#8217;un copier/coller sans trop réfléchir&#8230;&#8203;
&lt;/code&gt;


Le premier contrôle est évidemment du ressort des développeurs qui peuvent éviter ce genre de bourdes par des revues de code et de vérifications automatisées, avec des outils comme checkstyle.


D&#8217;autres cas peuvent être litigieux, car prévus par les développeurs, mais non déclarés à l&#8217;exploitation. On peut penser par exemple à des accès disque ou réseau.



Intérêt dans JBoss

Même en faisant une confiance absolue aux développeurs, il peut être utile de limiter les actions autorisées aux applications.


JBoss permet de déployer des applications à chaud, via sa console jmx. Les applications déployées de cette façon ne sont pas forcément dans le répertoire de JBoss, voire sur un autre serveur. Interdire toute action à une application déployée depuis une localisation non prévue permet de limiter les risques !





Mise en place


Cas général

Le SecurityManager est activé au lancement de java, en passant les paramètres suivants :



-Djava.security.manager
-Djava.security.policy=myapp.policy



Le fichier &lt;code&gt;myapp.policy contient les permissions accordées à l&#8217;application. Les permissions par défaut sont dans le fichier jre/lib/java.policy.



JBoss

Au lancement de JBoss, on peut passer ces paramètres via la variable JAVA_OPTS :



set JAVA_OPTS=-Djava.security.manager -Djava.security.policy="./server/secured/conf/server.policy"



Jusqu&#8217;à la version 4.0.2, JBoss fournissait un fichier server.policy dans le répertoire conf des configurations. Ce fichier par défaut n&#8217;existe plus depuis la version 4.0.3, ce qui est plutôt une bonne chose car il ne servait à rien puisqu&#8217;il autorisait tout à toutes les classes !



grant
   permission java.security.AllPermission;
};




Fichier policy par défaut

Sur les wiki de JBoss, il est proposé un fichier policy par défaut. Ce fichier peut encore être affiné, puisqu&#8217;il fait une totale confiance aux classes de JBoss, ainsi qu&#8217;à toutes les archives déployées comme librairie. Il donne aussi beaucoup de liberté aux applications déployées au niveau du réseau.



Fichier policy affiné

La technique pour affiner un tel fichier peut être fastidieuse. On positionne en général les permissions auquelles on pense en premier lieu, muis commence le travail de fourmi qui consiste à lancer l&#8217;application, à constater qu&#8217;il manque une permission, puis à modifier le fichier et relancer l&#8217;ensemble.


Si cette technique est envisageable pour un administrateur chevronné, elle pendrait trop de temps pour un néophyte.



Utilitaire jChains

L&#8217;utilitaire open source jChains permet de recenser toutes les permissions nécessaires à une application.


La procédure (expliquée surla page d&#8217;accueil de l&#8217;utilitaire) est la suivante :




Lancer ORBD





  title ORBD
  c:\java\jdk505\bin\orbd -ORBInitialPort 1050 -serverPollingTime 200





Enregistrer le composant CORBA de jChains





  title servertool
  servertool -ORBInitialPort 1050 "register -server org.illegalaccess.jchains.receiver.Receiver -applicationName PermissionTransfer -classpath chains.jar -vmargs -Xmx192m"





Lancer JBoss, en utilisant jChains comme SecurityManager





  title JBoss - jChains
  set JBOSS_CLASSPATH=jchains.jar
  set JAVA_OPTS=-Dorg.illegalaccess.emitClass=org.illegalaccess.jchains.intercept.CORBAEmitter
                -Dorg.illegalaccess.jchains.outputfile=jbossout
                -Dorg.illegalaccess.jchains.CNameServiceIOR=corbaloc::localhost:1050/NameService
                -Djava.security.manager=org.illegalaccess.jchains.intercept.JChainsSecInterceptor
  run -c jchains



Quelques précautions supplémentaires doivent être prises :




Avec le JDK 5, il faut ajouter une permission supplémentaire dans le fichier java.policy par défaut.





  permission javax.management.MBeanTrustPermission "register";





Dans JBoss, il faut réduire le niveau de traces Log4J. Dans mon cas, avec la configuration par défaut, le lancement de JBoss avec jChains a généré un fichier log de 750Mo.


Le receiver doit avoir suffisamment de mémoire. C&#8217;est pris en compte par l&#8217;argument -vmargs -Xmx192m de la commande servertool.


Il faut être patient. Le lancement de JBoss a mis plus d'1 heure, au lieu de 30 secondes habituelle&#8230;&#8203;




Attention, jChains est très verbeux, il faudra retravailler ces permissions, en le regroupant. Rien que pour le démarrage de JBoss 4 , il a généré plus de 20000 permissions. De plus, il a un peu de mal pour les MBeanPermission, qu&#8217;il faut dans certains cas ajouter manuellement.





Conclusion


jChains est un outil intéressant, mais peu pratique dans le cas d&#8217;applications JEE. De plus, il est peu probable de voir arriver une version améliorée, la version actuelle datant de plus de 2 ans.
Il nous reste donc la méthode empirique, qui nécessite de nombreux redémarrages.


Sinon, on peut faire confiance aux développeurs et blinder les autres aspects de la sécurité.


</description>
          <pubDate>2006-08-29T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/SecurityManager</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/SecurityManager</guid>
        </item>
      
    
      
        <item>
          <title>Base de données intégrée à JBoss</title>
          <description>
Par défaut, JBoss utilise une base de données HypersonicDB, mais déconseille de la conserver pour la production. Il faut donc remplacer HypersoncDB par autre chose, ou se passer de base de données.


Remplacer HsqlDB


Pour remplacer HypersonicDB par une autre base de données, il suffit de modifier la datasource DefaultDS et la connecter à une base externe à JBoss. Cette procédure n&#8217;est pas tout à fait suffisante car JBoss tente de créer des tables et de les remplir, pour JMS.


Dans JBoss 4

Pour cela, les scripts de création et d&#8217;insertion de JBossMQ sont dans les fichiers deploy/jms/hsqldb-jdbc-state-service.xml et deploy/jms/hsqldb-jdbc2-service.xml. Il faut donc remplacer ces fichiers avec les équivalents adaptés à votre base. On trouve des exemples prêts à l"emploi dans le répertoire docs/examples/jms, pour DB2, Derby, Microsoft SqlServer, MySQL, Oracle, PostgreSQL et Sybase.



Dans JBoss 5

La principale différence dans JBoss 5 est le remplacement de JBossMQ par JBoss Messaging. Si le nouveau moteur de JMS doit aussi créer ses propres tables, les scripts ne sont plus que dans un fichier : deploy/messaging/hsqldb-persistence-service.xml. Il faut donc remplacer ce fichier par un exemplaire propre à votre base. En jetant un œil au contenu du répertoire docs/examples/jms, on constate que la liste des bases a légèrement évolué : DB2, Microsoft SqlServer, MySQL (InnoDB et NDB), Oracle, PostgreSQL et Sybase. Derby a disparu, mais une petite recherche sur le Web permet de trouver un exemple qui fonctionne, comme par exemple dans le Trac de Bedework.





Retirer DefaultDS


Pour se passer de base de données, il faut couper toutes les dépendances avec la datasource DefaultDS et désinstaller cette dernière.


Cette procédure à été testée avec JBoss 4.0 et JBoss 4.2.


JMS

Si vous n&#8217;utilisez pas les services JMS, supprimez le répertoire deploy/jms


Si vous voulez conserver JMS, il faut modifier les paramètres de persistence.


Le fichier deploy/jms/hsqldb-jdbc2-service.xml concerne la persistance des vieux messages, il peut être remplacé par null-persistence-service.xml (cf. docs/examples/jms).


Le fichier deploy/jms/hsqldb-jdbc-state-service.xml concerne la persistance de l&#8217;état JMS (souscriptions persistentes), il peut être remplacé par file-state-service.xml (cf. docs/exemples/jms). Dans ce cas, il faut ajouter jbossmq-state.xml dans conf/ ou un de ses sous-répertoires, et modifier le paramétrage "jbossmq" dans conf/login-config.xml : remplacer le DatabaseServerLoginModule par le DynamicLoginModule.


Récapitulatif :




Remplacer deploy/jms/hsqldb-jdbc2-service.xml par null-persistence-service.xml


Remplacer deploy/jms/hsqldb-jdbc-state-service.xml par file-state-service.xml


Ajouter jbossmq-state.xml dans conf/


Remplacer l&#8217;application-policy jbossmq (fichier conf/login-config.xml) :





   &lt;application-policy name = "jbossmq"&gt;
      &lt;authentication&gt;
         &lt;login-module code = "org.jboss.mq.sm.file.DynamicLoginModule"
            flag = "required"&gt;
            &lt;module-option name = "unauthenticatedIdentity"&gt;guest&lt;/module-option&gt;
            &lt;module-option name = "sm.objectname"&gt;jboss.mq:service=StateManager&lt;/module-option&gt;
         &lt;/login-module&gt;
      &lt;/authentication&gt;
   &lt;/application-policy&gt;




Timer

Dans le fichier deploy/ejb-deployer.xml, il faut remplacer jboss.ejb:service=EJBTimerService,persistencePolicy=database par jboss.ejb:service=EJBTimerService,persistencePolicy=noop (commentaire juste au dessus) et reporter la modification dans les depends du fichier.


Cela permet de ne plus stocker l&#8217;état du service de Timer.


Récapitulatif (dans deploy/ejb-deployer.xml) :




Activer





 &lt;mbean code="org.jboss.ejb.txtimer.NoopPersistencePolicy" name="jboss.ejb:service=EJBTimerService,persistencePolicy=noop"/&gt;





Remplacer





   &lt;attribute name="PersistencePolicy"&gt;jboss.ejb:service=EJBTimerService,persistencePolicy=database&lt;/attribute&gt;





par





   &lt;attribute name="PersistencePolicy"&gt;jboss.ejb:service=EJBTimerService,persistencePolicy=noop&lt;/attribute&gt;




Sécurité

En plus de la modification concernant jbossmq, il faut retirer le HsqlDbRealm dans le fichier conf/login-config.xml.



Générateur de clé

Le générateur de clé est généralement utilisé en EntityBean CMP. Il peut donc souvent être retiré en supprimant le fichier deploy/uuid-key-generator.sar.


Si vous souhaitez le conserver, il faut modifier la référence à la datasource, en modifiant META-INF/jboss-service.xml.



DataSource

Toutes les dépendances sont donc retirées, nous pouvous maintenant supprimer le datasource : deploy/hsqldb-ds.xml.



</description>
          <pubDate>2006-06-27T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/DefaultDS</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/DefaultDS</guid>
        </item>
      
    
      
        <item>
          <title>Glassfish/Installation</title>
          <description>
Installer JavaEE 5 SDK sous Ubuntu Server 6.10


Cette procédure décrit l&#8217;installation de JavaEE 5 SDK (soit Glassfish, version SJSAS PE9) sur une distribution Ubuntu de base. Nous allons ainsi créer configuration Linux / Glassfish minimale.


Installer un serveur FTP

Cf http://www.jtips.info/WebLogic/Installation



Installer Glassfish

L&#8217;installation se fait en mode console car nous ne disposons pas d&#8217;environnement X.




Installer un JDK


Cette étape est nécessaire sous Ubuntu car le JDK packagé avec JavaEE 5 SDK ne s&#8217;installe pas correctement (problème de blocage sur la suppression de fichiers temporaires)


Télécharger le fichier jdk-1_5_0_07-linux-i586.bin sur le serveur Ubuntu


Donner les droits d&#8217;exécution sur le fichier d&#8217;installation : sudo chmod +x ./jdk-1_5_0_07-linux-i586.bin


Exécuter l&#8217;auto-extraction : sudo ./jdk-1_5_0_07-linux-i586.bin


Déplacer le JDK : sudo mv jdk1.5.0_07/ /opt/jdk1.5.0_07


Installer le server JavaEE 5 SDK


Donner les droits d&#8217;exécution sur le fichier d&#8217;installation : sudo chmod a+x java_ee_sdk-5-linux.bin


Exécuter le fichier d&#8217;installation en précisant la machine virtuelle à utiliser : sudo ./java_ee_sdk-5-linux.bin -javahome /opt/jdk1.5.0_07 -console







Glassfish 2 sous Windows et Linux


Pour pré-requis, nous considérerons qu&#8217;un JDK, en version 5 ou 6, est déjà installé.
La procédure décrite ici a été testée avec GlassFish Server v2 Update Release 2 (UR2).


Téléchargement

Glassfish peut être téléchargé sur le site https://glassfish.dev.java.net/, le fichier récupéré est une archive jar d&#8217;installation.



Installation

L&#8217;archive d&#8217;installation doit être lancée avec java, avec plus de mémoire que la valeur par défaut :



java -Xmx256m -jar glassfish-installer-v2ur2-b04-windows.jar



Un écran demande la validation de la licence et propose d&#8217;activer les mises à jours automatiques.
Il faut noter que le système de mises à jour automatiques active l&#8217;envoi à Sun d&#8217;informations sur l&#8217;utilisation du serveur d&#8217;applications.


Ensuite, un script Ant permet de finaliser l&#8217;installation :



lib/ant/bin/ant -f setup.xml




Démarrage

Le script de démarrage est dans le répertoire bin :



asadmin start-domain



Ainsi, le domaine par défaut (domain1) est démarré. Si tout se passe bien, la page d&#8217;accueil par défaut est accessible à l&#8217;URL http://localhost:8080/.


Notre Glassfish est prêt à recevoir des applications.


Nous arrêterons le serveur d&#8217;application avec la commande :



asadmin stop-domain




Déploiement d&#8217;applications

Il est possible de déployer des applications sous forme d&#8217;archives war, jar ou ear, en les copiant dans le répertoire autodeploy du domaine (domains/domain1/autodeploy).


Il est également possible de déployer des applications avec la console d&#8217;administration ou la commande asadmin.



Console d&#8217;administration

La console d&#8217;administration est accessible à l&#8217;adresse http://localhost:4848/.
Le login est admin et le mot de passe par défaut est adminadmin.
Les personnes habituées à JBoss, comme moi, apprécieront la richesse et l&#8217;ergonomie de celle-ci ; c&#8217;est quant même plus sexy que la jmx-console !



Installation sous Linux / Unix

La seule différence pour les systèmes Linux, est le besoin d&#8217;accorder des droits d&#8217;exécution :



chmod -R +x lib/ant/bin
chmod -R +x bin






Glassfish avec Eclipse


Glassfish ne fait pas partie des serveurs d&#8217;applications supportés en standard par Eclipse.
Il est nécessaire d&#8217;installer un adaptateur supplémentaire, qu&#8217;on trouvera sur le site https://glassfishplugins.dev.java.net/.


Lorsqu&#8217;on ajoute le serveur, plutôt de sélectionner notre version dans la liste standard (Tomcat, JBoss, OC4J,&#8230;&#8203;), il faut cliquer sur le lien "Download additional server adapters".
Une liste d&#8217;adaptateurs fournis par des tiers est proposée : Geronimo, Jetty, Weblogic,&#8230;&#8203; ainsi que Glassfish.
Le même adaptateur supporte les versions 1 à 3.


La suite est la même que pour les autres serveurs d&#8217;applications.


</description>
          <pubDate>2006-06-25T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Glassfish/Installation</link>
          <guid isPermaLink="true">https://www.jtips.info/Glassfish/Installation</guid>
        </item>
      
    
      
        <item>
          <title>WebLogic/Installation</title>
          <description>
Installer WebLogic 9.1 sous Ubuntu Server 6.06


Cette procédure décrit l&#8217;installation de WebLogic sur une distribution Ubuntu de base. Nous allons ainsi créer configuration Linux / WebLogic minimale.


Installer un serveur FTP

Afin de récupérer le fichier d&#8217;installation de WebLogic sur la machine, il faut mettre en place un serveur FTP (par défaut, aucun serveur FTP n&#8217;est installé sur Ubuntu). Voici la procédure :




Activer les dépôts Ubuntu suivants (fichier /etc/apt/sources.list):


_deb http://fr.archive.ubuntu.com/ubuntu dapper universe _


_deb-src http://fr.archive.ubuntu.com/ubuntu dapper universe _


Installer le serveur proftpd


sudo apt-get install proftpd


A l&#8217;issue de l&#8217;installation, le serveur sera démarré automatiquement




Il suffit maintenant de transférer le fichier server910_linux32.bin (version RedHat Enterprise x86) à l&#8217;aide d&#8217;un client FTP (effectuer le transfert en mode binaire). Le fichier sera placé sous le répertoire home de l&#8217;utilisateur connecté sous FTP.



Installer WebLogic

L&#8217;installation se fait en mode console car nous ne disposons pas d&#8217;environnement X.




Donner les droits d&#8217;exécution sur le fichier


sudo chmod a+x server910_linux32.bin


Lancer le programme d&#8217;installation en mode console


sudo ./server910_linux32.bin -mode=console


Créer un domaine


sudo /root/bea/weblogic91/common/bin/config.sh -mode=console


Dans le menu Choose Configuration Option, sélectionner le choix 1 (Yes) afin de pouvoir modifier l&#8217;adresse IP sur lequel le serveur doit écouter (par défaut, host = localhost &#8658; le serveur est inaccessible depuis une autre machine)


Modifier le paramètre Listen Address (choix 2) (exemple : 192.168.0.167)


Dans le menu Edit Domain Information, modifier le nom du domaine


Démarrer le domaine


sudo /root/bea/user_projects/domains/mydomain/startWebogic.sh


mydomain est le nom du domaine créé précédemment


Vérifier que le serveur est à l&#8217;écoute sur le port 7001


sudo netstat -tanpu | grep ":7001"


Ouvrir le port 7001 pour accéder au serveur depuis une autre machine


sudo iptables -A INPUT -p tcp -i eth0 --dport 7001 -j ACCEPT


Pour arrêter le domaine, invoquer l&#8217;url suivante depuis un browser web :


http://hostname:7001/Shutdown







Installer un driver JDBC


WebLogic 8.1



Placer le driver (fichier(s) jar) dans le répertoire WLS_HOME/server/ext/jdbc/type_base


Déclarer une variable d&#8217;environnement CLASSPATH pointant sur le driver souhaité


Exemple : CLASSPATH=C:\bea\weblogic81\server\ext\jdbc\db2\db2jcc.jar;C:\bea\weblogic81\server\ext\jdbc\db2\db2jcc_javax.jar




Cette procédure a été testée sous Windows.



WebLogic 9.1



Copier le driver dans le répertoire %WL_HOME%\server\lib\


Dans le fichier WL_HOME\weblogic91\common\bin\commEnv.cmd, ajouter le driver à la variable WEBLOGIC_CLASSPATH


Exemple : set WEBLOGIC_CLASSPATH=%WEBLOGIC_CLASSPATH%;%WL_HOME%\server\lib\mysql-connector-java-3.1.10-bin.jar




Cette procédure a été testée sous Windows.



</description>
          <pubDate>2006-06-20T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WebLogic/Installation</link>
          <guid isPermaLink="true">https://www.jtips.info/WebLogic/Installation</guid>
        </item>
      
    
      
        <item>
          <title>WebLogic/Déploiement</title>
          <description>
Application Web et Hibernate 3.1 sous WebLogic 8.1


Hibernate et WebLogic sous tous les deux livrés avec la librairie ANTLR, mais dans des versions différentes. Une application web à déployer sous WebLogic 8.1 doit donc comporter la version ANTLR nécessaire pour le fonctionnement d&#8217;Hibernate (fichier antlr.jar dans le répertoire WEB-INF/lib). Toutefois, cela ne suffit pas, car WebLogic ne chargera pas les classes correspondant à cette version, lors du déploiement de l&#8217;application, puisque ANTLR fait déjà partie du CLASSPATH de WebLogic. Il faut donc indiquer explicitement à WebLogic de charger les classes présentes dans l&#8217;archive WAR. Cette information est indiquée dans le fichier weblogic.xml de l&#8217;application web (répertoire WEB-INF) :



&lt;container-descriptor&gt;
  &lt;prefer-web-inf-classes&gt;true&lt;/prefer-web-inf-classes&gt;
&lt;/container-descriptor&gt;



</description>
          <pubDate>2006-06-20T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/WebLogic/D%C3%A9ploiement</link>
          <guid isPermaLink="true">https://www.jtips.info/WebLogic/D%C3%A9ploiement</guid>
        </item>
      
    
      
        <item>
          <title>JSF</title>
          <description>
Pour définir une variable avec &lt;c:set&gt; dans une page JSF, deux éléments sont à considérer :




Le scope page n&#8217;existe pas avec JSF


Si la variable est définie à partir d&#8217;un backing bean, il convient de s&#8217;assurer que le backing bean existe effectivement




1. Définir le scope dans &lt;c:set&gt;


Par défaut, &lt;c:set&gt; créé une variable en portée page. Il faut donc définir explicitement le scope (request, session ou application) si vous souhaitez exploiter cette variable dans une EL JSF.



&lt;c:set var="d" value="${detailProduitBean}" scope="request"/&gt;



2. Gestion du backing bean


Les backing beans sont instanciés par le framework JSF (Managed Bean Creation Facility), lorsqu&#8217;ils sont accédés pour la première fois (pour un scope donné) dans une EL JSF. Par conséquent, si la balise &lt;c:set&gt; est exécutée avant qu&#8217;une EL JSF ne sollicite le backing bean, la variable du &lt;c:set&gt; ne sera pas correctement initialisée. Pour résoudre ce problème, on peut utiliser le VariableResolver de l&#8217;application pour s&#8217;assurer que le bean en question existe effectivement.



FacesContext ctx = FacesContext.getCurrentInstance()
VariableResolver resolver = ctx.getApplication().getVariableResolver();

// création du backing bean par JSF
DetailProduitBean d = (DetailProduitBean)resolver.resolveVariable(ctx, "detailProduitBean");

// initialisation du bean à partir des infos récupérées depuis la couche métier
d.setInfoProduit(service.findInfoProduit());



Ce code doit être placé dans un backing bean qui est invoqué avant l&#8217;affichage de la page JSF.
</description>
          <pubDate>2006-06-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JSF/index</link>
          <guid isPermaLink="true">https://www.jtips.info/JSF/index</guid>
        </item>
      
    
      
        <item>
          <title>Certifications</title>
          <description>
Quelques tests pour préparer les certifications de Sun.
La plupart de ces tests nécessitent de s&#8217;enregistrer en ligne.


Sun Certified Java Programmer (SCJP)




http://www.witscale.com/signup.html (j2se 1.4)


http://www.examulator.com/phezam/login.php


http://www.javaprepare.com/quests/question.html


http://www.jiris.com/online/page.do






Sun Certified Web Component Developer (SCWCD)




http://java.sun.com/developer/Quizzes/jsptut/


http://www.javaranch.com/carl/scwcd/scwcd_mock.jsp


http://www.podar.net/cgi-bin/scwcd/start.pl


http://www.javaranch.com/carl/SCWCD.htm


http://enthuware.com/jwebplus/index.html


http://www.jiris.com/online/page.do






Sun Certified Business Component Developer (SCBCD)




http://www.ejbcertificate.com/






Pour les 3 certifications




http://www.sun.com/training/catalog/courses/WGS-PREX-10-QUEST.xml


http://www.jdiscuss.com/


http://www.jpilotexam.org/






Certifications indépendantes




Javablackbelt




</description>
          <pubDate>2006-05-22T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/Certifications</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/Certifications</guid>
        </item>
      
    
      
        <item>
          <title>JSF/Immediate</title>
          <description>
Un action listener est normalement invoqué dans la phase Invoke Application, i.e. après la phase de conversion et validation des données saisies dans le formulaire auquel le listener est rattaché. Ce fonctionnement par défaut empêche la mise en oeuvre d&#8217;un bouton "Annuler" dans le formulaire (pour retourner par exemple sur une page liste): en effet, l'action listener associé au bouton "Annuler" ne sera pas invoqué, puisque la validation échouera si l&#8217;utilisateur ne rempli pas correctement le formulaire.







La solution consiste à shunter le cycle de traitement de la requête en positionnant immediate=true sur l'action listener du bouton "Annuler". Ainsi, le listener sera invoqué pendant la phase Apply Request Value, et donc avant la phase de conversion et validation des données.







Code du formulaire :



&lt;h:form&gt;
  &lt;h:panelGrid columns="3"&gt;
    &lt;h:outputText value="Code" style="font-weight:bold;"/&gt;
    &lt;h:inputText id="code" required="true"/&gt;
    &lt;h:message for="code" errorStyle="color: red"/&gt;
    &lt;h:outputText value="Libellé" style="font-weight:bold;"/&gt;
    &lt;h:inputText id="libelle" required="true"/&gt;
    &lt;h:message for="libelle" errorStyle="color: red"/&gt;
    &lt;h:commandButton value="Valider"/&gt;
    &lt;h:commandButton value="Annuler" immediate="true"/&gt;
    &lt;h:panelGroup/&gt;
  &lt;/h:panelGrid&gt;
&lt;/h:form&gt;

</description>
          <pubDate>2006-05-15T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JSF/Immediate</link>
          <guid isPermaLink="true">https://www.jtips.info/JSF/Immediate</guid>
        </item>
      
    
      
        <item>
          <title>Reference</title>
          <description>
Java




http://www.javapractices.com/






Blogs Java (fr)


J&#8217;ai séparé les blogs personnels de ceux créés et animés par des entreprises parce que leurs contenus sont assez différents.
Les blogs d&#8217;entreprise sont généralement plus fournis, mais aussi plus orientés marketing alors que les blogs personnels sont parfois plus bruts, mais plus denses en information intéressante.


Blogs personnels




Le Touilleur Express : Nicolas Martignole, un de plus actifs


Olivier Croisier


Thomas Queste : des sujets que je trouve très intéressants, mais une fréquence aléatoire


Nicolas Deloof


Java in the Alps : Emmanuel Hugonnet, de Silverpeas ; on y trouve surtout ses présentations (ah, il manque celle du LyonJUG)


Le blog d&#8217;Arnaud : Arnaud Héritier




Blogs d&#8217;entreprises




Octo talks, par Octo Technology ; sélectionnez vos catégories parce que tout n&#8217;est pas passionnant


Ippon, par Ippon technologies


Xebia


Zenika






Podcasts


En français




Les Cast Codeurs : alternance entre des interviews et des discussions entre Emmanuel Bernard (Hibernate), Guillaume Laforge (Groovy), Antonio Goncalves, Vincent Massol (XWiki) et d&#8217;autres (en français)




En anglais




Java Posse, la référence


JBoss Community Asylum




</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Reference</link>
          <guid isPermaLink="true">https://www.jtips.info/Reference</guid>
        </item>
      
    
      
        <item>
          <title>Open Source</title>
          <description>
Open Source


Définition

Un logiciel "Open Source" est un logiciel dont le code source est librement accessible et peut être librement copié, utilisé, modifié et redistribué. Tout développeur, mais aussi tout utilisateur, peut contribuer à son amélioration.
Le terme "Open Source" est la propriété de l’OSI, Open Source Initiative.


Les conditions d&#8217;utilisation, de modification et de redistribution doivent être précisées dans une licence soumise à l&#8217;approbation de l&#8217;OSI, selon une dizaine de critères. Il existe actuellement une soixantaine de licences approuvées, ce nombre croît régulièrement.





Modèle économique


Vendre de l&#8217;Open Source n&#8217;est pas interdit mais&#8230;&#8203;




Code source joint et modifiable


Redistribuable




Les sources de revenu sont :




Conseil et support


Produits dérivés






Licences Open Source classiques


&lt;table border="1"&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;/th&gt;
&lt;th&gt; GPL
&lt;/th&gt;
&lt;th&gt; LGPL
&lt;/th&gt;
&lt;th&gt; ASL, MPL, EPL,&#8230;&#8203;
&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt; Logiciel gratuit
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt; Autorisation d’utiliser, copier et distribuer
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt; Code source disponible
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt; Autorisation de modifier
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt; Obligation de rendre libres les modifications
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;td&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt; Intégrable à un logiciel non libre
&lt;/td&gt;
&lt;td&gt;
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;td&gt; X
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;




GPL = GNU General Public License


LGPL = GNU Lesser General Public License


APL = Apache Public License


MPL = Mozilla Public License


EPL = Eclipse Public License




</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Open_Source</link>
          <guid isPermaLink="true">https://www.jtips.info/Open_Source</guid>
        </item>
      
    
      
        <item>
          <title>OpenSource/CriteresOSI</title>
          <description>
Attention, ceci n&#8217;est qu&#8217;un court résumé des critères. Pour élaborer une licence compatible, il convient de se référer au texte original.


Code source


Le programme doit inclure le code source.




Redistribution libre et gratuite


La licence ne doit pas limiter le droit de vendre ou de donner le logiciel.




Travaux dérivés


La licence doit autoriser les modifications et travaux dérivés.




Autres critères




Intégrité du code source de l&#8217;auteur


Absence de discrimination envers des personnes ou des groupes


Absence de discrimination envers des domaines d&#8217;activité


Distribution de licence


La licence ne doit pas être spécifique à un produit


La licence ne doit pas restreindre d&#8217;autres logiciels


La licence doit être neutre technologiquement




</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/OpenSource/CriteresOSI</link>
          <guid isPermaLink="true">https://www.jtips.info/OpenSource/CriteresOSI</guid>
        </item>
      
    
      
        <item>
          <title>Java</title>
          <description>
Applets sous Linux / Firefox


Si les applets ne fonctionnent pas sous Linux avec Firefox, il suffit de créer un lien entre Firefox et la JRE.
Depuis le répertoire plugins de firefox, créer un lien vers le fichier libjavaplugin_oji.so situé dans le répertoire plugin/i386/ns7, pour une JRE 5, ou plugin/i386/ns610-gcc32, pour une JRE 1.4.2 :



ln -s /usr/java/jdk1.5/jre/plugin/i386/ns7/libjavaplugin_oji.so

ou

ln -s /usr/java/jdk142/jre/plugin/i386/ns610-gcc32/libjavaplugin_oji.so



Cette technique fonctionne pour Firefox ou pour Mozilla. Le répertoire de plugin est dans le répertoire d&#8217;installation de Firefox (/usr/local/firefox/plugins, par exemple) ou dans le répertoire home de l&#8217;utilisateur (~/.mozilla/plugins).


Remarques :




Les répertoires dépendent évidemment de l&#8217;installation !


Pour les versions anciennes de Mozilla, utiliser plugin/i386/ns610






URL avec protocole classpath


La classe java.net.URL offre la possibilité de travailler avec plusieurs protocoles, qui sont mis en oeuvre par l&#8217;implémentation d&#8217;un URLStreamHandler. Par exemple, pour se connecter au site 'ftp.debian.org', il suffit d&#8217;instancier un objet URL de la façon suivante :



URL url = new URL("ftp://ftp.debian.org/debian/README");



Le JDK est ainsi livré avec les protocoles suivants : file, http, ftp, mailto, gopher et jar. Le protocole jar est intéressant lorsque l&#8217;on veut travailler avec une URL qui pointe sur une ressource présente dans un fichier jar. Cependant, son utilisation est
contraignante car il faut désigner explicitement le fichier jar pour accéder à une ressource. Par exemple :



URL url = new URL("jar:file:c:\\repertoire\\archive.jar!/images/une_image.gif");



En particulier, il ne donne pas la possibilité de référencer un fichier présent dans le classpath, non packagé dans une archive
(ce qui est souvent le cas lorsque l&#8217;on travaille avec un IDE).


Le protocole classpath n&#8217;étant pas fourni avec le JDK, voici comment le créer :


1. La classe handler


Il faut créer une classe avec le nom Handler (pas le choix du nom !) qui hérite de URLStreamHandler.



public class Handler extends URLStreamHandler {
   protected URLConnection openConnection(URL url) throws java.io.IOException {
       URL classpathUrl = Handler.class.getClassLoader().getResource(url.getPath());
       if (classpathUrl == null)
           throw new FileNotFoundException(url.toExternalForm());
       return classpathUrl.openConnection();
   }
}



Le nom du package de cette classe doit se terminer par le nom du protocole à implémenter. Par exemple : org.monsite.classpath.


2. Incrire le handler


L&#8217;incription du handler se fait en déclarant la propriété système Java java.protocol.handler.pkgs, soit dans le code, soit en paramètre de lancement de la machine virtuelle :



System.setProperty("java.protocol.handler.pkgs", "org.monsite.classpath");

ou

java -Djava.protocol.handler.pkgs=org.monsite.classpath



La machine virtuelle connait alors d&#8217;une part le nom du protocole en lisant la dernière partie du package (dans notre cas 'classpath'),
et d&#8217;autre part la classe d&#8217;implémentation qui se nomme nécessairement Handler.


3. Utilisation


Instancier une URL :



URL url = new URL("classpath:images/une_image.gif");



Afficher une image dans un JEditorPane :



String html = "&lt;img src=\"classpath:images/une_image.gif/&gt;";
JEditorPane pane = new JEditorPane("text/html", html);



</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Java/Misc</link>
          <guid isPermaLink="true">https://www.jtips.info/Java/Misc</guid>
        </item>
      
    
      
        <item>
          <title>P6Spy</title>
          <description>
Un proxy JDBC est un composant qui s&#8217;installe entre une application et le driver JDBC. Il sert à tracer les requêtes à la base de donner sans avoir à modifier son code.


Principe


DataSource JDBC classique







DataSource JDBC avec un proxy









Installation


P6Spy

P6Spy s&#8217;installe comme un driver JDBC, dans la datasource :



driver-class=com.p6spy.engine.spy.P6SpyDriver



Ensuite, P6Spy se configure dans le fichier spy.properties, qui doit être placé dans le classpath de JBoss ($JBOSS_CLASSPATH) :



realdriver=org.hsqldb.jdbcDriver




Irongrid

IronTrack est une application de rendu pour P6Spy. Il peut lire un fichiers de trace spy.log ou exploiter une connexion directe à l&#8217;application. Cela se configure dans le fichier p6spy.properties :



module.ibeam=com.irongrid.ibeam.server.IBeamFactory
monitorport=2000



Pour installer IronTrack, on lance le fichier d&#8217;installation, irontracksql-installer-1_0_172.jar, avec java.


Ensuite, il faut installer les fichiers p6spy.jar et irontracksql.jar dans le répertoire lib de la configuration, puis spy.properties dans un répertoire du classpath. Pour déclarer un répertoire dans le classpath de JBoss, on le référence via la variable d&#8217;environnement JBOSS_CLASSPATH.





Configuration


Configuration classique

L&#8217;étape suivante consiste à modifier la datasource pour remplacer le driver de la base de données par celui de P6Spy :



   &lt;driver-class&gt;com.p6spy.engine.spy.P6SpyDriver&lt;/driver-class&gt;



Le driver de la base de données doit être déclaré dans le fichier de configuration de P6Spy, spy.properties. Dans notre cas, nous avons déclaré le driver de hypersonic et avant mis le driver mySql en commentaire.



 realdriver=org.hsqldb.jdbcDriver
 # realdriver=org.gjt.mm.mysql.Driver



Lorsque plusieurs datasources utilisent le même driver, toutes doivent être interceptées par P6Spy. Dans notre cas, DefaultDS, déclarée dans le fichier deploy/hsqldb-ds.xml de la configuration, doit être modifiée.



Cas hypersonic

Le cas de hypersonic est particulier car pour que cela fonctionne, il faut désactiver la base de données en mode « in-process » et la passer en mode « server ».



  &lt;mbean code="org.jboss.jdbc.HypersonicDatabase"
    name="jboss:service=Hypersonic"&gt;
    &lt;attribute name="Port"&gt;1701&lt;/attribute&gt;
    &lt;attribute name="Silent"&gt;true&lt;/attribute&gt;
    &lt;attribute name="Database"&gt;default&lt;/attribute&gt;
    &lt;attribute name="Trace"&gt;false&lt;/attribute&gt;
    &lt;attribute name="No_system_exit"&gt;true&lt;/attribute&gt;
  &lt;/mbean&gt;



La datasource, elle-même doit être modifiée pour prendre ce changement en compte.



 &lt;local-tx-datasource&gt;

   &lt;jndi-name&gt;DefaultDS&lt;/jndi-name&gt;
   &lt;connection-url&gt;
     jdbc:hsqldb:hsql://localhost:1701
   &lt;/connection-url&gt;

   &lt;driver-class&gt;com.p6spy.engine.spy.P6SpyDriver&lt;/driver-class&gt;

   &lt;user-name&gt;sa&lt;/user-name&gt;
   &lt;password&gt;&lt;/password&gt;

   &lt;min-pool-size&gt;5&lt;/min-pool-size&gt;
   &lt;max-pool-size&gt;20&lt;/max-pool-size&gt;
   &lt;idle-timeout-minutes&gt;0&lt;/idle-timeout-minutes&gt;

   &lt;security-domain&gt;HsqlDbRealm&lt;/security-domain&gt;

   &lt;metadata&gt;
     &lt;type-mapping&gt;Hypersonic SQL&lt;/type-mapping&gt;
   &lt;/metadata&gt;

   &lt;depends&gt;jboss:service=Hypersonic&lt;/depends&gt;
 &lt;/local-tx-datasource&gt;






Remarques


L&#8217;outil IronTrack a été développé par la société IronGrid qui a cessé son activité. Cependant, celui-ci a été développé sous la licence open source « Apache Public License », ce qui permet de le redistribuer sans contrainte. Il est donc téléchargeable sur le site du Java User Group de Belgique et son code source est disponible sur le site sourceforge.net.


Dans son paramétrage intégré à IronTrack, P6Spy communique les traces en temps réel et les envoie dans un fichier spy.log. Ce fichier est stocké dans le repertoire qui a été utilisé pour le lancement de JBoss. Pour modifier ce lieu de stockage, il faut modifier la ligne suivante dans spy.properties :



logfile     = spy.log



en (par exemple)



logfile     = /usr/local/jboss/server/myconf/log/spy.log



Pour réduire le volume des traces, il est possible d&#8217;activer un filtre en précisant la liste des tables à tracer.


</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/P6SPy</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/P6SPy</guid>
        </item>
      
    
      
        <item>
          <title>JBoss 4 et JAAS</title>
          <description>
Les principes de JAAS sont exposés dans l&#8217;article Sécurité des EJB avec JAAS.


Nous allons à présent détailler la façon de développer un module personnalisé, qui travaille avec une classe Principal personnalisée, et nous verrons les éléments spécifiques à JBoss.


Développement d&#8217;un module client


Le rôle de ce module est de transmettre les informations de login, de password et d&#8217;éventuelles autres informations au serveur.







Ce module doit implémenter l&#8217;interface javax.security.auth.spi.LoginModule.



package fr.sewatech.j2ee.security.client;

import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.jboss.security.SecurityAssociation;

public class SwClientLoginModule implements LoginModule {

   private Subject subject;
   private CallbackHandler callbackHandler;
   private Map sharedState;
   private Map options;
   private boolean loginOk = false;
   private boolean commitOk = false;
   private String username;
   private char[] password;
   private SwPrincipal userPrincipal;
   private Object userCredential;

   public SwClientLoginModule() {
       super();
   }

   ...
}



Les méthodes à implémenter sont :




initialize : initialise le module avec le sujet qui cherche à acquérir une nouvelle identité, le gestionnaire de callback qui va fournir le login et le mot de passe ; la méthode reçoit aussi 2 autres paramètres que nous n&#8217;exploiterons pas : un état, commun aux différents modules et des options saisie dans la configuration du module.





   public void initialize(Subject subject, CallbackHandler handler, Map&lt;String, ?&gt; state, Map&lt;String, ?&gt; options) {
       this.subject = subject;
       this.callbackHandler = handler;
       this.sharedState = state;
       this.options = options;
   }





login : demande le login et le mot de passe au gestionnaire de callback puis les valide ; dans notre cas, aucune vérification n&#8217;est faite.





   public boolean login() throws LoginException {
       if (callbackHandler == null)
           throw new LoginException(
                   "Erreur d'initialisation : pas de callback handler");

       // Initialisation du callback demanant un nom et un mot de passe
       Callback[] callbacks = new Callback[2];
       callbacks[0] = new NameCallback("user name: ");
       callbacks[1] = new PasswordCallback("password: ", false);

       try {
           callbackHandler.handle(callbacks);
           username = ((NameCallback) callbacks[0]).getName();
           char[] tmpPassword = ((PasswordCallback) callbacks[1])
                   .getPassword();
           if (tmpPassword == null) {
               tmpPassword = new char[0];
           }
           password = new char[tmpPassword.length];
           System.arraycopy(tmpPassword, 0, password, 0, tmpPassword.length);
           ((PasswordCallback) callbacks[1]).clearPassword();
           userCredential = password;

       } catch (java.io.IOException ioe) {
           throw new LoginException(ioe.toString());
       } catch (UnsupportedCallbackException uce) {
           throw new LoginException(
                   "Erreur d'initialisation : mauvais callback handler");
       }

       loginOk = true;
       return true;
   }





commit : une fois que tous les modules ont approuvé le login et le mot de passe, les méthodes commit sont appelées pour la construction des objets principal ; dans notre cas, nous instantions un objet SwPrincipal.





   public boolean commit() throws LoginException {
       if (loginOk == false) {
           return false;
       } else {
           userPrincipal = new SwPrincipal(username);
           if (!subject.getPrincipals().contains(userPrincipal))
               subject.getPrincipals().add(userPrincipal);
           if (!subject.getPublicCredentials().contains(userCredential))
               subject.getPublicCredentials().add(userCredential);

           // Spécifique JBoss
           SecurityAssociation.pushSubjectContext(subject, userPrincipal, userCredential);

           username = null;
           password = null;

           commitOk = true;
           return true;
       }
   }





abort : réinitialise les champs en cas d&#8217;échec.





   public boolean abort() throws LoginException {
       if (loginOk == false) {
           return false;
       } else if (loginOk == true &amp;amp;&amp;amp; commitOk == false) {
           loginOk = false;
           username = null;
           password = null;
           userPrincipal = null;
       } else {
           logout();
       }
       return true;
   }





logout : réinitialise les champs en cas de déconnexion





   public boolean logout() throws LoginException {
       subject.getPrincipals().remove(userPrincipal);
       loginOk = false;
       username = null;
       password = null;
       userPrincipal = null;
       return true;
   }



Une partie spécifique à JBoss a été insérée dans la méthode commit(), sans laquelle les informations de login et password ne sont pas transmises aux modules du serveur.


Cette classe crée des objets de type SwPrincipal, qui seront transmis au serveur.
Les objets doivent au moins avoir le login, il est possble d&#8217;ajouter des information spécifiques (mot de passe, nom du poste de travail,&#8230;&#8203;).



package fr.sewatech.j2ee.security.common;

import java.io.Serializable;
import java.security.Principal;

public class SwPrincipal implements Principal, Serializable {

   private String name;

   public SwPrincipal(String username) {
       super();
       this.name = username;
   }

   public String getName() {
       return name;
   }

   public String toString() {
       return ("Principal:  " + name);
   }

   public boolean equals(Object o) {
       if (o == null)
           return false;
       if (this == o)
           return true;
       if (!(o instanceof Principal))
           return false;
       Principal that = (Principal) o;
       if (this.getName().equals(that.getName()))
           return true;
       return false;
   }

   public int hashCode() {
       return name.hashCode();
   }
}





Développement d&#8217;un module serveur


Ce module doit implémenter l&#8217;interface javax.security.auth.spi.LoginModule, comme pour le module client. Il doit implémenter les mêmes méthodes.



package fr.sewatech.j2ee.security.server;
...
public class SwServerLoginModule implements LoginModule {
    ...
}





login





   public boolean login() throws LoginException {
       if (callbackHandler == null)
           throw new LoginException(
                   "Erreur d'initialisation : pas de callback handler");

       // Initialisation du callback demanant un nom et un mot de passe
       Callback[] callbacks = new Callback[] {
               new NameCallback("user name: "),
               new PasswordCallback("password: ", false),
               new SecurityAssociationCallback() };

       try {
           callbackHandler.handle(callbacks);
           username = ((NameCallback) callbacks[0]).getName();
           char[] tmpPassword = ((PasswordCallback) callbacks[1])
                   .getPassword();
           if (tmpPassword == null) {
               tmpPassword = new char[0];
           }
           password = new char[tmpPassword.length];
           System.arraycopy(tmpPassword, 0, password, 0, tmpPassword.length);
           ((PasswordCallback) callbacks[1]).clearPassword();
       } catch (java.io.IOException ioe) {
           throw new LoginException(ioe.toString());
       } catch (UnsupportedCallbackException uce) {
           throw new LoginException(
                   "Erreur d'initialisation : mauvais callback handler");
       }

       loginOk = true;
       return true;
   }





commit





   public boolean commit() throws LoginException {
       if (loginOk == false) {
           return false;
       } else {
           Set&lt;Principal&gt; principals = subject.getPrincipals();
           userPrincipal = new SwPrincipal(username);
           if (!principals.contains(userPrincipal)) {
               principals.add(userPrincipal);

               Group roles = new SwGroup("Roles");
               Group group = new SwGroup("sewa");
               group.addMember(userPrincipal);
               roles.addMember(group);
               principals.add(roles);
           }

           username = null;
           password = null;

           commitOk = true;
           return true;
       }
   }





initialize, logout et abort sont identiques au module client
La principale différence réside dans le fait que c&#8217;est le serveur d&#8217;application qui gère le CallbackHandler. Les seuls callback qui sont obligatoirement gérés sont le NameCallback et le PasswordCallback. Dans le cas de JBoss, un SecurityAssociationCallback permettrait de récupérer le Principal transmis par le client, mais de façon strictement spécifique à JBoss.




Pour approuver le login, il faut affecter l&#8217;objet principal à un rôle autorisé aux EJB. Pour gérer les rôles, nous avons développé une classe qui implémente l&#8217;interface Group.



package fr.sewatech.j2ee.security.server;

import java.security.Principal;
import java.security.acl.Group;
import java.util.*;

public class SwGroup implements Group {

   private String name;
   private Map members;

   public SwGroup(String name){
       super();
       this.name = name;
       members = new HashMap();
   }

   public boolean addMember(Principal user) {
       boolean isMember = members.containsKey(user.getName());
       if( !isMember )
           members.put(user.getName(), user);
       return !isMember;
   }

   public boolean removeMember(Principal user) {
       return (members.remove(user.getName()) != null);
   }

   public boolean isMember(Principal member) {
       return members.containsKey(member.getName());
   }

   public Enumeration&lt;? extends Principal&gt; members() {
       return Collections.enumeration(members.values());
   }

   public String getName() {
       return name;
   }

   public String toString()
   {
      StringBuffer tmp = new StringBuffer(getName());
      tmp.append("(members:");
      Iterator iter = members.keySet().iterator();
      while( iter.hasNext() )
      {
         tmp.append(iter.next());
         tmp.append(',');
      }
      tmp.setCharAt(tmp.length()-1, ')');
      return tmp.toString();
   }
}



</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/LoginModule</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/LoginModule</guid>
        </item>
      
    
      
        <item>
          <title>Monitoring JMX dans JBoss 4 et 5</title>
          <description>
JMX est le protocole universel dans JBoss 4. Tous les composants déployés dans le serveur d&#8217;application peuvent être intérrogés par ce protocole.
Il est possible de récupérer les valeurs de leurs attributs et d&#8217;invoquer des opérations.


JMX est aussi, et avant tout, le protocole standard de monitoring des applications Java.


Consoles JBoss


Console JMX

La plus ancienne façon d&#8217;accéder aux composants JMX de JBoss est l&#8217;application jmx-console (http://localhost:8080/jmx-console).


Les premières informations inspectées habituellement sont dans les MBeans suivants :




jboss:service:JNDIView &#8658; permet de parcourir l&#8217;annuaire JNDI


jboss.system:type=ServerInfo &#8658; permet de suivre les informations de mémoire





Console Web

Les informations JMX sont classées dans l&#8217;arborescence « System / JMX MBeans ».
Elles sont ensuite classées par catégorie, comme dans la jmx-console.


Les informations générales du serveur sont affichées sur la page d&#8217;accueil et peuvent être retrouvées dans le MBean jboss.system:type=ServerInfo.


Les applications déployées sont classées dans l&#8217;arborescence "J2EE Domains / Manager / JBoss", puis par fichier déployé (.jar, .war, .sar,&#8230;&#8203;).
Pour les EJB, les statistiques montrent le nombre de create et de remove, ainsi que les temps d&#8217;exécution des méthodes.
Pour les servlet, les statistiques concernent les temps de traitement de requêtes.


Pour suivre l&#8217;évolution de la mémoire dans une courbe, il faut se positionner sur le MBean jboss.system:type=ServerInfo, attribut FreeMemory puis par clic droit sélectionner "graph".
Cette manipulation peut se faire sur n&#8217;importe quel attribut numérique.
Il faut noter que le graphe s&#8217;affiche dans une applet.
La fonctionnalité de Snapshot permet de collecter les données et d&#8217;afficher une courbe statique.


Pour placer une alerte sur une donnée, on sélectionne "create monitor".
La valeur limite est placée dans le champs Threshold, la fréquence de surveillance est placée dans le champs "Time Period".
Il ne faut pas oublier d&#8217;activer la surveillance en cochant la case "Enable Monitor".
Les recueils de données et les surveillances sont placées respectivement dans les hierarchies "Monitors / Snapshots" et "Monitor / Alerts".
Dans la configuration standard, seule la "Console Alert" est disponible.
Celle-ci envoie une trace dans Log4J lorsque la valeur limite est franchie.
Une "Email Alert" peut être activée dans le fichier deploy/monitoring-service.xml de la configuraion.


Remarques:


La web-console affiche l&#8217;arborescence de ressources dans une applet.
Il faut donc que le navigateur supporte cette fonctionnalité via le plug-in java.



JOPR

JOPR est la version open source de l&#8217;outil de monitoring JBoss Operation Network.
Cette outil peut être considéré comme un concurrent de Nagios, puisqu&#8217;il permet de surveiller plusieurs types de ressources: système, réseau, java,&#8230;&#8203;
C&#8217;est donc bien plus qu&#8217;une console concurrente des deux citées précédemment.


Par contre, il existe une version allégée, Embedded JOPR, qui se déploie sous forme d&#8217;un war et qui peut être utilisée pour certaines tâches d&#8217;administration.


Celle-ci permet de parcourir les applications déployées et de consulter leurs statistiques, comme la web-console, de consulter l&#8217;état des ressources, comme les datasources.
Elle permet aussi d&#8217;accéder à certaines informations standard de la JVM et des composants JMX.
Si EmbJOPR est plus joli que ses concurrents, LE point sur lequel il les surpasse est la modification de configuration: il est possible d&#8217;ajouter une datasource, par exemple, et de conserver cette modification après le prochain redémarrage.
Les modifications de configuration de JBossAS deviennent persistantes, sans avoir à modifier soi-même les fichiers XML.





Outils indépendants


jConsole

JConsole est un outil de monitoring intégré au JDK depuis la version 5.
Il utilise en particulier le protocole JMX.


Pour qu&#8217;une application puisse être vue par jConsole, il faut activer sont interface JMX:



JAVA_OPTS="-Dcom.sun.management.jmxremote"



Cette option permet un accès local; il est nécessaire avec le JDK 5, mais inutile à partir du JDK 6.


Pour qu&#8217;une application puisse être vue à distances, il faut préciser le port de communication:



JAVA_OPTS="-Dcom.sun.management.jmxremote.port=9999"



Par défaut, la communication est sécurisée. Il est possible de préciser l&#8217;emplacement des fichiers d&#8217;authentification, ou de désactiver la sécurité (mode développement).



 # Mode développement
 JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=false"
 JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"




 # Mode sécurisé
 JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.password.file=jmxremote.password"
 JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.access.file=jmxremote.access"
 JAVA_OPTS="$JAVA_OPTS -Djavax.net.ssl.keyStore=tomboss.ks -Djavax.net.ssl.keyStorePassword=tompass"



Avec Tomcat, par exemple, cette technique permet d&#8217;accéder aux informations JMX de la machine virtuelle, ainsi qu&#8217;à celles du serveur d&#8217;application.
Comme JBoss embarque son propre serveur JMX, les informations sont séparées.
Depuis JBoss 4.0.3, il est possible de les rassembler, en ajoutant les options suivantes:



 # Intégrer JMX de JBoss 4 et de JVM (à partir de JBoss 4.0.3)
 JAVA_OPTS="$JAVA_OPTS -Djboss.platform.mbeanserver -Djavax.management.builder.initial=org.jboss.system.server.jmx.MBeanServerBuilderImpl"




 # Intégrer JMX de JBoss 5 et de JVM
 JAVA_OPTS="$JAVA_OPTS -Djboss.platform.mbeanserver"



Il faut noter, toutefois, que la combinaison de -Djboss.platform.mbeanserver et de -Dcom.sun.management.jmxremote fait planter JBoss 5 au démarrage!
D&#8217;après le JIRA de JBoss, ce problème existe en version 5.0 et 5.1, mais est corrigé en 6.0.
Pour les clients de RedHat ayant souscrit au support et utilisant EAP, le problème a été corrigé dans la version JBoss EAP 5.0.



Client JMX

L&#8217;application MC4J est plus nettement plus conviviale pour exploiter les informations des MBeans.
Cependant, l&#8217;appel de certaines méthodes fonctionne mal car l&#8217;outil supporte mal le polymorphisme.


jManage est une solution intéressante car il s&#8217;installe en tant que serveur indépendant et peut surveiller plusieurs JBoss, Tomcat ou autres.


Je ne l&#8217;ai pas encore évalué, mais MBeanMonitor a l&#8217;air intéressant.



Nagios

L&#8217;application Nagios est fréquemment utilisée pour le suivi des environnements réseaux et serveurs.
Les informations issues de MBeans de JBoss peuvent y être affichées.
JBoss fournit la procédure pour installer un plug-in adapté à cette tâche dans Nagios.
Vous trouverez cette information dans le wiki de JBoss.





Twiddle


Twiddle est un outil en ligne de commande fourni avec JBoss (répertoire bin). Il permet d&#8217;accéder aux informations et aux opérations des MBeans déployés depuis la ligne de commande.


Pour des informations plus récentes que ce paragraphe, vous pouvez consulter le wiki de jboss.org (en anglais).


Options



-s: nom ou adresse IP du serveur, avec éventuellement le port pour l&#8217;accès distant





twiddle -s myserver ...
twiddle -s myserver:1299 ...





-u, -p: authentification





twiddle -u admin -p admin ...





-h: aide





twiddle -h





--help-commands: liste des commandes





twiddle --help-commands





-H: aide sur une commande





twiddle -H ...




Liste des commandes



info &lt;mbean-name&gt;: Get the metadata for an MBean


get: Get the values of one or more MBean attributes


invoke: Invoke an operation on an MBean


create: Create an MBean


setattrs: Set the values of one or more MBean attributes


unregister: Unregister one or more MBeans


query: Query the server for a list of matching MBeans


set: Set the value of one MBean attribute


serverinfo: Get information about the MBean server





Exemples



twiddle info "jboss.system:service=MainDeployer"


twiddle query "jboss.system:*"


twiddle serverinfo -l &lt;&#8658; twiddle query ":"


twiddle get "jboss.system:type=ServerInfo"


twiddle get "jboss.system:type=ServerInfo" FreeMemory


twiddle invoke "jboss.system:service=MainDeployer" redeploy "file:///C:/mandeploy/hello.war/"


twiddle invoke "jboss:service=JNDIView" list true




L&#8217;exemple ci-dessous est valable pour JBoss 4.x.




twiddle invoke "jboss.deployment:flavor=URL,type=DeploymentScanner" scan




En JBoss 5, pour avoir la même chose, il faut invoquer les 2 opérations suivantes:




twiddle invoke "jboss.deployment:flavor=URL,type=DeploymentScanner" stop


twiddle invoke "jboss.deployment:flavor=URL,type=DeploymentScanner" start





Problèmes

L&#8217;invocation de certaines opérations ou l&#8217;accès à certains attributs peuvent provoquer des exceptions java.io.NotSerializableException.
Ceci peut être le cas lorsqu&#8217;on déclenche un scan.


Ce problème ne se produit que dans certaines conditions.
Par exemple, avec JBoss 4.0.5 et un JDK 5 de Sun.
En revanche, le problème a été résolu pour la même version de JBoss avec un JDK 1.4.
Un patch est disponible dans JIRA et intégré dans la version 4.0.5.SP1 (uniquement dans SVN).


Le problème ne se produit pas dans JBoss 4.2.





Accès depuis java


RMIAdaptor

L&#8217;appel depuis une classe Java peut se faire à distance (protocole RMI).
Dans ce cas, il faut recherche l&#8217;adaptateur dans l&#8217;annuaire JNDI.
Le lookup renvoie un objet de type javax.management.MBeanServerConnection à partir duquel on peut interroger des attributs de MBeans et invoquer des opérations.



Context ctx = new InitialContext();
MBeanServerConnection server = (MBeanServerConnection) ctx
                               .lookup("jmx/invoker/RMIAdaptor");
Long freeMemory = (Long) server.getAttribute(new ObjectName(
                               "jboss.system:type=ServerInfo"),
                               "FreeMemory");
Long totalMemory = (Long) server.getAttribute(new ObjectName(
                               "jboss.system:type=ServerInfo"),
                               "TotalMemory");
System.out.println("Memoire utilisée="
                             + (totalMemory-freeMemory)/1024/1024
                             + "Mo");
server.invoke(new ObjectName(
                   "jboss.system:service=MainDeployer"),
               "redeploy",
               new Object[] {"file:///C:/mandeploy/hello.war/"}	,
               new String[] {"java.lang.String"});




MBeanServerLocator

L&#8217;appel depuis une classe Java peut se faire en local, depuis une JSP, une servlet ou un EJB, par exemple :



&lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"&gt;
&lt;%@ page language="java"
         import="javax.management.,org.jboss.mx.util."%&gt;

&lt;%
   MBeanServer server = MBeanServerLocator.locateJBoss();

   long freeMemory = (Long) server.getAttribute(new ObjectName(
                        "jboss.system:type=ServerInfo"),
                        "FreeMemory");
   long totalMemory = (Long) server.getAttribute(new ObjectName(
                        "jboss.system:type=ServerInfo"),
                        "TotalMemory");
%&gt;

&lt;HTML&gt;
&lt;HEAD&gt;
  &lt;TITLE&gt;Hello JSP&lt;/TITLE&gt;
&lt;/HEAD&gt;

&lt;BODY&gt;
  Memoire utilisée=
  &lt;%=(totalMemory - freeMemory) / 1024 / 1024%&gt;
  Mo
&lt;/BODY&gt;
&lt;/HTML&gt;




</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/JMX</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/JMX</guid>
        </item>
      
    
      
        <item>
          <title>Installation JBoss AS 4</title>
          <description>
La procédure décrite ci-dessous s&#8217;applique pour JBoss 4.x


Pré-requis (JBoss 4.0)


Système



Windows


Unix, Linux


Mac OS X




Il semblerait qu&#8217;on puisse installer JBoss sur un IBM iSeries (AS/400), mais je n&#8217;ai jamais fait le test.



Machine virtuelle



JSE 5.0 (JDK ou JRE)


J2SE 1.4 (JDK ou JRE)





Téléchargement



http://www.jboss.org/jbossas/downloads/




OU directement sur sourceforge




http://sourceforge.net/projects/jboss




Fichiers :




jboss-4.0.x.zip / jboss-4.0.x.tar.gz / jboss-4.0.x.tar.bz2


Installation


jboss-4.0.x-src.zip / jboss-4.0.x-src.tar.gz / jboss-4.0.x-src.tar.bz2


Code source


jbas-xxxx-patch.zip


Patch (tous systèmes)







Pré-requis (JBoss 4.2)


Machine virtuelle

JBoss 4.2 fonctionne normalement avec le JDK 5. Cependant, pour la version 4.2.3, une distribution est prévue pour le JDK 6.



Téléchargement

On retrouve des fichiers pour les versions installables, ainsi que pour le code source. Par contre, il n&#8217;y a plus de patch.




jboss-4.2.3.GA.zip


Installation avec JavaSE 5


jboss-4.2.3.GA-jdk6.zip


Installation avec JavaSE 6


jboss-4.2.3.GA-src.tar.gz


Code source




Les fichiers MD5 et SHA permettent de valider l&#8217;intégrité des fichiers téléchargés.





Installation


Installation simple

Décompresser l&#8217;archive !


Pour tester l&#8217;installation, lancer le script bin/run.bat ou bin/run.sh. La variable JAVA_HOME doit pointer sur le répertoire du JDK.


La console dos ou shell doit afficher ceci :



==========================================================================
  JBoss Bootstrap Environment
  JBOSS_HOME: ...\bin\\..
  JAVA: ...
  JAVA_OPTS:  -Dprogram.name=run.bat -Xms128m -Xmx512m
  CLASSPATH: ...\lib\tools.jar;...\bin\\run.jar
==========================================================================
17:29:33,852 INFO  [Server] Starting JBoss (MX MicroKernel)...
...
...
...
17:29:50,243 INFO  [Server] JBoss (MX MicroKernel) [4.0.2 (build:
     CVSTag=JBoss_4_0_2 date=200505022023)] Started in 16s:375ms



Un second test consiste à lancer la page d&#8217;accueil depuis le navigateur web, sur le port 8080.


Pour arrêter JBoss, on peut utiliser le script shutdown.sh, ou envoyer un kill au processus. En mixant kill et jps, on peut avoir une commande générique comme celle-ci.



kill jps -l | grep org.jboss.Main | awk '{ print $1 }'




Installation en service Linux

Une fois JBoss installé, il peut être pratique de l&#8217;installer en service, afin qu&#8217;il se lance sans intervention humaine, au démarrage du serveur. JBoss fournit des scripts types, pour RedHat, Suse et HP-UX. Il semblerait que le script RedHat fonctionne sur Ubuntu.


Cependant, j&#8217;ai préféré refaire un script, simplifié, pour mon serveur préféré. Un des raisons à cela, est que le script fourni en standard ne fonctionne que si JMX et twiddle ne sont pas sécurisés !!! Je préfère donc arrêter mon JBoss par un kill -TERM.





Script d&#8217;installation


Même si elle est relativement simple, cette installation peut être automatisée par script, sous Linux. Celui-ci peut alors aussi prendre en charge automatiquement l&#8217;installation en service.


Le script que j&#8217;ai conçu a été testé pour une installation de JBoss 5.1, avec JavaSE 6, sur un serveur Ubuntu Server 9.04.


Une partie du script, en particulier pour l&#8217;installation de Java est commune avec l&#8217;installation par script de Tomcat. Pour le reste, les principales différences sont :




Téléchargement depuis Sourceforge


Pas de modification du système de traces




</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/Installation</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/Installation</guid>
        </item>
      
    
      
        <item>
          <title>Invoker JBoss</title>
          <description>
Un EJB est accessible à distance par RMI (Remote Method Invocation) qui s&#8217;appuie de façon standard sur le protocole réseau propriétaire JRMP (Java Remote Method Protocol). JBoss propose une alternative à ce protocole avec « RMI sur http ».


Principe des invokers JBoss


Dans JBoss, les invokers implémentent la communication entre le client et le serveur distant.
L&#8217;invoker par défaut s&#8217;appuie sur JRMP, en respectant le modèle de thread de la spécification J2EE. Il est classiquement remplacé par le « pooled invoker », qui traite les requêtes RMI via un poll de threads ou, de façon moins classique, par le « http invoker ».







La modification d&#8217;invoker peut se faire EJB par EJB, dans le fichier META-INF/jboss.xml ou de façon globale, par profil d&#8217;EJB, dans le fichier conf/standardjboss.xml.




Mise en oeuvre de « http invoker »


Pour que cela fonctionne, il faut que le répertoire http-invoker.sar soit déployé ; c&#8217;est le cas dans la configuration par défaut.
Il faut ensuite configurer JNDI et RMI pour qu&#8217;ils utilisent http.


Configuration JNDI / http

La configuration se fait dans le fichier jndi.properties du client.



java.naming.provider.url=http://myserver:8080/invoker/JNDIFactory




Configuration RMI / http

La configuration se fait dans le fichier conf/standardjboss.xml du serveur.



&lt;invoker-proxy-binding&gt;
  &lt;name&gt;stateless-rmi-invoker&lt;/name&gt;
  &lt;invoker-mbean&gt;jboss:service=invoker,type=jrmp&lt;/invoker-mbean&gt;
  ...
&lt;/invoker-proxy-binding&gt;



Est remplacée par



&lt;invoker-proxy-binding&gt;
  &lt;name&gt;stateless-rmi-invoker&lt;/name&gt;
  &lt;invoker-mbean&gt;jboss:service=invoker,type=http&lt;/invoker-mbean&gt;
  ...
&lt;/invoker-proxy-binding&gt;






Compression http


La compression des flux http n&#8217;est pas prise en compte par les invocateurs de JBoss.


Pour ajouter cette fonctionnalité coté serveur, il existe 2 techniques :


&lt;ol&gt;
* Ajouter un filtre de compression / décompression à l&#8217;application Web « invoker ».
* Activer l&#8217;option de compression sur le connecteur Coyote de Tomcat
&lt;/ol&gt;
Pour ajouter cette fonctionnalité coté client, il faut modifier l&#8217;invoker afin d&#8217;ajouter les tâches de compression / décompression au niveau du proxy.







Compression http dans Tomcat

L&#8217;activation de la compression par Tomcat se fait par l&#8217;ajout de l&#8217;option « compression="force" » dans le connecteur http Coyote. Il est possible de paramétrer un nouveau connecteur http, dédié aux flux compressés (fichier deploy/jbossweb-tomcat55.sar/server.xml).



&lt;Connector port="8090" compression="force" /&gt;



Ce connecteur doit être pris en compte dans un nouvel invoker (fichier deploy/http-invoker.sar/META-INF/jboss-service.xml).


Cet invoker est implémenté par 3 classes (info.jtips.jboss.GzHttpInvoker, GzHttpInvokerProxy et GzHttpUtil) inspirées des classes de l&#8217;invoker http standard de JBoss. Le jar contenant ces classes doit être déployé dans le répertoire lib de la configuration de JBoss ainsi que sur le poste client. Le détail de ces classe est listé en annexe.



&lt;mbean code="info.jtips.jboss.GzHttpInvoker"
       name="jboss:service=invoker,type=gzhttp"&gt;
   &lt;attribute name="InvokerURLPrefix"&gt;http://&lt;/attribute&gt;
   &lt;attribute name="InvokerURLSuffix"&gt;
       :8090/invoker/EJBInvokerServlet
   &lt;/attribute&gt;
   &lt;attribute name="UseHostName"&gt;true&lt;/attribute&gt;
&lt;/mbean&gt;



La configuration des EJB stateless doit être modifiée dans le fichier conf/standardjboss.xml du serveur, afin d&#8217;utiliser cet invoker.



&lt;invoker-proxy-binding&gt;
  &lt;name&gt;stateless-rmi-invoker&lt;/name&gt;
  &lt;invoker-mbean&gt;jboss:service=invoker,type=gzhttp&lt;/invoker-mbean&gt;
  ...
&lt;/invoker-proxy-binding&gt;



L&#8217;avantage de cette mise en oeuvre est sa simplicité au niveau de Tomcat et la possibilité de faire cohabiter des communications compressées et non compressées.
En revanche, cette solution limite la compression aux réponses.



Compression http avec un filtre

Le filtre de compression / décompression doit être installé dans l&#8217;application http-invoker.sar/invoker.war. Les essais ont été réalisés avec un composant open source (http://www.netspade.com/software/java/compression-filter/) ; ce composant ne pourra pas être utilisé car sa licence est incompatible avec le projet (GNU GPL).


Le filtre (netspade-compression-filter.jar) s&#8217;installe dans le répertoire deploy/http-invoker.sar/invoker.war/WEB-INF/lib. Il se paramètre dans le fichier  deploy/http-invoker.sar/invoker.war/WEB-INF/web.xml.



&lt;filter&gt;
   &lt;filter-name&gt;compress&lt;/filter-name&gt;
   &lt;filter-class&gt;
       com.netspade.servlet.compress.CompressionFilter
   &lt;/filter-class&gt;
&lt;/filter&gt;

&lt;filter-mapping&gt;
   &lt;filter-name&gt;compress&lt;/filter-name&gt;
   &lt;url-pattern&gt;/compress/*&lt;/url-pattern&gt;
&lt;/filter-mapping&gt;



L&#8217;invoker doit être adapté à cette configuration.



&lt;mbean code="info.jtips.jboss.GzHttpInvoker"
      name="jboss:service=invoker,type=gzhttp"&gt;
  &lt;attribute name="InvokerURLPrefix"&gt;http://&lt;/attribute&gt;
  &lt;attribute name="InvokerURLSuffix"&gt;
      :8080/invoker/compress/EJBInvokerServlet
   &lt;/attribute&gt;
  &lt;attribute name="UseHostName"&gt;true&lt;/attribute&gt;
&lt;/mbean&gt;



Dans la version actuelle, le filtre fait uniquement de la compression de réponses. Il est possible de le faire évoluer vers une compression des requêtes, bien que cela semble plus complexe.


La configuration proposée permet également de faire cohabiter des communications compressées et non compressées.


Remarques :


Cette configuration a été testée avec JBoss 4.0.2, configuration dérivée de « default », sous Windows XP pro.


Annexe : GzHttp Invoker



</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/HttpInvoker</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/HttpInvoker</guid>
        </item>
      
    
      
        <item>
          <title>Invocation avec compression sous JBoss 4</title>
          <description>
Cette page présente le code pour activer la compression GZip sous JBoss 4.
</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/GzHttpInvoker</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/GzHttpInvoker</guid>
        </item>
      
    
      
        <item>
          <title>JBoss derrière un firewall</title>
          <description>
Deux stratégies sont envisageables pour utiliser JBoss derrière un firewall : ouvrir les ports nécessaires ou n&#8217;utiliser que les ports ouverts. Dans la première approche, il faut connaître tous les ports utilisés, ainsi que leur utilisation. Dans la deuxième approche, on passe accède à tous les services et composants par un protocole standard comme http.


Ports utilisés


La liste des ports utilisés peut être obtenue grâce à la commande netstat, avec l&#8217;option -ao sous windows ou -al sous linux.


Dans la configuration default, les ports suivants sont utilisés :




1099, 1098 : JNDI standard, JNDI / RMI


4444, 4445 : JRMPInvoker (invoker standard), PooledInvoker


8080, 8009 : Connecteurs Tomcat (http, AJP)


8083 : Chargement de classes à distance via http (WebService)


8093 : JMS






JBoss sur http


Il est possible de faire passer la plupart des connexions réseau nécessaires au fonctionnement des applications JBoss sur le protocole http, généralement par le port 8080.


Pour cela, il faut adapter les protocoles suivants :




JNDI sur http


RMI sur http


JMS sur http




En standard, JBoss utilise 2 ports http : 8080 pour Tomcat et 8083 pour le ClassLoader http.


Port Tomcat

La gestion des ports Web se fait directement dans le conteneur Web. Pour Tomcat, les ports sont spécifiés dans le fichier deploy/jbossweb-tomcat55.sar/server.xml.


Le port AJP peut être supprimé si Tomcat fonctionne sans Apache.



  &lt;Connector port="8009" address="${jboss.bind.address}"
             emptySessionPath="true" enableLookups="false"
             redirectPort="8443" protocol="AJP/1.3"/&gt;



Pour http, nous conserverons le port 8080 puisque nous voulons y faire passer tous les flux !



http class loader

Pour supprimer ce service, dans le fichier conf/jboss-service.xml, on retire la portion suivante :



&lt;mbean code="org.jboss.web.WebService"
       name="jboss:service=WebService"&gt;
   &lt;attribute name="Port"&gt;8083&lt;/attribute&gt;
   &lt;attribute name="DownloadServerClasses"&gt;true&lt;/attribute&gt;
   &lt;attribute name="Host"&gt;${jboss.bind.address}&lt;/attribute&gt;
  &lt;attribute name="BindAddress"&gt;${jboss.bind.address}&lt;/attribute&gt;
&lt;/mbean&gt;



Services dépendants :




EJBDeployer : dans ejb-deployer.xml, enlever la dépendance optionnelle du MBean jboss.ejb:service=EJBDeployer





  &lt;depends optional-attribute-name="WebServiceName"&gt;
    jboss:service=WebService&lt;/depends&gt;





AxisService : la dépendance n&#8217;est pas marquée optionnelle, mais JBoss ne pose pas de problème au démarrage lorsqu&#8217;on enlève la portion suivante au MBean jboss.ws4ee:service=AxisService dans deploy/jboss-ws4ee.sar/META-INF/jboss-service.xml.





  &lt;depends&gt;jboss:service=WebService&lt;/depends&gt;



RESTE A FAIRE : TESTER EN WEB SERVICE





JNDI sur http


Appeler JNDI en http permet de fermer les port 1099 et 1098.


La modification se fait coté client, dans le fichier jndi.properties :



java.naming.factory.initial=org.jboss.naming.HttpNamingContextFactory
java.naming.provider.url=http://localhost:8080/invoker/JNDIFactory



Le MBean de nommage peut dans ce cas être reconfiguré pour désactiver le port 1099 (Port=-1), ainsi que le port 1098, dans le fichier conf/jboss-service.xml.



&lt;mbean code="org.jboss.naming.NamingService"
  name="jboss:service=Naming"
  xmbean-dd="resource:xmdesc/NamingService-xmbean.xml"&gt;
  &lt;attribute name="CallByValue"&gt;false&lt;/attribute&gt;
  &lt;attribute name="Port"&gt;-1&lt;/attribute&gt;
  &lt;depends optional-attribute-name="LookupPool"
           proxy-type="attribute"&gt;
    jboss.system:service=ThreadPool
  &lt;/depends&gt;
&lt;/mbean&gt;





RMI sur http


L&#8217;utilisation du protocole http pour appeler les EJB permet de fermer les ports 4444 (jrmp invoker) et 4445 (pooled invoker). Cependant, l&#8217;impact du passage de RMI sur http ne se limite pas aux EJB.


pooled invoker

Le port 4445 peut être directement désinstallé de la configuration default puisqu&#8217;il n&#8217;est pas utilisé.
La portion suivante peut être supprimée du fichier conf/jboss-service.xml :



&lt;mbean code="org.jboss.invocation.pooled.server.PooledInvoker"
       name="jboss:service=invoker,type=pooled"&gt;
  ...
&lt;/mbean&gt;




http invoker

Avant de désinstaller le jrmp invoker, il faut modifier tous les MBeans qui l&#8217;utilisent.




jboss:service=ClientUserTransaction


jboss:service=proxyFactory,target=ClientUserTransaction


jboss.jmx:type=adaptor,name=Invoker,protocol=jrmp,service=proxyFactory




Il faut aussi reconfigurer tous les profils d&#8217;EJB, ainsi que les datasources.



EJB

L&#8217;utilisation du « http invoker » se paramètre dans le fichier conf/standardjboss.xml, pour les profils d&#8217;EJB ; les profils en cluster peuvent être supprimés (configuration default).



&lt;invoker-proxy-binding&gt;
  &lt;name&gt;entity-rmi-invoker&lt;/name&gt;
  &lt;invoker-mbean&gt;jboss:service=invoker,type=http&lt;/invoker-mbean&gt;
  ...
&lt;/invoker-proxy-binding&gt;

&lt;invoker-proxy-binding&gt;
  &lt;name&gt;stateless-rmi-invoker&lt;/name&gt;
  &lt;invoker-mbean&gt;jboss:service=invoker,type=http&lt;/invoker-mbean&gt;
  ...
&lt;/invoker-proxy-binding&gt;
...




ClientUserTransaction

Le MBean ClientUserTransaction utilise aussi le jrmp invoker. Il peut habituellement être désinstallé, mais il peut aussi être conservé, avec la configuration suivante (fichier conf/jboss-service.xml) :



&lt;mbean
   code="org.jboss.tm.usertx.server.ClientUserTransactionService"
   name="jboss:service=ClientUserTransaction"
   xmbean-dd="resource:xmdesc/ClientUserTransaction-xmbean.xml"&gt;
  &lt;depends&gt;
    &lt;mbean
         code="org.jboss.invocation.http.server.HttpProxyFactory"
         name="jboss:service=proxyFactory,
               target=ClientUserTransactionFactory"&gt;
      &lt;attribute name="InvokerName"&gt;
        jboss:service=invoker,type=http
      &lt;/attribute&gt;
      &lt;attribute name="JndiName"&gt;
        UserTransactionSessionFactory
      &lt;/attribute&gt;
      &lt;attribute name="ExportedInterface"&gt;
        org.jboss.tm.usertx.interfaces.UserTransactionSessionFactory
      &lt;/attribute&gt;
      &lt;attribute name="ClientInterceptors"&gt;
        &lt;interceptors&gt;
          &lt;interceptor&gt;
            org.jboss.proxy.ClientMethodInterceptor
          &lt;/interceptor&gt;
          &lt;interceptor&gt;
            org.jboss.invocation.InvokerInterceptor
          &lt;/interceptor&gt;
        &lt;/interceptors&gt;
      &lt;/attribute&gt;
      &lt;depends&gt;jboss:service=invoker,type=http&lt;/depends&gt;
    &lt;/mbean&gt;
  &lt;/depends&gt;
  &lt;depends optional-attribute-name="TxProxyName"&gt;
    &lt;mbean
         code="org.jboss.invocation.http.server.HttpProxyFactory"
         name="jboss:service=proxyFactory,
               target=ClientUserTransaction"&gt;
      &lt;attribute name="InvokerName"&gt;
        jboss:service=invoker,type=http
      &lt;/attribute&gt;
      &lt;attribute name="JndiName"&gt;&lt;/attribute&gt;
      &lt;attribute name="ExportedInterface"&gt;
        org.jboss.tm.usertx.interfaces.UserTransactionSession
      &lt;/attribute&gt;
      &lt;attribute name="ClientInterceptors"&gt;
        &lt;interceptors&gt;
          &lt;interceptor&gt;
            org.jboss.proxy.ClientMethodInterceptor
          &lt;/interceptor&gt;
          &lt;interceptor&gt;
            org.jboss.invocation.InvokerInterceptor
          &lt;/interceptor&gt;
        &lt;/interceptors&gt;
      &lt;/attribute&gt;
      &lt;depends&gt;jboss:service=invoker,type=http&lt;/depends&gt;
    &lt;/mbean&gt;
  &lt;/depends&gt;
&lt;/mbean&gt;




Adaptateur JMX

Enfin, l&#8217;adaptateur JMX pour RMI doit être implémenté sur http (fichier deploy/jmx-invoker-service.xml).



&lt;mbean code="org.jboss.invocation.http.server.HttpProxyFactory"
       name="jboss.jmx:type=adaptor,name=Invoker,protocol=http,
             service=proxyFactory"&gt;
  &lt;attribute name="InvokerURL"&gt;
    http://localhost:8080/invoker/JMXInvokerServlet
  &lt;/attribute&gt;
  &lt;depends optional-attribute-name="InvokerName"&gt;
    jboss.jmx:type=adaptor,name=Invoker
  &lt;/depends&gt;
  &lt;attribute name="ExportedInterface"&gt;
    org.jboss.jmx.adaptor.rmi.RMIAdaptor
  &lt;/attribute&gt;
  &lt;attribute name="JndiName"&gt;jmx/invoker/HttpAdaptor&lt;/attribute&gt;
  &lt;attribute name="ClientInterceptors"&gt;
    ...
  &lt;/attribute&gt;
&lt;/mbean&gt;



Cette modification doit être reportée sur au niveau du MBean « jboss.admin:service=PluginManager » déployé avec le console-mgr (fichier deploy/ console-mgr.sar/META-INF/jboss-service.xml).



&lt;mbean code="org.jboss.console.manager.PluginManager"
       name="jboss.admin:service=PluginManager"&gt;
  &lt;depends&gt;
    jboss.jmx:type=adaptor,name=Invoker,protocol=http,
              service=proxyFactory
  &lt;/depends&gt;
  ...
&lt;/mbean&gt;
...




DataSource

Pour passer les datasources sur http, il faut ajouter un attribut jmx-invoker-name à la configuration de la data source.



  &lt;jmx-invoker-name&gt;jboss:service=invoker,type=http
  &lt;/jmx-invoker-name&gt;



Remarque :


Pour une datasource accessible à distance, cela se comprend. Pour une datasource standard, donc visible uniquement depuis JBoss, je ne vois pas le besoin d&#8217;un protocole jrmp.


Pour rendre une datasource accessible à distance :



  &lt;use-java-context&gt;false&lt;/use-java-context&gt;




jrmp invoker

Le jrmp invoker peut maintent être désinstallé. La portion suivante peut être supprimée du fichier conf/jboss-service.xml :



&lt;mbean code="org.jboss.invocation.jrmp.server.JRMPInvoker"
       name="jboss:service=invoker,type=jrmp"&gt;
  &lt;attribute name="RMIObjectPort"&gt;4444&lt;/attribute&gt;
  &lt;attribute name="ServerAddress"&gt;
     ${jboss.bind.address}&lt;/attribute&gt;
  &lt;depends&gt;jboss:service=TransactionManager&lt;/depends&gt;
&lt;/mbean&gt;






JMS sur http


Toute la configuration JMS se fait dans le répertoire deploy/jms/.


Le port 9093 est utilisé par le protocole de JBoss UIL2 (Unified Invocation Layer), paramétré dans le fichier uil2-service.xml. Il suffit de supprimer ce fichier&#8230;&#8203;


Le clients JMS devront s&#8217;adresser à la ConnectionFactory référencée sous le nom HTTPConnectionFactory dans JNDI. Pour minimiser l&#8217;impact sur les client, on peut renommer la factory http afin qu&#8217;elle soit enregistrée sous le nom ConnectionFactory. Cela se fait dans le fichier deploy/jms/jbossmq-httpil.sar/META-INF/jboss-service.xml, en remplaçant :



  &lt;attribute name="ConnectionFactoryJNDIRef"&gt;
    HTTPConnectionFactory&lt;/attribute&gt;



par :



  &lt;attribute name="ConnectionFactoryJNDIRef"&gt;
    ConnectionFactory&lt;/attribute&gt;



Il est possible aussi d&#8217;utiliser la technique des alias JNDI, qui référence le même composants sous plusieurs nom. Dans ce cas, plutôt que de remplacer l&#8217;attribut  ConnectionFactoryJNDIRef, on ajoute un mbean dans le fichier cité ci-dessus :



&lt;mbean code="org.jboss.naming.NamingAlias"
       name="jboss.mq:service=InvocationLayer,type=HTTP,
              alias=ConnectionFactory"&gt;
  &lt;attribute name="FromName"&gt;ConnectionFactory&lt;/attribute&gt;
  &lt;attribute name="ToName"&gt;HTTPConnectionFactory&lt;/attribute&gt;
&lt;/mbean&gt;



</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/Firewall</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/Firewall</guid>
        </item>
      
    
      
        <item>
          <title>Déploiement avec JBoss</title>
          <description>
La configuration standard de JBoss propose un système de déploiement d&#8217;applications très simple et pratique, mais pas forcément adapté à un environnement de production. Heureusement, il propose d&#8217;autres solutions&#8230;&#8203;


Déploiement automatique


De façon standard, le répertoire deploy/ d&#8217;une configuration est l&#8217;endroit où déployer les services, composants et applications. Il suffit d&#8217;y déposer un fichier, conforme aux spécifications propres à chaque type de composant, pour que JBoss prenne le déploiement en compte. Il est possible de déployer les fichiers dans deploy/ ou dans ses sous-répertoires.


Types de déploiement

Chaque type de fichier est pris en compte par un service de déployement approprié. Il faut noter que le déploiement peut se faire sous 3 forme :




Fichier d&#8217;archive (.jar, .war, .ear,&#8230;&#8203;)


Répertoire, avec un nom de fichier d&#8217;archive, extension comprise


Fichier de service




EARDeployer



Fichier d&#8217;archives .ear (enterprise archive)


META-INF/application.xml


Application J2EE, contiennent d&#8217;autres archives


deploy/ear-deployer.xml





EJBDeployer



Fichier d&#8217;archives .jar (java archive)


Contient META-INF/ejb-jar.xml


Module d&#8217;EJBs


deploy/ejb-deployer.xml





SARDeployer



Fichier xml *-service.xml


Fichier d&#8217;archives .sar (service archive)


META-INF/jboss-service.xml


Service JBoss (MBeans)


conf/xmdesc/org.jboss.deployment.JARDeployer-xmbean.xml





AbstractWebDeployer



Doit être implémenté pour le conteneur de servlet


TomcatDeployer


Fichier d&#8217;archives .war (web archive)


WEB-INF/web.xml


Application Web





RARDeployer



Fichier d&#8217;archives .rar (resource archive)


META-INF/ra.xml


Connecteurs JCA


deploy/jbossjca-service.xml





XSLSubDeployer



Fichier xml


Complément de configuration JCA


Fichiers *-ds.xml (datasources)


deploy/jbossjca-service.xml





HARDeployer



Fichier d&#8217;archives .har (hibernate archive)


META-INF/hibernate-service.xml


Module de persistence Hibernate


deploy/jboss-hibernate.deployer/META-INF/jboss-service.xml





AspectDeployer



Fichier xml *-aop.xml


Fichier d&#8217;archives .aop


META-INF/jboss-aop.xml


deploy/jboss-aop.deployer/META-INF/jboss-service.xml





BeanShellSubDeployer



Fichier .bsh


Script Bean Shell


deploy/bsh-deployer.xml






Répertoires de déploiement

Le répertoire de déploiement par défaut est deploy/, il est possible de le modifier, d&#8217;en ajouter d&#8217;autres, locaux ou distants.


Pour que les déploiements dans le nouveau répertoire (deployapp) soient pris en compte, il faut le déclarer au scanner, dans le fichier conf/jboss-service.xml de la configuration.



 &lt;mbean code="org.jboss.deployment.scanner.URLDeploymentScanner"
     name="jboss.deployment:type=DeploymentScanner,flavor=URL"&gt;
   ...
   &lt;attribute name="URLs"&gt;
     deploy/,deployapp/
   &lt;/attribute&gt;
   ...
 &lt;/mbean&gt;






Déploiement manuel


Il est possible de désactiver le scan automatique dans le fichier conf/jboss-service.xml.



&lt;attribute name="ScanEnabled"&gt;false&lt;/attribute&gt;



scan manuel

Pour déclencher un scan manuel depuis la jmx-console :




MBean jboss.deployment:DeploymentScanner


opération scan




Pour réactiver temporairement le scan automatique depuis la jmx-console :




MBean jboss.deployment:DeploymentScanner


attribut ScanEnabled=true, "Apply Changes"




Pour Ajouter / enlever temporairement une URL :




MBean jboss.deployment:DeploymentScanner


addURL(url) / removeURL(url)





déploiement manuel

Pour Déployer / retirer manuellement un fichier :




MBean jboss.system:MainDeployer


opération deploy(url) / undeploy(url)





Remarques

Les opérations deploy, undeploy et redeploy du MBean jboss.system:MainDeployer existent en plusieurs version, avec des types de paramètres différents. Depuis la jmx-console, il faut utiliser les versions qui prennent une url de type java.net.URL ; les autres ne fonctionnent pas.


L&#8217;appel de ces opérations est mal géré dans MC4J (1.2b6) :




L&#8217;appel de deploy / undeploy / redeploy provoque une exception.


Le MBean jboss.deployment:flavor=URL,type=DeploymentScanner ne se charge pas.




L&#8217;appel depuis l&#8217;utilitaire twiddle se fait par les lignes de commande suivantes :



twiddle invoke "jboss.system:service=MainDeployer"  redeploy "file:///C:/mandeploy/hello.war/"



L&#8217;appel JMX depuis une classe Java est décrit dans un autre article.





Ordre de déploiement


Scanner et Sorter

Au démarrage de JBoss, l&#8217;ordre de déploiement des fichiers est déterminé par un DeploymentSorter. Par défaut, celui-ci déploie les fichiers en fonction de leur extension ("sar", "service.xml", "rar", "jar", "war", "ear",&#8230;&#8203;), dans l&#8217;ordre alphabétique. JBoss fournit un PrefixDeploymentSorter qui déploie d&#8217;abord les fichiers sans préfixe, selon leur extension, puis déploie les fichiers préfixé par un nombre entier sont déployés selon la valeur numérique de ce préfixe.



Dépendances

Plutôt que de gérer des noms avec préfix, il est possible de spécifier les dépendances dans les fichiers de déploiement, EJB par EJB. Ainsi, si l&#8217;EJB A a une référence vers l&#8217;EJB B (&lt;ejb-ref&gt; dans ejb-jar.xml), il est possible de préciser cette dépendance dans le fichier jboss.xml :



&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;!DOCTYPE jboss PUBLIC "-//JBoss//DTD JBOSS 4.0//EN" "http://www.jboss.org/j2ee/dtd/jboss_4_0.dtd"&gt;

&lt;jboss&gt;
  &lt;enterprise-beans&gt;
     &lt;session&gt;
       &lt;ejb-name&gt;A&lt;/ejb-name&gt;
       &lt;jndi-name&gt;ejb/A&lt;/jndi-name&gt;
       &lt;depends&gt;jboss.j2ee:service=EJB,jndiName=ejb/B&lt;/depends&gt;
     &lt;/session&gt;
  &lt;/enterprise-beans&gt;
&lt;/jboss&gt;




</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/JBoss/Deploiement</link>
          <guid isPermaLink="true">https://www.jtips.info/JBoss/Deploiement</guid>
        </item>
      
    
      
        <item>
          <title>JAAS</title>
          <description>
JAAS (Java Authentication and Authorization Service) est une API standard de java permettant de gérer des identifications et les droits associés (par rôles) au niveau du client et du serveur d&#8217;application. JAAS est intégrée à J2SE et est l&#8217;API standard utilisée par les serveurs d&#8217;application J2EE.
Elle permet de séparer la gestion des droits d&#8217;accès aux composants J2EE du code métier. Dans un premier temps, JAAS authentifie, c&#8217;est-à-dire qu&#8217;il valide l&#8217;identité du client, puis gère les autorisations, c&#8217;est-à-dire qu&#8217;il valide les droits d&#8217;accès pour un client authentifié. Pour ce faire, les identités sont regroupées en rôles et les autorisations accordées par rôle.


Sécurité des EJB


Restrictions d&#8217;accès

Les autorisations d&#8217;accès aux EJB se basent sur des permissions accordées à des rôles pour des méthodes. Elles se paramètrent dans le fichier META-INF/ejb-jar.xml.



&lt;method-permission&gt;
  &lt;role-name&gt;Role1&lt;/role-name&gt;
  &lt;method&gt;
    &lt;ejb-name&gt;MyEjbStateless&lt;/ejb-name&gt;
    &lt;method-name&gt;myBusinessMethod&lt;/method-name&gt;
  &lt;/method&gt;
&lt;/method-permission&gt;




JAAS en client / serveur

Dans une architecture client / serveur à base d&#8217;EJB, JAAS se met en oeuvre sous forme de modules coté client et coté serveur, ainsi que du paramétrage des 2 parties.








Principales classes

Les principales classes mises en oeuvre sont




Principal




Représente identité du demandeur




Subject




Représente le demandeur (personne ou application), il peut avoir plusieurs identités




LoginModule




Interface du composant d&#8217;authentification, implémentée par le fournisseur d&#8217;authentification




LoginContext




Méthode login(), elle exploite les modules décrites dans la configuration de JAAS




CallbackHandler




Gestionnaire de callbacks




Callback




Interface de demande d&#8217;information (login, password)










Mise en oeuvre avec JBoss


Les exemples ci-dessous ont été testés avec JBoss 4.0.2.


Coté client

Callback

Les informations de login et password sont transmises au module via le callback handler. Dans notre exemple, les informations sont transmises au handler lors de sa création. Elle sont ensuite transmises au module via les objets de callback appropriés : NameCallback pour le login et PasswordCallback pour le mot de passe.



package fr.sewatech.j2ee.security.client;

import java.io.IOException;
import javax.security.auth.callback.*;

public class CustomLoginHandler implements CallbackHandler {

   private String login;
   private char[] password;

   public CustomLoginHandler(String login, String password) {
       this.login = login;
       this.password = password.toCharArray();
   }

   public void handle(Callback[] callbacks) throws IOException,
           UnsupportedCallbackException {
       for (int i = 0; i &lt; callbacks.length; i++) {
           if (callbacks[i] instanceof NameCallback) {
               NameCallback nc = (NameCallback) callbacks[i];
               nc.setName(login);
           } else if (callbacks[i] instanceof PasswordCallback) {
               PasswordCallback pc = (PasswordCallback) callbacks[i];
               pc.setPassword(password);
           } else {
               throw new UnsupportedCallbackException(callbacks[i],
                       "Mauvais Callback");
           }
       }
   }
}




Login

La connexion à JAAS se fait via le LoginContext. Pour que celui-ci fonctionne correctement, avec les modules sélectionnés, il faut spécifier un fichier de configuration. La meilleure façon de procéder est de passer la propriété système -Djava.security.auth.login.config==jaas.conf.


Exemple simple de fichier de congiguration JAAS :



sewatech {
  fr.sewatech.j2ee.security.client.SwClientLoginModule  required;
}
jboss {
  // jBoss LoginModule
  org.jboss.security.ClientLoginModule  required;
};



L&#8217;appel de la méthode login du LoginContext appellera les modules en fonction du nom du contexte.



   private Subject login() throws LoginException {
       CallbackHandler handler = new CustomLoginHandler(login, password);
       // Initialisation du contexte (configuration "jboss")
       LoginContext lc = new LoginContext("jboss", handler);
       // Demande d'authentification
       lc.login();
       // Lecture du sujet authentifié
       return getSubject();
   }




Logout

La déconnexion se fait par l&#8217;appel de la méthode logout() du LoginContext.



         lc.logout();





Coté serveur

Le serveur d&#8217;application fournit des modules prêts à l&#8217;emploi, pour vérifier l&#8217;authentification dans une base de données, dans un fichier, dans un annuaire LDAP,&#8230;&#8203; Des modules externes peuvent aussi être intégrés.


Les configurations JAAS sont décrites dans le fichier conf/login-service.xml, elles sont nommées, ce qui permettra de choisir EJB par EJB à quel domaine de sécurité ont souhaite le soumettre.
L&#8217;exemple ci-dessous utilise le module de validation en base de données ; il est nécessaire de passer les requêtes à la base en paramètres.



&lt;application-policy name="sewatech"&gt;
  &lt;authentication&gt;
    &lt;login-module
         code="org.jboss.security.auth.spi.DatabaseServerLoginModule"
         flag="required"&gt;
      &lt;module-option name="dsJndiName"&gt;java:/SwDS&lt;/module-option&gt;
      &lt;module-option name="principalsQuery"&gt;
        SELECT PASSWD FROM SW_USERS WHERE USERID=?
      &lt;/module-option&gt;
      &lt;module-option name="rolesQuery"&gt;
        SELECT ROLEID, 'Roles' FROM SW_ROLES WHERE USERID=?
      &lt;/module-option&gt;
    &lt;/login-module&gt;
  &lt;/authentication&gt;
&lt;/application-policy&gt;



Le domaine de sécurité peut être spécifié pour chaque EJB, dans le fichier META-INF/ejb-jar.xml ou globalement, pour tous les EJB, dans le fichier conf/standardjboss.xml.



 &lt;jboss&gt;
   ...
   &lt;container-configuration&gt;
     &lt;container-name&gt;Standard Stateless SessionBean&lt;/container-name&gt;
     ...
     &lt;security-domain&gt;java:/jaas/sewatech&lt;/security-domain&gt;
   &lt;/container-configuration&gt;
   ...
 &lt;/jboss&gt;






Mise en oeuvre dans BEA Weblogic


Les exemples ci-dessous ont été testés avec BEA Weblogic 9.1.
Pour les informations d&#8217;identification soient transmises du client vers le serveur, Weblogic supporte JAAS et la technique du contexte JNDI.


Coté client

Contexte JNDI

C&#8217;est la technique la plus simple, mais aussi la plus ancienne et la plus limitée.
Le username et le password sont transmis avant le lookup au contexte JNDI :



Hashtable props = new Hashtable();
props.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
props.put(Context.PROVIDER_URL, "t3://myserver:7001");
props.put(Context.SECURITY_PRINCIPAL, "alexis");
props.put(Context.SECURITY_CREDENTIAL, "alexis01");

Context ctx = new InitialContext(props);
...




Module JAAS

Comme pour JBoss, il est possible de transmettre au serveur des informations d&#8217;identité via JAAS. PAr contre, il y a quelques contrainte en plus par rapport à JBoss.


Tout d&#8217;abord, il faut obligatoirement utiliser le module fourni par BEA.



weblo {
  // jBoss LoginModule
  weblogic.security.auth.login.UserPasswordLoginModule  required;
};



Ensuite, il faut obligatoirement utiliser un CallbackHandler qui gère les URLCallback, comme par exemple l&#8217;URLCallbackHandler.



   private Subject login() throws LoginException {
       CallbackHandler handler = new URLCallbackHandler(login, password.getBytes(), url);
       // Initialisation du contexte (configuration "weblo")
       LoginContext lc = new LoginContext("jboss", handler);
       // Demande d'authentification
       lc.login();
       // Lecture du sujet authentifié
       return getSubject();
   }



Enfin, l&#8217;accès au serveur doit se faire dans une PrivilegedAction, via la classe weblogic.security.Security



   Security.runAs(subject, new PrivilegedAction() {
       public Object run() {
           try {
               Context ctx = new InitialContext();
               MyHome home = ctx.lookup(...);
               ...
           catch (Exception ex) {
               ...
           }
           return null;
       }});







Conclusion


JAAS apporte une solution souple et pratique pour sécuriser des composants J2EE, que ce soient des EJB ou des applications Web. Cependant, des solutions alternatives, basées sur les mêmes principes de configuration externe, avec le même fonctionnement modulaire, existent. Des solutions comme Acegi Security offrent même une souplesse plus importante.


</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/J2EE/JAAS</link>
          <guid isPermaLink="true">https://www.jtips.info/J2EE/JAAS</guid>
        </item>
      
    
      
        <item>
          <title>Mise en oeuvre du design pattern _Business Delegate_</title>
          <description>
Principes


La partie cliente de cette communication doit prendre en compte l&#8217;organisation de la partie serveur, les protocoles choisis et les exigences techniques identifiées, dont la possibilité d&#8217;ajouter ultérieurement un cache coté client.


Par ailleurs, le client et les façades métiers s&#8217;échangent des données sous forme d&#8217;objets de transfert.


Cache client

Certains débits réseaux pouvant être très bas (64 kbps), il est envisagé d&#8217;introduire un cache coté client afin d&#8217;accélérer le fonctionnement de l&#8217;application dans ce type d&#8217;environnement.
Il est donc nécessaire de mettre d&#8217;encapsuler la sortie de la couche client dans un Business Delegate qui pourra supporter le cache.



Business Delegate

Nous utiliserons un Business Delegate pour :




Réduire le couplage entre la couche métier et la couche présentation


Cacher la complexité de manipulation des objets métiers distribués, par encapsulation


Mettre en cache les références distantes (et, ultérieurement les résultats)


Répartir plus facilement le travail entre les développeurs de la couche présentation et les développeurs de la couche métier




Le principe est d&#8217;implémenter une classe business delegate par façade métier, en reprenant les méthodes métier, avec leur signature. Chaque business delegate gérera les exceptions bas niveau, en particulier les problèmes de déconnexion.


Les recherches JNDI et la manipulation des home seront factorisés dans un objet « Service Locator ».


Les préoccupations périphériques au métiers pourront être prises en compte par le delegate :




Gestion d&#8217;un cache de données


Gestion et transmission de l&#8217;authentification





Service Locator



Centraliser toutes les utilisations de JNDI et de l&#8217;objet InitialContext


Améliorer les performances d&#8217;accès aux home (par utilisation d’un cache)


Fournir un seul point de contrôle


Recherche les EJB home et crée les EJB




Ce pattern apporte de la flexibilité et de l’extensibilité au niveau framework.



Transfer Object

Les échanges entre les couches se font grâce à des objets de transfert, ou à des listes d&#8217;objets de transfert.
Ces objets sont des java beans, sans méthode métier, dont la responsabilité est uniquement de transporter des données.
Ces beans seront différents de ceux gérés par Hibernate au sein de la couche serveur. Ceci évite tous les problèmes potentiels de lazy loading en dehors des session hibernate. Ces beans pourront être implémentés par des beans standards ou par des beans dynamiques. Dans tous les cas, l&#8217;utilisation de la librairie BeanUtils d&#8217;Apache facilitera la manipulation de ces beans (cf. org.apache.commons.beanutils.DynaBean et org.apache.commons.beanutils.BasicDynaBean).





Mise en oeuvre avec Spring


L&#8217;adoption de Spring pour l&#8217;implémentation de l&#8217;accès au serveur simplifie grandement la tâche de développement.


Business Delegate simple

Le SimpleRemoteStatelessSessionProxyFactoryBean permet de construire dynamiquement des beans d&#8217;accès aux EJB, prenant en compte les fonctions de Service Locator.


Les méthodes de délégation sont déclarées dans une interface qui reprend la plupart des méthodes présentes dans l&#8217;interface remote de l&#8217;EJB, sans les RemoteException.



public interface HelloDelegate {
   HelloTO hello(String who);
}



Le bean est ensuite déclaré (fichier ejb-client.xml) :



&lt;bean id="helloDelegateSimple" lazy-init="true"
     class="org.springframework.ejb.access.




SimpleRemoteStatelessSessionProxyFactoryBean"&gt;




   &lt;property name="jndiName" value="ejb/Hello" /&gt;
   &lt;property name="businessInterface"
             value="info.jtips.j2ee.springdelegate.HelloDelegate" /&gt;
&lt;/bean&gt;



De cette façon, il est possible d&#8217;accéder à l&#8217;EJB de façon très souple, via le contexte Spring.



   final ApplicationContext ctx ;
   ctx = new ClassPathXmlApplicationContext(new String[] {"ejb-client.xml"});

   HelloDelegate del = (HelloDelegate)ctx.getBean("helloDelegateSimple");
   HelloTO hi = del.hello("someone"));
   ...




Business Delegate élaboré

La technique décrite dans le paragraphe précédent est efficace pour accéder aux EJB. Elle réduit considérablement la dépendance entre le client et les EJB. Cependant, elle ne répond pas totalement aux objectifs du business delegate car elle ne permet pas d&#8217;encapsuler la gestion des préoccupations périphériques.
L&#8217;ajout de ces préoccupations peut se faire par une technique de développement orientée aspect (AOP), elle aussi prise en compte par Spring.


Pour implémenter cette technique, nous développerons d&#8217;abord un intercepteur qui prendra en compte une préoccupation spécifique (par exemple, gestion des exceptions).



public class ExceptionHandlingInterceptor implements MethodInterceptor {

   public Object invoke(MethodInvocation invocation) throws Throwable {
       try {
           return invocation.proceed();
       } catch (ServerException e) {
           throw new BusinessDelegateException(
                   "Problème de connexion au serveur", e.getCause());
       } catch (Exception e) {
           throw new BusinessDelegateException(e);
       }
   }
}



Cet intercepteur appel la méthode cible, puis attrape les exceptions ServerException.


Ensuite, il faut implémenter un bean dynamique (proxy) qui implémente l&#8217;interface de délégation, qui a pour cible le bean de délégation simple et qui oriente systématiquement les appels vers l&#8217;intercepteur choisi.



&lt;bean id="helloDelegate"
      class="org.springframework.aop.framework.ProxyFactoryBean"&gt;
    &lt;property name="proxyInterfaces"&gt;
        &lt;value&gt;info.jtips.j2ee.springdelegate.HelloDelegate&lt;/value&gt;
    &lt;/property&gt;
    &lt;property name="target"&gt;
        &lt;ref local="helloDelegateSimple"/&gt;
    &lt;/property&gt;
    &lt;property name="interceptorNames"&gt;
       &lt;list&gt;
           &lt;value&gt;exception&lt;/value&gt;
       &lt;/list&gt;
    &lt;/property&gt;
&lt;/bean&gt;



Dans une version plus avancée, il peut y avoir plusieurs intercepteurs et, surtout, les règles d&#8217;interception peuvent être affinées par la mise en place de « pointcuts » et d'« advisors ».


L&#8217;accès aux EJB est tout aussi simple que précédemment :



   final ApplicationContext ctx ;
   ctx = new ClassPathXmlApplicationContext(new String[] {"ejb-client.xml"});

   HelloDelegate del = (HelloDelegate)ctx.getBean("helloDelegate");
   HelloTO hi = del.hello("someone"));
   ...




</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/J2EE/BusinessDelegate</link>
          <guid isPermaLink="true">https://www.jtips.info/J2EE/BusinessDelegate</guid>
        </item>
      
    
      
        <item>
          <title>Accueil</title>
          <description>
Ce wiki a été créé en 2006 par des développeurs et formateurs, spécialistes de Java.
Le but principal est de recueillir les informations techniques rassemblées au cours de leurs missions, sous forme de bloc-note.
</description>
          <pubDate>2006-04-18T00:00:00+00:00</pubDate>
          <link>https://www.jtips.info/Accueil</link>
          <guid isPermaLink="true">https://www.jtips.info/Accueil</guid>
        </item>
      
    
  </channel>
</rss>
