SpringBoot優雅開發REST API最佳實踐

糖拌西红柿發表於2024-08-13

寫在前面

博主最近在做一個資料服務的專案,而這個資料服務的核心就是對外暴露的API,值得高興的這是一個從0開始的專案,所以終於不用受制於“某些歷史”因素去續寫各種風格的Controller,可以在專案伊始就以規範的技術和統一形式去搭建API。藉此機會,梳理和彙總一下基於SpringBoot專案開發REST API的技術點和規範點。

介面服務主要由兩部分組成,即引數(輸入)部分,響應(輸出)部分。其中在SpringBoot中主要是Controller層作為API的開發處,其實在架構層面來講,Controller本身是一個最高的應用層,它的職責是呼叫、組裝下層的interface服務資料,核心是組裝和呼叫,不應該摻雜其他相關的邏輯。

但是往往很多專案裡針對Controller部分的程式碼都是十分混亂,有的Controller兼顧各種if else的引數校驗,有的甚至直接在Controller進行業務程式碼編寫;對於Controller的輸出,有的粗略的加個外包裝,有的甚至直接把service層的結構直接丟出去;對於異常的處理也是各種各樣。

以上對於Controller相關的問題,這裡統一用一系列Controller的封裝處理來提供最佳化思路。優雅且規範的開發REST API需要做以下幾步:

  • 介面版本控制
  • 引數校驗
  • 異常捕獲處理
  • 統一響應封裝
  • 介面文件的維護和更新

@RestController註解

直接來看@RestController原始碼

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}

@RestController註解等價於@Controller和@@ResponseBody,@ResponseBody註解的作用是告訴Spring MVC框架,該方法的返回值應該直接寫入HTTP響應體中,而不是返回一個檢視(View)。當一個控制器方法被標記為 @ResponseBody 時,Spring MVC會將方法的返回值序列化成JSON或XML等格式,然後傳送給客戶端。更適用於REST API的構建。

所以針對Controller介面的開發,直接使用@RestController為好。它會自動將Controller下的方法返回內容轉為REST API的形式

例如:

@RestController
@RequestMapping("/dataserver/manage")
public class DataServerController{
    
    @PostMapping("/search")
    public Response searchData(@RequestBody SearchTaskDto param){
        return Response.success(taskScheduleManagerService.searchTaskForPage(param));
    }
}

介面版本管理

對於API來講,一般是對外服務的基礎,不能隨意變更,但是隨著需求和業務不斷變化,介面和引數也會發生相應的變化。此時儘可能保證“開閉原則”,以新增介面或增強介面功能來支撐,此時就需要對API的版本進行維護,以版本號來確定同一介面的不同能力,一般版本都基於url來控制

例如:

  • http://localhost:8080/dataserver/v1/queryAccount
  • http://localhost:8080/dataserver/v2/queryAccount:相比v1版本增強了引數查詢的靈活性

進行API版本控制主要分三步:

  • 定義版本號註解
  • 編寫版本號匹配邏輯處理器
  • 註冊處理器

定義版本號註解

/**
 * API版本控制註解
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    /**
     *版本號,預設為1
     */
    int value() default 1;
}

該註解可直接使用在Controller類上

@RestController
@RequestMapping("dataserver/{version}/account")
@ApiVersion(2)//輸入版本號,對應{version}
public class AccountController{
    @GetMapping("/test")
    public String test() {
        return "XXXX";
    }
}

編寫版本號匹配邏輯處理器

首先定義一個條件匹配類,對應解析Url中的version與ApiVersion註解

/**
*實現Request的條件匹配介面
*
**/
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
    private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*v(\\d+).*");
 
    private int apiVersion;
 
    ApiVersionCondition(int apiVersion) {
        this.apiVersion = apiVersion;
    }
 
    private int getApiVersion() {
        return apiVersion;
    }
 
 
    @Override
    public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
        return new ApiVersionCondition(apiVersionCondition.getApiVersion());
    }
 
    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
        Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());
        if (m.find()) {
            Integer version = Integer.valueOf(m.group(1));
            if (version >= this.apiVersion) {
                return this;
            }
        }
        return null;
    }
 
    @Override
    public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
        return apiVersionCondition.getApiVersion() - this.apiVersion;
    }
}

這裡補充一下 RequestCondition<ApiVersionCondition>相關概念:

它是 Spring 框架中用於請求對映處理的一部分。在 Spring MVC 中,RequestCondition 介面允許開發者定義自定義的請求匹配邏輯,這可以基於請求的任何屬性,例如路徑、引數、HTTP 方法、頭部等。相關的應用場景包括:

  1. 路徑匹配(Path Matching):使用 PatternsRequestCondition 來定義請求的路徑模式,支援 Ant 風格的路徑模式匹配,如 /api/* 可以匹配所有 /api 開頭的請求路徑 。

  2. 請求方法匹配(Request Method Matching):透過 RequestMethodsRequestCondition 來限制請求的 HTTP 方法,例如只允許 GET 或 POST 請求 。

  3. 請求引數匹配(Request Params Matching):使用 ParamsRequestCondition 來定義請求必須包含的引數,例如某些介面可能需要特定的查詢引數才能訪問 。

  4. 請求頭匹配(Request Headers Matching)HeadersRequestCondition 允許定義請求頭的條件,例如某些介面可能需要特定的認證頭部才能訪問 。

  5. 消費媒體型別匹配(Consumes Media Type Matching)ConsumesRequestCondition 用來定義控制器方法能夠處理的請求體媒體型別,通常用於 RESTful API 中,例如只處理 application/json 型別的請求體 。

  6. 產生媒體型別匹配(Produces Media Type Matching)ProducesRequestCondition 定義了控制器方法能夠返回的媒體型別,這通常與 Accept 請求頭結合使用以確定響應的格式 。

  7. 自定義條件匹配:開發者可以透過實現 RequestCondition 介面來定義自己的匹配邏輯,例如根據請求中的版本號來路由到不同版本的 API,實現 API 的版本控制 。

  8. 組合條件匹配(Composite Conditions Matching):在某些情況下,可能需要根據多個條件來匹配請求,CompositeRequestCondition 可以將多個 RequestCondition 組合成一個條件來進行匹配 。

  9. 請求對映的優先順序選擇(Priority Selection for Request Mapping):當存在多個匹配的處理器方法時,RequestConditioncompareTo 方法用於確定哪個條件具有更高的優先順序,以選擇最合適的處理器方法 。

建立一個版本對映處理器,使用 ApiVersionCondition 作為自定義條件來處理請求對映。當 Spring MVC 處理請求時,它會使用這個自定義的對映處理器來確定哪個版本的 API 應該處理請求。

public class ApiRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
    private static final String VERSION_FLAG = "{version}";
   
    /**
     *檢查類上是否有 @RequestMapping 註解,如果有,它會構建請求對映的 URL。如果 URL 中包含版本 
     *標識 VERSION_FLAG,並且類上有 ApiVersion 註解,它將建立並返回一個 ApiVersionCondition 
     *例項,表示這個類關聯的 API 版本。
     **/
    private static RequestCondition<ApiVersionCondition> createCondition(Class<?> clazz) {
        RequestMapping classRequestMapping = clazz.getAnnotation(RequestMapping.class);
        if (classRequestMapping == null) {
            return null;
        }
        StringBuilder mappingUrlBuilder = new StringBuilder();
        if (classRequestMapping.value().length > 0) {
            mappingUrlBuilder.append(classRequestMapping.value()[0]);
        }
        String mappingUrl = mappingUrlBuilder.toString();
        if (!mappingUrl.contains(VERSION_FLAG)) {
            return null;
        }
        ApiVersion apiVersion = clazz.getAnnotation(ApiVersion.class);
        return apiVersion == null ? new ApiVersionCondition(1) : new ApiVersionCondition(apiVersion.value());
    }
 
    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        return createCondition(method.getClass());
    }
 
    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        return createCondition(handlerType);
    }
}

註冊處理器

將上述的處理器註冊到SpringMvc的處理流程中

@Configuration
public class WebMvcRegistrationsConfig implements WebMvcRegistrations {
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new ApiRequestMappingHandlerMapping();
    }
}

驗證:

@RestController
@RequestMapping("dataserver/{version}/account")
@ApiVersion(1)
public class AccountOneController {
 
    @GetMapping("/test")
    public String test() {
        return "測試介面,版本1";
    }
    @GetMapping("/extend")
    public String extendTest() {
        return "版本1的測試介面延申";
    }
}


@RestController
@RequestMapping("dataserver/{version}/account")
@ApiVersion(2)
public class AccountTwoController {
    @GetMapping("/test")
    public String test() {
        return "測試介面,版本2";
    }
}

針對test介面進行不同版本的請求:

針對Account擴充套件版本呼叫上一版本介面

當請求對應的版本不存在介面時,會匹配之前版本的介面,即請求/v2/account/extend 介面時,由於v2 控制器未實現該介面,所以自動匹配v1 版本中的介面。這就實現了API版本繼承

引數校驗

@Validated註解

@Validated 是一個用於 Java 應用程式中的註解,特別是在 Spring 框架中,以指示目標物件或方法需要進行驗證。這個註解通常與 JSR 303/JSR 380 規範的 Bean Validation API 結合使用,以確保資料的合法性和完整性。

@Validated註解的三種用法:

方法級別驗證:當 @Validated 註解用在方法上時,它指示 Spring 在呼叫該方法之前執行引數的驗證。如果引數不符合指定的驗證條件,將丟擲 MethodArgumentNotValidException

@PostMapping("/user")
@Validated
public ResVo createUser(@RequestBody @Valid User user) {
    // 方法實現
}

類級別驗證:將 @Validated 註解用在類上,表示該類的所有處理請求的方法都會進行驗證。這可以減少在每個方法上重複註解的需要。

@RestController
@Validated
public class UserController {
    // 類中的所有方法都會進行驗證
}

組合註解:Spring 還提供了 @Valid 註解,它是 @Validated 的一個更簡單的形式,只觸發驗證並不指定特定的驗證組(Validation Groups)。@Validated 允許你指定一個或多個驗證組,這在需要根據不同情況執行不同驗證規則時非常有用。

@Validated(OnCreate.class)
public void createUser(User user) {
    // 只使用 OnCreate 組的驗證規則
}

使用註解進行引數校驗

在REST API中進行引數驗證一般使用方法級別驗證即可,即對引數Dto的類內資訊進行驗證,例如一個分頁的查詢引數類:

@Data
public class BaseParam implements Serializable {

    @NotNull(message = "必須包含關鍵字")
    private String  keyFilter;

    @Min(value = 1,message = "頁碼不可小於1")
    private int pageNo;

    @Max(value = 100,message = "考慮效能問題,每頁條數不可超過100")
    private int pageSize;

}

在Controller中配合@Validated使用:

  @PostMapping("/findProductByVo")
    public PageData findByVo(@Validated ProductParam param) {
        //……業務邏輯
        return PageData.success(data);
    }

此時如果前端傳入引數不合法,例如pageNo為0又或者productType不存在,則會丟擲MethodArgumentNotValidException 的異常。稍後對於異常進行處理即可完成引數的驗證。

這裡的@Max@Min@NotNull 註解屬於 Bean Validation API 的一部分,這是一個 JSR 303/JSR 380 規範,用於在 Java 應用程式中提供宣告式驗證功能。這些註解用於約束欄位值的範圍和非空性。類似的註解還有:

註解 作用

@NotNull

驗證註解的欄位值不能為 null
@NotEmpty @NotNull 類似,但用於集合或字串,驗證註解的欄位值不能為 null,且對於字串,長度不能為 0。
@NotBlank 驗證註解的欄位值不能為 null,且不能是空白字串(空白包括空格、製表符等)。
@Min(value) 驗證註解的欄位值是否大於或等於指定的最小值。value 引數接受一個整數。
@Max(value) 驗證註解的欄位值是否小於或等於指定的最大值。value 引數接受一個整數。
@Size(min, max) 驗證字串或集合的大小在指定的最小值和最大值之間。
@Pattern(regex) 驗證欄位值是否符合指定的正規表示式。

注:SpringBoot 2.3.1 版本預設移除了校驗功能,如果想要開啟的話需要新增以上依賴

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

統一異常捕獲

@RestControllerAdvice註解

@RestControllerAdvice 是 @ResponseBody+@ControllerAdvice的集合註解,用於定義一個控制器級別的異常處理類。一般用來進行全域性異常處理,在 @RestControllerAdvice 類中處理異常後,可以直接返回一個物件,該物件會被轉換為 JSON 或 XML 響應體,返回給客戶端。

使用@RestControllerAdvice註解處理引數異常

使用@Validated和 Bean Validation API 的註解進行引數校驗後,當出現不符合規定的引數會丟擲MethodArgumentNotValidException 異常,這裡就可以使用@RestControllerAdvice註解來建立一個全域性Controller異常攔截類,來統一處理各類異常

@RestControllerAdvice
public class ControllerExceptionAdvice {

    @ExceptionHandler({MethodArgumentNotValidException .class})//此處可以根據引數異常的各類情況進行相關異常類的繫結
    public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        // 從異常物件中拿到ObjectError物件
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        return "引數異常錯誤";
    }
}

