手把手教你使用 Spring Boot 3 开发上线一个前后端分离的生产级系统(三) - 项目初始化

项目开发前,请确保已安装好以下开发环境:

  • MySQL 8.0
  • Redis 7.0
  • Elasticsearch 8.2.0(选装)
  • RabbitMQ 3.10.2(选装)
  • JDK 17
  • Maven 3.8.5
  • IntelliJ IDEA(选装)
  • Node 16.14

我们使用 Spring Initializr 来初始化我们的项目。

Spring Initializr

操作步骤如下:

  1. 导航到 Spring Initializr。该服务引入应用程序所需的所有依赖项,并自动完成大部分设置。

  2. 选择 Gradle 或 Maven 以及要使用的语言。本项目选择 Maven 和 Java。

  3. 选择 Spring Boot 的版本。本项目选择 3.0.0-SNAPSHOT 版本。

  4. 填写项目元数据。本项目选择 Java 17 版本。

  5. 单击 ADD DEPENDENCIES 并选择项目依赖项。本项目选择的依赖如上图所示。

  6. 单击 GENERATE,下载生成的 ZIP 文件,该文件是根据我们的选择来配置的 Spring Boot 应用程序存档。

  7. 解压 ZIP 文件后导入我们的 IDE 中即可。

注:如果我们的 IDE 具有 Spring Initializr 集成,可以直接从 IDE 中完成此过程

包结构创建

在项目 src/main/java 下面创建如下的包结构:

io
 +- github
     +- xxyopen   
        +- novel
            +- NovelApplication.java -- 项目启动类
            |
            +- core -- 项目核心模块,包括各种工具、配置和常量等
            |   +- common -- 业务无关的通用模块
            |   |   +- exception -- 通用异常处理
            |   |   +- constant -- 通用常量   
            |   |   +- req -- 通用请求数据格式封装,例如分页请求数据  
            |   |   +- resp -- 接口响应工具及响应数据格式封装 
            |   |   +- util -- 通用工具   
            |   | 
            |   +- auth -- 用户认证授权相关
            |   +- config -- 业务相关配置
            |   +- constant -- 业务相关常量         
            |   +- filter -- 过滤器 
            |   +- interceptor -- 拦截器
            |   +- task -- 定时任务
            |   +- util -- 业务相关工具 
            |   +- wrapper -- 装饰器
            |
            +- dto -- 数据传输对象,包括对各种 Http 请求和响应数据的封装
            |   +- req -- Http 请求数据封装
            |   +- resp -- Http 响应数据封装
            |
            +- dao -- 数据访问层,与底层 MySQL 进行数据交互
            +- manager -- 通用业务处理层,对第三方平台封装、对 Service 层通用能力的下沉以及对多个 DAO 的组合复用 
            +- service -- 相对具体的业务逻辑服务层  
            +- controller -- 主要是处理各种 Http 请求,各类基本参数校验,或者不复用的业务简单处理,返回 JSON 数据等
            |   +- front -- 小说门户相关接口
            |   +- author -- 作家管理后台相关接口
            |   +- admin -- 平台管理后台相关接口
            |   +- app -- app 接口
            |   +- applet -- 小程序接口
            |   +- open -- 开放接口,供第三方调用 

通用请求/响应数据格式封装

  1. io.github.xxyopen.novel.core.common.req包下创建分页请求数据格式封装类:
/**
 * 分页请求数据格式封装,所有分页请求的 Dto 类都应继承该类
 *
 * @author xiongxiaoyang
 * @date 2022/5/11
 */
@Data
public class PageReqDto {

    /**
     * 请求页码,默认第 1 页
     * */
    private int pageNum = 1;

    /**
     * 每页大小,默认每页 10 条
     * */
    private int pageSize = 10;

    /**
     * 是否查询所有,默认不查所有
     * 为 true 时,pageNum 和 pageSize 无效
     * */
    private boolean fetchAll = false;
    
}
  1. io.github.xxyopen.novel.core.common.resp包下创建分页响应数据格式封装类:
