From spring
Apply core Spring Framework APIs for the container, Java configuration, bean lifecycle, transactions, events, validation, servlet MVC, WebFlux, WebClient, and TestContext support. Use this skill when the task depends on core Spring Framework APIs rather than Boot conventions, especially the container, Java configuration, bean lifecycle, transactions, events, validation, servlet MVC controllers and exception handling, reactive HTTP with WebFlux, WebClient, and TestContext support.
npx claudepluginhub ririnto/sinon --plugin springThis skill uses the workspace's default tool permissions.
Use this skill when the task depends on core Spring Framework APIs rather than Boot conventions, especially the container, Java configuration, bean lifecycle, transactions, events, validation, servlet MVC controllers and exception handling, reactive HTTP with WebFlux, WebClient, and TestContext support.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Use this skill when the task depends on core Spring Framework APIs rather than Boot conventions, especially the container, Java configuration, bean lifecycle, transactions, events, validation, servlet MVC controllers and exception handling, reactive HTTP with WebFlux, WebClient, and TestContext support.
Use spring-framework for core Spring Framework APIs: container behavior, bean wiring, lifecycle hooks, transactions, events, scheduling, property binding, conversion, validation, ordinary servlet MVC, reactive HTTP, WebClient, and TestContext-driven framework tests.
Use narrower guidance when the task is primarily about security or Boot auto-configuration rather than Spring Framework modules and APIs.
The ordinary Spring Framework job is:
Use only the Spring Framework modules the application actually needs.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>7.0.7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Add spring-webmvc, spring-webflux, spring-jdbc, or other modules only when the task truly needs them.
@Configuration
class AppConfig {
@Bean
InventoryService inventoryService(InventoryRepository repository) {
return new InventoryService(repository);
}
}
Prefer constructor injection and explicit bean graphs. Start with explicit @Bean wiring before reaching for broader framework indirection.
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("test");
@Configuration
@PropertySource("classpath:app.properties")
class AppConfig {
}
@Autowired
ApplicationContext ctx;
String value = ctx.getEnvironment().getProperty("db.url");
Resource resource = ctx.getResource("classpath:data.json");
Use the environment to externalize configuration and profiles to control which beans or configurations are active. Keep resource loading explicit when the application needs files from the classpath or filesystem.
| Scope | Use when |
|---|---|
singleton | one shared instance per container (default) |
prototype | new instance every time the bean is requested |
| custom scope | lifecycle is neither singleton nor prototype and requires explicit scope registration |
@Bean
@Scope("prototype")
MyPrototypeBean prototypeBean() {
return new MyPrototypeBean();
}
Use singleton by default. Reach for prototype only when the lifecycle difference genuinely matters. Web-specific scopes belong to web-focused configurations rather than the ordinary framework-core path.
Open references/container-extension-scopes.md when the task needs a custom scope, container extension point, or clarification of @Configuration lite mode.
@Bean(initMethod = "init", destroyMethod = "cleanup")
MyService myService() {
return new MyService();
}
Or use @PostConstruct and @PreDestroy:
@Component
class InventoryWarmup {
@PostConstruct
void init() {
}
@PreDestroy
void cleanup() {
}
}
@Component
class InventoryWarmup implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
}
}
Use lifecycle hooks only when initialization or shutdown semantics genuinely matter. Prefer one lifecycle style consistently instead of mixing @PostConstruct / @PreDestroy with initMethod / destroyMethod in the same component graph.
Publish events for decoupled follow-up work:
@Service
class OrderService {
private final ApplicationEventPublisher events;
OrderService(ApplicationEventPublisher events) {
this.events = events;
}
public void place(Order order) {
events.publishEvent(new OrderPlacedEvent(this, order));
}
}
Listen to events:
@Component
class OrderNotificationListener implements ApplicationListener<OrderPlacedEvent> {
@Override
public void onApplicationEvent(OrderPlacedEvent event) {
}
}
Use application events for genuinely decoupled follow-up work, not as a substitute for basic method calls. Keep event classes immutable and scoped to the application package.
Open references/container-extension-scopes.md when the task depends on ordered listeners, @EventListener conditions, or lower-level listener infrastructure.
DataBinder binder = new DataBinder(new InventoryForm());
binder.bind(new MutablePropertyValues(Map.of("maxItems", "100")));
@Configuration
class AppConfig {
@Bean
ConversionService conversionService() {
DefaultConversionService service = new DefaultConversionService();
service.addConverter(new MyCustomConverter());
return service;
}
}
Use DataBinder when the framework must bind incoming values onto an object. Add custom converters only when the framework does not provide the needed conversion.
@Bean
MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
@Service
@Validated
class TransferService {
@Transactional
void transfer(@NotNull Account from, @NotNull Account to, @Positive BigDecimal amount) {
}
}
Validate at the boundary where input enters the application. Use standard Bean Validation annotations (@NotNull, @NotBlank, @Size, @Min, @Max) on input objects, and register method-validation infrastructure explicitly when service-layer method validation is part of the ordinary path.
Enable the MVC infrastructure with a configuration class and a DispatcherServlet registration, or let a servlet container initializer wire both together. The controller examples below assume the infrastructure is already in place and focus on controller shape.
@Configuration
@EnableWebMvc
class WebConfig implements WebMvcConfigurer {
}
Define controllers with constructor-injected dependencies:
@RestController
@RequestMapping("/orders")
class OrderController {
private final OrderService orders;
OrderController(OrderService orders) {
this.orders = orders;
}
@GetMapping("/{id}")
Order get(@PathVariable Long id) {
return orders.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
Order create(@RequestBody @Valid CreateOrderRequest request) {
return orders.create(request);
}
}
Handle exceptions centrally with @RestControllerAdvice:
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
ErrorResponse handleNotFound(OrderNotFoundException ex) {
return new ErrorResponse(ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
return new ErrorResponse(ex.getBindingResult().getFieldError().getDefaultMessage());
}
}
Keep controllers thin: delegate all logic to services. Use @RestControllerAdvice as a single exception boundary rather than scattering try/catch blocks across controllers.
Add spring-webflux and use annotated controllers returning Mono and Flux. The examples below assume WebFlux infrastructure is already configured and focus on controller shape.
@RestController
@RequestMapping("/items")
class ItemController {
private final ItemService items;
ItemController(ItemService items) {
this.items = items;
}
@GetMapping("/{id}")
Mono<Item> get(@PathVariable Long id) {
return items.findById(id);
}
@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<Item> stream() {
return items.streamAll();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
Mono<Item> create(@RequestBody @Valid Mono<CreateItemRequest> request) {
return request.flatMap(items::create);
}
}
Handle errors in the reactive chain with onErrorMap or a @RestControllerAdvice that returns Mono<ResponseEntity<?>>:
@ExceptionHandler(ItemNotFoundException.class)
Mono<ResponseEntity<ErrorResponse>> handleNotFound(ItemNotFoundException ex) {
return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(ex.getMessage())));
}
Keep operator chains short. Return early by flatMapping into the service rather than blocking.
In plain Spring Framework, register the builder explicitly as shown below.
Build a WebClient bean once and inject it where needed:
@Bean
WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
@Bean
WebClient webClient(WebClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
Make a typed GET request:
Mono<Order> order = client.get()
.uri("/orders/{id}", orderId)
.retrieve()
.bodyToMono(Order.class);
Handle 4xx and 5xx responses explicitly rather than letting them propagate as WebClientResponseException:
Mono<Order> order = client.get()
.uri("/orders/{id}", orderId)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, response -> response.bodyToMono(String.class).map(ApiException::new))
.onStatus(HttpStatusCode::is5xxServerError, response -> response.bodyToMono(String.class).map(UpstreamServiceException::new))
.bodyToMono(Order.class);
Use bodyToFlux for streaming responses and ExchangeStrategies when the default codec buffer limit needs adjustment.
Keep client configuration explicit. Centralize base URL and default headers in the bean definition rather than scattering them across call sites.
Open references/webclient-reactive-depth.md when the task needs client filters, Reactor Netty-specific timeouts, retry behavior, or deeper reactive-chain patterns.
Enable transaction management explicitly in plain Spring Framework:
@Configuration
@EnableTransactionManagement
class TxConfig {
}
Pair this with the appropriate PlatformTransactionManager bean for the chosen data-access technology.
@Service
class TransferService {
private final AccountRepository accounts;
TransferService(AccountRepository accounts) {
this.accounts = accounts;
}
@Transactional
void transfer(String from, String to, BigDecimal amount) {
accounts.debit(from, amount);
accounts.credit(to, amount);
}
}
Keep transaction boundaries on service methods that own one business unit of work. Avoid transactions that span multiple unrelated operations.
@Configuration
class DataConfig {
@Bean
DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
}
Use DriverManagerDataSource or SimpleDriverDataSource only for testing and stand-alone environments. Pair plain Spring JDBC with a real pool such as HikariCP or Apache DBCP2 when the application manages its own production data source.
@Configuration
class DataConfig {
@Bean
JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
Query a single row:
@Service
class InventoryRepository {
private final JdbcTemplate jdbc;
InventoryRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
Item findById(Long id) {
return jdbc.queryForObject("SELECT id, name, quantity FROM items WHERE id = ?", (rs, rowNum) -> new Item(rs.getLong("id"), rs.getString("name"), rs.getInt("quantity")), id);
}
}
Update with parameters:
void updateQuantity(Long id, int quantity) {
jdbc.update("UPDATE items SET quantity = ? WHERE id = ?", quantity, id);
}
Batch operations:
void insertBatch(List<Item> items) {
jdbc.batchUpdate("INSERT INTO items (name, quantity) VALUES (?, ?)", items.stream().map(item -> new Object[]{item.name(), item.quantity()}).toList());
}
Keep JDBC templates as the data-access primitive when the application needs plain SQL without an ORM layer. Use NamedParameterJdbcTemplate when named parameters improve readability over positional ? placeholders.
Open references/plain-jdbc-wiring.md when the task needs transaction-scoped connections, SqlRowSet, RowMapper reuse, or DataSourceTransactionManager with plain JDBC.
@Configuration
@EnableAsync
class AppConfig {
@Bean
TaskExecutor taskExecutor() {
return new SimpleAsyncTaskExecutor();
}
}
Async method:
@Component
class InventoryNotifier {
@Async
void sendAlert(String itemId) {
}
}
Use @EnableAsync to activate framework-managed async execution. The default SimpleAsyncTaskExecutor creates a new thread per call. Register a custom TaskExecutor bean when pooled thread behavior, queue depth, or naming strategy matters.
@Configuration
@EnableScheduling
class AppConfig {
}
Scheduled method:
@Component
class InventoryCleanup {
@Scheduled(cron = "0 0 2 * * ?")
void cleanup() {
}
}
Use @EnableScheduling to activate framework-scheduled tasks. Keep scheduled jobs idempotent and document the cron expression.
| Executor type | Use when |
|---|---|
SimpleAsyncTaskExecutor | default async; each call gets a new thread |
ThreadPoolTaskExecutor | pooled threads with queue; most common choice |
ThreadPoolTaskScheduler | for @Scheduled methods that need a dedicated scheduler pool |
TaskExecutor interface | execute(Runnable) abstraction for swapping executor implementations |
@Bean
TaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("inventory-");
executor.initialize();
return executor;
}
Open references/async-executor-registration.md when the task needs async exception handling, completion coordination with Future / CompletableFuture, or custom TaskDecorator for thread-local propagation.
Do not stack @Async and @Scheduled on the same method casually. Treat that combination as a proxy and executor design decision rather than ordinary scheduling. Separate concerns into distinct methods when both behaviors are needed.
Use @EnableCaching only when the task explicitly needs framework-managed cache annotations and the cache boundary is clear.
Treat AOT and native-image hints as an escalation point rather than part of the ordinary path. Keep the default implementation reflective and explicit until the task specifically requires AOT-friendly wiring.
Open references/aop-cross-cutting.md when cross-cutting behavior must wrap many beans consistently and the ordinary bean wiring path is not enough.
Open references/container-extension-scopes.md when the task needs to customize bean definitions, post-process beans, register a custom scope, or understand @Configuration lite mode versus full configuration.
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AppConfig.class)
class AppConfigTests {
@Autowired
InventoryService inventoryService;
@Test
void contextLoads() {
assertNotNull(inventoryService);
}
}
Test the smallest framework integration that proves the behavior. Use @ExtendWith(SpringExtension.class) to integrate the Spring ApplicationContext with JUnit Jupiter.
Test a servlet MVC controller with MockMvc wired from the WebApplicationContext:
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = {AppConfig.class, WebConfig.class})
class OrderControllerTests {
@Autowired
WebApplicationContext wac;
MockMvc mockMvc;
@BeforeEach
void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
void getOrder_returnsOk() throws Exception {
mockMvc.perform(get("/orders/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1));
}
@Test
void createOrder_withInvalidBody_returnsBadRequest() throws Exception {
mockMvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isBadRequest());
}
}
Test a reactive controller or WebClient interaction with WebTestClient:
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {AppConfig.class, ReactiveWebConfig.class})
class ItemControllerTests {
@Autowired
ApplicationContext ctx;
WebTestClient client;
@BeforeEach
void setup() {
client = WebTestClient.bindToApplicationContext(ctx).build();
}
@Test
void getItem_returnsOk() {
client.get().uri("/items/1")
.exchange()
.expectStatus().isOk()
.expectBody(Item.class)
.value(item -> assertNotNull(item.id()));
}
}
Keep controller tests focused on HTTP semantics: status codes, headers, and response shape. Delegate business-logic assertions to plain unit tests against the service layer.
@Bean
InventoryService inventoryService(InventoryRepository repository)
class InventoryWarmup implements ApplicationListener<ContextRefreshedEvent>
@ContextConfiguration@ContextConfiguration(classes = AppConfig.class)
MockMvc.WebClient interactions return the expected status and body under WebTestClient.@RestControllerAdvice exception mappings produce the intended status codes.Future / CompletableFuture, or TaskDecorator for thread-local propagation.@Configurable, or AspectJ join points beyond Spring AOP proxies.@Configuration lite-mode behavior.SqlRowSet, RowMapper reuse, or DataSourceTransactionManager with plain JDBC.