Jesse A. Green / notebook

Spring Framework User Guide

Generated May 2026 from spring-projects/spring-framework at 5e6e223

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

1. Quick Start

Spring Framework is the foundational Java application framework that provides comprehensive infrastructure support for developing enterprise Java applications. It handles the plumbing so you can focus on business logic through dependency injection, aspect-oriented programming, data access abstractions, web frameworks (both servlet-based and reactive), messaging, testing, and more.

Key features:

  • Inversion of Control (IoC) container — manages object creation, wiring, and lifecycle
  • Aspect-Oriented Programming (AOP) — cross-cutting concerns without boilerplate
  • Spring MVC — annotation-driven servlet-based web framework
  • Spring WebFlux — fully reactive, non-blocking web framework built on Project Reactor
  • Data access abstractions — JdbcTemplate, R2DBC, ORM integration, transaction management
  • Messaging — JMS, WebSocket/STOMP, and generic messaging abstractions
  • Spring Expression Language (SpEL) — powerful expression evaluation at runtime
  • Comprehensive testing support — MockMvc, WebTestClient, context caching, bean overrides

Prerequisites: JDK 17+ (JDK 21+ recommended for virtual threads support)

Minimal Gradle setup:

dependencies {
    implementation 'org.springframework:spring-context:7.0.0'
}

Minimal example — annotation-based IoC:

import org.springframework.context.annotation.*;

@Configuration
@ComponentScan("com.example")
public class AppConfig {

    @Bean
    public GreetingService greetingService() {
        return new GreetingService();
    }
}
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Application {
    public static void main(String[] args) {
        var ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        GreetingService svc = ctx.getBean(GreetingService.class);
        svc.greet("World");
        ctx.close();
    }
}
Hello, World!

Tip

Most Spring applications use Spring Boot which auto-configures Spring Framework. If you are starting a new project, begin with Spring Initializr — it generates a ready-to-run Spring Boot project with Spring Framework as the underlying engine.

What to try next:

  • See "Installation" for all dependency management options
  • See "Core Usage — Dependency Injection" for full IoC container usage
  • See "Core Usage — Web Applications" for building REST APIs and web apps
  • See Admin Guide for architecture deep-dives and module internals

2. Installation

System Requirements

Requirement Minimum Recommended
JDK 17 21+ (for virtual threads, pattern matching)
Build tool Gradle 8.x or Maven 3.6+ Latest stable
Servlet container (for spring-webmvc) Jakarta Servlet 6.0+ (Tomcat 11, Jetty 12, Undertow) Tomcat 11.0+
Reactive runtime (for spring-webflux) Reactor Netty, Tomcat, Jetty, Undertow, or Servlet 6.0+ Reactor Netty

Important

Spring Framework 7.x requires Jakarta EE 10+ APIs (jakarta.* namespace). If migrating from Spring 5.x, you must update all javax.* imports to jakarta.*.

Dependency Management with BOM

Spring Framework publishes a Bill of Materials (BOM) so you can manage all module versions in one place.

Maven

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-framework-bom</artifactId>
            <version>7.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
    </dependency>
</dependencies>

Gradle

dependencies {
    implementation platform('org.springframework:spring-framework-bom:7.0.0')
    implementation 'org.springframework:spring-context'
}

Module Dependency Guide

Choose the modules you need based on your use case:

Use Case Module(s) Maven Artifact ID
IoC container only spring-context spring-context
Web MVC (Servlet) spring-webmvc spring-webmvc
Reactive Web spring-webflux spring-webflux
JDBC / SQL spring-jdbc spring-jdbc
Reactive SQL (R2DBC) spring-r2dbc spring-r2dbc
JPA / Hibernate spring-orm spring-orm
Transaction management spring-tx spring-tx
JMS messaging spring-jms spring-jms
WebSocket / STOMP spring-websocket spring-websocket
Testing spring-test spring-test
AOP (AspectJ) spring-aspects spring-aspects
Object/XML mapping spring-oxm spring-oxm

Note

Transitive dependencies are managed automatically. Adding spring-webmvc pulls in spring-web, spring-context, spring-aop, spring-beans, spring-core, and spring-expression.

Maven Repositories

repositories {
    mavenCentral()
    // For milestones and release candidates:
    maven { url = "https://repo.spring.io/milestone" }
    // For snapshots:
    maven { url = "https://repo.spring.io/snapshot" }
}

Post-Install Verification

import org.springframework.core.SpringVersion;

public class VersionCheck {
    public static void main(String[] args) {
        System.out.println("Spring Framework: " + SpringVersion.getVersion());
    }
}
Spring Framework: 7.0.0

Install Troubleshooting

Problem: ClassNotFoundException: jakarta.servlet.ServletException

Your servlet container or dependency set uses the old javax.servlet namespace. Spring Framework 7.x requires Jakarta EE 10+ (jakarta.*). Upgrade your container (Tomcat 11+, Jetty 12+) and dependencies.

Problem: Gradle dependency resolution errors with SNAPSHOT versions

Add the Spring snapshot repository and ensure dynamic version caching is disabled:

configurations.all {
    resolutionStrategy {
        cacheChangingModulesFor 0, "seconds"
        cacheDynamicVersionsFor 0, "seconds"
    }
}

Problem: NoSuchMethodError or NoClassDefFoundError at runtime

Multiple Spring versions on the classpath. Use the BOM (see above) to enforce a single consistent version across all modules.

Problem: Module conflicts with spring-jcl

Spring Framework uses spring-jcl (Jakarta Commons Logging bridge) for logging. If you see conflicts with commons-logging, exclude it:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jcl</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Problem: Kotlin extension functions not resolving

Ensure you have both spring-context and the Kotlin standard library on the classpath. Kotlin extensions are defined in companion source sets (e.g., spring-r2dbc/src/main/kotlin/) and require the Kotlin runtime.

Problem: @ComponentScan not finding beans

Verify the base package matches where your annotated classes live. By default, @ComponentScan without arguments scans the package of the configuration class and all sub-packages.

Problem: Java module system (JPMS) issues

Spring Framework provides Automatic-Module-Name in each JAR manifest. If using the module system, add requires org.springframework.context; (or the relevant module) to your module-info.java.


3. Configuration

Spring Framework supports three configuration styles that can be mixed freely.

@Configuration
@ComponentScan("com.example")
@PropertySource("classpath:application.properties")
public class AppConfig {

    @Value("${app.name}")
    private String appName;

    @Bean
    public DataSource dataSource() {
        var ds = new HikariDataSource();
        ds.setJdbcUrl("jdbc:postgresql://localhost/mydb");
        return ds;
    }
}

Key annotations:

Annotation Purpose
@Configuration Declares a configuration class
@Bean Registers a method's return value as a bean
@ComponentScan Auto-detects @Component, @Service, @Repository, @Controller
@PropertySource Loads external properties files
@Value Injects property values or SpEL expressions
@Import Imports additional configuration classes
@Profile Activates beans only under specific profiles
@Conditional Conditional bean registration
@Lazy Defers bean initialization until first use
@Scope Sets bean scope (singleton, prototype, request, session)
@Primary Marks a bean as the default when multiple candidates exist
@Qualifier Disambiguates between multiple beans of the same type
@DependsOn Forces bean initialization order
@Order / @Priority Controls ordering of list-injected beans

Java-Based Configuration (Programmatic)

var ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class, SecurityConfig.class);
ctx.getEnvironment().setActiveProfiles("production");
ctx.refresh();

XML Configuration (Legacy)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           https://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.example"/>
    <context:property-placeholder location="classpath:app.properties"/>

    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
        <property name="jdbcUrl" value="${db.url}"/>
    </bean>
</beans>

Tip

Annotation-based configuration is the standard for new projects. XML configuration remains fully supported for legacy codebases and is required for certain namespace features (<aop:config>, <tx:annotation-driven>, etc.).

XML Namespace Reference (User-Facing)

The following XML namespaces are available for configuration. These are taken from the source code's NamespaceHandler implementations:

