让springcloud-feignclient-完全支持springmvc的RequestParam注解的特性

9次阅读

共计 13732 个字符,预计需要花费 35 分钟才能阅读完成。

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

正文完
 0