从阿里跳槽来的工程师,写个Controller都这么优雅
目录一个优秀的Controller层逻辑从现状看问题改造 Controller 层逻辑总结
一个优秀的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 层逻辑
统一返回结构
统一返回值类型无论项目前后端是否分离都是非常必要的,方便对接接口的开发人员更加清晰地知道这个接口的调用是否成功(不能仅仅简单地看返回值是否为 就判断成功与否,因为有些接口的设计就是如此),使用一个状态码、状态信息就能清楚地了解接口调用情况//定义返回数据结构
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 {
private Integer code;
private String message;
private T data;
public static Result success(T data) {
return new Result<>(ResultEnum.SUCCESS.getCode, ResultEnum.SUCCESS.getMessage, data);
}
public static Result 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, );
}
public static Result<?> failed(String message) {
return new Result<>(ResultEnum.COMMON_FAILED.getCode, message, );
}
public static Result<?> failed(IResult errorResult) {
return new Result<>(errorResult.getCode, errorResult.getMessage, );
}
public static Result instance(Integer code, String message, T data) {
Result result = new Result<>;
result.setCode(code);
result.setMessage(message);
result.setData(data);
return result;
}
}
统一返回结构后,在 Controller 中就可以使用了,但是每一个 Controller 都写这么一段最终封装的逻辑,这些都是很重复的工作,所以还要继续想办法进一步处理统一返回结构
统一包装处理
Spring 中提供了一个类 ResponseBodyAdvice
,能帮助我们实现上述需求
ResponseBodyAdvice是对 Controller 返回的内容在HttpMessageConverter进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。那这样就可以把统一包装的工作放到这个类里面。public interface ResponseBodyAdvice {
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
@able
T beforeBodyWrite(@able T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);
}
supports:判断是否要交给 beforeBodyWrite 方法执行,ture:需要;false:不需要
beforeBodyWrite:对 response 进行具体的处理// 如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice