本课程采用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> {
@Override
public boolean isValid(String phoneField, ConstraintValidatorContext context) {
if (phoneField == null) return true; // can be null
return 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;
@Getter
private 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
*/
@Override
public Throwable fillInStackTrace() {
return this;
}
public Throwable doFillInStackTrace() {
return super.fillInStackTrace();
}
}
GlobalExceptionTranslator
@RestControllerAdvice
public 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
@Builder
public class BaseResponse {
private String message;
@Builder.Default
private ResultCode code = ResultCode.SUCCESS;
public boolean isSuccess() {
return code == ResultCode.SUCCESS;
}
}
**
* Result Code Enum
*
* @author william
*/
@Getter
@AllArgsConstructor
public 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);
@Autowired
ErrorPageFactory errorPageFactory;
@Autowired
SentryClient sentryClient;
@Autowired
StaffjoyProps staffjoyProps;
@Autowired
EnvConfig envConfig;
@Override
public 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 mode
logger.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=en
xmlns: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:16px
width: 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:hidden
line-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 SDK
eventId: /*[[${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 context
executor.setTaskDecorator(new ContextCopyingDecorator());
executor.setCorePoolSize(3);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setThreadNamePrefix("AsyncThread-");
executor.initialize();
return executor;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
注意父子线程复制上下文
// https://stackoverflow.com/questions/23732089/how-to-enable-request-scope-in-async-task-executor
public class ContextCopyingDecorator implements TaskDecorator {
@Override
public 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
@Component
public class ServiceHelper {
@Async(AppConfig.ASYNC_EXECUTOR_NAME)
public void syncUserAsync(String userId) {
...
}
}