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<List<Product>> 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<Order> orders;        Sort() {        super();    }        Sort(List<Order> orders) {        super();        this.orders = orders;    }        public static Sort by(Order... orders) {        return new Sort(Arrays.asList(orders));    }    public List<Order> getOrders() {        return orders;    }    public void setOrders(List<Order> 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<List<Product>> getProductListByPage(Product condition, Page page, Sort sort) {        List<Product> 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):

@RestControllerpublic class ProductController implements ProductApiService {    //远程调用provider的feign代理服务    @Resource(name="productApiService")    private ProductApiService productApiService;    @Override    public PageResult<List<Product>> 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&currentPage=1&pageSize=20&orders=createTime:desc,updateTime:desc

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

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

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

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

public interface ProductApiService {    @GetMapping(value="/api/product/list", produces=APPLICATION_JSON)    public PageResult<List<Product>> 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<List<Product>> getProductListByPage1(@RequestParam Product condition, @RequestParam Page page, @RequestParam Sort sort) {        //...    }        或者        @GetMapping(value="/api/product/list2", produces=APPLICATION_JSON)    public PageResult<List<Product>> 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&currentPage=1&pageSize=20&orders=createTime:desc,updateTime:desc *  * @GetMapping(value="/api/user/list", produces=APPLICATION_JSON) * public PageResult<List<User>> getUserListByPage( @RequestParam User condition, @RequestParam Page page, @RequestParam Sort sort ); *  * 如上例所示,请求1的参数能够正确地被@RequestParam注解解析,但是请求2却不行,该实现即是解决此问题的 *  */public class EnhancedRequestParamMethodArgumentResolver extends RequestParamMethodArgumentResolver {    /**     * 明确指出的可解析的参数类型列表     */    private List<Class<?>> 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<String,Object> 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<String,Object> getRequestParameters(HttpServletRequest request) {        Map<String,Object> parameters = new HashMap<String,Object>();        Map<String,String[]> 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<Class<?>> getResolvableParameterTypes() {        return resolvableParameterTypes;    }    public void setResolvableParameterTypes(List<Class<?>> resolvableParameterTypes) {        this.resolvableParameterTypes = resolvableParameterTypes;    }    }public class SpringBeanUtils {        /**     * 将properties中的值填充到指定bean中去     * @param bean     * @param properties     * @param conversionService     */    public static void setBeanProperty(Object bean, Map<String,Object> properties, ConversionService conversionService) {        Assert.notNull(bean, "Parameter 'bean' can not be null!");        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);        beanWrapper.setConversionService(conversionService);        for(Map.Entry<String,Object> 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<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<HandlerMethodArgumentResolver>(getArgumentResolvers());        replaceRequestParamMethodArgumentResolvers(argumentResolvers);        setArgumentResolvers(argumentResolvers);                List<HandlerMethodArgumentResolver> initBinderArgumentResolvers = new ArrayList<HandlerMethodArgumentResolver>(getInitBinderArgumentResolvers());        replaceRequestParamMethodArgumentResolvers(initBinderArgumentResolvers);        setInitBinderArgumentResolvers(initBinderArgumentResolvers);    }        /**     * 替换RequestParamMethodArgumentResolver为增强版的EnhancedRequestParamMethodArgumentResolver     * @param methodArgumentResolvers     */    protected void replaceRequestParamMethodArgumentResolvers(List<HandlerMethodArgumentResolver> 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到容器中去

@Configurationpublic 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<ConvertiblePair> 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<ConvertiblePair> 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> 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());        }            }    }