/**
 * 分页响应数据格式封装
 *
 * @author xiongxiaoyang
 * @date 2022/5/11
 */
@Getter
public class PageRespDto<T> {

    /**
     * 页码
     */
    private final long pageNum;

    /**
     * 每页大小
     */
    private final long pageSize;

    /**
     * 总记录数
     */
    private final long total;

    /**
     * 分页数据集
     */
    private final List<? extends T> list;

    /**
     * 该构造函数用于通用分页查询的场景
     * 接收普通分页数据和普通集合
     */
    public PageRespDto(long pageNum, long pageSize, long total, List<T> list) {
        this.pageNum = pageNum;
        this.pageSize = pageSize;
        this.total = total;
        this.list = list;
    }

    public static <T> PageRespDto<T> of(long pageNum, long pageSize, long total, List<T> list) {
        return new PageRespDto<>(pageNum, pageSize, total, list);
    }

    /**
     * 获取分页数
     * */
    public long getPages() {
        if (this.pageSize == 0L) {
            return 0L;
        } else {
            long pages = this.total / this.pageSize;
            if (this.total % this.pageSize != 0L) {
                ++pages;
            }

            return pages;
        }
    }
}

Rest 接口响应工具及响应数据格式封装

  1. io.github.xxyopen.novel.core.common.constant包下创建错误码枚举类:
/**
 * 错误码枚举类。
 *
 * 错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。
 * 错误产生来源分为 A/B/C, A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付
 * 超时等问题; B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题; C 表示错误来源
 * 于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999,大类之间的
 * 步长间距预留 100。
 *
 * 错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。
 * 在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码。
 *
 * @author xiongxiaoyang
 * @date 2022/5/11
 */
@Getter
@AllArgsConstructor
public enum ErrorCodeEnum {

    /**
     * 正确执行后的返回
     * */
    OK("00000","一切 ok"),

    /**
     * 一级宏观错误码,用户端错误
     * */
    USER_ERROR("A0001","用户端错误"),

    /**
     * 二级宏观错误码,用户注册错误
     * */
    USER_REGISTER_ERROR("A0100","用户注册错误"),

    /**
     * 二级宏观错误码,用户未同意隐私协议
     * */
    USER_NO_AGREE_PRIVATE_ERROR("A0101","用户未同意隐私协议"),

    /**
     * 二级宏观错误码,注册国家或地区受限
     * */
    USER_REGISTER_AREA_LIMIT_ERROR("A0102","注册国家或地区受限"),

    /**
     * 二级宏观错误码,用户请求参数错误
     * */
    USER_REQUEST_PARAM_ERROR("A0400","用户请求参数错误"),

    // ...省略若干用户端二级宏观错误码

    /**
     * 一级宏观错误码,系统执行出错
     * */
    SYSTEM_ERROR("B0001","系统执行出错"),

    /**
     * 二级宏观错误码,系统执行超时
     * */
    SYSTEM_TIMEOUT_ERROR("B0100","系统执行超时"),

    // ...省略若干系统执行二级宏观错误码

    /**
     * 一级宏观错误码,调用第三方服务出错
     * */
    THIRD_SERVICE_ERROR("C0001","调用第三方服务出错"),

    /**
     * 一级宏观错误码,中间件服务出错
     * */
    MIDDLEWARE_SERVICE_ERROR("C0100","中间件服务出错")

    // ...省略若干三方服务调用二级宏观错误码    

    ;

    /**
     * 错误码
     * */
    private String code;

    /**
     * 中文描述
     * */
    private String message;

}
  1. io.github.xxyopen.novel.core.common.resp包下创建 Http Rest 响应工具及数据格式封装类:
/**
 * Http Rest 响应工具及数据格式封装
 *
 * @author xiongxiaoyang
 * @date 2022/5/11
 */
@Getter
public class RestResp<T> {

    /**
     * 响应码
     */
    private String code;

    /**
     * 响应消息
     */
    private String message;

    /**
     * 响应数据
     */
    private T data;

