Browse Source

common 包,基础组件

YunaiV 4 years ago
parent
commit
0a6078610b

+ 31 - 0
src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java

@@ -0,0 +1,31 @@
+package cn.iocoder.dashboard.common.exception;
+
+import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange;
+import lombok.Data;
+
+/**
+ * 错误码对象
+ *
+ * 全局错误码,占用 [0, 999],参见 {@link GlobalException}
+ * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange}
+ *
+ * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备
+ */
+@Data
+public class ErrorCode {
+
+    /**
+     * 错误码
+     */
+    private final Integer code;
+    /**
+     * 错误提示
+     */
+    private final String message;
+
+    public ErrorCode(Integer code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+}

+ 41 - 0
src/main/java/cn/iocoder/dashboard/common/exception/GlobalException.java

@@ -0,0 +1,41 @@
+package cn.iocoder.dashboard.common.exception;
+
+import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 全局异常 Exception
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GlobalException extends RuntimeException {
+
+    /**
+     * 全局错误码
+     *
+     * @see GlobalErrorCodeConstants
+     */
+    private Integer code;
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 空构造方法,避免反序列化问题
+     */
+    public GlobalException() {
+    }
+
+    public GlobalException(ErrorCode errorCode) {
+        this.code = errorCode.getCode();
+        this.message = errorCode.getMessage();
+    }
+
+    public GlobalException(Integer code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+}

+ 59 - 0
src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java

@@ -0,0 +1,59 @@
+package cn.iocoder.dashboard.common.exception;
+
+import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 业务逻辑异常 Exception
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public final class ServiceException extends RuntimeException {
+
+    /**
+     * 业务错误码
+     *
+     * @see ServiceErrorCodeRange
+     */
+    private Integer code;
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 空构造方法,避免反序列化问题
+     */
+    public ServiceException() {
+    }
+
+    public ServiceException(ErrorCode errorCode) {
+        this.code = errorCode.getCode();
+        this.message = errorCode.getMessage();
+    }
+
+    public ServiceException(Integer code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public ServiceException setCode(Integer code) {
+        this.code = code;
+        return this;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public ServiceException setMessage(String message) {
+        this.message = message;
+        return this;
+    }
+
+}

+ 38 - 0
src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java

@@ -0,0 +1,38 @@
+package cn.iocoder.dashboard.common.exception.enums;
+
+import cn.iocoder.dashboard.common.exception.ErrorCode;
+
+/**
+ * 全局错误码枚举
+ * 0-999 系统异常编码保留
+ *
+ * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
+ * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的
+ * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。
+ *
+ * @author 芋道源码
+ */
+public interface GlobalErrorCodeConstants {
+
+    ErrorCode SUCCESS = new ErrorCode(0, "成功");
+
+    // ========== 客户端错误段 ==========
+
+    ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确");
+    ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录");
+    ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限");
+    ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到");
+    ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确");
+
+    // ========== 服务端错误段 ==========
+
+    ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
+
+    ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
+
+   static boolean isMatch(Integer code) {
+       return code != null
+               && code >= SUCCESS.getCode() && code <= UNKNOWN.getCode();
+   }
+
+}

+ 34 - 0
src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java

@@ -0,0 +1,34 @@
+package cn.iocoder.dashboard.common.exception.enums;
+
+/**
+ * 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用
+ *
+ * 一共 10 位,分成四段
+ *
+ * 第一段,1 位,类型
+ *      1 - 业务级别异常
+ *      x - 预留
+ * 第二段,3 位,系统类型
+ *      001 - 用户系统
+ *      002 - 商品系统
+ *      003 - 订单系统
+ *      004 - 支付系统
+ *      005 - 优惠劵系统
+ *      ... - ...
+ * 第三段,3 位,模块
+ *      不限制规则。
+ *      一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子:
+ *          001 - OAuth2 模块
+ *          002 - User 模块
+ *          003 - MobileCode 模块
+ * 第四段,3 位,错误码
+ *       不限制规则。
+ *       一般建议,每个模块自增。
+ *
+ * @author 芋道源码
+ */
+public class ServiceErrorCodeRange {
+
+    // 模块 system 错误码区间 [1-000-001-000 ~ 1-000-002-000]
+
+}

+ 122 - 0
src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java

@@ -0,0 +1,122 @@
+package cn.iocoder.dashboard.common.exception.util;
+
+import cn.iocoder.dashboard.common.exception.ErrorCode;
+import cn.iocoder.dashboard.common.exception.ServiceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * {@link ServiceException} 工具类
+ *
+ * 目的在于,格式化异常信息提示。
+ * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化
+ *
+ * 因为 {@link #MESSAGES} 里面默认是没有异常信息提示的模板的,所以需要使用方自己初始化进去。目前想到的有几种方式:
+ *
+ * 1. 异常提示信息,写在枚举类中,例如说,cn.iocoder.oceans.user.api.constants.ErrorCodeEnum 类 + ServiceExceptionConfiguration
+ * 2. 异常提示信息,写在 .properties 等等配置文件
+ * 3. 异常提示信息,写在 Apollo 等等配置中心中,从而实现可动态刷新
+ * 4. 异常提示信息,存储在 db 等等数据库中,从而实现可动态刷新
+ */
+public class ServiceExceptionUtil {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(ServiceExceptionUtil.class);
+
+    /**
+     * 错误码提示模板
+     */
+    private static final ConcurrentMap<Integer, String> MESSAGES = new ConcurrentHashMap<>();
+
+    public static void putAll(Map<Integer, String> messages) {
+        ServiceExceptionUtil.MESSAGES.putAll(messages);
+    }
+
+    public static void put(Integer code, String message) {
+        ServiceExceptionUtil.MESSAGES.put(code, message);
+    }
+
+    public static void delete(Integer code, String message) {
+        ServiceExceptionUtil.MESSAGES.remove(code, message);
+    }
+
+    // ========== 和 ServiceException 的集成 ==========
+
+    public static ServiceException exception(ErrorCode errorCode) {
+        String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMessage());
+        return exception0(errorCode.getCode(), messagePattern);
+    }
+
+    public static ServiceException exception(ErrorCode errorCode, Object... params) {
+        String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMessage());
+        return exception0(errorCode.getCode(), messagePattern, params);
+    }
+
+    /**
+     * 创建指定编号的 ServiceException 的异常
+     *
+     * @param code 编号
+     * @return 异常
+     */
+    public static ServiceException exception(Integer code) {
+        return exception0(code, MESSAGES.get(code));
+    }
+
+    /**
+     * 创建指定编号的 ServiceException 的异常
+     *
+     * @param code 编号
+     * @param params 消息提示的占位符对应的参数
+     * @return 异常
+     */
+    public static ServiceException exception(Integer code, Object... params) {
+        return exception0(code, MESSAGES.get(code), params);
+    }
+
+    public static ServiceException exception0(Integer code, String messagePattern, Object... params) {
+        String message = doFormat(code, messagePattern, params);
+        return new ServiceException(code, message);
+    }
+
+    // ========== 格式化方法 ==========
+
+    /**
+     * 将错误编号对应的消息使用 params 进行格式化。
+     *
+     * @param code           错误编号
+     * @param messagePattern 消息模版
+     * @param params         参数
+     * @return 格式化后的提示
+     */
+    private static String doFormat(int code, String messagePattern, Object... params) {
+        StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
+        int i = 0;
+        int j;
+        int l;
+        for (l = 0; l < params.length; l++) {
+            j = messagePattern.indexOf("{}", i);
+            if (j == -1) {
+                LOGGER.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
+                if (i == 0) {
+                    return messagePattern;
+                } else {
+                    sbuf.append(messagePattern.substring(i, messagePattern.length()));
+                    return sbuf.toString();
+                }
+            } else {
+                sbuf.append(messagePattern.substring(i, j));
+                sbuf.append(params[l]);
+                i = j + 2;
+            }
+        }
+        if (messagePattern.indexOf("{}", i) != -1) {
+            LOGGER.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params);
+        }
+        sbuf.append(messagePattern.substring(i, messagePattern.length()));
+        return sbuf.toString();
+    }
+
+}

+ 6 - 0
src/main/java/cn/iocoder/dashboard/common/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * 基础的通用类,和框架无关
+ *
+ * 例如说,CommonResult 为通用返回
+ */
+package cn.iocoder.dashboard.common;

+ 106 - 0
src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java

@@ -0,0 +1,106 @@
+package cn.iocoder.dashboard.common.pojo;
+
+import cn.iocoder.dashboard.common.exception.ErrorCode;
+import cn.iocoder.dashboard.common.exception.GlobalException;
+import cn.iocoder.dashboard.common.exception.ServiceException;
+import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+import org.springframework.util.Assert;
+
+import java.io.Serializable;
+
+/**
+ * 通用返回
+ *
+ * @param <T> 数据泛型
+ */
+@Data
+public final class CommonResult<T> implements Serializable {
+
+    /**
+     * 错误码
+     *
+     * @see ErrorCode#getCode()
+     */
+    private Integer code;
+    /**
+     * 返回数据
+     */
+    private T data;
+    /**
+     * 错误提示,用户可阅读
+     *
+     * @see ErrorCode#getMessage() ()
+     */
+    private String msg;
+
+    /**
+     * 将传入的 result 对象,转换成另外一个泛型结果的对象
+     *
+     * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。
+     *
+     * @param result 传入的 result 对象
+     * @param <T> 返回的泛型
+     * @return 新的 CommonResult 对象
+     */
+    public static <T> CommonResult<T> error(CommonResult<?> result) {
+        return error(result.getCode(), result.getMsg());
+    }
+
+    public static <T> CommonResult<T> error(Integer code, String message) {
+        Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!");
+        CommonResult<T> result = new CommonResult<>();
+        result.code = code;
+        result.msg = message;
+        return result;
+    }
+
+    public static <T> CommonResult<T> error(ErrorCode errorCode) {
+        return error(errorCode.getCode(), errorCode.getMessage());
+    }
+
+    public static <T> CommonResult<T> success(T data) {
+        CommonResult<T> result = new CommonResult<>();
+        result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
+        result.data = data;
+        result.msg = "";
+        return result;
+    }
+
+    @JSONField(serialize = false) // 避免序列化
+    public boolean isSuccess() {
+        return GlobalErrorCodeConstants.SUCCESS.getCode().equals(code);
+    }
+
+    @JSONField(serialize = false) // 避免序列化
+    public boolean isError() {
+        return !isSuccess();
+    }
+
+    // ========= 和 Exception 异常体系集成 =========
+
+    /**
+     * 判断是否有异常。如果有,则抛出 {@link GlobalException} 或 {@link ServiceException} 异常
+     */
+    public void checkError() throws GlobalException, ServiceException {
+        if (isSuccess()) {
+            return;
+        }
+        // 全局异常
+        if (GlobalErrorCodeConstants.isMatch(code)) {
+            throw new GlobalException(code, msg);
+        }
+        // 业务异常
+        throw new ServiceException(code, msg);
+    }
+
+    public static <T> CommonResult<T> error(ServiceException serviceException) {
+        return error(serviceException.getCode(), serviceException.getMessage());
+    }
+
+    public static <T> CommonResult<T> error(GlobalException globalException) {
+        return error(globalException.getCode(), globalException.getMessage());
+    }
+
+}

+ 26 - 0
src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java

@@ -0,0 +1,26 @@
+package cn.iocoder.dashboard.common.pojo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.hibernate.validator.constraints.Range;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@ApiModel("分页参数")
+@Data
+public class PageParam implements Serializable {
+
+    @ApiModelProperty(value = "页码,从 1 开始", required = true,example = "1")
+    @NotNull(message = "页码不能为空")
+    @Min(value = 1, message = "页码最小值为 1")
+    private Integer pageNo;
+
+    @ApiModelProperty(value = "每页条数,最大值为 100", required = true, example = "10")
+    @NotNull(message = "每页条数不能为空")
+    @Range(min = 1, max = 100, message = "条数范围为 [1, 100]")
+    private Integer pageSize;
+
+}

+ 20 - 0
src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java

@@ -0,0 +1,20 @@
+package cn.iocoder.dashboard.common.pojo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@ApiModel("分页结果")
+@Data
+public final class PageResult<T> implements Serializable {
+
+    @ApiModelProperty(value = "数据", required = true)
+    private List<T> list;
+
+    @ApiModelProperty(value = "总量", required = true)
+    private Long total;
+
+}

+ 56 - 0
src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java

@@ -0,0 +1,56 @@
+package cn.iocoder.dashboard.common.pojo;
+
+import java.io.Serializable;
+
+/**
+ * 排序字段 DTO
+ *
+ * 类名加了 ing 的原因是,避免和 ES SortField 重名。
+ */
+public class SortingField implements Serializable {
+
+    /**
+     * 顺序 - 升序
+     */
+    public static final String ORDER_ASC = "asc";
+    /**
+     * 顺序 - 降序
+     */
+    public static final String ORDER_DESC = "desc";
+
+    /**
+     * 字段
+     */
+    private String field;
+    /**
+     * 顺序
+     */
+    private String order;
+
+    // 空构造方法,解决反序列化
+    public SortingField() {
+    }
+
+    public SortingField(String field, String order) {
+        this.field = field;
+        this.order = order;
+    }
+
+    public String getField() {
+        return field;
+    }
+
+    public SortingField setField(String field) {
+        this.field = field;
+        return this;
+    }
+
+    public String getOrder() {
+        return order;
+    }
+
+    public SortingField setOrder(String order) {
+        this.order = order;
+        return this;
+    }
+}

+ 66 - 0
src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java

@@ -0,0 +1,66 @@
+package cn.iocoder.dashboard.util.collection;
+
+import cn.hutool.core.collection.CollectionUtil;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Collection 工具类
+ *
+ * @author 芋道源码
+ */
+public class CollectionUtils {
+
+    public static <T> Set<T> asSet(T... objs) {
+        return new HashSet<>(Arrays.asList(objs));
+    }
+
+    public static <T, U> List<U> convertList(List<T> from, Function<T, U> func) {
+        return from.stream().map(func).collect(Collectors.toList());
+    }
+
+    public static <T, U> Set<U> convertSet(List<T> from, Function<T, U> func) {
+        return from.stream().map(func).collect(Collectors.toSet());
+    }
+
+    public static <T, K> Map<K, T> convertMap(List<T> from, Function<T, K> keyFunc) {
+        return from.stream().collect(Collectors.toMap(keyFunc, item -> item));
+    }
+
+    public static <T, K, V> Map<K, V> convertMap(List<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
+        return from.stream().collect(Collectors.toMap(keyFunc, valueFunc));
+    }
+
+    public static <T, K> Map<K, List<T>> convertMultiMap(List<T> from, Function<T, K> keyFunc) {
+        return from.stream().collect(Collectors.groupingBy(keyFunc,
+                Collectors.mapping(t -> t, Collectors.toList())));
+    }
+
+    public static <T, K, V> Map<K, List<V>> convertMultiMap(List<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
+        return from.stream().collect(Collectors.groupingBy(keyFunc,
+                Collectors.mapping(valueFunc, Collectors.toList())));
+    }
+
+    // 暂时没想好名字,先以 2 结尾噶
+    public static <T, K, V> Map<K, Set<V>> convertMultiMap2(List<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
+        return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet())));
+    }
+
+    public static boolean containsAny(Collection<?> source, Collection<?> candidates) {
+        return org.springframework.util.CollectionUtils.containsAny(source, candidates);
+    }
+
+    public static <T> T getFirst(List<T> from) {
+        return !CollectionUtil.isEmpty(from) ? from.get(0) : null;
+    }
+
+    public static <T> void addIfNotNull(Collection<T> coll, T item) {
+        if (item == null) {
+            return;
+        }
+        coll.add(item);
+    }
+
+}

+ 29 - 0
src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java

@@ -0,0 +1,29 @@
+package cn.iocoder.dashboard.util.collection;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.google.common.collect.Multimap;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Map 工具类
+ *
+ * @author 芋道源码
+ */
+public class MapUtils {
+
+    public static <K, V> List<V> getList(Multimap<K, V> multimap, Collection<K> keys) {
+        List<V> result = new ArrayList<>();
+        keys.forEach(k -> {
+            Collection<V> values = multimap.get(k);
+            if (CollectionUtil.isEmpty(values)) {
+                return;
+            }
+            result.addAll(values);
+        });
+        return result;
+    }
+
+}

+ 7 - 0
src/main/java/cn/iocoder/dashboard/util/package-info.java

@@ -0,0 +1,7 @@
+/**
+ * 对于工具类的选择,优先查找 Hutool 中有没对应的方法
+ * 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分
+ *
+ * ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。
+ */
+package cn.iocoder.dashboard.util;