alibaba / alibaba/spring-cloud-alibaba

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

Closed
#2,847 9 comments 0 reactions 0 assignees View on GitHub
good first issue kind/discussion
Dominant language
Java
Stars
29.2k
Forks
8.5k
Avg merge
1d 23h
Merged PRs (30d)
12

Description

## 前言
这个问题在 cloud 社区讨论视乎不太合适?但我发现 web&boot 社区并不活跃,借此占个楼。

## 问题描述
在企业项目开发中,spring-boot 对于 java 开发者来说已经是密不可分,但是多数中小型企业在开发 web 应用时缺乏基本的业务架构意识。据我所知,许多开发者对于 Controller 模块具备增加参数校验、全局异常捕捉以及封装模板统一响应功能的意识,少部分会使用公司内部依赖以覆盖上述功能。

即便如此,我发现这些开发者们虽然具备这样的功能意识,但是视乎也是因为要做参数校验而去做参数校验,要做异常捕捉而去做异常捕捉······导致对 Controller 模块做了许多重复工作,其架构如下图所示:
![普遍做法](https://user-images.githubusercontent.com/69103691/196387367-70ab8327-324a-489a-a22e-03ae163d5a5e.png)

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

## 愿景
进而我认为对 Controller 进行增强时,需要进行解耦,其架构如下图所示:
![期望做法](https://user-images.githubusercontent.com/69103691/196387038-921e0c71-a6c2-4589-9d70-4627b1842d1e.png)

## 好奇促使
接着我怀着好奇之心探索存在如此做法的脚手架,但是都没有发现,随后在业余时间,我开发了一款简陋版,对比效果图如下:
### 普遍做法
```java
@RestController
@RequestMapping("/contrast")
public class StudentController {

@Autowired
private StudentService studentService;

@RequestMapping("/queryStudentById/{id}")
public ResponseResult queryStudentById(@PathVariable int id) {
StudentValidator.queryStudentById(id);
Student student = studentService.getStudentById(id);
return ResponseResult.success(student);
}

@RequestMapping("/queryStudentsByAge/{age}")
public ResponseResult> queryStudentsByAge(@PathVariable int age) {
StudentValidator.queryStudentsByAge(age);
List students = studentService.getStudentsByAge(age);
return ResponseResult.success(students);
}

}
```

### 新的做法
#### Controller
```java
@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 queryStudentsByAge(@PathVariable int age) {
return studentService.getStudentsByAge(age);
}

}
```
#### Validator
```java
@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 统一配置
```java
@Configuration
public class ControllerResponseConfiguration {

@Bean
public ControllerExceptionResponse controllerExceptionResponse() {
return new ControllerExceptionResponse() {

private final Logger logger = LoggerFactory.getLogger(getClass());

@Override
public void setResponseBody(Map 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 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 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 responseHeader, Object returnValue, ServerHttpRequest request, ServerHttpResponse response) {
ControllerReturnResponse.super.setResponseHeader(responseHeader, returnValue, request, response);
}
};
}

}
```

## 共建与普及
本人调研了本校同学与公司同事,对于此框架表示期待,借此想向阿里社区的前辈赐教:
- 是否有必要这么做?如有,是否有意往此方向开源共建?
- 是否能够带来一定的效益?
- 是否接受使用?

Contributor guide

No contributing guide indexed for this repository

Research direction

The issue names no repository files, tests, or concrete entry point. Start by reviewing the Spring MVC controller, validator, and response-configuration examples in the description, then confirm project scope and acceptance criteria with maintainers; done is not defined until the proposed decoupling and supported behavior are agreed.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring, spring-boot
Domain
backend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.