springboot/web項目優秀的後端接口體系,看一篇就夠了

springboot/web項目優秀的後端接口體系,看一篇就夠了

項目構建-統一參數校驗,統一結果響應,統一異常處理,統一錯誤處理,統一日誌記錄,統一生成api文檔

1. 前言

一個後端接口大致分爲四個部分組成:接口地址(url)、接口請求方式(get、post等)、請求數據(request)、響應數據(response)。
本文主要演示如何構建起一個優秀的後端接口體系,體系構建好了自然就有了規範,同時再構建新的後端接口也會十分輕鬆。

2. 所需依賴包

這裏用的是SpringBoot配置項目,本文講解的重點是後端接口,所以只需要導入一個spring-boot-starter-web包就可以了:

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.rudecrab</groupId>
    <artifactId>validation-and-exception-handler</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>validation-and-exception-handler</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--web依賴包,web應用必備-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--(新版本)swagger增強工具依賴包,方便生成接口文檔。非必須導入-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>2.0.1</version>
        </dependency>
        <!--lombok依賴包,簡化類。非必須導入-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

本文還用了swagger來生成API文檔,lombok來簡化類,logback來生成日誌。都不是必須的,可用可不用。

3.統一參數校驗

一個接口一般對參數(請求數據)都會進行安全校驗,參數校驗的重要性自然不必多說,那麼如何對參數進行校驗就有講究了。

業務層校驗
首先我們來看一下最常見的做法,就是在業務層進行參數校驗:

public String addUser(User user) {
     if (user == null || user.getId() == null || user.getAccount() == null || user.getPassword() == null || user.getEmail() == null) {
         return "對象或者對象字段不能爲空";
     }
     if (StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) {
         return "不能輸入空字符串";
     }
     if (user.getAccount().length() < 6 || user.getAccount().length() > 11) {
         return "賬號長度必須是6-11個字符";
     }
     if (user.getPassword().length() < 6 || user.getPassword().length() > 16) {
         return "密碼長度必須是6-16個字符";
     }
     if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
         return "郵箱格式不正確";
     }
     // 參數校驗完畢後這裏就寫上業務邏輯
     return "success";
 }

這還沒有進行業務操作呢光是一個參數校驗就已經這麼多行代碼,實在不夠優雅。

使用Spring Validator 和 Hibernate Validator,這兩套Validator來進行方便的參數校驗!
(這兩套Validator依賴包已經包含在前面所說的web依賴包裏了,所以可以直接使用。)

3.1 Validator + BindResult進行校驗

Validator可以非常方便的制定校驗規則,並自動幫你完成校驗。首先在入參裏需要校驗的字段加上註解,每個註解對應不同的校驗規則,並可制定校驗失敗後的信息:

@Data
public class User {
    @NotNull(message = "用戶id不能爲空")
    private Long id;

    @NotNull(message = "用戶賬號不能爲空")
    @Size(min = 6, max = 11, message = "賬號長度必須是6-11個字符")
    private String account;

    @NotNull(message = "用戶密碼不能爲空")
    @Size(min = 6, max = 11, message = "密碼長度必須是6-16個字符")
    private String password;

    @NotNull(message = "用戶郵箱不能爲空")
    @Email(message = "郵箱格式不正確")
    private String email;
}

校驗規則和錯誤提示信息配置完畢後,接下來只需要在接口需要校驗的參數上加上@Valid註解,並添加BindResult參數 即可方便完成驗證:

@RestController
@RequestMapping("user")
public class UserController {
    @Autowired
    private UserService userService;
    
    @PostMapping("/addUser")
    public String addUser(@RequestBody @Valid User user, BindingResult bindingResult) {
        // 如果有參數校驗失敗,會將錯誤信息封裝成對象組裝在BindingResult裏
        for (ObjectError error : bindingResult.getAllErrors()) {
            return error.getDefaultMessage();
        }
        return userService.addUser(user);
    }
}

