RequestBody 和 ResponseBody

SpringMVC 执行流程

  • 当用户通过浏览器发起一个HTTP请求,请求直接到前端控制器DispatcherServlet;
  • 前端控制器接收到请求以后调用处理器映射器HandlerMapping,处理器映射器根据请求的URL找到具体的Handler,并将它返回给前端控制器;
  • 前端控制器调用处理器适配器HandlerAdapter去适配调用Handler;
  • 处理器适配器会根据Handler去调用真正的处理器去处理请求,并且处理对应的业务逻辑;
  • 当处理器处理完业务之后,会返回一个ModelAndView对象给处理器适配器,HandlerAdapter再将该对象返回给前端控制器;这里的Model是返回的数据对象,View是逻辑上的View。
  • 前端控制器DispatcherServlet将返回的ModelAndView对象传给视图解析器ViewResolver进行解析,解析完成之后就会返回一个具体的视图View给前端控制器。(ViewResolver根据逻辑的View查找具体的View)
  • 前端控制器DispatcherServlet将具体的视图进行渲染,渲染完成之后响应给用户(浏览器显示)。

RequestBody 解析

源码基于 springboot 2.3.12

  • http 请求处理入口:DispatcherServlet#doDispatch
  • 请求具体处理流程:RequestMappingHandlerAdapter#handleInternal
  • 实际处理:ServletInvocableHandlerMethod#invokeAndHandle
    • InvocableHandlerMethod#invokeForRequest
    • 参数解析:InvocableHandlerMethod#getMethodArgumentValues
    • @RequestBody 最终由 RequestResponseBodyMethodProcessor#resolveArgument 负责解析

ResponseBody 解析

  • 参考上面 @RequestBody 的解析,InvocableHandlerMethod#invokeForRequest 处理请求完成后,处理后的结果由 HandlerMethodReturnValueHandlerComposite#handleReturnValue
  • @RequestBody 最终由 RequestResponseBodyMethodProcessor#handleReturnValue 负责解析

实战应用

背景

dubbo 应用升级到 springcloud,支持 dubbo + http 双协议发布,对外提供给 Feign 客户端,原 dubbo 应用接口均继承自 BaseFacade 接口,请求参数和响应结果分别是泛型 BaseRequest、BaseResponse

改造

  1. 同 BaseFacade,定义针对 Feign 客户端的统一接口 BaseFacadeFeign,业务接口均继承自该接口

    java 复制代码
    public interface BaseFacadeFeign<Q extends BaseRequest, P extends BaseResponse> {
    
        String APP_NAME = "online-service";
    
        @PostMapping
        P execute(@RequestBody Q request);
    }
    
    @FeignClient(value = BaseFacadeFeign.APP_NAME, path = "/queryAcctInfo")
    public interface QueryAcctInfoFacadeFeign extends BaseFacadeFeign<QueryAcctInfoRequest,
    	QueryAcctInfoResponse> {
    
    }
  2. 根据 Feign 接口定义,动态实现 http 服务发布

总体思路:

  • 通过 Feign 接口定义,将其请求路径映射为 http 请求地址
  • http 请求的具体 Handler 由 Facade 的具体实现来处理

Feign 接口 和 Facade 接口分别定义不同的包:com.xxx.feign、com.xxx.facade,需要先解析所有的 Feign、Facade接口,类扫描基类:

java 复制代码
@RequiredArgsConstructor
public abstract class ClassFilePatternResolver {

    private final String patten;

    @SneakyThrows
    public Set<Class<?>> resole() {
        Set<Class<?>> classSet = Sets.newHashSet();
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources(patten);
        // MetadataReader 的工厂类
        MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resolver);
        for (Resource resource : resources) {
            // 用于读取类信息
            MetadataReader reader = readerFactory.getMetadataReader(resource);
            // 扫描到的class
            String classname = reader.getClassMetadata().getClassName();
            Class<?> clazz = Class.forName(classname);
            if (!clazz.isInterface()) {
                continue;
            }
            if (support(clazz)) {
                classSet.add(clazz);
            }
        }
        return classSet;
    }

    protected abstract boolean support(Class<?> clazz);
}

Feign 接口扫描:

java 复制代码
public class FeignClassFilePatternResolver extends ClassFilePatternResolver {

    public FeignClassFilePatternResolver() {
        super(String.format("classpath:%s/**/*.class", 
			ClassUtils.convertClassNameToResourcePath("com.xxx.feign")));
    }

    @Override
    public boolean support(Class<?> clazz) {
        return !BaseFacadeFeign.class.equals(clazz) && BaseFacadeFeign.class.equals(clazz.getInterfaces()[0])
                && AnnotationUtils.isAnnotationPresent(clazz, FeignClient.class);
    }
}

Facade 接口扫描:

java 复制代码
public class FacadeClassFilePatternResolver extends ClassFilePatternResolver {

    public FacadeClassFilePatternResolver() {
        super(String.format("classpath:%s/**/*.class", 
			ClassUtils.convertClassNameToResourcePath("com.xxx.facade")));
    }

