资讯专栏INFORMATION COLUMN

让springcloud feign-client 完全支持springmvc的@RequestPa

codergarden / 2971人阅读

摘要:要解决的问题在微服务中,使用来做声明式微服务调用的时,经常会遇到的原生注解不支持自定义对象的问题,例如服务的接口服务的提供者服务的消费者远程调用的代理服务期望能兼容中的原生特性即假如请求为华为期望对于以下两种写法完全兼容写法的原生写法

1、要解决的问题

在springcloud微服务中,使用feign来做声明式微服务调用的client时,经常会遇到springmvc的原生注解@RequestParam不支持自定义POJO对象的问题,例如:

服务的API接口:

@FeignClient(name="springcloud-nacos-producer", qualifier="productApiService")
public interface ProductApiService {
    @GetMapping(value="/api/product/list", produces=APPLICATION_JSON)
    public PageResult> getProductListByPage(@RequestParam Product condition, @RequestParam Page page, @RequestParam Sort sort);
}

public class Page implements DtoModel {

    private static final long serialVersionUID = 1L;
    
    private Integer currentPage = 1;
    
    private Integer pageSize = 10;
    
    private Integer totalRowCount = 0;

    //get/set...
}

public class Sort implements DtoModel {

    private static final long serialVersionUID = 1L;

    private List orders;
    
    Sort() {
        super();
    }
    
    Sort(List orders) {
        super();
        this.orders = orders;
    }
    
    public static Sort by(Order... orders) {
        return new Sort(Arrays.asList(orders));
    }

    public List getOrders() {
        return orders;
    }

    public void setOrders(List orders) {
        this.orders = orders;
    }
    
    public Order first() {
        if(orders != null && orders.size() > 0) {
            return orders.get(0);
        }
        return null;
    }
    
    public static class Order {
        
        public static final String DIRECTION_ASC = "asc";

        public static final String DIRECTION_DESC = "desc";
        
        private String property;
        
        private String direction;

        Order() {
            super();
        }
        
        Order(String property, String direction) {
            super();
            if(direction != null) {
                direction = direction.toLowerCase();
                direction = DIRECTION_DESC.equals(direction) ? DIRECTION_DESC : DIRECTION_ASC;
            } else {
                direction = DIRECTION_ASC;
            }
            this.property = property;
            this.direction = direction;
        }
        
        public static Order by(String property, String direction) {
            return new Order(property, direction);
        }
        
        public static Order asc(String property) {
            return new Order(property, DIRECTION_ASC);
        }
        
        public static Order desc(String property) {
            return new Order(property, DIRECTION_DESC);
        }

        public String getProperty() {
            return property;
        }

        public void setProperty(String property) {
            this.property = property;
        }

        public String getDirection() {
            return direction;
        }

        public void setDirection(String direction) {
            this.direction = direction;
        }
        
        /**
         * Used by SpringMVC @RequestParam and JAX-RS @QueryParam
         * @param order
         * @return
         */
        public static Order valueOf(String order) {
            if(order != null) {
                String[] orders = order.trim().split(":");
                String prop = null, dir = null;
                if(orders.length == 1) {
                    prop = orders[0] == null ? null : orders[0].trim();
                    if(prop != null && prop.length() > 0) {
                        return Order.asc(prop);
                    }
                } else if (orders.length == 2) {
                    prop = orders[0] == null ? null : orders[0].trim();
                    dir = orders[1] == null ? null : orders[1].trim();
                    if(prop != null && prop.length() > 0) {
                        return Order.by(prop, dir);
                    }
                }
            }
            return null;
        }

        @Override
        public String toString() {
            return property + ":" + direction;
        }
        
    }
    
    @Override
    public String toString() {
        return "Sort " + orders + "";
    }

}

服务的提供者(Provider):

@RestController("defaultProductApiService")
public class ProductApiServiceImpl extends HttpAPIResourceSupport implements ProductApiService {

