How do I map a request to a controller in Spring MVC?

In Spring MVC, you can map a request to a controller using the @RequestMapping (or aliases like @GetMapping, @PostMapping, etc.) annotation on a method. These annotations define a specific URL path and HTTP method that the method will handle. Here’s how you can do it:

Step-by-Step Instructions:

  1. Annotate the Class as a Controller: Use the @Controller annotation (or @RestController for REST APIs) on the class to indicate that it is a controller in your Spring application.
  2. Map URLs with @RequestMapping: Use the @RequestMapping annotation on methods to specify the URL patterns you want to map HTTP requests to. You can also use HTTP method-specific annotations, such as @GetMapping and @PostMapping.

Example

package org.kodejava.app;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/my-controller") // Base path for all endpoints in the controller
public class MyController {

    // Handles GET requests to /my-controller/hello
    @GetMapping("/hello")
    @ResponseBody
    public String sayHello() {
        return "Hello, World!";
    }

    // Handles POST requests to /my-controller/data
    @PostMapping("/data")
    @ResponseBody
    public String handleDataSubmission() {
        return "Data submitted successfully!";
    }
}

Key Annotations:

  1. @Controller: Marks the class as a Spring MVC controller.
  2. @RequestMapping: Used to map web requests onto specific methods or classes. It can be used with a combination of HTTP methods, URL patterns, and request parameters.
    @RequestMapping(value = "/example", method = RequestMethod.GET)
    
  3. HTTP Method-Specific Annotations: These are shorthand annotations for specific HTTP methods:
    • @GetMapping for GET requests
    • @PostMapping for POST requests
    • @PutMapping for PUT requests
    • @DeleteMapping for DELETE requests
    • @PatchMapping for PATCH requests

Notes:

  1. Class-Level @RequestMapping: If you specify a base path with @RequestMapping at the class level, all methods will inherit this as part of their path.
  2. Return Types:
    • Use @ResponseBody to return plain text, JSON, or XML directly in the HTTP response body.
    • If you are returning a view name, you don’t need @ResponseBody.
  3. Path Variables and Query Parameters: You can also handle dynamic path variables using the @PathVariable and request parameters using the @RequestParam annotations.

Example with Path Variables and Request Parameters:

package org.kodejava.app;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserController {

    // Dynamic path variable
    @GetMapping("/{id}")
    public String getUserById(@PathVariable("id") Long id) {
        return "User ID: " + id;
    }

    // Query parameter
    @GetMapping("/search")
    public String searchUsers(@RequestParam("name") String name) {
        return "Searching for user: " + name;
    }
}

By following this pattern, you can map requests to specific controller methods and build robust web applications with Spring MVC.

Testing Spring Boot Applications with JUnit and Mockito

Testing Spring Boot applications with JUnit and Mockito is a good practice to ensure the correctness and reliability of your application. Below, I’ll present a brief overview of how to write such test cases with explanations and examples.

1. JUnit Basics in Spring Boot

Spring Boot provides out-of-the-box support for JUnit through the spring-boot-starter-test dependency, which includes:

  • JUnit 5 (Jupiter) for writing test cases.
  • Mockito for mocking dependencies.
  • Additional libraries like Hamcrest and AssertJ for assertions.

Add the dependency in your pom.xml (if it’s not already present):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

2. Structural Overview

Testing can be divided into:

  • Unit Tests: Independent and isolated tests of individual classes, written using JUnit + Mockito.
  • Integration Tests: Testing multiple layers/classes, typically with Spring’s @SpringBootTest.

3. Writing Unit Tests with JUnit and Mockito

Below is an example of unit testing a Service class.

Example Scenario:

We have a class that depends on UserRepository. UserService
UserService.java:

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User findUserById(Long id) {
        return userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
    }
}

UserRepository.java:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

Corresponding Test Case

@ExtendWith(MockitoExtension.class) // Enables Mockito in JUnit 5
class UserServiceTest {

    @Mock // Creates a mock instance of UserRepository
    private UserRepository userRepository;

    @InjectMocks // Creates UserService and injects the mock UserRepository
    private UserService userService;

    @Test
    void testFindUserById_UserExists() {
        // Arrange
        Long userId = 1L;
        User mockUser = new User(userId, "John Doe", "[email protected]");

        // Mock the behavior of userRepository
        Mockito.when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));

        // Act
        User result = userService.findUserById(userId);

        // Assert
        assertNotNull(result);
        assertEquals(mockUser.getName(), result.getName());
        Mockito.verify(userRepository).findById(userId); // Verify method call
    }

    @Test
    void testFindUserById_UserNotFound() {
        // Arrange
        Long userId = 1L;

        // Mock the behavior of userRepository
        Mockito.when(userRepository.findById(userId)).thenReturn(Optional.empty());

        // Act & Assert
        Exception exception = assertThrows(RuntimeException.class, () -> userService.findUserById(userId));
        assertEquals("User not found", exception.getMessage());
    }
}

4. Integration Tests with @SpringBootTest

Integration tests are used to test the entire Spring application context.

Testing UserController

UserController.java:

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        return ResponseEntity.ok(userService.findUserById(id));
    }
}

Test Case for UserController

Use @SpringBootTest with for integration-like testing. MockMvc

@SpringBootTest
@AutoConfigureMockMvc // Configures MockMvc
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean // Mock a bean in the Spring context
    private UserService userService;

    @Test
    void testGetUserById() throws Exception {
        // Arrange
        Long userId = 1L;
        User mockUser = new User(userId, "John Doe", "[email protected]");

        Mockito.when(userService.findUserById(userId)).thenReturn(mockUser);

        // Act and Assert
        mockMvc.perform(get("/users/{id}", userId))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.name").value("John Doe"))
               .andExpect(jsonPath("$.email").value("[email protected]"));

        Mockito.verify(userService).findUserById(userId);
    }
}

5. Tips for Effective Testing

  1. Mock dependencies: Mock all external dependencies to isolate the unit being tested.
  2. Use Assertions effectively: Leverage libraries like AssertJ or Hamcrest for expressive assertions.
  3. Test exceptions: Always test boundary cases and exceptions.
  4. Verify mock behavior: Use Mockito.verify() to ensure mocked methods were invoked correctly.
  5. Spring utilities: @MockBean is useful for overriding beans in the application context for integration testing.

Summary

Spring Boot testing with JUnit and Mockito allows you to:

  • Write isolated unit tests for business logic.
  • Write integration tests to validate Spring components working together.

How do I configure Spring MVC using Java-based configuration?

To configure Spring MVC using a Java-based configuration, you can follow these steps:

  1. Enable Spring MVC support by using the @EnableWebMvc annotation.
  2. Create a Spring configuration class annotated with @Configuration.
  3. Configure the component scanning to detect controllers, services, and other components.
  4. Define a ViewResolver bean to map view names to actual views (e.g., JSPs, Thymeleaf templates, etc.).
  5. Set up other essential configurations like static resource handling, CORS, or message converters if necessary.

Here’s an example configuration class:

package org.kodejava.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "org.kodejava.demo") // Specify your base package where controllers are located
public class WebConfig implements WebMvcConfigurer {

    // Define a ViewResolver bean
    @Bean
    public InternalResourceViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/views/"); // Path to your view templates
        viewResolver.setSuffix(".jsp"); // View file extension (e.g., .jsp or .html)
        return viewResolver;
    }

    // Static resource handling (e.g., for serving CSS, JS, images, etc.)
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/resources/");
    }
}

Step-by-step Explanation:

  1. @EnableWebMvc:
    • This annotation imports Spring MVC configuration from WebMvcConfigurationSupport, which enables features like DispatcherServlet, handler mappings, and more.
  2. @ComponentScan:
    • This annotation configures Spring to scan the specified package(s) for components, such as controllers, services, and repositories.
  3. Define a ViewResolver:
    • In the example, InternalResourceViewResolver maps view names to JSP files located under /WEB-INF/views/ with the .jsp extension.
  4. Static Resources:
    • The addResourceHandlers method maps requests for static resources (e.g., CSS, JS, images) to physical locations.

Setting Up the DispatcherServlet

You’ll also need to configure the DispatcherServlet in your file (if you’re using a traditional deployment structure) or via a programmatic initializer (preferred in modern setups). web.xml
Here’s an example of a Java-based initializer:

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] { RootConfig.class }; // Configuration for application-wide beans (e.g., data sources)
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { WebConfig.class }; // Configuration for Spring MVC
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" }; // Map all requests to DispatcherServlet
    }
}

Explanation of the Java-Based Initializer:

  • getRootConfigClasses:
    • Specifies configuration for the root application context, such as services, persistence, or security.
  • getServletConfigClasses:
    • Specifies configuration for the Spring MVC child context (e.g., controllers, view resolvers).
  • getServletMappings:
    • Maps the DispatcherServlet to handle requests starting at . /

With these steps, you’ll have a fully functional Spring MVC configuration using Java-based configuration.

Implementing Global Exception Handling with @ControllerAdvice

To implement global exception handling in a Spring application, the @ControllerAdvice annotation is used. It allows you to centralize exception handling across multiple controllers.

Below is an example of how you can implement global exception handling with @ControllerAdvice in your application:


1. Define a Global Exception Handler

Create a class with the @ControllerAdvice annotation to handle exceptions globally. Within this class, use the @ExceptionHandler annotation on methods to define specific exception handling logic.

package org.kodejava;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

// Marks this class as a global exception handler
@ControllerAdvice
public class GlobalExceptionHandler {

    // Handle specific exceptions
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<?> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
        ErrorResponse errorResponse = new ErrorResponse(
                HttpStatus.NOT_FOUND.value(),
                ex.getMessage(),
                request.getDescription(false));
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }

    // Handle global exceptions (for all other exceptions)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleGlobalException(Exception ex, WebRequest request) {
        ErrorResponse errorResponse = new ErrorResponse(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                "An unexpected error occurred",
                request.getDescription(false));
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

2. Create a Custom Exception Class (Optional)

Define specific exception classes for your business use cases. For example, a ResourceNotFoundException for handling “not found” errors.

package org.kodejava;

public class ResourceNotFoundException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    public ResourceNotFoundException(String message) {
        super(message);
    }
}

3. Create an Error Response Model

Create a class to structure the error response data consistently.

package org.kodejava;

public class ErrorResponse {

    private int statusCode;
    private String message;
    private String details;

    public ErrorResponse(int statusCode, String message, String details) {
        this.statusCode = statusCode;
        this.message = message;
        this.details = details;
    }

    // Getters and Setters
    public int getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(int statusCode) {
        this.statusCode = statusCode;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getDetails() {
        return details;
    }

    public void setDetails(String details) {
        this.details = details;
    }
}

4. Throw Custom Exceptions in Your Controller

You can now throw the ResourceNotFoundException or other exceptions in your controllers, and let the global exception handler process them.

package org.kodejava;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class TestController {

    @GetMapping("/resource")
    public String getResource() {
        throw new ResourceNotFoundException("Resource not found with ID");
    }
}

5. Test the Application

When you access the /api/resource endpoint, the global exception handler will catch the ResourceNotFoundException and return a structured error response. Example response:

{
  "statusCode": 404,
  "message": "Resource not found with ID",
  "details": "uri=/api/resource"
}

Advantages of Using @ControllerAdvice

  1. Centralized Exception Handling: Removes the need to write exception handling in multiple controllers.
  2. Improved Readability: Controllers are cleaner as they no longer handle exceptions.
  3. Reusability: Reuse the exception handler for different types of exceptions across the application.
  4. Custom Responses: Provides flexibility to return consistent error responses.

This implementation ensures your application has a robust and maintainable error-handling mechanism using Spring’s @ControllerAdvice.

How do I create a simple Hello World web app using Spring MVC?

Creating a simple “Hello World” web application using Spring MVC involves several steps. Below is a guide that will walk you through creating the application from scratch:


1. Set Up the Project

Use a build tool like Maven or Gradle to manage your dependencies. Here, we’ll use Maven.

pom.xml

Create a pom.xml file and include the necessary dependencies:

<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.kodejava</groupId>
    <artifactId>hello-world-spring-mvc</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <dependencies>
        <!-- Spring MVC -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>6.1.0</version>
        </dependency>

        <!-- Spring Boot Starter (Optional for running the app with Boot) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>3.2.0</version>
        </dependency>

        <!-- JSTL for view rendering -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

        <!-- Servlet API -->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>5.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

2. Configure the Project

web.xml

If you’re using traditional deployment (instead of Spring Boot), a web.xml configuration file is required under src/main/webapp/WEB-INF/.

<web-app xmlns="http://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://jakarta.ee/xml/ns/jakartaee http://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
    <display-name>Hello World App</display-name>

    <!-- DispatcherServlet mapping -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!-- Context loader -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
    </context-param>
</web-app>

3. Spring Configuration

dispatcher-servlet.xml (Spring MVC Application Context)

Place this file under WEB-INF/.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans 
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context 
           http://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/mvc 
           http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- Enable annotation-based Spring components -->
    <context:component-scan base-package="org.kodejava" />
    <mvc:annotation-driven />

    <!-- Configure the view resolver -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

4. Creating the Controller

HelloWorldController.java

Create a controller in the org.kodejava package.

package org.kodejava;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.ui.Model;

@Controller
public class HelloWorldController {

    @GetMapping("/")
    public String helloWorld(Model model) {
        model.addAttribute("message", "Hello, World!");
        return "hello"; // Refers to the hello.jsp view
    }
}

5. Create a View

hello.jsp

Place the JSP file in src/main/webapp/WEB-INF/views/.

<!DOCTYPE html>
<html>
<head>
    <title>Hello World</title>
</head>
<body>
    <h1>${message}</h1>
</body>
</html>

6. Running the Application

Using Spring Boot (Simpler)

  • If you added Spring Boot dependencies in your pom.xml, include a main method and run the project:
package org.kodejava;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HelloWorldApplication {
    public static void main(String[] args) {
        SpringApplication.run(HelloWorldApplication.class, args);
    }
}

Using a Traditional Servlet Container

  • Package your application as a WAR file (mvn clean package) and deploy it to a servlet container (e.g., Apache Tomcat).

Access the Application

Once the application is deployed, access the URL: http://localhost:8080/

You will see the Hello, World! message displayed.