Bonnes pratiques Helidon
Votre objectif est de m'aider à écrire des applications Helidon de haute qualité en suivant les bonnes pratiques établies.
Changements d'API Helidon 3 → 4
Helidon 4 a renommé ou resignaturé des API qui apparaissent largement sous leur forme Helidon 3. La colonne de gauche ne compile pas sur Helidon 4. Vérifiez le code généré par rapport à ce tableau avant de le retourner.
| À ne pas utiliser (Helidon 3) | À utiliser (Helidon 4) |
|---|---|
io.helidon.common.http.Http.Status |
io.helidon.http.Status |
io.helidon.webserver.Service |
io.helidon.webserver.http.HttpService |
Routing.Rules, update(Routing.Rules) |
HttpRules, routing(HttpRules) |
request.path().param("id") |
request.path().pathParameters().get("id") |
String s = column.as(String.class) |
column.getString() ou column.get(String.class) |
dbClient.execute(exec -> ...) renvoyant Single/Multi |
dbClient.execute() renvoyant Optional<DbRow> / Stream<DbRow> |
javax.* |
jakarta.* |
helidon-microprofile-tests-junit5 |
helidon-microprofile-testing-junit5 |
Value.as(Class) dans Helidon 4 retourne OptionalValue<T>, pas T. C'est l'erreur de compilation Helidon 4
la plus courante dans le code généré.
Configuration du projet et structure
- Modèle de programmation : Déterminez si le projet utilise Helidon SE ou Helidon MP avant de générer du code. Ne mélangez pas les deux modèles de programmation sauf si explicitement requis.
- Version Java : Utilisez Java 21 ou une version ultérieure pour les applications Helidon 4.
- Outil de compilation : Utilisez Maven (
pom.xml) ou Gradle (build.gradle) pour la gestion des dépendances. - Gestion des dépendances : Utilisez le BOM Helidon ou la plateforme pour maintenir les versions des modules Helidon alignées.
- Structure des packages : Organisez le code par fonctionnalité ou domaine, comme
com.example.app.orderetcom.example.app.customer, plutôt que uniquement par couche technique.
Helidon SE
- Composition explicite : Construisez les services et les dépendances explicitement dans la couche d'amorçage de l'application.
- Injection par constructeur : Transmettez les dépendances requises via les constructeurs et déclarez les champs de dépendance comme
private final. - Services HTTP : Groupez les routes associées dans des implémentations
HttpServiceciblées. - Logique métier : Gardez la logique métier en dehors des gestionnaires de routes.
- Threads virtuels : Préférez le code bloquant direct avec la gestion des requêtes basée sur les threads virtuels de Helidon 4. N'introduisez pas de complexité réactive sans une raison claire. Helidon 4 n'est pas réactif ; ne générez pas de chaînes
Single,MultiouCompletionStage.
Helidon MP
- Jakarta et MicroProfile : Préférez les API standard Jakarta EE et Eclipse MicroProfile si disponibles.
- Injection de dépendances : Utilisez CDI avec l'injection par constructeur pour les dépendances requises.
- Portées de bean : Utilisez intentionnellement les portées CDI comme
@ApplicationScopedet@RequestScoped. - Beans à portée normale : Ajoutez un constructeur sans arguments non-privé aux beans à portée normale qui utilisent l'injection par constructeur, afin que le proxy client CDI puisse être créé de façon portable.
- Logique métier : Gardez les classes de ressources Jakarta REST légères et déléguez les opérations métier à des classes de service.
- Portabilité : Préférez les API Jakarta et MicroProfile portables aux API spécifiques à Helidon quand la portabilité est importante.
Configuration
- Configuration externalisée : Stockez la configuration non secrète dans
application.yamlouapplication.properties. - Configuration Helidon SE : Utilisez Helidon Config et transmettez les valeurs de configuration ou les objets de configuration typés aux composants.
- Configuration Helidon MP : Utilisez MicroProfile Config pour les paramètres d'application injectés.
- Remplacements d'environnement : Utilisez les variables d'environnement ou les sources de configuration spécifiques au déploiement pour les valeurs dépendantes de l'environnement.
- Gestion des secrets : Ne codez jamais en dur les identifiants, les clés API, les tokens ou les certificats privés.
Couche web
- DTOs : Utilisez des modèles de requête et de réponse dédiés. N'exposez pas les entités de persistance directement via les API.
- Validation : Validez les paramètres de chemin, les paramètres de requête, les en-têtes et les corps de requête avant d'invoquer la logique métier.
- Codes de statut : Retournez les codes de statut HTTP appropriés pour les requêtes réussies, invalides, non autorisées, interdites, manquantes et échouées. Sur
PUTetDELETE, retournez 404 quand la cible n'existe pas plutôt que de réussir inconditionnellement. - Gestion des erreurs : Utilisez la gestion centralisée des erreurs dans Helidon SE et les implémentations
ExceptionMapperde Jakarta REST dans Helidon MP. - Informations sensibles : N'exposez pas les traces de pile, les détails de base de données, les chemins de système de fichiers ou les messages d'exception internes aux clients.
Exemple Helidon SE
Utilisez un HttpService pour enregistrer les routes par programmation. Gardez les gestionnaires de requêtes petits et déléguez la logique métier à un service.
import io.helidon.http.Status;
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.http.HttpService;
import io.helidon.webserver.http.ServerRequest;
import io.helidon.webserver.http.ServerResponse;
public final class CustomerHttpService implements HttpService {
private final CustomerService customerService;
public CustomerHttpService(CustomerService customerService) {
this.customerService = customerService;
}
@Override
public void routing(HttpRules rules) {
rules.get("/{id}", this::findById);
}
private void findById(ServerRequest request, ServerResponse response) {
var id = request.path().pathParameters().get("id");
customerService.findById(id)
.ifPresentOrElse(
response::send,
() -> response.status(Status.NOT_FOUND_404).send()
);
}
}
Enregistrez le service HTTP lors de la construction du serveur :
import io.helidon.webserver.WebServer;
WebServer server = WebServer.builder()
.routing(routing -> routing.register("/customers", customerHttpService))
.build()
.start();
Exemple Helidon MP
Utilisez les annotations Jakarta REST pour les endpoints et CDI pour l'injection de dépendances.
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path("/customers")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON)
public class CustomerResource {
private final CustomerService customerService;
protected CustomerResource() {
this.customerService = null;
}
@Inject
public CustomerResource(CustomerService customerService) {
this.customerService = customerService;
}
@GET
@Path("/{id}")
public Response findById(@PathParam("id") String id) {
return customerService.findById(id)
.map(customer -> Response.ok(customer).build())
.orElseGet(() -> Response.status(Response.Status.NOT_FOUND).build());
}
}
Couche service
- Transactions : Définissez les limites de transaction autour des opérations métier complètes.
- Mapping d'entités : Mappez les entités de persistance aux modèles API à la limite du service afin que les signatures de méthode de service n'exposent que des modèles API. Un service retournant
Optional<CustomerEntity>alors que l'appelant attendOptional<Customer>est une erreur de compilation du code généré courante. - Concurrence : Évitez l'état partagé mutable dans les composants à portée application sauf si l'accès est correctement coordonné.
Exemple Helidon SE
Les services Helidon SE sont normalement des classes Java simples avec des dépendances explicitement fournies.
public final class CustomerService {
private final CustomerRepository customerRepository;
public CustomerService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public Optional<Customer> findById(String id) {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("Customer ID is required");
}
return customerRepository.findById(id);
}
public Customer create(CreateCustomerRequest request) {
if (request.name() == null || request.name().isBlank()) {
throw new IllegalArgumentException("Customer name is required");
}
var customer = new Customer(request.id(), request.name().trim());
customerRepository.save(customer);
return customer;
}
}
Construisez le graphe de dépendances explicitement :
var repository = new DbCustomerRepository(dbClient);
var service = new CustomerService(repository);
var httpService = new CustomerHttpService(service);
Exemple Helidon MP
Utilisez les portées CDI et l'injection par constructeur. Appliquez les transactions à la couche service quand une opération métier change l'état persistant. Mappez l'entité de persistance au modèle API ici, afin que le service ne fuie jamais CustomerEntity aux appelants.
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
@ApplicationScoped
public class CustomerService {
private final JpaCustomerRepository customerRepository;
protected CustomerService() {
this.customerRepository = null;
}
@Inject
public CustomerService(JpaCustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public Optional<Customer> findById(String id) {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("Customer ID is required");
}
return customerRepository.findById(id)
.map(Customer::fromEntity);
}
@Transactional
public Customer create(CreateCustomerRequest request) {
if (request.name() == null || request.name().isBlank()) {
throw new IllegalArgumentException("Customer name is required");
}
var entity = new CustomerEntity(request.id(), request.name().trim());
customerRepository.save(entity);
return Customer.fromEntity(entity);
}
}
Couche données
- Accès à la base de données : Utilisez Helidon DB Client, Jakarta Persistence ou un autre mécanisme de persistance déjà établi par le projet.
- Requêtes paramétrées : Utilisez toujours le binding de paramètres ou les instructions préparées. Ne concaténez jamais d'entrée non fiable dans SQL.
- Accesseurs de colonne : Lisez une valeur de colonne typée avec
column("name").getString()(ougetInt(),getLong(), etc.) ou aveccolumn("name").get(String.class).DbColumn.as(String.class)retourne uneOptionalValue<String>dans Helidon 4, pas uneString. - Colonnes nullables : Lisez les colonnes nullables via
asOptional()ou un autre accesseur conscient des optionnels. Les accesseurs directs commegetString()lèvent une exception quand la valeur de colonne est null. - Mapping de lignes : Pour le mapping de lignes complètes,
DbRow.as(Customer.class)retourne l'instance mappée directement, mais nécessite unDbMapperenregistré via une entrée de service-loaderDbMapperProvider. Préférez les lectures de colonne explicites pour un petit nombre de référentiels simples, et introduisez unDbMapperquand la même forme de ligne est mappée en plusieurs endroits. - Migrations : Utilisez un outil de migration de base de données pour les changements de schéma plutôt que les mises à jour de schéma destructives automatiques.
- Séparation d'entités : N'exposez pas les entités de base de données directement comme contrats API.
Exemple Helidon SE
Utilisez Helidon DB Client avec des instructions paramétrées. Mappez les lignes de base de données dans les modèles d'application à l'intérieur du référentiel.
import io.helidon.dbclient.DbClient;
public final class DbCustomerRepository implements CustomerRepository {
private static final String FIND_BY_ID =
"SELECT id, name FROM customers WHERE id = :id";
private static final String INSERT =
"INSERT INTO customers (id, name) VALUES (:id, :name)";
private final DbClient dbClient;
public DbCustomerRepository(DbClient dbClient) {
this.dbClient = dbClient;
}
@Override
public Optional<Customer> findById(String id) {
return dbClient.execute()
.createGet(FIND_BY_ID)
.addParam("id", id)
.execute()
.map(row -> new Customer(
row.column("id").getString(),
row.column("name").getString()
));
}
@Override
public void save(Customer customer) {
dbClient.execute()
.createInsert(INSERT)
.addParam("id", customer.id())
.addParam("name", customer.name())
.execute();
}
}
Les instructions nommées peuvent aussi être stockées dans la configuration au lieu d'embarquer le SQL dans Java :
db:
source: "jdbc"
connection:
url: "jdbc:postgresql://localhost:5432/customers"
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
statements:
find-customer-by-id: >
SELECT id, name
FROM customers
WHERE id = :id
Référencez l'instruction nommée par nom au lieu de passer le texte SQL :
return dbClient.execute()
.createNamedGet("find-customer-by-id")
.addParam("id", id)
.execute()
.map(row -> new Customer(
row.column("id").getString(),
row.column("name").getString()
));
Exemple Helidon MP
Utilisez Jakarta Persistence dans un référentiel géré par CDI. Gardez les limites de transaction à la couche service. Le référentiel fonctionne avec des entités ; le service les mappe aux modèles API.
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@ApplicationScoped
public class JpaCustomerRepository {
@PersistenceContext
private EntityManager entityManager;
public Optional<CustomerEntity> findById(String id) {
return Optional.ofNullable(entityManager.find(CustomerEntity.class, id));
}
public void save(CustomerEntity customer) {
entityManager.persist(customer);
}
}
Définissez l'entité de persistance séparément du modèle API public :
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "customers")
public class CustomerEntity {
@Id
private String id;
@Column(nullable = false)
private String name;
protected CustomerEntity() {
}
public CustomerEntity(String id, String name) {
this.id = id;
this.name = name;
}
public String id() {
return id;
}
public String name() {
return name;
}
}
Le modèle API reste libre d'annotations de persistance et possède le mapping :
public record Customer(String id, String name) {
public static Customer fromEntity(CustomerEntity entity) {
return new Customer(entity.id(), entity.name());
}
}
Observabilité
- Santé : Utilisez Helidon Health dans SE ou MicroProfile Health dans MP pour les vérifications de vivacité et de disponibilité.
- Métriques : Utilisez Helidon Metrics ou MicroProfile Metrics pour les mesures opérationnelles et métier.
- Traçage : Propagez le contexte de traçage sur les appels de service entrants et sortants.
- Cardinalité : Évitez les ID utilisateur, les ID de requête, les adresses email et les URL brutes comme étiquettes de métrique.
Journalisation
- API de journalisation : Utilisez l'API de journalisation et l'implémentation configurée par le projet.
- Informations sensibles : Ne journalisez jamais les mots de passe, les tokens d'accès, les en-têtes d'autorisation, les cookies ou les corps de requête sensibles complets. Ne placez pas non plus les secrets ou les informations personnelles dans les métriques ou les attributs de traçage.
Tests
- Tests unitaires : Écrivez des tests unitaires pour les services métier en utilisant JUnit 5.
- Tests Helidon SE : Utilisez
helidon-webserver-testing-junit5avec@ServerTestpour les tests de serveur complets et@RoutingTestpour les tests de routage uniquement. Ceux-ci démarrent le serveur sur un port sélectionné dynamiquement et injectent unHttp1Clientlié à celui-ci. Ne codez jamais un port en dur. - Tests Helidon MP : Utilisez
helidon-microprofile-testing-junit5avec@HelidonTest, qui démarre le conteneur CDI et le serveur pour la classe de test. Confirmez les coordonnées de l'artefact par rapport à la version Helidon en utilisation, car ce module a été renommé entre les versions 4.x. - Testcontainers : Considérez Testcontainers pour les tests d'intégration utilisant de vraies bases de données, des brokers de messages ou d'autres infrastructures.
- Chemins d'échec : Testez les défaillances de validation, les ressources manquantes, les défaillances de service externe et les défaillances d'autorisation.
Sécurité
- Sécurité Helidon : Utilisez Helidon Security ou les API de sécurité Jakarta et MicroProfile supportées pour l'authentification et l'autorisation.
- Autorisation : Appliquez les permissions à une limite d'application claire et refusez les opérations protégées par défaut.
- JWT et OIDC : Validez les signatures de token, les émetteurs, les audiences et les temps d'expiration.
- TLS : Utilisez TLS pour le trafic en production et vérifiez les certificats pour les connexions sortantes.
- CORS : Configurez explicitement les origines autorisées. Ne combinez pas les origines avec joker et les credentials.
- Secrets : Stockez les secrets dans la configuration d'environnement protégée ou un système de gestion des secrets dédié.
- Requêtes sortantes : Validez les destinations sortantes pour réduire les risques de falsification de requête côté serveur.