SpringMVC is a web framework built by the Spring team for the presentation layer, following the MVC design pattern. It simplifies common web operations like request data processing, response handling, and page navigation.
Getting Started
Project Setup
Maven Dependencies
<dependencies>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- JSP Support -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<!-- SpringMVC Framework -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8080</port>
<path>/</path>
<uriEncoding>utf-8</uriEncoding>
</configuration>
</plugin>
</plugins>
</build>
Web Configuration
<servlet>
<servlet-name>frontController</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:mvc-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>frontController</servlet-name>
<!-- "/" handles all requests except JSPs -->
<!-- "*.do" handles only .do requests -->
<!-- "/*" should not be used -->
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- Character Encoding Filter -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
SpringMVC Configuration
<!-- Component scanning - controller layer only -->
<context:component-scan base-package="com.example.controller"/>
<!-- Static resource handling -->
<mvc:default-servlet-handler/>
<!-- JSON and encoding configuration -->
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg value="utf-8"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
Basic Controller
@Controller
@RequestMapping("/api")
public class BasicController {
@RequestMapping("/greet")
public String greet() {
System.out.println("Handling greeting request");
return "result";
}
}
Request Mapping Configuration
The @RequestMapping annotation can be applied to classes or methods to define matching rules for icnoming requests.
Mapping by Path
@Controller
@RequestMapping("/resources")
public class PathController {
@RequestMapping("/list")
public String listResources() {
return "resourceList";
}
}
Mapping by HTTP Method
@Controller
@RequestMapping("/items")
public class MethodController {
// Explicit method specification
@RequestMapping(value = "/create", method = RequestMethod.POST)
public String createItem() {
return "success";
}
// Shorthand annotations
@GetMapping("/{id}")
public String getItem(@PathVariable("id") Integer itemId) {
return "itemDetail";
}
@PostMapping("/new")
public String addItem() {
return "success";
}
@PutMapping("/update")
public String modifyItem() {
return "success";
}
@DeleteMapping("/remove")
public String deleteItem() {
return "success";
}
}
Mapping by Parameters
@Controller
@RequestMapping("/query")
public class ParamController {
// Requires 'code' parameter to exist
@RequestMapping(value = "/check", method = RequestMethod.GET, params = "code")
public String checkWithCode() {
return "success";
}
// Requires 'code' parameter to NOT exist
@RequestMapping(value = "/check", method = RequestMethod.GET, params = "!code")
public String checkWithoutCode() {
return "success";
}
// Requires 'code' parameter to equal specific value
@RequestMapping(value = "/verify", method = RequestMethod.GET, params = "code=abc123")
public String verifyCode() {
return "success";
}
// Requires 'code' parameter to NOT equal specific value
@RequestMapping(value = "/verify", method = RequestMethod.GET, params = "code!=xyz789")
public String verifyDifferentCode() {
return "success";
}
}
Mapping by Headers
@Controller
@RequestMapping("/headers")
public class HeaderController {
// Requires specific header
@RequestMapping(value = "/device", method = RequestMethod.GET, headers = "deviceType")
public String handleDeviceRequest() {
return "success";
}
// Header must not exist
@RequestMapping(value = "/device", method = RequestMethod.GET, headers = "!deviceType")
public String handleWithoutDeviceType() {
return "success";
}
// Header must equal specific value
@RequestMapping(value = "/device", method = RequestMethod.GET, headers = "deviceType=ios")
public String handleIosRequest() {
return "success";
}
}
Mapping by Content-Type
@Controller
@RequestMapping("/content")
public class ContentTypeController {
// Requires specific Content-Type
@RequestMapping(value = "/upload", method = RequestMethod.POST,
consumes = "multipart/form-data")
public String handleMultipartUpload() {
return "success";
}
// Content-Type must NOT be multipart
@RequestMapping(value = "/upload", method = RequestMethod.POST,
consumes = "!multipart/form-data")
public String handleNonMultipartUpload() {
return "success";
}
}
RESTful Architecture
RESTful design principles:
- Each URI represents a specific resource
- Use appropriate HTTP verbs: GET (retrieve), POST (create), PUT (update), DELETE (remove)
- Simple parameters embedded in URL path:
/users/123 - Complex data sent as JSON in request body
Request Parameter Handling
Path Variables
@RestController
@RequestMapping("/users")
public class UserApiController {
// Single path variable
@GetMapping("/{userId}")
public User findById(@PathVariable("userId") Integer id) {
System.out.println("Fetching user: " + id);
return new User(id, "Sample", 25);
}
// Multiple path variables
@GetMapping("/{userId}/posts/{postId}")
public Post findUserPost(@PathVariable("userId") Integer uid,
@PathVariable("postId") Integer pid) {
System.out.println("User: " + uid + ", Post: " + pid);
return new Post(pid, uid, "Content");
}
}
JSON Request Body
Configuration requires Jackson dependency and annotation-driven MVC.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
private Integer accountId;
private String accountName;
private Integer balance;
}
@RestController
@RequestMapping("/accounts")
public class AccountApiController {
// Deserialize JSON to object
@PostMapping("/create")
public String createAccount(@RequestBody Account account) {
System.out.println("Creating: " + account);
return "success";
}
// Deserialize JSON to Map
@PostMapping("/create")
public String createFromMap(@RequestBody Map<String, Object> data) {
System.out.println("Data received: " + data);
return "success";
}
// Deserialize JSON array to List
@PostMapping("/batch")
public String batchCreate(@RequestBody List<Account> accounts) {
System.out.println("Batch size: " + accounts.size());
return "success";
}
}
Note: Content-Type header must be application/json when using @RequestBody.
Query String Parameters
@Controller
@RequestMapping("/search")
public class SearchController {
// Direct parameter mapping (parameter names must match)
@RequestMapping("/query")
public String search(Integer categoryId, String keyword, String[] filters) {
System.out.println("Category: " + categoryId);
System.out.println("Keyword: " + keyword);
System.out.println("Filters: " + Arrays.toString(filters));
return "results";
}
// Explicit parameter binding
@RequestMapping("/query")
public String searchWithBinding(
@RequestParam("categoryId") Integer cid,
@RequestParam("keyword") String kw,
@RequestParam("filters") String[] filterList) {
return "results";
}
// Auto-mapping to POJO
@RequestMapping("/query")
public String searchByEntity(SearchCriteria criteria) {
System.out.println("Criteria: " + criteria);
return "results";
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SearchCriteria {
private Integer categoryId;
private String keyword;
private String[] filters;
}
Parameter Annotation Attributes
@Controller
@RequestMapping("/advanced")
public class AdvancedParamController {
// Optional parameter
@RequestMapping("/optional")
public String handleOptional(
@RequestParam(value = "id", required = false) Integer optionalId) {
return "result";
}
// Default value when parameter missing
@RequestMapping("/default")
public String handleDefault(
@RequestParam(value = "page", required = false, defaultValue = "1") Integer pageNum) {
return "result";
}
}
Type Converters
Custom Converter Implementation
public class DateConverter implements Converter<String, Date> {
@Override
public Date convert(String source) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
try {
return sdf.parse(source);
} catch (ParseException e) {
return null;
}
}
}
<mvc:annotation-driven conversion-service="customConverterService">
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg value="utf-8"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<bean id="customConverterService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="com.example.converter.DateConverter"/>
</set>
</property>
</bean>
@DateTimeFormat Annotation
A simpler approach for date parsing:
@RequestMapping("/birthday")
public String processBirthday(@DateTimeFormat(pattern = "yyyy-MM-dd") Date birthday) {
System.out.println("Birthday: " + birthday);
return "result";
}
Response Handling
@ResponseBody Annotation
@RestController
@RequestMapping("/api/users")
public class UserRestController {
@GetMapping("/{id}")
public User getUser(@PathVariable Integer id) {
return new User(id, "John", 30);
}
@GetMapping("/all")
public List<User> getAllUsers() {
List<User> users = new ArrayList<>();
users.add(new User(1, "Alice", 25));
users.add(new User(2, "Bob", 28));
users.add(new User(3, "Charlie", 32));
return users;
}
}
The @RestController annotation combines @Controller and @ResponseBody.
Page Navigation
@Controller
public class PageController {
// Forward (default)
@RequestMapping("/page1")
public String navigatePage1() {
return "target";
}
// Explicit forward
@RequestMapping("/page2")
public String navigatePage2() {
return "forward:target";
}
// Redirect
@RequestMapping("/page3")
public String navigatePage3() {
return "redirect:/target";
}
}
View Resolver Configuration
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
With this configuration, returning "user" resolves to /WEB-INF/views/user.jsp.
Servlet API Access
@Controller
public class ServletController {
@RequestMapping("/servlet")
public String accessServletObjects(HttpServletRequest request,
HttpServletResponse response,
HttpSession session) {
String method = request.getMethod();
session.setAttribute("data", "value");
return "result";
}
}
Header and Cookie Access
@Controller
public class HeaderCookieController {
@RequestMapping("/headers")
public String readHeader(@RequestHeader("X-Custom-Header") String customHeader) {
System.out.println("Header value: " + customHeader);
return "result";
}
@RequestMapping("/cookies")
public String readCookie(@CookieValue("SESSION_ID") String sessionId) {
System.out.println("Session ID: " + sessionId);
return "result";
}
}
JSP Integration
Storing Data in Request Scope
@Controller
public class JspController {
@RequestMapping("/display")
public String displayData(Model model) {
model.addAttribute("message", "Hello World");
model.addAttribute("timestamp", new Date());
return "display";
}
@RequestMapping("/display2")
public ModelAndView displayDataWithView(ModelAndView mav) {
mav.addObject("message", "Hello World");
mav.addObject("timestamp", new Date());
mav.setViewName("display");
return mav;
}
}
Session Scope Management
@Controller
@SessionAttributes({"message", "userId"})
public class SessionController {
@RequestMapping("/store")
public String storeSessionData(Model model) {
model.addAttribute("message", "Stored in session");
model.addAttribute("userId", 12345);
return "result";
}
@RequestMapping("/retrieve")
public String getSessionData(@SessionAttribute("userId") Integer userId) {
System.out.println("User from session: " + userId);
return "result";
}
}
Interceptors
Creating an Interceptor
public class TimingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
System.out.println("Before handler execution");
return true; // false blocks the request
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) {
System.out.println("After handler, before view rendering");
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
System.out.println("After complete view rendering");
}
}
Interceptor Configuration
<mvc:interceptors>
<mvc:interceptor>
<!-- Path patterns -->
<!-- "/*" matches single level: /test/a -->
<!-- "/**" matches multiple levels: /test/a/b/c -->
<mvc:mapping path="/api/**"/>
<mvc:exclude-mapping path="/api/auth/**"/>
<mvc:exclude-mapping path="/static/**"/>
<bean class="com.example.interceptor.TimingInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
Spring Boot Configuration
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private TimingInterceptor timingInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timingInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/auth/**", "/static/**");
}
}
Login Authentication Interceptor
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
Integer loggedInUserId = (Integer) request.getSession()
.getAttribute("loggedInUserId");
if (loggedInUserId == null) {
response.sendRedirect(request.getContextPath() + "/login.html");
return false;
}
return true;
}
}
Multiple Interceptor Execution
When all interceptors return true:
- Interceptor1 preHandle
- Interceptor2 preHandle
- Handler executes
- Interceptor2 postHandle
- Interceptor1 postHandle
- View renders
- Interceptor2 afterCompletion
- Interceptor1 afterCompletion
When an interceptor returns false, execution stops and afterCompletion runs only for interceptors that already passed preHandle.
Exception Handling
HandlerExceptionResolver Implementation
@Component
public class GlobalExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception exception) {
ModelAndView mav = new ModelAndView();
mav.addObject("error", exception.getMessage());
mav.setViewName("error");
return mav;
}
}
@ControllerAdvice Approach
@ControllerAdvice
@Component
public class GlobalExceptionHandler {
@ExceptionHandler({NullPointerException.class, ArithmeticException.class})
public ModelAndView handleCommonExceptions(Exception e) {
ModelAndView mav = new ModelAndView();
mav.addObject("error", e.getMessage());
mav.setViewName("error");
return mav;
}
@ExceptionHandler(Exception.class)
public ModelAndView handleGenericException(Exception e) {
ModelAndView mav = new ModelAndView();
mav.addObject("error", "An unexpected error occurred");
mav.setViewName("error");
return mav;
}
}
REST API Exception Handling
@ControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public Result handleException(Exception e) {
Result result = new Result();
result.setSuccess(false);
result.setMessage(e.getMessage());
result.setCode(500);
return result;
}
}
File Upload
Upload Requirements
- HTTP method must be POST
- Content-Type must be
multipart/form-data
Configuration
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="utf-8"/>
<property name="maxUploadSize" value="#{1024*1024*100}"/>
<property name="maxUploadSizePerFile" value="#{1024*1024*50}"/>
</bean>
Upload Handler
@Controller
public class FileUploadController {
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file)
throws IOException {
if (file.isEmpty()) {
return "error";
}
String originalFilename = file.getOriginalFilename();
file.transferTo(new File("/uploads/" + originalFilename));
return "success";
}
}
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">Upload</button>
</form>
MultipartFile Methods
String originalFilename = file.getOriginalFilename();
String contentType = file.getContentType();
long fileSize = file.getSize();
InputStream inputStream = file.getInputStream();
File Download
@Controller
public class FileDownloadController {
@RequestMapping("/download")
public void downloadFile(HttpServletRequest request,
HttpServletResponse response) throws Exception {
String filePath = "/WEB-INF/files/document.pdf";
String realPath = request.getServletContext().getRealPath(filePath);
File file = new File(realPath);
String filename = file.getName();
String mimeType = request.getServletContext().getMimeType(filename);
response.setHeader("Content-Type", mimeType);
String encodedFilename = URLEncoder.encode(filename, "UTF-8");
response.setHeader("Content-Disposition",
"attachment; filename=" + encodedFilename + ";filename*=utf-8''" + encodedFilename);
try (InputStream is = new FileInputStream(file);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
}
}
SpringMVC Execution Flow
JSP-Style Development Flow
- Request arrives at DispatcherServlet
- DispatcherServlet queries HandlerMapping for matching handler
- HandlerMapping returns execution chain (handler + interceptors)
- DispatcherServlet invokes appropriate HandlerAdapter
- HandlerAdapter executes handler method, converting parameters
- Handler returns ModelAndView to DispatcherServlet
- DispatcherServlet forwards ModelAndView to ViewResolver
- ViewResolver converts logical view name to View object
- DispatcherServlet renders view and sends response
REST API Development Flow
- Request arrives at DispatcherServlet
- DispatcherServlet queries HandlerMapping for matching handler
- HandlerMapping returns execution chain
- DispatcherServlet invokes appropriate HandlerAdapter
- HandlerAdapter executes handler method with
@ResponseBody - HandlerAdapter converts return value to JSON and writes to response body
- ModelAndView is null, skipping view resolution entirely
The key difference: REST APIs skip view resolution entirely when @ResponseBody is present.