作者归档:Rick

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

SpringMVC返回值处理器 ReturnValueHandler(五)

SpringMVC默认的返回值处理器有:

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} 

我们最常用的就是 Controller + 返回值"String"Controller + 返回值+ @ResponseBody,现在分析一下,这两种情况是如何处理的。

Controller + 返回值”String”

@Controller
public class TestController {

    @GetMapping("index")
    public String toPage() {
        return "index";
    }
}
GET http://localhost:8080/index

会由 ViewNameMethodReturnValueHandler 去处理,将返回值放到ModeAndView的viewName中。这样视图解析器就会去找相应的视图。

Controller + 返回值+ @ResponseBody

RestController + 返回值 = Controller + 返回值+ @ResponseBody

GET http://localhost:8080/index

会由 RequestResponseBodyMethodProcessor 去处理,看下这个解析器的源码

@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

    mavContainer.setRequestHandled(true);
    ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
    ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

    // Try even with null return value. ResponseBodyAdvice could get involved.
    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}

做2件事情:

  • 设置setRequestHandled(true) 告诉视图解析器,已经处理过了,不需要渲染
  • 找到合适的MessageConverter,然后将值直接写出。

RequestResponseBodyMethodProcessor 在处理的过程中,客户端可以使用注解@ControllerAdvice在write之前对值进行处理

@ControllerAdvice
public class MyAdvice implements ResponseBodyAdvice {
@Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        System.out.println("ResponseBodyAdvice: 执行 " + o);
        return o;
    }}

自定义HandlerMethodReturnValueHandler

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
        handlers.add(new HandlerMethodReturnValueHandler() {

            @Override
            public boolean supportsReturnType(MethodParameter returnType) {
                return Integer.class == returnType.getParameterType();
            }

            @Override
            public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
                Integer value = (Integer) returnValue;
                mavContainer.addAttribute("intValue", value);
                if (value > 10) {
                    mavContainer.setViewName("big");
                } else {
                    mavContainer.setViewName("small");
                }
            }
        });
    }
}

自定义的HandlerMethodReturnValueHandler,做了2件事。

  • 将值放入Model中
  • 如果大于10跳转big.html,否则跳转small.html
@GetMapping("int/{intValue}")
public int toIntPage(@PathVariable Integer intValue) {
    return intValue;
}
GET http://localhost:8080/int/11

big.html

<body>
big.html
<h1 th:text="${intValue}"></h1>
</body>

SpringMVC自定义RequestMappingHandlerMapping实现接口的版本控制(四)

说明

在Spring MVC项目中,如果要进行restful接口的版本控制一般有以下几个方向:

  • 基于参数的版本控制
  • 基于header的版本控制
  • 基于path的版本控制

在spring MVC下,url映射到哪个method是由RequestMappingHandlerMapping来控制的,那么我们可以通过对RequestMappingHandlerMapping 定义条件来做版本控制

环境搭建

ApiVersion 表示请求的版本

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiVersion {
    String value() default "";
}

WebMvcRegistrationsConfig.java

@Configuration
public class WebMvcRegistrationsConfig implements WebMvcRegistrations {

    private static final String VERSION_PARAM_NAME = "version";

    private static final String HEADER_VERSION = "X-VERSION";

    /**
     * RequestMappingHandlerMapping 被 VersionRequestMappingHandlerMappingHandlerMapping 替换
     * 按照VersionRequestMappingHandlerMappingHandlerMapping映射逻辑进行映射
     * @return
     */
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new VersionRequestMappingHandlerMapping();
    }

    private class VersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

        @Override
        protected RequestCondition<?> getCustomMethodCondition(Method method) {
            ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
            if (apiVersion == null) {
                return null;
            }

            return new RequestCondition() {

                @Override
                public Object combine(Object o) {
                    return null;
                }

                @Override
                public Object getMatchingCondition(HttpServletRequest request) {
                    String version = resolveVersion(method, request);

                    if (Objects.equals(version, apiVersion.value())) {
                        return this;
                    }

                    return null;
                }

                @Override
                public int compareTo(Object o, HttpServletRequest request) {
                    return 0;
                }
            };
        }

        private String resolveVersion(Method method, HttpServletRequest request) {
            String version = request.getParameter(VERSION_PARAM_NAME);
            if (StringUtils.hasText(version)) {
                return version;
            }
            version = request.getHeader(HEADER_VERSION);
            if (StringUtils.hasText(version)) {
                return version;
            }
            return resolvePathVersion(method, request);
        }

        private String resolvePathVersion(Method method, HttpServletRequest request) {
            String bastPath = null;
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                if (annotation.annotationType() == RequestMapping.class) {
                    bastPath = ((RequestMapping)annotation).value()[0];
                    break;
                } else if (annotation.annotationType() == GetMapping.class) {
                    bastPath = ((GetMapping)annotation).value()[0];
                    break;
                }  else if (annotation.annotationType() == PostMapping.class) {
                    bastPath = ((PostMapping)annotation).value()[0];
                    break;
                } else if (annotation.annotationType() == DeleteMapping.class) {
                    bastPath = ((DeleteMapping)annotation).value()[0];
                    break;
                } else if (annotation.annotationType() == PutMapping.class) {
                    bastPath = ((PutMapping)annotation).value()[0];
                    break;
                }
            }

            Map<String, String> uriVariables = getPathMatcher().extractUriTemplateVariables(bastPath.startsWith("/") ? bastPath : "/" + bastPath,
                    request.getServletPath());
            return uriVariables.get(VERSION_PARAM_NAME);
        }
    }
}

测试

@GetMapping("{version}/test")
public String v1(@PathVariable String version) {
    return "default";
}

@GetMapping("{version}/test")
@ApiVersion("v2")
public String v2() {
    return "v2";
}

@GetMapping("{version}/test")
@ApiVersion("v3")
public String v3() {
    return "v3";
}

/**
 * version在不一定非要在第一个位置
 * @return
 */
@GetMapping("/test/{version}")
@ApiVersion("v4")
public String v4() {
    return"v4";
}

@GetMapping("/test/{version}")
public String v5() {
    return "default";
}
curl -X GET \
  'http://localhost:8080/v1/test?version=v3' \
  -H 'X-VERSION: v2'

优先级依次是:request参数 > header > path参数

path参数,变量不一定写在第一个,只要变量名称是${version}就可以
GitHub:https://github.com/jkxyx205/spring-boot-learn/tree/master/spring-mvc-mapping