good first issuekind/discussion
描述
前言
这个问题在 cloud 社区讨论视乎不太合适?但我发现 web&boot 社区并不活跃,借此占个楼。
问题描述
在企业项目开发中,spring-boot 对于 java 开发者来说已经是密不可分,但是多数中小型企业在开发 web 应用时缺乏基本的业务架构意识。据我所知,许多开发者对于 Controller 模块具备增加参数校验、全局异常捕捉以及封装模板统一响应功能的意识,少部分会使用公司内部依赖以覆盖上述功能。
即便如此,我发现这些开发者们虽然具备这样的功能意识,但是视乎也是因为要做参数校验而去做参数校验,要做异常捕捉而去做异常捕捉······导致对 Controller 模块做了许多重复工作,其架构如下图所示:

如上图所示,每个 Controller 的每个方法都需要进行重复的两步操作——参数校验和封装模板统一响应,以及对外抛出异常时基于业务认知进行异常捕捉。
- 对于参数校验,每个方法的参数不相同,难以进行统一操作,但我认为不应该耦合至方法中,理应在方法执行前进行。
- 对于封装模板统一响应,模板内容只有结果集不一样,因此可以抽出来进行统一操作,进而降低代码复杂度和开发成本。
- 对于异常捕捉,在一个全局异常监听类中,基于业务认知使用过多的 @ ExceptionHandler 注解视乎也不妥,一方面一旦业务认知扩大,就可能需要增加新的异常类,这样做并不符合开闭原则,而且会增加架构部门对上支持的耦合;另一方面,削弱了对与非预期异常(BUG)的认知,因为 BUG 可以是任何异常,并不能因为知道存在这个异常,当它抛出时它就不是BUG了。
愿景
进而我认为对 Controller 进行增强时,需要进行解耦,其架构如下图所示:

好奇促使
接着我怀着好奇之心探索存在如此做法的脚手架,但是都没有发现,随后在业余时间,我开发了一款简陋版,对比效果图如下:
普遍做法
@RestController
@RequestMapping("/contrast")
public class StudentController {
@Autowired
private StudentService studentService;
@RequestMapping("/queryStudentById/{id}")
public ResponseResult<Student> queryStudentById(@PathVariable int id) {
StudentValidator.queryStudentById(id);
Student student = studentService.getStudentById(id);
return ResponseResult.success(student);
}
@RequestMapping("/queryStudentsByAge/{age}")
public ResponseResult<List<Student>> queryStudentsByAge(@PathVariable int age) {
StudentValidator.queryStudentsByAge(age);
List<Student> students = studentService.getStudentsByAge(age);
return ResponseResult.success(students);
}
}
新的做法
Controller
@RestController
@RequestMapping("/sample")
public class StudentController {
@Autowired
private StudentService studentService;
@RequestMapping("/queryStudentById/{id}")
public Student queryStudentById(@PathVariable int id) {
return studentService.getStudentById(id);
}
@RequestMapping("/queryStudentsByAge/{age}")
public List<Student> queryStudentsByAge(@PathVariable int age) {
return studentService.getStudentsByAge(age);
}
}
Validator
@Validator("/sample")
public class StudentValidator {
@ValidateMapping("/queryStudentById")
public void queryStudentById(int id) {
if (id < 0) {
Throw.badRequest(StudentError.QUERY_BY_ID);
}
}
@ValidateMapping("/queryStudentsByAge")
public void queryStudentsByAge(int age) {
if (age < 0 || age > 150) {
Throw.badRequest(StudentError.QUERY_BY_AGE);
}
}
}
Response 统一配置
@Configuration
public class ControllerResponseConfiguration {
@Bean
public ControllerExceptionResponse controllerExceptionResponse() {
return new ControllerExceptionResponse() {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void setResponseBody(Map<String, Object> responseBody, UtilityException e, HttpServletRequest request, HttpServletResponse response) {
Throwable rootCause = NestedExceptionUtils.getRootCause(e);
logger.error(NestedExceptionUtils.buildMessage(e.getMessage(), rootCause));
responseBody.put("code", e.getCode());
responseBody.put("message", e.getMessage());
}
@Override
public void setResponseHeader(Map<String, String> responseHeader, UtilityException e, HttpServletRequest request, HttpServletResponse response) {
ControllerExceptionResponse.super.setResponseHeader(responseHeader, e, request, response);
}
};
}
@Bean
public ControllerReturnResponse controllerReturnResponse() {
return new ControllerReturnResponse() {
@Override
public void setResponseBody(Map<String, Object> responseBody, Object returnValue, ServerHttpRequest request, ServerHttpResponse response) {
responseBody.put("code", 200000);
responseBody.put("message", "success");
responseBody.put("data", returnValue);
responseBody.put("time", new Date());
}
@Override
public void setResponseHeader(Map<String, String> responseHeader, Object returnValue, ServerHttpRequest request, ServerHttpResponse response) {
ControllerReturnResponse.super.setResponseHeader(responseHeader, returnValue, request, response);
}
};
}
}
共建与普及
本人调研了本校同学与公司同事,对于此框架表示期待,借此想向阿里社区的前辈赐教:
- 是否有必要这么做?如有,是否有意往此方向开源共建?
- 是否能够带来一定的效益?
- 是否接受使用?