SpringBoot中controller深层详细讲解

 更新时间:2023年02月02日 16:39:03   作者:ldcaws  
这篇文章主要介绍了SpringBoot在Controller层接收参数的常用方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

在基于spring框架的项目开发中,必然会遇到controller层,它可以很方便的对外提供数据接口服务,也是非常关键的出口,所以非常有必要进行规范统一,使其既简洁又优雅。

controller层的职责为负责接收和响应请求,一般不负责具体的逻辑业务的实现。controller主要工作如下:

  • 接收请求并解析参数;
  • 调用service层执行具体的业务逻辑(可能包含参数校验);
  • 捕获业务异常做出反馈;
  • 业务逻辑执行成功做出响应;

目前controller层代码会存在的问题:

  • 参数校验过多地耦合了业务代码,违背了单一职责原则;
  • 可能在多个业务逻辑中抛出同一个异常,导致代码重复;
  • 各种异常反馈和成功响应格式不统一,接口对接不友好;

优雅写法一:统一返回结构

统一返回值类型,无论项目前后端是否分离都是非常必要的,方便对接接口的前端开发人员更加清晰地知道这个接口的调用是否成功,不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此。

统一返回结构,通过状态码就能清楚的知道接口的调用情况:

@Data
public class ResponseData<T> {
    private Boolean status = true;
    private int code = 200;
    private String message;
    private T data;
    public static ResponseData ok(Object data) {
        return new ResponseData(data);
    }
    public static ResponseData ok(Object data,String message) {
        return new ResponseData(data,message);
    }
    public static ResponseData fail(String message,int code) {
        ResponseData responseData= new ResponseData();
        responseData.setCode(code);
        responseData.setMessage(message);
        responseData.setStatus(false);
        responseData.setData(null);
        return responseData;
    }
    public ResponseData() {
        super();
    }
    public ResponseData(T data) {
        super();
        this.data = data;
    }
    public ResponseData(T data,String message) {
        super();
        this.data = data;
        this.message=message;
    }
}
@AllArgsConstructor
@Data
public enum ResponseCode {
    SYS_FAIL(1, "操作失败"),
    SYS_SUCESS(200, "操作成功"),
    SYSTEM_ERROR_CODE_403(403, "权限不足"),
    SYSTEM_ERROR_CODE_404(404, "未找到请求资源"),
	;
	private int code;
    private String msg;
}

统一返回结构后,就可以在controller中使用了,但是每个controller都这么写,都是很重复的工作,所以还可以继续想办法处理统一返回结构。

优雅写法二:统一包装处理

Spring 中提供了一个类 ResponseBodyAdvice ,能帮助我们实现上述需求:

ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。这样就可以把统一包装处理的工作放到这个类里面,其中supports判断是否要交给beforeBodyWrite 方法执行,true为需要,false为不需要,beforeBodyWrite 是对response的具体处理。

@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 如果不需要进行封装的,可以添加一些校验手段,比如添加标记排除的注解
        return true;
    }
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 提供一定的灵活度,如果body已经被包装了,就不进行包装
        if (body instanceof Result) {
            return body;
        }
        return Result.success(body);
    }
}

这样即能实现对controller返回的数据进行统一,又不需要对原有代码进行大量的改动了。

优雅写法三:参数校验

Java API 的规范 JSR303 定义了校验的标准 validation-api ,其中一个比较出名的实现是 hibernate validation。

@PathVariable 和 @RequestParam 参数校验:get请求的参数接收一般依赖这两个注解,但是处于 url 有长度限制和代码的可维护性,超过 5 个参数尽量用实体来传参;

对 @PathVariable 和 @RequestParam 参数进行校验需要在入参处声明约束的注解,如果校验失败,会抛出 MethodArgumentNotValidException 异常。

@RestController
@RequestMapping("/test")
public class TestController {
    private TestService testService;
	@Autowired
    public void setTestService(TestService prettyTestService) {
        this.testService = prettyTestService;
    }
    @GetMapping("/{num}")
    public Integer num(@PathVariable("num") @Min(1) @Max(20) Integer num) {
        return num * num;
    }
    @GetMapping("/email")
    public String email(@RequestParam @NotBlank @Email String email) {
        return email;
    }
}

@RequestBody 参数校验:post和put 请求的参数推荐使用 @RequestBody 请求体参数;

对 @RequestBody 参数进行校验需要在 DTO 对象中加入校验条件后,再搭配 @Validated 即可完成自动校验。如果校验失败,会抛出 ConstraintViolationException 异常。

@Data
public class TestDTO {
    @NotBlank
    private String userName;
    @NotBlank
    @Length(min = 6, max = 20)
    private String password;
    @NotNull
    @Email
    private String email;
}
@RestController
@RequestMapping("/test")
public class TestController {
    private TestService testService;
	@Autowired
    public void setTestService(TestService testService) {
        this.testService = testService;
    }
    @PostMapping("/testValidation")
    public void testValidation(@RequestBody @Validated TestDTO testDTO) {
        this.testService.save(testDTO);
    }
}

自定义校验规则:有些时候 JSR303 标准中提供的校验规则不满足复杂的业务需求,也可以自定义校验规则;

优雅写法四:自定义异常与统一拦截异常

