作者归档:Rick

sharp-formflow中自定义表单

自定义表单

自定义表单顾名思义就是由用户定义表单,由用户觉定标签和组件,决定要填写什么格式的数据。

创建表单

方式一

创建组件

http://localhost:8080/form/configurers

[{
    "label": "姓名",
    "cpnType": "TEXT",
    "validators": [{
        "min": 0,
        "max": 16,
        "message": "长度范围 0 - 16 个字符",
        "validatorType": "LENGTH"
    }, {
        "required": true,
        "message": "必填项",
        "validatorType": "REQUIRED"
    }],
    "defaultValue": "Rick",
    "placeholder": "请输入姓名"
}, {
    "label": "年龄",
    "cpnType": "NUMBER_TEXT",
    "validators": [{
        "min": 18,
        "max": 100,
        "message": "大小范围是18 - 100",
        "validatorType": "SIZE"
    }, {
        "required": true,
        "message": "必填项",
        "validatorType": "REQUIRED"
    }],
    "defaultValue": "18",
    "placeholder": "请输入年龄"
}, {
    "label": "兴趣爱好(单选)",
    "cpnType": "SELECT",
    "validators": [{
        "required": true,
        "message": "必填项",
        "validatorType": "REQUIRED"
    }],
    "options": ["足球", "篮球", "乒乓球", "羽毛球"],
    "defaultValue": "足球",
    "placeholder": "请输入兴趣爱好"
}, {
    "label": "信息收集",
    "cpnType": "TABLE",
    "validators": [],
    "placeholder": "请输入信息收集",
    "additionalInfo": {
        "labels": ["姓名", "年龄"]
    }
}, {
    "label": "兴趣爱好(多选)",
    "cpnType": "CHECKBOX",
    "validators": [],
    "options": ["足球", "篮球", "乒乓球", "羽毛球"],
    "defaultValue": "[\"足球\", \"篮球\"]",
    "placeholder": "请输入兴趣爱好"
}]

创建表单

http://localhost:8080/form/configurers

{
    "name": "人员信息登记表"
}

表单关联组件

http://localhost:8080/forms/488683635382583296/configs

["488675379486556160","488675379490750464","488675379490750465","488675379490750466","488675379494944768"]

方式二

创建表单和组件并关联

{
    "form": {
        "name": "我的第N个表单"
    },
    "configs": [{
        "label": "姓名",
        "cpnType": "TEXT",
        "validators": [{
            "min": 0,
            "max": 16,
            "message": "长度范围 0 - 16 个字符",
            "validatorType": "LENGTH"
        }, {
            "required": true,
            "message": "必填项",
            "validatorType": "REQUIRED"
        }],
        "defaultValue": "Rick",
        "placeholder": "请输入姓名"
    }, {
        "label": "年龄",
        "cpnType": "NUMBER_TEXT",
        "validators": [{
            "min": 18,
            "max": 100,
            "message": "大小范围是18 - 100",
            "validatorType": "SIZE"
        }, {
            "required": true,
            "message": "必填项",
            "validatorType": "REQUIRED"
        }],
        "defaultValue": "18",
        "placeholder": "请输入年龄"
    }, {
        "label": "兴趣爱好(单选)",
        "cpnType": "SELECT",
        "validators": [{
            "required": true,
            "message": "必填项",
            "validatorType": "REQUIRED"
        }],
        "options": ["足球", "篮球", "乒乓球", "羽毛球"],
        "defaultValue": "足球",
        "placeholder": "请输入兴趣爱好"
    }, {
        "label": "信息收集",
        "cpnType": "TABLE",
        "validators": [],
        "placeholder": "请输入信息收集",
        "additionalInfo": {
            "labels": ["姓名", "年龄"]
        }
    }, {
        "label": "兴趣爱好(多选)",
        "cpnType": "CHECKBOX",
        "validators": [],
        "options": ["足球", "篮球", "乒乓球", "羽毛球"],
        "defaultValue": "[\"足球\", \"篮球\"]",
        "placeholder": "请输入兴趣爱好"
    }]
}