    @Autowired
    private ProductMapper productMapper;

    @Override
    public PageResult> getProductListByPage(Product condition, Page page, Sort sort) {
        List dataList = productMapper.selectModelPageListByExample(condition, sort, new RowBounds(page.getOffset(), page.getLimit()));
        page.setTotalRowCount(productMapper.countModelPageListByExample(condition));
        return PageResult.success().message("OK").data(dataList).totalRowCount(page.getTotalRowCount()).build();
    }

}

服务的消费者(Consumer):

@RestController
public class ProductController implements ProductApiService {

    //远程调用provider的feign代理服务
    @Resource(name="productApiService")
    private ProductApiService productApiService;

    @Override
    public PageResult> getProductListByPage(Product condition, Page page, Sort sort) {
        return productApiService.getProductListByPage(condition, page, sort);
    }

}
2、期望能兼容springmvc中@RequestParam的原生特性:

即:假如请求URL为:http://127.0.0.1:18181/api/product/list?productName=华为&productType=1¤tPage=1&pageSize=20&orders=createTime:desc,updateTime:desc

期望1:对于以下两种写法完全兼容:

写法1(springmvc的原生写法):

@RestController
public class ProductController1 {

    @GetMapping(value="/api/product/list", produces=APPLICATION_JSON)
    public PageResult> getProductListByPage(Product condition, Page page, Sort sort) {
        ....
    }
}

写法2(兼容feign的写法):

public interface ProductApiService {

    @GetMapping(value="/api/product/list", produces=APPLICATION_JSON)
    public PageResult> getProductListByPage(@RequestParam Product condition, @RequestParam Page page, @RequestParam Sort sort);
}

期望2:不管是直调Provider还是调Consumer,请求URL都是兼容的!

3、解决方案

(1)、继承RequestParamMethodArgumentResolver,增强springmvc对@RequestParam的解析能力,能够解析如下定义的handler:

    @GetMapping(value="/api/product/list1", produces=APPLICATION_JSON)
    public PageResult> getProductListByPage1(@RequestParam Product condition, @RequestParam Page page, @RequestParam Sort sort) {
        //...
    }
    
    或者
    
    @GetMapping(value="/api/product/list2", produces=APPLICATION_JSON)
    public PageResult> getProductListByPage1(@RequestParam("condition") Product condition, @RequestParam("page") Page page, @RequestParam("sort") Sort sort) {
        //...
    }

自定义的EnhancedRequestParamMethodArgumentResolver

/**
 * 增强的RequestParamMethodArgumentResolver,解决@RequestParam注解显示地用于用户自定义POJO对象时的参数解析问题
 * 
 * 举个例子:
 * 
 * 请求1:http://172.16.18.174:18180/api/user/list1/?condition={"userName": "a", "status": 1}&page={"currentPage": 1, "pageSize": 20}&sort={"orders": [{"property": "createTime", "direction": "desc"},{"property": "updateTime", "direction": "asc"}]}
 * 
 * 请求2:http://172.16.18.174:18180/api/user/list/?userName=a&status=1¤tPage=1&pageSize=20&orders=createTime:desc,updateTime:desc
 * 
 * @GetMapping(value="/api/user/list", produces=APPLICATION_JSON)
 * public PageResult> getUserListByPage( @RequestParam User condition, @RequestParam Page page, @RequestParam Sort sort );
 * 
 * 如上例所示,请求1的参数能够正确地被@RequestParam注解解析,但是请求2却不行,该实现即是解决此问题的
 * 
 */
public class EnhancedRequestParamMethodArgumentResolver extends RequestParamMethodArgumentResolver {

    /**
     * 明确指出的可解析的参数类型列表
     */
    private List> resolvableParameterTypes;
    
    private volatile ConversionService conversionService;
    
    private BeanFactory beanFactory;
    
    public EnhancedRequestParamMethodArgumentResolver(boolean useDefaultResolution) {
        super(useDefaultResolution);
    }