Namespace Prefix Key Elements
context context: property-placeholder, property-override, annotation-config, component-scan, load-time-weaver, spring-configured, mbean-export, mbean-server
aop aop: config, aspectj-autoproxy, scoped-proxy
tx tx: annotation-driven, advice, jta-transaction-manager
mvc mvc: annotation-driven, default-servlet-handler, interceptors, resources, view-controller, cors, path-matching
jdbc jdbc: embedded-database, initialize-database
jms jms: listener-container, jca-listener-container
oxm oxm: jaxb2-marshaller
websocket websocket: handlers, message-broker, stomp-endpoint
cache cache: annotation-driven, advice
task task: annotation-driven, executor, scheduler, scheduled-tasks

See Admin Guide for the complete configuration reference with all attributes.

Profiles

@Configuration
@Profile("development")
public class DevConfig {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("schema.sql")
            .build();
    }
}

Activate profiles via:

ctx.getEnvironment().setActiveProfiles("development");

Or via system property: -Dspring.profiles.active=development

Warning

Beans without a @Profile annotation are always active. If you define a DataSource in both a profiled and un-profiled config, you may get duplicate bean conflicts. Use @Primary or ensure only one is active per profile.

Environment Abstraction

@Component
public class EnvironmentAwareService {

    @Autowired
    private Environment env;

    public String getDbUrl() {
        return env.getProperty("db.url", "jdbc:h2:mem:default");
    }

    public boolean isProduction() {
        return env.acceptsProfiles(Profiles.of("production"));
    }
}

4. Core Usage

4.1 Dependency Injection

Constructor Injection (Preferred)

@Service
public class OrderService {

    private final OrderRepository repository;
    private final NotificationService notifications;

    public OrderService(OrderRepository repository, NotificationService notifications) {
        this.repository = repository;
        this.notifications = notifications;
    }

    public void placeOrder(Order order) {
        repository.save(order);
        notifications.sendConfirmation(order);
    }
}

Tip

When a class has a single constructor, @Autowired is not required — Spring injects dependencies automatically. This is the cleanest pattern for testability and immutability.

Field and Setter Injection

@Component
public class ReportGenerator {

    @Autowired
    private TemplateEngine templateEngine;

    private DataSource dataSource;

    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

Qualifier and Primary

@Bean
@Primary
public DataSource primaryDataSource() { /* ... */ }

@Bean("analyticsDs")
public DataSource analyticsDataSource() { /* ... */ }

// Inject specific bean:
@Autowired
@Qualifier("analyticsDs")
private DataSource analyticsDataSource;

Collection and Map Injection

@Service
public class PluginManager {

    // Injects all beans of type Plugin, ordered by @Order / @Priority
    private final List<Plugin> plugins;

    // Injects all beans of type Plugin, keyed by bean name
    private final Map<String, Plugin> pluginMap;

    public PluginManager(List<Plugin> plugins, Map<String, Plugin> pluginMap) {
        this.plugins = plugins;
        this.pluginMap = pluginMap;
    }
}

ObjectProvider (Lazy / Optional Dependencies)

@Service
public class FlexibleService {

    private final ObjectProvider<CacheManager> cacheManagerProvider;

    public FlexibleService(ObjectProvider<CacheManager> cacheManagerProvider) {
        this.cacheManagerProvider = cacheManagerProvider;
    }

    public void doWork() {
        CacheManager cm = cacheManagerProvider.getIfAvailable();
        if (cm != null) {
            // Use caching
        }
    }
}

Bean Scopes

Scope Behavior
singleton (default) One instance per IoC container
prototype New instance every time requested
request One instance per HTTP request (web only)
session One instance per HTTP session (web only)
application One instance per ServletContext (web only)

Bean Lifecycle Callbacks

@Component
public class ConnectionPool implements InitializingBean, DisposableBean {

    @Override
    public void afterPropertiesSet() {
        // Called after all properties are set — initialize pool
    }

    @Override
    public void destroy() {
        // Called on container shutdown — close connections
    }
}

// Or annotation-based:
@Component
public class ConnectionPool {

    @PostConstruct
    public void init() {
        // Initialize
    }

    @PreDestroy
    public void cleanup() {
        // Cleanup
    }
}

4.2 Aspect-Oriented Programming (AOP)

Enable AOP with @EnableAspectJAutoProxy:

@Configuration
@EnableAspectJAutoProxy
public class AopConfig { }

Define aspects:

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void logMethodEntry(JoinPoint jp) {
        System.out.println("Entering: " + jp.getSignature().getName());
    }

    @Around("@annotation(com.example.Timed)")
    public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        Object result = pjp.proceed();
        long elapsed = System.nanoTime() - start;
        System.out.printf("%s took %d ms%n", pjp.getSignature().getName(), elapsed / 1_000_000);
        return result;
    }

    @AfterReturning(pointcut = "within(com.example..*)", returning = "result")
    public void logReturn(JoinPoint jp, Object result) {
        System.out.println(jp.getSignature().getName() + " returned: " + result);
    }

    @AfterThrowing(pointcut = "within(com.example..*)", throwing = "ex")
    public void logException(JoinPoint jp, Exception ex) {
        System.err.println("Exception in " + jp.getSignature() + ": " + ex.getMessage());
    }
}

Pointcut expression reference:

Expression Matches
execution(* com.example.*.*(..)) Any method in com.example package
within(com.example.service..*) Any type within service package and sub-packages
@annotation(com.example.Loggable) Methods annotated with @Loggable
@within(org.springframework.stereotype.Service) Types annotated with @Service
bean(orderService) The bean named orderService
args(java.io.Serializable,..) Methods whose first arg is Serializable
this(com.example.MyInterface) Proxy implements MyInterface
target(com.example.MyClass) Target object is MyClass

Note

Spring AOP uses JDK dynamic proxies for interfaces and CGLIB proxies for classes. If you need to advise private or final methods, use full AspectJ weaving via spring-aspects and load-time weaving.

Combining Pointcuts

@Aspect
@Component
public class SecurityAspect {

    @Pointcut("within(com.example.service..*)")
    private void inServiceLayer() {}

    @Pointcut("@annotation(com.example.RequiresAuth)")
    private void requiresAuth() {}

    @Before("inServiceLayer() && requiresAuth()")
    public void checkAuth(JoinPoint jp) {
        // Combined pointcut: service layer methods annotated with @RequiresAuth
    }
}

4.3 Spring Expression Language (SpEL)

SpEL enables dynamic value resolution at runtime:

@Value("#{systemProperties['user.home']}")
private String userHome;

@Value("#{T(java.lang.Math).random() * 100}")
private double randomValue;

@Value("#{orderService.getActiveOrderCount()}")
private int activeOrders;

// Conditional default:
@Value("#{systemProperties['cache.size'] ?: 256}")
private int cacheSize;

// Collection selection:
@Value("#{users.?[role == 'ADMIN']}")
private List<User> admins;

// Collection projection:
@Value("#{orders.![total]}")
private List<BigDecimal> orderTotals;

SpEL in XML:

<bean id="config" class="com.example.AppConfig">
    <property name="maxRetries" value="#{systemProperties['max.retries'] ?: 3}"/>
</bean>

4.4 Events

The ApplicationContext publishes and receives events:

// Define an event
public class OrderPlacedEvent extends ApplicationEvent {
    private final Order order;
    public OrderPlacedEvent(Object source, Order order) {
        super(source);
        this.order = order;
    }
    public Order getOrder() { return order; }
}

// Publish
@Service
public class OrderService {
    @Autowired
    private ApplicationEventPublisher publisher;

    public void placeOrder(Order order) {
        // ... save order
        publisher.publishEvent(new OrderPlacedEvent(this, order));
    }
}

// Listen — annotation-based (recommended)
@Component
public class OrderEventListener {

    @EventListener
    public void handleOrderPlaced(OrderPlacedEvent event) {
        System.out.println("Order placed: " + event.getOrder().getId());
    }

    @EventListener(condition = "#event.order.total > 1000")
    public void handleHighValueOrder(OrderPlacedEvent event) {
        // Special handling for high-value orders
    }

