SpringBoot優雅開發REST API最佳實踐
寫在前面
博主最近在做一個數據服務的項目,而這個數據服務的核心就是對外暴露的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/queryAccounthttp://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,"參數解析錯誤",""); } }

接口服務主要由兩部分組成,即參數(輸入)部分,響應(輸出)部分。其中在SpringBoot中主要是Controller層作為API的開發處,其實在架構層面來講,Controller本身是一個最高的應用層,它的職責是調用、組裝下層的interface服務數據,核心是組裝和調用,不應該摻雜其他相關的邏輯。這里統一用一系列Controller的封裝處理來提供優化思路。優雅且規范的開發REST API需要做以下幾步:接口版本控制、參數校驗、異常捕獲處理、統一響應封裝、接口文檔的維護和更新
浙公網安備 33010602011771號