Master Spring MVC Web Framework
Spring MVC
Spring MVC is Spring's web framework that implements the Model-View-Controller design pattern. It provides a flexible and powerful way to build web applications and RESTful web services. Spring MVC separates the concerns of handling requests, processing business logic, and rendering responses, making applications more maintainable and testable.
MVC Architecture Overview
Spring MVC follows the front controller pattern with DispatcherServlet as the central component:
Request → DispatcherServlet → HandlerMapping → Controller → Service → Repository
↓ ↓
Response ← ViewResolver ← View ← Model ← Controller ← Business Logic
Core Components:
- DispatcherServlet: Front controller that handles all requests
- HandlerMapping: Maps requests to appropriate controllers
- Controller: Handles requests and returns model and view
- ViewResolver: Resolves logical view names to actual views
- View: Renders the model data (JSP, Thymeleaf, JSON, etc.)
Basic Controller Setup
// Simple REST Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
Optional<User> user = userService.findById(id);
return user.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) {
User user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
try {
User updated = userService.updateUser(id, request);
return ResponseEntity.ok(updated);
} catch (UserNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
try {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
} catch (UserNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
}
// Traditional MVC Controller (returns views)
@Controller
@RequestMapping("/web/users")
public class UserWebController {
private final UserService userService;
public UserWebController(UserService userService) {
this.userService = userService;
}
@GetMapping
public String listUsers(Model model) {
List<User> users = userService.findAll();
model.addAttribute("users", users);
return "users/list"; // Returns view name
}
@GetMapping("/{id}")
public String viewUser(@PathVariable Long id, Model model) {
User user = userService.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
model.addAttribute("user", user);
return "users/detail";
}
@GetMapping("/new")
public String newUserForm(Model model) {
model.addAttribute("user", new CreateUserRequest());
return "users/form";
}
@PostMapping
public String createUser(@Valid @ModelAttribute CreateUserRequest request,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
return "users/form";
}
User created = userService.createUser(request);
redirectAttributes.addFlashAttribute("message", "User created successfully");
return "redirect:/web/users/" + created.getId();
}
}
Request Mapping and Parameters
Spring MVC provides flexible request mapping options:
Path Variables and Request Parameters
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
// Path variables
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.findById(id);
}
@GetMapping("/category/{categoryId}/products/{productId}")
public Product getProductInCategory(@PathVariable Long categoryId,
@PathVariable Long productId) {
return productService.findByIdInCategory(productId, categoryId);
}
// Request parameters
@GetMapping
public List<Product> searchProducts(
@RequestParam(required = false) String name,
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "name") String sortBy) {
ProductSearchCriteria criteria = ProductSearchCriteria.builder()
.name(name)
.category(category)
.page(page)
.size(size)
.sortBy(sortBy)
.build();
return productService.search(criteria);
}
// Matrix variables
@GetMapping("/filter/{filters}")
public List<Product> filterProducts(@MatrixVariable Map<String, String> filters) {
return productService.filterProducts(filters);
}
// Usage: /api/products/filter/color=red;size=large;brand=nike
// Headers and cookies
@GetMapping("/recommendations")
public List<Product> getRecommendations(
@RequestHeader("User-Agent") String userAgent,
@CookieValue(name = "userId", required = false) String userId) {
return productService.getRecommendations(userId, userAgent);
}
}
Request Body and Validation
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<Order> createOrder(@Valid @RequestBody CreateOrderRequest request) {
Order order = orderService.createOrder(request);
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}
@PatchMapping("/{id}")
public ResponseEntity<Order> updateOrderStatus(
@PathVariable Long id,
@RequestBody @Valid UpdateOrderStatusRequest request) {
Order updated = orderService.updateStatus(id, request.getStatus());
return ResponseEntity.ok(updated);
}
}
// Request DTOs with validation
public class CreateOrderRequest {
@NotNull(message = "Customer ID is required")
private Long customerId;
@NotEmpty(message = "Order items cannot be empty")
@Valid
private List<OrderItemRequest> items;
@NotBlank(message = "Shipping address is required")
@Size(max = 500, message = "Address cannot exceed 500 characters")
private String shippingAddress;
@Email(message = "Invalid email format")
private String email;
// Getters and setters
public Long getCustomerId() { return customerId; }
public void setCustomerId(Long customerId) { this.customerId = customerId; }
public List<OrderItemRequest> getItems() { return items; }
public void setItems(List<OrderItemRequest> items) { this.items = items; }
public String getShippingAddress() { return shippingAddress; }
public void setShippingAddress(String shippingAddress) { this.shippingAddress = shippingAddress; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
public class OrderItemRequest {
@NotNull(message = "Product ID is required")
private Long productId;
@Min(value = 1, message = "Quantity must be at least 1")
@Max(value = 100, message = "Quantity cannot exceed 100")
private Integer quantity;
// Getters and setters
public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }
public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }
}
Exception Handling
Spring MVC provides powerful exception handling mechanisms:
Controller-Level Exception Handling
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
}
// Controller-specific exception handler
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
"USER_NOT_FOUND",
ex.getMessage(),
System.currentTimeMillis()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException ex) {
ErrorResponse error = new ErrorResponse(
"INVALID_REQUEST",
ex.getMessage(),
System.currentTimeMillis()
);
return ResponseEntity.badRequest().body(error);
}
}
Global Exception Handling
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
logger.warn("User not found: {}", ex.getMessage());
ErrorResponse error = new ErrorResponse(
"USER_NOT_FOUND",
ex.getMessage(),
System.currentTimeMillis()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidationErrors(
MethodArgumentNotValidException ex) {
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
Map<String, String> errors = fieldErrors.stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
(existing, replacement) -> existing
));
ValidationErrorResponse errorResponse = new ValidationErrorResponse(
"VALIDATION_FAILED",
"Request validation failed",
errors,
System.currentTimeMillis()
);
return ResponseEntity.badRequest().body(errorResponse);
}
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ErrorResponse> handleDataIntegrityViolation(
DataIntegrityViolationException ex) {
logger.error("Data integrity violation", ex);
String message = "Data integrity constraint violated";
if (ex.getMessage().contains("unique")) {
message = "Resource already exists";
}
ErrorResponse error = new ErrorResponse(
"DATA_INTEGRITY_VIOLATION",
message,
System.currentTimeMillis()
);
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
logger.error("Unexpected error occurred", ex);
ErrorResponse error = new ErrorResponse(
"INTERNAL_SERVER_ERROR",
"An unexpected error occurred",
System.currentTimeMillis()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
// Error response DTOs
public class ErrorResponse {
private String code;
private String message;
private long timestamp;
public ErrorResponse(String code, String message, long timestamp) {
this.code = code;
this.message = message;
this.timestamp = timestamp;
}
// Getters
public String getCode() { return code; }
public String getMessage() { return message; }
public long getTimestamp() { return timestamp; }
}
public class ValidationErrorResponse extends ErrorResponse {
private Map<String, String> fieldErrors;
public ValidationErrorResponse(String code, String message,
Map<String, String> fieldErrors, long timestamp) {
super(code, message, timestamp);
this.fieldErrors = fieldErrors;
}
public Map<String, String> getFieldErrors() { return fieldErrors; }
}
Content Negotiation and Response Formats
Spring MVC supports multiple content types and formats:
Content Negotiation
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
// Supports both JSON and XML based on Accept header
@GetMapping(value = "/{id}", produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
})
public Product getProduct(@PathVariable Long id) {
return productService.findById(id);
}
// Accepts both JSON and XML
@PostMapping(consumes = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
})
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
Product created = productService.create(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
// Returns CSV format
@GetMapping(value = "/export", produces = "text/csv")
public ResponseEntity<String> exportProducts() {
String csv = productService.exportToCsv();
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=products.csv")
.body(csv);
}
// Returns file download
@GetMapping("/{id}/image")
public ResponseEntity<Resource> getProductImage(@PathVariable Long id) {
Resource image = productService.getProductImage(id);
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_JPEG)
.header("Content-Disposition", "inline; filename=product-" + id + ".jpg")
.body(image);
}
}
Custom Message Converters
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// Add custom CSV converter
converters.add(new CsvMessageConverter());
// Configure Jackson for JSON
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
.simpleDateFormat("yyyy-MM-dd");
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000", "https://myapp.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
// Custom CSV message converter
public class CsvMessageConverter implements HttpMessageConverter<Object> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false; // We only write CSV, not read
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return MediaType.valueOf("text/csv").includes(mediaType);
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return List.of(MediaType.valueOf("text/csv"));
}
@Override
public Object read(Class<?> clazz, HttpInputMessage inputMessage) {
throw new UnsupportedOperationException("CSV reading not supported");
}
@Override
public void write(Object object, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException {
outputMessage.getHeaders().setContentType(MediaType.valueOf("text/csv"));
if (object instanceof List<?> list) {
writeCsv(list, outputMessage.getBody());
} else {
writeCsv(List.of(object), outputMessage.getBody());
}
}
private void writeCsv(List<?> objects, OutputStream outputStream) throws IOException {
// CSV writing implementation
try (PrintWriter writer = new PrintWriter(outputStream)) {
if (!objects.isEmpty()) {
// Write header
Object first = objects.get(0);
writeHeader(first.getClass(), writer);
// Write data
for (Object obj : objects) {
writeRow(obj, writer);
}
}
}
}
private void writeHeader(Class<?> clazz, PrintWriter writer) {
// Implementation to write CSV header
}
private void writeRow(Object obj, PrintWriter writer) {
// Implementation to write CSV row
}
}
Interceptors and Filters
Spring MVC supports request/response interception:
Interceptors
@Component
public class LoggingInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String requestURI = request.getRequestURI();
String method = request.getMethod();
String userAgent = request.getHeader("User-Agent");
logger.info("Incoming request: {} {} from {}", method, requestURI, userAgent);
// Add request start time
request.setAttribute("startTime", System.currentTimeMillis());
return true; // Continue processing
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
Long startTime = (Long) request.getAttribute("startTime");
long duration = System.currentTimeMillis() - startTime;
logger.info("Request processing completed in {} ms", duration);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
if (ex != null) {
logger.error("Request completed with exception", ex);
} else {
logger.info("Request completed successfully with status: {}",
response.getStatus());
}
}
}
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
private final TokenService tokenService;
public AuthenticationInterceptor(TokenService tokenService) {
this.tokenService = tokenService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// Skip authentication for public endpoints
if (isPublicEndpoint(request.getRequestURI())) {
return true;
}
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
String token = authHeader.substring(7);
if (!tokenService.isValidToken(token)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
// Add user context to request
User user = tokenService.getUserFromToken(token);
request.setAttribute("currentUser", user);
return true;
}
private boolean isPublicEndpoint(String uri) {
return uri.startsWith("/api/public/") ||
uri.equals("/api/auth/login") ||
uri.equals("/api/auth/register");
}
}
// Register interceptors
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoggingInterceptor loggingInterceptor;
private final AuthenticationInterceptor authInterceptor;
public WebConfig(LoggingInterceptor loggingInterceptor,
AuthenticationInterceptor authInterceptor) {
this.loggingInterceptor = loggingInterceptor;
this.authInterceptor = authInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// Logging interceptor for all requests
registry.addInterceptor(loggingInterceptor);
// Authentication interceptor for API endpoints
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**", "/api/auth/**");
}
}
File Upload and Download
Spring MVC provides excellent support for file handling:
File Upload
@RestController
@RequestMapping("/api/files")
public class FileController {
private final FileStorageService fileStorageService;
public FileController(FileStorageService fileStorageService) {
this.fileStorageService = fileStorageService;
}
@PostMapping("/upload")
public ResponseEntity<FileUploadResponse> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam(required = false) String description) {
// Validate file
if (file.isEmpty()) {
throw new IllegalArgumentException("File cannot be empty");
}
if (file.getSize() > 10 * 1024 * 1024) { // 10MB limit
throw new IllegalArgumentException("File size cannot exceed 10MB");
}
String contentType = file.getContentType();
if (!isAllowedContentType(contentType)) {
throw new IllegalArgumentException("File type not allowed: " + contentType);
}
try {
FileMetadata metadata = fileStorageService.store(file, description);
FileUploadResponse response = new FileUploadResponse(
metadata.getId(),
metadata.getOriginalName(),
metadata.getStoredName(),
metadata.getSize(),
metadata.getContentType(),
metadata.getUploadTime()
);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
} catch (IOException e) {
throw new RuntimeException("Failed to upload file", e);
}
}
@PostMapping("/upload/multiple")
public ResponseEntity<List<FileUploadResponse>> uploadMultipleFiles(
@RequestParam("files") MultipartFile[] files) {
if (files.length > 5) {
throw new IllegalArgumentException("Cannot upload more than 5 files at once");
}
List<FileUploadResponse> responses = new ArrayList<>();
for (MultipartFile file : files) {
try {
FileMetadata metadata = fileStorageService.store(file, null);
responses.add(new FileUploadResponse(
metadata.getId(),
metadata.getOriginalName(),
metadata.getStoredName(),
metadata.getSize(),
metadata.getContentType(),
metadata.getUploadTime()
));
} catch (IOException e) {
throw new RuntimeException("Failed to upload file: " + file.getOriginalFilename(), e);
}
}
return ResponseEntity.status(HttpStatus.CREATED).body(responses);
}
@GetMapping("/{fileId}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileId) {
try {
FileMetadata metadata = fileStorageService.getFileMetadata(fileId);
Resource resource = fileStorageService.loadAsResource(fileId);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(metadata.getContentType()))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + metadata.getOriginalName() + "\"")
.body(resource);
} catch (FileNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
@GetMapping("/{fileId}/info")
public ResponseEntity<FileMetadata> getFileInfo(@PathVariable String fileId) {
try {
FileMetadata metadata = fileStorageService.getFileMetadata(fileId);
return ResponseEntity.ok(metadata);
} catch (FileNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{fileId}")
public ResponseEntity<Void> deleteFile(@PathVariable String fileId) {
try {
fileStorageService.delete(fileId);
return ResponseEntity.noContent().build();
} catch (FileNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
private boolean isAllowedContentType(String contentType) {
List<String> allowedTypes = List.of(
"image/jpeg", "image/png", "image/gif",
"application/pdf",
"text/plain", "text/csv",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
);
return allowedTypes.contains(contentType);
}
}
// File upload configuration
@Configuration
public class FileUploadConfig {
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxFileSize(DataSize.ofMegabytes(10));
factory.setMaxRequestSize(DataSize.ofMegabytes(50));
return factory.createMultipartConfig();
}
}
Testing Spring MVC Controllers
Comprehensive testing strategies for Spring MVC:
Unit Testing Controllers
@ExtendWith(MockitoExtension.class)
class UserControllerTest {
@Mock
private UserService userService;
@InjectMocks
private UserController userController;
private MockMvc mockMvc;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(userController)
.setControllerAdvice(new GlobalExceptionHandler())
.build();
objectMapper = new ObjectMapper();
}
@Test
void shouldReturnUserWhenFound() throws Exception {
// Given
Long userId = 1L;
User user = new User(userId, "John Doe", "[email protected]");
when(userService.findById(userId)).thenReturn(Optional.of(user));
// When & Then
mockMvc.perform(get("/api/users/{id}", userId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(userId))
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$.email").value("[email protected]"));
}
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
// Given
Long userId = 1L;
when(userService.findById(userId)).thenReturn(Optional.empty());
// When & Then
mockMvc.perform(get("/api/users/{id}", userId))
.andExpected(status().isNotFound());
}
@Test
void shouldCreateUserWithValidRequest() throws Exception {
// Given
CreateUserRequest request = new CreateUserRequest("John Doe", "[email protected]");
User createdUser = new User(1L, "John Doe", "[email protected]");
when(userService.createUser(any(CreateUserRequest.class))).thenReturn(createdUser);
// When & Then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpected(jsonPath("$.id").value(1))
.andExpected(jsonPath("$.name").value("John Doe"));
}
@Test
void shouldReturn400ForInvalidRequest() throws Exception {
// Given
CreateUserRequest request = new CreateUserRequest("", "invalid-email");
// When & Then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpected(jsonPath("$.code").value("VALIDATION_FAILED"))
.andExpected(jsonPath("$.fieldErrors.name").exists())
.andExpected(jsonPath("$.fieldErrors.email").exists());
}
}
Integration Testing
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class UserControllerIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void shouldCreateAndRetrieveUser() {
// Given
CreateUserRequest request = new CreateUserRequest("John Doe", "[email protected]");
// When - Create user
ResponseEntity<User> createResponse = restTemplate.postForEntity(
"/api/users", request, User.class);
// Then - Verify creation
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
User createdUser = createResponse.getBody();
assertThat(createdUser.getId()).isNotNull();
assertThat(createdUser.getName()).isEqualTo("John Doe");
// When - Retrieve user
ResponseEntity<User> getResponse = restTemplate.getForEntity(
"/api/users/" + createdUser.getId(), User.class);
// Then - Verify retrieval
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
User retrievedUser = getResponse.getBody();
assertThat(retrievedUser.getId()).isEqualTo(createdUser.getId());
assertThat(retrievedUser.getName()).isEqualTo("John Doe");
}
@Test
void shouldReturn404ForNonExistentUser() {
// When
ResponseEntity<String> response = restTemplate.getForEntity(
"/api/users/999", String.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
}
Summary
Spring MVC provides a comprehensive web framework:
Key Features:
- MVC Architecture: Clear separation of concerns
- Flexible Routing: Powerful request mapping capabilities
- Content Negotiation: Support for multiple formats (JSON, XML, etc.)
- Exception Handling: Global and controller-specific error handling
- Validation: Comprehensive validation support
- File Handling: Built-in file upload/download capabilities
Core Components:
- Controllers: Handle HTTP requests and responses
- Request Mapping: Map URLs to controller methods
- Data Binding: Automatic request parameter binding
- Validation: Bean validation with custom validators
- Interceptors: Cross-cutting concerns like authentication
- Message Converters: Handle different content types
Best Practices:
- Use @RestController for REST APIs
- Implement proper exception handling with @ControllerAdvice
- Validate input with @Valid and custom validators
- Use DTOs for request/response objects
- Test thoroughly with both unit and integration tests
- Handle file uploads securely with proper validation
Testing Strategies:
- Unit Tests: Test controllers in isolation with MockMvc
- Integration Tests: Test full stack with TestRestTemplate
- Use test containers for database integration tests
- Mock external dependencies appropriately
Spring MVC is a mature, flexible framework that provides everything needed to build robust web applications and RESTful services.