Java服务_服务稳定性和标准化建设六件套_缓存&监控&限流&日志&异常&响应体

Java服务_服务稳定性和标准化建设六件套_缓存&监控&限流&日志&异常&响应体

服务稳定性

在常见服务开发中,为了保障服务稳定性,一定都需要建立缓存、监控和限流机制的,这个一般通过AOP的方式建立在抽象方法的周围。

服务标准化

服务的标准化建设主要是方便自己,方便他人的一些规范性建设,我觉得主要有三点:

  1. 日志标准化:1)格式化添加uuid、erp、traceid等前缀,2)mybatis打印sql日志;
  2. 全局异常捕捉:1)异常统一定义和出口,2)异常码标准化定义;
  3. 通用响应和请求体:1)标准化header、body。

1.使用MDC添加uuid

使用MDC添加uuid可以方便与上下游请求链路串联,也可以方便研发快速定位一次请求的整个调用过程,可以在开发测试时提效,也方便快速定位线上问题。一个使用实战如下:

SLF4J、Log4j、logback的使用和配置方法就不再赘述了,可参考之前博文进行知识复习:使用SLF4J&Log4jSLF4J日志应用

1.配置文件中设置日志格式

添加了uuid、erp、PFTID三个日志前缀,其中:-uuid表示默认值为字符串’uuid’。

1
2
3
4
5
6
7
logging:
config: classpath:logback-spring.xml
pattern:
level: ' %X{uuid:-uuid} %X{erp:-erp} %X{PFTID:-PFTID} %5p'
file:
# 不要动这个配置,要是测试可以动具体的配置文件
name: /export/Logs/meta-data-center.log

输出的日志格式如下:

2.创建MDCUtil工具类

mdc是slf4j提供的一个线程安全的存放诊断日志的容器,本质和ThreadLocal类似。可以直接使用mdc来操作设置每个线程独有的一些kv数据。注意这里虽然用同一个静态方法封装了mdc的操作方法,但是每个不同的线程调用这个静态方法时,方法体中操作的是独属于自己线程的mdc。

同时还用一个ERR_MSG_KEY来保存异常发生时的错误信息,在发生错误时存入,在方法最终返回时写入返回错误信息中。

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
public class MDCUtil {

private static final String UUID_KEY = "UUID";

private static final String ERR_MSG_KEY = "errMsg";

/**
* 设置线程中全局UUID
*/
public static void setUUID() {
MDC.put(UUID_KEY, UUID.randomUUID().toString());
}

/**
* 获取线程中全局UUID
*/
public static String getUUID() {
String uuid = MDC.get(UUID_KEY);
if (Strings.isBlank(uuid)) {
setUUID();
uuid = MDC.get(UUID_KEY);
}
return uuid;
}

/**
* 设置线程中全局UUID
*/
public static void setErrMsg(String errMsg) {
MDC.put(ERR_MSG_KEY, errMsg);
}

/**
* 设置线程中全局UUID
*/
public static void addErrMsg(String errMsg) {
if (MDCUtil.getErrMsg() == null) {
MDC.put(ERR_MSG_KEY, errMsg);
} else {
MDC.put(ERR_MSG_KEY, getErrMsg() + "; " + errMsg);
}
}

/**
* 获取线程中全局UUID
*/
public static String getErrMsg() {
return MDC.get(ERR_MSG_KEY);
}

public static void clear() {
MDC.clear();
}
}

3.创建过滤器在业务方法前设置uuid,业务方法后清空MDC

一般可以通过aop的方式来设置这种统一的操作,使用过滤器或者拦截器都可以方便的实现。

