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 alljavax.*imports tojakarta.*.
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-webmvcpulls inspring-web,spring-context,spring-aop,spring-beans,spring-core, andspring-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.
Annotation-Based Configuration (Recommended)
@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
@Profileannotation are always active. If you define aDataSourcein both a profiled and un-profiled config, you may get duplicate bean conflicts. Use@Primaryor 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,
@Autowiredis 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
privateorfinalmethods, use full AspectJ weaving viaspring-aspectsand 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
@Asyncevent listeners require@EnableAsyncon a configuration class and a configuredTaskExecutor.@TransactionalEventListenerrequires 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
@EnableWebMvctakes full control of Spring MVC configuration. If using Spring Boot, do NOT use@EnableWebMvc— Boot provides its own auto-configuration that@EnableWebMvcwill 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-postgresqlfor PostgreSQL,io.r2dbc:r2dbc-h2for H2). Kotlin coroutine extensions are available inspring-r2dbc/src/main/kotlin/.
4.9 Transaction Management
Declarative Transactions (Recommended)
@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
@Transactionalonly works on public methods called from outside the class. Self-invocation (calling a@Transactionalmethod from within the same class) bypasses the proxy and the transaction is not applied. UseTransactionTemplatefor 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
ContextPropagatingTaskDecoratorto 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 aLocalValidatorFactoryBean.
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@MockBeanfrom 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:
- Create a
@Configurationclass with@EnableWebMvcand@ComponentScan - Define your domain model (POJO with getters/setters or Java record)
- Create a
@RestControllerwith@GetMapping,@PostMapping,@PutMapping,@DeleteMapping - Add
@Validon@RequestBodyparameters for input validation - Create a
@RestControllerAdvicefor 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:
- Add
spring-jdbcdependency and a JDBC driver (e.g.,org.postgresql:postgresql) - Configure a
DataSourcebean (e.g., HikariCP:com.zaxxer:HikariCP) - Create
@Repositoryclasses that acceptDataSourceand createJdbcTemplate - 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:
- Add
@EnableTransactionManagementto your configuration - Define a
PlatformTransactionManagerbean (e.g.,DataSourceTransactionManager) - Annotate service methods or classes with
@Transactional - Use
readOnly = truefor 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:
- Add
spring-websocketandspring-messagingdependencies - Create a
@Configurationclass implementingWebSocketMessageBrokerConfigurer - Enable a simple broker (
/topic,/queue) and set application destination prefix (/app) - Register a STOMP endpoint with optional SockJS fallback
- Create
@Controllermethods with@MessageMappingand@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:
- Add
spring-webfluxandio.projectreactor.netty:reactor-nettydependencies - Create
@RestControllerclasses returningMono<T>andFlux<T> - Use
WebClientfor non-blocking HTTP calls to external services - For database access, use
spring-r2dbcwithDatabaseClient
Warning
Do not mix blocking calls (JDBC,
Thread.sleep(), synchronous I/O) in a reactive pipeline. This blocks the event loop and degrades performance. UseMono.fromCallable(...).subscribeOn(Schedulers.boundedElastic())if you must call blocking code.
Set Up Method-Level Caching
Goal: Cache expensive method results automatically.
Steps:
- Add
@EnableCachingto your configuration - Define a
CacheManagerbean (e.g.,ConcurrentMapCacheManagerfor development, Caffeine or Redis for production) - Annotate methods with
@Cacheable("cacheName")— the cache key defaults to the method parameters - Use
@CacheEvictto invalidate entries,@CachePutto 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:
- Add
spring-jmsand your JMS provider client library - Define a
ConnectionFactorybean - Create
@EnableJmsconfiguration with aDefaultJmsListenerContainerFactory - Use
JmsTemplatefor sending,@JmsListenerfor receiving - 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
@Transactionalon 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:
- Define a Java interface with
@GetExchange,@PostExchange, etc. - Create a
RestClientorWebClientbean with base URL and default headers - Use
HttpServiceProxyFactoryto generate a proxy implementation - 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:
- Create event classes (extend
ApplicationEventor use any POJO) - Inject
ApplicationEventPublisherand callpublishEvent() - Create listeners with
@EventListeneron any Spring-managed bean - For async processing, add
@Asyncand ensure@EnableAsyncis configured - 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:
- Verify the class is annotated with a stereotype annotation
- Check
@ComponentScanbase packages include the class's package - Check active profiles match any
@Profileannotations - Check
@Conditionalannotations 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:
- Refactor to break the cycle (preferred)
- Use
@Lazyon one dependency to defer proxy creation - Switch one injection point from constructor to setter injection
- 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:
- Ensure the client sends
Content-Type: application/jsonheader - Ensure the request body is valid JSON matching the expected type
- 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:
- Add
@Transactionalto the calling method - Ensure
@EnableTransactionManagementis present - 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:
- Program to the interface, not the implementation
- 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?
- Use
spring-context-indexerto pre-compute component scan results at compile time - Use
@Lazyon beans that aren't needed immediately - Minimize component scan base packages
- 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.