本课程采用k8s+springboot, k8s自带服务发现LB等一系列微服务治理能力, 无需springcloud

工程结构

image.png

接口参数校验

采用springValidation

  1. // 采用@Valid校验请求体
  2. @PostMapping(path = "/get_or_create")
  3. public GenericAccountResponse getOrCreate(@RequestBody @Valid GetOrCreateRequest request) {
  4. ...}
  5. // 直接校验路径参数
  6. public GenericAccountResponse getAccount(@RequestParam @NotBlank String userId) {
  7. ...}
  8. // 采用自定义注解
  9. public GenericAccountResponse getAccountByPhonenumber(@RequestParam @PhoneNumber String phoneNumber){
  10. ...}

自定义注解进行参数校验

  1. @Documented
  2. @Constraint(validatedBy = PhoneNumberValidator.class)
  3. @Target({ElementType.FIELD, ElementType.PARAMETER})
  4. @Retention(RetentionPolicy.RUNTIME)
  5. public @interface PhoneNumber {
  6. String message() default "Invalid phone number";
  7. Class[] groups() default {};
  8. Class[] payload() default {};
  9. }
  1. public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
  2. @Override
  3. public boolean isValid(String phoneField, ConstraintValidatorContext context) {
  4. if (phoneField == null) return true; // can be null
  5. return phoneField != null && phoneField.matches("[0-9]+")
  6. && (phoneField.length() > 8) && (phoneField.length() < 14);
  7. }
  8. }

统一异常处理

restful

使用RestControllerAdvice来进行统一异常处理
image.png

自定义业务异常

  1. public class ServiceException extends RuntimeException {
  2. private static final long serialVersionUID = 2359767895161832954L;
  3. @Getter
  4. private final ResultCode resultCode;
  5. public ServiceException(String message) {
  6. super(message);
  7. this.resultCode = ResultCode.FAILURE;
  8. }
  9. public ServiceException(ResultCode resultCode) {
  10. super(resultCode.getMsg());
  11. this.resultCode = resultCode;
  12. }
  13. public ServiceException(ResultCode resultCode, String msg) {
  14. super(msg);
  15. this.resultCode = resultCode;
  16. }
  17. public ServiceException(ResultCode resultCode, Throwable cause) {
  18. super(cause);
  19. this.resultCode = resultCode;
  20. }
  21. public ServiceException(String msg, Throwable cause) {
  22. super(msg, cause);
  23. this.resultCode = ResultCode.FAILURE;
  24. }
  25. /**
  26. * for better performance
  27. *
  28. * @return Throwable
  29. */
  30. @Override
  31. public Throwable fillInStackTrace() {
  32. return this;
  33. }
  34. public Throwable doFillInStackTrace() {
  35. return super.fillInStackTrace();
  36. }
  37. }

