Was macht Java-Codierungsstandards?
Standards für lesbaren, wartbaren Java (17+)-Code in Spring Boot- und Quarkus-Diensten.
Wann zu verwenden
- Schreiben oder Überprüfen von Java-Code in Spring Boot- oder Quarkus-Projekten
- Durchsetzen von Namenskonventionen, Unveränderlichkeit oder Ausnahmebehandlung
- Arbeiten mit Records, Sealed Classes oder Pattern Matching (Java 17+)
- Überprüfen der Verwendung von Optional, Streams oder Generics
- Strukturieren von Paketen und Projektlayout
- [QUARKUS]: Arbeiten mit CDI-Scopes, Panache-Entitäten oder reaktiven Pipelines
Funktionsweise
Framework-Erkennung
Vor der Anwendung der Standards wird das Framework anhand der Build-Datei ermittelt:
- Build-Datei enthält
quarkus→ [QUARKUS]-Konventionen anwenden - Build-Datei enthält
spring-boot→ [SPRING]-Konventionen anwenden - Keines erkannt → nur gemeinsame Konventionen anwenden
Kernprinzipien
- Klarheit vor Cleverness bevorzugen
- Unveränderlichkeit als Standard; gemeinsamen veränderlichen Zustand minimieren
- Früh scheitern mit aussagekräftigen Ausnahmen
- Einheitliche Namensgebung und Paketstruktur
- [QUARKUS]: Build-Zeit gegenüber Laufzeitverarbeitung bevorzugen; Laufzeit-Reflektion vermeiden, wo möglich
Beispiele
Die folgenden Abschnitte zeigen konkrete Beispiele für Spring Boot, Quarkus und gemeinsame Java-Beispiele für Namensgebung, Unveränderlichkeit, Abhängigkeitsinjektion, reaktiven Code, Ausnahmen, Projektlayout, Logging, Konfiguration und Tests.
Namensgebung
// PASS: Klassen/Records: PascalCase
public class MarketService {}
public record Money(BigDecimal amount, Currency currency) {}
// PASS: Methoden/Felder: camelCase
private final MarketRepository marketRepository;
public Market findBySlug(String slug) {}
// PASS: Konstanten: UPPER_SNAKE_CASE
private static final int MAX_PAGE_SIZE = 100;
// PASS: [QUARKUS] JAX-RS-Ressourcen heißen *Resource, nicht *Controller
public class MarketResource {}
// PASS: [SPRING] REST-Controller heißen *Controller
public class MarketController {}
Unveränderlichkeit
// PASS: Records und finale Felder bevorzugen
public record MarketDto(Long id, String name, MarketStatus status) {}
public class Market {
private final Long id;
private final String name;
// nur Getter, keine Setter
}
// PASS: [QUARKUS] Panache-Active-Record-Entitäten verwenden öffentliche Felder (Quarkus-Konvention)
@Entity
public class Market extends PanacheEntity {
public String name;
public MarketStatus status;
// Panache generiert Zugriffsmethoden zur Build-Zeit; öffentliche Felder sind hier idiomatisch
}
// PASS: [QUARKUS] Panache-MongoDB-Entitäten
@MongoEntity(collection = "markets")
public class Market extends PanacheMongoEntity {
public String name;
public MarketStatus status;
}
Optional-Verwendung
// PASS: Optional von find*-Methoden zurückgeben
// [SPRING]
Optional<Market> market = marketRepository.findBySlug(slug);
// [QUARKUS] Panache
Optional<Market> market = Market.find("slug", slug).firstResultOptional();
// PASS: Map/flatMap anstelle von get()
return market
.map(MarketResponse::from)
.orElseThrow(() -> new EntityNotFoundException("Markt nicht gefunden"));
Best Practices für Streams
// PASS: Streams für Transformationen verwenden, Pipelines kurz halten
List<String> names = markets.stream()
.map(Market::name)
.filter(Objects::nonNull)
.toList();
// FAIL: Komplexe verschachtelte Streams vermeiden; zur Verdeutlichung Schleifen bevorzugen
Abhängigkeitsinjektion
// PASS: [SPRING] Konstruktorinjektion (bevorzugt gegenüber @Autowired auf Feldern)
@Service
public class MarketService {
private final MarketRepository marketRepository;
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// PASS: [QUARKUS] Konstruktorinjektion
@ApplicationScoped
public class MarketService {
private final MarketRepository marketRepository;
@Inject
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// PASS: [QUARKUS] Package-private Feldinjektion (akzeptabel in Quarkus – vermeidet Proxy-Probleme)
@ApplicationScoped
public class MarketService {
@Inject
MarketRepository marketRepository;
}
// FAIL: [SPRING] Feldinjektion mit @Autowired
@Autowired
private MarketRepository marketRepository; // Konstruktorinjektion verwenden
// FAIL: [QUARKUS] @Singleton, wenn Abfangen oder verzögerte Initialisierung benötigt wird
@Singleton // nicht proxybar – stattdessen @ApplicationScoped verwenden
public class MarketService {}
Reaktive Muster [QUARKUS]
// PASS: Uni/Multi von reaktiven Endpunkten zurückgeben
@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: Nicht-blockierende Pipeline-Komposition
public Uni<OrderConfirmation> placeOrder(OrderRequest req) {
return validateOrder(req)
.chain(valid -> persistOrder(valid))
.chain(order -> notifyFulfillment(order));
}
// FAIL: Blockierender Aufruf innerhalb einer Uni/Multi-Pipeline
public Uni<Market> find(String slug) {
Market m = Market.find("slug", slug).firstResult(); // BLOCKING – unterbricht Ereignisschleife
return Uni.createFrom().item(m);
}
// FAIL: Mehr als einmal auf eine geteilte Uni subscriben
Uni<Market> shared = fetchMarket(slug);
shared.subscribe().with(m -> log(m));
shared.subscribe().with(m -> cache(m)); // Doppeltes Subscribe – Uni.memoize() verwenden
Ausnahmen
- Ungeprüfte Ausnahmen für Domänenfehler verwenden; technische Ausnahmen mit Kontext umschließen
- Domänenspezifische Ausnahmen erstellen (z. B.
MarketNotFoundException) - Breit gefasste
catch (Exception ex)vermeiden, es sei denn, es wird zentral erneut geworfen/protokolliert
throw new MarketNotFoundException(slug);
Zentrale Ausnahmebehandlung
// [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));
}
Generics und Typsicherheit
- Rohypen vermeiden; generische Parameter deklarieren
- Beschränkte Generics für wiederverwendbare Dienstprogramme bevorzugen
public <T extends Identifiable> Map<Long, T> indexById(Collection<T> items) { ... }
Projektstruktur
[SPRING] Maven/Gradle
src/main/java/com/example/app/
config/
controller/
service/
repository/
domain/
dto/
util/
src/main/resources/
application.yml
src/test/java/... (Spiegelt main wider)
[QUARKUS] Maven/Gradle
src/main/java/com/example/app/
config/ # @ConfigMapping, @ConfigProperty-Beans, Producers
resource/ # JAX-RS-Ressourcen (nicht "controller")
service/
repository/ # PanacheRepository-Implementierungen (falls kein Active Record verwendet wird)
domain/ # JPA/Panache-Entitäten, MongoDB-Entitäten
dto/
util/
mapper/ # MapStruct-Mapper (falls verwendet)
src/main/resources/
application.properties # Quarkus-Konvention (YAML wird mit quarkus-config-yaml unterstützt)
import.sql # Hibernate-Auto-Import für Dev/Test
src/test/java/... (Spiegelt main wider)
Formatierung und Stil
- 2 oder 4 Leerzeichen konsistent verwenden (Projektstandard)
- Ein öffentlicher Top-Level-Typ pro Datei
- Methoden kurz und fokussiert halten; Hilfsmethoden auslagern
- Reihenfolge der Member: Konstanten, Felder, Konstruktoren, öffentliche Methoden, geschützte, private
Zu vermeidende Code-Gerüche
- Lange Parameterlisten → DTOs/Builder verwenden
- Tiefe Verschachtelung → frühe Rückgaben
- Magische Zahlen → benannte Konstanten
- Statischer veränderlicher Zustand → Abhängigkeitsinjektion bevorzugen
- Stille Catch-Blöcke → protokollieren und handeln oder erneut werfen
- [QUARKUS]:
@Singleton, wo@ApplicationScopedbeabsichtigt ist – unterbricht Proxying und Interception - [QUARKUS]: Mischen von
quarkus-resteasy-reactiveundquarkus-resteasy(klassisch) – einen Stack wählen - [QUARKUS]: Panache Active Record + Repository-Muster im selben begrenzten Kontext – eines wählen
Logging
// [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 (Standard, kostenlos zur Build-Zeit)
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: vereinfachtes Logging mit @Inject
@Inject
Logger log; // CDI-injiziert, auf die deklarierende Klasse beschränkt
Null-Behandlung
@Nullablenur akzeptieren, wenn unvermeidbar; sonst@NonNullverwenden- Bean-Validation (
@NotNull,@NotBlank) bei Eingaben verwenden - [QUARKUS]:
@Validauf@BeanParam,@RestFormund Parameter des Anforderungstexts anwenden
Konfiguration
// [SPRING] @ConfigurationProperties
@ConfigurationProperties(prefix = "market")
public record MarketProperties(int maxPageSize, Duration cacheTtl) {}
// [QUARKUS] @ConfigMapping (typsicher, zur Build-Zeit validiert)
@ConfigMapping(prefix = "market")
public interface MarketConfig {
int maxPageSize();
Duration cacheTtl();
}
// [QUARKUS] Einfache Werte mit @ConfigProperty
@ConfigProperty(name = "market.max-page-size", defaultValue = "100")
int maxPageSize;
Testerwartungen
Gemeinsam
- JUnit 5 + AssertJ für fließende Behauptungen
- Mockito zum Mocken; partielle Mocks vermeiden, wo möglich
- Deterministische Tests bevorzugen; keine versteckten Sleeps
[SPRING]
@WebMvcTestfür Controller-Slices,@DataJpaTestfür Repository-Slices@SpringBootTestfür vollständige Integrationstests reservieren@MockBeanzum Ersetzen von Beans im Spring-Kontext
[QUARKUS]
- Einfaches JUnit 5 + Mockito für Unit-Tests (kein
@QuarkusTest) @QuarkusTestfür CDI-Integrationstests reservieren@InjectMockzum Ersetzen von CDI-Beans in Integrationstests- Dev Services für Datenbank/Kafka/Redis – manuelle Testcontainers-Einrichtung vermeiden, wenn Dev Services ausreichen
@QuarkusTestResourcefür benutzerdefinierte Lebenszyklen externer Dienste
// [SPRING] Controller-Test
@WebMvcTest(MarketController.class)
class MarketControllerTest {
@Autowired MockMvc mockMvc;
@MockBean MarketService marketService;
}
// [QUARKUS] Integrationstest
@QuarkusTest
class MarketResourceTest {
@InjectMock
MarketService marketService;
@Test
void should_return_404_when_market_not_found() {
given().when().get("/markets/unknown").then().statusCode(404);
}
}
// [QUARKUS] Unit-Test (kein CDI, kein @QuarkusTest)
@ExtendWith(MockitoExtension.class)
class MarketServiceTest {
@Mock MarketRepository marketRepository;
@InjectMocks MarketService marketService;
}
Denken Sie daran: Halten Sie Code absichtlich, typisiert und beobachtbar. Optimieren Sie auf Wartbarkeit statt auf Mikrooptimierungen, sofern nicht nachweislich notwendig.