SSM入门到实战: 3.6 SpringMVC RESTful API开发

👋 大家好,我是 阿问学长!专注于分享优质开源项目解析、毕业设计项目指导支持、幼小初高教辅资料推荐等,欢迎关注交流!🚀

20-SpringMVC RESTful API开发

📖 本文概述

本文是SSM框架系列SpringMVC进阶篇的最后一篇,将深入探讨如何使用SpringMVC开发RESTful API。通过详细的代码示例和最佳实践,帮助读者掌握现代Web API开发的核心技能。

🎯 学习目标

  • 深入理解RESTful架构风格和设计原则
  • 掌握SpringMVC RESTful API开发技巧
  • 学会处理JSON数据和内容协商
  • 了解API版本控制和文档生成
  • 掌握RESTful API的测试方法

1. RESTful架构概述

1.1 REST基本概念

java 复制代码
/**
 * RESTful架构概述
 */
public class RESTfulOverview {
    
    /**
     * REST (Representational State Transfer) 是一种架构风格,
     * 它定义了一组约束条件和原则,用于创建Web服务。
     * 
     * REST的核心原则:
     * 
     * 1. 统一接口 (Uniform Interface)
     *    - 资源标识:每个资源都有唯一的URI
     *    - 资源操作:通过HTTP方法操作资源
     *    - 自描述消息:消息包含足够的信息来描述如何处理
     *    - 超媒体驱动:通过超链接发现可用操作
     * 
     * 2. 无状态 (Stateless)
     *    - 每个请求都包含处理该请求所需的所有信息
     *    - 服务器不保存客户端状态
     * 
     * 3. 可缓存 (Cacheable)
     *    - 响应数据可以被缓存以提高性能
     * 
     * 4. 客户端-服务器分离 (Client-Server)
     *    - 客户端和服务器独立演化
     * 
     * 5. 分层系统 (Layered System)
     *    - 系统可以由多层组成,每层只知道相邻层
     * 
     * 6. 按需代码 (Code on Demand) - 可选
     *    - 服务器可以向客户端发送可执行代码
     */
    
    /**
     * HTTP方法与CRUD操作的对应关系:
     * 
     * GET    - 读取 (Read)
     * POST   - 创建 (Create)
     * PUT    - 更新 (Update) - 完整更新
     * PATCH  - 更新 (Update) - 部分更新
     * DELETE - 删除 (Delete)
     * HEAD   - 获取资源元数据
     * OPTIONS - 获取资源支持的操作
     */
    
    /**
     * RESTful URL设计原则:
     * 
     * 1. 使用名词而不是动词
     *    ✅ GET /users/123
     *    ❌ GET /getUser/123
     * 
     * 2. 使用复数形式
     *    ✅ GET /users
     *    ❌ GET /user
     * 
     * 3. 使用层次结构表示关系
     *    ✅ GET /users/123/orders
     *    ❌ GET /getUserOrders/123
     * 
     * 4. 使用查询参数进行过滤、排序、分页
     *    ✅ GET /users?status=active&page=1&size=10
     *    ❌ GET /users/active/page1/size10
     */
}

1.2 HTTP状态码规范

java 复制代码
/**
 * HTTP状态码使用规范
 */
public class HTTPStatusCodes {
    
    /**
     * 2xx 成功状态码
     */
    public static final int OK = 200;                    // 请求成功
    public static final int CREATED = 201;              // 资源创建成功
    public static final int ACCEPTED = 202;             // 请求已接受,正在处理
    public static final int NO_CONTENT = 204;           // 请求成功,无返回内容
    
    /**
     * 3xx 重定向状态码
     */
    public static final int NOT_MODIFIED = 304;         // 资源未修改
    
