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 注册自定义参数、响应结果的解析
相关推荐
HaiFan.25 分钟前
SpringBoot 事务
java·数据库·spring boot·sql·mysql
大梦百万秋2 小时前
Spring Boot实战:构建一个简单的RESTful API
spring boot·后端·restful
斌斌_____2 小时前
Spring Boot 配置文件的加载顺序
java·spring boot·后端
苹果醋33 小时前
React系列(八)——React进阶知识点拓展
运维·vue.js·spring boot·nginx·课程设计
等一场春雨5 小时前
springboot 3 websocket react 系统提示,选手实时数据更新监控
spring boot·websocket·react.js
荆州克莱6 小时前
Golang的性能监控指标
spring boot·spring·spring cloud·css3·技术
AI人H哥会Java6 小时前
【Spring】控制反转(IoC)与依赖注入(DI)—IoC容器在系统中的位置
java·开发语言·spring boot·后端·spring
赖龙6 小时前
springboot restful mybatis连接mysql返回日期格式不对
spring boot·mybatis·restful
自律的kkk6 小时前
SpringBoot中使用AOP切面编程实现登录拦截
java·spring boot·aop·切面编程·登录拦截
武昌库里写JAVA6 小时前
【MySQL】MySQL 通过127.0.0.1和localhost登录的区别
spring boot·spring·毕业设计·layui·课程设计