這樣當請求數據傳遞到接口的時候Validator就自動完成校驗了,校驗的結果就會封裝到BindingResult中去,如果有錯誤信息我們就直接返回給前端,業務邏輯代碼也根本沒有執行下去。
此時,傳統的那種業務層裏的校驗代碼就已經不需要了:

public String addUser(User user) {
     // 直接編寫業務邏輯
     return "success";
 }

現在可以看一下參數校驗效果。我們故意給這個接口傳遞一個不符合校驗規則的參數,先傳遞一個錯誤數據給接口,故意將password這個字段不滿足校驗條件:

{
	"account": "12345678",
	"email": "[email protected]",
	"id": 0,
	"password": "123"
}

再來看一下接口的響應數據:

在這裏插入圖片描述

使用Validator+ BindingResult已經是非常方便實用的參數校驗方式了,在實際開發中也有很多項目就是這麼做的,不過這樣還是不太方便,因爲你每寫一個接口都要添加一個BindingResult參數,然後再提取錯誤信息返回給前端。這樣有點麻煩,並且重複代碼很多(儘管可以將這個重複代碼封裝成方法)。

我們能否去掉BindingResult這一步呢?當然是可以的!

3.2 Validator + 自動拋出異常

我們完全可以將BindingResult這一步給去掉:

@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user) {
    return userService.addUser(user);
}

去掉之後會發生什麼事情呢?直接來試驗一下,還是按照之前一樣故意傳遞一個不符合校驗規則的參數給接口。

此時我們觀察控制檯可以發現接口引發MethodArgumentNotValidException異常:在這裏插入圖片描述

異常是引發了,可我們並沒有編寫返回錯誤信息的代碼呀,那參數校驗失敗了會響應什麼數據給前端呢? 我們來看一下剛纔異常發生後接口響應的數據:
在這裏插入圖片描述

沒錯,是直接將整個錯誤對象相關信息都響應給前端了!這樣就很難受,不過解決這個問題也很簡單,就是我們接下來要講的全局異常處理!

4. 統一異常處理(全局異常處理)

(全局異常處理也叫統一異常處理)
參數校驗失敗會自動引發異常,又不想手動捕捉這個異常,又要對這個異常進行處理,那正好使用SpringBoot全局異常處理來達到一勞永逸的效果!

4.1 @ControllerAdvice註解

該註解爲spirngboot中統一異常處理的核心。

是一種作用於控制層的切面通知(Advice),該註解能夠將通用的@ExceptionHandler、@InitBinder和@ModelAttributes方法收集到一個類型,並應用到所有控制器上。

該類中的設計思路:

  1. 使用@ExceptionHandler註解捕獲指定或自定義的異常;
  2. 使用@ControllerAdvice集成@ExceptionHandler的方法到一個類中;
  3. 必須定義一個通用的異常捕獲方法,便於捕獲未定義的異常信息;
  4. 自定一個異常類,捕獲針對項目或業務的異常;
  5. 異常的對象信息補充到統一結果枚舉中;

4.2 全局異常處理類

  1. 首先,需要新建一個類,在這個類上加上 @ControllerAdvice或@RestControllerAdvice註解 , 這個類就配置成全局處理類了。(根據你的Controller層用的是 @Controller還是@RestController 來決定選哪個註解) 。
  2. 然後在類中新建方法,在方法上加上 @ExceptionHandler註解 並指定你想處理的異常類型,接着在方法內編寫對該異常的操作邏輯,就完成了對該異常的全局處理!

我們現在就來演示一下對參數校驗失敗拋出的 MethodArgumentNotValidException 全局處理。

ExceptionControllerAdvice.java:

@RestControllerAdvice
public class ExceptionControllerAdvice {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
    	// 從異常對象中拿到ObjectError對象
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        // 然後提取錯誤提示信息進行返回
        return objectError.getDefaultMessage();
    }
}

我們再來看下這次校驗失敗後的響應數據:
在這裏插入圖片描述

沒錯,這次返回的就是我們制定的錯誤提示信息!