    @Async
    @EventListener
    public void asyncNotification(OrderPlacedEvent event) {
        // Runs asynchronously in a separate thread
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void afterOrderCommitted(OrderPlacedEvent event) {
        // Only fires after the transaction commits successfully
    }
}

Note

@Async event listeners require @EnableAsync on a configuration class and a configured TaskExecutor. @TransactionalEventListener requires an active transaction context.

4.5 Web Applications — Spring MVC

Basic REST Controller

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping
    public List<Order> listOrders(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return orderService.findAll(page, size);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Order> getOrder(@PathVariable Long id) {
        return orderService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Order createOrder(@Valid @RequestBody OrderRequest request) {
        return orderService.create(request);
    }

    @PutMapping("/{id}")
    public Order updateOrder(@PathVariable Long id, @Valid @RequestBody OrderRequest request) {
        return orderService.update(id, request);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteOrder(@PathVariable Long id) {
        orderService.delete(id);
    }
}

MVC Configuration

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoggingInterceptor())
            .addPathPatterns("/api/**")
            .excludePathPatterns("/api/health");
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
            .addResourceLocations("classpath:/static/")
            .setCachePeriod(3600);
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.defaultContentType(MediaType.APPLICATION_JSON);
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter());
        converters.add(new StringHttpMessageConverter());
    }
}

Warning

@EnableWebMvc takes full control of Spring MVC configuration. If using Spring Boot, do NOT use @EnableWebMvc — Boot provides its own auto-configuration that @EnableWebMvc will override.

Exception Handling

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(OrderNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleNotFound(OrderNotFoundException ex) {
        return new ErrorResponse("NOT_FOUND", ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
        var errors = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .toList();
        return new ErrorResponse("VALIDATION_ERROR", String.join("; ", errors));
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleGeneral(Exception ex) {
        return new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred");
    }
}

Request Mapping Annotations

Annotation HTTP Method Usage
@GetMapping GET Retrieve resources
@PostMapping POST Create resources
@PutMapping PUT Replace resources
@PatchMapping PATCH Partial update
@DeleteMapping DELETE Delete resources
@RequestMapping Any Generic mapping (use method attribute)

Parameter Binding Annotations

Annotation Source Example
@PathVariable URL path /orders/{id} → @PathVariable Long id
@RequestParam Query string ?page=2 → @RequestParam int page
@RequestBody Request body JSON → Java object
@RequestHeader HTTP header @RequestHeader("X-Request-Id") String reqId
@CookieValue Cookie @CookieValue("sessionId") String sessionId
@ModelAttribute Form data / model @ModelAttribute UserForm form
@MatrixVariable Matrix params /cars;color=red → @MatrixVariable String color

API Versioning (Spring Framework 7.0+)

Spring Framework 7.0 introduced built-in API versioning support through ApiVersionConfigurer:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        configurer
            .useRequestHeader("X-API-Version")
            .useQueryParam("version")
            .setDefaultVersion("1.0");
    }
}

Version resolvers (confirmed from ApiVersionConfigurer source):

Resolver Method Extraction Location
Header useRequestHeader(name) HTTP request header
Query parameter useQueryParam(name) URL query string
Path segment usePathSegment(index) URL path (e.g., /v2/api/...)
Media type parameter useMediaTypeParameter(mediaType, param) Accept/Content-Type header parameter
Custom useVersionResolver(resolver...) Any custom extraction logic

The default version parser is SemanticApiVersionParser which supports semantic versioning.

Content Negotiation and Message Converters

Spring MVC auto-detects the following HttpMessageConverter implementations based on classpath availability (confirmed from AnnotationDrivenBeanDefinitionParser source):

Converter Format Library Required
MappingJackson2HttpMessageConverter JSON jackson-databind
JacksonJsonHttpMessageConverter JSON (newer) jackson-databind 3.x
GsonHttpMessageConverter JSON gson
Jaxb2RootElementHttpMessageConverter XML JAXB (JDK built-in)
MappingJackson2XmlHttpMessageConverter XML jackson-dataformat-xml
JacksonXmlHttpMessageConverter XML (newer) jackson-dataformat-xml 3.x
MappingJackson2SmileHttpMessageConverter Smile jackson-dataformat-smile
JacksonSmileHttpMessageConverter Smile (newer) jackson-dataformat-smile 3.x
MappingJackson2CborHttpMessageConverter CBOR jackson-dataformat-cbor
JacksonCborHttpMessageConverter CBOR (newer) jackson-dataformat-cbor 3.x
MappingJackson2YamlHttpMessageConverter YAML jackson-dataformat-yaml
JacksonYamlHttpMessageConverter YAML (newer) jackson-dataformat-yaml 3.x
AtomFeedHttpMessageConverter Atom rome
RssChannelHttpMessageConverter RSS rome
StringHttpMessageConverter Plain text (built-in)
ByteArrayHttpMessageConverter Bytes (built-in)
ResourceHttpMessageConverter Binary resources (built-in)
AllEncompassingFormHttpMessageConverter Form data (built-in)

4.6 Web Applications — Spring WebFlux (Reactive)

Annotation-Based Reactive Controller

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductRepository repository;

    public ProductController(ProductRepository repository) {
        this.repository = repository;
    }

    @GetMapping
    public Flux<Product> listProducts() {
        return repository.findAll();
    }

    @GetMapping("/{id}")
    public Mono<ResponseEntity<Product>> getProduct(@PathVariable String id) {
        return repository.findById(id)
            .map(ResponseEntity::ok)
            .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<Product> createProduct(@Valid @RequestBody Product product) {
        return repository.save(product);
    }

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<Product> streamProducts() {
        return repository.findAll().delayElements(Duration.ofSeconds(1));
    }
}

Functional Endpoints

@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction<ServerResponse> routes(ProductHandler handler) {
        return RouterFunctions.route()
            .GET("/api/products", handler::listAll)
            .GET("/api/products/{id}", handler::getById)
            .POST("/api/products", handler::create)
            .filter((request, next) -> {
                // Filter/middleware
                return next.handle(request);
            })
            .build();
    }
}

@Component
public class ProductHandler {

    private final ProductRepository repository;

    public ProductHandler(ProductRepository repository) {
        this.repository = repository;
    }

    public Mono<ServerResponse> listAll(ServerRequest request) {
        return ServerResponse.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .body(repository.findAll(), Product.class);
    }

    public Mono<ServerResponse> getById(ServerRequest request) {
        String id = request.pathVariable("id");
        return repository.findById(id)
            .flatMap(p -> ServerResponse.ok().bodyValue(p))
            .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> create(ServerRequest request) {
        return request.bodyToMono(Product.class)
            .flatMap(repository::save)
            .flatMap(p -> ServerResponse.created(URI.create("/api/products/" + p.getId())).bodyValue(p));
    }
}

Tip

Choose annotation-based controllers when migrating from Spring MVC (same programming model). Choose functional endpoints when you want a lambda-based routing DSL with explicit request/response handling.

WebFlux Configuration

@Configuration
@EnableWebFlux
public class WebFluxConfig implements WebFluxConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("*")
            .allowedMethods("GET", "POST");
    }

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        configurer.usePathSegment(0)
            .setDefaultVersion("1.0");
    }

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().maxInMemorySize(512 * 1024); // 512KB
    }
}

4.7 Data Access — JDBC

JdbcTemplate

@Repository
public class OrderRepository {

    private final JdbcTemplate jdbc;

    public OrderRepository(DataSource dataSource) {
        this.jdbc = new JdbcTemplate(dataSource);
    }

    public List<Order> findAll() {
        return jdbc.query("SELECT id, customer, total FROM orders",
            (rs, rowNum) -> new Order(
                rs.getLong("id"),
                rs.getString("customer"),
                rs.getBigDecimal("total")
            ));
    }

