Przejdź do głównej zawartości

Receptury Java i Spring Boot dla agentów AI sprowadzają się do trzech ruchów: przypięcia Spring Boot 4.1, Hibernate 7 i Spring Security 7 w kontekście agenta, żeby przestał pisać usunięte API, proszenia agenta o określone zachowanie w razie błędu zamiast o „CRUD” oraz oddania dowodu testom Testcontainers, testowi liczby zapytań i regułom ArchUnit zamiast czytania linijka po linijce.

Ta receptura jest dla programistów Javy, którzy oddają Claude Code, Codexowi lub Cursorowi pisanie serwisów Spring Boot. Kontroler od agenta się kompiluje, a testy na mockach przechodzą. Potem build pada na @Where, którego Hibernate 7 już nie ma, lista zamówień wykonuje jedno zapytanie na wiersz, a timeout płatności kończy się błędem 500, bo nikt nie podpiął circuit breakera.

  • Blok przypinający wersje, który powstrzymuje agenta przed pisaniem kodu pod Spring Boot 3, Hibernate 6 i Security 6
  • Gotowe prompty na zasób REST, encje JPA z soft delete, odporny klient usługi zewnętrznej, zabezpieczenie JWT przez resource server i zestaw testów Testcontainers
  • Test liczby zapytań, który przerywa build przy regresji N+1
  • Reguły ArchUnit odrzucające wstrzykiwanie przez pola, dostęp kontrolerów do repozytoriów i wyciek encji, bez czytania diffa przez człowieka
  • Listę typowych awarii z procedurą wyjścia z każdej

Modele uczyły się na latach kodu Spring Boot 2 i 3, więc prompt bez przypiętych wersji produkuje API, których już nie ma. Aktualne wersje według BOM-u spring-boot-dependencies 4.1.1 z Maven Central (wydanie 2026-08-20, sprawdzone 2026-09-26): Spring Framework 7.0.9, Spring Security 7.1.1, Spring Kafka 4.1.1, Hibernate ORM 7.4.5, Jackson 3.1.5 i Testcontainers 2.0.5. Spring Boot 4.1 wymaga Javy 17 lub nowszej.

Wstaw ten blok do pliku, który agent czyta przy każdym uruchomieniu:

## Stack (do not downgrade, do not use older APIs)
- Spring Boot 4.1.x, Java 25, Maven wrapper (./mvnw)
- Spring Framework 7, Spring Security 7 (lambda DSL only), Hibernate ORM 7, Jackson 3, Testcontainers 2
- Starters: spring-boot-starter-webmvc (not -web, deprecated), spring-boot-starter-security-oauth2-resource-server
- Banned: @Where (removed in Hibernate 7; use @SoftDelete or @SQLRestriction), WebSecurityConfigurerAdapter,
.and() and authorizeRequests() in security config, ListenableFuture/addCallback on KafkaTemplate,
@MockBean (use @MockitoBean), org.testcontainers.containers.PostgreSQLContainer (use org.testcontainers.postgresql)
## Conventions
- Constructor injection only; no @Autowired fields
- Controllers return DTO records, never entities; no business logic or try/catch in controllers
- One @RestControllerAdvice returning ProblemDetail (application/problem+json)
- @Transactional on public service methods only; readOnly = true for reads
- Every endpoint ships with a Testcontainers integration test

Zapisz blok w CLAUDE.md w katalogu głównym repozytorium. Claude Code wczytuje go na początku każdej sesji. Resztę zawartości tego pliku opisuje inicjalizacja projektu.

Szkielet serwisu Spring Boot 4.1 wygenerowany przez agenta

Dział zatytułowany „Szkielet serwisu Spring Boot 4.1 wygenerowany przez agenta”

Źródłem prawdy dla drzewa zależności jest Spring Initializr pod adresem start.spring.io. Niech agent pobierze projekt stamtąd, zamiast pisać pom.xml z pamięci, bo właśnie w ręcznie pisanym POM-ie wracają stare nazwy starterów i nieaktualne wersje. Prompt jest ten sam w każdym narzędziu; różni się tylko miejsce uruchomienia.

Okno terminala
claude "Download a Spring Boot 4.1 Maven project for Java 25 from start.spring.io with web, data-jpa, validation, security, oauth2-resource-server, actuator, postgresql, flyway and testcontainers. Then check pom.xml against the Stack section of CLAUDE.md and fix any starter or version that differs. Run ./mvnw -q verify and show me the result."

Szkielet jest gotowy, gdy ./mvnw verify przechodzi, a pom.xml zawiera spring-boot-starter-webmvc zamiast przestarzałego spring-boot-starter-web.