這裡只以 MethodArgumentNotValidException 異常進行攔截,在@RestControllerAdvice類內可以建立多個方法,透過@ExceptionHandler對不同的異常進行定製化處理,這樣當Controller內發生異常,都可以在@RestControllerAdvice類內進行截獲、處理、返回給客戶端安全的資訊。

@RestControllerAdvice
public class ControllerExceptionAdvice {

    //HttpMessageNotReadableException異常為webJSON解析出錯
    @ExceptionHandler({HttpMessageNotReadableException.class})
    public String MethodArgumentNotValidExceptionHandler(HttpMessageNotReadableException e) 
   {
        return "引數錯誤";
    }

     @ExceptionHandler({XXXXXException .class})
    public String otherExceptionHandler(Exception e) {
       
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        return objectError..getDefaultMessage();
    }
}

統一響應封裝

首先,進行統一的響應格式,這裡需要封裝一個固定返回格式的結構物件:ResponseData

public class Response<T> implements Serializable {
    private Integer code;
    private String msg;
    private T data;

    public Response() {
        this.code = 200;
        this.msg = "ok";
        this.data = null;
    }

    public Response(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public Response(String msg, T data) {
        this(200, msg, data);
    }

    public Response(T data) {
        this("ok", data);
    }

    public static <T> Response<T> success(T data) {
        return new Response(data);
    }

    public Integer getCode() {
        return this.code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return this.msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return this.data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String toJsonString() {
        String out = "";
        try {
            out = JSONUtil.toJsonPrettyStr(this);
        } catch (Exception var3) {
            this.setData(null);
            var3.printStackTrace();
        }
        return out;
    }
}

統一狀態碼

其中對於相關的狀態碼最好進行統一的封裝,便於以後的開發,建立狀態列舉:

//面向介面開發,首先定義介面
public interface StatusCode {
    Integer getCode();
    String getMessage();
}
//建立列舉類
public enum ResponseStatus implements StatusCode{

    //正常響應
    SUCCESS(200, "success"),
    //伺服器內部錯誤
    FAILED(500, " Server Error"),
    //引數校驗錯誤
    VALIDATE_ERROR(400, "Bad Request");

    //……補充其他內部約定狀態

    private int code;
    private String msg;

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

    @Override
    public Integer getCode() {
        return this.code;
    }

    @Override
    public String getMessage() {
        return this.msg;
    }
}

統一返回結構

將上述的ResponseData中狀態類相關替換為列舉

public class Response<T> implements Serializable {
    private Integer code;
    private String msg;
    private T data;

    public Response() {
        this.code = 200;
        this.msg = "success";
        this.data = null;
    }
    public Response(StatusCode status, T data) {
        this.code = status.getCode();
        this.msg = status.getMssage();
        this.data = data;
    }
    public Response(T data) {
        this(ResponseStatus.SUCCESS, data);
    }
    public static <T> Response<T> success(T data) {
        return new Response(data);
    }
    public Integer getCode() {
        return this.code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMsg() {
        return this.msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public T getData() {
        return this.data;
    }
    public void setData(T data) {
        this.data = data;
    }
    public String toJsonString() {
        String out = "";
        try {
            out = JSONUtil.toJsonPrettyStr(this);
        } catch (Exception var3) {
            this.setData(null);
            var3.printStackTrace();
        }
        return out;
    }
}

這樣Controller的介面統一返回格式就是標準的結構了。

{
  "code":200,
  "msg":"success",
  "data":{
    "total":123,
    "record":[]
  }
}

統一封裝Controller響應

有了統一響應體的Controller在返回時可以這樣寫:

    @PostMapping("/search")
    @Operation(summary = "分頁查詢任務")
    public Response searchData(@RequestBody SearchParam param){
        return Response.success(XXXXService.searchForPage(param));
    }

即便如此,團隊開發中可能還會出現換個人新寫Controller不知道有統一返回體這回事,為了更保險,可以透過AOP進行統一對結果進行封裝,不論Controller返回啥,到客戶端的資料都包含一個包裝體。

具體實現是使用@RestControllerAdvice類實現ResponseBodyAdvice介面來完成。

@RestControllerAdvice
public class ControllerResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        // 返回結構是Response型別都不進行包裝
        return !methodParameter.getParameterType().isAssignableFrom(Response.class);
    }

   @Override
    public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
        // String型別不能直接包裝
        if (returnType.getGenericParameterType().equals(String.class)) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                // 將資料包裝在ResultVo裡後轉換為json串進行返回
                return objectMapper.writeValueAsString(Response.success(data));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
       
        // 其他所有結果統一包裝成Response返回
        return Response.success(data);
    }
}

我們以test介面為例,test介面原本返回的是String,而toint返回的是Integer

@RestController
@RequestMapping("dataserver/{version}/account")
@ApiVersion(1)
public class AccountOneController {