    /**
     * 4xx 客户端错误状态码
     */
    public static final int BAD_REQUEST = 400;          // 请求参数错误
    public static final int UNAUTHORIZED = 401;         // 未授权
    public static final int FORBIDDEN = 403;            // 禁止访问
    public static final int NOT_FOUND = 404;            // 资源不存在
    public static final int METHOD_NOT_ALLOWED = 405;   // 方法不允许
    public static final int CONFLICT = 409;             // 资源冲突
    public static final int UNPROCESSABLE_ENTITY = 422; // 请求格式正确但语义错误
    
    /**
     * 5xx 服务器错误状态码
     */
    public static final int INTERNAL_SERVER_ERROR = 500; // 服务器内部错误
    public static final int NOT_IMPLEMENTED = 501;       // 功能未实现
    public static final int SERVICE_UNAVAILABLE = 503;   // 服务不可用
    
    /**
     * 状态码使用示例
     */
    public void statusCodeExamples() {
        /*
         * GET /users/123
         * 200 OK - 用户存在,返回用户信息
         * 404 Not Found - 用户不存在
         * 
         * POST /users
         * 201 Created - 用户创建成功
         * 400 Bad Request - 请求参数错误
         * 409 Conflict - 用户已存在
         * 
         * PUT /users/123
         * 200 OK - 更新成功,返回更新后的用户信息
         * 204 No Content - 更新成功,无返回内容
         * 404 Not Found - 用户不存在
         * 
         * DELETE /users/123
         * 204 No Content - 删除成功
         * 404 Not Found - 用户不存在
         */
    }
}

2. SpringMVC RESTful API开发

2.1 基础RESTful控制器

java 复制代码
/**
 * 用户RESTful API控制器
 */
@RestController
@RequestMapping("/api/v1/users")
@CrossOrigin(origins = "*", maxAge = 3600)
public class UserRestController {
    
    @Autowired
    private UserService userService;
    
    /**
     * 获取所有用户 - 支持分页和过滤
     * GET /api/v1/users?page=1&size=10&status=active&keyword=john
     */
    @GetMapping
    public ResponseEntity<ApiResponse<PageResult<UserDTO>>> getUsers(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(required = false) String status,
            @RequestParam(required = false) String keyword,
            @RequestParam(defaultValue = "id") String sortBy,
            @RequestParam(defaultValue = "asc") String sortDir) {
        
        UserQueryParams params = UserQueryParams.builder()
                .page(page)
                .size(size)
                .status(status)
                .keyword(keyword)
                .sortBy(sortBy)
                .sortDir(sortDir)
                .build();
        
        PageResult<UserDTO> result = userService.findUsers(params);
        
        return ResponseEntity.ok(ApiResponse.success(result));
    }
    
    /**
     * 根据ID获取用户
     * GET /api/v1/users/{id}
     */
    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<UserDTO>> getUserById(@PathVariable Long id) {
        UserDTO user = userService.findById(id);
        
        if (user == null) {
            return ResponseEntity.notFound().build();
        }
        
        return ResponseEntity.ok(ApiResponse.success(user));
    }
    
    /**
     * 创建新用户
     * POST /api/v1/users
     */
    @PostMapping
    public ResponseEntity<ApiResponse<UserDTO>> createUser(
            @Valid @RequestBody CreateUserRequest request,
            BindingResult bindingResult) {
        
        if (bindingResult.hasErrors()) {
            return ResponseEntity.badRequest()
                    .body(ApiResponse.error("参数验证失败", getValidationErrors(bindingResult)));
        }
        
        try {
            UserDTO createdUser = userService.createUser(request);
            
            URI location = ServletUriComponentsBuilder
                    .fromCurrentRequest()
                    .path("/{id}")
                    .buildAndExpand(createdUser.getId())
                    .toUri();
            
            return ResponseEntity.created(location)
                    .body(ApiResponse.success(createdUser));
                    
        } catch (UserAlreadyExistsException e) {
            return ResponseEntity.status(HttpStatus.CONFLICT)
                    .body(ApiResponse.error("用户已存在", e.getMessage()));
        }
    }
    
