Skip to main content

spring-boot

Use when building, reviewing, testing, securing or configuring a Spring Boot 4 / Framework 7 backend — controllers, services, Spring Data JPA, application.yml, SecurityFilterChain, slice tests. NOT plain modern-Java language work like records or virtual threads (that is `java`); NOT engine-level SQL schema/index/EXPLAIN (that is `postgresdb`).

معلومات المصدر

المستودع
ericrisco/rsc-harness
آخر نشاط في المصدر
٢٩ يوليو ٢٠٢٦ في ٢٢:٥١
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١٤٢
التفرعات
١١

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

مستكشف الملفات
7 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
spring-boot
description
Use when building, reviewing, testing, securing or configuring a Spring Boot 4 / Framework 7 backend — controllers, services, Spring Data JPA, application.yml, SecurityFilterChain, slice tests. NOT plain modern-Java language work like records or virtual threads (that is `java`); NOT engine-level SQL schema/index/EXPLAIN (that is `postgresdb`).
tags
["java","spring","jpa","security","backend"]
recommends
["java","postgresdb","secure-coding","deployment"]
origin
risco
# Spring Boot backends (Boot 4 / Framework 7) A Spring Boot app is **a thin web layer delegating to a transactional service layer over Spring Data JPA repositories** — wired by constructor injection, configured by typed `@ConfigurationProperties`, locked down by a `SecurityFilterChain` bean. Controllers validate input and delegate; they never own business logic, transactions, or persistence. Hold that shape and most "where does this go?" questions answer themselves. **Pinned stack** (verify against the project's `pom.xml`/`build.gradle` — do not assume): Spring Boot 4.0 (GA 2025-11-20), Spring Framework 7, Java 17 baseline / Java 25 LTS, Jakarta EE 11 (`jakarta.*`, never `javax.*`), Jackson 3, Spring Security 7, Spring Data JPA / Hibernate 7, JUnit 5 + Testcontainers, Maven 3.9 / Gradle. If you are typing `WebSecurityConfigurerAdapter`, `@MockBean`, field `@Autowired`, `authorizeRequests`, or `javax.persistence` — **stop**. Those are the previous generation. The modern idioms below replace every one of them. ## Boundaries - Plain Java language work (records, sealed types, virtual threads, streams, pattern matching) with no Spring -> [`../java/SKILL.md`](../java/SKILL.md). - Async Python FastAPI -> [`../fastapi/SKILL.md`](../fastapi/SKILL.md). NestJS/Node -> [`../nestjs/SKILL.md`](../nestjs/SKILL.md). Django -> [`../django/SKILL.md`](../django/SKILL.md). - Engine-level SQL: schema/index design, `EXPLAIN`, partitioning, zero-downtime DDL -> [`../postgresdb/SKILL.md`](../postgresdb/SKILL.md) (this skill drives the JPA layer above it). - Language-agnostic injection/authz/secret theory -> [`../secure-coding/SKILL.md`](../secure-coding/SKILL.md). - Dockerfile/Compose/CI/CD mechanics -> [`../deployment/SKILL.md`](../deployment/SKILL.md) (keep only a build note here). ## Project layout Package by feature, not by layer — colocation keeps a change to one feature in one folder. ```text com.acme.shop ├── order/ │ ├── OrderController.java // @RestController — web edge │ ├── OrderService.java // @Service — @Transactional unit of work │ ├── OrderRepository.java // extends JpaRepository<Order, Long> │ ├── Order.java // @Entity (jakarta.persistence) │ └── dto/CreateOrderRequest.java, OrderResponse.java // records, never entities ├── config/AppProperties.java // @ConfigurationProperties record ├── security/SecurityConfig.java // SecurityFilterChain bean └── ShopApplication.java // @SpringBootApplication ``` ## Controllers `@RestController` + DTO records, `@Valid` on the body (Bean Validation, `jakarta.validation`) so business code can assume valid data, `ResponseEntity` for 201/`Location`, a `@RestControllerAdvice` for one error envelope. The controller parses, validates, delegates and maps — any branch with business meaning belongs in the service, where it is transactional and unit-testable without MVC. Boot 4 adds first-class versioning via a `version` attribute on the mapping — one controller serves many versions, no path duplication. ```java @RestController @RequestMapping("/api/users") class UserController { private final UserService users; UserController(UserService users) { this.users = users; } // constructor injection @PostMapping(version = "1") // Boot 4 API versioning ResponseEntity<UserResponse> create(@Valid @RequestBody CreateUserRequest req) { UserResponse body = users.create(req); URI location = URI.create("/api/users/" + body.id()); return ResponseEntity.created(location).body(body); // 201 + Location } } record CreateUserRequest(@NotBlank String name, @Email String email) {} record UserResponse(Long id, String name, String email) {} ``` ```java @RestControllerAdvice class ApiExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) ResponseEntity<ApiError> onInvalid(MethodArgumentNotValidException e) { var details = e.getBindingResult().getFieldErrors().stream() .map(f -> f.getField() + ": " + f.getDefaultMessage()).toList(); return ResponseEntity.badRequest().body(new ApiError("validation_failed", "Invalid request", details)); } } record ApiError(String code, String message, List<String> details) {} ``` **Bad -> Good** — never return the entity; it leaks columns and lazy-loads in the serializer: ```java // Bad: leaks columns; lazy fields blow up in the serializer after the tx closes. @GetMapping("/{id}") User get(@PathVariable Long id) { return repo.findById(id).orElseThrow(); } // Good: map to a DTO inside the transactional service. @GetMapping("/{id}") UserResponse get(@PathVariable Long id) { return users.get(id); } ``` ## Service + transactions Constructor-injected, `final` fields, `@Transactional` on the write path, `readOnly = true` on queries (lets Hibernate skip dirty checking). `@Transactional` belongs on service methods, never on a controller or repository: the transaction must wrap the unit of work, not the HTTP request or a single query. ```java @Service class UserService { private final UserRepository repo; private final PasswordEncoder encoder; UserService(UserRepository repo, PasswordEncoder encoder) { this.repo = repo; this.encoder = encoder; } @Transactional UserResponse create(CreateUserRequest req) { var user = repo.save(new User(req.name(), req.email(), encoder.encode(req.rawPassword()))); return new UserResponse(user.getId(), user.getName(), user.getEmail()); } @Transactional(readOnly = true) UserResponse get(Long id) { return repo.findById(id).map(this::toResponse).orElseThrow(() -> new NotFoundException(id)); } } ``` Two traps that produce "my `@Transactional` isn't rolling back": - **Self-invocation.** Calling `this.other()` inside the same bean bypasses the proxy, so its `@Transactional` is ignored. Split into another bean or accept the outer transaction. - **Checked exceptions don't roll back by default.** Spring rolls back on `RuntimeException` only; use `@Transactional(rollbackFor = ...)` for checked ones. **Bad -> Good** — field injection vs constructor: ```java // Bad: not testable with `new`, hides missing beans until runtime, allows final-less mutation. @Autowired private UserRepository repo; // Good: private final UserRepository repo; UserService(UserRepository repo) { this.repo = repo; } ``` ## JPA persistence `jakarta.persistence` imports (never `javax`). Spring Data gives you derived queries for free and `@Query` for the rest; `Pageable`/`Page` for paging. ```java import jakarta.persistence.*; @Entity @Table(name = "users") class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Column(unique = true) private String email; @OneToMany(mappedBy = "user") private List<Order> orders = new ArrayList<>(); // getters; protected no-arg ctor for Hibernate } interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByEmail(String email); // derived query Page<User> findByNameContaining(String q, Pageable page); // paginated @Query("select u from User u join fetch u.orders where u.id = :id") Optional<User> findWithOrders(@Param("id") Long id); // fetch join kills N+1 } ``` **N+1 symptom:** iterating a lazy collection issues one query per parent. Fix with a `join fetch`, an `@EntityGraph`, or `@BatchSize`. **`LazyInitializationException`** means you touched a lazy field after the transaction (and its Hibernate session) closed — map to a DTO *inside* the `@Transactional` service, or fetch eagerly for that path. Relationship/cascade depth, projections, Specifications, optimistic locking and migration tooling are in [`references/jpa.md`](references/jpa.md). ## Configuration & profiles ```yaml # application.yml — no secrets committed here; import them at boot. spring: config: import: "optional:configtree:/run/secrets/" # mount real secrets at runtime datasource: url: ${DB_URL} username: ${DB_USER} password: ${DB_PASSWORD} app: invite-ttl: 24h max-orders-per-day: 50 --- spring: config: activate: on-profile: dev app: max-orders-per-day: 5 ``` ```java @ConfigurationProperties(prefix = "app") record AppProperties(Duration inviteTtl, int maxOrdersPerDay) {} // typed, validated at startup // register once: @EnableConfigurationProperties(AppProperties.class) on a @Configuration ``` **Bad -> Good** — scattered `@Value("${app.max-orders-per-day}")` strings vs one injected `AppProperties` record. One typed binding beats string keys sprinkled across the codebase and fails fast on a missing/mistyped key instead of NPE-ing later. ## Security A single `SecurityFilterChain` bean with the lambda DSL, stateless for token APIs, JWT via the resource server. ```java @Configuration @EnableMethodSecurity // enables @PreAuthorize class SecurityConfig { @Bean SecurityFilterChain api(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) // OK: stateless token API, no cookies .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.POST, "/api/users").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated()) .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults())); return http.build(); } @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ``` Order `requestMatchers` from most specific to least — the first match wins, so a broad `permitAll` placed early opens routes you meant to lock. Full JWT/OAuth2 client, method security, CORS, and CSRF posture (token vs cookie apps) live in [`references/security.md`](references/security.md). For the language-agnostic authz/secret principles behind these rules, see [`../secure-coding/SKILL.md`](../secure-coding/SKILL.md). ## Testing Pick the narrowest slice that exercises what you changed — `@SpringBootTest` only when you genuinely need the full context: | Slice | Loads | Use for | Collaborators | |---|---|---|---| | `@WebMvcTest` | web layer + Security + MockMvc | one controller's HTTP contract | `@MockitoBean` the service | | `@DataJpaTest` | JPA + in-memory/TC DB, rolls back per test | repository queries, mappings | real repo, test DB | | `@SpringBootTest` | full context | end-to-end / integration | real beans, Testcontainers | `@MockBean`/`@SpyBean` are removed — use `@MockitoBean`/`@MockitoSpyBean` from `org.springframework.test.context.bean.override.mockito`. ```java @WebMvcTest(UserController.class) class UserControllerTest { @Autowired MockMvc mvc; @MockitoBean UserService users; // not @MockBean @Test void rejectsBlankName() throws Exception { mvc.perform(post("/api/users").contentType(MediaType.APPLICATION_JSON) .content("{\"name\":\"\",\"email\":\"a@b.co\"}")) .andExpect(status().isBadRequest()); } } ``` Integration DB via Testcontainers + `@ServiceConnection` (auto-wires connection details, no `@DynamicPropertySource`): ```java @TestConfiguration(proxyBeanMethods = false) class ContainersConfig { @Bean @ServiceConnection PostgreSQLContainer<?> postgres() { return new PostgreSQLContainer<>("postgres:17"); } } ``` Slice deep dive, container reuse, `MockMvcTester`/`WebTestClient`, and the CI gate are in [`references/testing.md`](references/testing.md). ## HTTP clients & resilience Outbound calls: declare an `@HttpExchange` interface and register it — no manual `RestTemplate`/`HttpServiceProxyFactory` boilerplate. ```java @HttpExchange("/v1") interface BillingClient { @GetExchange("/invoices/{id}") Invoice invoice(@PathVariable String id); } // register: @ImportHttpServices(group = "billing", types = BillingClient.class) on a @Configuration ``` `RestClient` is the modern synchronous client for ad-hoc calls. For built-in resilience, `@Retryable` and `@ConcurrencyLimit` are core in Framework 7 — no extra Spring Retry dependency for the basics. ## Anti-patterns | Anti-pattern | Why it's wrong | Do instead | |---|---|---| | Extend `WebSecurityConfigurerAdapter` | Removed in Security 6/7 | `SecurityFilterChain` bean + lambda DSL | | `@Autowired` on a field | Untestable, hides missing beans till runtime | constructor injection, `final` fields | | `@Transactional` on a `@RestController` | Tx must wrap the unit of work, not the request | put it on the service method | | Business branching in the controller | Not transactional, needs MVC to test | move the decision into the `@Service` | | Return the `@Entity` from a controller | Leaks columns, lazy-loads in serializer (LIE) | map to a DTO record inside the tx | | Request body reaching the service unvalidated | Business code can no longer assume valid data | `@Valid` + `jakarta.validation` at the edge | | Scattered `@Value("${...}")` config keys | String keys, no validation, fails late | one typed `@ConfigurationProperties` record | | Use `@MockBean` / `@SpyBean` | Replaced in Boot 4 | `@MockitoBean` / `@MockitoSpyBean` | | `import javax.persistence` / `javax.validation` | Jakarta EE 11 baseline | `jakarta.*` | | `authorizeRequests` / `antMatchers` | Gone in Security 6/7 | `authorizeHttpRequests` + `requestMatchers` | | `csrf().disable()` with no rationale | Silently opens cookie-session apps | disable only for stateless token APIs; comment why | | `@SpringBootTest` for one controller | Slow, loads everything | `@WebMvcTest` + `@MockitoBean` | | One 800-line `@Service` | Untestable, tangled transactions | split per use case / aggregate |
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub