Spring MVC Best Practices with Code Snippets
1. Use Constructor Injection
Prefer constructor injection over field injection for better testability and immutability.
Constructor injection ensures that the dependencies are provided when the object is created, making it easier to test and ensuring the object's state remains consistent.
Code Snippet:
@Controller
public class MyController {
private final MyService myService;
@Autowired
public MyController(MyService myService) {
this.myService = myService;
}
// Handler methods
}
2. Centralized Exception Handling
Use @ControllerAdvice
to handle exceptions globally.
Centralizing exception handling keeps your controllers clean and ensures consistent error responses.
Code Snippet:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleResourceNotFoundException(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGenericException(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error occurred");
}
}
3. Validate User Input
Use @Valid
and @Validated
annotations to validate request objects.
Validating user input ensures that your application receives well-formed data, reducing the risk of errors and security issues.
Code Snippet:
public class User {
@NotEmpty(message = "Name is required")
private String name;
@Email(message = "Email should be valid")
private String email;
// Getters and setters
}
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
// Save user
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
}
4. Optimize Database Access
Use Spring Data JPA repositories and pagination to optimize database access.
Spring Data JPA simplifies data access and using pagination helps in handling large datasets efficiently.
Code Snippet:
public interface UserRepository extends JpaRepository<User, Long> {
Page<User> findAll(Pageable pageable);
}
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserRepository userRepository;
@Autowired
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping
public ResponseEntity<Page<User>> getAllUsers(@RequestParam int page, @RequestParam int size) {
Pageable pageable = PageRequest.of(page, size);
Page<User> users = userRepository.findAll(pageable);
return ResponseEntity.ok(users);
}
}
5. Implement Security
Use Spring Security to secure your web application.
Securing your application ensures that only authorized users can access certain endpoints, protecting sensitive data and operations.
Code Snippet:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER")
.and()
.withUser("admin").password("{noop}admin").roles("ADMIN");
}
}
6. Use Thymeleaf for Templating
Use Thymeleaf for rendering views in Spring MVC applications.
Thymeleaf integrates well with Spring MVC and provides a natural templating approach, making it easier to work with HTML templates.
Code Snippet:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>My Spring MVC Application</title>
</head>
<body>
<h1 th:text="'Welcome, ' + ${username} + '!'"></h1>
</body>
</html>
@Controller
public class HomeController {
@GetMapping("/home")
public String home(Model model) {
model.addAttribute("username", "John Doe");
return "home";
}
}
7. Write Unit Tests
Write unit tests for your controllers, services, and repositories using JUnit and Mockito.
Unit testing ensures that each part of your application works correctly, reducing the risk of bugs and making refactoring safer.
Code Snippet:
@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
public void testGetUserById() throws Exception {
User user = new User(1L, "John Doe", "john@example.com");
given(userService.findUserById(1L)).willReturn(user);
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"));
}
}
8. Use ResponseEntity for Better Control Over HTTP Responses
Use ResponseEntity
to provide better control over the HTTP responses, including status codes and headers.
ResponseEntity
allows you to customize the HTTP response, making it easier to convey the correct status and any additional metadata.
Code Snippet:
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.findProductById(id);
if (product != null) {
return ResponseEntity.ok(product);
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
}
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product createdProduct = productService.saveProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(createdProduct);
}
}
9. Implement DTOs (Data Transfer Objects)
Use DTOs to transfer data between the client and the server.
DTOs help in separating the internal data model from the external representation, providing a clear contract for data exchange.
Code Snippet:
public class UserDTO {
private Long id;
private String name;
private String email;
// Getters and setters
}
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
User user = userService.findUserById(id);
if (user != null) {
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user, userDTO);
return ResponseEntity.ok(userDTO);
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
}
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody UserDTO userDTO) {
User user = new User();
BeanUtils.copyProperties(userDTO, user);
User createdUser = userService.saveUser(user);
BeanUtils.copyProperties(createdUser, userDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(userDTO);
}
}
10. Use Caching for Improved Performance
Use caching to improve the performance of your application by reducing the load on the database.
Caching frequently accessed data can significantly reduce the response times and load on your database.
Code Snippet:
@EnableCaching
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@Service
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Cacheable("products")
public Product findProductById(Long id) {
return productRepository.findById(id).orElse(null);
}
}\
11. Use Asynchronous Processing
Use asynchronous processing for long-running tasks to improve the responsiveness of your application. Asynchronous processing allows you to handle long-running tasks in the background, freeing up the main thread to handle other requests.
Code Snippet:
@EnableAsync
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@Service
public class EmailService {
@Async
public void sendEmail(String to, String subject, String body) {
// Simulate a long-running task
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Send email logic
}
}
@RestController
@RequestMapping("/api/notifications")
public class NotificationController {
private final EmailService emailService;
@Autowired
public NotificationController(EmailService emailService) {
this.emailService = emailService;
}
@PostMapping("/sendEmail")
public ResponseEntity<String> sendEmail(@RequestParam String to, @RequestParam String subject, @RequestParam String body) {
emailService.sendEmail(to, subject, body);
return ResponseEntity.ok("Email is being sent");
}
}
12. Use Profiles to Manage Environments
Use Spring Profiles to manage different environments (e.g., development, testing, production).
Profiles allow you to define different configurations for different environments, making it easier to manage environment-specific settings.
Code Snippet:
// application.yml
spring:
profiles:
active: dev
// application-dev.yml
datasource:
url: jdbc:mysql://localhost:3306/devdb
username: devuser
password: devpass
// application-prod.yml
datasource:
url: jdbc:mysql://prod-db:3306/proddb
username: produser
password: prodpass
@Configuration
@Profile("dev")
public class DevConfig {
// Development-specific beans
}
@Configuration
@Profile("prod")
public class ProdConfig {
// Production-specific beans
}
13. Use RESTful Conventions
Follow RESTful conventions for API design to make your APIs more intuitive and standard. Using standard RESTful conventions makes your APIs easier to understand and use for other developers.
Code Snippet:
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService bookService;
@Autowired
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping
public ResponseEntity<List<Book>> getAllBooks() {
List<Book> books = bookService.findAllBooks();
return ResponseEntity.ok(books);
}
@GetMapping("/{id}")
public ResponseEntity<Book> getBookById(@PathVariable Long id) {
Book book = bookService.findBookById(id);
if (book != null) {
return ResponseEntity.ok(book);
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
}
@PostMapping
public ResponseEntity<Book> createBook(@Valid @RequestBody Book book) {
Book createdBook = bookService.saveBook(book);
return ResponseEntity.status(HttpStatus.CREATED).body(createdBook);
}
@PutMapping("/{id}")
public ResponseEntity<Book> updateBook(@PathVariable Long id, @Valid @RequestBody Book book) {
book.setId(id);
Book updatedBook = bookService.saveBook(book);
return ResponseEntity.ok(updatedBook);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
bookService.deleteBook(id);
return ResponseEntity.noContent().build();
}
}
Code
To provide you with a practical understanding, I've created a comprehensive GitHub repository that demonstrates these principles in action. The repository includes an application backend, showcasing key features like MVC Architecture, microservice architecture, JDBC for database interactions (Upgrade to JPA). Each part of the code is meticulously commented on to ensure clarity and ease of understanding. You can explore the repository here and follow along to enhance your Spring Boot MVC skills.
Conclusion
By following these best practices, you can build robust, maintainable, and secure Spring MVC applications. These practices ensure that your application is well-structured, efficient, and easy to manage.
Happy coding !!!
Subscribe to my newsletter
Read articles from Prashant Bale directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Prashant Bale
Prashant Bale
With 17+ years in software development and 14+ years specializing in Android app architecture and development, I am a seasoned Lead Android Developer. My comprehensive knowledge spans all phases of mobile application development, particularly within the banking domain. I excel at transforming business needs into secure, user-friendly solutions known for their scalability and durability. As a proven leader and Mobile Architect.