以後我們再想寫接口參數校驗,就只需要在傳參的成員變量上加上Validator校驗規則的註解(比如 @NotNull(message = “用戶id不能爲空”)
,然後在參數上加上@Valid註解即可完成校驗,校驗失敗會自動返回錯誤提示信息,無需任何其他代碼!

4.3 自定義全局異常類

在很多情況下,有一些異常我們需要手動拋出,比如在業務層中有些條件並不符合業務邏輯,這時候就可以手動拋出異常從而觸發事務回滾。
那手動拋出異常最簡單的方式就是throw new RuntimeException(“異常信息”)了,不過使用自定義異常 規範會更好一些。

我們現在就來開始寫一個自定義異常:繼承 RuntimeException。

APIException.java:

@Getter //只要getter方法,無需setter
public class APIException extends RuntimeException {
    private int code;
    private String msg;

    public APIException() {
        this(1001, "接口錯誤");
    }
    public APIException(String msg) {
        this(1001, msg);
    }
    public APIException(int code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }
}

在剛纔的全局異常處理類ExceptionControllerAdvice.java 中添加對我們自定義異常的聲明(類似於註冊原理):

@ExceptionHandler(APIException.class)
public String APIExceptionHandler(APIException e) {
    return e.getMsg();
}

這樣就無論發生什麼異常我們都能屏蔽掉,然後響應數據給前端。

5. 統一結果響應

現在我們規範好了參數校驗方式和全局異常,自定義異常的處理方式,然而還沒有規範響應數據!

比如我要獲取一個分頁信息數據,獲取成功了呢,自然就返回的數據列表,獲取失敗了後臺就會響應異常信息,即一個字符串,就是說前端開發者壓根就不知道後端響應過來的數據會是啥樣的!所以,統一響應數據是前後端規範中必須要做的!

5.1 自定義統一響應體

在目前的前後端分離架構下,後端主要是一個RESTful API的數據接口。但是HTTP的狀態碼數量有限,而隨着業務的增長,HTTP狀態碼無法很好地表示業務中遇到的異常情況。那麼可以通過修改響應返回的JSON數據,讓其帶上一些固有的字段
其中關鍵屬性的用途如下:

  • code爲返回結果的狀態碼
  • msg爲返回結果的消息
  • data爲返回的業務數據

這3個屬性爲固有屬性,每次響應結果都會有帶有它們。

統一數據響應第一步肯定要做的就是我們自己自定義一個響應體類,無論後臺是運行正常還是發生異常,響應給前端的數據格式是不變的!

自定義一個統一的響應體類,ResultVO.java:

@Getter
public class ResultVO<T> {
    /**
     * 狀態碼,比如1000代表響應成功
     */
    private int code;
    /**
     * 響應信息,用來說明響應情況
     */
    private String msg;
    /**
     * 響應的具體數據
     */
    private T data;

    public ResultVO(T data) {
        this(1000, "success", data);
    }

    public ResultVO(int code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

然後我們修改一下全局異常處理那的return的返回值:

@ExceptionHandler(APIException.class)
public ResultVO<String> APIExceptionHandler(APIException e) {
    // 注意哦,這裏返回類型是自定義響應體
    return new ResultVO<>(e.getCode(), "響應失敗", e.getMsg());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
    ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
    // 注意哦,這裏返回類型是自定義響應體
    return new ResultVO<>(1001, "參數校驗失敗", objectError.getDefaultMessage());
}

我們再來看一下此時如果發生異常了會響應什麼數據給前端:
在這裏插入圖片描述
(凡是你寫了一個自定義的異常,就別忘了到接口那修改返回類型。)

OK,這個異常信息響應就非常好了,狀態碼和響應說明還有錯誤提示數據都返給了前端,並且是所有異常都會返回相同的格式!異常這裏搞定了,別忘了我們到接口那也要修改返回類型。

5.2 控制層返回測試

新增一個接口好來看看效果,視圖層使用統一結果。
在controller中新增方法:

@GetMapping("/getUser")
public ResultVO<User> getUser() {
    User user = new User();
    user.setId(1L);
    user.setAccount("12345678");
    user.setPassword("12345678");
    user.setEmail("[email protected]");
    
    return new ResultVO<>(user);
}

看一下如果響應正確返回的是什麼效果:
在這裏插入圖片描述

這樣無論是正確響應還是發生異常,響應數據的格式都是統一的,十分規範!

數據格式是規範了,不過響應碼code和響應信息msg還沒有規範呀!

自定義註解版:
基於Spring AOP的統一響應體的實現(註解版)

6. 響應碼枚舉

要規範響應體中的響應碼和響應信息用枚舉簡直再恰當不過了,我們現在就來創建一個響應碼枚舉類:

ResultCode.java:

@Getter
public enum ResultCode {

    SUCCESS(1000, "操作成功"),
    FAILED(1001, "響應失敗"),
    VALIDATE_FAILED(1002, "參數校驗失敗"),
    ERROR(5000, "未知錯誤");

    private int code;
    private String msg;

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

然後修改響應體的構造方法,讓其只准接受響應碼枚舉來設置響應碼和響應信息:

在ResultVO.java類中:

   // 返回具體數據
    public ResultVO(T data) {
        this(ResultCode.SUCCESS, data);
    }

    // 返回對應的枚舉響應碼和響應信息,以及具體的數據。
    public ResultVO(ResultCode resultCode, T data) {
        this.code = resultCode.getCode();
        this.msg = resultCode.getMsg();
        this.data = data;
    }

    // 返回對應的枚舉響應碼和響應信息。
    public ResultVO(ResultCode resultCode) {
        this.code = resultCode.getCode();
        this.msg = resultCode.getMsg();
    }

然後同時修改全局異常處理的響應碼設置方式:

@ExceptionHandler(APIException.class)
public ResultVO<String> APIExceptionHandler(APIException e) {
    // 注意哦,這裏傳遞的響應碼枚舉
    return new ResultVO<>(ResultCode.FAILED, e.getMsg());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
    ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
    // 注意哦,這裏傳遞的響應碼枚舉
    return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());
}

這樣響應碼和響應信息只能是枚舉規定的那幾個,就真正做到了響應數據格式、響應碼和響應信息規範化、統一化!

7. 全局處理響應數據

接口返回統一響應體 + 異常也返回統一響應體,其實這樣已經很好了,但還是有可以優化的地方。
要知道一個項目下來定義的接口搞個幾百個太正常不過了,要是每一個接口返回數據時都要用響應體來包裝一下(return new ResultVO<>)好像有點麻煩,有沒有辦法省去這個包裝過程呢?當然是有滴,還是要用到全局處理。

7.1 響應增強類

Conrtoller增強的統一響應體處理類。

首先,先創建一個類加上註解使其成爲全局處理類。然後繼承ResponseBodyAdvice接口重寫其中的方法,即可對我們的controller進行增強操作,具體看代碼和註釋。

新建ResponseControllerAdvice.java類:

package com.rudecrab.demo.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rudecrab.demo.enums.ResultCode;
import com.rudecrab.demo.exception.APIException;
import com.rudecrab.demo.vo.ResultVO;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.Map;

/**
 * @description 全局處理響應數據--響應增強類
 * 接口返回統一響應體 + 異常也返回統一響應體,
 * 其實這樣已經很好了,但還是有可以優化的地方。
 *
 * 先創建一個類加上註解使其成爲全局處理類。
 * 然後繼承ResponseBodyAdvice接口重寫其中的方法,
 * 即可對我們的controller進行增強操作
 */

// 可以改爲@ControllerAdvice註解來攔截所有Controller的處理結果
@RestControllerAdvice(basePackages = {"com.rudecrab.demo.controller"}) // 注意哦,這裏要加上需要掃描的包
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {

    // 1.關於哪些請求要執行beforeBodyWrite,返回true執行,返回false不執行
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
        // 如果接口返回的類型本身就是ResultVO那就沒有必要進行額外的操作,返回false
 		System.out.println("=================="+!returnType.getParameterType().equals(ResultVO.class));
        return !returnType.getParameterType().equals(ResultVO.class);
    }

    // 2.如果接口返回的類型本身不是ResultVO,那就將原本的數據包裝在ResultVO裏再返回
    @Override
    public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
        // String類型不能直接包裝,所以要進行些特別的處理
        if (returnType.getParameterType().equals(String.class)) {
            System.out.println(data instanceof String);
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                System.out.println("===========進入了包裝數據======");
                // 將String轉換,再將數據包裝在ResultVO裏,再轉換爲json字符串響應給前端
                return objectMapper.writeValueAsString(new ResultVO<>(data));
            } catch (JsonProcessingException e) {
                throw new APIException("返回String類型錯誤");
            }
        }
        // 將原本的數據包裝在ResultVO裏
        return new ResultVO<>(data);
    }
}

