Przejdź do głównej zawartości

Wzorce Java i Spring Boot

Twój serwis Spring Boot się kompiluje, ale AI wstrzyknął zależności przez pola, zostawił nieszczelną granicę @Transactional i żadnego @ControllerAdvice — i wszystko się sypie przy pierwszym wyścigu dwóch żądań o to samo zamówienie. Wygenerowana Java wygląda idiomatycznie, dopóki produkcyjny ruch nie znajdzie szwów: zapytania N+1, połknięte wyjątki, wywołanie KafkaTemplate, które nie kompiluje się już na Spring Boot 3. Te przepisy produkują kod Spring Boot 3.x na Javie 21, który się broni, i — co równie ważne — pokazują, jak wyłapać tryby awarii, które model wciąż psuje.

  • Gotowy do wklejenia prompt na zasób REST z walidacją, paginacją i jednym globalnym handlerem wyjątków
  • Prompt JPA, który używa zarządzanego soft-delete z Hibernate 6.4 zamiast przestarzałego wzorca @Where
  • Prompt na odporny klient downstream (circuit breaker Resilience4j + aktualne API Kafki na CompletableFuture)
  • Prompt Spring Security 6 na bezstanowe uwierzytelnianie JWT, które przejdzie review
  • Prompt na testy produkujący testy integracyjne Testcontainers, a nie mocki happy-path
  • Specyficzne dla Springa pułapki do sprawdzenia przy każdej generacji oraz serwery MCP, które zacieśniają pętlę

Krok 1: Scaffold projektu (ta część różni się per narzędzie)

Dział zatytułowany „Krok 1: Scaffold projektu (ta część różni się per narzędzie)”

Spring Initializr jest źródłem prawdy dla drzewa zależności, więc sensowne jest, by agent go wywołał, a potem nałożył na to twoje konwencje.

W trybie agenta wskaż Cursorowi świeży katalog:

Generate a Spring Boot 3.4 project on Java 21 using start.spring.io with
dependencies: web, data-jpa, validation, security, actuator, postgresql,
flyway. Maven build. Then add a CLAUDE.md-style conventions file documenting
the rules below and apply them to the generated code.

Cursor robi scaffold w workspace, pokazuje diff, a ty akceptujesz per plik. Trzymaj konwencje w pliku .cursor/rules/, żeby każda późniejsza edycja je dziedziczyła.

Niezależnie od narzędzia, podaj mu te konwencje z góry — zapobiegają najczęstszemu outputowi blokującemu review:

- Java 21: records for DTOs, pattern matching, virtual threads where I/O-bound
- Constructor injection only (never @Autowired field injection)
- One @ControllerAdvice for the whole app; never catch-and-swallow
- @Transactional only on service methods, readOnly=true by default
- No business logic in controllers; no entities returned from controllers (DTOs only)
- Every new endpoint ships with a Testcontainers integration test

Teraz część identyczna we wszystkich trzech narzędziach. Sztuczka polega na określeniu koperty odpowiedzi i zachowania przy awarii, a nie samego “CRUD” — inaczej dostaniesz kontroler happy-path, który wycieka encje i zwraca stack trace’y.

Następnie przejrzyj to, co wróciło, pod kątem konwencji. Najcenniejsza kontrola dotyczy handlera wyjątków: modele uwielbiają rozsiewać bloki try/catch, które logują i zwracają null. Chcesz dokładnie jednej klasy advice i zera połkniętych wyjątków. Poprawny kontroler pozostaje tak chudy:

@RestController
@RequestMapping("/api/v1/products")
class ProductController {
private final ProductService products;
ProductController(ProductService products) { // constructor injection, no @Autowired
this.products = products;
}
@GetMapping
Page<ProductResponse> list(@PageableDefault(size = 20) Pageable pageable,
@RequestParam(required = false) String search) {
return products.search(search, pageable); // validation + mapping live in the service
}
}

Jeśli wygenerowany kontroler jest grubszy niż to — logika mapowania, try/catch, wstrzyknięty EntityManager — odeślij go z powrotem: “Move all mapping and error handling out of the controller; rely on the @RestControllerAdvice.”

Krok 3: Persystencja bez pułapki przestarzałego soft-delete

Dział zatytułowany „Krok 3: Persystencja bez pułapki przestarzałego soft-delete”

Poproś o soft delete, a większość modeli wciąż wypluwa parę @SQLDelete + @Where z Hibernate 5. @Where jest przestarzałe w Hibernate 6 (zastąpione przez @SQLRestriction), a Spring Boot 3.2+ dostarcza Hibernate 6.4, który ma w pełni zarządzaną adnotację @SoftDelete. Nazwij wersję, żeby dostać nowoczesną ścieżkę.

Zarządzana adnotacja zwija dawny taniec z dwiema adnotacjami do jednej linijki, a Hibernate automatycznie filtruje wiersze usunięte miękko wszędzie:

@Entity
@Table(name = "orders")
@SoftDelete // Hibernate 6.4+: replaces @SQLDelete + @Where, applied to every query
@EntityListeners(AuditingEntityListener.class)
class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
// ... items, status, auditing fields
}

Mikroserwis, który synchronicznie wywołuje płatności lub magazyn, potrzebuje circuit breakera, a każdy kod dotykający Kafki musi używać aktualnego API. Na Spring Kafka 3.x (Spring Boot 3.x) KafkaTemplate.send() zwraca CompletableFuture — dawne ListenableFuture.addCallback(...) już nie istnieje i się nie skompiluje.

Publisher Kafki powinien wyglądać tak — whenComplete, nie addCallback:

@Component
class OrderEventPublisher {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
OrderEventPublisher(KafkaTemplate<String, OrderEvent> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
@EventListener
void onOrderCreated(OrderCreatedEvent event) {
var kafkaEvent = OrderEvent.from(event);
// Spring Kafka 3.x returns CompletableFuture; .addCallback was removed
kafkaTemplate.send("order-events", kafkaEvent).whenComplete((result, ex) -> {
if (ex == null) log.info("Event sent: {}", kafkaEvent.eventId());
else log.error("Failed to send event {}", kafkaEvent.eventId(), ex);
});
}
}

Spring Security 6 porzucił WebSecurityConfigurerAdapter i dawny DSL. Przypnij wersję, żeby agent wyprodukował lambda DSL z beanem SecurityFilterChain, a nie wskrzeszał przestarzałą klasę bazową.

Krok 6: Testy, które dowodzą zachowania, a nie teatru pokrycia

Dział zatytułowany „Krok 6: Testy, które dowodzą zachowania, a nie teatru pokrycia”

Powód, dla którego upieramy się przy Testcontainers w konwencjach, jest taki, że testy z zamockowanym repozytorium przechodzą, podczas gdy prawdziwe zapytanie jest zepsute. Generacja nie jest skończona, dopóki nie dostarczy testu integracyjnego, który ćwiczy prawdziwą bazę danych.

  • Wzorce baz danych — głębsze JPA, strojenie zapytań i workflowy migracji
  • Wzorce API — międzyjęzykowy REST i projektowanie koperty błędów
  • Wzorce ORM — playbook na N+1 i lazy-loading w różnych ORM-ach
  • Wzorce Kubernetes — wdrażanie tych serwisów na klaster