Spring Boot API Validation, Global Exception Handling & Secure Versioning
This guide walks through building robust Spring Boot backend APIs by detailing request parameter validation with @Validator, implementing global exception handling, standardizing unified response structures, applying custom annotations for response control, managing API versioning via path and header strategies, and enhancing security with token authentication, timestamps, signatures, and HTTPS.
Overview
Backend API development with Spring Boot is divided into URL, request method, request data and response data. Standardization is essential.
Environment
Include spring-boot-starter-web, lombok, knife4j-spring-boot-starter and spring-boot-starter-validation dependencies.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>Parameter Validation
Three approaches are discussed: service‑layer validation, Validator + BindingResult, and Validator + automatic exception. The article recommends the third method, where @Valid triggers a validation exception automatically.
@PostMapping("/addUser")
public String addUser(@RequestBody @Validated User user, BindingResult bindingResult) {
List<ObjectError> allErrors = bindingResult.getAllErrors();
if (!allErrors.isEmpty()) {
return allErrors.stream()
.map(o -> o.getDefaultMessage())
.collect(Collectors.toList())
.toString();
}
return validationService.addUser(user);
} @PostMapping("/addUser")
public String addUser(@RequestBody @Validated User user) {
return validationService.addUser(user);
} @Data
public class User {
@NotNull(message = "用户id不能为空")
private Long id;
@NotNull(message = "用户账号不能为空")
@Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")
private String account;
@NotNull(message = "用户密码不能为空")
@Size(min = 6, max = 16, message = "密码长度必须是6-16个字符")
private String password;
@NotNull(message = "用户邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}Global Exception Handling
Define a @RestControllerAdvice class with @ExceptionHandler methods for MethodArgumentNotValidException, BindException, custom APIException, and generic exceptions, returning a unified ResultVO object.
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResultVO<String> handle(MethodArgumentNotValidException e) {
List<String> msgs = e.getBindingResult().getFieldErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
return new ResultVO<>(ResultCode.VALIDATE_FAILED, msgs);
}
// other handlers omitted for brevity
}Unified Response
Define ResultCode enum and ResultVO<T> class to wrap data, code and message. Controllers return ResultVO instead of raw objects.
public enum ResultCode {
SUCCESS(1000, "操作成功"),
FAILED(1001, "响应失败"),
VALIDATE_FAILED(1002, "参数校验失败"),
ERROR(5000, "未知错误");
private final int code;
private final String msg;
ResultCode(int code, String msg) { this.code = code; this.msg = msg; }
} public class ResultVO<T> {
private int code;
private String msg;
private T data;
public ResultVO(T data) { this(ResultCode.SUCCESS, data); }
public ResultVO(ResultCode rc, T data) {
this.code = rc.getCode();
this.msg = rc.getMsg();
this.data = data;
}
}Optional Response Wrapping
A custom annotation @NotResponseBody can be placed on methods to skip the global wrapper. A ResponseBodyAdvice implementation checks for this annotation.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface NotResponseBody { } @RestControllerAdvice
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return !(returnType.getParameterType().equals(ResultVO.class) ||
returnType.hasMethodAnnotation(NotResponseBody.class));
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (returnType.getGenericParameterType().equals(String.class)) {
try {
return new ObjectMapper().writeValueAsString(new ResultVO<>(body));
} catch (JsonProcessingException e) {
throw new APIException("返回String类型错误");
}
}
return new ResultVO<>(body);
}
}API Version Control
Two strategies: path‑based versioning using a custom @ApiVersion annotation and a PathVersionHandlerMapping, and header‑based versioning checking the X‑VERSION header.
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
String value() default "1.0";
} public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
private final String version;
public ApiVersionCondition(String version) { this.version = version; }
@Override
public ApiVersionCondition combine(ApiVersionCondition other) { return new ApiVersionCondition(other.version); }
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
Matcher m = Pattern.compile("v(\\d+\\.\\d+)").matcher(request.getRequestURI());
if (m.find() && Objects.equals(m.group(1), version)) { return this; }
return null;
}
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) { return 0; }
public String getApiVersion() { return version; }
} @Configuration
public class WebMvcConfiguration implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new PathVersionHandlerMapping();
}
}API Security
Discuss token authentication, timestamp validation, URL signing, replay protection, and HTTPS. Token is generated as md5(userId + timestamp + secretKey) and stored in Redis. Each request includes token, timestamp and sign; the server verifies all three and caches the signature to prevent replay attacks.
Conclusion
The article assembles validation, global exception handling, unified responses, versioning and security into a coherent Spring Boot backend API framework, allowing developers to focus on business logic while maintaining consistency and safety.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.
