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 注册自定义参数、响应结果的解析
相关推荐
小小娥子23 分钟前
【Redis】Hash类型的常用命令
数据库·spring boot·redis
2401_857439692 小时前
“衣依”服装销售平台:Spring Boot技术应用与优化
spring boot·后端·mfc
冷琴19963 小时前
基于java+springboot的酒店预定网站、酒店客房管理系统
java·开发语言·spring boot
九圣残炎4 小时前
【springboot】简易模块化开发项目整合Redis
spring boot·redis·后端
.生产的驴4 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
琴智冰4 小时前
SpringBoot
java·数据库·spring boot
爱码少年4 小时前
springboot工程中使用tcp协议
spring boot·后端·tcp/ip
《源码好优多》5 小时前
基于SpringBoot+Vue+Uniapp的植物园管理小程序系统(2024最新,源码+文档+远程部署+讲解视频等)
vue.js·spring boot·uni-app
计算机学姐5 小时前
基于微信小程序的调查问卷管理系统
java·vue.js·spring boot·mysql·微信小程序·小程序·mybatis
2401_8576226612 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php