来源:https://juejin.cn/post/7123091045071454238
一个优秀的 Controller 层逻辑
说到 Controller,相信大家都不陌生,它可以很方便地对外提供数据接口。它的定位,我认为是「不可或缺的配角」。
说它不可或缺是因为无论是传统的三层架构还是现在的 COLA 架构,Controller 层依旧有一席之地,说明他的必要性。
说它是配角是因为 Controller 层的代码一般是不负责具体的逻辑业务逻辑实现,但是它负责接收和响应请求。
从现状看问题
Controller 主要的工作有以下几项:
- 接收请求并解析参数
- 调用 Service 执行具体的业务代码(可能包含参数校验)
- 捕获业务逻辑异常做出反馈
- 业务逻辑执行成功做出响应
//DTO @Data public class TestDTO { private Integer num; private String type; } //Service @Service public class TestService { public Double service(TestDTO testDTO) throws Exception { if (testDTO.getNum() <= 0) { throw new Exception("输入的数字需要大于0"); } if (testDTO.getType().equals("square")) { return Math.pow(testDTO.getNum(), 2); } if (testDTO.getType().equals("factorial")) { double result = 1; int num = testDTO.getNum(); while (num > 1) { result = result * num; num -= 1; } return result; } throw new Exception("未识别的算法"); } } //Controller @RestController public class TestController { private TestService testService; @PostMapping("/test") public Double test(@RequestBody TestDTO testDTO) { try { Double result = this.testService.service(testDTO); return result; } catch (Exception e) { throw new RuntimeException(e); } } @Autowired public DTOid setTestService(TestService testService) { this.testService = testService; } }
- 如果真的按照上面所列的工作项来开发 Controller 代码会有几个问题:
参数校验过多地耦合了业务代码,违背单一职责原则
可能在多个业务中都抛出同一个异常,导致代码重复
各种异常反馈和成功响应格式不统一,接口对接不友好
改造 Controller 层逻辑
统一返回结构
统一返回值类型无论项目前后端是否分离都是非常必要的,方便对接接口的开发人员更加清晰地知道这个接口的调用是否成功(不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此)。
推荐一个开源免费的 Spring Boot 最全教程:
https://github.com/javastacks/spring-boot-best-practice
使用一个状态码、状态信息就能清楚地了解接口调用情况:
//定义返回数据结构 public interface IResult { Integer getCode(); String getMessage(); } //常用结果的枚举 public enum ResultEnum implements IResult { SUCCESS(2001, "接口调用成功"), VALIDATE_FAILED(2002, "参数校验失败"), COMMON_FAILED(2003, "接口调用失败"), FORBIDDEN(2004, "没有权限访问资源"); private Integer code; private String message; //省略get、set方法和构造方法 } //统一返回数据结构 @Data @NoArgsConstructor @AllArgsConstructor public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data); } public static <T> Result<T> success(String message, T data) { return new Result<>(ResultEnum.SUCCESS.getCode(), message, data); } public static Result<?> failed() { return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null); } public static Result<?> failed(String message) { return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null); } public static Result<?> failed(IResult errorResult) { return new Result<>(errorResult.getCode(), errorResult.getMessage(), null); } public static <T> Result<T> instance(Integer code, String message, T data) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); result.setData(data); return result; } }
统一返回结构后,在 Controller 中就可以使用了,但是每一个 Controller 都写这么一段最终封装的逻辑,这些都是很重复的工作,所以还要继续想办法进一步处理统一返回结构。
统一包装处理
Spring 中提供了一个类 ResponseBodyAdvice ,能帮助我们实现上述需求:
public interface ResponseBodyAdvice<T> { boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType); @Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response); }
ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。
那这样就可以把统一包装的工作放到这个类里面:
supports: 判断是否要交给 beforeBodyWrite 方法执行,ture:需要;false:不需要
beforeBodyWrite: 对 response 进行具体的处理
// 如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成 @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 返回的数据进行统一包装,又不需要对原有代码进行大量的改动。