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