    private RestResp() {
        this.code = ErrorCodeEnum.OK.getCode();
        this.message = ErrorCodeEnum.OK.getMessage();
    }

    private RestResp(ErrorCodeEnum errorCode) {
        this.code = errorCode.getCode();
        this.message = errorCode.getMessage();
    }

    private RestResp(T data) {
        this.data = data;
    }

    /**
     * 业务处理成功,无数据返回
     */
    public static RestResp<Void> ok() {
        return new RestResp<>();
    }

    /**
     * 业务处理成功,有数据返回
     */
    public static <T> RestResp<T> ok(T data) {
        return new RestResp<>(data);
    }

    /**
     * 业务处理失败
     */
    public static RestResp<Void> fail(ErrorCodeEnum errorCode) {
        return new RestResp<>(errorCode);
    }

    /**
     * 系统错误
     */
    public static RestResp<Void> error() {
        return new RestResp<>(ErrorCodeEnum.SYSTEM_ERROR);
    }

    /**
     * 判断是否成功
     */
    public boolean isOk() {
        return Objects.equals(this.code, ErrorCodeEnum.OK.getCode());
    }
    
}

通用异常处理

在 Spring 3.2 中,新增了 @ControllerAdvice 注解,用于定义适用于所有 @RequestMapping 方法的 @ExceptionHandler、@InitBinder 和 @ModelAttribute 方法。Spring Boot 默认情况下会映射到 /error 进行异常处理,但是提示十分不友好。我们可以使用该注解定义 @ExceptionHandler 方法来捕获 Controller 抛出的通用异常,并统一进行处理。

  1. io.github.xxyopen.novel.core.common.exception包下创建自定义业务异常类:
/**
 * 自定义业务异常,用于处理用户请求时,业务错误时抛出
 *
 * @author xiongxiaoyang
 * @date 2022/5/11
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class BusinessException extends RuntimeException {

    private final ErrorCodeEnum errorCodeEnum;

    public BusinessException(ErrorCodeEnum errorCodeEnum) {
        // 不调用父类 Throwable的fillInStackTrace() 方法生成栈追踪信息,提高应用性能
        // 构造器之间的调用必须在第一行
        super(errorCodeEnum.getMessage(), null, false, false);
        this.errorCodeEnum = errorCodeEnum;
    }

}
  1. io.github.xxyopen.novel.core.common.exception包下创建通用异常处理器,处理系统异常、数据校验异常和我们自定义的业务异常:
/**
 * 通用的异常处理器
 *
 * @author xiongxiaoyang
 * @date 2022/5/11
 */
@Slf4j
@RestControllerAdvice
public class CommonExceptionHandler {

    /**
     * 处理数据校验异常
     * */
    @ExceptionHandler(BindException.class)
    public RestResp<Void> handlerBindException(BindException e){
        log.error(e.getMessage(),e);
        return RestResp.fail(ErrorCodeEnum.USER_REQUEST_PARAM_ERROR);
    }

    /**
     * 处理业务异常
     * */
    @ExceptionHandler(BusinessException.class)
    public RestResp<Void> handlerBusinessException(BusinessException e){
        log.error(e.getMessage(),e);
        return RestResp.fail(e.getErrorCodeEnum());
    }

    /**
     * 处理系统异常
     * */
    @ExceptionHandler(Exception.class)
    public RestResp<Void> handlerException(Exception e){
        log.error(e.getMessage(),e);
        return RestResp.error();
    }

}

常量类创建

  1. io.github.xxyopen.novel.core.common.constant包下创建通用常量类和 API 路由常量类
/**
 * 通用常量
 *
 * @author xiongxiaoyang
 * @date 2022/5/12
 */
public class CommonConsts {

    /**
     * 是
     * */
    public static final Integer YES = 1;

    /**
     * 否
     * */
    public static final Integer NO = 0;

    /**
     * 性别常量
     * */
    public enum SexEnum{

        /**
         * 男
         * */
        MALE(0,"男"),

        /**
         * 女
         * */
        FEMALE(1,"女");

