RESTful 规范的 CRUD实践

发布 | 2024-09-10 | JAVA,Mysql,JavaWeb

RESTful 规范的 CRUD 操作

符合 REST 架构风格的 API,能够使服务具有良好的可扩展性和松耦合性。

RESTful API 的核心概念包括资源、URI(统一资源标识符)、无状态性,以及使用标准的 HTTP 方法来对资源进行操作。

HTTP 方法在 RESTful API 中用于定义资源操作:

  • GET 用于获取资源数据,不会对服务器端的数据造成任何修改。示例:GET /api/users/1 用于获取 ID 为 1 的用户信息。
  • POST 用于在服务器端创建新资源,通常会在成功创建后返回新资源的 URI。示例:POST /api/users 用于创建一个新用户。
  • PUT 用于更新资源,通常用于更新整个资源或根据请求内容对资源进行修改。示例:PUT /api/users/1 用于更新 ID 为 1 的用户信息。
  • PATCH 用于部分更新资源,区别于 PUTPATCH 只更新提供的字段。示例:PATCH /api/users/1 用于更新 ID 为 1 的用户的部分信息。
  • DELETE 用于删除资源。示例:DELETE /api/users/1 用于删除 ID 为 1 的用户。

状态码在 RESTful API 中用于表示请求结果的状态:

  • 200 OK 表示请求成功,返回期望的响应数据。
  • 201 Created 表示成功创建资源,返回新资源的 URI。
  • 204 No Content 表示请求成功,但不返回任何内容,通常用于删除操作。
  • 400 Bad Request 表示客户端请求无效,通常由于请求参数有误。
  • 401 Unauthorized 表示请求未经授权,通常因为身份验证失败。
  • 403 Forbidden 表示请求被服务器拒绝,通常因为权限不足。
  • 404 Not Found 表示请求的资源不存在。
  • 500 Internal Server Error 表示服务器内部错误。

RESTful API 的 URI 设计应清晰明了、符合层次结构,并使用名词表示资源。通常使用名词的复数形式表示资源集合,例如 /api/users 表示用户资源。路径设计中避免使用动词,如 /getUser,而应通过 HTTP 方法表达操作类型。版本控制可以通过在 URI 中添加版本号(如 /api/v1/users),或者通过请求头指定

在 RESTful API 的版本控制中,使用 URI 版本控制或请求头版本控制是较为常见的两种策略:

  • URI 版本控制:直接在 URI 中添加版本号,例如 /api/v1/users。这种方式简单直观,易于理解,但会导致 URI 更加冗长,并且如果版本众多,管理起来会比较复杂。
  • 请求头版本控制:通过自定义请求头来指定版本号,例如 Accept: application/vnd.example.v1+json。这种方式更加灵活,URI 不会随着版本的增加而变得混乱,但客户端需要明确设置请求头。

在实际应用中,RESTful API 通常需要实现基本的 CRUD 操作,并遵循最佳实践以确保 API 的易用性、可维护性和安全性。

实现 CRUD 操作的一个基本示例使用 Spring Boot 框架。以下是如何在 Spring Boot 中创建一个完整的 RESTful CRUD 操作示例。

创建实体类 User 表示用户资源:

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 主键自增策略
    private Long id;

    @NotBlank(message = "Name is mandatory") // 校验字段不为空
    private String name;

    @Email(message = "Email should be valid") // 校验邮箱格式
    private String email;

    // Getters 和 Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

在数据访问层,创建一个接口 UserRepository,继承自 JpaRepository,用于基本的 CRUD 操作:

public interface UserRepository extends JpaRepository<User, Long> {
    // 自定义查询方法:按名称查找用户
    List<User> findByName(String name);
}

服务层的实现用于处理业务逻辑。UserService 提供了用户的增删改查操作:

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    // 创建用户
    public User createUser(User user) {
        return userRepository.save(user);
    }

    // 根据ID获取用户
    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }

    // 获取所有用户
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    // 更新用户
    public User updateUser(Long id, User userDetails) {
        User user = getUserById(id);
        if (user != null) {
            user.setName(userDetails.getName());
            user.setEmail(userDetails.getEmail());
            return userRepository.save(user);
        }
        return null;
    }

    // 删除用户
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
}