注意:重寫的這兩個方法是用來在controller將數據進行返回前進行增強操作,supports方法要返回爲true纔會執行beforeBodyWrite方法,所以如果有些情況不需要進行增強操作可以在supports方法裏進行判斷。對返回數據進行真正的操作還是在beforeBodyWrite方法中,我們可以直接在該方法裏包裝數據,這樣就不需要每個接口都進行數據包裝了,省去了很多麻煩。

我們可以現在去掉接口的數據包裝來看下效果:

@GetMapping("/getUser")
public User getUser() {
    User user = new User();
    user.setId(1L);
    user.setAccount("12345678");
    user.setPassword("12345678");
    user.setEmail("[email protected]");
    // 注意哦,這裏是直接返回的User類型,並沒有用ResultVO進行包裝
    return user;
}
@ApiOperation("獲得所有用戶")
@GetMapping("/getAllUser")
public Map<String, List<User>> getAllUser() {

   User user1 = new User();
   user1.setId(1L);
   user1.setAccount("12345678");
   user1.setPassword("12345678");
   user1.setEmail("[email protected]");

   User user2 = new User();
   user2.setId(2L);
   user2.setAccount("9877986");
   user2.setPassword("adasdasd");
   user2.setEmail("[email protected]");

   List<User> list = new ArrayList<>();
   list.add(user1);
   list.add(user2);

   Map<String, List<User>> map = new HashMap<>();
   map.put("items", list);

   Set<String> keys = map.keySet(); //獲取所有的key值
   for(String key: keys){
       System.out.println(key);
   }
   //用ResultVO進行包裝了
   //return new ResultVO<>(user);
   // 這裏是直接返回的map,沒用ResultVO進行包裝,beforeBodyWrite會自動增強操作,給我們包裝起來。
   return map;
}