    public Optional<Order> findById(long id) {
        try {
            return Optional.ofNullable(jdbc.queryForObject(
                "SELECT id, customer, total FROM orders WHERE id = ?",
                (rs, rowNum) -> new Order(rs.getLong("id"), rs.getString("customer"), rs.getBigDecimal("total")),
                id));
        } catch (EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }

    public int save(Order order) {
        return jdbc.update("INSERT INTO orders (customer, total) VALUES (?, ?)",
            order.getCustomer(), order.getTotal());
    }

    public int[] batchSave(List<Order> orders) {
        return jdbc.batchUpdate(
            "INSERT INTO orders (customer, total) VALUES (?, ?)",
            new BatchPreparedStatementSetter() {
                @Override
                public void setValues(PreparedStatement ps, int i) throws SQLException {
                    ps.setString(1, orders.get(i).getCustomer());
                    ps.setBigDecimal(2, orders.get(i).getTotal());
                }
                @Override
                public int getBatchSize() { return orders.size(); }
            });
    }
}

NamedParameterJdbcTemplate

@Repository
public class UserRepository {

    private final NamedParameterJdbcTemplate jdbc;

    public UserRepository(DataSource dataSource) {
        this.jdbc = new NamedParameterJdbcTemplate(dataSource);
    }

    public List<User> findByRole(String role) {
        return jdbc.query(
            "SELECT * FROM users WHERE role = :role",
            Map.of("role", role),
            (rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name"), rs.getString("role"))
        );
    }

    public List<User> findByIds(List<Long> ids) {
        return jdbc.query(
            "SELECT * FROM users WHERE id IN (:ids)",
            Map.of("ids", ids),
            (rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name"), rs.getString("role"))
        );
    }
}

SimpleJdbcInsert

@Repository
public class OrderRepository {

    private final SimpleJdbcInsert insert;

    public OrderRepository(DataSource dataSource) {
        this.insert = new SimpleJdbcInsert(dataSource)
            .withTableName("orders")
            .usingGeneratedKeyColumns("id");
    }

    public long save(Order order) {
        Map<String, Object> params = Map.of(
            "customer", order.getCustomer(),
            "total", order.getTotal()
        );
        return insert.executeAndReturnKey(params).longValue();
    }
}

Embedded Database (Testing)

@Bean
public DataSource testDataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.H2)
        .addScript("classpath:schema.sql")
        .addScript("classpath:test-data.sql")
        .build();
}

4.8 Data Access — R2DBC (Reactive)

@Repository
public class ReactiveOrderRepository {

    private final DatabaseClient client;

    public ReactiveOrderRepository(ConnectionFactory connectionFactory) {
        this.client = DatabaseClient.create(connectionFactory);
    }

    public Flux<Order> findAll() {
        return client.sql("SELECT id, customer, total FROM orders")
            .map(row -> new Order(
                row.get("id", Long.class),
                row.get("customer", String.class),
                row.get("total", BigDecimal.class)))
            .all();
    }

    public Mono<Order> findById(Long id) {
        return client.sql("SELECT id, customer, total FROM orders WHERE id = :id")
            .bind("id", id)
            .map(row -> new Order(
                row.get("id", Long.class),
                row.get("customer", String.class),
                row.get("total", BigDecimal.class)))
            .one();
    }

    public Mono<Void> save(Order order) {
        return client.sql("INSERT INTO orders (customer, total) VALUES (:customer, :total)")
            .bind("customer", order.getCustomer())
            .bind("total", order.getTotal())
            .then();
    }
}

Note

R2DBC provides non-blocking database access for reactive applications. It requires an R2DBC driver for your database (e.g., io.r2dbc:r2dbc-postgresql for PostgreSQL, io.r2dbc:r2dbc-h2 for H2). Kotlin coroutine extensions are available in spring-r2dbc/src/main/kotlin/.

4.9 Transaction Management

@Configuration
@EnableTransactionManagement
public class TxConfig {

    @Bean
    public PlatformTransactionManager transactionManager(DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
}

@Service
@Transactional
public class OrderService {

    @Transactional(readOnly = true)
    public List<Order> findAll() {
        return repository.findAll();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 30)
    public void placeOrder(Order order) {
        repository.save(order);
        inventoryService.reserve(order.getItems());
    }

    @Transactional(rollbackFor = BusinessException.class, noRollbackFor = WarningException.class)
    public void processPayment(Payment payment) {
        // ...
    }
}

@Transactional attributes:

Attribute Default Purpose
propagation REQUIRED Transaction propagation behavior
isolation DEFAULT Transaction isolation level
timeout -1 (no timeout) Transaction timeout in seconds
readOnly false Read-only optimization hint
rollbackFor (unchecked exceptions) Exception types that trigger rollback
noRollbackFor (none) Exception types that do NOT trigger rollback
transactionManager (default) Name of specific transaction manager bean
label (none) Labels for transaction monitoring

Transaction propagation behaviors:

Propagation Behavior
REQUIRED Join existing or create new (default)
REQUIRES_NEW Always create new, suspend existing
SUPPORTS Join existing or run non-transactional
NOT_SUPPORTED Suspend existing, run non-transactional
MANDATORY Must join existing, throw if none
NEVER Throw if transaction exists
NESTED Nested transaction with savepoint

Warning

@Transactional only works on public methods called from outside the class. Self-invocation (calling a @Transactional method from within the same class) bypasses the proxy and the transaction is not applied. Use TransactionTemplate for programmatic transactions within the same class.

Programmatic Transactions

@Service
public class BatchService {

    private final TransactionTemplate txTemplate;

    public BatchService(PlatformTransactionManager txManager) {
        this.txTemplate = new TransactionTemplate(txManager);
        this.txTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
    }

    public void processBatch(List<Item> items) {
        txTemplate.execute(status -> {
            for (Item item : items) {
                process(item);
            }
            return null; // Commit on successful return
        });
    }
}

Reactive Transactions

@Configuration
@EnableTransactionManagement
public class R2dbcTxConfig {

    @Bean
    public ReactiveTransactionManager transactionManager(ConnectionFactory cf) {
        return new R2dbcTransactionManager(cf);
    }
}

@Service
public class ReactiveOrderService {

    @Transactional
    public Mono<Order> placeOrder(Order order) {
        return repository.save(order)
            .flatMap(saved -> inventoryService.reserve(saved.getItems())
                .thenReturn(saved));
    }
}

4.10 Task Execution and Scheduling

Enable Async and Scheduling

@Configuration
@EnableAsync
@EnableScheduling
public class TaskConfig {

    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        return executor;
    }

    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5);
        return scheduler;
    }
}

Scheduled Tasks

@Component
public class ScheduledJobs {

    @Scheduled(fixedRate = 60000)
    public void refreshCache() {
        // Runs every 60 seconds
    }

    @Scheduled(cron = "0 0 2 * * ?")
    public void nightlyCleanup() {
        // Runs at 2 AM daily
    }

    @Scheduled(fixedDelay = 30000, initialDelay = 5000)
    public void pollExternalSystem() {
        // Runs 30s after previous completion, 5s initial delay
    }
}

Async Methods

@Service
public class EmailService {

    @Async
    public CompletableFuture<SendResult> sendEmail(String to, String subject, String body) {
        // Long-running email send
        SendResult result = mailSender.send(to, subject, body);
        return CompletableFuture.completedFuture(result);
    }

    @Async("customExecutor")
    public void sendBulkEmails(List<EmailRequest> requests) {
        // Uses a specific named executor
        requests.forEach(r -> mailSender.send(r));
    }
}

Task Decorators (Context Propagation)

@Bean
ThreadPoolTaskExecutor decoratedTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
    return executor;
}

Tip

Use ContextPropagatingTaskDecorator to propagate observability context (trace IDs, MDC) across async task boundaries.

4.11 Caching

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("products", "users");
    }
}

@Service
public class ProductService {

    @Cacheable("products")
    public Product findById(Long id) {
        // Expensive lookup — result cached by id
        return repository.findById(id).orElseThrow();
    }

    @Cacheable(value = "products", key = "#category + '-' + #page")
    public List<Product> findByCategory(String category, int page) {
        return repository.findByCategory(category, page);
    }

    @CachePut(value = "products", key = "#product.id")
    public Product update(Product product) {
        return repository.save(product);
    }