    public EnhancedRequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
            boolean useDefaultResolution) {
        super(beanFactory, useDefaultResolution);
        this.beanFactory = beanFactory;
    }

    @Override
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
        Object arg = super.resolveName(name, parameter, request);
        if(arg == null) {
            if(isResolvableParameter(parameter)) {
                HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
                Map parameterMap = getRequestParameters(servletRequest);
                arg = instantiateParameter(parameter);
                SpringBeanUtils.setBeanProperty(arg, parameterMap, getConversionService());
            }
        }
        return arg;
    }
    
    /**
     * 判断@RequestParam注解的参数是否是可解析的
     * 1、不是一个SimpleProperty (由BeanUtils.isSimpleProperty()方法决定)
     * 2、不是一个Map类型 (Map类型走RequestParamMapMethodArgumentResolver,此处不做考虑)
     * 3、该参数类型具有默认的无参构造器
     * @param parameter
     * @return
     */
    protected boolean isResolvableParameter(MethodParameter parameter) {
        Class clazz = parameter.getNestedParameterType();
        if(!CollectionUtils.isEmpty(resolvableParameterTypes)) {
            for(Class parameterType : resolvableParameterTypes) {
                if(parameterType.isAssignableFrom(clazz)) {
                    return true;
                }
            }
        }
        if(!BeanUtils.isSimpleProperty(clazz) && !Map.class.isAssignableFrom(clazz)) {
            Constructor[] constructors = clazz.getDeclaredConstructors();
            if(!ArrayUtils.isEmpty(constructors)) {
                for(Constructor constructor : constructors) {
                    if(constructor.getParameterTypes().length == 0) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    
    /**
     * 实例化一个@RequestParam注解参数的实例
     * @param parameter
     * @return
     */
    protected Object instantiateParameter(MethodParameter parameter) {
        return BeanUtils.instantiateClass(parameter.getNestedParameterType());
    }
    
    protected Map getRequestParameters(HttpServletRequest request) {
        Map parameters = new HashMap();
        Map paramMap = request.getParameterMap();
        if(!CollectionUtils.isEmpty(paramMap)) {
            paramMap.forEach((key, values) -> {
                parameters.put(key, ArrayUtils.isEmpty(values) ? null : (values.length == 1 ? values[0] : values));
            });
        }
        return parameters;
    }

    protected ConversionService getConversionService() {
        if(conversionService == null) {
            synchronized (this) {
                if(conversionService == null) {
                    try {
                        conversionService = (ConversionService) beanFactory.getBean("mvcConversionService"); //lazy init mvcConversionService, create by WebMvcAutoConfiguration
                    } catch (BeansException e) {
                        conversionService = new DefaultConversionService();
                    }
                }
            }
        }
        return conversionService;
    }

    public List> getResolvableParameterTypes() {
        return resolvableParameterTypes;
    }

    public void setResolvableParameterTypes(List> resolvableParameterTypes) {
        this.resolvableParameterTypes = resolvableParameterTypes;
    }
    
}

public class SpringBeanUtils {
    
    /**
     * 将properties中的值填充到指定bean中去
     * @param bean
     * @param properties
     * @param conversionService
     */
    public static void setBeanProperty(Object bean, Map properties, ConversionService conversionService) {
        Assert.notNull(bean, "Parameter "bean" can not be null!");
        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);
        beanWrapper.setConversionService(conversionService);
        for(Map.Entry entry : properties.entrySet()) {
            String propertyName = entry.getKey();
            if(beanWrapper.isWritableProperty(propertyName)) {
                beanWrapper.setPropertyValue(propertyName, entry.getValue());
            }
        }
    }
}

继承RequestMappingHandlerAdapter替换自定义的EnhancedRequestParamMethodArgumentResolver到springmvc中去:

public class EnhancedRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {

    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        List argumentResolvers = new ArrayList(getArgumentResolvers());
        replaceRequestParamMethodArgumentResolvers(argumentResolvers);
        setArgumentResolvers(argumentResolvers);
        
        List initBinderArgumentResolvers = new ArrayList(getInitBinderArgumentResolvers());
        replaceRequestParamMethodArgumentResolvers(initBinderArgumentResolvers);
        setInitBinderArgumentResolvers(initBinderArgumentResolvers);
    }
    
    /**
     * 替换RequestParamMethodArgumentResolver为增强版的EnhancedRequestParamMethodArgumentResolver
     * @param methodArgumentResolvers
     */
    protected void replaceRequestParamMethodArgumentResolvers(List methodArgumentResolvers) {
        methodArgumentResolvers.forEach(argumentResolver -> {
            if(argumentResolver.getClass().equals(RequestParamMethodArgumentResolver.class)) {
                Boolean useDefaultResolution = ReflectionUtils.getFieldValue(argumentResolver, "useDefaultResolution");
                EnhancedRequestParamMethodArgumentResolver enhancedArgumentResolver = new EnhancedRequestParamMethodArgumentResolver(getBeanFactory(), useDefaultResolution);
                enhancedArgumentResolver.setResolvableParameterTypes(Arrays.asList(DtoModel.class));
                Collections.replaceAll(methodArgumentResolvers, argumentResolver, enhancedArgumentResolver);
            }
        });
    }
    
}

注册自定义的EnhancedRequestMappingHandlerAdapter到容器中去

@Configuration
public class MyWebMvcConfiguration implements WebMvcConfigurer, WebMvcRegistrations {
    
    private final RequestMappingHandlerAdapter defaultRequestMappingHandlerAdapter = new EnhancedRequestMappingHandlerAdapter();
    
    /**
     * 自定义RequestMappingHandlerAdapter
     */
    @Override
    public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
        return defaultRequestMappingHandlerAdapter;
    }
    
}