    /**
     * 完整更新用户
     * PUT /api/v1/users/{id}
     */
    @PutMapping("/{id}")
    public ResponseEntity<ApiResponse<UserDTO>> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody UpdateUserRequest request,
            BindingResult bindingResult) {
        
        if (bindingResult.hasErrors()) {
            return ResponseEntity.badRequest()
                    .body(ApiResponse.error("参数验证失败", getValidationErrors(bindingResult)));
        }
        
        try {
            UserDTO updatedUser = userService.updateUser(id, request);
            return ResponseEntity.ok(ApiResponse.success(updatedUser));
            
        } catch (UserNotFoundException e) {
            return ResponseEntity.notFound().build();
        }
    }
    
    /**
     * 部分更新用户
     * PATCH /api/v1/users/{id}
     */
    @PatchMapping("/{id}")
    public ResponseEntity<ApiResponse<UserDTO>> patchUser(
            @PathVariable Long id,
            @RequestBody Map<String, Object> updates) {
        
        try {
            UserDTO updatedUser = userService.patchUser(id, updates);
            return ResponseEntity.ok(ApiResponse.success(updatedUser));
            
        } catch (UserNotFoundException e) {
            return ResponseEntity.notFound().build();
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest()
                    .body(ApiResponse.error("无效的更新字段", e.getMessage()));
        }
    }
    
    /**
     * 删除用户
     * DELETE /api/v1/users/{id}
     */
    @DeleteMapping("/{id}")
    public ResponseEntity<ApiResponse<Void>> deleteUser(@PathVariable Long id) {
        try {
            userService.deleteUser(id);
            return ResponseEntity.noContent().build();
            
        } catch (UserNotFoundException e) {
            return ResponseEntity.notFound().build();
        }
    }
    
    /**
     * 批量删除用户
     * DELETE /api/v1/users
     */
    @DeleteMapping
    public ResponseEntity<ApiResponse<BatchOperationResult>> batchDeleteUsers(
            @RequestBody List<Long> userIds) {
        
        BatchOperationResult result = userService.batchDeleteUsers(userIds);
        return ResponseEntity.ok(ApiResponse.success(result));
    }
    
    /**
     * 获取用户的订单
     * GET /api/v1/users/{id}/orders
     */
    @GetMapping("/{id}/orders")
    public ResponseEntity<ApiResponse<List<OrderDTO>>> getUserOrders(
            @PathVariable Long id,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        try {
            PageResult<OrderDTO> orders = userService.getUserOrders(id, page, size);
            return ResponseEntity.ok(ApiResponse.success(orders.getData()));
            
        } catch (UserNotFoundException e) {
            return ResponseEntity.notFound().build();
        }
    }
    
    /**
     * 激活/禁用用户
     * PATCH /api/v1/users/{id}/status
     */
    @PatchMapping("/{id}/status")
    public ResponseEntity<ApiResponse<UserDTO>> updateUserStatus(
            @PathVariable Long id,
            @RequestBody UpdateStatusRequest request) {
        
        try {
            UserDTO updatedUser = userService.updateUserStatus(id, request.getStatus());
            return ResponseEntity.ok(ApiResponse.success(updatedUser));
            
        } catch (UserNotFoundException e) {
            return ResponseEntity.notFound().build();
        }
    }
    
    /**
     * 重置用户密码
     * POST /api/v1/users/{id}/reset-password
     */
    @PostMapping("/{id}/reset-password")
    public ResponseEntity<ApiResponse<Void>> resetPassword(@PathVariable Long id) {
        try {
            userService.resetPassword(id);
            return ResponseEntity.ok(ApiResponse.success(null, "密码重置成功"));
            
        } catch (UserNotFoundException e) {
            return ResponseEntity.notFound().build();
        }
    }
    
    /**
     * 获取验证错误信息
     */
    private Map<String, String> getValidationErrors(BindingResult bindingResult) {
        Map<String, String> errors = new HashMap<>();
        bindingResult.getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage())
        );
        return errors;
    }
}