    @CacheEvict(value = "products", allEntries = true)
    public void clearProductCache() {
        // Evicts all entries
    }

    @Caching(
        put = @CachePut(value = "products", key = "#product.id"),
        evict = @CacheEvict(value = "productLists", allEntries = true)
    )
    public Product save(Product product) {
        return repository.save(product);
    }
}

4.12 Validation

public class OrderRequest {

    @NotBlank(message = "Customer name is required")
    private String customer;

    @NotNull @Positive
    private BigDecimal total;

    @Size(min = 1, message = "At least one item is required")
    private List<@Valid OrderItem> items;

    @Email
    private String email;

    // getters/setters
}

@RestController
public class OrderController {

    @PostMapping("/api/orders")
    public Order create(@Valid @RequestBody OrderRequest request) {
        // Validation errors automatically return 400
        return orderService.create(request);
    }
}

Important

Bean Validation requires a JSR-380 implementation on the classpath (e.g., Hibernate Validator: org.hibernate.validator:hibernate-validator). Spring auto-detects it and creates a LocalValidatorFactoryBean.

Programmatic Validation

@Service
public class OrderService {

    @Autowired
    private Validator validator;

    public void processOrder(Order order) {
        Set<ConstraintViolation<Order>> violations = validator.validate(order);
        if (!violations.isEmpty()) {
            throw new ConstraintViolationException(violations);
        }
        // process
    }
}

4.13 WebSocket and STOMP

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic", "/queue");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();
    }
}

@Controller
public class ChatController {

    @MessageMapping("/chat.send")
    @SendTo("/topic/messages")
    public ChatMessage send(ChatMessage message) {
        return message;
    }

    @MessageMapping("/chat.private")
    @SendToUser("/queue/reply")
    public ChatMessage privateReply(ChatMessage message, Principal principal) {
        return new ChatMessage("Reply to " + principal.getName());
    }
}

Note

For production WebSocket deployments, consider using a full-featured STOMP broker relay (e.g., RabbitMQ with STOMP plugin) instead of the simple in-memory broker. Configure with config.enableStompBrokerRelay("/topic", "/queue").

4.14 JMS Messaging

@Configuration
@EnableJms
public class JmsConfig {

    @Bean
    public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(
            ConnectionFactory connectionFactory) {
        var factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setConcurrency("3-10");
        factory.setSessionTransacted(true);
        return factory;
    }
}

// Sending
@Service
public class NotificationSender {

    private final JmsTemplate jmsTemplate;

    public NotificationSender(JmsTemplate jmsTemplate) {
        this.jmsTemplate = jmsTemplate;
    }

    public void send(Notification notification) {
        jmsTemplate.convertAndSend("notifications", notification);
    }

    public void sendWithHeaders(Notification notification) {
        jmsTemplate.convertAndSend("notifications", notification, message -> {
            message.setStringProperty("priority", "HIGH");
            return message;
        });
    }
}

// Receiving
@Component
public class NotificationListener {

    @JmsListener(destination = "notifications", concurrency = "3-5")
    public void onMessage(Notification notification) {
        System.out.println("Received: " + notification);
    }

    @JmsListener(destination = "orders", selector = "priority = 'HIGH'")
    public void onHighPriorityOrder(Order order) {
        // Only receives messages where JMS property priority='HIGH'
    }
}

4.15 REST Client (WebClient and RestClient)

RestClient (Blocking — Spring Framework 7.x)

RestClient client = RestClient.builder()
    .baseUrl("https://api.example.com")
    .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token)
    .build();

// GET
User user = client.get()
    .uri("/users/{id}", userId)
    .retrieve()
    .body(User.class);

// POST
Order created = client.post()
    .uri("/orders")
    .body(orderRequest)
    .retrieve()
    .body(Order.class);

WebClient (Reactive)

WebClient client = WebClient.builder()
    .baseUrl("https://api.example.com")
    .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token)
    .build();

// GET
Mono<User> user = client.get()
    .uri("/users/{id}", userId)
    .retrieve()
    .bodyToMono(User.class);

// POST
Mono<Order> created = client.post()
    .uri("/orders")
    .bodyValue(orderRequest)
    .retrieve()
    .bodyToMono(Order.class);

// Error handling
Mono<User> userWithErrors = client.get()
    .uri("/users/{id}", userId)
    .retrieve()
    .onStatus(HttpStatusCode::is4xxClientError,
        resp -> Mono.error(new UserNotFoundException(userId)))
    .bodyToMono(User.class);

// Streaming
Flux<ServerSentEvent<String>> events = client.get()
    .uri("/events/stream")
    .retrieve()
    .bodyToFlux(new ParameterizedTypeReference<ServerSentEvent<String>>() {});

4.16 HTTP Interface Clients (Declarative)

public interface UserClient {

    @GetExchange("/users/{id}")
    User getUser(@PathVariable Long id);

    @PostExchange("/users")
    User createUser(@RequestBody UserRequest request);

    @GetExchange("/users")
    List<User> listUsers(@RequestParam("role") String role);

    @DeleteExchange("/users/{id}")
    void deleteUser(@PathVariable Long id);
}

@Configuration
public class ClientConfig {

    @Bean
    public UserClient userClient(RestClient.Builder builder) {
        RestClient restClient = builder.baseUrl("https://api.example.com").build();
        return HttpServiceProxyFactory
            .builderFor(RestClientAdapter.create(restClient))
            .build()
            .createClient(UserClient.class);
    }
}

4.17 Testing

Unit Testing with MockMvc

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private OrderService orderService;

    @Test
    void shouldReturnOrder() throws Exception {
        when(orderService.findById(1L)).thenReturn(Optional.of(new Order(1L, "Alice", BigDecimal.TEN)));

        mockMvc.perform(get("/api/orders/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.customer").value("Alice"))
            .andExpect(jsonPath("$.total").value(10));
    }

    @Test
    void shouldReturn404WhenNotFound() throws Exception {
        when(orderService.findById(999L)).thenReturn(Optional.empty());

        mockMvc.perform(get("/api/orders/999"))
            .andExpect(status().isNotFound());
    }

    @Test
    void shouldValidateInput() throws Exception {
        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"customer\":\"\",\"total\":-1}"))
            .andExpect(status().isBadRequest());
    }
}

Integration Testing

@SpringJUnitConfig(AppConfig.class)
@Transactional
class OrderServiceIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Test
    void shouldCreateAndRetrieveOrder() {
        Order order = orderService.create(new OrderRequest("Bob", BigDecimal.valueOf(99.99)));
        assertThat(order.getId()).isNotNull();

        Order found = orderService.findById(order.getId()).orElseThrow();
        assertThat(found.getCustomer()).isEqualTo("Bob");
    }
}

WebTestClient (Reactive)

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class ProductControllerTest {

    @Autowired
    private WebTestClient webTestClient;

    @Test
    void shouldListProducts() {
        webTestClient.get().uri("/api/products")
            .exchange()
            .expectStatus().isOk()
            .expectBodyList(Product.class)
            .hasSize(3);
    }

    @Test
    void shouldStreamProducts() {
        webTestClient.get().uri("/api/products/stream")
            .accept(MediaType.TEXT_EVENT_STREAM)
            .exchange()
            .expectStatus().isOk()
            .expectBodyList(Product.class)
            .hasSize(10);
    }
}

Bean Override in Tests

@SpringJUnitConfig(AppConfig.class)
class ServiceTest {

    @MockitoBean
    private ExternalApiClient apiClient;

    @Autowired
    private MyService myService;

    @Test
    void shouldHandleApiFailure() {
        when(apiClient.fetchData()).thenThrow(new RuntimeException("API down"));
        assertThatThrownBy(() -> myService.process())
            .isInstanceOf(ProcessingException.class);
    }
}

Tip

@MockitoBean (Spring Framework 7.x) replaces the older @MockBean from Spring Boot. It creates a Mockito mock and registers it as a bean override in the test application context.

SQL Test Scripts

@SpringJUnitConfig(TestConfig.class)
@Sql(scripts = "classpath:test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Transactional
class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    void shouldFindUsers() {
        List<User> admins = userService.findByRole("ADMIN");
        assertThat(admins).hasSize(2);
    }
}