查看表单

http://localhost:8080/forms/ajax/488686638231617536

{
    "form": {
        "id": "488686638231617536",
        "name": "我的第N个表单"
    },
    "instanceId": null,
    "propertyList": [
        {
            "id": 488686640483958784,
            "name": "ZghRwhvcGu",
            "configurer": {
                "id": "488686639036923904",
                "label": "姓名",
                "cpnType": "TEXT",
                "validators": [
                    {
                        "min": 0,
                        "max": 16,
                        "message": "长度范围 0 - 16 个字符",
                        "validatorType": "LENGTH"
                    },
                    {
                        "required": true,
                        "message": "必填项",
                        "validatorType": "REQUIRED"
                    }
                ],
                "options": null,
                "defaultValue": "Rick",
                "placeholder": "请输入姓名",
                "additionalInfo": null
            },
            "value": "Rick",
            "validatorProperies": {
                "Required.required": true,
                "Length.min": 0,
                "Length.max": 16
            }
        },
        {
            "id": 488686640483958785,
            "name": "VnmnWkziwe",
            "configurer": {
                "id": "488686639036923905",
                "label": "年龄",
                "cpnType": "NUMBER_TEXT",
                "validators": [
                    {
                        "min": 18,
                        "max": 100,
                        "message": "大小范围是18 - 100",
                        "validatorType": "SIZE"
                    },
                    {
                        "required": true,
                        "message": "必填项",
                        "validatorType": "REQUIRED"
                    }
                ],
                "options": null,
                "defaultValue": "18",
                "placeholder": "请输入年龄",
                "additionalInfo": null
            },
            "value": 18,
            "validatorProperies": {
                "Size.min": 18,
                "Size.max": 100,
                "Required.required": true
            }
        },
        {
            "id": 488686640488153088,
            "name": "WIRyhGtAhh",
            "configurer": {
                "id": "488686639036923906",
                "label": "兴趣爱好(单选)",
                "cpnType": "SELECT",
                "validators": [
                    {
                        "required": true,
                        "message": "必填项",
                        "validatorType": "REQUIRED"
                    }
                ],
                "options": [
                    "足球",
                    "篮球",
                    "乒乓球",
                    "羽毛球"
                ],
                "defaultValue": "足球",
                "placeholder": "请输入兴趣爱好",
                "additionalInfo": null
            },
            "value": "足球",
            "validatorProperies": {
                "Required.required": true
            }
        },
        {
            "id": 488686640488153089,
            "name": "EsaBSGeoNF",
            "configurer": {
                "id": "488686639041118208",
                "label": "信息收集",
                "cpnType": "TABLE",
                "validators": [],
                "options": null,
                "defaultValue": null,
                "placeholder": "请输入信息收集",
                "additionalInfo": {
                    "labels": [
                        "姓名",
                        "年龄"
                    ]
                }
            },
            "value": null,
            "validatorProperies": {}
        },
        {
            "id": 488686640488153090,
            "name": "dCrOsUvnkb",
            "configurer": {
                "id": "488686639041118209",
                "label": "兴趣爱好(多选)",
                "cpnType": "CHECKBOX",
                "validators": [],
                "options": [
                    "足球",
                    "篮球",
                    "乒乓球",
                    "羽毛球"
                ],
                "defaultValue": "[\"足球\", \"篮球\"]",
                "placeholder": "请输入兴趣爱好",
                "additionalInfo": null
            },
            "value": [
                "足球",
                "篮球"
            ],
            "validatorProperies": {}
        }
    ],
    "method": "POST",
    "actionUrl": "488686638231617536"
}

填写表单数据

我的模版是是基于 thymeleaf jQuery Bootstrap 实现的。
浏览器访问:http://localhost:8080/forms/page/488686638231617536

http://xhope.top/wp-content/uploads/2021/11/1122.png
数据填写完成后,会生实例id

实例id查看实例数据

488693516797902848 就是实例id

http://localhost:8080/forms/page/488686638231617536/488693516797902848

