Java 异常处理

1. Java异常体系

Java异常体系设计的目的在于通过使用少量代码,实现大型、健壮、可靠程序。

1.1. 异常处理

异常处理是Java中唯一正式的错误报告机制。异常处理机制最大的好处就是降低错误代码处理的复杂程度。

如果不使用异常,那么就必须在调用点检查特定的错误,并在程序的很多地方去处理它;如果使用异常,那么就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误。因此只需要在一个地方处理错误,这种方式不仅节省代码,而且把“描述正确执行过程做什么事”和“出了问题怎么办”相分离。

异常处理的一个重要原则就是“只有在你知道如何处理的情况下才捕获异常”,实际上异常处理的一个重要目标就是把错误处理的代码与错误发生地点相分离。这使你能在一段代码中专注于要做的事,至于异常处理,则放在另一端代码中。这样,主干代码就不会与错误处理逻辑混在一起,更容易理解和维护。通过允许一个异常处理程序处理多个异常点,使得异常处理代码集中于一处。

“异常情形”,指阻止当前方法或作用域继续执行的问题。异常最重要的方面之一就是如果发生问题,他将不允许程序沿着正常路径继续执行,而是将控制权转交给异常处理程序,强制处理出现的问题,并恢复稳定状态。

与程序正常处理不同,当抛出异常时,有几件事会随之发生:

  • 首先,使用new关键字在堆上创建一个异常对象;
  • 然后,当前执行路径被终止,从当前环境中throw出异常对象引用;
  • 此时,异常处理机制接管程序,并寻找一个合适的异常处理程序来继续执行程序;
  • 如果,找到合适的异常处理程序,则使用该处理程序对异常进行恢复;如果未找到合适的异常处理程序,则将异常向调用链上级抛出;
  • 如果,到最顶层仍旧未找到合适的异常处理程序,则当前线程异常退出。

Java异常处理:

1
2
3
4
5
6
7
8
9
10
11
try {
// 正常处理流程,正确执行过程做什么事
Path path = Paths.get("var", "error");
List<String> lines = Files.readAllLines(path, Charset.defaultCharset());
System.out.println(lines);
} catch (IOException e) {
// 异常处理流程,出了问题怎么办
e.printStackTrace();
}finally {
// 清理资源
}

1.1.1. Exception

异常与其他对象一样,使用new关键字在堆上创建异常对象,也伴随着存储空间的分配和构造函数的调用。

标准异常会有几个构造函数:

  • 无参默认构造函数
  • 接受一个字符串的构造函数
  • 接收一个字符串和 Throwable 的构造函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Exception extends Throwable {
    static final long serialVersionUID = -3387516993124229948L;
    public Exception() {
    super();
    }
    public Exception(String message) {
    super(message);
    }
    public Exception(String message, Throwable cause) {
    super(message, cause);
    }
    public Exception(Throwable cause) {
    super(cause);
    }
    }
1.1.2. throw

将异常对象的引用传递给throw,从效果上看,它就像从方法中“返回”一样,可以将异常处理当做一种不同的返回机制。不同的是return返回到方法调用点,throw返回到异常处理程序。

1
2
3
4
private String throwException(){
//return "Test";
throw new RuntimeException("Test Exception");
}
1.1.3. try

“监控区域”是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。可以简单的理解为try块就是监控区域。

如果在方法内部抛出异常(或调用其他方法出现异常),这个方法将在抛出异常的点结束,如果不希望方法结束,那么需要在方法内设置一个特殊的块来捕获异常。

1
2
3
4
5
6
7
8
9
private String tryException(){
try {
// 监控区域
return throwException();
}catch (Exception e){
// 异常处理区域
}
return "";
}
1.1.4. catch

抛出的异常必须在某处得到处理,这个点就是异常处理程序。针对每个要捕获的异常,准备相应的处理程序。异常处理程序紧跟着try块,以关键字catch表示。

每个catch子句,看起来就像是接收一个且只接收一个特殊异常类型的方法。当异常发生后,异常处理机制会搜寻参数与异常类型匹配的第一个异常处理器,然后进入catch子句执行,此时认为异常得到了处理。一旦catch子句结束,则处理程序的查找过程结束。

