SpringBoot优雅开发REST API最佳实践

news2024/9/21 10:38:49

目录

@RestController注解

接口版本管理

定义版本号注解

 编写版本号匹配逻辑处理器

 注册处理器

参数校验

@Validated注解

使用注解进行参数校验

统一异常捕获

@RestControllerAdvice注解

使用@RestControllerAdvice注解处理参数异常

统一响应封装

统一状态码

统一返回结构 

统一封装Controller响应

文档:调试维护API利器—Swagger

引入依赖

装配配置类

使用注解

效果

补充:完整的Controller类代码模板

补充:完整的@RestControllerAdvice类代码模板


博主最近在做一个数据服务的项目,而这个数据服务的核心就是对外暴露的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接口进行不同版本的请求:

ff18a8cf45c04422a366a44238f8a04c.png

93758c4d8d2643aab91c925638693637.png

针对Account扩展版本调用上一版本接口

c62b313429a64199831b6199f3049c64.png

当请求对应的版本不存在接口时,会匹配之前版本的接口,即请求/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>

使用API注解参数校验后,参数错误异常多为MethodArgumentNotValidException

统一异常捕获

@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字符串和返回体:

f8bca2b475c14a6c9582a71cf9a165c2.png

3d4f26a112a14b818a012aac2b649dd6.png

文档:调试维护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即可访问接口页面

本地调试可以直接使用调试功能 

补充:完整的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,"参数解析错误","");
    }
    
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2033192.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[Vue3] 8 toRef与toRefs的用法

前言 目标 1 toRef与toRefs的用法 2 toRef与ref的不同 toRef与toRefs的用法 看一下官方给的解释 也是就当定义const name = toRef(person,name) 改变name = 李四的值,person中的name值也会同步变化 toRef 只能处理一个对象中的一个属性 <h2>{{person }}</h2&g…

超详细!!! LVS(Linux virual server)负载均衡知识及其NAT模式、DR模式、火墙标记实验

目录 前言系统性能扩展方式集群Cluster分布式集群与分布式 四层转发与七层转发的区别 LVS&#xff08;Linux virual server&#xff09;一、LVS介绍LVS相关概念 二、LVS集群结构体系1. 负载均衡层&#xff08;Load Balancer&#xff09;2. 服务器群组层&#xff08;Server Pool…

RuoYi-Cloud开启控制台日志

打开对应微服务resources目录下的logback.xml文件&#xff0c;将"系统模块日志级别控制"改为debug。

坐标变换矩阵

在高级驾驶辅助系统&#xff08;ADAS&#xff09;领域&#xff0c;存在多种常用的坐标系&#xff1a;雷达Lidar坐标系、车辆坐标系、相机坐标系、图像坐标系。 旋转变换矩阵&#xff08;Rotation Matrix&#xff09; 在二维平面xoy上&#xff0c;由绿色坐标系逆时针旋转θ到蓝…

U盘出现文件目录损坏的修复之道

在数字化时代&#xff0c;U盘作为便携式存储设备&#xff0c;承载着无数重要的文件与数据&#xff0c;成为我们工作、学习和生活中不可或缺的一部分。然而&#xff0c;当U盘遭遇“文件或目录损坏且无法读取”的困境时&#xff0c;这份便捷瞬间转化为焦虑与困扰。本文将深入探讨…

【安卓】连接真机和使用通知

文章目录 连接到真机使用通知通知的简单使用通知的详细信息 连接到真机 先用USB线将手机与电脑连接。 打开手机的设置&#xff0c;找到关于手机&#xff0c;点开之后&#xff0c;找到开发者选项界面。或者找到软件版本号&#xff0c;连续点击&#xff0c;系统会提示你点击几次能…

go在linux上安装

1.首先要确定Linux架构 uname -m如果你的系统是 armv7l&#xff08;32-bit ARM&#xff09;&#xff0c;你需要下载 armv6l 版的Go语言。 如果你的系统是 aarch64&#xff08;64-bit ARM&#xff09;&#xff0c;你需要下载 arm64 版的Go语言。 如果你的系统是 x86_64&#xf…

母婴生活馆小程序开发制作方案

母婴生活馆小程序系统主要是想满足年轻父母群体对于高品质母婴产品和服务的需求。开发一款集商品展示、在线购买、育儿知识分享、社区互动等功能于一体的母婴生活馆小程序。一、主要用户群体 年龄在25-35岁之间&#xff0c;初为人父母或即将成为父母的年轻夫妇。他们通常具备一…

自动驾驶大模型算法助力端到端顺利落地

 FSD 效果惊艳&#xff0c;Robotaxi 迈向现实 2024 年 3 月 FSD V12.3 推出&#xff0c;解决复杂场景能力大幅提升&#xff0c;驾驶体验平顺丝滑拟人化程度高。FSD 开始从“测试版”晋级为“监督版”&#xff0c;并面向所有北美车主免费试用 30 天。随后马斯克在社交媒体上表…