        SexEnum(int code,String desc){
            this.code = code;
            this.desc = desc;
        }

        private int code;
        private String desc;

        public int getCode() {
            return code;
        }

        public String getDesc() {
            return desc;
        }

    }

    // ...省略若干常量
}
/**
 * API 路由常量
 *
 * @author xiongxiaoyang
 * @date 2022/5/12
 */
public class ApiRouterConsts {

    /**
     * API请求路径前缀
     */
    String API_URL_PREFIX = "/api";

    /**
     * 前台门户系统请求路径前缀
     */
    String API_FRONT_URL_PREFIX = API_URL_PREFIX + "/front";

    /**
     * 作家管理系统请求路径前缀
     */
    String API_AUTHOR_URL_PREFIX = API_URL_PREFIX + "/author";

    /**
     * 平台后台管理系统请求路径前缀
     */
    String API_ADMIN_URL_PREFIX = API_URL_PREFIX + "/admin";

    /**
     * 首页模块请求路径前缀
     * */
    String HOME_URL_PREFIX = "/home";

    /**
     * 小说模块请求路径前缀
     * */
    String BOOK_URL_PREFIX = "/book";

    /**
     * 会员模块请求路径前缀
     * */
    String USER_URL_PREFIX = "/user";

    /**
     * 前台门户首页API请求路径前缀
     */
    String API_FRONT_HOME_URL_PREFIX = API_FRONT_URL_PREFIX + HOME_URL_PREFIX;

    /**
     * 前台门户小说相关API请求路径前缀
     */
    String API_FRONT_BOOK_URL_PREFIX = API_FRONT_URL_PREFIX + BOOK_URL_PREFIX;

    /**
     * 前台门户会员相关API请求路径前缀
     */
    String API_FRONT_USER_URL_PREFIX = API_FRONT_URL_PREFIX + USER_URL_PREFIX;

    // ...省略若干常量

}
  1. io.github.xxyopen.novel.core.constant包下创建缓存常量类
/**
 * 缓存相关常量
 *
 * @author xiongxiaoyang
 * @date 2022/5/12
 */
public class CacheConsts {

    /**
     * 本项目 Redis 缓存前缀
     * */
    public static final String REDIS_CACHE_PREFIX = "Cache::Novel::";


    /**
     * Caffeine 缓存管理器
     * */
    public static final String CAFFEINE_CACHE_MANAGER = "caffeineCacheManager";

    /**
     * Redis 缓存管理器
     * */
    public static final String REDIS_CACHE_MANAGER = "redisCacheManager";

    /**
     * 首页小说推荐缓存
     * */
    public static final String HOME_BOOK_CACHE_NAME = "homeBookCache";

    /**
     * 首页友情链接缓存
     * */
    public static final String HOME_FRIEND_LINK_CACHE_NAME = "homeFriendLinkCache";

    /**
     * 缓存配置常量
     */
    public enum CacheEnum {

        HOME_BOOK_CACHE(1,HOME_BOOK_CACHE_NAME,0,1),

        HOME_FRIEND_LINK_CACHE(2,HOME_FRIEND_LINK_CACHE_NAME,1000,1)

        ;

        /**
         * 缓存类型 0-本地 1-本地和远程 2-远程
         */
        private int type;
        /**
         * 缓存的名字
         */
        private String name;
        /**
         * 失效时间(秒) 0-永不失效
         */
        private int ttl;
        /**
         * 最大容量
         */
        private int maxSize;

        CacheEnum(int type, String name, int ttl, int maxSize) {
            this.type = type;
            this.name = name;
            this.ttl = ttl;
            this.maxSize = maxSize;
        }

        public boolean isLocal() {
            return type <= 1;
        }

        public boolean isRemote() {
            return type >= 1;
        }

        public String getName() {
            return name;
        }

        public int getTtl() {
            return ttl;
        }

        public int getMaxSize() {
            return maxSize;
        }

    }

}
posted @ 2022-05-28 11:36  xxyopen  阅读(790)  评论(0编辑  收藏  举报