本课程采用k8s+springboot, k8s自带服务发现LB等一系列微服务治理能力, 无需springcloud
工程结构
- 采用父子工程(聚合工程), 使用单体仓库
- 所有服务依赖公共包common-lib
- 微服务拆分svc和api, svc依赖api, api提供公共接口(feignClient)以及实体
- 在父工程下建立了config文件夹存放私钥等不受版本管理的配置信息
springboot配置: https://docs.spring.io/spring-boot/docs/1.2.0.M1/reference/html/boot-features-external-config.html

接口参数校验
采用springValidation
// 采用@Valid校验请求体@PostMapping(path = "/get_or_create")public GenericAccountResponse getOrCreate(@RequestBody @Valid GetOrCreateRequest request) {...}// 直接校验路径参数public GenericAccountResponse getAccount(@RequestParam @NotBlank String userId) {...}// 采用自定义注解public GenericAccountResponse getAccountByPhonenumber(@RequestParam @PhoneNumber String phoneNumber){...}
自定义注解进行参数校验
@Documented@Constraint(validatedBy = PhoneNumberValidator.class)@Target({ElementType.FIELD, ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)public @interface PhoneNumber {String message() default "Invalid phone number";Class[] groups() default {};Class[] payload() default {};}
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {@Overridepublic boolean isValid(String phoneField, ConstraintValidatorContext context) {if (phoneField == null) return true; // can be nullreturn phoneField != null && phoneField.matches("[0-9]+")&& (phoneField.length() > 8) && (phoneField.length() < 14);}}
统一异常处理
restful
使用RestControllerAdvice来进行统一异常处理
自定义业务异常
public class ServiceException extends RuntimeException {private static final long serialVersionUID = 2359767895161832954L;@Getterprivate final ResultCode resultCode;public ServiceException(String message) {super(message);this.resultCode = ResultCode.FAILURE;}public ServiceException(ResultCode resultCode) {super(resultCode.getMsg());this.resultCode = resultCode;}public ServiceException(ResultCode resultCode, String msg) {super(msg);this.resultCode = resultCode;}public ServiceException(ResultCode resultCode, Throwable cause) {super(cause);this.resultCode = resultCode;}public ServiceException(String msg, Throwable cause) {super(msg, cause);this.resultCode = ResultCode.FAILURE;}/*** for better performance** @return Throwable*/@Overridepublic Throwable fillInStackTrace() {return this;}public Throwable doFillInStackTrace() {return super.fillInStackTrace();}}
GlobalExceptionTranslator
@RestControllerAdvicepublic class GlobalExceptionTranslator {static final ILogger logger = SLoggerFactory.getLogger(GlobalExceptionTranslator.class);@ExceptionHandler(MissingServletRequestParameterException.class)public BaseResponse handleError(MissingServletRequestParameterException e) {logger.warn("Missing Request Parameter", e);String message = String.format("Missing Request Parameter: %s", e.getParameterName());return BaseResponse.builder().code(ResultCode.PARAM_MISS).message(message).build();}@ExceptionHandler(MethodArgumentTypeMismatchException.class)public BaseResponse handleError(MethodArgumentTypeMismatchException e) {logger.warn("Method Argument Type Mismatch", e);String message = String.format("Method Argument Type Mismatch: %s", e.getName());return BaseResponse.builder().code(ResultCode.PARAM_TYPE_ERROR).message(message).build();}@ExceptionHandler(MethodArgumentNotValidException.class)public BaseResponse handleError(MethodArgumentNotValidException e) {logger.warn("Method Argument Not Valid", e);BindingResult result = e.getBindingResult();FieldError error = result.getFieldError();String message = String.format("%s:%s", error.getField(), error.getDefaultMessage());return BaseResponse.builder().code(ResultCode.PARAM_VALID_ERROR).message(message).build();}@ExceptionHandler(BindException.class)public BaseResponse handleError(BindException e) {logger.warn("Bind Exception", e);FieldError error = e.getFieldError();String message = String.format("%s:%s", error.getField(), error.getDefaultMessage());return BaseResponse.builder().code(ResultCode.PARAM_BIND_ERROR).message(message).build();}@ExceptionHandler(ConstraintViolationException.class)public BaseResponse handleError(ConstraintViolationException e) {logger.warn("Constraint Violation", e);Set<ConstraintViolation<?>> violations = e.getConstraintViolations();ConstraintViolation<?> violation = violations.iterator().next();String path = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName();String message = String.format("%s:%s", path, violation.getMessage());return BaseResponse.builder().code(ResultCode.PARAM_VALID_ERROR).message(message).build();}@ExceptionHandler(NoHandlerFoundException.class)public BaseResponse handleError(NoHandlerFoundException e) {logger.error("404 Not Found", e);return BaseResponse.builder().code(ResultCode.NOT_FOUND).message(e.getMessage()).build();}@ExceptionHandler(HttpMessageNotReadableException.class)public BaseResponse handleError(HttpMessageNotReadableException e) {logger.error("Message Not Readable", e);return BaseResponse.builder().code(ResultCode.MSG_NOT_READABLE).message(e.getMessage()).build();}@ExceptionHandler(HttpRequestMethodNotSupportedException.class)public BaseResponse handleError(HttpRequestMethodNotSupportedException e) {logger.error("Request Method Not Supported", e);return BaseResponse.builder().code(ResultCode.METHOD_NOT_SUPPORTED).message(e.getMessage()).build();}@ExceptionHandler(HttpMediaTypeNotSupportedException.class)public BaseResponse handleError(HttpMediaTypeNotSupportedException e) {logger.error("Media Type Not Supported", e);return BaseResponse.builder().code(ResultCode.MEDIA_TYPE_NOT_SUPPORTED).message(e.getMessage()).build();}@ExceptionHandler(ServiceException.class)public BaseResponse handleError(ServiceException e) {logger.error("Service Exception", e);return BaseResponse.builder().code(e.getResultCode()).message(e.getMessage()).build();}@ExceptionHandler(PermissionDeniedException.class)public BaseResponse handleError(PermissionDeniedException e) {logger.error("Permission Denied", e);return BaseResponse.builder().code(e.getResultCode()).message(e.getMessage()).build();}@ExceptionHandler(Throwable.class)public BaseResponse handleError(Throwable e) {logger.error("Internal Server Error", e);return BaseResponse.builder().code(ResultCode.INTERNAL_SERVER_ERROR).message(e.getMessage()).build();}}
最后封装返回给到前端BaseResponse
@Data@NoArgsConstructor@AllArgsConstructor@Builderpublic class BaseResponse {private String message;@Builder.Defaultprivate ResultCode code = ResultCode.SUCCESS;public boolean isSuccess() {return code == ResultCode.SUCCESS;}}
*** Result Code Enum** @author william*/@Getter@AllArgsConstructorpublic enum ResultCode {SUCCESS(HttpServletResponse.SC_OK, "Operation is Successful"),FAILURE(HttpServletResponse.SC_BAD_REQUEST, "Biz Exception"),UN_AUTHORIZED(HttpServletResponse.SC_UNAUTHORIZED, "Request Unauthorized"),NOT_FOUND(HttpServletResponse.SC_NOT_FOUND, "404 Not Found"),MSG_NOT_READABLE(HttpServletResponse.SC_BAD_REQUEST, "Message Can't be Read"),METHOD_NOT_SUPPORTED(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "Method Not Supported"),MEDIA_TYPE_NOT_SUPPORTED(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "Media Type Not Supported"),REQ_REJECT(HttpServletResponse.SC_FORBIDDEN, "Request Rejected"),INTERNAL_SERVER_ERROR(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error"),PARAM_MISS(HttpServletResponse.SC_BAD_REQUEST, "Missing Required Parameter"),PARAM_TYPE_ERROR(HttpServletResponse.SC_BAD_REQUEST, "Parameter Type Mismatch"),PARAM_BIND_ERROR(HttpServletResponse.SC_BAD_REQUEST, "Parameter Binding Error"),PARAM_VALID_ERROR(HttpServletResponse.SC_BAD_REQUEST, "Parameter Validation Error");final int code;final String msg;}
mvc
对于springmvc, 比如web-app服务, 统一异常处理是实现ErrorController, 重写handleError方法, 并自定义error.html页面
补充: 也可以使用@ControllerAdvice
GlobalErrorController
@Controller@SuppressWarnings(value = "Duplicates")public class GlobalErrorController implements ErrorController {static final ILogger logger = SLoggerFactory.getLogger(GlobalErrorController.class);@AutowiredErrorPageFactory errorPageFactory;@AutowiredSentryClient sentryClient;@AutowiredStaffjoyProps staffjoyProps;@AutowiredEnvConfig envConfig;@Overridepublic String getErrorPath() {return "/error";}@RequestMapping("/error")public String handleError(HttpServletRequest request, Model model) {Object statusCode = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);Object exception = request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);ErrorPage errorPage = null;if (statusCode != null && (Integer)statusCode == HttpStatus.NOT_FOUND.value()) {errorPage = errorPageFactory.buildNotFoundPage();} else {errorPage = errorPageFactory.buildInternalServerErrorPage();}if (exception != null) {if (envConfig.isDebug()) { // no sentry aop in debug modelogger.error("Global error handling", exception);} else {sentryClient.sendException((Exception)exception);UUID uuid = sentryClient.getContext().getLastEventId();errorPage.setSentryErrorId(uuid.toString());errorPage.setSentryPublicDsn(staffjoyProps.getSentryDsn());logger.warn("Reported error to sentry", "id", uuid.toString(), "error", exception);}}model.addAttribute(Constant.ATTRIBUTE_NAME_PAGE, errorPage);return "error";}}
error.html
<!DOCTYPE html><html lang=enxmlns:th="http://www.thymeleaf.org"><meta charset=utf-8><meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width"><title th:text="${page.title}" /><link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400" rel="stylesheet"><style>*{margin:0;padding:0}html{font-size: 15px;font-family: 'Open Sans', sans-serif;background: #f7f7f7;padding:16pxwidth: 100%;}h1 {font-size: 24px;font-weight: 300;letter-spacing: -1px;color: #423a3f;margin-top: 32px;}body{width: 100%;height: 100%;}p {margin:11px 0 22px;overflow:hiddenline-height: 1.75;letter-spacing: normal;text-align: center;color: #423a3f;margin-top: 12px;}a {text-decoration: none;font-size: 16px;line-height: 1.75;text-align: center;color: #3daa9b;margin-top: 32px;}#container {padding: 20% 10%;max-width: 80%;width: 380;text-align: center;margin: 0 auto;}img {width: 340px;max-width: 100%;}</style><body><div id="container"><img th:src="|data:image/png;base64,${page.imageBase64}|" alt="Spilled Coffee" /><h1 th:text="${page.title}" /><p th:text="${page.explanation}" /><p><a th:href="${page.linkHref}" th:text="${page.linkText}" /></p></div><th:block th:if="${page.sentryErrorId != null and page.sentryPublicDsn != null}"><script src="https://cdn.ravenjs.com/2.1.0/raven.min.js"></script><script th:inline="javascript">/*<![CDATA[*/Raven.showReportDialog({// grab the eventId generated by the Sentry SDKeventId: /*[[${page.sentryErrorId}]]*/ '',// use the public DSN (dont include your secret!)dsn: /*[[${page.sentryPublicDsn}]]*/ ''});/*]]>*/</script></th:block></body></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
异步
使用springAsync来实现异步操作
首先定义个业务线程池
@Configuration@EnableAsync@Import(value = {StaffjoyRestConfig.class})@SuppressWarnings(value = "Duplicates")public class AppConfig {public static final String ASYNC_EXECUTOR_NAME = "asyncExecutor";@Bean(name=ASYNC_EXECUTOR_NAME)public Executor asyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();// for passing in request scope contextexecutor.setTaskDecorator(new ContextCopyingDecorator());executor.setCorePoolSize(3);executor.setMaxPoolSize(5);executor.setQueueCapacity(100);executor.setWaitForTasksToCompleteOnShutdown(true);executor.setThreadNamePrefix("AsyncThread-");executor.initialize();return executor;}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}}
注意父子线程复制上下文
// https://stackoverflow.com/questions/23732089/how-to-enable-request-scope-in-async-task-executorpublic class ContextCopyingDecorator implements TaskDecorator {@Overridepublic Runnable decorate(Runnable runnable) {RequestAttributes context = RequestContextHolder.currentRequestAttributes();return () -> {try {RequestContextHolder.setRequestAttributes(context);runnable.run();} finally {RequestContextHolder.resetRequestAttributes();}};}}
使用方式如下:
- 使用@Async注解标注在异步方法上, 并指定对应的线程池
- 注意异步方法与调用方不能在同一个bean中(AOP的限制) ```java @Service @RequiredArgsConstructor public class AccountService {
private final ServiceHelper serviceHelper;
public AccountDto create(String name, String email, String phoneNumber) {...serviceHelper.syncUserAsync(account.getId());...}
}
```java@RequiredArgsConstructor@Componentpublic class ServiceHelper {@Async(AppConfig.ASYNC_EXECUTOR_NAME)public void syncUserAsync(String userId) {...}}