1)如果上游有传入uuid,则取上游uuid写入mdc;如果上游没有传入,则取一个随机值作为uuid。2)最后一定要使用clear()方法手动清空mdc中的数据。3)注意要想日志中能够像这些设置值打印出来,一定要将key与第一步配置文件中的配置的占位字符串一致,如本例中的uuid、erp、PFTID等。

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
public class IntelligentProduceJsfFilter extends AbstractFilter {

public static final Logger LOGGER = LoggerFactory.getLogger(IntelligentProduceJsfFilter.class);

/**
* uuid的key
*/
private static final String UUID_KEY = "uuid";

/**
* erp的key
*/
private static final String ERP_KEY = "erp";

@Override
public ResponseMessage invoke(RequestMessage requestMessage) {
Object[] originalArgs = requestMessage.getInvocationBody().getArgs();
if (originalArgs != null && originalArgs.length == 1 &&
originalArgs[0] instanceof UReqData) {
// 设置uuid
UReqData uReqData = (UReqData) originalArgs[0];
Header header = uReqData.getHeader();
String uuid = null;
String erp = null;
if (header != null && header.getUuid() != null && header.getUuid().trim().length() > 0) {
uuid = uReqData.getHeader().getUuid();
}
if (header.getContext() != null && header.getContext().containsKey(ERP_KEY) &&
header.getContext().get(ERP_KEY) != null &&
header.getContext().get(ERP_KEY).trim().length() > 0) {
erp = header.getContext().get(ERP_KEY);
}
putMDCIfNotNull(UUID_KEY, uuid);
putMDCIfNotNull(ERP_KEY, erp);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("IntelligentProduceJsfFilter invoke. uuid = {}, erp = {}", uuid, erp);
}
try {
return this.getNext().invoke(requestMessage);
} finally {
MDC.clear();
}
}
return this.getNext().invoke(requestMessage);
}

/**
* 如果key-value不等于空,则将其放到MDC中
*
* @param key 键
* @param value 值
*/
private void putMDCIfNotNull(String key, String value) {
if (value != null && value.trim().length() > 0) {
MDC.put(key, value);
}
}
}

4.在spring配置文件中配置过滤器

在spring配置文件中通过ioc依赖注入的方式将IntelligentProduceJsfFilter过滤器配置在IntelligentProduceService业务类之中。

1
2
3
4
5
6
7
8
<bean id="intelligentProduceJsfFilter" class="com.jd.bdaa.arch.filter.IntelligentProduceJsfFilter" scope="prototype"/>

<jsf:provider id="intelligentProduceService"
interface="com.jd.bdaa.arch.provider.service.IntelligentProduceService"
alias="#{environment.DRIVE_JSF_ALIAS == null ? '${jsf.alias.drive:}' : environment.DRIVE_JSF_ALIAS}"
ref="intelligentProduceServiceImpl" server="data-asset-drive-jsf-server"
timeout="#{environment.DRIVE_RROVIDER_TIMEOUT == null ? 20000 : environment.DRIVE_RROVIDER_TIMEOUT}"
filter="jsfMdcInterceptor, jsfLogMessageInterceptor, intelligentProduceJsfFilter, observabilityOTelPropagatorFilter"/>

如何快速查询一个接口的所有执行日志?利用SLF4J的MDC轻松搞定

SpringBoot logback日志打印增加trace_uuid追踪接口请求输出日志

spring cloud脚手架项目(五)日志模块之出入参数和日志链路追踪UUID

2.mybatis打印sql日志

只需在yml文件中添加一行配置即可,就是logging.level.com.jd.data.application.decision.mapper=debug,其中key就是mapper类所在的分层。

案例如下:

1
2
3
4
5
6
7
logging:
file: ../Logs/decision-engine.log
pattern:
level: ' %X{uuid:-uuid} %X{pin:-pin} %X{erp:-erp} %5p'
level:
com.jd.jsf: WARN
com.jd.data.application.decision.mapper: debug

mybatis打印sql日志

3.全局异常捕获和异常码标准化定义

全局异常捕获就是多自定义RuntimeException的子类,把所有运行时异常情况都包括进行来,然后对应可能出现该异常的地方加上对该异常的catch,并总定义异常信息。总而言之,就是要保障透出给外界的异常信息一定不能是最原始的e.getMessage()的类似于printStackTrace()的一长串方法之类的detailMessage,最好是给出简短的一句话和修改建议等简洁明了的信息。就比如后端给前端透出的异常信息,数据服务给后端透出的异常信息。

比如希望透出的不是这么一长串用户看不懂的东西:

而是如下简短明了的信息:

1.自定义异常case

这种自定义异常case越多越好,能覆盖到的异常场景越多越好。

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
@Slf4j
public class CommonException extends RuntimeException {

/**
* 异常构造方法
* @param message
*/
public CommonException(String message) {
super(message);
}

/**
* 抛异常
*/
public static void throwCommonCommonException(String msg) {
log.info(msg);
throw new CommonException(msg);
}

/**
* 抛异常_不存在
*/
public static void throwNotFoundException(String entity, Long id) {
String msg = String.format("%s %s not found", entity, id);
throwCommonCommonException(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
@Slf4j
public class UserException extends RuntimeException {
/**
* 异常构造方法
*
* @param message
*/
public UserException(String message) {
super(message);
}

/**
* 抛异常
*/
public static void throwUserException(String msg) {
log.info(msg);
throw new UserException(msg);
}

/**
* 抛异常_不存在
*/
public static void throwNotFoundException(String entity, Long id) {
String msg = String.format("%s %s not found", entity, id);
throwUserException(msg);
}
}

2.异常码标准化定义case

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
public enum ResponseCode {
/**
* 成功
*/
SUCCESS(200, ""),
/**
* 指标传参异常
*/
INDICATORS_PARAM_ERROR(1001, "指标传参异常"),
/**
* 维度查询不到该指标
*/
INDICATORS_PARAM_ERROR_DIM_NOT_ENOUGH(1002, "维度查询不到该指标"),
/**
* 指标不存在
*/
INDICATORS_PARAM_ERROR_METRIC_NOT_EXIST(1003, "指标不存在"),
/**
* 维度传参异常
*/
DIM_PARAM_ERROR(2001, "维度传参异常"),
/**
* 维度信息不存在
*/
DIM_PARAM_NOT_FOUND(2002, "维度信息不存在"),
/**
* group by参数校验相关
*/
GROUP_PARAM_ERROR(3001, "聚合传参异常"),
/**
* 聚合维度不存在
*/
GROUP_PARAM_ERROR_WRONG_DIM(3002, "聚合信息中维度不存在"),

/**
* 加速策略参数异常
*/
SPEED_UP_STRATEGY_PARAM_ERROR(3003, "加速策略参数异常"),

DIMENSION_COMBINE_PARAM_ERROR(3004, "聚合维度组合传参异常"),
/**
* 系统异常,exception
*/
SYSTEM_ERROR(4001, "系统异常"),

/**
* 原子服务传参异常
*/
SERVICES_PARAM_ERROR(1003, "原子服务传参异常"),

DATA_IS_REFRESHING(1003, "原子服务传参异常");

/**
* 错误码
*/
private final int code;

/**
* 错误消息
*/
private final String message;

/**
* 构造方法
*/
ResponseCode(int code, String message) {
this.code = code;
this.message = message;
}

public int getCode() {
return code;
}

public String getMessage() {
return message;
}
}

4.通用响应和请求体

标准化的响应和请求体,包括request和response,包括header和body,包括返回码和返回描述枚举值,等等能标准化的都尽量标准化。

case如下:

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
@Data
public class DriveRequest<T> implements Serializable {

/**
* 设置请求的header
*/
private HeaderRequest header;

/**
* 设置请求体
*/
private T body;

}


@Data
public class DriveResponse<T> implements Serializable {

/**
* 设置返回的header
*/
private HeaderResponse header;

/**
* 设置返回body
*/
private T body;

}


@Data
@NoArgsConstructor
@AllArgsConstructor
public class HeaderRequest implements Serializable {

/**
* 应用授权key,必传
*/
private String appKey;
/**
* 可选
*/
private String uuid;
/**
* 服务名称
*/
private String serviceName;

/**
* 其他参数可选,例如pin(登录pin)、moduleId(模块id)等
*/
private Map<String, String> context;
}


@Data
@NoArgsConstructor
@AllArgsConstructor
public class HeaderResponse implements Serializable {

/**
* 返回code 200成功
*/
private int code;
/**
* 返回描述
*/
private String msg;
/**
* 日志唯一标识id
*/
private String traceId;
}