在查找异常处理器时,并不要求抛出的异常与异常处理器所声明的异常完全匹配。派生类的对象也可以匹配基类的处理器。

1
2
3
4
5
6
7
8
9
10
11
12
private String tryException(){
try {
// 监控区域
return throwException();
}catch (RuntimeException e){
// 处理 RuntimeException 情况
}
catch (Exception e){
// 处理 Exception 情况
}
return "";
}

顺序,异常处理机制会搜索第一个匹配的异常处理器,因此catch语句的顺序至关重要,通常将具体类型前置,通用类型后置。

1.1.5. finally

对于一些代码,可能会希望无论try块中是否抛出异常,他们都会执行。为了达到效果,可以在异常处理后面加上finally子句。

对于没有垃圾回收和析构函数自动调用机制的语言来说,finally非常重要。它是程序员能够保证在任何情况下,内存总能得到释放。但在Java中存在垃圾回收机制,内存释放不再是个问题。当要把除内存外的资源恢复到他们的初始化状态时,就需要使用finally子句。常见的资源包括:网络链接、文件句柄、显示锁等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private String tryException(){
try {
// 监控区域
return throwException();
}catch (RuntimeException e){
// 处理 RuntimeException 情况
}
catch (Exception e){
// 处理 Exception 情况
}finally {
// 对 网络链接、文件句柄、锁等资源进行处理
}
return "";
}

1.2 方法异常说明

Java鼓励将方法可能会抛出的异常告知使用该方法的客户端。这种做法,使得调用者能知道在代码中可以获取所有的异常。

异常说明在方法声明中使用附加的关键字throws,后面接一个所有潜在异常类型列表,所以方法签名变成:

1
2
3
4
5
// 方法异常说明
private List<String> readFromFile(String filePath) throws IOException {
Path path = Paths.get(filePath);
return Files.readAllLines(path, Charset.defaultCharset());
}

代码必须和异常说明保存一致。如果方法里面的代码产生了异常却没有被处理,编译器会报错,要么处理这个异常,要么在异常说明列表中添加这个异常类型。

当然,可以在方法签名中声明异常,实际上并不抛出。这样可以为异常占个位置,以后可以抛出该异常而不用修改调用代码。

被检查异常,这种在编译阶段被强制检查的异常成为被检查异常。

备注: 被检查异常,可以通过反射机制获取异常列表。

1.3 异常的限制

当覆盖方法时,只能抛出在基类方法的异常列表中列出的异常,这意味着当基类使用的代码应用到其派生类对象的时候,程序一样能正常工作。

1.3.1. 方法重写

尽管在继承过程中,编译器会对异常说明做强制要求,但异常说明并不是方法类型的一部分,方法类型由方法名和参数类型组成。因此不能基于异常说明来重载方法。

对于方法重写时子类方法中的异常列表,要求要宽松得多。

  • 子类方法异常列表与父类完全一致
  • 子类方法异常列表是父类方法异常列表的子集
  • 子类方法没有抛出异常
  • 子类方法抛出父类方法异常的子异常

具体代码如下:

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
// 父类接口
public interface FileReader {
List<String> readFromFile(String filePath) throws IOException;
}

class FileReader1 implements FileReader{

// 子类方法异常列表与父类完全一致
@Override
public List<String> readFromFile(String filePath) throws IOException {
return null;
}
}

class FileReader2 implements FileReader{
// 子类方法抛出父类方法异常的子异常
@Override
public List<String> readFromFile(String filePath) throws FileNotFoundException {
return null;
}
}
class FileReader3 implements FileReader{

// 子类方法没有抛出异常
@Override
public List<String> readFromFile(String filePath){
return null;
}
}

1.3.2. 方法重载

Java 的方法重载,只涉及方法名和参数列表。方法返回值和异常列表都作为方法重载的依据。

1
2
3
4
5
6
7
8
9
10
public List<String> readFromFile(String path) throws IOException{
return null;
}

/**
编译不过
public List<String> readFromFile(String path) throws FileNotFoundException{
return null;
}
*/
1.3.3. 构造函数