Proś o zachowanie w razie błędu, nie o „CRUD”. Bez tego agent zwraca encje, rozrzuca bloki try/catch, które logują i zwracają null, a na błąd walidacji odpowiada stack trace’em.

Poprawny kontroler pozostaje chudy. Jeśli wygenerowany mapuje pola, łapie wyjątki albo wstrzykuje EntityManager, odeślij go z poleceniem „move mapping and error handling out of the controller; rely on the advice”:

@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); // mapping lives in the service
}
}

Poproś o soft delete, a agenci często generują parę z Hibernate 5: @SQLDelete i @Where. Adnotacji @Where w ogóle nie ma w Hibernate ORM 7.4.5 (sprawdzone w opublikowanym JAR-ze 2026-09-26), więc taki kod się nie skompiluje. Zarządzana adnotacja @SoftDelete zastępuje obie i odfiltrowuje usunięte wiersze w każdym zapytaniu.

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

Głębszą pracę z JPA, w tym pułapki leniwego ładowania w różnych ORM-ach, opisuje strona dobre praktyki ORM. Migracje Flyway opisują wzorce migracji baz danych.

Zapytania N+1 to częsta wada wydajnościowa w generowanym JPA: asocjacja LAZY czytana w pętli wykonuje jedno zapytanie na wiersz i przy pięciu wierszach danych testowych wygląda niewinnie. Test, który liczy instrukcje SQL, zamienia ją w czerwony build. Włącz statystyki w profilu testowym przez spring.jpa.properties.hibernate.generate_statistics=true, a potem sprawdź licznik:

@Test
void listingOrdersDoesNotScaleQueriesWithRows() {
fixtures.createOrders(50); // enough rows for an N+1 to show
var stats = entityManagerFactory.unwrap(SessionFactory.class).getStatistics();
stats.clear();
mockMvc.perform(get("/api/v1/orders?size=50")).andExpect(status().isOk());
assertThat(stats.getPrepareStatementCount()).isLessThanOrEqualTo(3);
}

Gdy test zrobi się czerwony, wklej błąd agentowi: „the list endpoint runs 51 statements; fix it with an @EntityGraph on to-one associations and batch fetching for collections, not by raising the limit”. Nie akceptuj JOIN FETCH kolekcji w zapytaniu stronicowanym: Hibernate stronicuje wtedy w pamięci (zobacz typowe awarie niżej).

Odporne wywołania usług zewnętrznych z Resilience4j

Dział zatytułowany „Odporne wywołania usług zewnętrznych z Resilience4j”

Serwis, który synchronicznie woła płatności albo magazyn, potrzebuje timeoutów i circuit breakera. Spring Framework 7.0 zawiera @Retryable i @ConcurrencyLimit w pakiecie org.springframework.resilience.annotation (włączane przez @EnableResilientMethods), ale nie ma circuit breakera, więc do tego użyj resilience4j-spring-boot4 2.4.0. W Kafce KafkaTemplate.send() zwraca CompletableFuture; stare ListenableFuture.addCallback(...) się nie kompiluje.

Publisher powinien wyglądać tak: whenComplete i dopiero po commicie, żeby wycofane zamówienie nigdy nie wygenerowało zdarzenia:

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
void onOrderCreated(OrderCreatedEvent event) {
var message = OrderEvent.from(event);
kafkaTemplate.send("order-events", message.orderId(), message).whenComplete((result, ex) -> {
if (ex != null) log.error("Failed to publish {}", message.eventId(), ex);
});
}

Ponowienia, klucze idempotencji i kompensację między serwisami opisują wzorce odzyskiwania.

Spring Security 7.1.1 nie ma WebSecurityConfigurerAdapter, and() ani authorizeRequests(): każda metoda HttpSecurity przyjmuje lambdę Customizer (sprawdzone w opublikowanym JAR-ze spring-security-config 2026-09-26). Agenci lubią też ręcznie pisać OncePerRequestFilter, który parsuje JWT. Nie akceptuj tego. OAuth2 resource server sam sprawdza podpis, ważność, wystawcę i odbiorcę tokenu.

Testy bezpieczeństwa wykraczające poza łańcuch filtrów opisuje strona testy bezpieczeństwa z AI.

Udowodnij to na prawdziwym Postgresie z Testcontainers 2

Dział zatytułowany „Udowodnij to na prawdziwym Postgresie z Testcontainers 2”

