diff --git a/src/main/java/cn/palmte/work/ErrorMessage.java b/src/main/java/cn/palmte/work/ErrorMessage.java new file mode 100644 index 0000000..e207ed7 --- /dev/null +++ b/src/main/java/cn/palmte/work/ErrorMessage.java @@ -0,0 +1,33 @@ +package cn.palmte.work; + +/** + * @author Harry Yang + * @since 2.0 2022/12/30 15:26 + */ +public class ErrorMessage implements Result { + + private String message; + + public ErrorMessage() { } + + public ErrorMessage(String message) { + this.message = message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public static ErrorMessage failed(String message) { + return new ErrorMessage(message); + } + + public static ErrorMessage failed() { + return new ErrorMessage("未知错误"); + } + +} diff --git a/src/main/java/cn/palmte/work/ErrorMessageException.java b/src/main/java/cn/palmte/work/ErrorMessageException.java new file mode 100644 index 0000000..530c405 --- /dev/null +++ b/src/main/java/cn/palmte/work/ErrorMessageException.java @@ -0,0 +1,50 @@ +package cn.palmte.work; + +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +import java.util.function.Supplier; + +/** + * @author Harry Yang + * @since 2.0 2022/12/30 15:25 + */ +public class ErrorMessageException extends NoStackTraceRuntimeException { + + private final HttpStatus status; + + public ErrorMessageException(/*@Nullable*/ String msg) { + this(msg, null, HttpStatus.BAD_REQUEST); + } + + public ErrorMessageException(/*@Nullable*/ String msg, /*@Nullable*/ Throwable cause, HttpStatus status) { + super(msg, cause); + Assert.notNull(status, "http status is required"); + this.status = status; + } + + public HttpStatus getStatus() { + return status; + } + + public static ErrorMessageException failed(String message) { + return new ErrorMessageException(message); + } + + public static ErrorMessageException failed(String message, HttpStatus status) { + return new ErrorMessageException(message, null, status); + } + + public static void notNull(Object obj, String message) { + if (obj == null) { + throw ErrorMessageException.failed(message, HttpStatus.NOT_FOUND); + } + } + + public static void notNull(Object obj, Supplier supplier) { + if (obj == null) { + throw new ErrorMessageException(supplier.get()); + } + } + +} diff --git a/src/main/java/cn/palmte/work/Json.java b/src/main/java/cn/palmte/work/Json.java new file mode 100644 index 0000000..72cb882 --- /dev/null +++ b/src/main/java/cn/palmte/work/Json.java @@ -0,0 +1,113 @@ +package cn.palmte.work; + +import java.util.function.Function; + +/** + * @author Harry Yang + * @since 2.0 2022/12/30 15:38 + */ + +public class Json implements Result { + + private Object data; + private String message; + private boolean success; + + public Object getData() { + return data; + } + + public boolean isSuccess() { + return success; + } + + public String getMessage() { + return message; + } + + public Json data(Object data) { + this.data = data; + return this; + } + + public Json message(String message) { + this.message = message; + return this; + } + + public Json success(boolean success) { + this.success = success; + return this; + } + + /** + * Apply the common {@link Json} result + * + * @param func the {@link Function} + * @param param parameter + */ + public static Json apply(Function func, T param) { + if (func.apply(param)) { + return Json.ok(); + } + return Json.failed(); + } + + public static Json apply(boolean success) { + if (success) { + return Json.ok(); + } + return Json.failed(); + } + + /** + * @param success if success + * @param message the message of the response + * @param data response data + */ + public static Json create(boolean success, String message, Object data) { + return new Json() + .data(data) + .message(message) + .success(success); + } + + public static Json ok() { + return create(true, "ok", null); + } + + public static Json ok(String message, Object data) { + return create(true, message, data); + } + + public static Json ok(Object data) { + return create(true, "ok", data); + } + + public static Json ok(String message) { + return create(true, message, null); + } + + public static Json failed() { + return create(false, "失败", null); + } + + public static Json failed(String message) { + return create(false, message, null); + } + + public static Json failed(String message, Object data) { + return create(false, message, data); + } + + @Override + public String toString() { + return new StringBuilder()// + .append("{\"message\":\"").append(message)// + .append("\",\"data\":\"").append(data)// + .append("\",\"success\":\"").append(success)// + .append("\"}")// + .toString(); + } +} + diff --git a/src/main/java/cn/palmte/work/NoStackTraceRuntimeException.java b/src/main/java/cn/palmte/work/NoStackTraceRuntimeException.java new file mode 100644 index 0000000..e0d7279 --- /dev/null +++ b/src/main/java/cn/palmte/work/NoStackTraceRuntimeException.java @@ -0,0 +1,96 @@ +package cn.palmte.work; + +import org.springframework.core.NestedExceptionUtils; +import org.springframework.core.NestedRuntimeException; + +/** + * 没有堆栈的异常, 降低性能消耗 + * + * @author Harry Yang + * @since 2.0 2022/12/30 15:25 + */ +public class NoStackTraceRuntimeException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public NoStackTraceRuntimeException() { + super(); + } + + public NoStackTraceRuntimeException(Throwable cause) { + super(cause); + } + + public NoStackTraceRuntimeException(String msg) { + super(msg); + } + + public NoStackTraceRuntimeException(/*@Nullable*/ String msg, /*@Nullable */Throwable cause) { + super(msg, cause); + } + + @Override + public final synchronized Throwable fillInStackTrace() { + return this; + } + + /** + * Retrieve the innermost cause of this exception, if any. + * + * @return the innermost exception, or {@code null} if none + */ +// @Nullable + public Throwable getRootCause() { + return NestedExceptionUtils.getRootCause(this); + } + + /** + * Retrieve the most specific cause of this exception, that is, + * either the innermost cause (root cause) or this exception itself. + *

Differs from {@link #getRootCause()} in that it falls back + * to the present exception if there is no root cause. + * + * @return the most specific cause (never {@code null}) + */ + public Throwable getMostSpecificCause() { + Throwable rootCause = getRootCause(); + return (rootCause != null ? rootCause : this); + } + + /** + * Check whether this exception contains an exception of the given type: + * either it is of the given class itself or it contains a nested cause + * of the given type. + * + * @param exType the exception type to look for + * @return whether there is a nested exception of the specified type + */ + public boolean contains(Class exType) { + if (exType == null) { + return false; + } + if (exType.isInstance(this)) { + return true; + } + Throwable cause = getCause(); + if (cause == this) { + return false; + } + if (cause instanceof NestedRuntimeException) { + return ((NestedRuntimeException) cause).contains(exType); + } + else { + while (cause != null) { + if (exType.isInstance(cause)) { + return true; + } + if (cause.getCause() == cause) { + break; + } + cause = cause.getCause(); + } + return false; + } + } + +} + diff --git a/src/main/java/cn/palmte/work/Result.java b/src/main/java/cn/palmte/work/Result.java new file mode 100644 index 0000000..1797f5d --- /dev/null +++ b/src/main/java/cn/palmte/work/Result.java @@ -0,0 +1,10 @@ +package cn.palmte.work; + +/** + * @author Harry Yang + * @since 2.0 2022/12/30 15:37 + */ +public interface Result { + +} + diff --git a/src/main/java/cn/palmte/work/ValidationError.java b/src/main/java/cn/palmte/work/ValidationError.java new file mode 100644 index 0000000..14d6206 --- /dev/null +++ b/src/main/java/cn/palmte/work/ValidationError.java @@ -0,0 +1,30 @@ +package cn.palmte.work; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +/** + * @author Harry Yang + * @since 2.0 2022/12/30 15:39 + */ + +@Getter +@Setter +@AllArgsConstructor +public class ValidationError implements Result { + + private Object validation; + + public static ValidationError failed(Object validation) { + return new ValidationError(validation); + } + + @JsonIgnore + public Object getData() { + return validation; + } + +} diff --git a/src/main/java/cn/palmte/work/config/ApplicationExceptionHandler.java b/src/main/java/cn/palmte/work/config/ApplicationExceptionHandler.java new file mode 100644 index 0000000..8a2ce26 --- /dev/null +++ b/src/main/java/cn/palmte/work/config/ApplicationExceptionHandler.java @@ -0,0 +1,175 @@ +package cn.palmte.work.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.http.HttpStatus; +import org.springframework.util.ObjectUtils; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.MaxUploadSizeExceededException; +import org.springframework.web.multipart.MultipartException; + +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import cn.palmte.work.ErrorMessage; +import cn.palmte.work.ErrorMessageException; +import cn.palmte.work.Json; +import cn.palmte.work.Result; +import cn.palmte.work.ValidationError; + +/** + * 异常处理 + * + * @author Harry Yang + * @since 2.0 2022/12/30 15:24 + */ +@RestControllerAdvice +public class ApplicationExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(ApplicationExceptionHandler.class); + + public static final ErrorMessage argsErrorMessage = ErrorMessage.failed("参数错误"); + public static final ErrorMessage sizeExceeded = ErrorMessage.failed("上传文件大小超出限制"); + public static final ErrorMessage methodNotSupported = ErrorMessage.failed("请求方式不支持"); + public static final ErrorMessage internalServerError = ErrorMessage.failed("服务器内部异常"); + public static final ErrorMessage notWritableError = ErrorMessage.failed("数据无法正常返回到客户端"); + + @ExceptionHandler(ErrorMessageException.class) + public ResponseEntity errorMessage(ErrorMessageException errorMessage) { + HttpStatus httpStatus = errorMessage.getStatus(); + return ResponseEntity.status(httpStatus) + .body(ErrorMessage.failed(errorMessage.getMessage())); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(IllegalArgumentException.class) + public ErrorMessage badRequest(IllegalArgumentException exception) { + return ErrorMessage.failed(exception.getMessage()); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({ MaxUploadSizeExceededException.class }) + public ErrorMessage badRequest() { + return sizeExceeded; + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ErrorMessage methodNotSupported() { + return methodNotSupported; + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorMessage error(Exception exception) { + log.error("An Exception occurred", exception); + if (exception instanceof SQLException) { + return internalServerError; + } + if (exception instanceof HttpMessageNotWritableException) { + return notWritableError; + } + if (exception instanceof TransactionSystemException) { + return ErrorMessage.failed("数据库出错"); + } + return ErrorMessage.failed(exception.getMessage()); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ErrorMessage typeMismatch(MethodArgumentTypeMismatchException mismatch) { + return ErrorMessage.failed("参数'" + mismatch.getName() + "'不能转换到对应类型"); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MissingServletRequestParameterException.class) + public ErrorMessage parameterError(MissingServletRequestParameterException e) { + return ErrorMessage.failed("缺少参数'" + e.getParameterName() + "'"); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({ + MultipartException.class, + HttpMessageNotReadableException.class, + HttpMediaTypeNotSupportedException.class + }) + public ErrorMessage messageNotReadable() { + return argsErrorMessage; + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({ BindException.class, MethodArgumentNotValidException.class }) + public Result validExceptionHandler(Exception e) { + + BindingResult result; + if (e instanceof MethodArgumentNotValidException) { + result = ((MethodArgumentNotValidException) e).getBindingResult(); + } + else if (e instanceof BindException) { + result = (BindingResult) e; + } + else { + return ErrorMessage.failed(); + } + List allErrors = result.getAllErrors(); + Map model = new HashMap<>(16); + + for (ObjectError error : allErrors) { + if (error instanceof FieldError) { + FieldError fieldError = (FieldError) error; + String field = fieldError.getField(); + String defaultMessage = error.getDefaultMessage(); + model.put(field, defaultMessage); + // log.error("[{}] -> [{}]", field, defaultMessage); + } + } + return ValidationError.failed(model); + } + + @ExceptionHandler(NullPointerException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Json nullPointer(NullPointerException exception) { + final StackTraceElement[] stackTrace = exception.getStackTrace(); + if (ObjectUtils.isEmpty(stackTrace)) { + return Json.failed("空指针", "暂无堆栈信息"); + } + return Json.failed("空指针", stackTrace[0]); + } + + @ExceptionHandler(DataAccessException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorMessage dataAccessException(DataAccessException accessException) { + String message = getDataAccessMessage(accessException.getCause()); + log.error(message, accessException); + return ErrorMessage.failed(message); + } + + String getDataAccessMessage(Throwable cause) { + return "数据库出错"; + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(DataAccessResourceFailureException.class) + public ErrorMessage dataAccessException(DataAccessResourceFailureException accessException) { + log.error("数据库连接出错", accessException); + return ErrorMessage.failed("数据库连接出错"); + } +} diff --git a/src/main/java/cn/palmte/work/controller/backend/ProcessController.java b/src/main/java/cn/palmte/work/controller/backend/ProcessController.java index 80c1c00..16e99b7 100644 --- a/src/main/java/cn/palmte/work/controller/backend/ProcessController.java +++ b/src/main/java/cn/palmte/work/controller/backend/ProcessController.java @@ -43,6 +43,7 @@ import javax.persistence.TypedQuery; import javax.validation.Valid; import javax.validation.constraints.NotNull; +import cn.palmte.work.ErrorMessageException; import cn.palmte.work.config.activiti.ActApproveTypeEnum; import cn.palmte.work.config.activiti.ActProjectTypeEnum; import cn.palmte.work.model.Admin; @@ -58,19 +59,19 @@ import cn.palmte.work.model.enums.ProcessType; import cn.palmte.work.model.enums.ProcurementMode; import cn.palmte.work.model.enums.ProjectType; import cn.palmte.work.model.enums.SealType; -import cn.palmte.work.model.process.ProcurementDetail; import cn.palmte.work.model.process.BudgetPurchaseAmount; +import cn.palmte.work.model.process.BudgetPurchaseAmountModel; import cn.palmte.work.model.process.BudgetPurchaseDetail; +import cn.palmte.work.model.process.BudgetPurchaseDetailModel; import cn.palmte.work.model.process.ProcessAttachment; import cn.palmte.work.model.process.ProcurementContract; +import cn.palmte.work.model.process.ProcurementDetail; import cn.palmte.work.model.process.ProjectProcess; import cn.palmte.work.model.process.ProjectProcessDetail; import cn.palmte.work.model.process.ProjectProcessRepository; import cn.palmte.work.model.process.SaleContract; import cn.palmte.work.model.process.SealTypeArray; import cn.palmte.work.model.process.SupplierMaterial; -import cn.palmte.work.model.process.BudgetPurchaseAmountModel; -import cn.palmte.work.model.process.BudgetPurchaseDetailModel; import cn.palmte.work.model.process.form.ProcessCreationForm; import cn.palmte.work.model.process.form.ProcessQueryForm; import cn.palmte.work.model.process.form.ProcessUpdateForm; @@ -326,7 +327,7 @@ public class ProcessController { public ProjectProcessDetail get(@PathVariable int id) { ProjectProcess process = processService.getById(id); if (process == null) { - throw new RuntimeException("流程不存在"); + throw ErrorMessageException.failed("流程不存在"); } ProjectProcessDetail detail = new ProjectProcessDetail();