寫在前面
博主最近在做一個資料服務的專案,而這個資料服務的核心就是對外暴露的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 方法、頭部等。相關的應用場景包括:
路徑匹配(Path Matching):使用
PatternsRequestCondition
來定義請求的路徑模式,支援 Ant 風格的路徑模式匹配,如/api/*
可以匹配所有/api
開頭的請求路徑 。請求方法匹配(Request Method Matching):透過
RequestMethodsRequestCondition
來限制請求的 HTTP 方法,例如只允許 GET 或 POST 請求 。請求引數匹配(Request Params Matching):使用
ParamsRequestCondition
來定義請求必須包含的引數,例如某些介面可能需要特定的查詢引數才能訪問 。請求頭匹配(Request Headers Matching):
HeadersRequestCondition
允許定義請求頭的條件,例如某些介面可能需要特定的認證頭部才能訪問 。消費媒體型別匹配(Consumes Media Type Matching):
ConsumesRequestCondition
用來定義控制器方法能夠處理的請求體媒體型別,通常用於 RESTful API 中,例如只處理application/json
型別的請求體 。產生媒體型別匹配(Produces Media Type Matching):
ProducesRequestCondition
定義了控制器方法能夠返回的媒體型別,這通常與Accept
請求頭結合使用以確定響應的格式 。自定義條件匹配:開發者可以透過實現
RequestCondition
介面來定義自己的匹配邏輯,例如根據請求中的版本號來路由到不同版本的 API,實現 API 的版本控制 。組合條件匹配(Composite Conditions Matching):在某些情況下,可能需要根據多個條件來匹配請求,
CompositeRequestCondition
可以將多個RequestCondition
組合成一個條件來進行匹配 。請求對映的優先順序選擇(Priority Selection for Request Mapping):當存在多個匹配的處理器方法時,
RequestCondition
的compareTo
方法用於確定哪個條件具有更高的優先順序,以選擇最合適的處理器方法 。
建立一個版本對映處理器,使用 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,"引數解析錯誤",""); } }