Was macht Java-Codierungsstandards?
Standards für lesbaren, wartbaren Java-Code (17+) 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 Ausnahmebehandlungs-Konventionen
- 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
Bevor Standards angewendet werden, wird das Framework anhand der Build-Datei bestimmt:
- Build-Datei enthält
quarkus→ [QUARKUS]-Konventionen anwenden - Build-Datei enthält
spring-boot→ [SPRING]-Konventionen anwenden - Keines erkannt → nur allgemeine Konventionen anwenden
Kernprinzipien
- Klarheit vor Cleverness bevorzugen
- Standardmäßig unveränderlich; gemeinsamen veränderlichen Zustand minimieren
- Frühzeitig mit aussagekräftigen Ausnahmen fehlschlagen
- Einheitliche Namenskonventionen und Paketstruktur
- [QUARKUS]: Build-Zeit gegenüber Laufzeitverarbeitung bevorzugen; Laufzeit-Reflection wo möglich vermeiden
Beispiele
Die folgenden Abschnitte zeigen konkrete Spring Boot-, Quarkus- und allgemeine Java-Beispiele für Namenskonventionen, Unveränderlichkeit, Abhängigkeitsinjektion, reaktiven Code, Ausnahmen, Projektlayout, Protokollierung, Konfiguration und Tests.
Namenskonventionen
// BESTANDEN: Klassen/Records: PascalCase
public class MarketService {}
public record Money(BigDecimal amount, Currency currency) {}
// BESTANDEN: Methoden/Felder: camelCase
private final MarketRepository marketRepository;
public Market findBySlug(String slug) {}
// BESTANDEN: Konstanten: UPPER_SNAKE_CASE
private static final int MAX_PAGE_SIZE = 100;
// BESTANDEN: [QUARKUS] JAX-RS-Ressourcen werden als *Resource benannt, nicht als *Controller
public class MarketResource {}
// BESTANDEN: [SPRING] REST-Controller werden als *Controller benannt
public class MarketController {}
Unveränderlichkeit
// BESTANDEN: 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
}
// BESTANDEN: [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 Accessors zur Build-Zeit; öffentliche Felder sind hier idiomatisch
}
// BESTANDEN: [QUARKUS] Panache MongoDB-Entitäten
@MongoEntity(collection = "markets")
public class Market extends PanacheMongoEntity {
public String name;
public MarketStatus status;
}
Optional-Nutzung
// BESTANDEN: Optional von find*-Methoden zurückgeben
// [SPRING]
Optional<Market> market = marketRepository.findBySlug(slug);
// [QUARKUS] Panache
Optional<Market> market = Market.find("slug", slug).firstResultOptional();
// BESTANDEN: Map/flatMap anstelle von get()
return market
.map(MarketResponse::from)
.orElseThrow(() -> new EntityNotFoundException("Market nicht gefunden"));
Best Practices für Streams
// BESTANDEN: Streams für Transformationen verwenden, Pipelines kurz halten
List<String> names = markets.stream()
.map(Market::name)
.filter(Objects::nonNull)
.toList();
// FEHLER: Komplexe verschachtelte Streams vermeiden; aus Gründen der Klarheit Schleifen vorziehen
Abhängigkeitsinjektion
// BESTANDEN: [SPRING] Konstruktorinjektion (bevorzugt gegenüber @Autowired auf Feldern)
@Service
public class MarketService {
private final MarketRepository marketRepository;
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// BESTANDEN: [QUARKUS] Konstruktorinjektion
@ApplicationScoped
public class MarketService {
private final MarketRepository marketRepository;
@Inject
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// BESTANDEN: [QUARKUS] Paket-private Feldinjektion (in Quarkus akzeptabel – vermeidet Proxy-Probleme)
@ApplicationScoped
public class MarketService {
@Inject
MarketRepository marketRepository;
}
// FEHLER: [SPRING] Feldinjektion mit @Autowired
@Autowired
private MarketRepository marketRepository; // Konstruktorinjektion verwenden
// FEHLER: [QUARKUS] @Singleton, wenn Interception oder Lazy-Initialisierung benötigt wird
@Singleton // nicht proxy-fähig – stattdessen @ApplicationScoped verwenden
public class MarketService {}
Reaktive Muster [QUARKUS]
// BESTANDEN: 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));
}
// BESTANDEN: Nicht-blockierende Pipeline-Komposition
public Uni<OrderConfirmation> placeOrder(OrderRequest req) {
return validateOrder(req)
.chain(valid -> persistOrder(valid))
.chain(order -> notifyFulfillment(order));
}
// FEHLER: Blockierender Aufruf innerhalb einer Uni/Multi-Pipeline
public Uni<Market> find(String slug) {
Market m = Market.find("slug", slug).firstResult(); // BLOCKIEREND – unterbricht die Event-Schleife
return Uni.createFrom().item(m);
}
// FEHLER: Mehrfaches Abonnieren einer gemeinsamen Uni
Uni<Market> shared = fetchMarket(slug);
shared.subscribe().with(m -> log(m));
shared.subscribe().with(m -> cache(m)); // doppeltes Abonnieren – 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) - Breites
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
- Roh-Typen vermeiden; generische Parameter deklarieren
- Begrenzte Generics für wiederverwendbare Hilfsprogramme 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 Active Record nicht verwendet)
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 Entwicklungs-/Testumgebungen
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
- Mitglieder ordnen: 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 auswählen - [QUARKUS]: Panache Active Record + Repository-Muster im selben begrenzten Kontext – eines auswählen
Protokollierung
// [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: vereinfachte Protokollierung mit @Inject
@Inject
Logger log; // CDI-injiziert, auf die deklarierende Klasse beschränkt
Null-Behandlung
@Nullablenur akzeptieren, wenn unvermeidbar; ansonsten@NonNullverwenden- Bean-Validation (
@NotNull,@NotBlank) für Eingaben verwenden - [QUARKUS]:
@Validauf@BeanParam,@RestFormund Anforderungstext-Parametern 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
Allgemein
- JUnit 5 + AssertJ für flüssige Assertions
- Mockito zum Mocken; partielle Mocks nach Möglichkeit vermeiden
- Deterministische Tests bevorzugen; keine versteckten Sleeps
[SPRING]
@WebMvcTestfür Controller-Slices,@DataJpaTestfür Repository-Slices@SpringBootTestnur für vollständige Integrationstests@MockBeanzum Ersetzen von Beans im Spring-Kontext
[QUARKUS]
- Normales JUnit 5 + Mockito für Unit-Tests (kein
@QuarkusTest) @QuarkusTestnur für CDI-Integrationstests@InjectMockzum Ersetzen von CDI-Beans in Integrationstests- Dev Services für Datenbank/Kafka/Redis – manuelles Testcontainers-Setup vermeiden, wenn Dev Services ausreichen
@QuarkusTestResourcefür benutzerdefinierten Lebenszyklus 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 absichtsvoll, typisiert und beobachtbar. Optimieren Sie auf Wartbarkeit statt auf Mikrooptimierungen, sofern diese nicht nachweislich notwendig sind.