Comprehensive Guide to Backend API Development with Spring Boot: Validation, Global Exception Handling, Unified Responses, Versioning, and Security
This article provides a detailed tutorial on building robust Spring Boot backend APIs, covering interface structure, environment setup, parameter validation methods, custom validators, global exception handling, unified response formats, optional response wrapping, API version control, and security measures such as token authentication and request signing.
This article presents a step‑by‑step tutorial for constructing a well‑structured Spring Boot backend API, covering everything from interface components to security mechanisms.
Environment
To follow the examples you need to import spring-boot-starter-web , Lombok for boilerplate reduction, and Knife4j for API documentation. Since Spring Boot 2.3 the validation starter is a separate dependency.
<dependency>
<!-- manual import of validation starter -->
<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
Introduction
An interface usually validates request parameters. Three common ways are:
Business‑layer validation (manual checks in the service layer)
Validator + BindingResult (explicit BindingResult parameter)
Validator + automatic exception (use @Valid and let Spring throw an exception)
Example using Validator + BindingResult :
@PostMapping("/addUser")
public String addUser(@RequestBody @Validated User user, BindingResult bindingResult) {
List
allErrors = bindingResult.getAllErrors();
if (!allErrors.isEmpty()) {
return allErrors.stream()
.map(o -> o.getDefaultMessage())
.collect(Collectors.toList()).toString();
}
return validationService.addUser(user);
}Validator + Automatic Exception
Define validation rules on the DTO and annotate the method parameter with @Valid . Spring will automatically throw MethodArgumentNotValidException on failure.
@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;
} @RestController
@RequestMapping("user")
public class ValidationController {
@Autowired
private ValidationService validationService;
@PostMapping("/addUser")
public String addUser(@RequestBody @Validated User user) {
return validationService.addUser(user);
}
}Global Exception Handling
Basic Usage
Use @ControllerAdvice or @RestControllerAdvice to define a class that handles exceptions globally.
@RestControllerAdvice
@ResponseBody
public class ExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return objectError.getDefaultMessage();
}
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public ResultVO
handleUnexpectedServer(Exception ex) {
log.error("系统异常:", ex);
return new ResultVO<>(GlobalMsgEnum.ERROR);
}
@ExceptionHandler(Throwable.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public ResultVO
exception(Throwable ex) {
log.error("系统异常:", ex);
return new ResultVO<>(GlobalMsgEnum.ERROR);
}
}Custom Exception
Define a custom annotation and a validator, then create a custom exception class.
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { HaveNoBlankValidator.class })
public @interface HaveNoBlank {
String message() default "字符串中不能含有空格";
Class
[] groups() default {};
Class
[] payload() default {};
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface List {
NotBlank[] value();
}
}
public class HaveNoBlankValidator implements ConstraintValidator
{
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
return !value.contains(" ");
}
} public class APIException extends RuntimeException {
private int code;
private String msg;
public APIException() {
this(1001, "接口错误");
}
public APIException(String msg) {
this(1001, msg);
}
public APIException(int code, String msg) {
super(msg);
this.code = code;
this.msg = msg;
}
}Unified Response
Define an enum for result codes and a generic wrapper class.
@Getter
public enum ResultCode {
SUCCESS(1000, "操作成功"),
FAILED(1001, "响应失败"),
VALIDATE_FAILED(1002, "参数校验失败"),
ERROR(5000, "未知错误");
private int code;
private String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
} package com.csdn.demo1.global;
import lombok.Getter;
@Getter
public class ResultVO
{
private int code;
private String msg;
private T data;
public ResultVO(T data) {
this(ResultCode.SUCCESS, data);
}
public ResultVO(ResultCode resultCode, T data) {
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
this.data = data;
}
}Optional Response Wrapping
Implement ResponseBodyAdvice to automatically wrap non‑ResultVO responses, with a custom @NotResponseBody switch.
@Retention(RUNTIME)
@Target({ElementType.METHOD})
public @interface NotResponseBody {}
@RestControllerAdvice
public class ResponseControllerAdvice implements ResponseBodyAdvice
{
@Override
public boolean supports(MethodParameter returnType, Class
> aClass) {
return !(returnType.getParameterType().equals(ResultVO.class) ||
returnType.hasMethodAnnotation(NotResponseBody.class));
}
@Override
public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType,
Class
> aClass,
ServerHttpRequest request, ServerHttpResponse response) {
if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(new ResultVO<>(data));
} catch (JsonProcessingException e) {
throw new APIException("返回String类型错误");
}
}
return new ResultVO<>(data);
}
}API Version Control
Path‑Based Versioning
Define @ApiVersion , a condition class, a custom handler mapping, and register it via WebMvcConfiguration .
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RUNTIME)
public @interface ApiVersion {
String value() default "1.0";
} public class ApiVersionCondition implements RequestCondition
{
private static final Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+\\.\\d+)");
private final String version;
public ApiVersionCondition(String version) { this.version = version; }
@Override
public ApiVersionCondition combine(ApiVersionCondition other) { return new ApiVersionCondition(other.getApiVersion()); }
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI());
if (m.find()) {
String pathVersion = m.group(1);
if (Objects.equals(pathVersion, version)) {
return this;
}
}
return null;
}
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) { return 0; }
public String getApiVersion() { return version; }
} public class PathVersionHandlerMapping extends RequestMappingHandlerMapping {
@Override
protected boolean isHandler(Class
beanType) {
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
}
@Override
protected RequestCondition
getCustomTypeCondition(Class
handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
return createCondition(apiVersion);
}
@Override
protected RequestCondition
getCustomMethodCondition(Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
return createCondition(apiVersion);
}
private RequestCondition
createCondition(ApiVersion apiVersion) {
return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value());
}
} @Configuration
public class WebMvcConfiguration implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new PathVersionHandlerMapping();
}
}Header‑Based Versioning
Modify the condition to read the X-VERSION header.
public class ApiVersionCondition implements RequestCondition
{
private static final String X_VERSION = "X-VERSION";
private final String version;
public ApiVersionCondition(String version) { this.version = version; }
@Override
public ApiVersionCondition combine(ApiVersionCondition other) { return new ApiVersionCondition(other.getApiVersion()); }
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
String headerVersion = request.getHeader(X_VERSION);
if (Objects.equals(version, headerVersion)) {
return this;
}
return null;
}
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) { return 0; }
public String getApiVersion() { return version; }
}API Security
Key security measures include token authentication, timestamp validation, URL signing, replay protection, and HTTPS.
Token = md5(userId + loginTimestamp + serverSecret) stored in Redis.
Clients send a timestamp parameter; server rejects requests whose time difference exceeds a configured window.
All request parameters (including token and timestamp) are sorted, concatenated, appended with a secret, and hashed with MD5 to produce a sign . The server recomputes the sign to verify integrity.
The generated sign is cached in Redis with the same expiration as the timestamp to prevent replay attacks.
HTTPS is used to encrypt the transport layer.
Conclusion
By combining validator‑based parameter checks, global exception handling, unified response structures, optional response wrapping, flexible version control, and a comprehensive security scheme, developers can build clean, maintainable, and secure backend APIs that keep business logic focused and reduce repetitive boilerplate.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.