sharp-database中的BaseDAOImpl实现多表级联@OneToMany(二)

目标

本文章主要介绍级联中的「插入」「查询」「删除」操作。

环境搭建

  • Project.java
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@TableName("p_project")
public class Project extends BaseComponentEntity {

    private String title;

    private String description;

    private Long ownerId;

    private String coverUrl;

    @ManyToOne(value = "project_group_id", parentTable = "p_project_group")
    private ProjectGroup projectGroup;
}

@ManyToOne 是子表注解,它有2个属性,value 表示引用外键的字段名,parentTable 引用的父表名。

  • ProjectGroup.java
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@TableName("p_project_group")
public class ProjectGroup extends BaseComponentEntity {

    private String title;

    @OneToMany(subTable = "p_project", reversePropertyName = "projectGroup", cascadeSaveOrUpdate = true)
    private List<Project> projectList;

}

@OneToMany 是父表注解,属性是 List 集合。它有2个属性,reversePropertyName 表示子对象中该对象的属性名,subTable 关联的子表名。cascadeSaveOrUpdate 开启级联更新。

测试

级联插入

@Test
public void save() {
    ProjectGroup projectGroup = ProjectGroup.builder().title("我的组").projectList(Lists.newArrayList(Project.builder().title("我的项目").ownerId(11L).build())).build();
    projectGroupService.save(projectGroup);

    Project project = Project.builder().title("我的项目-2").ownerId(11L).build();
    project.setProjectGroup(projectGroup);
    projectService.save(project);
}

projectGroupService.save 可以级联插入;projectService.save单个插入,但是属性需要关联对象 ProjectGroup,需要对象中的 id 信息。

级联查询

@Test
public void select() {
    ProjectGroup projectGroup = projectGroupService.getByTitle("我的组").get(0);
    Assert.assertEquals("我的项目", projectGroup.getProjectList().get(0).getTitle());
    Assert.assertEquals("我的项目-2", projectGroup.getProjectList().get(1).getTitle());

    Project project = projectService.findById(486880981274755072L).get();
    Assert.assertEquals("我的组", project.getProjectGroup().getTitle());
}

级联删除

@Test
public void deleteLogically() {
    projectGroupService.deleteLogically(486678250836623360L);
    Assert.assertEquals(false, projectService.findById(486678250924703744L).isPresent());
}

@Test
public void deleteHardly() {
    projectGroupService.delete(486678250836623360L);
}

sharp-database中的BaseDAOImpl实现多表级联(一)

测试环境搭建

主表 Dimension

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@TableName(value = "sys_dimension", subTables = {"sys_dimension_unit"})
public class Dimension extends BaseComponentEntity {

    @NotBlank(message = "维度不能为空")
    @Length(max = 32, message = "维度不能超过32个字符")
    private String code;

    @NotBlank(message = "名称不能为空")
    @Length(max = 32, message = "名称不能超过32个字符")
    private String name;

    @NotNull(message = "分类不能为空")
    private DimensionCategoryEnum category;

}

@TableName 添加属性 subTables = {"sys_dimension_unit"} 表示子表是 sys_dimension_unit

那么,当删除的时候就会级联删除子表的数据。子表的外键是约定大于配置,采用“表名小写_id”。本例中的外键是 dimension_id
子表 Unit

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@TableName("sys_dimension_unit")
public class Unit extends BaseComponentEntity {

    @NotBlank(message = "单位code不能为空")
    @Length(max = 32, message = "维度不能超过32个字符")
    private String code;

    @NotBlank(message = "名称不能为空")
    @Length(max = 32, message = "名称不能超过32个字符")
    private String name;

    @NotNull(message = "分子不能为空")
    private Integer n;

    @NotNull(message = "分母不能为空")
    private Integer d;

    private Integer e;

    private BigDecimal k;

    @NotNull(message = "维度id不能为空")
    private Long dimensionId;

}

dimensionId 表示外键,对应的表字段是 dimension_id

编程式级联查询

  • selectAsSubTable
