让springcloud feign-client 完全支持springmvc的@RequestParam注解的特性

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):

@RestController
public 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的原生写法):

@RestController
public 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到容器中去

@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<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());
        }
        
    }
    
}
    原文作者:penggle
    原文地址: https://segmentfault.com/a/1190000019349695
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