2.2 请求和响应DTO

java 复制代码
/**
 * 创建用户请求DTO
 */
public class CreateUserRequest {
    
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
    private String username;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{6,}$", 
             message = "密码必须包含至少一个大写字母、一个小写字母和一个数字")
    private String password;
    
    @NotBlank(message = "确认密码不能为空")
    private String confirmPassword;
    
    @NotNull(message = "年龄不能为空")
    @Min(value = 18, message = "年龄不能小于18岁")
    @Max(value = 100, message = "年龄不能大于100岁")
    private Integer age;
    
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
    
    @Size(max = 200, message = "个人简介不能超过200个字符")
    private String bio;
    
    // getter/setter...
    
    /**
     * 自定义验证:确认密码必须与密码一致
     */
    @AssertTrue(message = "确认密码与密码不一致")
    public boolean isPasswordMatching() {
        return password != null && password.equals(confirmPassword);
    }
}

/**
 * 更新用户请求DTO
 */
public class UpdateUserRequest {
    
    @Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
    private String username;
    
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @Min(value = 18, message = "年龄不能小于18岁")
    @Max(value = 100, message = "年龄不能大于100岁")
    private Integer age;
    
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
    
    @Size(max = 200, message = "个人简介不能超过200个字符")
    private String bio;
    
    // getter/setter...
}

/**
 * 用户响应DTO
 */
public class UserDTO {
    
    private Long id;
    private String username;
    private String email;
    private Integer age;
    private String phone;
    private String bio;
    private String status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    private LocalDateTime lastLoginTime;
    
    // 关联数据
    private List<RoleDTO> roles;
    private UserProfileDTO profile;
    
    // getter/setter...
}

/**
 * 统一API响应格式
 */
public class ApiResponse<T> {
    
    private boolean success;
    private String message;
    private T data;
    private Map<String, Object> meta;
    private long timestamp;
    
    public ApiResponse() {
        this.timestamp = System.currentTimeMillis();
    }
    
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setSuccess(true);
        response.setMessage("操作成功");
        response.setData(data);
        return response;
    }
    
    public static <T> ApiResponse<T> success(T data, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setSuccess(true);
        response.setMessage(message);
        response.setData(data);
        return response;
    }
    
    public static <T> ApiResponse<T> error(String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setSuccess(false);
        response.setMessage(message);
        return response;
    }
    
    public static <T> ApiResponse<T> error(String message, T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setSuccess(false);
        response.setMessage(message);
        response.setData(data);
        return response;
    }
    
    // getter/setter...
}

/**
 * 分页结果DTO
 */
public class PageResult<T> {
    
    private List<T> data;
    private long total;
    private int page;
    private int size;
    private int totalPages;
    private boolean hasNext;
    private boolean hasPrevious;
    
    public PageResult(List<T> data, long total, int page, int size) {
        this.data = data;
        this.total = total;
        this.page = page;
        this.size = size;
        this.totalPages = (int) Math.ceil((double) total / size);
        this.hasNext = page < totalPages;
        this.hasPrevious = page > 1;
    }
    
    // getter/setter...
}

/**
 * 批量操作结果DTO
 */
public class BatchOperationResult {
    
    private int total;
    private int success;
    private int failed;
    private List<String> errors;
    
    public BatchOperationResult(int total, int success, int failed) {
        this.total = total;
        this.success = success;
        this.failed = failed;
        this.errors = new ArrayList<>();
    }
    
    // getter/setter...
}

3. 内容协商和数据转换

3.1 JSON配置