(2)、支持feign-client,需要自定义相应的Converter来解析请求参数:

/**
 * feign-client在解析@RequestParam注解的复杂对象时,feign-client发起请求时将对象序列化为String的转换器
 * 
 */
public class ObjectRequestParamToStringConverter implements ConditionalGenericConverter {

    private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
    
    private final ObjectMapper objectMapper;
    
    public ObjectRequestParamToStringConverter() {
        super();
        this.objectMapper = JsonUtils.createDefaultObjectMapper();
        this.objectMapper.setSerializationInclusion(Include.NON_EMPTY);
    }

    @Override
    public Set getConvertibleTypes() {
        return Collections.singleton(new ConvertiblePair(Object.class, String.class));
    }

    @Override
    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
        try {
            return objectMapper.writeValueAsString(source);
        } catch (Exception e) {
            throw new ApplicationRuntimeException(e);
        }
    }

    @Override
    public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
        if(STRING_TYPE_DESCRIPTOR.equals(targetType)) {
            Class clazz = sourceType.getObjectType();
            if(!BeanUtils.isSimpleProperty(clazz)) {
                if(sourceType.hasAnnotation(RequestParam.class)) {
                    return true;
                }
            }
        }
        return false;
    }

}

/**
 * feign-client在解析@RequestParam注解的复杂对象时,在springmvc收到请求时将String反序列化为对象的转换器
 * 
 */
public class StringToObjectRequestParamConverter implements ConditionalGenericConverter {

    private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
    
    public StringToObjectRequestParamConverter() {
        super();
    }

    @Override
    public Set getConvertibleTypes() {
        return Collections.singleton(new ConvertiblePair(String.class, Object.class));
    }

    @Override
    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
        try {
            if(source != null && JsonUtils.isJsonObject(source.toString())) {
                return JsonUtils.json2Object(source.toString(), targetType.getObjectType());
            }
            return null;
        } catch (Exception e) {
            throw new ApplicationRuntimeException(e);
        }
    }

    @Override
    public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
        if(STRING_TYPE_DESCRIPTOR.equals(sourceType)) {
            Class clazz = targetType.getObjectType();
            if(!BeanUtils.isSimpleProperty(clazz)) {
                if(targetType.hasAnnotation(RequestParam.class)) {
                    return true;
                }
            }
        }
        return false;
    }

}

