Spring Boot Web 开发
1. Web 开发概述
1.1 Spring Boot Web 特性
Spring Boot 为 Web 开发提供了开箱即用的支持:
- 内嵌 Servlet 容器(Tomcat、Jetty、Undertow)
- 自动配置 Spring MVC
- 支持 RESTful API
- 支持 WebSocket
- 支持模板引擎
1.2 常用依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>2. RESTful API
2.1 RESTful 设计原则
| HTTP 方法 | 操作 | 示例 |
|---|---|---|
| GET | 查询 | GET /users |
| POST | 创建 | POST /users |
| PUT | 更新(全量) | PUT /users/1 |
| PATCH | 更新(部分) | PATCH /users/1 |
| DELETE | 删除 | DELETE /users/1 |
2.2 控制器开发
java
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<User> getAll() {
return userService.findAll();
}
@GetMapping("/{id}")
public User getById(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public User create(@Valid @RequestBody UserRequest request) {
return userService.create(request);
}
@PutMapping("/{id}")
public User update(@PathVariable Long id, @Valid @RequestBody UserRequest request) {
return userService.update(id, request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
userService.delete(id);
}
}2.3 统一响应格式
java
public record ApiResponse<T>(
int code,
String message,
T data,
long timestamp
) {
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "success", data, System.currentTimeMillis());
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(200, message, data, System.currentTimeMillis());
}
public static <T> ApiResponse<T> error(int code, String message) {
return new ApiResponse<>(code, message, null, System.currentTimeMillis());
}
}
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@GetMapping("/{id}")
public ApiResponse<User> getById(@PathVariable Long id) {
User user = userService.findById(id);
return ApiResponse.success(user);
}
@PostMapping
public ApiResponse<User> create(@Valid @RequestBody UserRequest request) {
User user = userService.create(request);
return ApiResponse.success("创建成功", user);
}
}3. 参数验证
3.1 验证注解
| 注解 | 说明 |
|---|---|
| @NotNull | 不能为 null |
| @NotBlank | 不能为空白 |
| @NotEmpty | 不能为空 |
| @Size | 字符串/集合长度 |
| @Min | 最小值 |
| @Max | 最大值 |
| @Pattern | 正则表达式 |
| 邮箱格式 | |
| @Past | 过去日期 |
| @Future | 未来日期 |
3.2 请求验证
java
public record UserRequest(
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度3-20位")
String username,
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$",
message = "密码至少8位,包含字母和数字")
String password,
@Email(message = "邮箱格式不正确")
String email,
@Min(value = 18, message = "年龄必须大于等于18")
@Max(value = 100, message = "年龄必须小于等于100")
Integer age
) {}
@RestController
public class UserController {
@PostMapping("/users")
public User create(@Valid @RequestBody UserRequest request) {
return userService.create(request);
}
}3.3 全局异常处理
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationException(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest()
.body(ApiResponse.error(400, "参数验证失败", errors));
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleConstraintViolation(
ConstraintViolationException ex) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach(violation ->
errors.put(violation.getPropertyPath().toString(),
violation.getMessage()));
return ResponseEntity.badRequest()
.body(ApiResponse.error(400, "参数验证失败", errors));
}
}4. 文件上传下载
4.1 文件上传
java
@RestController
@RequestMapping("/api/files")
public class FileController {
private static final String UPLOAD_DIR = "uploads/";
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
throw new BusinessException("文件不能为空");
}
String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
Path path = Paths.get(UPLOAD_DIR + fileName);
try {
Files.createDirectories(path.getParent());
Files.copy(file.getInputStream(), path);
return fileName;
} catch (IOException e) {
throw new BusinessException("文件上传失败: " + e.getMessage());
}
}
@PostMapping("/uploads")
public List<String> uploadMultiple(@RequestParam("files") MultipartFile[] files) {
List<String> fileNames = new ArrayList<>();
for (MultipartFile file : files) {
if (!file.isEmpty()) {
String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
Path path = Paths.get(UPLOAD_DIR + fileName);
try {
Files.copy(file.getInputStream(), path);
fileNames.add(fileName);
} catch (IOException e) {
throw new BusinessException("文件上传失败");
}
}
}
return fileNames;
}
}4.2 文件下载
java
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> download(@PathVariable String fileName) {
Path path = Paths.get(UPLOAD_DIR + fileName);
Resource resource = new FileSystemResource(path);
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
String contentType = Files.probeContentType(path);
if (contentType == null) {
contentType = "application/octet-stream";
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + URLEncoder.encode(fileName, "UTF-8") + "\"")
.body(resource);
}4.3 配置文件大小限制
yaml
spring:
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 100MB5. 异步请求
5.1 Callable
java
@GetMapping("/async")
public Callable<String> asyncRequest() {
return () -> {
Thread.sleep(2000);
return "async-result";
};
}5.2 DeferredResult
java
@GetMapping("/deferred")
public DeferredResult<String> deferredRequest() {
DeferredResult<String> result = new DeferredResult<>(5000L, "timeout");
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(2000);
result.setResult("deferred-result");
} catch (InterruptedException e) {
result.setErrorResult(e);
}
});
return result;
}5.3 CompletableFuture
java
@GetMapping("/future")
public CompletableFuture<String> futureRequest() {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
return "future-result";
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}6. 跨域配置
6.1 @CrossOrigin
java
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "http://localhost:3000")
public class ApiController {
@GetMapping("/users")
@CrossOrigin(origins = "*")
public List<User> getUsers() {
return userService.findAll();
}
}6.2 全局配置
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}6.3 CorsFilter
java
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}7. 全局异常处理
7.1 自定义异常
java
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(String message) {
super(message);
this.code = 400;
}
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}7.2 全局异常处理器
java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
log.warn("Business exception: {}", ex.getMessage());
return ResponseEntity.status(ex.getCode())
.body(ApiResponse.error(ex.getCode(), ex.getMessage()));
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleResourceNotFoundException(
ResourceNotFoundException ex) {
log.warn("Resource not found: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error(404, ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationException(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest()
.body(ApiResponse.error(400, "参数验证失败", errors));
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ApiResponse<Void>> handleMethodNotSupported(
HttpRequestMethodNotSupportedException ex) {
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(ApiResponse.error(405, "不支持的请求方法: " + ex.getMethod()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
log.error("Unexpected exception", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error(500, "服务器内部错误"));
}
}8. 请求日志
8.1 请求日志过滤器
java
@Component
@Slf4j
public class RequestLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
ContentCachingRequestWrapper requestWrapper =
new ContentCachingRequestWrapper(httpRequest);
ContentCachingResponseWrapper responseWrapper =
new ContentCachingResponseWrapper((HttpServletResponse) response);
long startTime = System.currentTimeMillis();
try {
chain.doFilter(requestWrapper, responseWrapper);
} finally {
long duration = System.currentTimeMillis() - startTime;
logRequest(requestWrapper);
logResponse(responseWrapper, duration);
responseWrapper.copyBodyToResponse();
}
}
private void logRequest(ContentCachingRequestWrapper request) {
String requestBody = new String(request.getContentAsByteArray());
log.info("Request: {} {} - Body: {}",
request.getMethod(),
request.getRequestURI(),
requestBody);
}
private void logResponse(ContentCachingResponseWrapper response, long duration) {
String responseBody = new String(response.getContentAsByteArray());
log.info("Response: {} - {} ms - Body: {}",
response.getStatus(),
duration,
responseBody);
}
}8.2 使用 AOP 记录日志
java
@Aspect
@Component
@Slf4j
public class ControllerLogAspect {
@Around("execution(* com.example.controller..*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("Request: {}.{}() - Args: {}", className, methodName, args);
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
log.info("Response: {}.{}() - {} ms - Result: {}",
className, methodName, duration, result);
return result;
}
}9. API 文档
9.1 添加依赖
xml
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>9.2 配置 OpenAPI
java
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("API 文档")
.version("1.0.0")
.description("Spring Boot 4 API 文档")
.contact(new Contact()
.name("开发团队")
.email("dev@example.com")))
.addSecurityItem(new SecurityRequirement().addList("Bearer"))
.components(new Components()
.addSecuritySchemes("Bearer",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}9.3 使用注解
java
@RestController
@RequestMapping("/api/users")
@Tag(name = "用户管理", description = "用户相关接口")
public class UserController {
@Operation(summary = "获取用户列表", description = "获取所有用户")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "成功"),
@ApiResponse(responseCode = "401", description = "未授权")
})
@GetMapping
public List<User> getAll() {
return userService.findAll();
}
@Operation(summary = "创建用户", description = "创建新用户")
@PostMapping
public User create(@RequestBody @Schema(description = "用户信息") UserRequest request) {
return userService.create(request);
}
}10. 小结
本章学习了 Spring Boot Web 开发的核心内容:
| 内容 | 要点 |
|---|---|
| RESTful API | HTTP 方法、资源设计 |
| 参数验证 | @Valid、验证注解 |
| 文件上传 | MultipartFile、大小限制 |
| 异步请求 | Callable、DeferredResult |
| 跨域配置 | @CrossOrigin、全局配置 |
| 异常处理 | @RestControllerAdvice |
| 请求日志 | Filter、AOP |
| API 文档 | SpringDoc、OpenAPI |
下一章将学习 Spring Boot 数据访问。