> For the complete documentation index, see [llms.txt](https://taocares.gitbook.io/development-guide/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://taocares.gitbook.io/development-guide/kai-fa-zhi-nan/yi-chang-chu-li.md).

# 异常处理

对于业务异常情况（数据找不到、找到子记录等），业务代码中只需封装异常并抛出，最后在请求处理层通过切面的方式进行统一的异常处理，返回统一的异常结构。

## 异常封装

后端返回的异常信息必需的元素是错误信息（message）。异常的错误编码（code）可以根据业务需要进行细化（一般来说使用HTTP status code即可满足要求），某些情况下，异常可能会携带发生异常的具体数据。

{% code title="ErrorResult.java" %}

```java
package com.example.demo;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class ErrorResult {
    private int code;
    private String message;
    private Object data;
}
```

{% endcode %}

{% hint style="info" %}
使用lombok提供的`@Builder`注解可以很方便的实现建造者模式。
{% endhint %}

## 异常定义

业务性质的异常，直接使用`BizException`即可。如果开发人员希望简化某些结构化异常的编码，可以继承`BizException`定义业务异常。

以找不到指定的数据记录为例：

{% code title="ResourceNotFoundException.java" %}

```java
package com.example.demo.exception;

public class ResourceNotFoundException extends BizException {
    // 记录业务主键
    private final Object pk;

    public ResourceNotFoundException(String entityName, Object pk) {
        super("找不到ID为" + pk + "的" + entityName);
        this.pk = pk;
    }
    
    public Object getPk() {
        return pk;
    }
}
```

{% endcode %}

* 使用自定义异常：`throw new ResourceNotFoundException("航班", 1);`
* 不使用自定义异常：`throw new BizException("找不到ID为1的航班");`

{% hint style="info" %}
业务异常的定义是灵活的，异常中的内容和构造方法可以根据需要定义。
{% endhint %}

## 异常抛出

业务代码中，根据需要抛出自定义的异常。

{% code title="FooController.java" %}

```java
package com.example.demo.controller;

import com.example.demo.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Optional;

@RestController
@RequestMapping("/foos")
@Api(tags = "Foo CRUD服务")
public class FooController {

    @Autowired
    private FooService fooService;

    @GetMapping("/{id}")
    @ApiOperation("根据ID查询结果")
    public FooDto findById(@PathVariable("id") Long id) {
        Optional<FooDto> optional = fooService.findById(id);
        if (optional.isPresent()) {
            FooDto foo = optional.get();
            if (foo.isDisabled()) {
                throw new BizException("Foo is disabled");
            }
            return foo;
        } else {
            throw new ResourceNotFoundException("Foo", id);
        }
    }
}
```

{% endcode %}

## 异常处理

{% hint style="warning" %}
此部分内容仅作为原理说明，实际开发需要使用统一异常处理的共用组件
{% endhint %}

开发者需要使用`@RestControllerAdvice`注解定义统一的异常处理类，针对需要处理的异常（可能是自定义业务异常，也有可能是JPA定义的`PersistenceException`），使用`@ExceptionHandler`注解进行全局的捕获和处理。

{% code title="GlobalExceptionHandler.java" %}

```java
package com.example.demo;

import com.example.demo.exception.ResourceNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@RestControllerAdvice
public class GlobalExceptionHandler {
    // 可以根据需要定义本系统的错误码
    public static final int RC_RESOURCE_NOT_FOUND = 40001;
    public static final int RC_UNKNOWN_ERROR = 50000;

    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(ResourceNotFoundException.class)
    public ErrorResult handleResourceNotFoundException(ResourceNotFoundException e) {
        return ErrorResult.builder()
                .code(RC_RESOURCE_NOT_FOUND) // 可选
                .message(e.getMessage())
                .data(e.getPk())
                .build();
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    public ErrorResult handleOtherException(Exception e) {
        return ErrorResult.builder()
                .code(RC_UNKNOWN_ERROR) // 可选
                .message(e.getMessage())
                .build();
    }
}

```

{% endcode %}

{% hint style="warning" %}
异常处理一定要指定`@ResponseStatus`，避免返回200（正常）的状态码。
{% endhint %}
