Master Spring Boot Parameter Validation: From Bean Validation to Global Exception Handling
This tutorial walks through the need for robust API parameter validation in Spring Boot, introduces the Bean Validation specification and its implementations, explains key constraint annotations, demonstrates a quick start with Maven dependencies, shows how to build DTOs and controllers, and details a unified global exception handling strategy for clean error responses.
1. Overview
When providing reliable API endpoints, validating request parameters is essential to ensure data integrity before persisting to the database; manual if‑else checks quickly become cumbersome, especially for complex objects like product creation.
<code>/**
* Verify if the parameters are correct
*/
private void verifyForm(SysMenuEntity menu) {
if (StringUtils.isBlank(menu.getName())) {
throw new RRException("菜单名称不能为空");
}
if (menu.getParentId() == null) {
throw new RRException("上级菜单不能为空");
}
// ... other validation logic ...
}
</code>Relying solely on front‑end checks is unsafe because attackers can bypass browsers and send malformed requests directly to the backend, e.g., SQL injection attacks. A standardized, reusable validation mechanism is therefore required.
2. Bean Validation
The Java ecosystem introduced the Bean Validation specification in 2009 (JSR‑303) and has evolved through JSR‑349 and JSR‑380 to version 2.0. Jakarta Bean Validation 3.0 merely changes the package name but retains the same functionality.
Popular implementations include:
Hibernate Validator
Apache BVal
Although Hibernate is often associated with ORM, it also provides validation capabilities and is widely used in Spring projects.
3. Annotations
Common constraint annotations from
javax.validation.constraints(22 in total) can be grouped as follows:
3.1 Null and Empty Checks
@NotBlank: String must be non‑null and trimmed length > 0
@NotEmpty: Collection or string must not be empty
@NotNull: Value cannot be null
@Null: Value must be null
3.2 Numeric Checks
@DecimalMax,
@DecimalMin @Digits @Positive,
@PositiveOrZero @Max,
@Min @Negative,
@NegativeOrZero3.3 Boolean Checks
@AssertTrue,
@AssertFalse3.4 Size Checks
@Size: Applies to strings, arrays, collections, maps
3.5 Date Checks
@Future,
@FutureOrPresent @Past,
@PastOrPresent3.6 Others
@Email @Pattern3.7 Hibernate Validator Extensions
@Range @Length @URL @SafeHtml3.8 @Valid vs @Validated
@Valid(from
javax.validation) triggers validation on method parameters, return values, and fields, supporting nested validation.
@Validated(from Spring) enables group validation and is typically placed on a controller class to apply AOP‑based validation to all handler methods.
4. Quick Start
4.1 Add Maven Dependencies
<code><project xmlns="http://maven.apache.org/POM/4.0.0" ...>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring AOP for validation interceptors -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<!-- Lombok for boilerplate reduction -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Knife4j for API documentation -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
</dependencies>
</project>
</code>Spring Boot’s
spring-boot-starter-webalready pulls in
spring-boot-starter-validation, which includes
hibernate-validator, so no extra validator dependency is needed.
4.2 Create DTO
<code>package com.ratel.validation.entity;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@Data
public class UserAddDTO {
@NotEmpty(message = "登录账号不能为空")
@Length(min = 5, max = 16, message = "账号长度为 5-16 位")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
private String username;
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
}
</code>4.3 Create Controller
<code>package com.ratel.validation.cotroller;
import com.ratel.validation.entity.UserAddDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;
@RestController
@RequestMapping("/users")
@Validated
public class UserController {
private Logger logger = LoggerFactory.getLogger(getClass());
@GetMapping("/get")
public UserAddDTO get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id) {
logger.info("[get][id: {}]", id);
UserAddDTO dto = new UserAddDTO();
dto.setUsername("张三");
dto.setPassword("123456");
return dto;
}
@PostMapping("/add")
public void add(@Valid @RequestBody UserAddDTO addDTO) {
logger.info("[add][addDTO: {}]", addDTO);
}
}
</code>Run the application and use Swagger (e.g.,
http://localhost:8080/doc.html#/home) to test the endpoints. Invalid parameters trigger validation errors.
4.4 Handling Validation Exceptions
Define a global exception handler to convert various validation failures into a unified JSON response.
<code>package com.ratel.validation.exception;
import com.ratel.validation.common.CommonResult;
import com.ratel.validation.enums.ServiceExceptionEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
@ControllerAdvice(basePackages = "com.ratel.validation.cotroller")
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@ResponseBody
@ExceptionHandler(ConstraintViolationException.class)
public CommonResult constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {
logger.error("[constraintViolationExceptionHandler]", ex);
StringBuilder detail = new StringBuilder();
ex.getConstraintViolations().forEach(v -> {
if (detail.length() > 0) detail.append(";");
detail.append(v.getMessage());
});
return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detail);
}
@ResponseBody
@ExceptionHandler(BindException.class)
public CommonResult bindExceptionHandler(HttpServletRequest req, BindException ex) {
logger.error("[bindExceptionHandler]", ex);
StringBuilder detail = new StringBuilder();
for (ObjectError err : ex.getAllErrors()) {
if (detail.length() > 0) detail.append(";");
detail.append(err.getDefaultMessage());
}
return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detail);
}
@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
public CommonResult methodArgumentNotValidExceptionHandler(HttpServletRequest req, MethodArgumentNotValidException ex) {
logger.error("[MethodArgumentNotValidException]", ex);
StringBuilder detail = new StringBuilder();
ex.getBindingResult().getAllErrors().forEach(err -> {
if (detail.length() > 0) detail.append(";");
detail.append(err.getDefaultMessage());
});
return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detail);
}
@ResponseBody
@ExceptionHandler(Exception.class)
public CommonResult exceptionHandler(HttpServletRequest req, Exception e) {
logger.error("[exceptionHandler]", e);
return CommonResult.error(ServiceExceptionEnum.SYS_ERROR.getCode(),
ServiceExceptionEnum.SYS_ERROR.getMessage());
}
}
</code>After adding the handler, validation errors are returned as concise JSON with a clear
messagefield, improving client‑side usability.
5. Testing
Calling
GET /users/get?id=-1returns a 500 error due to
ConstraintViolationException. Calling
POST /users/addwith an invalid body returns a 400 error with a merged error message such as "账号长度为 5-16 位;密码长度为 4-16 位".
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.