Projet de synthèse
Introduction
L'objectif de cet article est de réaliser une application REST (REpresentational State Transfer) en Java avec les technologies Jakarta EE 8. Le format d'échange de donnée sera JSON (JavaScript Object Notation).
Il s’agira d'une simple application CRUD (Create Read Upade Delete). Sans logique métier particulier.
Nom de code : Dyna Une application dynamique et générique pour les opérations de CRUD.
Préparatif
Pour réaliser ce projet, nous utiliserons :
- Java 11
- Payara Server 5.X
- Maven 3.6.X
Pas besoin de bibliothèque logicielle supplémentaire.
Arborescence du projet :
+ fr.zelmoacademy.dyna
+ common
- controller
- persistence
- service
- util
- feature1
- feature2
- ...
Légende:
fr.zelmoacademy.dyna
: package racine du projet.common
: Partie commune de tout le projet (c'est ici que nous travaillerons).controller
: Partie contrôleur REST.persistence
: Partie modèle et persistance en base de données.service
: Partie traitement applicatif.util
/ Classe utilitaire.feature1
,feature2
, ... : Partie spécifique à une zone fonctionnelle.
Note Afin de simplifier la structure du projet, l'application sera une archive de type WAR. Mais rien ne vous empêche d'utiliser le format EAR.
Couche de persistance
Entité
Pour réduire la quantité de ligne de code, les entités persistances seront manipulées dans toutes les couches de notre architecture logicielle. Pour cela nous utiliserons les annotations Jakarta EE pour :
- Réaliser la liaison avec la base données (avec JPA.
- Réaliser la conversion au format JSON (avec JSON-B).
- Porter les contraintes de validation des attributs (avec BeanValidation).
Afin de regrouper nos entités sous une même hiérarchie de classe, nous codons cette interface commune à toutes les entités:
package fr.zelmoacademy.dyna.common.persistence;
// Utilisation de la généricité pour typer la clef primaire.
public interface Identifiable<K> {
// Vérifie l'état de la clef primaire.
// Si la valeur n'est pas définie, elle sera alors générée.
// Sera utile plus tard pour différencier une création d'une mise à jour.
void checkId();
// Simple accesseur & mutateur de la clef primaire.
K getId();
void setId(K id);
}
Pour plus de facilité, voici un exemple de classe mère afin de factoriser du code :
package fr.zelmoacademy.dyna.common.persistence;
import ...
// JSON-B: Trier les attributs par ordre alphabétique
@JsonbPropertyOrder(PropertyOrderStrategy.LEXICOGRAPHICAL)
// JPA: Indique que les attributs de cette classe seront fusionnés dans la table SQL enfant.
@MappedSuperclass
// Implémenter 'Identifiable' avec une clef primaire de type 'UUID'.
public abstract class AbstractEntity implements Identifiable<UUID>, Serializable {
// Numéro de série pour respecter le mécanisme de sérialisation.
private static final long serialVersionUID = 1L;
// BeanValidation: Contrainte non nulle.
@NotNull
// JSON-B: Nom et contrainte de l'attribut pour le format JSON.
@JsonbProperty(value = "id", nillable = true)
// JPA: Clef primaire
@Id
// JPA: Nom et contrainte de la colonne en base de données.
// /!\ Ce type n'est pas géré nativement par JPA, on explicite le type SQL 'VARCHAR(36)'.
@Column(name = "id", nullable = false, unique = true, columnDefinition = "VARCHAR(36)")
protected UUID id;
// BeanValidation: Contrainte non nulle.
@NotNull
// JSON-B: Nom et contrainte de l'attribut pour le format JSON.
@JsonbProperty(value = "version", nillable = true)
// JPA: Numéro de version du tuple en base de données, s'incrémente à chaque modification.
@Version
// JPA: Nom et contrainte de la colonne en base de données.
@Column(name = "version", nullable = false)
protected Long version;
// Java Bean: Constructeur par défaut.
protected AbstractEntity() {
this.id = UUID.randomUUID();
this.version = 0L;
}
// Surchage des méthodes: 'equals', 'hashCode', 'toString'
// ...
@Override
public void checkId(){
// Dans notre cas, c'est l'application qui assure
// l'intégrité de la clef primaire.
if(Objects.isNull(id){
id = UUID.randomUUID();
}
if(Objects.isNull(version){
version = 0L;
}
}
// Accesseurs & Mutateurs
// ...
}
Exemple d'entité concrète :
package fr.zelmoacademy.dyna.customer;
import ...;
// CDI: Cette classe est injectable
@Dependent
// JSON-B: Trier les attributs par ordre alphabétique
@JsonbPropertyOrder(PropertyOrderStrategy.LEXICOGRAPHICAL)
// JPA: Entité persistante liée à la table 'customer' en base de données.
@Entity
@Table(name = "customer")
public class Customer extends AbstractEntity {
private static final long serialVersionUID = 1L;
// BeanValidation: Contrainte non vide.
@NotBlank
// JSON-B: Nom et contrainte de l'attribut pour le format JSON.
@JsonbProperty(value = "givenName", nillable = false)
// JPA: Nom et contrainte de la colonne en base de données.
@Column(name = "given_name", nullable = false)
private String givenName;
// BeanValidation: Contrainte non vide.
@NotBlank
// JSON-B: Nom et contrainte de l'attribut pour le format JSON.
@JsonbProperty(value = "familyName", nillable = false)
// JPA: Nom et contrainte de la colonne en base de données.
@Column(name = "family_name", nullable = false)
private String familyName;
// BeanValidation: Contrainte, format adresse de courriel et non vide.
@Email
@NotBlank
// JSON-B: Nom et contrainte de l'attribut pour le format JSON.
@JsonbProperty(value = "email", nillable = false)
// JPA: Nom et contrainte de la colonne en base de données.
@Column(name = "email", nullable = false, unique = true)
private String email;
public Customer() {
}
// Surchage des méthodes: 'equals', 'hashCode', 'toString'
// ...
// Accesseurs & Mutateurs
// ...
}
Convertisseur d'attribut
Le type java.util.UUID
n'est pas généré nativement par JPA.
Il est nécessaire de coder un convertisseur de type.
Dans notre code, on convertira le type java.util.UUID
en java.lang.String
.
package fr.zelmoacademy.dyna.common.persistence;
import ...;
// Applique le convertisseur automatiquent pour tous les attributs JPA de type 'java.util.UUID'.
@Converter(autoApply = true)
public class UUIDConverter implements AttributeConverter<UUID, String> {
public static final String COLUMN_DEFINITION = "VARCHAR(36)";
public UUIDConverter() {
}
// Convertir un attribut en colonne de base de données.
@Override
public String convertToDatabaseColumn(UUID attribute) {
String column;
if (Objects.nonNull(attribute)) {
column = attribute.toString();
} else {
column = null;
}
return column;
}
// Convertir une colonne de base de données en attribut.
@Override
public UUID convertToEntityAttribute(String column) {
UUID attribute;
if (Objects.nonNull(column)) {
attribute = UUID.fromString(column);
} else {
attribute = null;
}
return attribute;
}
}
Alignement CDI
La technologie CDI (Context Dependency Injection) est une technologie qui prend de l'ampleur dans Jakarta EE. À ce titre nous pouvons unifier les injections de dépendance à travers CDI.
package fr.zelmoacademy.dyna.common.persistence;
import ...;
// Cette classe est initialisée au démarrage de l'application.
@ApplicationScoped
public class PersistenceResource {
// Gestionnaire d'entité injectable avec CDI
@Produces
@PersistenceContext
private EntityManager em;
public PersistenceResource() {
}
}
Grâce à cette classe vous pouvez utiliser l'annotation @Inject
sur un attribut de type javax.persistence.EntityManager
.
Entrepôt
package fr.zelmoacademy.dyna.common.persistence;
import ...;
// Entrepôt de données générique & polymorphique
// Le pattern 'Repository/DAO' doit à l'usage avoir le même comportement qu'une 'Collection/List' en Java.
// Comme cette classe s'adapte à la plupart des entités persistantes, les types manipulés seront donc
// polymorphé avec 'Identifiable' afin d’assurer un minimum de typage.
// Cette classe est injectable avec CDI.
@Dependent
public class GenericDAO implements Serializable {
// Numéro de série pour respecter la sérialisation.
private static final long serialVersionUID = 1L;
// JPA: Gestionnaire d'entité
@Inject
private transient EntityManager em;
public GenericDAO() {
}
// Récupérer le nombre d'occurrence en base de données pour une entité donnée.
public Long size(final Class<? extends Identifiable<?>> entityClass) {
// SQL:
// SELECT count(t.id) FROM table t;
var builder = em.getCriteriaBuilder();
var query = builder.createQuery(Long.class);
var root = query.from(entityClass);
query.select(builder.count(root));
return em.createQuery(query).getSingleResult();
}
// Indique si la table associée à une entité est vide.
public boolean isEmpty(final Class<? extends Identifiable<?>> entityClass) {
return size(entityClass) == 0L;
}
// Vérifier d'une entité existe déjà en base de données.
public boolean contains(final Identifiable<?> entity) {
var entityClass = entity.getClass();
boolean exists;
if (em.contains(entity)) {
// Vérification de l’existence de l'entité dans le contexte de persistance.
// (plus rapide qu'une requête)
exists = true;
} else {
// Sinon
// Requêter en base de données.
// SQL:
// SELECT count(t.id) FROM table t WHERE t.id = ?;
var builder = em.getCriteriaBuilder();
var query = builder.createQuery(Long.class);
var root = query.from(entityClass);
query.select(builder.count(root));
var idTypeName = getIdentifierTypeName(entity);
var predicate = builder.equal(root.get(idTypeName), entity.getId());
query.where(predicate);
exists = em.createQuery(query).getSingleResult() == 1L;
}
return exists;
}
// Sauvegarder une entité
// Si elle existe déjà, la mettre à jour en base de données
// Sinon l'insérer en base de données.
public Identifiable<?> add(final Identifiable<?> entity) {
Identifiable<?> mangedEntity;
if (contains(entity)) {
// Mettre à jour
mangedEntity = em.merge(entity);
} else {
// Insérer en base de données.
em.persist(entity);
mangedEntity = entity;
}
return mangedEntity;
}
// Supprimer une entité
// SQL:
// DELETE FROM table t WHERE t.id = ?;
public void remove(final Identifiable<?> entity) {
var entityClass = entity.getClass();
var id = getIdentifier(entity);
var attachedEntity = em.getReference(entityClass, id);
em.remove(attachedEntity);
CDI.current().destroy(attachedEntity);
}
// Rechercher toutes les entités d'un type donné:
// /!\ Peut remonter beaucoup d'objet et saturer la mémoire !
// SELECT t.col1, t.col2, t.col3, ... FROM table t;
public List<? extends Identifiable<?>> get(final Class<? extends Identifiable<?>> entityClass) {
var builder = em.getCriteriaBuilder();
var query = builder.createQuery(entityClass);
return em
.createQuery(query)
.getResultList();
}
// Rechercher une entité d'un type donné en fonction de son identifiant.
public Optional<Identifiable<?>> get(final Class<? extends Identifiable<?>> entityClass, final Object id) {
// Lecture dans le cache de persistance.
var entity = em.find(entityClass, id);
return Optional.ofNullable(entity);
}
// Méthode interne:
// Récupérer dynamiquement la valeur de la clef primaire.
private <K> K getIdentifier(final Identifiable<K> entity) {
return (K) em
.getEntityManagerFactory()
.getPersistenceUnitUtil()
.getIdentifier(entity);
}
// Méthode interne:
// Récupérer dynamiquement le nom de l'attribut de la clef primaire.
private String getIdentifierTypeName(final Identifiable<?> entity) {
var entityClass = entity.getClass();
var id = getIdentifier(entity);
var idClass = id.getClass();
return em
.getMetamodel()
.entity(entityClass)
.getId(idClass)
.getName();
}
}
Couche de services
Le service métier ne possède pas de logique particulière. Son travail consiste principalement à de la délégation de méthode.
package fr.zelmoacademy.dyna.common.service;
import ...;
// Classe de service générique pour les opérations de CRUD
// Pas de logique particulière.
// Simple appel de la DAO.
// EJB: Indique que cette classe est sans état.
@Stateless
@LocalBean
public class CommonService implements Serializable {
// Numéro de série.
private static final long serialVersionUID = 1L;
// CDI: Injection de l'entrepôt de donnée générique.
@Inject
private GenericDAO dao;
// Java Bean: Constructeur par défaut.
public CommonService() {
}
// JTA:Java Transaction API
// Gestionnaire de transaction
// Par défaut le EJB gèrent automatiquement les transactions
// Mais dans de rare cas, il peut être nécessaire de définir ce comportement soi-même
// JTA: Indique que cette méthode requiert une nouvelle transaction.
// Exemple: @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void save(final Identifiable<?> entity) {
// Si la clef primaire n'est pas définie, alors elle est générée
// Permet de distinguer la création d'une mise à jour
entity.checkId();
dao.add(entity);
}
// JTA: Indique que cette méthode requiert une transaction.
// Exemple: @TransactionAttribute(TransactionAttributeType.REQUIRED)
public void remove(final Identifiable<?> entity) {
dao.remove(entity);
}
// JTA: Indique que cette méthode n'est pas besoin de transaction (lecture seule en base de données)
// Exemple: @TransactionAttribute(TransactionAttributeType.NEVER)
public Optional<? extends Identifiable<?>> findById(
final Class<? extends Identifiable<?>> entityClass,
final Object id) {
return dao.get(entityClass, id);
}
// JTA: Indique que cette méthode n'est pas besoin de transaction (lecture seule en base de données)
// Exemple: @TransactionAttribute(TransactionAttributeType.NEVER)
public List<? extends Identifiable<?>> find(final Class<? extends Identifiable<?>> entityClass) {
return dao.get(entityClass);
}
}
Variante CDI
Variante de cette classe mais avec la technologie CDI.
package fr.zelmoacademy.dyna.common.service;
import ...;
// Classe de service générique pour les opérations de CRUD
// Pas de logique particulière.
// Simple appel de la DAO.
// CDI: Instance charge au démarrage de l'application et dans un régime transactionnel
@ApplicationScoped
@Transactional
public class CommonService {
// CDI: Injection de l'entrepôt de donnée générique.
@Inject
private GenericDAO dao;
// Java Bean: Constructeur par défaut.
public CommonService() {
}
// JTA:Java Transaction API
// Gestionnaire de transaction
// Par défaut le CDI NE gèrent PAS automatiquement les transactions
// Il est nécessaire d'ajouter l'annotation @Transactional pour avoir un comportement
// similaire aux EJB.
// Dans de rare cas, il peut être nécessaire de définir ce comportement soi-même
// Pour cela, il suffit de porter l'annotation @Transactional sur les méthodes.
// Exiger une nouvelle transaction pour méthode.
// Même logique pour EJB, seule l'annotation diffère.
// Exemple: @Transactional(Transactional.TxType.REQUIRES_NEW)
public void save(final Identifiable<?> entity) { ... }
// Autres méthodes
// ...
}
Note
Effectivement, les EJB ne sont plus aussi indispensables qu'avant. Cette nouvelle écriture est valable depuis Java EE 7. Source: [[https://blogs.oracle.com/theaquarium/jta-12-its-not-your-grandfathers-transactions-anymore | Blog Oracle]]
Couche de contrôle
Entité dynamique
Nous allons mettre en œuvre une énumération qui va porter en lui toute la spécificité de notre CRUD. Il arrive un stade où on ne peut plus être générique, afin de simplifier le code aux maximums, cette partie spécifique va se limiter à une seule classe.
package fr.zelmoacademy.dyna;
import ...;
public enum DynamicEntity {
// Exemple
CUSTOMER("customer", Customer.class, UUID::fromString);
// Nom REST de l'entité cible pour le CRUD.
// Ce nom est celui qui apparait dans l'URL.
private final String name;
// Type Java de l'entité
private final Class<? extends Identifiable<?>> entityClass;
// Fonction générique de conversion de la clef primaire
// Depuis l'API, il arrive sous le type 'String'
// cette fonction permet de le convertir sous un autre type
// par exemple 'UUID' (voir exemple)
private final Function<String, Object> identifierConverter;
DynamicEntity(
final String name,
final Class<? extends Identifiable<?>> entityClass,
final Function<String, Object> identifierConverter) {
this.name = name;
this.entityClass = entityClass;
this.identifierConverter = identifierConverter;
}
// À partir du nom de l'entité
// Rechercher la valeur de l'enumération correspondante.
public static Optional<DynamicEntity> fromName(final String name) {
return Stream
.of(DynamicEntity.values())
.filter(e -> Objects.equals(e.name, name))
.findFirst();
}
// Convertir une clef primaire de type 'String'
// en autre type (polymorphé en 'Object')
public Object convertAsIdentifier(final String id) {
return identifierConverter.apply(id);
}
// Accesseurs
public String getName() {
return name;
}
public Class<? extends Identifiable<?>> getEntityClass() {
return entityClass;
}
}
Note
À présent, lorsque le développeur souhaite ajouter une entité dans le CRUD, il suffira d'ajouter une entrée dans cette énumération. Exemple:
CUSTOMER("customer", Customer.class, UUID::fromString)
.
Convertisseur dynamique
Depuis notre API pour être générique, nous recevons les données sous forme de texte, sous java.lang.String
en Java.
Mais pour que l'application fonctionne correctement, nous avons besoin de convertir ces données dans les bons types.
Bien entendu, les types réels seront polymorphé en type abstrait pour conserver la généricité.
package fr.zelmoacademy.dyna.common.controller;
import ...;
public final class DynamicEntityMapper {
// Constructeur interne.
// Pas d'instanciation.
private DynamicEntityMapper() {
throw new UnsupportedOperationException("Instance not allowed");
}
// Convertir un 'String JSON' en entité (polymorphé en 'Identifiable')
// Si le type n'existe pas, une exception sera levée.
public static Identifiable<?> mapToEntity(final String type, final String jsonEntity) {
return DynamicEntity
.fromName(type)
.map(DynamicEntity::getEntityClass)
.map(e -> JsonbBuilder.create().fromJson(jsonEntity, e))
.orElseThrow(() -> new WebApplicationException("Invalid entity"));
}
// Convertir le nom du type en la classe Java de l'entité.
// Si le type n'existe pas, une exception sera levée.
public static Class<? extends Identifiable<?>> mapToEntityClass(final String type) {
return DynamicEntity
.fromName(type)
.map(DynamicEntity::getEntityClass)
.orElseThrow(() -> new WebApplicationException("Invalid entity"));
}
// Convertir un 'String' de la clef primaire en un autre type
// en fonction du nom du type d'entité.
// Si le type n'existe pas, une exception sera levée.
public static Object mapToEntityId(final String type, final String id) {
return DynamicEntity
.fromName(type)
.map(e -> e.convertAsIdentifier(id))
.orElseThrow(() -> new WebApplicationException("Invalid entity"));
}
}
Contrôleur
Pour utiliser les contrôleurs REST en Jakarta EE, une simple classe de configuration est nécessaire:
package fr.zelmoacademy.dyna.common.controller;
import ...;
// JAXRS: Chemin racine de toute l'API.
@ApplicationPath("api")
public class RESTConfiguration extends Application {
public RESTConfiguration() {
}
// RAS
// Tout se configure avec les annotations.
}
Enfin, voici le contrôleur unique pour toutes les opérations de CRUD.
package fr.zelmoacademy.dyna.common.controller;
import ...;
// Cette classe est injectable
// La durée de vie de l'instance est celui d'une requête HTTP.
@RequestScoped
// Chemin de base du contrôleur
@Path("v1/entity")
// Indique que le contrôleur produit et consomme du JSON.
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class CommonController {
// Injection du service commun pour toutes les opérations de CRUD.
@Inject
private CommonService service;
public CommonController() {
}
// CREATE
// 1er paramètre: L'élément de l'URL dynamique en fonction du nom de l'entité cible.
// 2e paramètre: Corps de la requête, contient le JSON de l'entité à créer.
@POST
@Path("{type}/create")
public Response create(
@PathParam("type") final String type,
final String jsonEntity) {
var entity = DynamicEntityMapper.mapToEntity(type, jsonEntity);
service.save(entity);
return Response
.status(Response.Status.CREATED)
.build();
}
// UPDATE
// 1er paramètre: L'élément de l'URL dynamique en fonction du nom de l'entité cible.
// 2e paramètre: Corps de la requête, contient le JSON de l'entité à mettre à jour.
@PUT
@Path("{type}/update")
public Response update(
@PathParam("type") final String type,
final String jsonEntity) {
var entity = DynamicEntityMapper.mapToEntity(type, jsonEntity);
service.save(entity);
return Response
.status(Response.Status.NO_CONTENT)
.build();
}
// DELETE
// 1er paramètre: L'élément de l'URL dynamique en fonction du nom de l'entité cible.
// 2e paramètre: Corps de la requête, contient le JSON de l'entité à supprimer.
@DELETE
@Path("{type}/remove")
public Response remove(
@PathParam("type") final String type,
final String jsonEntity) {
var entity = DynamicEntityMapper.mapToEntity(type, jsonEntity);
service.remove(entity);
return Response
.status(Response.Status.NO_CONTENT)
.build();
}
// READ ALL
// 1er paramètre: L'élément de l'URL dynamique en fonction du nom de l'entité cible.
@GET
@Path("{type}/read")
public Response read(@PathParam("type") final String type) {
var entityClass = DynamicEntityMapper.mapToEntityClass(type);
var entities = service.find(entityClass);
return Response
.status(Response.Status.OK)
.entity(entities)
.build();
}
// READ
// 1er paramètre: L'élément de l'URL dynamique en fonction du nom de l'entité cible.
// 2e paramètre: Clef primaire de l'entité cible.
@GET
@Path("{type}/read/{id}")
public Response read(
@PathParam("type") final String type,
@PathParam("id") final String id) {
var entityClass = DynamicEntityMapper.mapToEntityClass(type);
var identifier = DynamicEntityMapper.mapToEntityId(type, id);
return service
.findById(entityClass, identifier)
.map(e -> Response.ok(e).build())
.orElseGet(() -> Response.status(Response.Status.NOT_FOUND).build());
}
}
Gestion des exceptions
Pour terminer ce projet, voici une manière de gérer les exceptions avec JAXRS.
package fr.zelmoacademy.dyna.common.controller;
import ...;
// JAXRS: Cette classe est appelée automatiquement lorsqu'une exception de type 'RuntimeException' survient.
@Provider
public class GenericExceptionMapper implements ExceptionMapper<RuntimeException> {
public GenericExceptionMapper() {
}
@Override
public Response toResponse(final RuntimeException exception) {
//TODO: Faire de la journalisation ici...
// Pour respecter le format d'échange JSON de l'API REST.
// en cas d'erreur on retourne un objet JSON.
var message = Json
.createObjectBuilder()
.add("error", exception.getClass().getCanonicalName())
.add("message", exception.getMessage())
.add("date", LocalDateTime.now())
.build();
return Response
.status(Response.Status.BAD_REQUEST)
.type(MediaType.APPLICATION_JSON)
.entity(message)
.build();
}
}
Note
Ceci n'est qu'un exemple générique. Dans la réalité, il conviendra de capturer les erreurs avec de type bien définit au lieu de
java.lang.RuntimeException
.
Usage
Voici quelques commandes pour interroger l'application:
Création :
curl -X POST \
http://localhost:8080/dyna/api/v1/entity/customer/create \
-H 'Content-Type: application/json' \
-d '{"givenName": "John", "familyName": "DOE", "email": "john.doe @sample.org"}'
**Lecture :**
<code bash>
curl -X GET \
http://localhost:8080/dyna/api/v1/entity/customer/read\
-H 'Content-Type: application/json' \
>>>
'[{
"email": "john.doe @sample.org",
"familyName": "DOE",
"givenName": "John",
"id": "cc32378c-784c-49be-926a-930c1d37f6cd",
"version": "0"
}]'
Mise à jour :
curl -X PUT \
http://localhost:8080/dyna/api/v1/entity/customer/update\
-H 'Content-Type: application/json' \
-d '{
"givenName": "Johnny",
"familyName": "TEST",
"email": "johnny.test@sample.org",
"id": "cc32378c-784c-49be-926a-930c1d37f6cd",
"version": "0"
}'
//Attention commande multi ligne.//
**Suppression :**
<code bash>
curl -X DELETE \
http://localhost:8080/dyna/api/v1/entity/customer/remove/cc32378c-784c-49be-926a-930c1d37f6cd \
-H 'Content-Type: application/json' \
Exemple de message d'erreur (Json malformé) :
{
"error":"javax.json.stream.JsonParsingException",
"message":"Internal error: Unexpected char -1 at (line no=1, column no=240, offset=239)"
}
Pas terrible en effet...
Axe d'amélioration
- Ajouter une meilleure gestion des exceptions, car dans l'exemple, le message laisse trahir le fonctionnement interne de l'application.
- Ajouter un mécanisme de sécurisation de l'API.
- Ajouter un mécanisme pour prendre en compte des règles métiers spécifiques.
- Faire en sorte que les services consomment des DTO à la place d'un JSON au format String (cohérence d'architecture).
- Ajouter un filtre dynamique pour avoir une granularité plus fine lors des requêtes vers l'API (jusqu'au niveau d'un attribut). => Principe de juste suffisance.
Conclusion
Voici un exemple minimaliste de CRUD en Jakarta EE 8. Pour ajouter de nouvelle entité à votre CRUD compléter simplement l'énumération DynamicEntity.