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/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,"参数解析错误","");
} }
SpringBoot优雅开发REST API最佳实践的更多相关文章
- SpringBoot快速开发REST服务最佳实践
一.为什么选择SpringBoot Spring Boot是由Pivotal团队提供的全新框架,被很多业内资深人士认为是可能改变游戏规则的新项目.早期我们搭建一个SSH或者Spring Web应用,需 ...
- 我所理解的Restful API最佳实践
一直在公司负责API数据接口的开发,期间也遇到了不小的坑,本篇博客算是做一个小小的记录. 1. 不要纠结于无意义的规范 在开始本文之前,我想先说这么一句:RESTful 真的很好,但它只是一种软 ...
- 我所认为的RESTful API最佳实践
我所认为的RESTful API最佳实践 不要纠结于无意义的规范 在开始本文之前,我想先说这么一句:RESTful 真的很好,但它只是一种软件架构风格,过度纠结如何遵守规范只是徒增烦恼,也违背了使用它 ...
- ASP.NET Core Web API 最佳实践指南
原文地址: ASP.NET-Core-Web-API-Best-Practices-Guide 介绍 当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求. 但是,你难 ...
- Typescript 开发环境的最佳实践
Typescript 开发环境的最佳实践 0️⃣ git init(略) 1️⃣️️ 初始化:$ yarn add -D ts-node typescript 2️⃣ 生成 tsconfig.json ...
- REST与RESTFul API最佳实践
我经常会面试一些做PHP的开发者,让我很奇怪的是,10个人总有8个多不知道什么是REST服务,甚至是没有听说过.但RESTFul API已经是现在互联网里对外开放接口的主流模式,可参考: 豆瓣API ...
- [转]ASP.NET Core Web API 最佳实践指南
原文地址: ASP.NET-Core-Web-API-Best-Practices-Guide 转自 介绍# 当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求. 但 ...
- js-新兴的API,最佳实践,离线应用于客户端存储
离线应用于客户端存储: 1.离线检测:online以及offline事件,都是在window对象上触发 navigator.online为true的时候是表示设备能够上网 2.使用一个描述文件(man ...
- SpringBoot系列: Spring项目异常处理最佳实践
===================================自定义异常类===================================稍具规模的项目, 一般都要自定义一组异常类, 这 ...
- SpringBoot 优雅整合Swagger Api 自动生成文档
前言 一个好的可持续交付的项目,项目说明,和接口文档是必不可少的,swagger api 就可以帮我们很容易自动生成api 文档,不需要单独额外的去写,无侵入式,方便快捷大大减少前后端的沟通方便查找和 ...
随机推荐
- Blender练习——SciFi枪械.md
Blender练习--SciFi枪械 一.基本操作 常用快捷键 E 挤出 B 倒角,中途可通过滚轮或S来调整细分 Alt+点选 循环选择 Ctrl Alt+点选 并排选择 F 补面,比如一个碗口,将碗 ...
- FEDORA 显卡驱动安装
FEDORA 显卡驱动安装 在fedora中akmod-nvidia包可以自动的处理开源驱动屏蔽等各种问题, 强烈推荐用这个安显卡驱动. -1. 在 BIOS 中关闭安全启动 0. 切换桌面环境至 X ...
- Linux 时间 与 定时器
背景 在学习 Linux 信号 有关知识中,提到了 alarm函数. 进程时间 (原文地址:https://www.cnblogs.com/clover-toeic/p/3845210.html) 进 ...
- STM32 CubeMX 学习:002-外部中断的使用
背景 上一讲 STM32 CubeMX 学习:GPIO的使用 介绍了如何配置以及操作GPIO引脚. 这一讲我们通过中断来控制按键.关于中断的概念不做介绍. HOST-OS : Windows-10 S ...
- Elastic-Search 整理(二):高级篇
ES高级篇 集群部署 集群的意思:就是将多个节点归为一体罢了,这个整体就有一个指定的名字了 window中部署集群 - 了解 把下载好的window版的ES中的data文件夹.logs文件夹下的所有的 ...
- Notepad++ 搭建简单Java编译运行环境
简介 有时候使用Eclips进行Java相关方法的测试和验证太繁琐,经过查询实践,使用了Notepad++和JDK搭建了一个简单的编译运行环境. 搭建过程 在电脑上安装Java环境(网上教程很多,此过 ...
- SpringBoot实现单机锁和分布式锁
1.使用Java的内置锁机制(单机锁) Java提供了synchronized关键字和java.util.concurrent.locks.Lock接口来实现锁. synchronized是Java语 ...
- Blazor Server App Cannot find the fallback endpoint specified by route values
github官方issues中提到的解决方案,CreateBuilder时指定项目绝对路径可以解决. 1 // 指定项目路径,也可以用Assembly.GetCallingAssembly获取 2 c ...
- linux 查看端口状态
查看端口 netstat -tlun 查看端口被那个服务占用 netstat -tunlp |grep 8080
- C# 判断字符串是否为整数
/// <summary> /// 判断一个字符串是否是正整数 /// </summary> /// <param name="s"></ ...