springboot中controller层代码优雅写法

本文探讨了Spring框架中Controller层的职责和常见问题,提出了优雅的解决方案。包括统一返回结构以增强接口友好性,使用ResponseBodyAdvice进行响应处理的统一,利用JSR303进行参数校验,以及自定义异常和全局异常处理,以提升代码的整洁性和可维护性。

在基于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的校验规则,可以明确返回的结构,包括异常情况。

<think> 我们正在讨论的是在Spring Boot中判断一个List为空的最优方案。根据之前的上下文,我们可能是在处理MyBatis批量操作时遇到空List导致SQL语句为空的问题,因此需要判断List是否为空。但用户现在的问题更一般化:在Spring Boot中如何判断List为空的最优方案。 注意:这个问题并不局限于MyBatis,而是Spring Boot应用中任何地方判断List为空的情况。 判断List为空通常有两个维度: 1. List对象是否为null(即没有被初始化) 2. List是否没有任何元素(即size为0) 在Java中,最优方案通常包括: - 使用工具类(如Spring Framework提供的`CollectionUtils`) - 使用Java原生方法(如`isEmpty()`) 具体方案: 方案1:使用Java原生方法(适用于非null的List) if (list != null && list.isEmpty()) { ... } 方案2:使用Spring Framework的CollectionUtils(可以同时检查null和空) if (CollectionUtils.isEmpty(list)) { ... } 方案3:使用Apache Commons Collections(如果项目中有依赖) if (CollectionUtils.isEmpty(list)) { ... } // 注意:这个CollectionUtils是org.apache.commons.collections4包下的 但是,在Spring Boot项目中,我们通常已经集成了Spring Framework,因此使用Spring自带的`CollectionUtils`是最为方便和推荐的方式。 具体实现: 1. 引入Spring Framework的core包(在Spring Boot中默认已经引入): 无需额外引入,因为spring-boot-starter-web或spring-boot-starter已经包含。 2. 使用示例: 在代码中: import org.springframework.util.CollectionUtils; if (CollectionUtils.isEmpty(list)) { // 处理为空的情况 } 3. 优点: - 同时检查null和空集合 - 代码简洁,避免空指针异常 - 是Spring生态的一部分,与Spring Boot无缝集成 4. 注意:如果List可能是null,那么直接调用list.isEmpty()会抛出NullPointerException,因此使用工具类更安全。 5. 其他方案:使用Java 8的Optional(但通常用于返回,不用于简单判断) Optional.ofNullable(list).orElse(Collections.emptyList()).isEmpty() 这种方式也可以,但不如直接使用CollectionUtils简洁。 因此,在Spring Boot项目中,推荐使用Spring Framework的`CollectionUtils.isEmpty()`方法。 另外,根据引用[1]提到的XML配置,虽然问题不直接相关,但要注意在XML配置中我们无法使用这样的工具类,所以XML中通常需要条件判断(如之前的MyBatis动态SQL中的判断)。 生成相关问题: 1. 在Spring Boot中如何安全地遍历一个可能为空的List? 2. 使用CollectionUtils.isEmpty()和Apache Commons的CollectionUtils.isEmpty()有什么区别? 3. 在Spring Boot的配置文件(如YAML)中如何配置一个空的List? 4. 如何统一处理Spring Boot控制器中返回空List的响应? 回答结构: 1. 问题分析:解释判断List为空的需求和常见问题(空指针) 2. 推荐方案:使用Spring Framework的CollectionUtils.isEmpty() 3. 代码示例 4. 注意事项 5. 相关问题(按格式) </think> ### Spring Boot中判断List为空的最优方案 在Spring Boot中判断List是否为空,推荐采用以下两种高效方案(根据场景选择): #### ✅ 方案1:Spring工具类(推荐) ```java import org.springframework.util.CollectionUtils; public void processList(List<?> data) { if (CollectionUtils.isEmpty(data)) { // 处理空集合逻辑 return; } // 正常处理逻辑 } ``` **优势**: 1. 同时检查`null`和空集合(size=0) 2. 避免空指针异常(NullPointerException) 3. Spring原生支持,无需额外依赖[^1] 4. 可读性强,语义清晰 #### ✅ 方案2:Java原生+Optional(简洁) ```java public void processList(List<?> data) { if (data == null || data.isEmpty()) { // 处理空集合逻辑 return; } // 或使用Optional Optional.ofNullable(data) .filter(list -> !list.isEmpty()) .orElseGet(() -> { // 空集合处理 return Collections.emptyList(); }); } ``` **适用场景**: - JDK 8+ 环境 - 需要链式处理时 - 需要返回默认空集合时 --- ### 💡 性能对比 | 方案 | 可读性 | 安全性 | 扩展性 | 适用场景 | |-------------------|--------|--------|--------|------------------| | `CollectionUtils` | ★★★ | ★★★ | ★★☆ | 通用业务逻辑 | | `Optional` | ★★☆ | ★★★ | ★★★ | 流式处理/函数式 | | `data.isEmpty()` | ★★☆ | ★★☆ | ★★☆ | 明确非null集合 | > **关键原则**: > 在Controller和Service**优先使用`CollectionUtils.isEmpty()`**,DAO可酌情使用原生判断。 --- ### 🚫 避免的常见错误 ```java // 反例1:遗漏null检查(可能NPE) if (data.isEmpty()) { ... } // 反例2:错误判空(size=0时仍执行) if (data != null) { ... } // 反例3:低效写法 if (data == null || data.size() == 0) { ... } ``` --- ### 🔍 相关问题 1. 如何在Spring Boot中安全遍历可能为空的List集合? 2. MyBatis如何处理批量操作时的空List问题?[^1] 3. Spring MVC如何优雅返回空集合的JSON响应? 4. 使用`Optional`处理集合时有哪些最佳实践? 5. 分布式环境下空集合判断有哪些特殊注意事项?[^2] --- 通过合理选用`CollectionUtils.isEmpty()`,可确保代码健壮性同时保持Spring Boot生态一致性。在涉及数据库操作时(如MyBatis批处理),务必结合集合空检查和业务过滤(参考[^1]的XML防护策略)。 [^1]: XML配置场景仍需显式空检查 [^2]: 分布式缓存需考虑空集合的特殊序列化
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值