GlobalExceptionTranslator

  1. @RestControllerAdvice
  2. public class GlobalExceptionTranslator {
  3. static final ILogger logger = SLoggerFactory.getLogger(GlobalExceptionTranslator.class);
  4. @ExceptionHandler(MissingServletRequestParameterException.class)
  5. public BaseResponse handleError(MissingServletRequestParameterException e) {
  6. logger.warn("Missing Request Parameter", e);
  7. String message = String.format("Missing Request Parameter: %s", e.getParameterName());
  8. return BaseResponse
  9. .builder()
  10. .code(ResultCode.PARAM_MISS)
  11. .message(message)
  12. .build();
  13. }
  14. @ExceptionHandler(MethodArgumentTypeMismatchException.class)
  15. public BaseResponse handleError(MethodArgumentTypeMismatchException e) {
  16. logger.warn("Method Argument Type Mismatch", e);
  17. String message = String.format("Method Argument Type Mismatch: %s", e.getName());
  18. return BaseResponse
  19. .builder()
  20. .code(ResultCode.PARAM_TYPE_ERROR)
  21. .message(message)
  22. .build();
  23. }
  24. @ExceptionHandler(MethodArgumentNotValidException.class)
  25. public BaseResponse handleError(MethodArgumentNotValidException e) {
  26. logger.warn("Method Argument Not Valid", e);
  27. BindingResult result = e.getBindingResult();
  28. FieldError error = result.getFieldError();
  29. String message = String.format("%s:%s", error.getField(), error.getDefaultMessage());
  30. return BaseResponse
  31. .builder()
  32. .code(ResultCode.PARAM_VALID_ERROR)
  33. .message(message)
  34. .build();
  35. }
  36. @ExceptionHandler(BindException.class)
  37. public BaseResponse handleError(BindException e) {
  38. logger.warn("Bind Exception", e);
  39. FieldError error = e.getFieldError();
  40. String message = String.format("%s:%s", error.getField(), error.getDefaultMessage());
  41. return BaseResponse
  42. .builder()
  43. .code(ResultCode.PARAM_BIND_ERROR)
  44. .message(message)
  45. .build();
  46. }
  47. @ExceptionHandler(ConstraintViolationException.class)
  48. public BaseResponse handleError(ConstraintViolationException e) {
  49. logger.warn("Constraint Violation", e);
  50. Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
  51. ConstraintViolation<?> violation = violations.iterator().next();
  52. String path = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName();
  53. String message = String.format("%s:%s", path, violation.getMessage());
  54. return BaseResponse
  55. .builder()
  56. .code(ResultCode.PARAM_VALID_ERROR)
  57. .message(message)
  58. .build();
  59. }
  60. @ExceptionHandler(NoHandlerFoundException.class)
  61. public BaseResponse handleError(NoHandlerFoundException e) {
  62. logger.error("404 Not Found", e);
  63. return BaseResponse
  64. .builder()
  65. .code(ResultCode.NOT_FOUND)
  66. .message(e.getMessage())
  67. .build();
  68. }
  69. @ExceptionHandler(HttpMessageNotReadableException.class)
  70. public BaseResponse handleError(HttpMessageNotReadableException e) {
  71. logger.error("Message Not Readable", e);
  72. return BaseResponse
  73. .builder()
  74. .code(ResultCode.MSG_NOT_READABLE)
  75. .message(e.getMessage())
  76. .build();
  77. }
  78. @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
  79. public BaseResponse handleError(HttpRequestMethodNotSupportedException e) {
  80. logger.error("Request Method Not Supported", e);
  81. return BaseResponse
  82. .builder()
  83. .code(ResultCode.METHOD_NOT_SUPPORTED)
  84. .message(e.getMessage())
  85. .build();
  86. }
  87. @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
  88. public BaseResponse handleError(HttpMediaTypeNotSupportedException e) {
  89. logger.error("Media Type Not Supported", e);
  90. return BaseResponse
  91. .builder()
  92. .code(ResultCode.MEDIA_TYPE_NOT_SUPPORTED)
  93. .message(e.getMessage())
  94. .build();
  95. }
  96. @ExceptionHandler(ServiceException.class)
  97. public BaseResponse handleError(ServiceException e) {
  98. logger.error("Service Exception", e);
  99. return BaseResponse
  100. .builder()
  101. .code(e.getResultCode())
  102. .message(e.getMessage())
  103. .build();
  104. }
  105. @ExceptionHandler(PermissionDeniedException.class)
  106. public BaseResponse handleError(PermissionDeniedException e) {
  107. logger.error("Permission Denied", e);
  108. return BaseResponse
  109. .builder()
  110. .code(e.getResultCode())
  111. .message(e.getMessage())
  112. .build();
  113. }
  114. @ExceptionHandler(Throwable.class)
  115. public BaseResponse handleError(Throwable e) {
  116. logger.error("Internal Server Error", e);
  117. return BaseResponse
  118. .builder()
  119. .code(ResultCode.INTERNAL_SERVER_ERROR)
  120. .message(e.getMessage())
  121. .build();
  122. }
  123. }