异常限制对构造函数不起作用,子类构造函数能够抛出任意异常,而不必理会基类构造函数所抛出的异常。但,因为基类构造函数必须以某种形式被调用,派生类构造函数的异常说明必定包含基类构造函数的异常说明。

构造器会把对象设置为安全的初始化状态,如果有别的工作,比如打开一个文件,这样的动作只有在对象使用完毕并且用户调用了清理方法才能清理。如果在构造函数中抛出异常,这些清理动作就不能正常工作,因此在编写构造器时要格外注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Parent{
Parent() throws IOException{

}
}
class Child extends Parent{

Child() throws IOException {
super(); //此次抛出异常
}
/**
Child() throws IOException {
// super 必须是第一个语句,无法对异常进行捕获
try {
super(); //此次抛出异常
}catch (Exception e){

}

}
*/
}

1.4 受检查异常

在编译时被强制检查的异常称为”受检查的异常”。即在方法的声明中声明的异常。

受检查异常要求方法调用者必须对异常进行处理。从某种角度来说,受检查异常违反了 Java 异常处理的初衷。

1
2
3
4
private List<String> readFromFile(String filePath) throws IOException {
Path path = Paths.get(filePath);
return Files.readAllLines(path, Charset.defaultCharset());
}

在调用 readFromFile 方法时,无法忽略对 IOException 的处理。

一般情况下,面对受检查异常,我们通常这样处理:

  1. 修改自己的方法签名,添加新的异常声明;
  2. 使用 try catch 包裹异常调用(大多时候,我们不知道如何进行恢复);
  3. 将受检查异常转化为运行时异常;
1
2
3
4
5
6
7
8
9
private void printFile2(String filePath){
try {
List<String> lines = readFromFile(filePath);
lines.forEach(System.out::println);
}catch (IOException e){
// 使用异常链,将受检查异常转化为运行时异常
throw new RuntimeException(e);
}
}
1.4.1 Spring DAO Support

JDBC 接口中存在大量的受检查异常,在操作数据库时,会出现大量的try catch 样板代码,使核心逻辑埋葬在代码海中。

为此,Spring 对其进行优化,具体优化措施主要有:

  1. 在运行时异常基础上,建立了一整套异常体系(DataAccessException以及子类);
  2. 将 jdbc 中的受检查异常转化为运行时异常;
  3. 使用模板方法降低冗余代码。

jdbcTempalte 代码片段如下:

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
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");

Connection con = DataSourceUtils.getConnection(obtainDataSource());
Statement stmt = null;
try {
stmt = con.createStatement();
applyStatementSettings(stmt);
T result = action.doInStatement(stmt);
handleWarnings(stmt);
return result;
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
String sql = getSql(action);
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
// 完成受检查异常到运行时异常的转化
throw translateException("StatementCallback", sql, ex);
}
finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}

2. 自定义异常

不必局限于Java提供的异常类型。我们可以自定义异常类来表示程序中可能会遇到的特定问题。

要自定义异常类,必须从已有异常类继承,最好的方式是选择意思相近的异常类继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 业务异常
class BizException extends RuntimeException{
public BizException() {
super();
}

public BizException(String message) {
super(message);
}

public BizException(String message, Throwable cause) {
super(message, cause);
}

public BizException(Throwable cause) {
super(cause);
}
}

异常一般是用名称代表发生的问题,并且异常的名称应该可以望文知意。

2.1. 异常继承体系

异常本身也是类,存在一个完整的继承体系。

2.2. Throwable

Throwable被用来表示任何可以作为异常被抛出的类。

Throwable对象可以分为俩种类型(从Throwable继承而来):

  • Error 用于表示编译时和系统错误,出特殊情况外,开发人员不必关系
  • Exception 表示可以被抛出的基础类型。在Java类库、用户方法以及运行时故障都可能抛出Exception异常。这是开发人员最关系的异常。

throwable主要是对异常栈进行维护,核心方法如下:
方法 | 含义
—|—
printStackTrace | 打印调用栈信息,输出到标准错误输出(System.error)
printStackTrace(PrintStream) | 指定Stream打印调用栈信息
printStackTrace(PrintWriter) | 指定Print打印调用栈信息
getStackTrace() | 获取调用栈序列信息
fillInStackTrace() | 记录栈帧的当前状态

