Implémentation d'une architecture deux tiers en Java EE

KX Messages postés 16733 Date d'inscription samedi 31 mai 2008 Statut Modérateur Dernière intervention 31 janvier 2024 - 21 mai 2022 à 11:58
Cet article a pour but d’expliquer un code Java complet d'une application serveur qui sert d'intermédiaire entre la base de données et les clients (architecture deux tiers).
Pour plus d'explications sur l'architecture voir l'article Architecture client/serveur à 3 niveaux
Le programme d'exemple fait très peu de chose, il permet de gérer une liste d'utilisateurs (table USER en base de données) qui disposent d'un LOGIN et d'un PASSWORD.

Stack technique

Dans le cadre de cet article nous utiliserons les technologies Java EE 7 suivantes :
Enfin, on utilise un serveur Grizzly 2.4 et une base de données H2 1.4

Le projet est configuré avec Maven, le fichier pom.xml liste les dépendances du programme :
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
        <modelVersion>4.0.0</modelVersion>

        <groupId>ccm.kx</groupId>
        <artifactId>server</artifactId>
        <packaging>jar</packaging>
        <version>1.0</version>

        <properties>
                <maven.compiler.source>1.8</maven.compiler.source>
                <maven.compiler.target>1.8</maven.compiler.target>
        </properties>

        <dependencies>
                <dependency>
                        <groupId>com.h2database</groupId>
                        <artifactId>h2</artifactId>
                        <version>1.4.197</version>
                        <scope>runtime</scope>
                </dependency>
                <dependency>
                        <groupId>org.hibernate</groupId>
                        <artifactId>hibernate-core</artifactId>
                        <version>5.3.7.Final</version>
                </dependency>
                <dependency>
                        <groupId>org.glassfish.jersey.containers</groupId>
                        <artifactId>jersey-container-grizzly2-http</artifactId>
                        <version>2.27</version>
                </dependency>
                <dependency>
                        <groupId>org.glassfish.jersey.inject</groupId>
                        <artifactId>jersey-hk2</artifactId>
                        <version>2.27</version>
                        <scope>runtime</scope>
                </dependency>
        </dependencies>
</project>

Accès aux données

La classe UserEntity.java définit la table USER de la base de données et l'objet Java qui permet de manipuler les données correspondantes (LOGIN et PASSWORD).

Remarque : il n'est pas nécessaire de créer la table en base de données, le programme le fera automatiquement lors de sa première exécution.
package ccm.kx.server.jpa;

import javax.persistence.*;

@Entity
@Table(name = "user")
@NamedQuery(name = "findAllUsers", query = "SELECT login FROM UserEntity")
public class UserEntity {

    @Id
    @Column(name = "login")
    private String login;

    @Column(name = "password")
    private String password;

    public String getLogin(){
        return login;
    }

    public void setLogin(String login) {
        this.login = login;
    }