然後我們來看下響應數據:
在這裏插入圖片描述
在這裏插入圖片描述
成功對數據進行了包裝!

getUser功能流程:
前端發出接口請求》》controller處理請求》》同時ResponseControllerAdvice中添加了掃描控制層的全局異常處理類》》beforeBodyWrite規定了返回統一的結果體》》結果體ResultVO中定義了其內容包含有code,msg,data》》所以controller中處理的結果被增強操作,封裝成了ResultVO中的格式》》由此達到了統一返回。

注意:上面方法裏,beforeBodyWrite方法裏包裝數據無法對String類型的數據直接進行強轉,所以要進行特殊處理。(注:只能捕獲到Controller類裏方法返回的結果)

特殊處理原因:
對於String類型springboot中默認會用org.springframework.http.converter.StringHttpMessageConverter處理,所以對String類型的數據是特殊情況,需特殊處理:

對於一個Controller中的方法來說,其返回值Data會被哪種MessageConverter處理取決於Data的類型:

對於@RestController下的方法來說,通常(除了String)的類型其返回的content-type=application/json,beforeBodyWrite返回值data將被converterType=MappingJackson2HttpMessageConverter處理

當Data爲String類型時是特例,其content-type=text/plain,
converterType=StringHttpMessageConverter

可見,Data是String時converterType爲StringHttpMessageConverter,若直接在beforeBodyWrite裏將其包裝爲ResultVO類型的data,則會報錯。