Test Context Configuration Annotations

Annotation Purpose
@SpringJUnitConfig Combines @ExtendWith(SpringExtension.class) + @ContextConfiguration
@WebMvcTest Slice test for MVC controllers
@Transactional Auto-rollback after each test
@Sql Execute SQL scripts before/after tests
@ActiveProfiles Activate profiles for test context
@TestPropertySource Override properties for tests
@DirtiesContext Force context recreation (use sparingly)
@MockitoBean Replace a bean with a Mockito mock
@MockitoSpyBean Wrap a bean with a Mockito spy

4.18 Kotlin Support

Spring Framework provides first-class Kotlin support with extensions and DSLs:

Routing DSL (WebFlux):

@Configuration
class AppConfig {

    @Bean
    fun router(handler: UserHandler) = coRouter {
        GET("/api/users", handler::listAll)
        GET("/api/users/{id}", handler::getById)
        POST("/api/users", handler::create)
    }
}

Bean definition DSL:

val beans = beans {
    bean<UserRepository>()
    bean<UserService>()
    bean {
        UserHandler(ref())
    }
}

JdbcTemplate extensions:

val count = jdbcTemplate.queryForObject<Int>("SELECT COUNT(*) FROM users")

R2DBC Coroutine extensions:

@Repository
class UserRepository(private val client: DatabaseClient) {

    suspend fun findById(id: Long): User? =
        client.sql("SELECT * FROM users WHERE id = :id")
            .bind("id", id)
            .map { row -> User(row.get("id", Long::class.java)!!, row.get("name", String::class.java)!!) }
            .awaitOneOrNull()

    fun findAll(): Flow<User> =
        client.sql("SELECT * FROM users")
            .map { row -> User(row.get("id", Long::class.java)!!, row.get("name", String::class.java)!!) }
            .flow()
}

4.19 Internationalization (i18n)

@Configuration
public class I18nConfig {

    @Bean
    public MessageSource messageSource() {
        var source = new ReloadableResourceBundleMessageSource();
        source.setBasename("classpath:messages");
        source.setDefaultEncoding("UTF-8");
        return source;
    }

    @Bean
    public LocaleResolver localeResolver() {
        var resolver = new SessionLocaleResolver();
        resolver.setDefaultLocale(Locale.ENGLISH);
        return resolver;
    }
}

Usage:

@Autowired
private MessageSource messageSource;

public String getGreeting(Locale locale) {
    return messageSource.getMessage("greeting", null, locale);
}

4.20 Resource Abstraction

@Component
public class ResourceReader {

    @Value("classpath:data/config.json")
    private Resource configResource;

    public String readConfig() throws IOException {
        return new String(configResource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
    }
}

Resource prefixes:

Prefix Example Resolves To
classpath: classpath:config/app.yaml Classpath resource
file: file:/opt/config/app.yaml Filesystem path
https: https://example.com/config.yaml URL resource
(none) config/app.yaml Depends on ApplicationContext type

5. Common Tasks

Create a REST API with JSON Support

Goal: Build a complete CRUD REST API with Jackson JSON serialization.

Prerequisites: spring-webmvc dependency, a servlet container

Steps:

  1. Create a @Configuration class with @EnableWebMvc and @ComponentScan
  2. Define your domain model (POJO with getters/setters or Java record)
  3. Create a @RestController with @GetMapping, @PostMapping, @PutMapping, @DeleteMapping
  4. Add @Valid on @RequestBody parameters for input validation
  5. Create a @RestControllerAdvice for global exception handling

Verify: Send HTTP requests with curl:

curl -X POST http://localhost:8080/api/items \
  -H "Content-Type: application/json" \
  -d '{"name":"Widget","price":9.99}'
{"id":1,"name":"Widget","price":9.99}

If it fails: Check that Jackson (com.fasterxml.jackson.core:jackson-databind) is on the classpath — it's auto-detected for JSON message conversion.


Set Up Database Access with JdbcTemplate

Goal: Query and update a relational database without ORM overhead.

Steps:

  1. Add spring-jdbc dependency and a JDBC driver (e.g., org.postgresql:postgresql)
  2. Configure a DataSource bean (e.g., HikariCP: com.zaxxer:HikariCP)
  3. Create @Repository classes that accept DataSource and create JdbcTemplate
  4. Use query(), queryForObject(), update(), batchUpdate() for SQL operations

Verify:

int count = jdbc.queryForObject("SELECT COUNT(*) FROM users", Integer.class);
System.out.println("Users: " + count);
Users: 42

Configure Declarative Transactions

Goal: Wrap service methods in database transactions automatically.

Steps:

  1. Add @EnableTransactionManagement to your configuration
  2. Define a PlatformTransactionManager bean (e.g., DataSourceTransactionManager)
  3. Annotate service methods or classes with @Transactional
  4. Use readOnly = true for read-only methods as an optimization

If it fails: Ensure you are calling @Transactional methods through the Spring proxy (from outside the class), not via this.method().


Add WebSocket Real-Time Messaging

Goal: Enable STOMP-over-WebSocket for browser push notifications.

Steps:

  1. Add spring-websocket and spring-messaging dependencies
  2. Create a @Configuration class implementing WebSocketMessageBrokerConfigurer
  3. Enable a simple broker (/topic, /queue) and set application destination prefix (/app)
  4. Register a STOMP endpoint with optional SockJS fallback
  5. Create @Controller methods with @MessageMapping and @SendTo

Verify: Connect from a browser using SockJS + STOMP.js, subscribe to /topic/messages, and send a message to /app/chat.send.


Build a Reactive Application with WebFlux

Goal: Create a non-blocking web application using reactive streams.

Steps:

  1. Add spring-webflux and io.projectreactor.netty:reactor-netty dependencies
  2. Create @RestController classes returning Mono<T> and Flux<T>
  3. Use WebClient for non-blocking HTTP calls to external services
  4. For database access, use spring-r2dbc with DatabaseClient

Warning

Do not mix blocking calls (JDBC, Thread.sleep(), synchronous I/O) in a reactive pipeline. This blocks the event loop and degrades performance. Use Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic()) if you must call blocking code.


Set Up Method-Level Caching

Goal: Cache expensive method results automatically.

Steps:

  1. Add @EnableCaching to your configuration
  2. Define a CacheManager bean (e.g., ConcurrentMapCacheManager for development, Caffeine or Redis for production)
  3. Annotate methods with @Cacheable("cacheName") — the cache key defaults to the method parameters
  4. Use @CacheEvict to invalidate entries, @CachePut to update without skipping execution

Run Database Scripts on Startup

Goal: Initialize or migrate database schema on application startup.

Using Java configuration:

@Bean
public DataSourceInitializer dataSourceInitializer(DataSource ds) {
    var initializer = new DataSourceInitializer();
    initializer.setDataSource(ds);
    var populator = new ResourceDatabasePopulator();
    populator.addScript(new ClassPathResource("schema.sql"));
    populator.addScript(new ClassPathResource("data.sql"));
    initializer.setDatabasePopulator(populator);
    return initializer;
}

Using XML (<jdbc:initialize-database>):

<jdbc:initialize-database data-source="dataSource">
    <jdbc:script location="classpath:schema.sql"/>
    <jdbc:script location="classpath:data.sql"/>
</jdbc:initialize-database>

The <jdbc:initialize-database> element supports ignore-failures (NONE, DROPS, ALL) and per-script separator attributes (confirmed from DatabasePopulatorConfigUtils source).


Implement Global CORS Configuration

Goal: Allow cross-origin requests for your API.

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://app.example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600);
    }
}

Or per-controller: @CrossOrigin(origins = "https://app.example.com").


Set Up JMS Message Processing

Goal: Send and receive messages via JMS (e.g., ActiveMQ, RabbitMQ with JMS).

Steps:

  1. Add spring-jms and your JMS provider client library
  2. Define a ConnectionFactory bean
  3. Create @EnableJms configuration with a DefaultJmsListenerContainerFactory
  4. Use JmsTemplate for sending, @JmsListener for receiving
  5. Configure concurrency for listener containers: factory.setConcurrency("3-10")

Test with Embedded Database

Goal: Run integration tests against a fresh database per test.

@SpringJUnitConfig(TestConfig.class)
@Sql(scripts = "classpath:test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "classpath:cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
@Transactional
class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    void shouldFindUsersByRole() {
        List<User> admins = userService.findByRole("ADMIN");
        assertThat(admins).hasSize(2);
    }
}

Tip

@Transactional on test classes causes automatic rollback after each test, keeping the database clean without explicit cleanup scripts.


Create Custom Annotations with AOP

Goal: Build a reusable @RateLimit annotation that throttles method calls.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int maxCalls() default 100;
    int periodSeconds() default 60;
}

@Aspect
@Component
public class RateLimitAspect {

    private final Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();

    @Around("@annotation(rateLimit)")
    public Object enforce(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
        String key = pjp.getSignature().toShortString();
        AtomicInteger counter = counters.computeIfAbsent(key, k -> new AtomicInteger(0));
        if (counter.incrementAndGet() > rateLimit.maxCalls()) {
            throw new RateLimitExceededException("Rate limit exceeded for " + key);
        }
        return pjp.proceed();
    }
}

Use HTTP Interface Clients for External APIs

Goal: Create type-safe declarative HTTP clients.

Steps:

  1. Define a Java interface with @GetExchange, @PostExchange, etc.
  2. Create a RestClient or WebClient bean with base URL and default headers
  3. Use HttpServiceProxyFactory to generate a proxy implementation
  4. Inject the interface and call methods as if they were local

Verify: Call userClient.getUser(1L) and inspect the returned object.


Implement Event-Driven Architecture

Goal: Decouple components using application events.

Steps:

  1. Create event classes (extend ApplicationEvent or use any POJO)
  2. Inject ApplicationEventPublisher and call publishEvent()
  3. Create listeners with @EventListener on any Spring-managed bean
  4. For async processing, add @Async and ensure @EnableAsync is configured
  5. For transaction-aware listeners, use @TransactionalEventListener

Expose JMX MBeans

Goal: Monitor and manage application beans via JMX.

<context:mbean-export/>
<context:mbean-server/>

Or annotation-based:

@ManagedResource(description = "Cache statistics")
@Component
public class CacheStats {

    @ManagedAttribute(description = "Cache hit count")
    public long getHitCount() { return hitCount.get(); }

    @ManagedOperation(description = "Clear all caches")
    public void clearAll() { /* ... */ }
}

6. Troubleshooting

Diagnostic Decision Tree

Application won't start
├── ClassNotFoundException / NoClassDefFoundError
│   → Check dependency versions, use BOM
├── BeanCreationException
│   ├── No qualifying bean
│   │   → Check @ComponentScan base packages
│   │   → Check bean is annotated with @Component/@Service/@Repository
│   └── Circular dependency
│       → Refactor to break cycle (use @Lazy, setter injection, or events)
├── Port already in use
│   → Kill existing process or change server port
└── Configuration error
    → Check XML syntax, annotation placement, property file location

Web request fails
├── 404 Not Found
│   → Check @RequestMapping paths, controller is component-scanned
├── 405 Method Not Allowed
│   → Check HTTP method matches (@GetMapping vs @PostMapping)
├── 415 Unsupported Media Type
│   → Check Content-Type header matches, message converter available
├── 400 Bad Request
│   → Check @Valid constraints, request body format
└── 500 Internal Server Error
    → Check exception handler, enable debug logging

Error: NoSuchBeanDefinitionException: No qualifying bean of type 'X' available

Cause: The requested bean is not registered in the application context. Common reasons: missing @Component/@Service/@Repository annotation, class not in a @ComponentScan base package, or a conditional (@Profile, @Conditional) excluded it.

Solution:

  1. Verify the class is annotated with a stereotype annotation
  2. Check @ComponentScan base packages include the class's package
  3. Check active profiles match any @Profile annotations
  4. Check @Conditional annotations and their conditions

Prevention: Use explicit @Import for configuration classes outside the component scan path.


Error: BeanCurrentlyInCreationException: Error creating bean ... Requested bean is currently in creation

Cause: Circular dependency — Bean A needs Bean B, and Bean B needs Bean A during construction.

Solution:

  1. Refactor to break the cycle (preferred)
  2. Use @Lazy on one dependency to defer proxy creation
  3. Switch one injection point from constructor to setter injection
  4. Extract shared logic into a third bean

Prevention: Design dependencies in a directed acyclic graph. Use events for cross-cutting communication.


Error: HttpMessageNotReadableException: Required request body is missing

Cause: The client sent a request without a body, or the Content-Type header is missing/incorrect.

Solution:

  1. Ensure the client sends Content-Type: application/json header
  2. Ensure the request body is valid JSON matching the expected type
  3. Check that Jackson is on the classpath

Error: MethodArgumentNotValidException (400 Bad Request)

Cause: The @Valid annotation triggered bean validation, and the input failed one or more constraints.

Solution: Read the BindingResult field errors to identify which fields failed. Fix the client input to meet the constraints.


Error: TransactionRequiredException: No EntityManager with actual transaction available

Cause: A JPA operation requiring a transaction was called outside of one.

Solution:

  1. Add @Transactional to the calling method
  2. Ensure @EnableTransactionManagement is present
  3. Ensure the method is called through the Spring proxy (not this.method())

Error: IllegalStateException: No thread-bound request found

Cause: Code is trying to access request-scoped beans or RequestContextHolder from a non-request thread (e.g., @Async method, scheduled task).

Solution: Pass needed request data as method parameters instead of accessing RequestContextHolder. For request-scoped beans, use ObjectProvider<T> and resolve within the request thread.


Error: java.lang.ClassCastException: com.sun.proxy.$Proxy... cannot be cast to ...

Cause: Spring created a JDK dynamic proxy (interface-based) but you're casting to the concrete class.

Solution:

  1. Program to the interface, not the implementation
  2. Or force CGLIB proxies: @EnableAspectJAutoProxy(proxyTargetClass = true)

Error: UnsatisfiedDependencyException with generic types

Cause: Spring's type resolution can struggle with complex generic types when multiple candidates exist.

Solution: Use @Qualifier to disambiguate, or define explicit @Bean methods with concrete types.


Error: MockHttpServletRequest.isRequestedSessionIdValid() inconsistency