public void selectAsSubTable(List<Map<String, Object>> masterData, String property, String refColumnName) {
    Map<Long, List<T>> refColumnNameMap = this.groupByColumnName(refColumnName, (Collection)masterData.stream().map((rowx) -> {
        return rowx.get("id");
    }).collect(Collectors.toSet()));
    Iterator var5 = masterData.iterator();

    while(var5.hasNext()) {
        Map<String, Object> row = (Map)var5.next();
        List<T> subTableList = (List)refColumnNameMap.get(row.get("id"));
        row.put(property, CollectionUtils.isEmpty(subTableList) ? Collections.emptyList() : subTableList);
    }

}

子表操作,通过从masterData中获取Id值后,获取对应的子表数据。property作为可以放到Map中。
这个一般配合主表的查询。

public Grid<Map<String, Object>> list(String code, String name) {
    Grid<Map<String, Object>> grid = GridUtils.list(warehouseDAO.getSelectSQL() + " WHERE name like :name AND code = :code",
            Params.builder(2).pv("code", code).pv("name", name).pv(EntityConstants.LOGIC_DELETE_COLUMN_NAME, false).build());

    warehouseSpaceDAO.selectAsSubTable(grid.getRows(), "spaces", "warehouse_id");
    return grid;
}
  • groupByColumnName
public Map<Long, List<T>> groupByColumnName(String refColumnName, Collection<?> refValues) {
    Map<Long, List<T>> refColumnNameMap = (Map)this.selectByParams(Params.builder(1).pv("refColumnName", refValues).build(), refColumnName + " IN (:refColumnName)").stream().collect(Collectors.groupingBy((t) -> {
        return (Long)this.getPropertyValue(t, (String)this.columnNameToPropertyNameMap.get(refColumnName));
    }));
    return refColumnNameMap;
}

根据字段进行分组,并通过 Map 进行收集。

sharp-database中的BaseDAOImpl实现多租户的功能

添加ConditionAdvice实现

sharp-database 提供了默认的 ConditionAdvice 实现,自动过滤逻辑删除的数据。

public class DefaultConditionAdvice implements ConditionAdvice {
    public DefaultConditionAdvice() {
    }

    public Map<String, Object> getCondition() {
        return Params.builder(1).pv("is_deleted", false).build();
    }
}

可以添加自己的实现,将租户字段添加到查询条件中。

@Bean
public ConditionAdvice conditionAdvice() {
    return () -> {
        final DefaultConditionAdvice defaultConditionAdvice = new DefaultConditionAdvice();
        Map<String, Object> params = Params.builder().pv("tenant_id", UserInfoHolder.get().getTenantId()).build();
        params.putAll(defaultConditionAdvice.getCondition());
        return params;
    };
}

Spring MVC处理流程分析(六)

思考

如果现在我们有这么一个 SimpleController.java

@RestController
public class SimpleController {

    @GetMapping("request")
    @ModelAttribute("attr")
    public Map<String, Object> method(Integer age, @Valid User user, BindingResult bindingResult) {
        Map<String, Object> map  = new HashMap<>();
        map.put("age", age);
        return map;
    }
}

User.java

public class User {

    private Integer age;

    @NotBlank
    private String name;

}

当我们发出请求:http://localhost:8080/request?name=Rick&age=23
那么,

  1. 响应是Json数据,还是跳转页面?
  2. 如果是跳转页面,那么下面的页面能正确获取数据:name => Rick, age => 23 吗?
<h1 th:text="'name => '+ ${#request.getAttribute('user').name}"></h1>
<h1 th:text="'age => ' + ${#request.getAttribute('attr').age}"></h1>
<h1 th:text="'age => ' + ${attr.age}"></h1>
<h1 th:text="'age => ' + ${user.age}"></h1>

我们可以继续问更多的问题

  1. 请求为什么能找到 SimpleController 中的方法 method?
  2. 请求的age是字符串类型,为什么能转换成Integer的类型?
  3. 为什么User对象加了注解 @Valid 就可以对对象进行验证?
  4. 参数age加上注解(@Valid @NotBlank Integer age)可以验证吗?
  5. 为什么 BindingResult 参数需要紧跟在 @Valid 参数后面?没有参数 BindingResult 参数,方法验证通过会抛出异常,有这个参数就不抛出异常?

