文章简介
本文的目标是为您提供一组推荐的使用 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 中构建更健壮和成功的微服务。不必严格遵守这些因素。但是,牢记它们可以让您构建可在持续交付环境中构建和维护的便携式应用程序或服务。