最后封装返回给到前端BaseResponse

  1. @Data
  2. @NoArgsConstructor
  3. @AllArgsConstructor
  4. @Builder
  5. public class BaseResponse {
  6. private String message;
  7. @Builder.Default
  8. private ResultCode code = ResultCode.SUCCESS;
  9. public boolean isSuccess() {
  10. return code == ResultCode.SUCCESS;
  11. }
  12. }
  1. **
  2. * Result Code Enum
  3. *
  4. * @author william
  5. */
  6. @Getter
  7. @AllArgsConstructor
  8. public enum ResultCode {
  9. SUCCESS(HttpServletResponse.SC_OK, "Operation is Successful"),
  10. FAILURE(HttpServletResponse.SC_BAD_REQUEST, "Biz Exception"),
  11. UN_AUTHORIZED(HttpServletResponse.SC_UNAUTHORIZED, "Request Unauthorized"),
  12. NOT_FOUND(HttpServletResponse.SC_NOT_FOUND, "404 Not Found"),
  13. MSG_NOT_READABLE(HttpServletResponse.SC_BAD_REQUEST, "Message Can't be Read"),
  14. METHOD_NOT_SUPPORTED(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "Method Not Supported"),
  15. MEDIA_TYPE_NOT_SUPPORTED(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "Media Type Not Supported"),
  16. REQ_REJECT(HttpServletResponse.SC_FORBIDDEN, "Request Rejected"),
  17. INTERNAL_SERVER_ERROR(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error"),
  18. PARAM_MISS(HttpServletResponse.SC_BAD_REQUEST, "Missing Required Parameter"),
  19. PARAM_TYPE_ERROR(HttpServletResponse.SC_BAD_REQUEST, "Parameter Type Mismatch"),
  20. PARAM_BIND_ERROR(HttpServletResponse.SC_BAD_REQUEST, "Parameter Binding Error"),
  21. PARAM_VALID_ERROR(HttpServletResponse.SC_BAD_REQUEST, "Parameter Validation Error");
  22. final int code;
  23. final String msg;
  24. }

mvc

对于springmvc, 比如web-app服务, 统一异常处理是实现ErrorController, 重写handleError方法, 并自定义error.html页面

补充: 也可以使用@ControllerAdvice

GlobalErrorController

  1. @Controller
  2. @SuppressWarnings(value = "Duplicates")
  3. public class GlobalErrorController implements ErrorController {
  4. static final ILogger logger = SLoggerFactory.getLogger(GlobalErrorController.class);
  5. @Autowired
  6. ErrorPageFactory errorPageFactory;
  7. @Autowired
  8. SentryClient sentryClient;
  9. @Autowired
  10. StaffjoyProps staffjoyProps;
  11. @Autowired
  12. EnvConfig envConfig;
  13. @Override
  14. public String getErrorPath() {
  15. return "/error";
  16. }
  17. @RequestMapping("/error")
  18. public String handleError(HttpServletRequest request, Model model) {
  19. Object statusCode = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
  20. Object exception = request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
  21. ErrorPage errorPage = null;
  22. if (statusCode != null && (Integer)statusCode == HttpStatus.NOT_FOUND.value()) {
  23. errorPage = errorPageFactory.buildNotFoundPage();
  24. } else {
  25. errorPage = errorPageFactory.buildInternalServerErrorPage();
  26. }
  27. if (exception != null) {
  28. if (envConfig.isDebug()) { // no sentry aop in debug mode
  29. logger.error("Global error handling", exception);
  30. } else {
  31. sentryClient.sendException((Exception)exception);
  32. UUID uuid = sentryClient.getContext().getLastEventId();
  33. errorPage.setSentryErrorId(uuid.toString());
  34. errorPage.setSentryPublicDsn(staffjoyProps.getSentryDsn());
  35. logger.warn("Reported error to sentry", "id", uuid.toString(), "error", exception);
  36. }
  37. }
  38. model.addAttribute(Constant.ATTRIBUTE_NAME_PAGE, errorPage);
  39. return "error";
  40. }
  41. }

error.html

  1. <!DOCTYPE html>
  2. <html lang=en
  3. xmlns:th="http://www.thymeleaf.org">
  4. <meta charset=utf-8>
  5. <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
  6. <title th:text="${page.title}" />
  7. <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400" rel="stylesheet">
  8. <style>
  9. *{margin:0;padding:0}
  10. html{
  11. font-size: 15px;
  12. font-family: 'Open Sans', sans-serif;
  13. background: #f7f7f7;
  14. padding:16px
  15. width: 100%;
  16. }
  17. h1 {
  18. font-size: 24px;
  19. font-weight: 300;
  20. letter-spacing: -1px;
  21. color: #423a3f;
  22. margin-top: 32px;
  23. }
  24. body{
  25. width: 100%;
  26. height: 100%;
  27. }
  28. p {
  29. margin:11px 0 22px;
  30. overflow:hidden
  31. line-height: 1.75;
  32. letter-spacing: normal;
  33. text-align: center;
  34. color: #423a3f;
  35. margin-top: 12px;
  36. }
  37. a {
  38. text-decoration: none;
  39. font-size: 16px;
  40. line-height: 1.75;
  41. text-align: center;
  42. color: #3daa9b;
  43. margin-top: 32px;
  44. }
  45. #container {
  46. padding: 20% 10%;
  47. max-width: 80%;
  48. width: 380;
  49. text-align: center;
  50. margin: 0 auto;
  51. }
  52. img {
  53. width: 340px;
  54. max-width: 100%;
  55. }
  56. </style>
  57. <body>
  58. <div id="container">
  59. <img th:src="|data:image/png;base64,${page.imageBase64}|" alt="Spilled Coffee" />
  60. <h1 th:text="${page.title}" />
  61. <p th:text="${page.explanation}" />
  62. <p><a th:href="${page.linkHref}" th:text="${page.linkText}" /></p>
  63. </div>
  64. <th:block th:if="${page.sentryErrorId != null and page.sentryPublicDsn != null}">
  65. <script src="https://cdn.ravenjs.com/2.1.0/raven.min.js"></script>
  66. <script th:inline="javascript">
  67. /*<![CDATA[*/
  68. Raven.showReportDialog({
  69. // grab the eventId generated by the Sentry SDK
  70. eventId: /*[[${page.sentryErrorId}]]*/ '',
  71. // use the public DSN (dont include your secret!)
  72. dsn: /*[[${page.sentryPublicDsn}]]*/ ''
  73. });
  74. /*]]>*/
  75. </script>
  76. </th:block>
  77. </body>
  78. </html>