WebMvcAutoConfiguration自动配置

http://xhope.top/wp-content/uploads/2021/10/start.png

请求响应处理流程分析

一般过程

http://xhope.top/wp-content/uploads/2021/10/f1.png

具体过程

http://xhope.top/wp-content/uploads/2021/10/f21.png

1. HandlerExecutionChain

HandlerExecutionChain mappedHandler = getHandler(processedRequest);

HandlerExecutionChain :依赖 handlerMapping 目的是找到需要执行的Controller的目标方法拦截器Interceptor。默认加载5个handlerMapping

  • 0 = {RequestMappingHandlerMapping@5687}
  • 1 = {WelcomePageHandlerMapping@7500}
  • 2 = {BeanNameUrlHandlerMapping@7501}
  • 3 = {RouterFunctionMapping@7502}
  • 4 = {SimpleUrlHandlerMapping@7503}

2. 获取HandlerAdapter

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

默认有4个适配器:

  • 0 = {RequestMappingHandlerAdapter@8086}
  • 1 = {HandlerFunctionAdapter@8087}
  • 2 = {HttpRequestHandlerAdapter@8088}
  • 3 = {SimpleControllerHandlerAdapter@8089}

RequestMappingHandlerAdapter 是我们常用的适配器,这个适配器聚合了请求阶段所使用的解析器/处理器。就像一个生产工厂一样,是一个非常重要的类。
比如:

  • HandlerMethodArgumentResolver:「方法的参数转换」的解析器
  • HandlerMethodReturnValueHandler:「方法返回值」处理器
  • HttpMessageConverter: 消息转换
  • RequestBodyAdvice:RequestBody请求值的处理
  • ResponseBodyAdvice:ResponseBody返回值的处理

3. 处理拦截器PreHandle

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    return;
}
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

可以获取被拦截的方法,哪个controller的哪个方法。

4. HandlerAdapter#handle

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

适配开始处理,并获取ModelAndView。实际上调用的是 RequestMappingHandlerAdapterinvokeHandlerMethod 方法。

RequestMappingHandlerAdapterinvokeHandlerMethod 方法有2个核心方法:

  • invocableMethod.invokeAndHandle(webRequest, mavContainer)

处理参数并得到方法的返回结果。通过名字可以知道 invokeAndHandle 也做了两件事情:invokehandle

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {

    Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);


    if (returnValue == null) {
        if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
            disableContentCachingIfNecessary(webRequest);
            mavContainer.setRequestHandled(true);
            return;
        }
    }
    else if (StringUtils.hasText(getResponseStatusReason())) {
        mavContainer.setRequestHandled(true);
        return;
    }

    mavContainer.setRequestHandled(false);

    this.returnValueHandlers.handleReturnValue(
            returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}

invokeForRequest 这个方法处理参数(依赖 HandlerMethodArgumentResolver ),并获取方法的返回值(获取参数后,反射)。mavContainer.setRequestHandled 设置请求是否被处理,后面视图渲染就不作处理。

「类型的转换」「对象的验证」都是在参数解析中完成的。「类型的转换」底层依赖 TypeConverterDelegate

默认的处理器有:

