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