alibaba/spring-cloud-alibaba

是否考虑对 MVC 进行增强并解耦?

Open

#2,847 opened on Oct 18, 2022

View on GitHub
 (8 comments) (0 reactions) (0 assignees)Java (29,106 stars) (8,513 forks)batch import
good first issuekind/discussion

Description

前言

这个问题在 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);
            }
        };
    }

}

共建与普及

本人调研了本校同学与公司同事,对于此框架表示期待,借此想向阿里社区的前辈赐教:

  • 是否有必要这么做?如有,是否有意往此方向开源共建?
  • 是否能够带来一定的效益?
  • 是否接受使用?

Contributor guide

是否考虑对 MVC 进行增强并解耦? · alibaba/spring-cloud-alibaba#2847 | Good First Issue