java 复制代码
/**
 * JSON配置
 */
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
    
    /**
     * 配置消息转换器
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // JSON转换器
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        jsonConverter.setObjectMapper(objectMapper());
        converters.add(jsonConverter);
        
        // XML转换器(可选)
        MappingJackson2XmlHttpMessageConverter xmlConverter = new MappingJackson2XmlHttpMessageConverter();
        converters.add(xmlConverter);
        
        // 字符串转换器
        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(StandardCharsets.UTF_8);
        converters.add(stringConverter);
    }
    
    /**
     * 配置ObjectMapper
     */
    @Bean
    @Primary
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        
        // 时间格式配置
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        
        // 序列化配置
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
        
        // 反序列化配置
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
        
        // 字段命名策略
        mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
        
        // 空值处理
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        
        return mapper;
    }
    
    /**
     * 配置内容协商
     */
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
                .favorParameter(true)
                .parameterName("format")
                .ignoreAcceptHeader(false)
                .useRegisteredExtensionsOnly(false)
                .defaultContentType(MediaType.APPLICATION_JSON)
                .mediaType("json", MediaType.APPLICATION_JSON)
                .mediaType("xml", MediaType.APPLICATION_XML);
    }
    
    /**
     * 配置CORS
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

3.2 自定义序列化

java 复制代码
/**
 * 自定义JSON序列化器
 */
public class UserDTOSerializer extends JsonSerializer<UserDTO> {
    
    @Override
    public void serialize(UserDTO user, JsonGenerator gen, SerializerProvider serializers) 
            throws IOException {
        
        gen.writeStartObject();
        
        gen.writeNumberField("id", user.getId());
        gen.writeStringField("username", user.getUsername());
        gen.writeStringField("email", maskEmail(user.getEmail()));
        gen.writeNumberField("age", user.getAge());
        
        if (user.getPhone() != null) {
            gen.writeStringField("phone", maskPhone(user.getPhone()));
        }
        
        gen.writeStringField("status", user.getStatus());
        
        // 格式化时间
        if (user.getCreateTime() != null) {
            gen.writeStringField("create_time", 
                user.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        }
        
        // 序列化关联对象
        if (user.getRoles() != null && !user.getRoles().isEmpty()) {
            gen.writeArrayFieldStart("roles");
            for (RoleDTO role : user.getRoles()) {
                gen.writeStartObject();
                gen.writeNumberField("id", role.getId());
                gen.writeStringField("name", role.getName());
                gen.writeEndObject();
            }
            gen.writeEndArray();
        }
        
        gen.writeEndObject();
    }
    
    private String maskEmail(String email) {
        if (email == null || !email.contains("@")) {
            return email;
        }
        String[] parts = email.split("@");
        String username = parts[0];
        if (username.length() <= 2) {
            return email;
        }
        return username.substring(0, 2) + "***@" + parts[1];
    }
    
    private String maskPhone(String phone) {
        if (phone == null || phone.length() != 11) {
            return phone;
        }
        return phone.substring(0, 3) + "****" + phone.substring(7);
    }
}

/**
 * 自定义反序列化器
 */
public class CreateUserRequestDeserializer extends JsonDeserializer<CreateUserRequest> {
    