Zamockowane repozytoria przechodzą, choć prawdziwe zapytanie jest zepsute, dlatego konwencje wymagają prawdziwej bazy. Testcontainers 2 przeniósł moduł Postgresa do artefaktu testcontainers-postgresql i pakietu org.testcontainers.postgresql; stara klasa org.testcontainers.containers.PostgreSQLContainer jest oznaczona jako przestarzała. Dzięki @ServiceConnection Spring Boot sam podpina datasource pod kontener, więc test nie potrzebuje @DynamicPropertySource.

@TestConfiguration(proxyBeanMethods = false)
class PostgresTestConfig {
@Bean
@ServiceConnection
PostgreSQLContainer postgres() { // org.testcontainers.postgresql, not .containers
return new PostgreSQLContainer("postgres:17");
}
}

Wzorce Testcontainers wykraczające poza warstwę bazy opisuje strona testy integracyjne z AI.

Daj agentowi żywy schemat przez serwer MCP dla Postgresa

Dział zatytułowany „Daj agentowi żywy schemat przez serwer MCP dla Postgresa”

Przy pracy nad indeksami i zapytaniami agent radzi sobie lepiej, gdy czyta prawdziwe indeksy i wynik EXPLAIN, niż gdy zgaduje na podstawie klas encji. Daje to Postgres MCP Pro (PyPI postgres-mcp 0.3.0, od Crystal DBA). Ostatnie wydanie na PyPI pochodzi z maja 2025, więc zanim zrobisz z niego standard zespołu, sprawdź aktywność projektu; alternatywy, DBHub i MCP Toolbox, opisuje strona serwery MCP dla baz danych. Skieruj go na bazę deweloperską z rolą tylko do odczytu:

Okno terminala
claude mcp add -s project postgres -e 'DATABASE_URI=${DEV_DATABASE_URL}' \
-- uvx postgres-mcp --access-mode=restricted

Pojedyncze cudzysłowy zostawiają placeholder dosłownie, więc .mcp.json przechowuje ${DEV_DATABASE_URL}, a Claude Code rozwija go ze zmiennych środowiskowych przy starcie; dane logowania nigdy nie trafiają do repozytorium.

Codex (codex mcp add … --env …) i Cursor przyjmują to samo polecenie i argumenty we własnej konfiguracji MCP. Strona serwery MCP dla baz danych zawiera wszystkie trzy konfiguracje i rolę tylko do odczytu, którą trzeba najpierw utworzyć.

Jak zweryfikować kod Springa od agenta bez czytania każdej linijki

Dział zatytułowany „Jak zweryfikować kod Springa od agenta bez czytania każdej linijki”

Przeczytaj SQL migracji, reguły bezpieczeństwa i nazwy testów. Resztę niech sprawdzą bramki. ArchUnit 1.5.1 zamienia konwencje w testy, więc zignorowana przez agenta konwencja przerywa build, zamiast czekać, aż zauważy ją recenzent:

@AnalyzeClasses(packages = "com.example.shop")
class ArchitectureTest {
@ArchTest
static final ArchRule noFieldInjection = GeneralCodingRules.NO_CLASSES_SHOULD_USE_FIELD_INJECTION;
@ArchTest
static final ArchRule controllersNeverTouchRepositories = noClasses()
.that().areAnnotatedWith(RestController.class)
.should().dependOnClassesThat()
// Spring Data's interface, not the @Repository annotation: with the annotation,
// areAssignableTo never matches and the rule passes vacuously
.areAssignableTo(org.springframework.data.repository.Repository.class);
@ArchTest
static final ArchRule controllersNeverReturnEntities = noMethods()
.that().areDeclaredInClassesThat().areAnnotatedWith(RestController.class)
.should().haveRawReturnType(annotatedWith(Entity.class));
}

Reguła dla encji sprawdza tylko surowy typ zwracany. Kontroler zwracający Page<Order> albo List<Order> ją przechodzi, więc typy generyczne pokryj własnym ArchCondition, który sprawdza argumenty typu.

Sprawdzenie zakazanych API z bloku wersji dodaj do CI jako jeden krok. Kończy się kodem różnym od zera, gdy znajdzie choć jedno trafienie:

Okno terminala
! git grep -nE '@Where\b|WebSecurityConfigurerAdapter|authorizeRequests\(|\.addCallback\(|@MockBean\b|org\.testcontainers\.containers\.PostgreSQLContainer' -- '*.java'
BramkaCzego dowodziGdzie działa
./mvnw verify z TestcontainersEndpointy, walidacja, ścieżki 409/404 i soft delete działają na prawdziwym PostgresiePrzed pushem i w CI
Testy liczby zapytańBrak regresji N+1 na endpointach listCI
Reguły ArchUnitBrak wstrzykiwania przez pola, dostępu kontrolerów do repozytoriów i encji zwracanych z kontroleraCI
Powyższy git grep po zakazanych APIŻadne usunięte API nie wróciłoCI
Testy wycinkowe bezpieczeństwa (@WebMvcTest)401, 403 i mapowanie ról działają zgodnie ze specyfikacjąCI
Test circuit breakeraFallback się uruchamia, a 4xx nigdy nie jest ponawianeCI
Lektura SQL Flyway i SecurityFilterChain przez człowiekaBlokady, usunięcia i reguły dostępu są zamierzoneCode review PR

Programista, który otwiera pull request, odpowiada za nazwy testów i niezmienniki, które one kodują. Recenzent zatwierdza migrację, konfigurację bezpieczeństwa i wyniki bramek, a nie każdą wygenerowaną linijkę. Jak spakować te dowody do review, opisuje pakiet dowodów. Dlaczego to zastępuje czytanie diffa, wyjaśnia strona czytanie dowodów zamiast kodu.

  • Kod celuje w nieobsługiwaną wersję główną (Spring Boot 3 / Hibernate 6). Objawy: cannot find symbol: class Where, brak WebSecurityConfigurerAdapter, brak addCallback na CompletableFuture albo niedziałające importy com.fasterxml.jackson.databind pod Jacksonem 3. Wyjście: dodaj blok wersji do CLAUDE.md lub AGENTS.md, wklej agentowi błąd kompilatora z dopiskiem „this is Spring Boot 4.1 / Hibernate 7”, a potem dodaj do CI opisany wyżej krok git grep po zakazanych API.
  • Testy typu slice (@WebMvcTest, @DataJpaTest) się nie kompilują. Objaw: po aktualizacji nie da się rozwiązać @WebMvcTest ani @DataJpaTest. Wyjście: Spring Boot 4 podzielił autokonfigurację testów na moduły; dodaj spring-boot-starter-webmvc-test lub spring-boot-starter-data-jpa-test, zamiast pozwalać agentowi zgadywać nazwy pakietów.
  • @Transactional nic nie robi. Objaw: częściowy zapis przeżywa wyjątek. Przyczyna: adnotacja jest na metodzie private albo na metodzie wywoływanej z wnętrza tej samej klasy, więc proxy Springa nie widzi wywołania. Wyjście: przenieś ją na publiczną metodę serwisu (i nie stawiaj jej na kontrolerach: transakcje należą do warstwy serwisów), a potem dodaj test, który rzuca wyjątek w połowie zapisu i sprawdza, że wiersz nie istnieje.
  • Walidacja się nie uruchamia. Objaw: puste nazwy trafiają do bazy. Przyczyna: brak @Valid przy @RequestBody. Wyjście: test 422 z promptu REST powinien już być czerwony; jeśli jest zielony, test wysyła poprawne dane.
  • Metoda @Async połyka wyjątek. Objaw: zadanie po cichu staje. Wyjście: zwracaj CompletableFuture albo zarejestruj AsyncUncaughtExceptionHandler i sprawdź ścieżkę błędu testem.
  • Zdarzenia dla wycofanych zamówień. Objaw: inne serwisy widzą zamówienia, które nie istnieją. Wyjście: publikuj z @TransactionalEventListener(phase = AFTER_COMMIT) albo przejdź na transactional outbox.
  • Hibernate stronicuje listę w pamięci. Objaw: w logu pojawia się ostrzeżenie HHH90003004 (firstResult/maxResults specified with collection fetch; applying in memory), a zużycie pamięci rośnie z rozmiarem tabeli, nie strony. Przyczyna: JOIN FETCH albo @EntityGraph na kolekcji items w zapytaniu stronicowanym, więc Hibernate wczytuje wszystkie pasujące wiersze i stronicuje w JVM. Wyjście: w zapytaniu stronicowanym pobieraj tylko asocjacje do-jednego, a kolekcje ładuj przez default_batch_fetch_size albo drugim zapytaniem po identyfikatorach; w testach ustaw hibernate.query.fail_on_pagination_over_collection_fetch=true, żeby ostrzeżenie stało się wyjątkiem.
  • N+1 wraca po refaktoryzacji. Objaw: opóźnienie rośnie z rozmiarem strony. Wyjście: test liczby zapytań powinien być czerwony; jeśli jest zielony, fixture ma za mało wierszy, więc dodaj więcej danych.

Czy ten artykuł Ci pomógł?

Jedno kliknięcie, bez logowania.

wcale bardzo