    public String getPassword(){
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

Quant à la classe UserService.java elle définit des méthodes d'accès qui permettent de faire des requêtes en base de données.
package ccm.kx.server.service;

import java.util.List;
import javax.persistence.EntityManager;
import ccm.kx.server.ServerLauncher;
import ccm.kx.server.jpa.UserEntity;

public class UserService {

    private EntityManager em = ServerLauncher.getEntityManager();

    public boolean checkUser(String login, String password) {
        UserEntity user = em.find(UserEntity.class, login);
        return user != null && user.getPassword().equals(password);
    }

    public void createUser(String login, String password) {
        UserEntity user = new UserEntity();
        user.setLogin(login);
        user.setPassword(password);
        em.getTransaction().begin();
        em.persist(user);
        em.getTransaction().commit();
    }

    public void updatePassword(String login, String password) {
        UserEntity user = em.find(UserEntity.class, login);
        if (user == null)
            return;
        user.setPassword(password);
        em.getTransaction().begin();
        em.persist(user);
        em.getTransaction().commit();
    }

    public List<String> listAllUsers(){
        return em.createNamedQuery("findAllUsers", String.class).getResultList();
    }
}

Remarque : comme pour la création de la table en base de données, il n'est pas nécessaire d'écrire les requêtes SQL non plus, le code Java qui décrit les données que l'on souhaite obtenir permet de construire automatiquement les bonnes requêtes.

NB. Pour la dernière méthode, on a utilisé une @NamedQuery "findAllUsers" qui est décrite dans UserEntity via une requête JPQL (pas SQL), c'est à dire que l'on écrit
SELECT login FROM UserEntity
(l'attribut login de la classe UserEntity) mais pas
SELECT login FROM user
(la colonne login de la table user).

Resources REST

Les services REST permettent aux clients de communiquer avec le serveur, c'est le point d'entrée des requêtes et le point de sortie des réponses, UserResource.java en définit trois :
  • /user/list pour lister les utilisateurs
  • /user/updatePassword pour modifier un mot de passe
  • /user/create pour créer un nouvel utilisateur.

package ccm.kx.server.rest;

import javax.ws.rs.*;
import javax.ws.rs.core.Response;

import ccm.kx.server.service.UserService;

@Path("user")
public class UserResource extends AbstractResource {
    private final UserService userService = new UserService();

    @GET
    @Path("list")
    public Response listUsers(){
        return runAction(() -> userService.listAllUsers());
    }

    @GET
    @Path("updatePassword")
    public Response updatePassword(
            @QueryParam("userLogin") String userLogin, 
            @QueryParam("userPassword") String userPassword,
            @QueryParam("newPassword") String newPassword) {
        return runAuthenticatedAction(userLogin, userPassword,
                () -> userService.updatePassword(userLogin, newPassword));
    }

    @GET
    @Path("create")
    public Response create(
            @QueryParam("userLogin") String userLogin,
            @QueryParam("userPassword") String userPassword,
            @QueryParam("newLogin") String newLogin,
            @QueryParam("newPassword") String newPassword) {
        return runAuthenticatedAction(userLogin, userPassword,
                () -> userService.createUser(newLogin, newPassword));
    }
}

Remarque : je me base ici sur des méthodes runAction et runAuthenticatedAction issues de la classe parente AbstractResource.java
package ccm.kx.server.rest;

import java.util.function.Supplier;
import java.util.logging.*;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import ccm.kx.server.service.UserService;

public abstract class AbstractResource {
    private final UserService userService = new UserService();

    protected final Response runAction(Supplier<? extends Object> actionWithResult) {
        try {
            return Response.status(Status.OK).entity(String.valueOf(actionWithResult.get())).build();
        } catch (RuntimeException e) {
            Logger.getGlobal().log(Level.SEVERE, e.toString(), e);
            return Response.status(Status.INTERNAL_SERVER_ERROR).build();
        }
    }

    protected final Response runAction(Runnable actionWithoutResult) {
        return runAction(asSupplier(actionWithoutResult));
    }

    protected final Response runAuthenticatedAction(String userLogin, String userPassword, Supplier<? extends Object> actionWithResult) {
        if (!userService.checkUser(userLogin, userPassword)) {
            return Response.status(Status.UNAUTHORIZED).build();
        } else {
            return runAction(actionWithResult);
        }
    }

    protected final Response runAuthenticatedAction(String userLogin, String userPassword, Runnable actionWithoutResult) {
        return runAuthenticatedAction(userLogin, userPassword, asSupplier(actionWithoutResult));
    }

    private static Supplier<Void> asSupplier(Runnable runnable) {
        return () -> {
            runnable.run();
            return null;
        };
    }
}

Configuration

La base de données et son lien avec les classes Java est défini dans persistence.xml :

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
 version="2.0">
   <persistence-unit name="ccm_kx_server" transaction-type="RESOURCE_LOCAL">
      <class>ccm.kx.server.jpa.UserEntity</class>
      <properties>
         <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
         <property name="javax.persistence.jdbc.url" value="jdbc:h2:file:C:/h2/ccm.kx.server.h2" />
         <property name="javax.persistence.jdbc.user" value="admin" />
         <property name="javax.persistence.jdbc.password" value="admin" />
         <property name="javax.persistence.schema-generation.database.action" value="drop-and-create" />
         <property name="javax.persistence.sql-load-script-source" value="META-INF/data.sql" />
      </properties>
   </persistence-unit>
</persistence>

Remarques :
  • dans cette configuration la base de données sera créée dans le dossier C:/h2/ qu'il conviendra de modifier si nécessaire.
  • on est ici en mode drop-and-create, qui va détruire et créer automatiquement la base de données à chaque démarrage, c'est pratique lors du développement du programme, mais dans la mesure où cela supprime toutes les données à chaque fois, il faudra retirer cette ligne une fois le programme terminé.
  • on définit dans data.sql un script à utiliser lors du démarrage du programme, c'est la seule requête SQL du programme :

INSERT INTO USER (LOGIN, PASSWORD)
SELECT 'admin', 'admin' WHERE NOT EXISTS (SELECT * FROM USER);

Quant à la configuration du serveur, elle se place dans la méthode main de ServerLauncher.java qui sera ici en standalone, mais pourrait être embarqué dans un serveur d'application par exemple.
package ccm.kx.server;

import java.io.IOException;
import java.net.URI;
import java.util.logging.Logger;
import javax.persistence.*;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;

public class ServerLauncher {

    private static final EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("ccm_kx_server");

    public static EntityManager getEntityManager(){
        return entityManagerFactory.createEntityManager();
    }

    public static void main(String[] args) throws IOException {
        try {
            URI uri = URI.create("http://localhost:8080/");
            ResourceConfig configuration = new ResourceConfig().packages("ccm.kx.server.rest");
            HttpServer server = GrizzlyHttpServerFactory.createHttpServer(uri, configuration);

            Logger.getGlobal().info("\n\tPress Enter to shutdown server.\n");
            System.in.read();

            server.shutdownNow();
        } finally {
            entityManagerFactory.close();
        }
    }
}

Utilisation

Le code complet et son mode d'emploi est disponible sur CodeS-SourceS :
https://codes-sources.commentcamarche.net/source/102836-implementation-d-une-architecture-deux-tiers-en-java-ee