    @Override
    public CreateUserRequest deserialize(JsonParser p, DeserializationContext ctxt) 
            throws IOException {
        
        JsonNode node = p.getCodec().readTree(p);
        CreateUserRequest request = new CreateUserRequest();
        
        // 用户名处理:去除空格并转小写
        if (node.has("username")) {
            String username = node.get("username").asText().trim().toLowerCase();
            request.setUsername(username);
        }
        
        // 邮箱处理:去除空格并转小写
        if (node.has("email")) {
            String email = node.get("email").asText().trim().toLowerCase();
            request.setEmail(email);
        }
        
        // 密码处理
        if (node.has("password")) {
            request.setPassword(node.get("password").asText());
        }
        
        if (node.has("confirm_password")) {
            request.setConfirmPassword(node.get("confirm_password").asText());
        }
        
        // 年龄处理
        if (node.has("age")) {
            request.setAge(node.get("age").asInt());
        }
        
        // 手机号处理:去除空格和特殊字符
        if (node.has("phone")) {
            String phone = node.get("phone").asText().replaceAll("[^0-9]", "");
            request.setPhone(phone);
        }
        
        if (node.has("bio")) {
            request.setBio(node.get("bio").asText());
        }
        
        return request;
    }
}

4. 异常处理和错误响应

4.1 全局异常处理器

java 复制代码
/**
 * 全局异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    /**
     * 处理参数验证异常
     */
    @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())
        );
        
        logger.warn("参数验证失败: {}", errors);
        
        return ResponseEntity.badRequest()
                .body(ApiResponse.error("参数验证失败", errors));
    }
    
    /**
     * 处理请求参数绑定异常
     */
    @ExceptionHandler(BindException.class)
    public ResponseEntity<ApiResponse<Map<String, String>>> handleBindException(BindException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage())
        );
        
        return ResponseEntity.badRequest()
                .body(ApiResponse.error("请求参数错误", errors));
    }
    
    /**
     * 处理业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
        logger.warn("业务异常: {}", ex.getMessage());
        
        return ResponseEntity.badRequest()
                .body(ApiResponse.error(ex.getMessage()));
    }
    
    /**
     * 处理资源不存在异常
     */
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleResourceNotFoundException(
            ResourceNotFoundException ex) {
        
        logger.warn("资源不存在: {}", ex.getMessage());
        
        return ResponseEntity.notFound().build();
    }
    
    /**
     * 处理权限异常
     */
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ApiResponse<Void>> handleAccessDeniedException(AccessDeniedException ex) {
        logger.warn("访问被拒绝: {}", ex.getMessage());
        
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body(ApiResponse.error("访问被拒绝"));
    }
    
    /**
     * 处理HTTP方法不支持异常
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ResponseEntity<ApiResponse<Void>> handleMethodNotSupportedException(
            HttpRequestMethodNotSupportedException ex) {
        
        String message = String.format("不支持的HTTP方法: %s", ex.getMethod());
        
        return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
                .body(ApiResponse.error(message));
    }
    
    /**
     * 处理媒体类型不支持异常
     */
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public ResponseEntity<ApiResponse<Void>> handleMediaTypeNotSupportedException(
            HttpMediaTypeNotSupportedException ex) {
        
        String message = String.format("不支持的媒体类型: %s", ex.getContentType());
        
        return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
                .body(ApiResponse.error(message));
    }
    
    /**
     * 处理JSON解析异常
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseEntity<ApiResponse<Void>> handleJsonParseException(
            HttpMessageNotReadableException ex) {
        
        logger.warn("JSON解析失败: {}", ex.getMessage());
        
        return ResponseEntity.badRequest()
                .body(ApiResponse.error("请求体格式错误"));
    }
    
    /**
     * 处理类型转换异常
     */
    @ExceptionHandler(TypeMismatchException.class)
    public ResponseEntity<ApiResponse<Void>> handleTypeMismatchException(TypeMismatchException ex) {
        String message = String.format("参数类型错误: %s", ex.getPropertyName());
        
        return ResponseEntity.badRequest()
                .body(ApiResponse.error(message));
    }
    
    /**
     * 处理数据库约束异常
     */
    @ExceptionHandler(DataIntegrityViolationException.class)
    public ResponseEntity<ApiResponse<Void>> handleDataIntegrityViolationException(
            DataIntegrityViolationException ex) {
        
        logger.error("数据完整性约束违反", ex);
        
        String message = "数据操作失败,请检查数据完整性";
        if (ex.getMessage().contains("Duplicate entry")) {
            message = "数据已存在,不能重复添加";
        }
        
        return ResponseEntity.status(HttpStatus.CONFLICT)
                .body(ApiResponse.error(message));
    }
    
    /**
     * 处理系统异常
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
        logger.error("系统异常", ex);
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error("系统内部错误"));
    }
}

/**
 * 自定义业务异常
 */
