Normes de Codage Java
Normes pour un code Java (17+) lisible et maintenable dans les services Spring Boot et Quarkus.
Quand l'utiliser
- Écrire ou relire du code Java dans des projets Spring Boot ou Quarkus
- Appliquer des conventions de nommage, d'immutabilité ou de gestion des exceptions
- Travailler avec des records, des classes sealed ou du pattern matching (Java 17+)
- Réviser l'utilisation d'Optional, des streams ou des génériques
- Structurer les packages et la disposition du projet
- [QUARKUS] : Travailler avec les portées CDI, les entités Panache ou les pipelines réactifs
Comment ça fonctionne
Détection du Framework
Avant d'appliquer les normes, déterminer le framework à partir du fichier de construction :
- Le fichier de construction contient
quarkus→ appliquer les conventions [QUARKUS] - Le fichier de construction contient
spring-boot→ appliquer les conventions [SPRING] - Aucun détecté → appliquer uniquement les conventions partagées
Principes Fondamentaux
- Privilégier la clarté à l'ingéniosité
- Immutable par défaut ; minimiser l'état mutable partagé
- Échouer rapidement avec des exceptions significatives
- Nommage et structure de package cohérents
- [QUARKUS] : Favoriser le traitement à la construction plutôt qu'à l'exécution ; éviter la réflexion à l'exécution lorsque possible
Exemples
Les sections ci-dessous montrent des exemples concrets pour Spring Boot, Quarkus, et des exemples Java partagés pour le nommage, l'immutabilité, l'injection de dépendances, le code réactif, les exceptions, la disposition du projet, la journalisation, la configuration et les tests.
Nommage
// PASS : Classes/Records : PascalCase
public class MarketService {}
public record Money(BigDecimal amount, Currency currency) {}
// PASS : Méthodes/champs : camelCase
private final MarketRepository marketRepository;
public Market findBySlug(String slug) {}
// PASS : Constantes : UPPER_SNAKE_CASE
private static final int MAX_PAGE_SIZE = 100;
// PASS : [QUARKUS] Ressources JAX-RS nommées *Resource, pas *Controller
public class MarketResource {}
// PASS : [SPRING] Contrôleurs REST nommés *Controller
public class MarketController {}
Immutabilité
// PASS : Privilégier les records et les champs finaux
public record MarketDto(Long id, String name, MarketStatus status) {}
public class Market {
private final Long id;
private final String name;
// uniquement des getters, pas de setters
}
// PASS : [QUARKUS] Les entités Panache active-record utilisent des champs publics (convention Quarkus)
@Entity
public class Market extends PanacheEntity {
public String name;
public MarketStatus status;
// Panache génère les accesseurs à la construction ; les champs publics sont idiomatiques ici
}
// PASS : [QUARKUS] Entités MongoDB Panache
@MongoEntity(collection = "markets")
public class Market extends PanacheMongoEntity {
public String name;
public MarketStatus status;
}
Utilisation d'Optional
// PASS : Retourner Optional depuis les méthodes find*
// [SPRING]
Optional<Market> market = marketRepository.findBySlug(slug);
// [QUARKUS] Panache
Optional<Market> market = Market.find("slug", slug).firstResultOptional();
// PASS : Utiliser map/flatMap au lieu de get()
return market
.map(MarketResponse::from)
.orElseThrow(() -> new EntityNotFoundException("Marché non trouvé"));
Bonnes pratiques avec les Streams
// PASS : Utiliser les streams pour les transformations, garder les pipelines courts
List<String> names = markets.stream()
.map(Market::name)
.filter(Objects::nonNull)
.toList();
// FAIL : Éviter les streams imbriqués complexes ; préférer les boucles pour la clarté
Injection de Dépendances
// PASS : [SPRING] Injection par constructeur (préférée à @Autowired sur les champs)
@Service
public class MarketService {
private final MarketRepository marketRepository;
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// PASS : [QUARKUS] Injection par constructeur
@ApplicationScoped
public class MarketService {
private final MarketRepository marketRepository;
@Inject
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// PASS : [QUARKUS] Injection de champ package-private (acceptable dans Quarkus — évite les problèmes de proxy)
@ApplicationScoped
public class MarketService {
@Inject
MarketRepository marketRepository;
}
// FAIL : [SPRING] Injection de champ avec @Autowired
@Autowired
private MarketRepository marketRepository; // utiliser l'injection par constructeur
// FAIL : [QUARKUS] @Singleton quand l'interception ou l'initialisation différée est nécessaire
@Singleton // non-proxyable — utiliser @ApplicationScoped à la place
public class MarketService {}
Patterns Réactifs [QUARKUS]
// PASS : Retourner Uni/Multi depuis les endpoints réactifs
@GET
@Path("/{slug}")
public Uni<Market> findBySlug(@PathParam("slug") String slug) {
return Market.find("slug", slug)
.<Market>firstResult()
.onItem().ifNull().failWith(() -> new MarketNotFoundException(slug));
}
// PASS : Composition de pipeline non bloquante
public Uni<OrderConfirmation> placeOrder(OrderRequest req) {
return validateOrder(req)
.chain(valid -> persistOrder(valid))
.chain(order -> notifyFulfillment(order));
}
// FAIL : Appel bloquant dans un pipeline Uni/Multi
public Uni<Market> find(String slug) {
Market m = Market.find("slug", slug).firstResult(); // BLOQUANT — casse la boucle d'événements
return Uni.createFrom().item(m);
}
// FAIL : S'abonner plus d'une fois à un Uni partagé
Uni<Market> shared = fetchMarket(slug);
shared.subscribe().with(m -> log(m));
shared.subscribe().with(m -> cache(m)); // double abonnement — utiliser Uni.memoize()
Exceptions
- Utiliser des exceptions non vérifiées pour les erreurs métier ; envelopper les exceptions techniques avec du contexte
- Créer des exceptions spécifiques au domaine (par exemple,
MarketNotFoundException) - Éviter les
catch (Exception ex)larges sauf pour relancer ou journaliser centralement
throw new MarketNotFoundException(slug);
Gestion Centralisée des Exceptions
// [SPRING]
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MarketNotFoundException.class)
public ResponseEntity<ErrorResponse> handle(MarketNotFoundException ex) {
return ResponseEntity.status(404).body(ErrorResponse.from(ex));
}
}
// [QUARKUS] Option A : ExceptionMapper
@Provider
public class MarketNotFoundMapper implements ExceptionMapper<MarketNotFoundException> {
@Override
public Response toResponse(MarketNotFoundException ex) {
return Response.status(404).entity(ErrorResponse.from(ex)).build();
}
}
// [QUARKUS] Option B : @ServerExceptionMapper (RESTEasy Reactive)
@ServerExceptionMapper
public RestResponse<ErrorResponse> handle(MarketNotFoundException ex) {
return RestResponse.status(Status.NOT_FOUND, ErrorResponse.from(ex));
}
Génériques et Sécurité des Types
- Éviter les types bruts ; déclarer des paramètres génériques
- Privilégier les génériques bornés pour les utilitaires réutilisables
public <T extends Identifiable> Map<Long, T> indexById(Collection<T> items) { ... }
Structure du Projet
[SPRING] Maven/Gradle
src/main/java/com/example/app/
config/
controller/
service/
repository/
domain/
dto/
util/
src/main/resources/
application.yml
src/test/java/... (miroir de main)
[QUARKUS] Maven/Gradle
src/main/java/com/example/app/
config/ # @ConfigMapping, beans @ConfigProperty, Producers
resource/ # Ressources JAX-RS (pas "controller")
service/
repository/ # Implémentations PanacheRepository (si on n'utilise pas active record)
domain/ # Entités JPA/Panache, entités MongoDB
dto/
util/
mapper/ # Mappers MapStruct (si utilisés)
src/main/resources/
application.properties # Convention Quarkus (YAML supporté avec quarkus-config-yaml)
import.sql # Import automatique Hibernate pour dev/test
src/test/java/... (miroir de main)
Formatage et Style
- Utiliser 2 ou 4 espaces de manière cohérente (standard du projet)
- Un seul type public de premier niveau par fichier
- Garder les méthodes courtes et ciblées ; extraire des méthodes d'aide
- Ordre des membres : constantes, champs, constructeurs, méthodes publiques, protégées, privées
Anti-patrons à Éviter
- Listes de paramètres longues → utiliser des DTO/builders
- Imbrication profonde → retours précoces
- Nombres magiques → constantes nommées
- État mutable statique → préférer l'injection de dépendances
- Blocs catch silencieux → journaliser et agir ou relancer
- [QUARKUS] :
@Singletonlà où@ApplicationScopedest prévu — casse le proxying et l'interception - [QUARKUS] : Mélanger
quarkus-resteasy-reactiveetquarkus-resteasy(classique) — choisir une pile - [QUARKUS] : Panache active-record + pattern repository dans le même contexte délimité — en choisir un
Journalisation
// [SPRING] SLF4J
private static final Logger log = LoggerFactory.getLogger(MarketService.class);
log.info("fetch_market slug={}", slug);
log.error("failed_fetch_market slug={}", slug, ex);
// [QUARKUS] JBoss Logging (par défaut, coût nul à la construction)
private static final Logger log = Logger.getLogger(MarketService.class);
log.infof("fetch_market slug=%s", slug);
log.errorf(ex, "failed_fetch_market slug=%s", slug);
// [QUARKUS] Alternative : journalisation simplifiée avec @Inject
@Inject
Logger log; // injecté par CDI, délimité par la classe déclarante
Gestion des Nulls
- Accepter
@Nullableuniquement lorsque c'est inévitable ; sinon utiliser@NonNull - Utiliser Bean Validation (
@NotNull,@NotBlank) sur les entrées - [QUARKUS] : Appliquer
@Validsur@BeanParam,@RestForm, et les paramètres du corps de la requête
Configuration
// [SPRING] @ConfigurationProperties
@ConfigurationProperties(prefix = "market")
public record MarketProperties(int maxPageSize, Duration cacheTtl) {}
// [QUARKUS] @ConfigMapping (type-safe, validé à la construction)
@ConfigMapping(prefix = "market")
public interface MarketConfig {
int maxPageSize();
Duration cacheTtl();
}
// [QUARKUS] Valeurs simples avec @ConfigProperty
@ConfigProperty(name = "market.max-page-size", defaultValue = "100")
int maxPageSize;
Attentes pour les Tests
Partagé
- JUnit 5 + AssertJ pour des assertions fluides
- Mockito pour le mocking ; éviter les mocks partiels lorsque possible
- Privilégier les tests déterministes ; pas de sleep cachés
[SPRING]
@WebMvcTestpour les tests de contrôleurs,@DataJpaTestpour les tests de repositories@SpringBootTestréservé aux tests d'intégration complets@MockBeanpour remplacer des beans dans le contexte Spring
[QUARKUS]
- JUnit 5 + Mockito pour les tests unitaires (pas de
@QuarkusTest) @QuarkusTestréservé aux tests d'intégration CDI@InjectMockpour remplacer les beans CDI dans les tests d'intégration- Dev Services pour la base de données/Kafka/Redis — éviter la configuration manuelle de Testcontainers quand Dev Services suffit
@QuarkusTestResourcepour le cycle de vie personnalisé des services externes
// [SPRING] Test de contrôleur
@WebMvcTest(MarketController.class)
class MarketControllerTest {
@Autowired MockMvc mockMvc;
@MockBean MarketService marketService;
}
// [QUARKUS] Test d'intégration
@QuarkusTest
class MarketResourceTest {
@InjectMock
MarketService marketService;
@Test
void should_return_404_when_market_not_found() {
given().when().get("/markets/unknown").then().statusCode(404);
}
}
// [QUARKUS] Test unitaire (pas de CDI, pas de @QuarkusTest)
@ExtendWith(MockitoExtension.class)
class MarketServiceTest {
@Mock MarketRepository marketRepository;
@InjectMocks MarketService marketService;
}
Rappelez-vous : Gardez le code intentionnel, typé et observable. Optimisez pour la maintenabilité plutôt que pour les micro-optimisations, sauf si elles sont nécessaires.