环境信息: Java 8Spring Boot 2.xSpring Cloud 2021.x

引言

微服务中的异常传递

在使用Feign进行远程调用、开启了熔断降级且存在全局异常处理时,异常的传递就变得尤为重要。

在微服务架构中,服务之间的调用是通过网络进行的,因此在调用过程中可能会出现各种异常,如网络超时、服务不可用等。

为了保证服务的稳定性,我们需要对这些异常进行处理,以避免异常在服务之间传递,导致整个系统的不稳定。

调用的几种情况

假设此时A服务正在调用B服务,通常会涉及以下几种情况:

  1. B服务成功返回
  2. B服务抛出异常,被全局异常处理器拦截后正常返回,因为全局异常拦截会将异常封装为统一结果对象返回,此时是不会走降级逻辑的
  3. B服务抛出异常,被全局异常处理器拦截后,继续抛出异常,此时会走降级逻辑并返回结果

详细分析

基础准备

  • 创建一个SpringCloud项目,并引入OpenFeign、Sentinel或Hystrix依赖,以实现服务间的调用和降级。

  • 自定义返回状态码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 返回码
*
* @author XieYT
*/
@SuppressWarnings("unused")
@Getter
@AllArgsConstructor
public enum ReturnCode {

/**
* 成功
*/
RC200(200, "成功"),

/**
* 失败
*/
RC500(500, "网络异常");
}
  • 创建统一返回结果类,用于对返回结果进行封装,这里使用R
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
/**
* 统一结果返回类
*
* @author XieYT
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@SuppressWarnings("unused")
public class R<T> {

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

/**
* 状态码
*/
private Integer code;

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

/**
* 时间戳
*/
private long timestamp;

public static <T> R<T> success() {
return R.<T>builder()
.code(ReturnCode.RC200.getCode())
.message(ReturnCode.RC200.getMessage())
.timestamp(System.currentTimeMillis())
.build();
}

public static <T> R<T> success(T data) {
return R.<T>builder()
.code(ReturnCode.RC200.getCode())
.message(ReturnCode.RC200.getMessage())
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}

/**
* 自定义消息的成功返回
*/
public static <T> R<T> success(T data, String message) {
return R.<T>builder()
.code(ReturnCode.RC200.getCode())
.message(message)
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}

/**
* 失败
*/
public static <T> R<T> fail(Integer code, String message) {
return R.<T>builder()
.code(code)
.message(message)
.timestamp(System.currentTimeMillis())
.build();
}

/**
* 失败
*/
public static <T> R<T> fail(ReturnCode returnCode) {
return R.<T>builder()
.code(returnCode.getCode())
.message(returnCode.getMessage())
.timestamp(System.currentTimeMillis())
.build();
}

/**
* 失败
*/
public static <T> R<T> fail(ReturnCode returnCode, String message) {
return R.<T>builder()
.code(returnCode.getCode())
.message(message)
.timestamp(System.currentTimeMillis())
.build();
}


}
  • 自定义业务异常类,用于抛出业务异常
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
/**
* 基础异常
*
* @author XieYT
*/
@SuppressWarnings("unused")
@Data
@EqualsAndHashCode(callSuper = true)
public class ServiceException extends RuntimeException {

private Integer code;

private String message;

public ServiceException(ReturnCode codeEnum) {
super(codeEnum.getMessage());
this.code = codeEnum.getCode();
this.message = codeEnum.getMessage();
}

public ServiceException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
}
  • 定义全局异常处理器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 全局异常处理器
*
* @author XieYT
*/
@Slf4j
@RestControllerAdvice()
public class GlobalExceptionHandler {

/**
* 业务异常
*/
@ExceptionHandler(ServiceException.class)
public R<String> serviceExceptionHandler(ServiceException e) {
log.error("业务异常: {} -> {}", Arrays.stream(e.getStackTrace()).findFirst().orElse(null), e.getMessage());
return R.fail(e.getCode(), e.getMessage());
}
}

情况一

B服务成功返回

无须特殊处理,正常返回即可。

情况二

B服务抛出异常,被全局异常处理器拦截后正常返回

当B服务抛出异常时,全局异常处理器会拦截异常,并将异常封装为统一结果对象返回,此时是不会走降级逻辑的。

此时我们可以直接通过R类中的状态码来判断是否成功返回,如下:

1
2
3
4
5
6
7
8
9
R<String> result = service.method();
boolean isSuccess = ReturnCode.RC200.getCode().equals(result.getCode());

if (isSuccess) {
// do something
} else {
// 如果想要获取异常信息,可以通过result.getMessage()获取,从而抛出
throw new ServiceException(result.getCode(), result.getMessage());
}

在上述代码中,我们先判断了是否成功返回,如果成功则继续执行,否则抛出异常。

抛出异常的操作可能会在多个方法中用到,我们不妨将其封装到R类中,以便于调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class R<T> {

// 省略其他代码...

/**
* 成功则返回,否则抛出异常
*/
@JsonIgnore
public T getCheckData() {
checkError();
return data;
}

private void checkError() {
if (isSuccess()) {
return;
}
throw new ServiceException(code, message);
}

private boolean isSuccess() {
return ReturnCode.RC200.getCode().equals(code);
}
}

在加入上述代码后,我们就可以直接通过getCheckData()方法来隐藏异常处理逻辑,使代码更加简洁。

1
2
3
4
5
public void getDataFromOtherService() {
R<String> result = service.method();
// 如果成功则返回,否则抛出异常并被全局异常处理器拦截
String data = result.getCheckData();
}

情况三

B服务抛出异常,被全局异常处理器拦截后,继续抛出异常

在情况二中,如果异常被全局异常处理器拦截,那么异常会被封装为统一结果对象返回,此时是不会走降级逻辑的。

所以我们可以定义一个异常来单独处理这种情况,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Feign异常
* <p>
* 该异常用于处理Feign调用异常
* <p>
* 全局异常处理其中不会对该异常进行处理,而是直接抛出
*
* @author XieYT
*/
@SuppressWarnings("unused")
@Data
@EqualsAndHashCode(callSuper = true)
public class FeignClientException extends RuntimeException {

private String message;

public FeignClientException(String message) {
super(message);
this.message = message;
}
}

并且在全局异常处理器中对该异常进行处理,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class GlobalExceptionHandler {

// 省略其他代码...

/**
* Feign异常处理
*/
@ExceptionHandler(value = FeignClientException.class)
public void defaultExceptionHandler(FeignClientException e) {
// 在这里直接将异常抛出,以便于走降级逻辑
throw e;
}
}

此时我们在B服务中可以通过如下方式抛出异常:

1
2
3
4
5
6
public Service {
public String method() {
// 省略其他代码...
throw new FeignClientException("Feign调用异常, 该异常会被继续抛出!");
}
}

此时异常会被全局异常处理器拦截,然后直接抛出,从而走降级逻辑。

总结

在本文中,我们探讨了在使用Spring Cloud中的OpenFeign进行远程服务调用时如何处理异常,特别是当这些异常被全局异常处理器捕获时的情况。

通过实际代码示例,我们演示了如何在微服务架构中适当地传递和封装异常,以保持系统的稳定性和响应性。