    @GetMapping("/test")
    public String test() {
        return "測試介面,版本1";
    }

   @GetMapping("/toint")
    public Integer toint() {
        return 1;
    }

}

但是頁面返回是JSON字串和返回體:

文件:除錯維護API利器—Swagger

介面開發完成,除錯時,大多數都是使用Postman模擬請求除錯或者直接用前端程式碼呼叫除錯,其實這兩種都比較麻煩,尤其面對複製引數時,Postman要逐個介面的錄入,十分費事,其實這裡可以在SpringBoot中引入Swagger介面文件元件,介面文件和介面除錯一併解決。

引入依賴

直接在maven中引入相關依賴:

     <!-- swagger 3 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>

標準的swagger3引入以上兩個依賴即可,相關版本可自行選擇

裝配配置類

下面進行swagger的配置類和一些swagger相關頁面的配置

@Configuration
public class SwaggerConfig {


    @Bean
    public Docket testApis(){
        return new Docket(DocumentationType.OAS_30)
                .apiInfo(apidoc())
                .select()
                .apis(RequestHandlerSelectors.basePackage("net.gcc.webrestapi.controller"))
                .paths(PathSelectors.any())
                .build()
                .groupName("測試服務")
                .enable(true);
    }

    private ApiInfo apidoc(){
        return new ApiInfoBuilder()
                .title("測試介面")
                .description("介面文件")
                .contact(new Contact("GCC", "#", "XXXX"))
                .version("1.0")
                .build();
    }
}

使用註解

Swagger相關注解明細

註解 使用位置 作用
@Api 作用在類上,Controller 表示對類的說明,通常用於描述 Controller 的作用和分類,如 @Api(tags = "使用者管理"),後續會在Swagger文件中以目錄形式展示
@ApiOperation 作用在方法上,一般為Controller中具體方法 描述 API 介面的具體操作和功能,例如 @ApiOperation(value = "獲取使用者列表", notes = "根據條件獲取使用者列表") ,在swagger文件中以目錄內容體現
@ApiModel 作用於類上,一般是引數實體類 表示這是一個模型類,通常與 @ApiModelProperty 結合使用來描述模型屬性 。
@ApiModelProperty 用於模型類的屬性上,引數類的成員變數 描述屬性的資訊,如 @ApiModelProperty(value = "使用者名稱", required = true)
@ApiImplicitParams@ApiImplicitParam 用於方法上,一般為Controller中具體方法 描述介面的隱含引數,例如請求引數或請求頭資訊
@ApiResponses@ApiResponse 用於方法上,一般為Controller中具體方法 描述介面的響應資訊,可以指定不同的響應狀態碼和對應的描述資訊 。
@ApiIgnore 用於類或方法上 表示忽略該類或方法,不將其顯示在Swagger文件中。

@Api@ApiOperation使用

@RestController
@RequestMapping("/dataserver/{version}/manage")
@Api(tags = "資料來源管理服務", description = "用於管理資料來源資訊")
@ApiVersion
public class DataServerController {

    @PostMapping("/search")
    @ApiOperation(summary = "分頁查詢資料來源")
    public IPage<DataSourceEntity> searchData(@RequestBody SearchParam param){
        //XXXX邏輯
        return new IPage<DataSourceEntity>();
    }
    
    
}

@ApiMode@ApiModelProperty

@Data
@ApiModel(value = "基礎引數")
public class BaseParam implements Serializable {

    @ApiModelProperty(value = "關鍵字", required = true)
    @NotNull(message = "必須包含關鍵字")
    private String  keyFilter;
    @ApiModelProperty(value = "頁碼", required = true)
    @Min(value = 1,message = "頁碼不可小於1")
    private int pageNo;
    @ApiModelProperty(value = "每頁大小", required = true)
    @Max(value = 100,message = "考慮效能問題,每頁條數不可超過100")
    private int pageSize;

}

@ApiImplicitParams@ApiImplicitParam

與ApiMode和ApiModeProperty功能一致,一般用於get請求中的引數描述

