Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
RovingSea opened this issue Oct 18, 2022 · 8 comments
Open

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

RovingSea opened this issue Oct 18, 2022 · 8 comments
Labels
good first issue kind/discussion Mark as discussion issues/pr

Comments

@RovingSea
Copy link

RovingSea commented Oct 18, 2022

前言

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

}

共建与普及

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

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

Good idea! At present, I also have some thoughts in this regard. The code written in this way is clearer, rather than mixing exceptions and business together. Better decoupling between codes.

@HaojunRen
Copy link
Collaborator

想法特别不错,可作为快速开发业务系统的模板,如果能搞出来也是大功一件,但是每个公司都众口难调,特别像异常控制需求各异,怎么收敛起来,其实,存在难度的

@RovingSea
Copy link
Author

想法特别不错,可作为快速开发业务系统的模板,如果能搞出来也是大功一件,但是每个公司都众口难调,特别像异常控制需求各异,怎么收敛起来,其实,存在难度的

因此我的观点是让工程师秉持预期和非预期的思想进行抛出异常,预期就是工程师手动抛出来的;非预期就是意料之外发生的。这样就避免了流水线似的用 try / catch 层层封装。就像生活中丢垃圾,我们只用关心垃圾的种类,而不需要在垃圾上还写上相关的信息。

@HaojunRen HaojunRen added good first issue kind/discussion Mark as discussion issues/pr labels Oct 18, 2022
@RovingSea
Copy link
Author

如果可以尝试,请问我能认领该任务吗?

@galaxy-sea
Copy link
Contributor

galaxy-sea commented Oct 20, 2022

@RovingSea

  • Response 统一配置
    Spring Boot 提供了全局异常拦截器(org.springframework.web.bind.annotation.ExceptionHandler)
    Spring Boot 提供了定制序列化org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice和org.springframework.http.converter.HttpMessageConverter

  • Validator
    hibernate-validator无需关注是否是Spring MVC都可以使用。

Spring Boot 无侵入式 实现API接口统一JSON格式返回
SpringBoot无侵入式API接口统一格式返回,在Spring Cloud OpenFeign 继承模式具有了侵入性

@RovingSea
Copy link
Author

RovingSea commented Oct 20, 2022

@RovingSea

  • Response 统一配置
    Spring Boot 提供了全局异常拦截器(org.springframework.web.bind.annotation.ExceptionHandler)
    Spring Boot 提供了定制序列化org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice和org.springframework.http.converter.HttpMessageConverter
  • Validator
    hibernate-validator无需关注是否是Spring MVC都可以使用。

Spring Boot 无侵入式 实现API接口统一JSON格式返回 SpringBoot无侵入式API接口统一格式返回,在Spring Cloud OpenFeign 继承模式具有了侵入性

感谢答复,关于该篇 Issue 中提到的功能我都在个人仓库 utility-framework 实现并做到了解耦。
至于 hibernate-validator 我也有所了解,但最终没有使用的原因是范围太大,灵活性不强,具备侵入性,以及本身对上下层的强依赖导致不方便调度异常时做定制化处理(类似于 lombok ),而使用 AOP 接入灵活性会更强,耦合也会大幅度降低。

@galaxy-sea
Copy link
Contributor

@RovingSea

  • Response 统一配置
    Spring Boot 提供了全局异常拦截器(org.springframework.web.bind.annotation.ExceptionHandler)
    Spring Boot 提供了定制序列化org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice和org.springframework.http.converter.HttpMessageConverter
  • Validator
    hibernate-validator无需关注是否是Spring MVC都可以使用。

Spring Boot 无侵入式 实现API接口统一JSON格式返回 SpringBoot无侵入式API接口统一格式返回,在Spring Cloud OpenFeign 继承模式具有了侵入性

感谢答复,关于该篇 Issue 中提到的功能我都在个人仓库 Utility-framework-springboot 实现并做到了解耦。 至于 hibernate-validator 我也有所了解,但最终没有使用的原因是范围太大,灵活性不强,具备侵入性,以及本身对上下层的强依赖导致不方便调度异常时做定制化处理(类似于 lombok ),而使用 AOP 接入灵活性会更强,耦合也会大幅度降低。

hello @RovingSea , 不可行的,你的全局拦截器会让项目崩溃的,你尝试一下Controller方法返回 Output试一下,世界的大门会为你打开的。

@RovingSea
Copy link
Author

@RovingSea

  • Response 统一配置
    Spring Boot 提供了全局异常拦截器(org.springframework.web.bind.annotation.ExceptionHandler)
    Spring Boot 提供了定制序列化org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice和org.springframework.http.converter.HttpMessageConverter
  • Validator
    hibernate-validator无需关注是否是Spring MVC都可以使用。

Spring Boot 无侵入式 实现API接口统一JSON格式返回 SpringBoot无侵入式API接口统一格式返回,在Spring Cloud OpenFeign 继承模式具有了侵入性

感谢答复,关于该篇 Issue 中提到的功能我都在个人仓库 Utility-framework-springboot 实现并做到了解耦。 至于 hibernate-validator 我也有所了解,但最终没有使用的原因是范围太大,灵活性不强,具备侵入性,以及本身对上下层的强依赖导致不方便调度异常时做定制化处理(类似于 lombok ),而使用 AOP 接入灵活性会更强,耦合也会大幅度降低。

hello @RovingSea , 不可行的,你的全局拦截器会让项目崩溃的,你尝试一下Controller方法返回 Output试一下,世界的大门会为你打开的。

感谢提问,再此是为了提出上述架构,为了避免刷屏,欢迎您来我的项目提Issue和PR。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
good first issue kind/discussion Mark as discussion issues/pr
Projects
None yet
Development

No branches or pull requests

4 participants