控制器层 UserController 负责处理 HTTP 请求并返回相应的响应数据:

@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping // 创建用户
    public ResponseEntity<User> createUser(@RequestBody User user) {
        User createdUser = userService.createUser(user);
        return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
    }

    @GetMapping("/{id}") // 根据ID获取用户
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userService.getUserById(id);
        if (user != null) {
            return new ResponseEntity<>(user, HttpStatus.OK);
        }
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }

    @GetMapping // 获取所有用户
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.getAllUsers();
        return new ResponseEntity<>(users, HttpStatus.OK);
    }

    @PutMapping("/{id}") // 更新用户
    public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User userDetails) {
        User updatedUser = userService.updateUser(id, userDetails);
        if (updatedUser != null) {
            return new ResponseEntity<>(updatedUser, HttpStatus.OK);
        }
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }

    @DeleteMapping("/{id}") // 删除用户
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

RESTful API 设计的最佳实践包括:

  • 使用 HTTPS 确保数据传输的安全性。
  • 实现身份验证和授权,例如使用 JWT(JSON Web Token)或 OAuth 2.0,确保只有合法的用户能够访问资源。
  • 使用标准化的 API 文档工具(如 Swagger)生成文档,使 API 更加易于理解和使用。
  • 提供分页、过滤和排序功能,以提升数据查询的灵活性和性能。
  • 遵循 HATEOAS(超媒体作为应用状态的引擎)原则,为客户端提供导航链接,帮助客户端更容易地探索和使用 API。

拦截器(Interceptor)

拦截器在 Web 应用中是一种特殊的过滤器,用于在请求到达控制器之前或返回响应之前执行某些逻辑。拦截器通常用于日志记录、权限验证、数据预处理、异常处理等场景。

在 Spring MVC 中,可以通过实现 HandlerInterceptor 接口来定义自定义拦截器。拦截器通常有三个方法:

  • preHandle:在请求到达控制器之前调用。可以用于身份验证、权限检查、日志记录等操作。如果该方法返回 false,请求将不会继续向下传递。
  • postHandle:在控制器执行完逻辑之后,但在视图渲染之前调用。可以用于修改视图或向模型中添加通用数据。
  • afterCompletion:在请求完全处理完毕后调用,通常用于清理资源、记录日志等操作。

在 Spring Boot 中,拦截器的配置可以通过创建一个配置类并实现 WebMvcConfigurer 接口来完成:

@Component
public class LoggingInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("Request URI: " + request.getRequestURI());
        return true; // 继续处理请求
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("Post Handle method is Calling");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) throws Exception {
        System.out.println("Request and Response is completed");
    }
}

通过创建一个配置类并将拦截器注册到 Spring 的拦截器链中:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoggingInterceptor loggingInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loggingInterceptor).addPathPatterns("/**");
    }
}

使用 @Validated 注解可以结合拦截器实现数据校验。例如,在拦截器中可以结合方法参数中的注解进行校验,确保请求数据的合法性。


异常处理

全局异常处理可以确保应用程序在出现异常时返回统一的错误响应,从而提高应用的健壮性和用户体验。在 Spring Boot 中,可以使用 @ControllerAdvice@ExceptionHandler 注解实现全局异常处理。

通过定义一个全局异常处理类,并使用 @ControllerAdvice 注解标识,该类会捕获并处理应用程序中的所有异常:

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<?> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
        ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleGlobalException(Exception ex, WebRequest request) {
        ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

自定义异常类 ResourceNotFoundException 用于表示资源未找到的情况:

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

通过这种方式,应用程序的异常处理逻辑集中在一个地方,便于管理和维护。异常处理程序根据捕获的异常类型,返回适当的 HTTP 状态码和错误信息。

使用 ResponseEntity 可以灵活地构建标准化的错误响应对象。在实际应用中,为了提高 API 的可用性和可维护性,统一的错误响应格式是一个良好的实践。通常会包含以下信息:

  • timestamp:错误发生的时间。
  • message:错误的具体信息。
  • details:错误的详细描述,例如请求的 URI。

示例的错误响应对象 ErrorDetails 类如下:

public class ErrorDetails {
    private Date timestamp;
    private String message;
    private String details;

    public ErrorDetails(Date timestamp, String message, String details) {
        super();
        this.timestamp = timestamp;
        this.message = message;
        this.details = details;
    }

    // Getters 和 Setters
    public Date getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(Date timestamp) {
        this.timestamp = timestamp;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getDetails() {
        return details;
    }

    public void setDetails(String details) {
        this.details = details;
    }
}

通过全局异常处理器,所有未捕获的异常都会被自动处理,并返回标准化的错误响应。这样可以确保 API 对外暴露的接口稳定,同时为客户端提供一致的错误信息格式,方便调试和错误处理。

自定义校验器

在实际应用中,有时我们需要对输入数据进行复杂的校验,而这些校验可能无法通过简单的注解(如 @NotNull@Email 等)来实现。此时可以通过实现自定义校验器来完成。

创建一个自定义校验注解,如 @ValidPassword,用于验证用户密码的复杂性(例如必须包含大写字母、小写字母、数字和特殊字符):

  1. 定义注解 ValidPassword
@Constraint(validatedBy = PasswordValidator.class) // 绑定自定义校验器
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidPassword {
    String message() default "Invalid password"; // 默认错误信息
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
  1. 实现自定义校验逻辑:
public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {

    private Pattern pattern;
    private Matcher matcher;
    private static final String PASSWORD_PATTERN = 
        "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$";

    @Override
    public void initialize(ValidPassword constraintAnnotation) {
        pattern = Pattern.compile(PASSWORD_PATTERN); // 初始化密码匹配正则
    }

    @Override
    public boolean isValid(String password, ConstraintValidatorContext context) {
        if (password == null) {
            return false; // 密码不能为空
        }
        matcher = pattern.matcher(password);
        return matcher.matches(); // 返回匹配结果
    }
}
  1. 在实体类中使用自定义校验注解:
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "Name is mandatory")
    private String name;

    @Email(message = "Email should be valid")
    private String email;

    @ValidPassword // 使用自定义的密码校验注解
    private String password;

    // Getters 和 Setters
}

当客户端请求中包含无效密码时,自定义校验器会触发相应的错误,并在响应中返回错误信息。通过自定义校验器,可以灵活地满足各种复杂的校验需求。

数据校验与错误消息提示

使用 @Validated 注解结合 BindingResult 对象,可以在控制器中实现更加细粒度的错误处理逻辑。

示例中,我们在创建用户时应用这些技术:

@PostMapping("/users")
public ResponseEntity<?> createUser(@Validated @RequestBody User user, BindingResult result) {
    if (result.hasErrors()) { // 检查校验结果是否有错误
        List<String> errors = result.getAllErrors().stream()
            .map(DefaultMessageSourceResolvable::getDefaultMessage) // 提取所有错误消息
            .collect(Collectors.toList());
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); // 返回错误响应
    }
    User createdUser = userService.createUser(user);
    return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
}

通过 BindingResult 对象,可以获取所有的校验错误,并返回给客户端。这样,客户端可以根据具体的错误消息提示用户输入的错误信息。

CORS(跨域资源共享)

跨域资源共享(CORS)是一种浏览器安全机制,用于防止不受信任的域对资源的未经授权的访问。RESTful API 通常需要支持 CORS,以允许来自不同域的客户端(如 Web 应用程序)与服务器通信。

在 Spring Boot 中,可以使用 @CrossOrigin 注解启用 CORS。例如,在控制器方法上添加此注解:

@CrossOrigin(origins = "http://example.com") // 允许来自指定来源的跨域请求
@GetMapping("/users")
public ResponseEntity<List<User>> getAllUsers() {
    List<User> users = userService.getAllUsers();
    return new ResponseEntity<>(users, HttpStatus.OK);
}

或者在全局配置中启用 CORS 支持:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 允许跨域的路径
                .allowedOrigins("http://example.com") // 允许的来源
                .allowedMethods("GET", "POST", "PUT", "DELETE") // 允许的 HTTP 方法
                .allowCredentials(true); // 是否允许发送凭证(如 cookies)
    }
}

通过正确配置 CORS,客户端可以安全地访问 RESTful API,同时确保数据和用户隐私的安全性。

标签
RESTful,CRUD

© 著作权归作者所有

本文由 趣代码Blog 创作,采用 知识共享署名4.0 国际许可协议进行许可,本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。

评论关闭