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'immuabilité ou de gestion des exceptions
- Travailler avec des records, des classes scellées ou le 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 scopes CDI, les entités Panache ou les pipelines réactifs
Comment ça fonctionne
Détection du framework
Avant d'appliquer les normes, déterminez le framework à partir du fichier de build :
- Le fichier de build contient
quarkus→ appliquer les conventions [QUARKUS] - Le fichier de build contient
spring-boot→ appliquer les conventions [SPRING] - Aucun détecté → appliquer uniquement les conventions communes
Principes fondamentaux
- Préférer la clarté à l'ingéniosité
- Immuable par défaut ; minimiser l'état mutable partagé
- Échouer rapidement avec des exceptions significatives
- Nommage et structure de packages cohérents
- [QUARKUS] : Privilégier le traitement au build-time plutôt qu'à l'exécution ; éviter la réflexion à l'exécution dans la mesure du possible
Exemples
Les sections ci-dessous présentent des exemples concrets pour Spring Boot, Quarkus et Java commun pour le nommage, l'immuabilité, 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] Les ressources JAX-RS sont nommées *Resource, pas *Controller
public class MarketResource {}
// PASS : [SPRING] Les contrôleurs REST sont nommés *Controller
public class MarketController {}
Immuabilité
// PASS : Privilégier les records et les champs finals
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 au build-time ; 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 pour 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 complexes et imbriqués ; 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 par champ package-private (acceptable dans Quarkus — évite les problèmes de proxy)
@ApplicationScoped
public class MarketService {
@Inject
MarketRepository marketRepository;
}
// FAIL : [SPRING] Injection par champ avec @Autowired
@Autowired
private MarketRepository marketRepository; // utiliser l'injection par constructeur
// FAIL : [QUARKUS] @Singleton quand l'interception ou l'initialisation paresseuse est nécessaire
@Singleton // non-proxifiable — 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 à l'intérieur d'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 de domaine ; envelopper les exceptions techniques avec du contexte
- Créer des exceptions spécifiques au domaine (par exemple,
MarketNotFoundException) - Éviter les blocs
catch (Exception ex)larges sauf pour relancer/centraliser la journalisation
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é de type
- Éviter les types bruts ; déclarer les paramètres de type
- 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 du main)
[QUARKUS] Maven/Gradle
src/main/java/com/example/app/
config/ # @ConfigMapping, @ConfigProperty beans, Producers
resource/ # Ressources JAX-RS (pas "controller")
service/
repository/ # Implémentations PanacheRepository (si on n'utilise pas l'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 le dev/test
src/test/java/... (miroir du 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 helpers
- Ordre des membres : constantes, champs, constructeurs, méthodes publiques, protégées, privées
Mauvaises pratiques à éviter
- Listes de paramètres longues → utiliser DTO/builders
- Imbrication profonde → retours précoces
- Nombres magiques → constantes nommées
- État mutable statique → privilégier l'injection de dépendances
- Blocs catch silencieux → logger et agir ou relancer
- [QUARKUS] :
@Singletonlà où@ApplicationScopedest prévu — casse le proxy 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 borné — 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, zéro coût au build-time)
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, limité à la classe déclarante
Gestion des valeurs nulles
- Accepter
@Nullableuniquement quand c'est inévitable ; sinon utiliser@NonNull - Utiliser la validation par Bean Validation (
@NotNull,@NotBlank) sur les entrées - [QUARKUS] : Appliquer
@Validsur@BeanParam,@RestFormet 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é au build-time)
@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
Commun
- JUnit 5 + AssertJ pour des assertions fluides
- Mockito pour les mocks ; éviter les mocks partiels dans la mesure du possible
- Privilégier les tests déterministes ; pas de
sleepcachés
[SPRING]
@WebMvcTestpour les tranches de contrôleurs,@DataJpaTestpour les tranches de repositories@SpringBootTestréservé aux tests d'intégration complets@MockBeanpour remplacer des beans dans le contexte Spring
[QUARKUS]
- JUnit 5 simple + Mockito pour les tests unitaires (pas de
@QuarkusTest) @QuarkusTestréservé aux tests d'intégration CDI@InjectMockpour remplacer des 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 de services externes personnalisés
// [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 avérées nécessaires.