Skip to content

AI编程自学网

ai2opencode您的编程助手

Menu
  • 网站首页
  • WordPress专栏
  • 程序员内功
  • 副业和涨工资
  • Windows11
  • Flutter 教程专栏
Menu

微服务之使用 SpringBoot 构建 Rest 微服务的最佳实践

Posted on 2021年7月26日 by ai2opencode

文章简介

本文的目标是为您提供一组推荐的使用 Spring Boot 构建 Java REST 微服务的最佳实践和技术。这些建议旨在帮助您创建高效、可维护且有效的基于 Spring Boot 的微服务。Spring Boot 已成为 Java™ 微服务的事实上的标准,它具有许多专门构建的功能,可以简化构建,在生产中大规模运行微服务。
这个最佳实践列表是根据我在 Google Kubernetes Engine (GKE) 上运行基于微服务的架构的经验构建的。我已经在几个项目中看到并应用了这些方法,多年来,我对它们形成了非常强烈的看法。我们将介绍在使用 spring-boot 构建基于 REST 的微服务时应考虑的不同因素和指南,并解释它们如何帮助您实现更健壮、更有基础和成功的微服务。
本文假设您熟悉 Java、Spring Data JPA 等 spring-boot 概念、Spring Data REST、Docker/Kubernetes 基本概念,以及微服务架构的一般知识。鉴于以最佳实践为基础实施微服务架构可以极大地改善您的软件架构。但是,我们只关注基于 REST 的微服务,而没有考虑其他类型,如事件驱动的微服务。
您可能同意也可能不同意这里介绍的一些最佳实践,这绝对没问题,只要它促使您在开发基于 spring-boot 的微服务时研究和检查这些最佳实践,这将帮助您尽可能多地构建更好的微服务尽可能。

以最佳方式设计 REST API

作为一般的最佳实践,您应该将所有相关的 API 分组到面向用例的单个控制器中(例如,聚合器 API 或域 API)。在保持它们干净和专注的同时,您应该遵循 REST API 设计的最佳实践,例如:

  • 在端点路径中使用名词而不是动词,代表实体/资源以获取或操作并使用一致的复数名词,例如/orders/{id}/productsover /order/{id}/product。
  • 该操作必须由 HTTP 请求表示,GET 检索资源。POST 创建一个新的数据记录。PUT 更新现有数据。DELETE 删除数据
  • 分层对象的嵌套资源
  • 提供过滤、排序和分页
  • 考虑 API 版本控制
  • API 应该被完整记录。
    除了路由和将操作委托给适当的服务之外,控制器不应该执行任何业务逻辑。您必须通过保持控制器尽可能轻量来强制分离关注点。考虑到以上几点,让我们看一个 REST 控制器的例子:
@RestController
@RequestMapping(path = {"/api/v1/orders"}, produces = APPLICATION_JSON_VALUE)
public class OrderController {

private static final Logger logger =
        LoggerFactory.getLogger(OrderController.class);

private static final String ID = "orderId";
private static final String NEW_ORDER_LOG = "New order was created id:{}";
private static final String ORDER_UPDATED_LOG = "Order:{} was updated";

private final OrderService orderService;

@Autowired
public OrderController(OrderService orderService) {
    this.orderService = orderService;
}

@Operation(summary = "Crate a new order")
@ApiResponse(responseCode = "201", description = "Order is created", content = {@Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = OrderDto.class))})
@PostMapping(consumes = APPLICATION_JSON_VALUE)
public ResponseEntity<OrderDto> createOrder(
        @Valid @RequestBody OrderIncomingDto orderIncomingDto) {
    final OrderDto createdOrder = orderService.createOrder(orderIncomingDto);
    logger.info(NEW_ORDER_LOG, createdOrder.toString());
    return ResponseEntity.status(HttpStatus.CREATED).body(createdOrder);
}

@Operation(summary = "Get an oder by its id")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Found the order", content = {@Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = OrderDto.class))}),
        @ApiResponse(responseCode = "404", description = "Order not found", content = @Content)})
@GetMapping(path = "/{orderId}")
public ResponseEntity<OrderDto> loadOrder(@PathVariable(value = ID) String id) {
    final Optional<OrderDto> order = orderService.loadOrder(id);
    if (order.isEmpty()) {
        return ResponseEntity.notFound().build();
    }
    return ResponseEntity.ok(order.get());
}

@Operation(summary = "Update an oder by its id")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Order was updated", content = {@Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = OrderDto.class))}),
        @ApiResponse(responseCode = "404", description = "Order not found", content = @Content)})
@PutMapping(path = "/{orderId}", consumes = APPLICATION_JSON_VALUE)
public ResponseEntity<OrderDto> updateCustomQuoteRequest(
        @PathVariable(value = ID) String id,
        @Valid @RequestBody OrderIncomingDto orderIncomingDto) {
    final Optional<OrderDto> updatedOrder =
            orderService.updateOrder(id, orderIncomingDto);
    if (updatedOrder.isEmpty()) {
        return ResponseEntity.notFound().build();
    }
    logger.info(ORDER_UPDATED_LOG, updatedOrder.toString());
    return ResponseEntity.ok(updatedOrder.get());
}

@Operation(summary = "Returns a list of orders and sorted/filtered based on the query parameters")
@ApiResponse(responseCode = "200", description = "Order was updated", content = {@Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = OrderPageDto.class))})
@GetMapping
public ResponseEntity<OrderPageDto> getOrders(
        @RequestParam(required = false, name = "page",
                defaultValue = "0") int page,
        @RequestParam(required = false, name = "size",
                defaultValue = "20") int size,
        @RequestParam(required = false, name = "sortField",
                defaultValue = "createdAt") String sortField,
        @RequestParam(required = false, name = "direction",
                defaultValue = "DESC") String direction,
        @RequestParam(required = false, name = "status") List<String> status,
        @RequestParam(required = false, name = "search") String search
) {
    final OrderPageDto ordersPage =
            orderService.getOrders(page, size, sortField, direction, status, search);
    return ResponseEntity.ok(ordersPage);
}
}

所述@RequestMapping注释提供“路由”的信息,使用consume和produce注释定义什么端点接受该请求的有效载荷,并将其返回作为响应,其中JSON在情况下,它为REST API的99%所用的东西。鉴于服务器端和客户端技术具有无需做太多工作即可编码/解码 JSON 的库。
在@Operation与@ApiResponse注解允许你将一些描述添加到使用的API文档开放API 3.0 规范。例如,这是我们 API 的 Swagger UI 页面.

简而言之,正确设计 REST API 很重要,同时至少要考虑 API 客户端的性能和易用性。

实体

强烈建议您为具有手动分配的自定义标识符和公共属性的实体使用基类,如以下示例所示:

@MappedSuperclass
public abstract class BaseEntity {

    @Id
    @GeneratedValue(generator = "custom-generator", 
            strategy = GenerationType.IDENTITY)
    @GenericGenerator(
            name = "custom-generator",
            strategy = "com.qadah.demo.data.model.id.generator.BaseIdentifierGenerator")
    protected String id;

    @CreationTimestamp
    @Column(name = "created_at", updatable = false, nullable = false)
    protected Instant createdAt;

    @UpdateTimestamp
    @Column(name = "modified_at")
    protected Instant modifiedAt;

    @Column
    @Version
    protected int version;

    // standard setters and getters
}
public class BaseIdentifierGenerator extends UUIDGenerator {

private static final int NUMBER_OF_CHARS_IN_ID_PART = -5;

@Override
public Serializable generate(SharedSessionContractImplementor session, Object obj) throws HibernateException {
    // Generate a custom ID for the new entity
    final String uuid = super.generate(session, obj).toString();

    final long longTimeRandom = System.nanoTime() + System.currentTimeMillis() 
    + new Random().nextLong() + Objects.hash(obj);

    final String timeHex = Long.toHexString(longTimeRandom);
    return StringUtils.substring(timeHex, NUMBER_OF_CHARS_IN_ID_PART) 
    + StringUtils.substring(uuid, NUMBER_OF_CHARS_IN_ID_PART);
}
}

使用数据传输对象 (DTO)

虽然也总是可以在 REST 端点中直接公开 JPA/数据库实体表示,以从客户端发送/接收数据。然而,尽管这听起来简单明了,但这并不是最好的方法,有几个原因不这样做:
它在持久性模型和 API 模型之间创建了高耦合。
根据用户角色和访问权限或其他一些业务规则隐藏或调整 API 响应中某些字段值的复杂性
当客户端能够覆盖某些自动生成的数据库字段(例如主 ID 或修改的日期时间或系统字段访问)时可能存在漏洞或误用
它公开了应用程序的实现细节
它使得在不更改 API 的情况下添加、删除或更新数据库实体的任何字段或在不更改内部数据库结构的情况下更改 REST 端点返回的数据变得更加困难
支持多个版本的 API 变得更具挑战性
更好的方法是概念上更简单的方法,它定义一个单独的数据传输对象 (DTO),该对象表示从数据库实体或多个实体映射的 API 资源类,假设它们在 API 中被序列化和反序列化,她的是为实体设计 DTO 类的示例:

public class OrderIncomingDto {

    @JsonProperty(required = true)
    @NotEmpty
    @NotBlank
    private String customerId;

    @JsonProperty(required = true)
    @NotNull
    @PastOrPresent
    private Instant datePurchased;

    @JsonProperty(required = true)
    @NotNull
    @Positive
    private BigDecimal total;

    @JsonProperty(required = true)
    @NotEmpty
    @NotBlank
    @Size(min = 5, max = 10)
    private String status;

    @JsonProperty(required = true)
    @NotEmpty
    private List<ProductDto> orderProducts;

    @JsonProperty(required = true)
    @NotEmpty
    @NotBlank
    private String paymentType;

    @JsonProperty(required = true)
    @Size(min = 2, max = 10)
    private String shippingMode;

    @JsonProperty(required = true)
    @Email(message = "Customer email should be valid")
    private String customerEmailAddress;

    // standard setters and getters

}

另一方面,您不需要手动将持久性实体映射到 DTO,反之亦然,您可以使用像ModelMapper或MapStruct这样的库来自动处理转换。总之,使用 DTO 使 REST API 和持久层能够相互独立地发展。

利用 java bean 验证

在将 DTO 转换为您的 API 资源之前,您总是希望确保 DTO 具有有效数据,在它通过某种验证过程之前,应该假设它是错误的。
javax.validation是 Bean Validation API 的顶级包,它具有一些预定义的基于注解的约束,因此您可以使用javax.validation.constraints.*@NotNull、@Min、@Max 和 @Size、@NotEmpty 等注释 DTO/实体,和@Email,如课堂中所示OrderIncomingDto。
我们应该注释控制器输入@Valid以启用 bean 验证并激活对收入的这些约束@RequestBody。如果 bean 验证失败,它将触发MethodArgumentNotValidException. 默认情况下,Spring 会发回 HTTP 状态400 Bad Request。我n个下一节中,我将介绍如何自定义验证错误的错误响应。

使用全局/自定义错误处理程序

由于服务可能以多种方式崩溃或中断,因此我们必须确保任何 REST API 使用标准 HTTP 代码优雅地处理错误,以在这种情况下帮助消费者。始终建议为客户端构建有意义的错误消息,明确目标是为客户端提供所有信息以轻松诊断问题,以下示例显示如何通过扩展ResponseEntityExceptionHandler:

@ControllerAdvice
public class GeneralExceptionHandler extends ResponseEntityExceptionHandler {

public static final String ACCESS_DENIED = "Access denied!";
public static final String INVALID_REQUEST = "Invalid request";
public static final String ERROR_MESSAGE_TEMPLATE = "message: %s %n requested uri: %s";
public static final String LIST_JOIN_DELIMITER = ",";
public static final String FIELD_ERROR_SEPARATOR = ": ";
private static final Logger local_logger = LoggerFactory.getLogger(GeneralExceptionHandler.class);
private static final String ERRORS_FOR_PATH = "errors {} for path {}";
private static final String PATH = "path";
private static final String ERRORS = "error";
private static final String STATUS = "status";
private static final String MESSAGE = "message";
private static final String TIMESTAMP = "timestamp";
private static final String TYPE = "type";

@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
        MethodArgumentNotValidException exception,
        HttpHeaders headers,
        HttpStatus status, WebRequest request) {
    List<String> validationErrors = exception.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> error.getField() + FIELD_ERROR_SEPARATOR + error.getDefaultMessage())
            .collect(Collectors.toList());
    return getExceptionResponseEntity(exception, HttpStatus.BAD_REQUEST, request, validationErrors);
}

@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(
        HttpMessageNotReadableException exception,
        HttpHeaders headers, HttpStatus status,
        WebRequest request) {
    return getExceptionResponseEntity(exception, status, request,
            Collections.singletonList(exception.getLocalizedMessage()));
}

@ExceptionHandler({ConstraintViolationException.class})
public ResponseEntity<Object> handleConstraintViolation(
        ConstraintViolationException exception, WebRequest request) {
    final List<String> validationErrors = exception.getConstraintViolations().stream().
            map(violation ->
                    violation.getPropertyPath() + FIELD_ERROR_SEPARATOR + violation.getMessage())
            .collect(Collectors.toList());
    return getExceptionResponseEntity(exception, HttpStatus.BAD_REQUEST, request, validationErrors);
}

/**
 * A general handler for all uncaught exceptions
 */
@ExceptionHandler({Exception.class})
public ResponseEntity<Object> handleAllExceptions(Exception exception, WebRequest request) {
    ResponseStatus responseStatus =
            exception.getClass().getAnnotation(ResponseStatus.class);
    final HttpStatus status =
            responseStatus!=null ? responseStatus.value():HttpStatus.INTERNAL_SERVER_ERROR;
    final String localizedMessage = exception.getLocalizedMessage();
    final String path = request.getDescription(false);
    String message = (StringUtils.isNotEmpty(localizedMessage) ? localizedMessage:status.getReasonPhrase());
    logger.error(String.format(ERROR_MESSAGE_TEMPLATE, message, path), exception);
    return getExceptionResponseEntity(exception, status, request, Collections.singletonList(message));
}

/**
 * Build a detailed information about the exception in the response
 */
private ResponseEntity<Object> getExceptionResponseEntity(final Exception exception,
                              final HttpStatus status,
                              final WebRequest request,
                              final List<String> errors) {
    final Map<String, Object> body = new LinkedHashMap<>();
    final String path = request.getDescription(false);
    body.put(TIMESTAMP, Instant.now());
    body.put(STATUS, status.value());
    body.put(ERRORS, errors);
    body.put(TYPE, exception.getClass().getSimpleName());
    body.put(PATH, path);
    body.put(MESSAGE, getMessageForStatus(status));
    final String errorsMessage = CollectionUtils.isNotEmpty(errors) ?
            errors.stream().filter(StringUtils::isNotEmpty).collect(Collectors.joining(LIST_JOIN_DELIMITER))
            :status.getReasonPhrase();
    local_logger.error(ERRORS_FOR_PATH, errorsMessage, path);
    return new ResponseEntity<>(body, status);
}

private String getMessageForStatus(HttpStatus status) {
    switch (status) {
        case UNAUTHORIZED:
            return ACCESS_DENIED;
        case BAD_REQUEST:
            return INVALID_REQUEST;
        default:
            return status.getReasonPhrase();
    }
}
}

总而言之,在上述类中实现的错误处理需要考虑以下几点:
为每种类型的异常返回正确的状态代码,例如对于具有无效数据的请求的 BAD_REQUEST(400)。
在响应正文中包含所有相关信息
以标准化方式处理所有异常
记录所有异常消息和跟踪以进行调试和监控
这是验证错误时返回的响应

一般准则和建议

  • 不要忘记通过编写单元和集成测试来测试您的代码。可以毫不夸张地说,每一段代码和API都要经过一个测试!
  • 添加故障和停机时间的监控警报
  • 自动化构建和部署管理(CI/CD 管道),例如,使用 Jenkins 或 bitbucket 管道
  • 通过定期升级依赖项,始终使用最新版本
  • 编写清晰简洁的日志,易于阅读和解析
  • 考虑应用领域驱动设计 (DDD) 原则
  • 缓存数据以提高性能
  • 保护您的 API
  • 最后,微服务应该尽可能小,每个服务只有一个业务功能

概括

这篇文章涵盖了基于 spring-boot 的微服务的最佳实践列表,我希望遵循这些最佳实践将帮助您在 spring-boot 中构建更健壮和成功的微服务。不必严格遵守这些因素。但是,牢记它们可以让您构建可在持续交付环境中构建和维护的便携式应用程序或服务。

Related posts:

  1. PHP杂谈 PHP优点之 03 Constructor promotion
  2. PHP杂谈 PHP优点之 01 Type hints
  3. PHP杂谈 PHP的缺点之 03 不一致的标准库函数
  4. PHP杂谈 PHP优点之 02 语法改进

发表回复 取消回复

要发表评论,您必须先登录。

工具区

繁体中文

近期文章

  • 通识基础之为什么π是宇宙中最重要的常数,无处不在的常数
  • 站立式办公桌是在浪费时间吗?为什么站着工作对健康的好处可能有点被夸大了?
  • 解题的三大法则,解决广泛领域问题的工具
  • (无标题)
  • 不要只是设定目标,构建系统,幸福和成就更多的秘诀

近期评论

  1. J1o! - V2EX-Flutter 您应该选择哪个 IDE/编辑器?(Android Studio VS Code Intellij IDEA)发表在Flutter 您应该选择哪个IDE/编辑器?(Android Studio VS Code Intellij IDEA)
  2. 编程书籍推荐之《Think Like a Programmer: An Introduction to Creative Problem Solvin》 - AI编程自学网发表在如何解决任何编程问题

    推荐文章

    1. PHP杂谈 PHP优点之 03 Constructor promotion
    2. PHP杂谈 PHP优点之 01 Type hints
    3. PHP杂谈 PHP的缺点之 03 不一致的标准库函数
    4. PHP杂谈 PHP优点之 02 语法改进
    • 0经验开发
    • Access
    • adsense
    • Android
    • App开发赚钱
    • AWS云计算
    • Chrome
    • Chrome 控制台实用程序开发
    • CSS
    • CSS 基础教程
    • Dart语言
    • Flutter
    • Flutter基础
    • Flutter杂谈
    • HarmonyOS 鸿蒙
    • HarmonyOS基础
    • HTML
    • HTML基础
    • HTML技巧
    • JavaScript
    • JavaScript 基础
    • JavaScript 技巧
    • JavaScript 简介
    • JavaScript问答
    • oracle
    • oracle
    • pandas教程
    • PHP
    • PHP 杂谈
    • Python
    • Python实战
    • Python技巧
    • Python杂谈
    • SEO 技巧
    • Tiktok抖音小程序
    • UI设计
    • Web编程
    • Windows11
    • WordPress
    • WordPress 部署云主机VPS
    • WordPress 问答
    • WordPress 问答已解决
    • WordPress 问答未解决
    • WordPress使用技巧
    • WordPress插件
    • WordPress杂谈
    • WordPress盈利
    • Wordpress配置
    • WorPress建站技巧
    • 云服务推广
    • 云计算
    • 人工智能
    • 人工智能与机器学习
    • 低代码与无代码
    • 信息论基础
    • 健康工作方式
    • 健康生活
    • 元宇宙
    • 副业和涨工资
    • 副业技巧
    • 在线课程
    • 学习编程技巧
    • 小程序
    • 建站指南
    • 微服务架构
    • 微软
    • 思想类
    • 技术文章技巧
    • 技术潮流
    • 技能考试
    • 抖音小程序
    • 教学方法
    • 教学策略
    • 教学经验
    • 教育信息化
    • 教育案例与方法
    • 教育趋势
    • 数学大师
    • 数学学习
    • 数据库
    • 未分类
    • 程序员内功
    • 程序员装备
    • 经典书籍学习
    • 编程书籍推荐
    • 编程书籍推荐
    • 编程人生
    • 编程历史
    • 编程市场研究
    • 编程思想
    • 编程意义
    • 编程组件
    • 编程能力提高
    • 编程语言
    • 编程面试与工作
    • 网站合集
    • 腾讯云
    • 视频博主
    • 计算机科学中的数学
    • 读书与听书
    • 读书笔记
    • 软件估价
    • 软考
    • 通识知识
    • 量子计算
    • 销售 API
    • 阿里云
    • 高级信息系统项目管理师
    • 高级系统架构设计师

    dart Discord flutter JavaScript SpringBoot windows11 元宇宙 微服务 程序员内功 计算机视觉 问题未解决

    登录
    © 2023 AI编程自学网 | Powered by Minimalist Blog WordPress Theme