注册应用上面自定义的ObjectRequestParamToStringConverter、StringToObjectRequestParamConverter

@Configuration
@ConditionalOnClass(SpringMvcContract.class)
public class MyFeignClientsConfiguration implements WebMvcConfigurer {

    @Bean
    public List feignFormatterRegistrar() {
        return Arrays.asList(new DefaultFeignFormatterRegistrar());
    }
    
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToObjectRequestParamConverter());
    }

    public static class DefaultFeignFormatterRegistrar implements FeignFormatterRegistrar {
        
        @Override
        public void registerFormatters(FormatterRegistry registry) {
            registry.addConverter(new ObjectRequestParamToStringConverter());
        }
        
    }
    
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74722.html

相关文章

  • springcloud-feign实现服务发现

    摘要:本文重点介绍一下基于实现服务发现。使用方式下面我们开始的使用添加和的依赖添加注解开启服务发现,注解支持客户端。同样子,他可以使用默认的也可以使用或者修改配置文件服务名字服务无端口会随机选择一个服务集群名字注册中心地址,完成。 springcloud-feign实现服务发现 上一篇介绍了nacos实现配置和注册中心,在微服务中只有配置和注册中心远远不够,还需要有服务发现。本文重点介绍一...

    _DangJin 评论0 收藏0
  • Spring Cloud中如何优雅使用Feign调用接口

    摘要:对进行了封装,使其支持标准注解和。可以与和组合使用以支持负载均衡。中使用当我们搭建好注册中心之后,就是需要将自己的服务注册到中,然后别的服务可以直接调用。 JAVA 项目中接口调用怎么做 ? Httpclient Okhttp Httpurlconnection RestTemplate 上面是最常见的几种用法,我们今天要介绍的用法比上面的更简单,方便,它就是 Feign Feig...

    ChanceWong 评论0 收藏0
  • SpringCloud(第 024 篇)简单文件上传微服务加入zuul微服务后用zuul微服务地址实

    摘要:提供给文件上传微服务用的。注意注解能注册到服务上,是因为该注解包含了客户端的注解,该是一个复合注解。地址可以查看该微服务网关代理了多少微服务的。 SpringCloud(第 024 篇)简单文件上传微服务,并加入 zuul 微服务后用 zuul 微服务地址采取curl或者页面点击实现文件上传 - 一、大致介绍 1、本章节主要将文件上传微服务加入到 zuul 服务中去,然后利用 zuul...

    Cympros 评论0 收藏0
  • 使用servlet3.0异步特性改造spring-cloud-zuul

    摘要:不过在出来之后支持异步了,可以把业务操作放到独立的线程池里面去,这样可以尽快释放线程,本身也支持异步了,本篇文章将带你如何使用的异步特性来改造优化其性能。 ​ 我们知道spring-cloud-zuul是依赖springMVC来注册路由的,而springMVC又是在建立在servlet之上的(这里微服务专家杨波老师写过一篇文章讲述其网络模型,可以参考看看),在servlet3.0...

    HmyBmny 评论0 收藏0
  • SpringMVC入门笔记

    摘要:入门笔记简介是一种基于的实现了设计模式的请求驱动类型的轻量级框架,是系开源项目中的一个,和配合使用。配置在中需要添加使用的和映射规则。入门较快,而掌握起来相对较难。 SpringMVC入门笔记 1. 简介 Spring MVC是一种基于Java的实现了Web MVC设计模式的请求驱动类型的轻量级Web框架 ,是Spring系开源项目中的一个,和IoC配合使用。通过策略接口,Spring...

    zhaochunqi 评论0 收藏0

发表评论

0条评论

codergarden

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<