feign异常

老师,微服务内部调用feign接口抛出的异常,如何统一处理,需要捕获什么类型的异常? 作者回复: 初步网上查了一下,一种统一处理feign接口异常的方法是:用RestControllerAdvice集中捕获和处理FeignException
参考:
https://stackoverflow.com/questions/55020389/spring-feign-client-exception-handling
https://www.javacodemonk.com/feign-exception-handling-spring-cloud-20d17f69

异常一般可以分为两类:
1. 底层http通讯异常,比如连不上或超时等
2. 服务端返回http错误码异常。

具体要根据应用场景分析。

DTO与DMO互转

可以都使用DMO或冗余DTO, 看项目选型, 课程使用modelmapper进行两者转换
一般项目有这两种就足够了, 保持风格一致性

  • DTO: 数据传输对象
  • DMO: 数据模型对象

mapping框架对比: https://www.baeldung.com/java-performance-mapping-frameworks
image.png

异步

使用springAsync来实现异步操作

首先定义个业务线程池

  1. @Configuration
  2. @EnableAsync
  3. @Import(value = {StaffjoyRestConfig.class})
  4. @SuppressWarnings(value = "Duplicates")
  5. public class AppConfig {
  6. public static final String ASYNC_EXECUTOR_NAME = "asyncExecutor";
  7. @Bean(name=ASYNC_EXECUTOR_NAME)
  8. public Executor asyncExecutor() {
  9. ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  10. // for passing in request scope context
  11. executor.setTaskDecorator(new ContextCopyingDecorator());
  12. executor.setCorePoolSize(3);
  13. executor.setMaxPoolSize(5);
  14. executor.setQueueCapacity(100);
  15. executor.setWaitForTasksToCompleteOnShutdown(true);
  16. executor.setThreadNamePrefix("AsyncThread-");
  17. executor.initialize();
  18. return executor;
  19. }
  20. @Bean
  21. public PasswordEncoder passwordEncoder() {
  22. return new BCryptPasswordEncoder();
  23. }
  24. }

注意父子线程复制上下文

  1. // https://stackoverflow.com/questions/23732089/how-to-enable-request-scope-in-async-task-executor
  2. public class ContextCopyingDecorator implements TaskDecorator {
  3. @Override
  4. public Runnable decorate(Runnable runnable) {
  5. RequestAttributes context = RequestContextHolder.currentRequestAttributes();
  6. return () -> {
  7. try {
  8. RequestContextHolder.setRequestAttributes(context);
  9. runnable.run();
  10. } finally {
  11. RequestContextHolder.resetRequestAttributes();
  12. }
  13. };
  14. }
  15. }

使用方式如下:

  • 使用@Async注解标注在异步方法上, 并指定对应的线程池
  • 注意异步方法与调用方不能在同一个bean中(AOP的限制) ```java @Service @RequiredArgsConstructor public class AccountService {

private final ServiceHelper serviceHelper;

  1. public AccountDto create(String name, String email, String phoneNumber) {
  2. ...
  3. serviceHelper.syncUserAsync(account.getId());
  4. ...
  5. }

}

  1. ```java
  2. @RequiredArgsConstructor
  3. @Component
  4. public class ServiceHelper {
  5. @Async(AppConfig.ASYNC_EXECUTOR_NAME)
  6. public void syncUserAsync(String userId) {
  7. ...
  8. }
  9. }