   @GetMapping("/extend")
    @ApiOperation(value = "賬號角色",notes = "測試版本1延申介面")
    @ApiImplicitParams({
            @ApiImplicitParam(value = "accountId",name = "賬號ID"),
            @ApiImplicitParam(value = "role",name = "角色")
    }
    )
    public String extendTest(String accountId,String role) {
        return new JSONObject().set("account",accountId).set("role",role).toJSONString(0);
    }

效果

使用swagger後,直接在頁面訪問 http://127.0.0.1:8080/XXX/doc.html即可訪問介面頁面

不要複雜的postman呼叫,本地除錯可以直接使用除錯功能

補充:完整的Controller類程式碼模板

@RestController
@RequestMapping("/dataserver/{version}/manage")
@Api(tags = "資料來源管理服務V1")
@ApiVersion
public class DataServerController {

    @PostMapping("/search")
    @ApiOperation(value = "分頁查詢資料來源",notes = "測試")
    public PageVo<DataSourceVo> searchData(@RequestBody BaseParam param){
        //XXXX邏輯
        return new PageVo<DataSourceVo>();
   }

    //get請求,使用ApiImplicitParams註解標明引數
    @GetMapping("/searchAccountAndRole")
    @ApiOperation(value = "賬號角色",notes = "查詢賬號角色")
    @ApiImplicitParams({
            @ApiImplicitParam(value = "accountId",name = "賬號ID"),
            @ApiImplicitParam(value = "role",name = "角色")
    })
    public String extendTest(String accountId,String role) {
        return new JSONObject().set("account",accountId).set("role",role).toJSONString(0);
    }

}

//部分引數程式碼:
@Data
@ApiModel
public class BaseParam implements Serializable {
    @NotNull(message = "必須包含關鍵字")
    @ApiModelProperty("關鍵字過濾")
    private String  keyFilter;

    @Min(value = 1,message = "頁碼不可小於1")
    @ApiModelProperty("分頁頁碼")
    private int pageNo;

    @Max(value = 100,message = "考慮效能問題,每頁條數不可超過100")
    @ApiModelProperty("分頁每頁條數")
    private int pageSize;

}
//響應部分程式碼
@Data
@ApiModel
public class DataSourceVo implements Serializable {
    @ApiModelProperty("id")
    private String id;
    @ApiModelProperty("資料來源名稱")
    private String name;
    @ApiModelProperty("資料來源url")
    private String url;
}

@Data
@ApiModel
public class PageVo<V> {

    @ApiModelProperty("總數量")
    private int total;
    @ApiModelProperty("具體內容")
    private List<V> rows;
}

補充:完整的@RestControllerAdvice類程式碼模板

關於引數驗證的異常處理和統一返回結構,可以使用一個類來完成,以下是完整模板:

@RestControllerAdvice(basePackages = "net.gcc.webrestapi")
public class ControllerExceptionAdvice implements ResponseBodyAdvice<Object> {


    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        // 返回結構是Response型別都不進行包裝
        return !methodParameter.getParameterType().isAssignableFrom(Response.class);
    }

    @Override
    public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
        // String型別不能直接包裝
        if (returnType.getGenericParameterType().equals(String.class)) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                // 將資料包裝在ResultVo裡後轉換為json串進行返回
                return objectMapper.writeValueAsString(Response.success(data));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        //系統特殊錯誤
        if(data instanceof LinkedHashMap
                && ((LinkedHashMap<?, ?>) data).containsKey("status")
                && ((LinkedHashMap<?, ?>) data).containsKey("message")
                &&((LinkedHashMap<?, ?>) data).containsKey("error")){
            int code = Integer.parseInt(((LinkedHashMap<?, ?>) data).get("status").toString());
            String mssage = ((LinkedHashMap<?, ?>) data).get("error").toString();
            return new Response<>(code,mssage,null);
        }
        // 其他所有結果統一包裝成Response返回
        return Response.success(data);
    }


    @ExceptionHandler({MethodArgumentNotValidException.class})
    public Response MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e)
    {
        // 預設統一返回響應體,填寫引數錯誤編碼, 從異常物件中拿到錯誤資訊
        return new Response(401,e.getBindingResult().getAllErrors().get(0).getDefaultMessage(),"");
    }

    //HttpMessageNotReadableException異常為webJSON解析出錯
    @ExceptionHandler({HttpMessageNotReadableException.class})
    public Response HttpNotReqadableExceptionHandler(HttpMessageNotReadableException e)
    {
        // 預設統一返回響應體,填寫引數錯誤編碼, 從異常物件中拿到錯誤資訊
        return new Response(401,"引數解析錯誤","");
    }
    
}

相關文章