LogicFlow工作流在React和Vue3中的使用

LogicFlow 是一款流程图编辑框架&#xff0c;提供了一系列流程图交互、编辑所必需的功能和简单灵活的节点自定义、插件等拓展机制&#xff0c;方便我们快速在业务系统内满足类流程图的需求。 核心能力 可视化模型&#xff1a;通过 LogicFlow 提供的直观可视化界面&#xff0c…

gRPC golang开发实践

gRPC golang开发实践 Protobuf定义消息类型标量类型复合类型引用其它消息类型 Protoc使用安装使用语言插件 Buf使用配置和构建buf模块代码生成 实现gRPC API初始化go.mod文件实现服务端代码实现客户端代码 测试gRPC API使用bloomRPC客户端工具使用grpcurl命令行工具使用buf cur…

RCE之突破长度限制

我们在写webshell时通常会遇到过滤&#xff0c;但除了过滤之外还可能会有长度限制&#xff0c;这里就简单说一下关于RCE突破长度限制的技巧 突破16位 例如&#xff1a;PHP Eval函数参数限制在16个字符的情况下 &#xff0c;如何拿到Webshell&#xff1f; <?php $param …

jenkins 安装以及自动构建maven项目并且运行

在这里找到你对应jdk的版本的jenkins包 War Jenkins Packages 我这里用的使java8,所以下载 https://mirrors.jenkins.io/war-stable/2.60.1/jenkins.war 然后jenkins可以安装到centos系统 在本地windows系统运行命令行 scp C:\Users\98090\Downloads\jenkins.war root@192…

在Oxygen中插入图形的三种方法

在Oxygen中有以下几种在内容中插入图形的方法&#xff1a; 方法一 1. 将光标放在想要插入图形的地方&#xff0c;并点击插入图形工具栏 2. 在弹出窗口选择需要插入的图形路径&#xff0c;并做相关的设置 注&#xff1a;图形最好是使用相对路径&#xff0c;这样不依赖于本地路…

NVIDIA H100 GPU,它将如何改变AI和计算领域的游戏规则?

大语言模型 (LLM) 的兴起标志着人工智能 (AI) 时代的重大进步。在这一背景下&#xff0c;Paperspace DigitalOcean 提供的云图形处理单元 (GPU) 已成为高质量 NVIDIA GPU 云服务的领先者&#xff0c;推动了计算技术的前沿发展。 NVIDIA 成立于 1993 年&#xff0c;由三位有远见…

软件测试需要具备的基础知识【功能测试】---后端知识(三)

​ ​ 您好&#xff0c;我是程序员小羊&#xff01; 前言 为了更好的学习软件测试的相关技能&#xff0c;需要具备一定的基础知识。需要学习的基础知识包括&#xff1a; 1、计算机基础 2、前端知识 3、后端知识 4、软件测试理论 后期分四篇文章进行编写&#xff0c;这是第三篇 …

Cycript安装报错 Library not loaded终极解决方案

一、下载安装 Cycript 官方完整 资源下载完成后&#xff0c;解压。目录如下&#xff1a; 二、执行 打开命令终端,cd到对应目录&#xff0c;然后执行./cycript #第一步&#xff1a;cd到解压的目录 cd /xx/cycrpt_0#执行&#xff1a; ./cycript 2.1、报错Library not Loaded …

oled使用 f4软件iic 数字 汉字 小图片 HAL库

基于江科大的oled标准库进行移植 到Hal库上 本人参考了许多大佬的源码 进行更改 由于F4和F1主频不一样 由于F4主频太高 在进行软件iic时需要延时一下 才可驱动oled 本人在网上找了一个开源的us延时函数 已经添加进入 文件分享 通过百度网盘分享的文件&#xff1a;delay&#…

如何自动抓取岗位数据?五种采集技巧

摘要&#xff1a; 本文将深入探讨如何从前程无忧网站自动抓取岗位信息&#xff0c;通过分享五大实用的采集技巧&#xff0c;助您轻松掌握大数据时代的招聘情报。无需编程基础&#xff0c;也能高效获取目标职位详情&#xff0c;优化人力资源管理与市场分析。 正文&#xff1a;…

电脑图片损坏打不开怎么办?能修复吗?

照片和视频是记录和保存现实生活中的事件的最好方式。由于手机储存空间有限&#xff0c;一般我们会把有纪念意义的照片放到电脑上进行保存&#xff0c;但有时难免会遇到照片被损坏打不开的情况&#xff0c;一旦遇到这种情况&#xff0c;先不要急&#xff0c;也不要因为照片打不…