springboot-starter-kit 企业级脚手架

本文将从零开始搭建一个 springboot-starter-kit 脚手架项目,以支持当前主流的前后端分离架构,开箱即用,快速开发!

模块分层

api(controller) –> service –> mapper(dao) –> pojo(entity) –> common

logback 日志处理

在 api 模块的 resources 目录下创建 logback-spring.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<!-- 日志文件存放路径 -->
<!-- <property name="logback.dir" value="/Users/zhanghao/data/@project.name@/log"/>-->
<springProperty scope="context" name="logback.dir" source="logging.logback.dir"/>

<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr"
converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wex"
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<conversionRule conversionWord="wEx"
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>

<!-- 控制台日志渲染格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(-){faint} %clr([%13.13t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<!-- 日志文件渲染格式 -->
<property name="FILE_LOG_PATTERN"
value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } - [%13.13t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>


<!-- 输出到控制台 -->
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<!-- 日志格式 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
${CONSOLE_LOG_PATTERN}
</pattern>
</layout>
<!-- 日志等级 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>

<appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 如果只是想要 Info 级别的日志,只是过滤 info 还是会输出 Error 日志,因为 Error 的级别高,
所以我们使用下面的策略,可以避免输出 Error 的日志 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤 Error -->
<level>ERROR</level>
<!-- 匹配到就禁止 -->
<onMatch>DENY</onMatch>
<!-- 没有匹配到就允许 -->
<onMismatch>ACCEPT</onMismatch>
</filter>
<!-- 日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则
如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天
的日志改名为今天的日期。即,<File> 的日志都是当天的。-->
<File>${logback.dir}/info.log</File>
<!--滚动策略,按照时间滚动 TimeBasedRollingPolicy-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
<FileNamePattern>${logback.dir}/info.%d{yyyy-MM-dd}.log</FileNamePattern>
<!--只保留最近90天的日志-->
<maxHistory>90</maxHistory>
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<!--<totalSizeCap>1GB</totalSizeCap>-->
</rollingPolicy>
<!--日志输出编码格式化-->
<encoder>
<charset>UTF-8</charset>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>


<appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--如果只是想要 Error 级别的日志,那么需要过滤一下,默认是 info 级别的,ThresholdFilter-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>Error</level>
</filter>
<!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则
如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天
的日志改名为今天的日期。即,<File> 的日志都是当天的。
-->
<File>${logback.dir}/error.log</File>
<!--滚动策略,按照时间滚动 TimeBasedRollingPolicy-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
<FileNamePattern>${logback.dir}/error.%d{yyyy-MM-dd}.log</FileNamePattern>
<!--只保留最近90天的日志-->
<maxHistory>90</maxHistory>
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<!--<totalSizeCap>1GB</totalSizeCap>-->
</rollingPolicy>
<!--日志输出编码格式化-->
<encoder>
<charset>UTF-8</charset>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>

<!--指定最基础的日志输出级别-->
<root level="INFO">
<!-- appender 将会添加到这个 logger -->
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileInfoLog"/>
<appender-ref ref="fileErrorLog"/>
</root>

</configuration>

在 application.yml 中添加日志根目录

1
2
3
logging:
logback:
dir: /Users/zhanghao/data/springboot-starter-kit/log

统一响应对象

异常对象枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package org.example.common.enums;

/**
* 错误码与错误信息枚举类
*
* @author zhanghao13
* @since 2020/11/17 20:39
*/
public enum RetEnum {

OK(200, "成功"),
BAD_REQUEST(400, "参数错误"),
UNAUTHORIZED(401, "您未登录或您的登录信息已过期,请重新登录"),
FORBIDDEN(403, "您没有该权限"),
NOT_FOUND(404, "您请求的资源未找到"),
SERVER_ERROR(500, "服务器异常"),
;

public final int code;
public final String msg;

RetEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
}

统一响应对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package org.example.common;

import lombok.Data;
import lombok.experimental.Accessors;
import org.example.common.enums.RetEnum;

import java.io.Serializable;

/**
* 统一响应, 用于返回 json 响应体
*
* @author zhanghao13
* @since 2020/11/17 20:31
*/
@Data
@Accessors(chain = true)
public class Ret implements Serializable {

private static final long serialVersionUID = 2665482313176083754L;

private int code;
private String msg;
private Object data;

public static Ret ok() {
return new Ret().setCode(RetEnum.OK.code).setMsg(RetEnum.OK.msg);
}

public static Ret ok(Object data) {
return Ret.ok().setData(data);
}

public static Ret fail(int code, String msg) {
return new Ret().setCode(code).setMsg(msg);
}

public static Ret fail(String msg) {
return Ret.fail(RetEnum.BAD_REQUEST.code, msg);
}

public static Ret fail(RetEnum retEnum) {
return Ret.fail(retEnum.code, retEnum.msg);
}

// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓以下为常用业务失败封装↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

public static Ret badRequest() {
return Ret.fail(RetEnum.BAD_REQUEST);
}

public static Ret unauthorized() {
return Ret.fail(RetEnum.UNAUTHORIZED);
}

public static Ret forbidden() {
return Ret.fail(RetEnum.FORBIDDEN);
}

public static Ret notFound() {
return Ret.fail(RetEnum.NOT_FOUND);
}

public static Ret serverError() {
return Ret.fail(RetEnum.SERVER_ERROR);
}

}

业务异常类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package org.example.common.exception;

/**
* 参数校验异常类
* 此异常阻止了异常栈追踪信息, 提高了性能, 避免抛出不必要的异常栈, 干扰调试
*
* @author zhanghao13
* @since 2020/11/18 14:29
*/
public class ServiceException extends RuntimeException {

private static final long serialVersionUID = -3287961933635362725L;

/**
* 仅记录异常信息, 不记录 cause by 和 stack trace, 提高性能
*
* @param message 异常信息
*/
public ServiceException(String message) {
/*
message 异常的描述信息,也就是在打印栈追踪信息时异常类名后面紧跟着的描述字符串
cause 导致此异常发生的父异常,即追踪信息里的caused by
enableSuppress 关于异常挂起的参数,这里我们永远设为false即可
writableStackTrace 表示是否生成栈追踪信息,只要将此参数设为false, 则在构造异常对象时就不会调用fillInStackTrace()
*/
super(message, null, false, false);
}

}

全局异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package org.example.api.configuration;

import lombok.extern.slf4j.Slf4j;
import org.example.common.Ret;
import org.example.common.exception.ServiceException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
* 全局统一异常处理类
*
* @author zhanghao13
* @since 2020/11/18 19:44
*/
@Slf4j
@ControllerAdvice
public class ExceptionConfiguration {

/**
* 业务异常处理器: 如参数错误
*
* @param e 异常对象
* @return Ret
*/
@ExceptionHandler(ServiceException.class)
@ResponseBody
public Ret handler(ServiceException e) {
log.info(e.getMessage(), e);
return Ret.fail(e.getMessage());
}

/**
* 默认异常处理器
*
* @param e 异常对象
* @return Ret
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public Ret handler(Exception e) {
log.error(e.getMessage(), e);
return Ret.serverError();
}

}

全局请求响应参数及耗时记录

异步调用处理

Redis 整合及序列化处理

Knife4j 接口文档整合