public class BusinessException extends RuntimeException {
    
    private String code;
    
    public BusinessException(String message) {
        super(message);
    }
    
    public BusinessException(String code, String message) {
        super(message);
        this.code = code;
    }
    
    public BusinessException(String message, Throwable cause) {
        super(message, cause);
    }
    
    public String getCode() {
        return code;
    }
}

/**
 * 资源不存在异常
 */
public class ResourceNotFoundException extends RuntimeException {
    
    public ResourceNotFoundException(String message) {
        super(message);
    }
    
    public ResourceNotFoundException(String resource, Object id) {
        super(String.format("%s not found with id: %s", resource, id));
    }
}

5. API版本控制

5.1 URL版本控制

java 复制代码
/**
 * URL版本控制示例
 */
@RestController
@RequestMapping("/api/v1/users")
public class UserV1Controller {
    
    @GetMapping("/{id}")
    public ResponseEntity<UserV1DTO> getUser(@PathVariable Long id) {
        // V1版本的用户信息,字段较少
        UserV1DTO user = userService.findUserV1(id);
        return ResponseEntity.ok(user);
    }
}

@RestController
@RequestMapping("/api/v2/users")
public class UserV2Controller {
    
    @GetMapping("/{id}")
    public ResponseEntity<UserV2DTO> getUser(@PathVariable Long id) {
        // V2版本的用户信息,增加了新字段
        UserV2DTO user = userService.findUserV2(id);
        return ResponseEntity.ok(user);
    }
}

5.2 请求头版本控制

java 复制代码
/**
 * 请求头版本控制
 */
@RestController
@RequestMapping("/api/users")
public class UserVersionController {
    
    @GetMapping(value = "/{id}", headers = "API-Version=1")
    public ResponseEntity<UserV1DTO> getUserV1(@PathVariable Long id) {
        UserV1DTO user = userService.findUserV1(id);
        return ResponseEntity.ok(user);
    }
    
    @GetMapping(value = "/{id}", headers = "API-Version=2")
    public ResponseEntity<UserV2DTO> getUserV2(@PathVariable Long id) {
        UserV2DTO user = userService.findUserV2(id);
        return ResponseEntity.ok(user);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<UserV2DTO> getUserDefault(@PathVariable Long id) {
        // 默认使用最新版本
        UserV2DTO user = userService.findUserV2(id);
        return ResponseEntity.ok(user);
    }
}

5.3 参数版本控制

java 复制代码
/**
 * 参数版本控制
 */
@RestController
@RequestMapping("/api/users")
public class UserParamVersionController {
    
    @GetMapping(value = "/{id}", params = "version=1")
    public ResponseEntity<UserV1DTO> getUserV1(@PathVariable Long id) {
        UserV1DTO user = userService.findUserV1(id);
        return ResponseEntity.ok(user);
    }
    
    @GetMapping(value = "/{id}", params = "version=2")
    public ResponseEntity<UserV2DTO> getUserV2(@PathVariable Long id) {
        UserV2DTO user = userService.findUserV2(id);
        return ResponseEntity.ok(user);
    }
}

6. 小结

本文深入介绍了SpringMVC RESTful API开发:

  1. RESTful架构:REST原则、HTTP方法和状态码规范
  2. API开发:控制器设计、请求响应DTO、数据验证
  3. 内容协商:JSON配置、自定义序列化、消息转换
  4. 异常处理:全局异常处理器、错误响应格式
  5. 版本控制:URL、请求头、参数等版本控制方式

掌握RESTful API开发的关键点:

  • 遵循REST架构原则和设计规范
  • 合理设计API接口和数据结构
  • 实现完善的异常处理机制
  • 考虑API的版本控制和向后兼容
  • 注重API的安全性和性能优化

🔗 下一篇预告

下一篇文章将介绍SSM项目部署与优化,学习如何将SSM项目部署到生产环境并进行性能优化。


相关文章: