Skip to content

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正则表达式
@Email邮箱格式
@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: 100MB

5. 异步请求

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 APIHTTP 方法、资源设计
参数验证@Valid、验证注解
文件上传MultipartFile、大小限制
异步请求Callable、DeferredResult
跨域配置@CrossOrigin、全局配置
异常处理@RestControllerAdvice
请求日志Filter、AOP
API 文档SpringDoc、OpenAPI

下一章将学习 Spring Boot 数据访问。