原来抛出的异常会有如下问题:

  • 抛出的异常不够具体,只是简单地把错误信息放到了 Exception 中;
  • 抛出异常后,Controller 不能具体地根据异常做出反馈;
  • 虽然做了参数自动校验,但是异常返回结构和正常返回结构不一致;

自定义异常是为了后面统一拦截异常时,对业务中的异常有更加细颗粒度的区分,拦截时针对不同的异常作出不同的响应。

统一拦截异常的是为了可以与前面定义下来的统一包装返回结构能对应上,还有就是希望无论系统发生什么异常,Http 的状态码都要是 200 ,尽可能由业务来区分系统的异常。

//自定义异常
public class ForbiddenException extends RuntimeException {
    public ForbiddenException(String message) {
        super(message);
    }
}
//自定义异常
public class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
}
//统一拦截异常
@RestControllerAdvice(basePackages = "com.example.demo")
public class ExceptionAdvice {
    /**
     * 捕获 {@code BusinessException} 异常
     */
    @ExceptionHandler({BusinessException.class})
    public Result<?> handleBusinessException(BusinessException ex) {
        return Result.failed(ex.getMessage());
    }
    /**
     * 捕获 {@code ForbiddenException} 异常
     */
    @ExceptionHandler({ForbiddenException.class})
    public Result<?> handleForbiddenException(ForbiddenException ex) {
        return Result.failed(ResultEnum.FORBIDDEN);
    }
    /**
     * {@code @RequestBody} 参数校验不通过时抛出的异常处理
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        if (StringUtils.hasText(msg)) {
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED);
    }
    /**
     * {@code @PathVariable} 和 {@code @RequestParam} 参数校验不通过时抛出的异常处理
     */
    @ExceptionHandler({ConstraintViolationException.class})
    public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
        if (StringUtils.hasText(ex.getMessage())) {
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED);
    }
    /**
     * 顶级异常捕获并统一处理,当其他异常无法处理时候选择使用
     */
    @ExceptionHandler({Exception.class})
    public Result<?> handle(Exception ex) {
        return Result.failed(ex.getMessage());
    }
}

通过上述写法,可以发现 Controller 的代码变得非常简洁优雅,可以清楚知道每个参数、每个DTO的校验规则,可以明确返回的结构,包括异常情况。

到此这篇关于SpringBoot中controller深层详细讲解的文章就介绍到这了,更多相关SpringBoot controller内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • ShardingSphere结合MySQL实现分库分表的项目实践

    ShardingSphere结合MySQL实现分库分表的项目实践

    在实际开发中,如果表的数据过大我们需要把一张表拆分成多张表,本文主要介绍了使用ShardingSphere实现MySQL分库分表,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03
  • 在controller中如何设置接收参数的默认值

    在controller中如何设置接收参数的默认值

    这篇文章主要介绍了在controller中如何设置接收参数的默认值,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • SpringBoot深入探究四种静态资源访问的方式

    SpringBoot深入探究四种静态资源访问的方式

    这一节详细的学习一下SpringBoot的静态资源访问相关的知识点。像这样的知识点还挺多,比如SpringBoot2的Junit单元测试等等。本章我们来了解静态资源访问的四种方式
    2022-05-05
  • Java中缀表达式转后缀表达式实现方法详解

    Java中缀表达式转后缀表达式实现方法详解

    这篇文章主要介绍了Java中缀表达式转后缀表达式实现方法,结合实例形式分析了Java中缀表达式转换成后缀表达式的相关算法原理与具体实现技巧,需要的朋友可以参考下
    2019-03-03
  • Java 数组迭代你会用吗

    Java 数组迭代你会用吗

    Java 数组是我们学习或工作中常用到的数据结构,我们会经常写数组迭代的代码,本文介绍三种数组迭代的方式,感兴趣的可以了解一下
    2021-09-09
  • Spring Security中自定义cors配置及原理解析

    Spring Security中自定义cors配置及原理解析

    在Spring框架中,通过自定义CORS配置可根据实际情况调整URL的协议、主机、端口等,以适应"同源安全策略",配置原理涉及CorsConfigurer和CorsFilter,自定义配置需要注意@Configuration注解、方法名以及可能的@Autowired注解
    2024-10-10
  • Jmeter后置处理器实现过程及方法应用

    Jmeter后置处理器实现过程及方法应用

    这篇文章主要介绍了Jmeter后置处理器实现过程及方法应用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-09-09
  • JDBC中Fetchsize的实现

    JDBC中Fetchsize的实现

    fetchsize是指在执行数据库查询时,每次从数据库中获取的记录条数,它对内存使用和网络传输效率有重要影响,在MyBatis中,可以通过全局设置或语句级别设置fetchsize,来控制查询操作的内存使用和提升性能,合理的fetchsize设置能有效减少网络往返次数和防止内存溢出
    2024-09-09
  • Java中Volatile关键字能保证原子性吗

    Java中Volatile关键字能保证原子性吗

    这篇文章主要介绍了Java中Volatile关键字能保证原子性吗,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-08-08
  • Java编程常见内存溢出异常与代码示例

    Java编程常见内存溢出异常与代码示例

    这篇文章主要介绍了Java编程常见内存溢出异常与代码示例,具有一定参考价值,需要的朋友可以了解下。
    2017-11-11

最新评论