异常栈记录了”把你带到异常抛出点”的方法调用序列,是问题排查的主要信息之一。

1
2
3
4
5
6
7
8
9
10
11
public static void main(String... arg){
try {
// 正常处理流程,正确执行过程做什么事
Path path = Paths.get("var", "error");
List<String> lines = Files.readAllLines(path, Charset.defaultCharset());
System.out.println(lines);
} catch (IOException e) {
// 异常处理流程,出了问题怎么办
e.printStackTrace();
}
}

运行程序,获得结果,异常栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java.nio.file.NoSuchFileException: var/error
at sun.nio.fs.UnixException.translateToIOException(UnixException.java:86)
at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:102)
at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:107)
at sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:214)
at java.nio.file.Files.newByteChannel(Files.java:361)
at java.nio.file.Files.newByteChannel(Files.java:407)
at java.nio.file.spi.FileSystemProvider.newInputStream(FileSystemProvider.java:384)
at java.nio.file.Files.newInputStream(Files.java:152)
at java.nio.file.Files.newBufferedReader(Files.java:2781)
at java.nio.file.Files.readAllLines(Files.java:3199)
at com.geekhalo.exception.Demo.main(Demo.java:15)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

2.3. Exception

Exception是与编程有关的所有异常类的基类。

方法 含义
getMessage 获取详细信息
getLocaliedMessage 获取本地语言表示的详细信息

2.4. RuntimeException

从RuntimeException派生出来的异常成为“不受检查异常”。这种异常会自动被Java虚拟机抛出,所以不必在方法的异常说明中列出来。

1
2
3
private void throwRuntimeException(){
throw new RuntimeException();
}

RuntimeException 及其子类 无需在方法中进行声明。

3. 常见异常处理策略

完成自定义异常后,下一个关键点便是如何处理异常。

3.1. 异常恢复

异常处理程序的目的就是处理所发生的异常。因此,第一个异常处理策略便是,处理异常,进行异常恢复。

1
2
3
4
5
6
7
8
9
10
11
12
13
private void recoveryException(String filePath){
try {
List<String> lines = readFromFile(filePath);
lines.forEach(System.out::println);
}catch (IOException e){
// 打印日志,从异常中恢复程序
LOGGER.error("failed to read from file {}", filePath, e);
}
}
private List<String> readFromFile(String filePath) throws IOException {
Path path = Paths.get(filePath);
return Files.readAllLines(path, Charset.defaultCharset());
}

3.2. 重新抛出异常

当你无法得到足够信息,从而对异常进行恢复时。可以把刚刚捕获的异常重新抛出。在catch子句中已经获得了对当前异常对象的引用,可以直接将其抛出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void printFile(String filePath) throws IOException{
try {
List<String> lines = readFromFile(filePath);
lines.forEach(System.out::println);
}catch (IOException e){
// 重新抛出异常
throw e;
}
}

// 方法异常说明
private List<String> readFromFile(String filePath) throws IOException {
Path path = Paths.get(filePath);
return Files.readAllLines(path, Charset.defaultCharset());
}

重抛异常会把异常抛给上一级调用,同一try后的catch子句被忽略。如果只是把当前异常抛出,那么printStackTrace显示的是原来异常抛出点的调用链信息,而非重新抛出点的信息。如果想要更新调用信息,可以调用fillInStackTrace方法,返回另一个Throwable对象,它将当前调用栈信息填入原来的异常对象。

3.3. 异常链

如果想要在捕获一个异常后抛出另一个新异常,并希望把原始异常信息保留下来,这成为异常连。

Throwable的子类在构造器中都可以接受一个cause对象,用于表示原始异常,这样把原始异常传递给新异常,使得当前位置创建并抛出的新异常,通过异常链追踪到异常最初发生的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void printFile2(String filePath){
try {
List<String> lines = readFromFile(filePath);
lines.forEach(System.out::println);
}catch (IOException e){
// 异常链
throw new BizException(e);
}
}