    @Override
    protected boolean support(Class<?> clazz) {
        return !BaseFacade.class.equals(clazz) && BaseFacade.class.equals(clazz.getInterfaces()[0]);
    }
}

http 服务动态发布:

java 复制代码
@Configuration
@RequiredArgsConstructor
public class DynamicMappingConfig {

    private static final String SUFFIX = "Feign";

    private final ApplicationContext context;
    private final RequestMappingHandlerMapping mapping;
    private final RequestMappingHandlerAdapter handlerAdapter;
    private final List<HttpMessageConverter<?>> messageConverters;

    @PostConstruct
    public void init() {
        OnlineRequestResponseMethodProcessor processor = new OnlineRequestResponseMethodProcessor(messageConverters);
        handlerAdapter.setArgumentResolvers(Collections.singletonList(processor));
        handlerAdapter.setReturnValueHandlers(Collections.singletonList(processor));

        Set<Class<?>> feignClassSet = new FeignClassFilePatternResolver().resole();
        Set<Class<?>> facadeClassSet = new FacadeClassFilePatternResolver().resole();
        feignClassSet.forEach(clazz -> {
            Class<?> facadeClass = findFacadeClass(clazz, facadeClassSet);
            FeignClient annotation = clazz.getAnnotation(FeignClient.class);
            String path = annotation.path();
            if (StringUtils.isEmpty(path)) {
                throw new ValidateException(String.format("接口[%s]未定义请求路径", clazz));
            }
            RequestMappingInfo mappingInfo = RequestMappingInfo.paths(path)
                    .consumes(MediaType.APPLICATION_JSON_VALUE)
                    .produces(MediaType.APPLICATION_JSON_VALUE)
                    .methods(RequestMethod.POST).build();
            Object handler = context.getBean(facadeClass);

            // Class<?>[] genetics = GenericTypeResolver.resolveTypeArguments(facadeClass, BaseFacade.class);
            // Class<?> paramType = genetics[0];
            // Class<?> returnType = genetics[1];
            Method method = Optional.ofNullable(ReflectionUtils.findMethod(facadeClass, "execute", (Class<?>) null))
                    .orElseThrow(() -> new ValidateException(String.format("根据接口[%s]未获取到execute方法", facadeClass)));
            mapping.registerMapping(mappingInfo, handler, method);
        });
    }

    private Class<?> findFacadeClass(Class<?> feignClass, Set<Class<?>> facadeClassSet) {
        String facadeClassShortName = ClassUtils.getShortName(feignClass);
        String prefix = facadeClassShortName.substring(0, facadeClassShortName.length() - SUFFIX.length());
        return facadeClassSet.stream()
                .filter(facadeClass -> ClassUtils.getShortName(facadeClass).contains(prefix))
                .findFirst()
                .orElseThrow(() -> new ValidateException(String.format("根据Feign接口[%s]未获取到对应Facade接口[%s]",
					 feignClass, prefix)));
    }

}

由于上面动态发布使用 @RequestBody、@ResponseBody,但其实际需要用到 RequestResponseBodyMethodProcessor

解析,因此需要重写 RequestResponseBodyMethodProcessor

java 复制代码
public class OnlineRequestResponseMethodProcessor extends RequestResponseBodyMethodProcessor {

    public OnlineRequestResponseMethodProcessor(List<HttpMessageConverter<?>> converters) {
        super(converters);
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Type type = GenericTypeResolver.resolveType(parameter.getGenericParameterType(), BaseFacade.class);
        return BaseRequest.class.equals(type);
    }

    @Override
    public boolean supportsReturnType(MethodParameter parameter) {
        Type type = GenericTypeResolver.resolveType(parameter.getGenericParameterType(), BaseFacade.class);
        return BaseResponse.class.equals(type);
    }

}

小结

实现动态发布 http 服务

  • RequestMappingHandlerMapping 实现 http 服务的注册
  • RequestMappingHandlerAdapter 注册自定义参数、响应结果的解析
相关推荐
小马爱打代码37 分钟前
SpringBoot原生实现分布式MapReduce计算
spring boot·分布式·mapreduce
iuyou️40 分钟前
Spring Boot知识点详解
java·spring boot·后端
一弓虽1 小时前
SpringBoot 学习
java·spring boot·后端·学习
来自星星的猫教授3 小时前
spring,spring boot, spring cloud三者区别
spring boot·spring·spring cloud
乌夷4 小时前
使用spring boot vue 上传mp4转码为dash并播放
vue.js·spring boot·dash
A阳俊yi6 小时前
Spring Boot日志配置
java·spring boot·后端
苹果酱05676 小时前
2020-06-23 暑期学习日更计划(机器学习入门之路(资源汇总)+概率论)
java·vue.js·spring boot·mysql·课程设计
斜月6 小时前
一个服务预约系统该如何设计?
spring boot·后端
Java水解6 小时前
线程池详解:在SpringBoot中的最佳实践
spring boot·后端
阿里小阿希7 小时前
解决 Spring Boot + MyBatis 项目迁移到 PostgreSQL 后的数据类型不匹配问题
spring boot·postgresql·mybatis