Spring MVC
DispatcherServlet, @RestController, request mapping, validation, exception handling
Spring MVC
Spring MVC is a synchronous, request-response web framework built on the Servlet API, organized around the DispatcherServlet.
1. Definition
Spring MVC (Model-View-Controller) is the web module of the Spring Framework, built on the jakarta.servlet.http.HttpServlet API. The central element is the DispatcherServlet, which implements the Front Controller pattern: every HTTP request arrives at a single entry point, and the framework routes it to the appropriate handler method based on the URL, HTTP method, and other conditions.
Spring MVC is synchronous and blocking by default: each request occupies a servlet thread for the entire duration of processing. Spring Boot automatically configures the embedded Tomcat/Jetty/Undertow server and the DispatcherServlet.
HTTP Request â DispatcherServlet â HandlerMapping â HandlerAdapter
â Controller â Service â Repository â Response
2. Core Concepts
DispatcherServlet architecture
The DispatcherServlet coordinates the following components:
| Component | Role |
|---|---|
| HandlerMapping | URL â Controller method resolution |
| HandlerAdapter | Invoking the controller method |
| HttpMessageConverter | Request/response body conversion (JSON, XML) |
| ViewResolver | View name â template resolution (Thymeleaf, JSP) |
| HandlerExceptionResolver | Exception handling |
| LocaleResolver | Locale resolution |
| MultipartResolver | File upload handling |
Controller annotations
@Controller // View-returning controller
@RestController // = @Controller + @ResponseBody (JSON/XML)
@RequestMapping // Class or method level URL mapping
@GetMapping // HTTP GET shortcut
@PostMapping // HTTP POST shortcut
@PutMapping // HTTP PUT shortcut
@DeleteMapping // HTTP DELETE shortcut
@PatchMapping // HTTP PATCH shortcut
Parameter binding
@PathVariable // URL path variable: /users/{id}
@RequestParam // Query parameter: ?name=John
@RequestBody // HTTP body â Java object (deserialization)
@RequestHeader // HTTP header values
@CookieValue // Cookie values
@ModelAttribute // Form data â Java object
@RequestPart // Multipart file
Request processing flow
- HTTP request arrives at the DispatcherServlet
- HandlerMapping finds the matching controller method
- HandlerInterceptor.preHandle() executes
- HandlerAdapter invokes the controller method
- HttpMessageConverter deserializes the request body
- Controller executes business logic
- HttpMessageConverter serializes the response body
- HandlerInterceptor.postHandle() executes
- Response is sent back to the client
3. Practical Usage
REST API controller
@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<UserDto> findAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return userService.findAll(PageRequest.of(page, size));
}
@GetMapping("/{id}")
public UserDto findById(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserDto create(@Valid @RequestBody CreateUserRequest request) {
return userService.create(request);
}
@PutMapping("/{id}")
public UserDto update(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return userService.update(id, request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
userService.delete(id);
}
}
Bean Validation integration
public record CreateUserRequest(
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
String name,
@Email(message = "Invalid email format")
@NotBlank
String email,
@Min(18) @Max(150)
int age
) {}
@Valid on the controller parameter activates validation. If validation fails, a MethodArgumentNotValidException is thrown.
Global exception handling
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(EntityNotFoundException ex) {
return new ErrorResponse("NOT_FOUND", ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.toList();
return new ErrorResponse("VALIDATION_FAILED", errors.toString());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGeneral(Exception ex) {
return new ErrorResponse("INTERNAL_ERROR", "Something went wrong");
}
}
public record ErrorResponse(String code, String message) {}
4. Code Examples
Content negotiation
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE})
public List<Product> findAll() {
return productService.findAll();
}
}
Based on the Accept header, Spring selects the appropriate HttpMessageConverter.
Custom HttpMessageConverter
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(
List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJackson2HttpMessageConverter(
new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
));
}
}
Async controller (DeferredResult)
@GetMapping("/async/{id}")
public DeferredResult<UserDto> findByIdAsync(@PathVariable Long id) {
DeferredResult<UserDto> result = new DeferredResult<>(5000L);
executorService.submit(() -> {
try {
UserDto user = userService.findById(id);
result.setResult(user);
} catch (Exception e) {
result.setErrorResult(e);
}
});
return result;
}
DeferredResult releases the servlet thread while processing continues in the background.
ResponseEntity fine-tuning
@GetMapping("/{id}")
public ResponseEntity<UserDto> findById(@PathVariable Long id) {
return userService.findOptional(id)
.map(user -> ResponseEntity.ok()
.header("X-Custom", "value")
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
.body(user))
.orElse(ResponseEntity.notFound().build());
}
5. Trade-offs
Spring MVC vs WebFlux
| Aspect | Spring MVC | WebFlux |
|---|---|---|
| I/O model | Blocking (thread-per-request) | Non-blocking (event loop) |
| Threads | ~200 Tomcat threads (default) | Few event loop threads |
| Scaling | Vertical (more threads) | Horizontal (fewer resources) |
| Debugging | Simple stack traces | Reactive chains harder |
| Ecosystem | Full (JPA, JDBC, etc.) | Limited (R2DBC, WebClient) |
| Learning curve | Low | High |
When to use Spring MVC
- CRUD applications with traditional databases (JPA/JDBC)
- Simple REST APIs with moderate concurrent requests
- Team unfamiliar with reactive programming
- Using synchronous third-party libraries
When NOT to use Spring MVC
- 10,000+ simultaneous connections (WebSocket, SSE)
- Microservice gateway with high throughput
- Streaming processing (Kafka, event sourcing)
6. Common Mistakes
â Business logic in controllers
// BAD: controller contains business logic
@PostMapping
public UserDto create(@RequestBody CreateUserRequest req) {
if (userRepo.existsByEmail(req.email())) {
throw new DuplicateException("Email exists");
}
User user = new User(req.name(), req.email());
user = userRepo.save(user);
emailService.sendWelcome(user);
return UserDto.from(user);
}
// GOOD: delegate to service layer
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserDto create(@Valid @RequestBody CreateUserRequest req) {
return userService.create(req);
}
â Missing @Valid annotation
// BAD: validation does not execute
@PostMapping
public UserDto create(@RequestBody CreateUserRequest req) { ... }
// GOOD: @Valid activates Bean Validation
@PostMapping
public UserDto create(@Valid @RequestBody CreateUserRequest req) { ... }
â API without exception handler
Validation and business errors leak as stack traces to the client. Always use @RestControllerAdvice!
â Forgetting @ResponseStatus
@PostMapping returns 200 OK by default. For resource creation, @ResponseStatus(HttpStatus.CREATED) is correct.
â Unhandled PathVariable type mismatch
// /api/users/abc â NumberFormatException â 500
// Handle with @ExceptionHandler or use regex:
@GetMapping("/{id:\\d+}")
public UserDto findById(@PathVariable Long id) { ... }
7. Deep Dive
HandlerInterceptor vs Servlet Filter
| Aspect | HandlerInterceptor | Servlet Filter |
|---|---|---|
| Registration | WebMvcConfigurer |
@Component / FilterRegistrationBean |
| Spring context | Accessible (Spring bean) | Only if Spring-managed |
| Execution point | Handler (controller) level | Servlet level (runs earlier) |
| preHandle | Yes | Before doFilter() |
| postHandle | Yes (before response) | No separate phase |
| afterCompletion | Yes (after response) | No separate phase |
HttpMessageConverter internals
Spring selects the converter based on Accept and Content-Type headers:
- Request:
Content-Type: application/jsonâMappingJackson2HttpMessageConverter.read() - Response:
Accept: application/jsonâMappingJackson2HttpMessageConverter.write() - If Jackson is on the classpath, automatic registration occurs
- For XML,
jackson-dataformat-xmldependency is required
Controller advice
@ControllerAdvice(basePackages = "com.example.api")
public class ApiAdvice {
// Applies only to controllers in the api package
@ModelAttribute
public void addCommonAttributes(Model model) {
model.addAttribute("appVersion", "2.0");
}
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(LocalDate.class,
new PropertyEditorSupport() {
@Override
public void setAsText(String text) {
setValue(LocalDate.parse(text));
}
});
}
}
Custom Argument Resolver
public class CurrentUserArgumentResolver
implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter param) {
return param.hasParameterAnnotation(CurrentUser.class);
}
@Override
public Object resolveArgument(MethodParameter param,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
return SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
}
}
8. Interview Questions
What is the role of DispatcherServlet? Front Controller pattern: single entry point, uses HandlerMapping to find the controller method, HandlerAdapter to invoke it.
What is the difference between @Controller and @RestController?
@RestController=@Controller+@ResponseBody.@Controllerreturns a View name (Thymeleaf),@RestControllerreturns a JSON/XML body.How does validation work in Spring MVC?
@Valid/@Validatedactivates Bean Validation (Hibernate Validator). On failure,MethodArgumentNotValidExceptionis thrown.@RestControllerAdvicehandles it globally.What is @RestControllerAdvice? A global exception handler + model attribute + init binder. Combination of
@ControllerAdvice+@ResponseBody.How do you customize the HTTP status code?
@ResponseStatusannotation,ResponseEntity<T>return type, orHttpServletResponse.setStatus().What is the difference between HandlerInterceptor and Servlet Filter? Filter: servlet level, runs without Spring context. Interceptor: Spring-managed, has preHandle/postHandle/afterCompletion lifecycle.
How do you handle file uploads?
@RequestPart MultipartFile file. AMultipartResolverbean is needed (Spring Boot auto-configures it).spring.servlet.multipart.max-file-sizesets the limit.
9. Glossary
| Term | Meaning |
|---|---|
| DispatcherServlet | Front Controller, single entry point for all HTTP requests |
| HandlerMapping | URL â controller method resolution |
| HandlerAdapter | Responsible for invoking the controller method |
| HttpMessageConverter | Java object â HTTP body conversion |
| @RestController | @Controller + @ResponseBody, for JSON/XML APIs |
| @RequestMapping | URL and HTTP method mapping annotation |
| @Valid | Activates Bean Validation on controller parameter |
| @RestControllerAdvice | Global exception handler + model attribute |
| ResponseEntity | HTTP status + headers + body fine-tuning |
| HandlerInterceptor | Spring-level pre/post processing |
| Content negotiation | Accept/Content-Type based format selection |
| DeferredResult | Async response (releases servlet thread) |
10. Cheatsheet
REQUEST LIFECYCLE:
Client â Filter â DispatcherServlet â Interceptor.preHandle()
â HandlerAdapter â Controller â Service â Repository
â Interceptor.postHandle() â Response
ANNOTATIONS:
@RestController JSON/XML API controller
@RequestMapping("/api") Base URL prefix
@GetMapping("/{id}") GET + path variable
@PostMapping POST endpoint
@Valid @RequestBody Body validation
@PathVariable URL segment
@RequestParam Query parameter
@ResponseStatus(201) HTTP status override
EXCEPTION HANDLING:
@RestControllerAdvice Global handler
@ExceptionHandler(X) Handle specific exception
ResponseEntity<Error> Custom response
CONFIGURATION:
WebMvcConfigurer Interceptor, converter, CORS
server.port=8080 Server port
spring.jackson.* JSON serialization settings
TESTING:
@WebMvcTest Controller layer test (slice)
MockMvc HTTP request simulation
@MockBean Service mock
đź Games
10 questions