// 方法异常说明
private List<String> readFromFile(String filePath) throws IOException {
Path path = Paths.get(filePath);
return Files.readAllLines(path, Charset.defaultCharset());
}

Throwable子类中,只有Error、Exception、RuntimeException在构造函数中提供了cause参数,如果要把其他异常链起来,可以使用initCause方法。

4. 异常实战

异常是框架设计不可遗漏的点。

框架中的异常处理,同样遵循固定的操作流程:

  1. 根据需求自定义异常;
  2. 提供异常处理器,统一对异常进行处理;

4.1. Spring MVC 统一异常处理

Spring MVC 是最常见的 Web 框架,上手简单,开发迅速。

遵循正常流程与异常处理分离策略。研发人员只需关心正常逻辑,由框架对异常流程进行统一处理。那应该怎么操作呢?

4.1.1. 定义业务异常

首先,需要定义自己的业务异常。

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 abstract class BusinessException extends RuntimeException{
/**
* 异常处理码
*/
private final int code;

/**
* 异常消息
*/
private final String msg;

private final String timestamp = String.valueOf(System.currentTimeMillis());

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

protected BusinessException(int code, String msg, Exception e) {
super(e);
this.code = code;
this.msg = msg;
}
}
4.1.2. 异常处理

可以使用 HandlerExceptionResolver 扩展,对异常进行定制。

RestHandlerExceptionResolverRest 请求的服务异常进行处理。将异常统一转化为 JSON 返回给用户。

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
@Component
public class RestHandlerExceptionResolver implements HandlerExceptionResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(RestHandlerExceptionResolver.class);

@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 是 Rest 请求 并且能够处理
if(isRestRequest(handler) && isAcceptException(ex)){
// 将异常传化为 RestResVo 对象
RestResVo<Void> restResVo = RestResVo.error((BusinessException)ex);
try {
// 以 Json 格式进行写回
response.getWriter().println(JSON.toJSONString(restResVo));
}catch (Exception e){
LOGGER.error("failed to write json {}", restResVo, e);
}
// empty ModelAndView说明已经处理
return new ModelAndView();
}
return null;
}

private boolean isRestRequest(Object handler) {
if (handler instanceof HandlerMethod){
HandlerMethod handlerMethod = (HandlerMethod) handler;
return AnnotationUtils.findAnnotation(handlerMethod.getMethod(), ResponseBody.class) !=null ||
AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), ResponseBody.class) != null;
}
return false;
}

private boolean isAcceptException(Exception ex) {
return ex instanceof BusinessException;
}
}

PageHandlerExceptionResolver 对页面请求的异常进行处理。将异常统一转发到 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
@Component
public class PageHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 是页面请求并且能够处理当前异常
if(isPageRequest(handler) && isAcceptException(ex)){
// 返回 error 视图
ModelAndView mv = new ModelAndView("error");
mv.addObject("error", ex);
return mv;
}
return null;
}


private boolean isPageRequest(Object handler) {
if (handler instanceof HandlerMethod){
HandlerMethod handlerMethod = (HandlerMethod) handler;
return AnnotationUtils.findAnnotation(handlerMethod.getMethod(), ResponseBody.class) == null
&& AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), ResponseBody.class) == null;
}
return true;
}

private boolean isAcceptException(Exception ex) {
return ex instanceof BusinessException;
}
}

4.2. Spring Cloud 异常穿透

在使用 Spring Cloud 进行微服务时,如果 Server 端发生异常,客户端会收到一个 5xx 错误,从而中断当前正常请求逻辑。但,异常中所含有的业务信息也一并丢失了,如何最大限度的保持异常信息呢?

4.2.1. 定义业务异常

首先,仍旧是定义自己的业务异常类。

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
@Data
public class CodeBasedException extends RuntimeException {
private Integer code;
private String msg;
private Object data;

public CodeBasedException(){
super();
}
public CodeBasedException(String msg) {
super(msg);
this.msg = msg;
}

public CodeBasedException(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}

public CodeBasedException(String message, Integer code, String msg, Object data) {
super(message);
this.code = code;
this.msg = msg;
this.data = data;
}


}
4.2.2. Server 端处理

在 Server 端,捕获业务异常,并将信息通过 Header 进行写回。

