Controller层代码这么写,简洁又优雅

一个优秀的Controller层逻辑

说到 Controller,相信大家都不陌生,它可以很方便地对外提供数据接口 。它的定位,我认为是「不可或缺的配角」,说它不可或缺是因为无论是传统的三层架构还是现在的COLA架构,Controller 层依旧有一席之地,说明他的必要性;说它是配角是因为 Controller 层的代码一般是不负责具体的逻辑业务逻辑实现,但是它负责接收和响应请求
从现状看问题Controller 主要的工作有以下几项
  • 接收请求并解析参数
  • 调用 Service 执行具体的业务代码(可能包含参数校验)
  • 捕获业务逻辑异常做出反馈
  • 业务逻辑执行成功做出响应
//DTO@Datapublic class TestDTO {private Integer num;private String type;}//Service@Servicepublic 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@RestControllerpublic 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);}}@Autowiredpublic DTOid setTestService(TestService testService) {this.testService = testService;}}如果真的按照上面所列的工作项来开发 Controller 代码会有几个问题
  • 参数校验过多地耦合了业务代码,违背单一职责原则
  • 可能在多个业务中都抛出同一个异常,导致代码重复
  • 各种异常反馈和成功响应格式不统一,接口对接不友好
改造 Controller 层逻辑统一返回结构统一返回值类型无论项目前后端是否分离都是非常必要的,方便对接接口的开发人员更加清晰地知道这个接口的调用是否成功(不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此),使用一个状态码、状态信息就能清楚地了解接口调用情况
//定义返回数据结构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@AllArgsConstructorpublic 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 ,能帮助我们实现上述需求
ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端 。那这样就可以把统一包装的工作放到这个类里面 。
public interface ResponseBodyAdvice<T> {boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);@NullableT beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);}


推荐阅读