0 = {RequestParamMethodArgumentResolver@8604} 
1 = {RequestParamMapMethodArgumentResolver@8605} 
2 = {PathVariableMethodArgumentResolver@8606} 
3 = {PathVariableMapMethodArgumentResolver@8607} 
4 = {MatrixVariableMethodArgumentResolver@8608} 
5 = {MatrixVariableMapMethodArgumentResolver@8609} 
6 = {ServletModelAttributeMethodProcessor@8610} 
7 = {RequestResponseBodyMethodProcessor@8611} 
8 = {RequestPartMethodArgumentResolver@8612} 
9 = {RequestHeaderMethodArgumentResolver@8613} 
10 = {RequestHeaderMapMethodArgumentResolver@8614} 
11 = {ServletCookieValueMethodArgumentResolver@8615} 
12 = {ExpressionValueMethodArgumentResolver@8616} 
13 = {SessionAttributeMethodArgumentResolver@8617} 
14 = {RequestAttributeMethodArgumentResolver@8618} 
15 = {ServletRequestMethodArgumentResolver@8619} 
16 = {ServletResponseMethodArgumentResolver@8620} 
17 = {HttpEntityMethodProcessor@8621} 
18 = {RedirectAttributesMethodArgumentResolver@8622} 
19 = {ModelMethodProcessor@8623} 
20 = {MapMethodProcessor@8624} 
21 = {ErrorsMethodArgumentResolver@8625} 
22 = {SessionStatusMethodArgumentResolver@8626} 
23 = {UriComponentsBuilderMethodArgumentResolver@8627} 
24 = {PrincipalMethodArgumentResolver@8629} 
25 = {RequestParamMethodArgumentResolver@8630} 
26 = {ServletModelAttributeMethodProcessor@8631} 

returnValueHandlers.handleReturnValue 对上一步的返回值做进一步处理。遍历所有的 HandlerMethodReturnValueHandler 找出合适的一个。然后对返回值做进一步处理,比如:把返回值放入Model中。默认有15个返回值处理器。

0 = {ModelAndViewMethodReturnValueHandler@8543} 
1 = {ModelMethodProcessor@8546} 
2 = {ViewMethodReturnValueHandler@8547} 
3 = {ResponseBodyEmitterReturnValueHandler@8548} 
4 = {StreamingResponseBodyReturnValueHandler@8549} 
5 = {HttpEntityMethodProcessor@8550} 
6 = {HttpHeadersReturnValueHandler@8551} 
7 = {CallableMethodReturnValueHandler@8552} 
8 = {DeferredResultMethodReturnValueHandler@8553} 
9 = {AsyncTaskMethodReturnValueHandler@8554} 
10 = {ServletModelAttributeMethodProcessor@8162} 
11 = {RequestResponseBodyMethodProcessor@8555} 
12 = {ViewNameMethodReturnValueHandler@8556} 
14 = {MapMethodProcessor@8557} 
14 = {ServletModelAttributeMethodProcessor@8558} 
  • getModelAndView(mavContainer, modelFactory, webRequest)

获取模型和视图

private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
        ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {

    modelFactory.updateModel(webRequest, mavContainer);
    if (mavContainer.isRequestHandled()) {
        // 处理过了,就不需要视图了
        return null;
    }
    ModelMap model = mavContainer.getModel();
    ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
    if (!mavContainer.isViewReference()) {
        // 如果指定了具体的视图
        mav.setView((View) mavContainer.getView());
    }
    if (model instanceof RedirectAttributes) {
        Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        if (request != null) {
            RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
        }
    }
    return mav;
}   

5. 处理拦截器PostHandle

mappedHandler.applyPostHandle(processedRequest, response, mv);
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable ModelAndView modelAndView)

可以对视图作出最后的处理

6. 视图渲染

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
        @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
        @Nullable Exception exception) throws Exception {

    boolean errorView = false;
    // 如果有异常,那么准备异常视图
    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            logger.debug("ModelAndViewDefiningException encountered", exception);
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        }
        else {
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            // ExceptionHandlerExceptionResolver 会拿到 @ControllerAdvice的ExceptionHandler 去处理
            mv = processHandlerException(request, response, handler, exception);
            errorView = (mv != null);
        }
    }

    // 如果有视图就渲染(@ResponseBody注解的requestHandled = true,所以视图是null,不需要渲染)
    if (mv != null && !mv.wasCleared()) {
        render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }

    // 处理拦截器afterCompletion方法,此时视图已经渲染完成。
    if (mappedHandler != null) {
        // Exception (if any) is already handled..
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}

回到思考

回到前面的思考,我们开始分析。
http://xhope.top/wp-content/uploads/2021/10/f3.png