Spring MVC Fundamentals
To build a Spring MVC application, start by declaring the required dependencies in your pom.xml:
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.33</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
<packaging>war</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/app</path>
<port>8080</port>
<uriEncoding>UTF-8</uriEncoding>
</configuration>
</plugin>
</plugins>
</build>
Controller Layer
Define RESTful endpoints using annotations:
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public Map<String, Object> createUser(@RequestBody User user) {
System.out.println("Creating user: " + user.getName());
Map<String, Object> result = new HashMap<>();
result.put("status", "success");
result.put("data", user);
return result;
}
@GetMapping("/{id}")
public ResponseEntity<User> fetchUser(@PathVariable Integer id) {
User mockUser = new User();
mockUser.setId(id);
mockUser.setName("Demo User");
return ResponseEntity.ok(mockUser);
}
}
Configuration Classes
Replace XML-based configuration with Java-based config:
@Configuration
@ComponentScan(basePackages = "com.example.controller")
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggingInterceptor())
.addPathPatterns("/api/**");
}
}
Servlet container initialization:
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
Parameter Binding and Data Handling
Primitive and POJO Parameters
Spring MVC automatically binds request parameters to method arguments:
@GetMapping("/search")
public String searchUsers(
@RequestParam(value = "q", required = false) String query,
@RequestParam(defaultValue = "0") int page,
@ModelAttribute UserCriteria criteria) {
// criteria is auto-populated from query string or form fields
return "search-results";
}
For nested objects, use dot notation in URLs:
GET /search?name=John&address.city=New+York&address.zip=10001
JSON Payloads
Use @RequestBody for JSON deserialization (requires Jackson on classpath):
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(
@PathVariable Long id,
@RequestBody @Valid UserUpdateRequest updateRequest) {
User updated = userService.update(id, updateRequest);
return ResponseEntity.ok(updated);
}
Date Formatting
Customize date parsing using @DateTimeFormat:
@GetMapping("/events")
public List<Event> listEvents(
@DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam Date startDate,
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") @RequestParam Date endDate) {
return eventService.findByRange(startDate, endDate);
}
RESTful Design Patterns
Adopt HTTP methods and resource-oriented URIs:
@RestController
@RequestMapping("/api/books")
public class BookController {
@PostMapping
public ResponseEntity<Book> create(@RequestBody Book book) { /* ... */ }
@GetMapping("/{isbn}")
public ResponseEntity<Book> read(@PathVariable String isbn) { /* ... */ }
@PutMapping("/{isbn}")
public ResponseEntity<Book> replace(@PathVariable String isbn, @RequestBody Book book) { /* ... */ }
@PatchMapping("/{isbn}")
public ResponseEntity<Book> update(@PathVariable String isbn, @RequestBody Map<String, Object> updates) { /* ... */ }
@DeleteMapping("/{isbn}")
public ResponseEntity<Void> delete(@PathVariable String isbn) { /* ... */ }
}
SSM Stack Integration
Core Dependencies
Include MyBatis, JDBC, and transaction management:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.21</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
Data Access Layer
Use MyBatis annotations or XML mappers:
@Mapper
public interface BookMapper {
@Select("SELECT * FROM books WHERE isbn = #{isbn}")
Book selectByIsbn(@Param("isbn") String isbn);
@Insert("INSERT INTO books (isbn, title, author) VALUES (#{isbn}, #{title}, #{author})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(Book book);
@Update("UPDATE books SET title = #{title}, author = #{author} WHERE isbn = #{isbn}")
int update(Book book);
@Delete("DELETE FROM books WHERE isbn = #{isbn}")
int delete(@Param("isbn") String isbn);
}
Service Layer with Transactions
@Service
@Transactional
public class BookService {
private final BookMapper bookMapper;
public BookService(BookMapper bookMapper) {
this.bookMapper = bookMapper;
}
public Book create(Book book) {
bookMapper.insert(book);
return book;
}
public Optional<Book> findByIsbn(String isbn) {
return Optional.ofNullable(bookMapper.selectByIsbn(isbn));
}
}
Unified Response Structure
Create a standardized response wrapper:
public record ApiResponse<T>(
int code,
String message,
T data) {
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "OK", data);
}
public static <T> ApiResponse<T> error(int code, String msg) {
return new ApiResponse<>(code, msg, null);
}
}
Apply it in controllers:
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@PostMapping
public ApiResponse<Book> create(@RequestBody Book book) {
try {
Book created = bookService.create(book);
return ApiResponse.success(created);
} catch (Exception e) {
return ApiResponse.error(400, "Invalid input");
}
}
}
Global Exception Handling
Centralize error responses using @RestControllerAdvice:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ApiResponse<Void> handleNotFound(ResourceNotFoundException ex) {
return ApiResponse.error(404, ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleValidation(MethodArgumentNotValidException ex) {
String errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining("; "));
return ApiResponse.error(400, "Validation failed: " + errors);
}
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleGeneric(Exception ex) {
return ApiResponse.error(500, "Internal server error");
}
}
Interceptor Implementation
Create reusable cross-cutting logic:
@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 {
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
logger.info("{} {} [{}]", request.getMethod(), request.getRequestURI(), startTime);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
long startTime = (Long) request.getAttribute("startTime");
long duration = System.currentTimeMillis() - startTime;
logger.info("Completed in {} ms with status {}", duration, response.getStatus());
}
}
Spring Boot Simplification
Modern Spring Boot eliminates boilerplate configuraton:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Configure database and logging in application.yml:
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/library?useSSL=false&serverTimezone=UTC
username: appuser
password: secret
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: true
properties:
hibernate:
format_sql: true
logging:
level:
root: INFO
com.example: DEBUG
Enable MyBatis auto-configuration:
@MapperScan("com.example.mapper")
@SpringBootApplication
public class Application { /* ... */ }