Cause: Known issue (#36631) where MockHttpServletRequest.isRequestedSessionIdValid() does not follow Servlet 6.1 Javadoc behavior.

Solution: Set the session explicitly on the mock request rather than relying on isRequestedSessionIdValid().


Error: @Sql fails with TransactionAwareDataSourceProxy-wrapped DataSource

Cause: Known issue (#36635) — when a DataSource is wrapped in a TransactionAwareDataSourceProxy, the @Sql annotation may fail to execute scripts.

Solution: Use the unwrapped DataSource for @Sql script execution, or configure the @Sql dataSource attribute to point to the original bean.


Error: Unexpected path element removal when resolving versioned resources

Cause: Known issue (#36699) — the resource resolver could unexpectedly remove path elements when versioned resources are configured.

Solution: Fixed in the latest patch. Upgrade to the latest Spring Framework 7.0.x release.


Error: setConnectTimeout deprecation on HttpComponentsClientHttpRequestFactory

Cause: #35748 — setConnectTimeout is deprecated in favor of configuring timeouts directly on the HttpClient builder.

Solution:

var requestConfig = RequestConfig.custom()
    .setConnectTimeout(Timeout.ofSeconds(5))
    .build();
var httpClient = HttpClientBuilder.create()
    .setDefaultRequestConfig(requestConfig)
    .build();
var factory = new HttpComponentsClientHttpRequestFactory(httpClient);

Error: Non-deterministic "Body token not expected" in multipart requests

Cause: Known issue (#36694) — the reactive multipart parser (PartGenerator) can produce non-deterministic errors under concurrent load.

Solution: Ensure you are not reading the request body multiple times. Use @RequestPart annotations correctly in WebFlux controllers.


Error: Parent traceId not reused with WebClient.awaitExchange in Kotlin coroutines

Cause: Known issue (#36182) — the parent trace ID may not propagate correctly when using WebClient.awaitExchange() with Kotlin coroutines.

Solution: Use ContextPropagatingTaskDecorator to ensure context propagation across async boundaries.


Error: JmsTemplate retries cause null cause in Dead Letter Queue messages

Cause: Known issue (#22817) — when using DefaultMessageListenerContainer with retries and ActiveMQ, the exception cause may be null in DLQ messages.

Solution: Implement a custom ErrorHandler on the listener container to preserve exception details before message redelivery.


Error: Spring Framework 7.0.5 upgrade breaks certification with application servers

Cause: Known issue (#36617) — upgrading to Spring Framework 7.0.5 may break certification with certain Jakarta EE application servers.

Solution: Check the issue tracker for the specific application server compatibility matrix. Pin to the last compatible version if needed.

Caution

Always test framework upgrades in a staging environment before deploying to production. Spring Framework minor releases may contain behavioral changes that affect application server compatibility.


Error: @AliasFor behavior inconsistency

Cause: Known issue (#36680) — @AliasFor may exhibit inconsistent behavior in certain annotation composition scenarios.

Solution: When using composed annotations, verify that aliased attributes resolve correctly. Test with explicit attribute values rather than relying on defaults.


Error: Nullable value classes as query parameters in reactive stack

Cause: Known issue (#36704) — nullable Kotlin value classes used as query parameters in the reactive web stack may cause errors.

Solution: Use standard wrapper types (String?, Int?) instead of value classes for query parameters until this is resolved.


7. FAQ

General

Q: What is the difference between Spring Framework and Spring Boot?

Spring Framework is the core library providing IoC, AOP, web, data access, and testing infrastructure. Spring Boot is an opinionated layer on top that provides auto-configuration, embedded servers, and starter dependencies. Spring Boot uses Spring Framework under the hood — they are complementary, not alternatives.

Q: Which Java version should I use with Spring Framework 7.x?

JDK 17 is the minimum supported version. JDK 21+ is recommended for virtual threads support, pattern matching, and other modern language features. The build system supports testing up to JDK 24.

Q: Can I use Spring Framework without Spring Boot?

Yes. Spring Framework is a standalone library. You manage your own configuration, dependency injection, and server setup. This is common for embedded library use cases, servlet-based deployments to external containers, and projects with custom bootstrapping requirements.

Q: Does Spring Framework support Kotlin?

Yes, with first-class support. Spring provides Kotlin extension functions for JdbcTemplate, RestTemplate, WebClient, DatabaseClient, and more. It supports Kotlin coroutines in WebFlux, bean definition DSLs, and router function DSLs. Kotlin nullability annotations are respected by Spring's dependency injection.

Q: What is the relationship between Spring Framework version numbers and Spring Boot?

Spring Boot 3.x uses Spring Framework 6.x. Spring Boot 4.x uses Spring Framework 7.x. The version numbers don't align — check the Spring Boot release notes for the exact Spring Framework version included.

Configuration

Q: How do I switch between development and production configurations?

Use Spring profiles. Define @Profile("dev") and @Profile("prod") beans, then activate with spring.profiles.active=prod as a system property, environment variable, or programmatically.

Q: Can I mix annotation and XML configuration?

Yes. Use @ImportResource("classpath:beans.xml") to import XML from annotation config, or <context:annotation-config/> to enable annotation processing in XML config.

Q: How do I load properties from multiple files?

Use multiple @PropertySource annotations or PropertySourcesPlaceholderConfigurer with an array of locations. Properties from later sources override earlier ones.

Q: What XML namespace handlers are available?

The source confirms these namespace handlers: context: (property-placeholder, component-scan, annotation-config, load-time-weaver, spring-configured, mbean-export, mbean-server, property-override), aop:, tx:, mvc:, jdbc: (embedded-database, initialize-database), jms:, oxm:, websocket:, cache:, task:. See the Configuration section for the full table.

Web

Q: Should I use Spring MVC or Spring WebFlux?

Use Spring MVC for traditional blocking applications, especially with JDBC/JPA databases. Use Spring WebFlux when you need non-blocking I/O, high-concurrency streaming, or are building on reactive data stores (R2DBC, MongoDB Reactive). Both support annotation-based controllers — the programming model is similar.

Q: How do I serve static files (CSS, JS, images)?

Configure resource handlers in your WebMvcConfigurer:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/static/**")
        .addResourceLocations("classpath:/static/");
}

Q: How do I handle file uploads?

Use @RequestParam("file") MultipartFile file in MVC controllers, or @RequestPart("file") Mono<FilePart> file in WebFlux controllers. Configure MultipartResolver bean for maximum file size limits.

Q: What is the new API versioning feature in Spring Framework 7.0?

Spring Framework 7.0 introduced ApiVersionConfigurer which supports extracting API versions from request headers, query parameters, URL path segments, and media type parameters. It uses SemanticApiVersionParser by default and allows setting default versions and marking versions as deprecated.

Q: How do I use RestClient vs WebClient?

RestClient is the blocking/synchronous HTTP client (new in Spring Framework 6.1+, replacing RestTemplate). WebClient is the reactive/non-blocking HTTP client for WebFlux applications. Use RestClient for traditional apps, WebClient for reactive apps.

Data Access

Q: When should I use JdbcTemplate vs JPA/Hibernate?

Use JdbcTemplate when you want full SQL control, lightweight mapping, and minimal overhead. Use JPA/Hibernate when you need complex object graphs, lazy loading, change tracking, and database portability.

Q: How do I use multiple DataSources?

Define multiple DataSource beans with @Bean, mark one as @Primary, and use @Qualifier to inject the specific one. Each needs its own PlatformTransactionManager.

Q: What is R2DBC and when should I use it?

R2DBC (Reactive Relational Database Connectivity) is a non-blocking database access API. Use it in Spring WebFlux applications where you need end-to-end reactive pipelines. Spring's DatabaseClient provides a fluent API similar to JdbcTemplate but returning Mono/Flux types.

Testing

Q: How do I speed up integration tests?

Spring's test framework caches application contexts across tests with the same configuration. Keep test configurations consistent to maximize cache hits. Use @DirtiesContext sparingly — it forces context recreation.

Q: What is @MockitoBean and how does it differ from @MockBean?

@MockitoBean is the Spring Framework 7.x native annotation that replaces Spring Boot's @MockBean. It creates a Mockito mock and registers it as a bean override in the test application context. Unlike @MockBean, it doesn't require Spring Boot on the classpath.

Q: How do I test async methods?

Use Awaitility or CompletableFuture.get() with a timeout. For @Async methods, ensure the test configuration includes a TaskExecutor and the method returns CompletableFuture<T>.

Q: Can I use strict stubbing with @MockitoBean?

Feature request #34419 tracks support for strict stubbing with @MockitoBean and @MockitoSpyBean. Currently, you configure strict stubbing globally via Mockito settings.

Performance

Q: How do I enable virtual threads with Spring Framework?

On JDK 21+, configure a virtual thread executor:

@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
    return new SimpleAsyncTaskExecutor(Thread.ofVirtual().factory());
}

Q: How do I reduce application startup time?

  1. Use spring-context-indexer to pre-compute component scan results at compile time
  2. Use @Lazy on beans that aren't needed immediately
  3. Minimize component scan base packages
  4. Consider GraalVM native image compilation for ahead-of-time processing

Q: How does the context indexer work?

Add spring-context-indexer as an annotation processor. At compile time, it scans for stereotype annotations and writes META-INF/spring.components. At runtime, Spring reads this index instead of classpath scanning, significantly reducing startup time for large applications.

Q: How do I configure thread pool sizes for @Async and @Scheduled?

Define a ThreadPoolTaskExecutor bean with corePoolSize, maxPoolSize, and queueCapacity. For scheduling, define a ThreadPoolTaskScheduler with a poolSize. See the Task Execution and Scheduling section for code examples.