HandlerInterceptorBasedExceptionBinder 在业务处理完成后,捕获 CodeBasedException 异常,并将异常信息通过 Response 对象回写到 Header 中。

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
public class HandlerInterceptorBasedExceptionBinder implements HandlerInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(HandlerInterceptorBasedExceptionBinder.class);

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if (ex == null){
return;
}
if (ex instanceof CodeBasedException){
CodeBasedException codeBasedException = (CodeBasedException) ex;
response.addHeader(SoaConstants.HEADER_ERROR_CODE, String.valueOf(codeBasedException.getCode()));
response.addHeader(SoaConstants.HEADER_ERROR_MSG, encode(codeBasedException.getMsg()));

response.addHeader(SoaConstants.HEADER_ERROR_EXCEPTION_MSG, encode(codeBasedException.getMessage()));
return;

}

response.setHeader(SoaConstants.HEADER_ERROR_CODE, "500");
response.setHeader(SoaConstants.HEADER_ERROR_MSG, encode(ex.getMessage()));

response.setHeader(SoaConstants.HEADER_ERROR_EXCEPTION_MSG, encode(String.valueOf(ex.getStackTrace())));
LOGGER.error("failed to handle request.", ex);
}
}

如果是 Spring Boot 项目,我们需要完成 HandlerInterceptorBasedExceptionBinder 的注册。

1
2
3
4
5
6
7
@Configuration
public class SoaWebMvcConfigurer implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptorBasedExceptionBinder()).addPathPatterns("/**");
}
}
4.2.3. Client 端处理

客户端在获取请求结果后,从 Header 中提取异常信息,并重新组装并抛出异常。

FeignErrorDecoderBasedExceptionConverter 从 Header 中提取异常信息,并重新组装并抛出 SoaRemoteCallException

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
public class FeignErrorDecoderBasedExceptionConverter implements ErrorDecoder {
private static final Logger LOGGER = LoggerFactory.getLogger(FeignErrorDecoderBasedExceptionConverter.class);

public FeignErrorDecoderBasedExceptionConverter() {
}

@Override
public Exception decode(String methodKey, Response response) {
Map<String, Collection<String>> headers = response.headers();
report(methodKey, response);
return checkException(headers);
}

private void report(String methodKey, Response response) {
String message = format("status %s reading %s", response.status(), methodKey);
try {
if (response.body() != null) {
String body = Util.toString(response.body().asReader());
message += "; content:\n" + body;
}
} catch (IOException ignored) { // NOPMD
}
LOGGER.error("status {}, message {}", response.status(), message);
}

private Exception checkException(Map<String, Collection<String>> headers) {
String code = getValue(headers, SoaConstants.HEADER_ERROR_CODE);

String msg = HeaderValueUtils.decode(getValue(headers, SoaConstants.HEADER_ERROR_MSG));
String exceptionMsg = HeaderValueUtils.decode(getValue(headers, SoaConstants.HEADER_ERROR_EXCEPTION_MSG));


Integer errorCode = NumberUtils.isNumber(code) ? Integer.valueOf(code) : -1;
return new SoaRemoteCallException(exceptionMsg, errorCode, msg, "");
}

private String getValue(Map<String, Collection<String>> headers, String key) {
Collection<String> values = headers.get(key);
if (values != null && values.size() == 1){
return values.iterator().next();
}
LOGGER.debug("failed to find value of {} in header {}", key, headers);
return null;
}
}

最后,需要完成 FeignErrorDecoderBasedExceptionConverter 的注册。

1
2
3
4
@Bean
public FeignErrorDecoderBasedExceptionConverter exceptionCheckFeignDecoder(){
return new FeignErrorDecoderBasedExceptionConverter();
}

5. 小节

  1. Java 异常的本质就是,将正常处理逻辑和异常处理逻辑进行分离;
  2. 异常使用方面,通过需要两步操作:
    • 自定义异常
    • 自定义异常处理器
  3. 不管是日常开发,还是框架扩展,Java 异常机制都能出色的完成分离任务。
wenxinzizhu wechat
扫一扫,添加我的微信,一起交流共同成长(备注为技术学习)