解決:如示例代碼所示,根據converterType確定是否將data轉爲String類型。
在這裏插入圖片描述

8. 統一錯誤處理

SpringBoot 根據 HTTP 的請求頭信息進行了不同的響應處理。

如果我們想,Web端發的請求則跳轉到404,500或error等自定義頁面,非Web端發的請求則返回統一JSON的結果。

8.1 自定義錯誤頁面:

瀏覽器訪問一個不存在的頁面時,springboot會默認返回一個錯誤頁面。
在這裏插入圖片描述
如果我們需要自定義錯誤頁面,就只需要在模版文件夾Template下的 error 文件夾下放4xx 或 5xx.html的網頁即可。

比如:

<!doctype html> 
<html lang="en" xmlns:th="http://www.thymeleaf.org"> 
<head> 
<meta charset="UTF-8"> 
<meta name="viewport" content="width=device-width, initial-scale=1.0"> 
<title>[[${status}]]</title> 
<!-- Bootstrap core CSS --> 
<link href="/webjars/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet"> </head> 
	<body> 
		<div class="m-5" > 
		<p>錯誤碼:[[${status}]]</p> 
		<p >信息:[[${message}]]</p> 
		<p >時間:[[${#dates.format(timestamp,'yyyy-MM-dd hh:mm:ss ')}]]</p> 
		<p >請求路徑:[[${path}]]</p> 
		</div> 
	</body> 
</html>

隨意訪問不存在路徑得到:

在這裏插入圖片描述

8.2 自定義錯誤JSON返回:

如果是其他客戶端請求,如接口測試工具postman,會默認返回JSON數據。
默認是這樣:
在這裏插入圖片描述

根據SpringBoot 錯誤處理原理分析,得知最終返回的 JSON 信息是從一個 map 對象中轉換出來的,那麼只要能自定義 map 中的值,就可以自定義錯誤信息的 json 格式了。

在這裏插入圖片描述

自定義後,輸入錯誤的地址響應是這樣的:
在這裏插入圖片描述

項目總體結構:

在這裏插入圖片描述

9. 統一日誌記錄

日誌的框架比較豐富,由於spring boot對logback的集成,因此推薦使用logback在項目中使用。

9.1 logback-spring.xml配置:

<?xml version="1.0" encoding="UTF-8" ?>
<configuration debug="false" scan="true">
    <!-- 日誌級別,參考地址https://cloud.tencent.com/developer/article/1397693 -->
    <springProperty scope="context" name="LOG_ROOT_LEVEL" source="logging.level.root" defaultValue="DEBUG"/>
    <!--  標識這個"STDOUT" 將會添加到這個logger, 使用springProperty纔可使用application.yml中的值,也可以設置默認值 -->
    <springProperty scope="context" name="STDOUT" source="logging.stdout" defaultValue="STDOUT"/>
    <springProperty scope="context" name="logPath" source="logging.path" defaultValue="logs"/>

    <!-- 日誌格式,%d:日期;%thread:線程名;%-5level:日誌級別從左顯示5個字符長度,列如:DEBUG;
        %logger{36}:java類名,例如:com.muses.taoshop.MyTest,36表示字符長度;%msg:日誌內容;%d:換行 -->
    <property name="LOG_PATTERN"
              value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />
    <!-- root日誌級別-->
    <property name="${LOG_ROOT_LEVEL}" value="DEBUG" />
    <!-- 日誌跟目錄-對應了springProperty中對應的name,也是yml中對應設置的value -->
    <property name="LOG_HOME" value="${logPath}" />
    <!-- 日誌文件路徑-->
    <property name="LOG_DIR" value="${LOG_HOME}/%d{yyyyMMdd}" />
    <!-- 日誌文件名稱 -->
    <property name="LOG_PREFIX" value="portal" />
    <!-- 日誌文件編碼 -->
    <property name="LOG_CHARSET" value="utf-8" />
    <!-- 配置日誌的滾動時間,保存時間爲15天-->
    <property name="MAX_HISTORY" value="15" />
    <!-- 文件大小,默認爲10MB-->
    <property name="MAX_FILE_SIZE" value="10" />

    <!-- 1.打印到控制檯 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 格式化日誌內容-->
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <!-- 2.打印所有日誌,保存到文件-->
    <appender name="FILE_ALL"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--日誌文件名-->
        <file>${LOG_HOME}/all_${LOG_PREFIX}.log</file>
        <!-- 設置滾動策略,當日志文件大小超過${MAX_FILE_SIZE}時,新的日誌內容寫到新的日誌文件-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 新的日誌文件路徑名稱,%d:日期 %i:i是變量 -->
            <fileNamePattern>${LOG_DIR}/all_${LOG_PREFIX}%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- 保存日誌15天 -->
            <maxHistory>${MAX_HISTORY}</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!-- 日誌文件的最大大小 -->
                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <!-- 格式日誌文件內容-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>${LOG_PATTERN}</pattern>
        </layout>
    </appender>

    <!-- 3.打印錯誤日誌,保存到文件-->
    <appender name="FILE_ERR"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--日誌文件名-->
        <file>${LOG_HOME}/err_${LOG_PREFIX}.log</file>
        <!-- 設置滾動策略,當日志文件大小超過${MAX_FILE_SIZE}時,新的日誌內容寫到新的日誌文件-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 新的日誌文件路徑名稱,%d:日期 %i:i是變量 -->
            <fileNamePattern>${LOG_DIR}/err_${LOG_PREFIX}%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- 保存日誌15天 -->
            <maxHistory>${MAX_HISTORY}</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!-- 日誌文件的最大大小 -->
                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
           </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <!-- 格式日誌文件內容-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>${LOG_PATTERN}</pattern>
        </layout>
    </appender>

    <!-- rest template logger-->
    <!--<logger name="org.springframework.web.client.RestTemplate" level="DEBUG" />-->
    <!--<logger name="org.springframework" level="DEBUG" />-->

    <!-- jdbc-->
    <!--<logger name="jdbc.sqltiming" level="DEBUG" />-->
    <logger name="org.mybatis" level="DEBUG" />

    <!-- zookeeper-->
    <logger name="org.apache.zookeeper"    level="ERROR"  />

    <!-- dubbo -->
    <logger name="com.alibaba.dubbo.monitor" level="ERROR"/>
    <logger name="com.alibaba.dubbo.remoting" level="ERROR" />

    <!-- 日誌輸出級別,高到低:ERROR, WARN, INFO, DEBUG or TRACE
 有時候我們要獲取更多的日誌信息,就可以降低日誌級別 -->
    <root leve="${LOG_ROOT_LEVEL}">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="FILE_ALL" />
        <appender-ref ref="FILE_ERR" />
    </root>
</configuration>

9.2 application.yml:

在yml配置裏面規定相關的輸出路徑:

logging:
  config: classpath:./logback-spring.xml
  level:
    com.rudecrab.demo.controller: WARN
  path: ./logs

9.3 測試生成日誌文件

然後在需要的方法中使用 @Slf4j註解,和log對象來輸出日誌信息。

import lombok.extern.slf4j.Slf4j;
@Slf4j
public class UserController {
    @PostMapping("/addUser")
    public String addUser(@RequestBody @Valid User user) {
        //日誌級別從低到高分爲TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果設置爲WARN,則低於WARN的信息都不會輸出。
        log.trace("日誌輸出 trace");
        log.debug("日誌輸出 debug");
        log.info("日誌輸出 info");
        log.warn("日誌輸出 warn");
        log.error("日誌輸出 error");
        return userService.addUser(user);
    }
}

如圖:
在這裏插入圖片描述
以後項目運行,會在當前項目的./logs文件夾下生成日誌文件。
在這裏插入圖片描述

10. knife4j生成API文檔

knife4j 是爲Java MVC框架集成Swagger 生成Api文檔的增強解決方案,前身是swagger-bootstrap-ui。

所以我們推薦採用了swagger的新增強版knife4j來生成API接口文檔。
knife4j的使用方法和swagger幾乎一模一樣。

10.1 pom.xml 引入依賴

pom.xml 引入依賴:

<!--(老版本)引用依賴包-->
<dependency>
  <groupId>com.github.xiaoymin</groupId>
  <artifactId>swagger-bootstrap-ui</artifactId>
  <version>1.9.6</version>
</dependency>

<!--(新版本)swagger增強工具依賴包,方便生成接口文檔。非必須導入-->
<dependency>
     <groupId>com.github.xiaoymin</groupId>
     <artifactId>knife4j-spring-boot-starter</artifactId>
     <version>2.0.1</version>
 </dependency>

在這裏我用的新版本的依賴。

10.2 新建SwaggerConfig類:

springboot的配置文件和啓動類不用做任何特殊配置,使用knife4j需要一個swagger的配置類。

knife4j/swagger的配置類:

package com.rudecrab.demo.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * 使用了其新版名稱爲:knife4j
 * @description swagger接口文檔配置類
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    /**
     * 是否啓用swagger文檔
     * enable 開啓 disenable 關閉
     * 默認訪問地址:http://${yourhost}:${yourport}/doc.html
     */
    @Value("${swagger.enable}")
    private boolean enable;

    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2)
                .enable(enable)
                .apiInfo(apiInfo())
                .select()
                // 這裏配置要掃描的包,接口在哪個包就配置哪個包
                .apis(RequestHandlerSelectors.basePackage("com.rudecrab.demo.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    public ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("參數校驗和統一異常處理Demo")
                .description("用來演示參數校驗和統一異常處理")
                .termsOfServiceUrl("zoutao.info")
                .contact(new Contact("ZouTao", "", "[email protected]"))
                .version("1.0")
                .build();
    }
}

可以看到,內容上和以前的swagger配置類沒什麼變化,唯一的變化是類註解需要比原來的swagger多加一個 @EnableSwaggerBootstrapUi。這樣knife4j的所有配置都完成了。

以上有兩個註解需要特別說明,如下:

@EnableSwagger2
該註解是Springfox-swagger框架提供的使用Swagger註解,該註解必須加

@EnableKnife4j
該註解是knife4j提供的增強註解,Ui提供了例如動態參數、參數過濾、接口排序等增強功能,如果你想使用這些增強功能就必須加該註解,否則可以不用加

10.3 添加對應的API信息:

在這裏插入圖片描述

swagger常用註解:

  • Api Api 用在類上,說明該類的作用。
  • ApiModel 描述一個Model的信息
  • ApiModelProperty 描述一個model的屬性。
  • ApiOperation 用在方法上,說明方法的作用,
  • ApiParam 請求屬性
  • ApiResponse 響應配置
  • ApiResponses 響應集配置
  • ResponseHeader 響應頭設置

具體可參考:swagger常用註解說明

10.4 查看生成的接口文檔:

默認訪問地址:http://host:{host}:{port}/doc.html

運行springboot項目,自動生成接口文檔,
訪問地址: http://localhost:8080/doc.html

在這裏插入圖片描述
還可以在其中進行接口調試:
在這裏插入圖片描述
更多功能參考 knife4j官網

11. 總結

自此整個後端接口基本體系就構建完畢了

  • 通過Validator+自動拋出異常來完成了方便的參數校驗
  • 通過全局異常處理 + 自定義異常完成了異常操作的規範
  • 通過統一結果響應完成了對響應數據的返回規範
  • 統一錯誤處理完成了不同端發出的請求的返回。
  • 通過logback統一日誌的輸出方式,進行規範記錄
  • 通過knife4j統一生成API文檔,方便API接口說明。

代碼地址晚些會給出,如果覺得不錯或者幫助到你,希望大家給個Star。


參考地址:
https://juejin.im/post/5e7ab0bae51d45271b749815#heading-13
https://juejin.im/post/5e073980f265da33f8653f2e#heading-2
https://segmentfault.com/a/1190000019795918
https://www.toutiao.com/a6755002314790011399/
https://www.cnblogs.com/zxg-6/p/12629821.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章