歡迎關注我的github睹耐,以后所有文章源碼都會陸續(xù)更新上去
遇到的困境
現(xiàn)我們服務提供端有如下的根據(jù)用戶查詢條件獲取滿足條件的用戶列表controller接口
@RestController
@RequestMapping("user")
public class UserController {
@GetMaping("search")
public List<User> search(User user) {
// ...
return list;
}
}
我們在使用Feign構建遠程服務請求客戶端的時候匈辱,會發(fā)現(xiàn)Feign官方版本是不支持GET請求傳遞自定義的對象愉粤,當我們的請求參數(shù)很多的時候,我們只能選擇以下兩種方式:
- @RequestParam注解方式泼掠,這種方式缺點很明顯怔软,查詢條件越多,feign方法參數(shù)越多择镇,而且我們是要求每一個微服務必須提供一個API jar包給其他小組使用的挡逼,這樣的話User對象完全沒法復用,而且純手寫@RequestParam增加了多余的開發(fā)量和出錯的風險
@FeignClient("user", path = "user")
public interface UserFeign {
@GetMapping("search")
public List<User> search(@RequestParam("user_id") int userId, @RequestParam("user_name") String userName, @RequestParam("gender") boolean gender);
}
- 使用Map傳遞參數(shù)腻豌,雖然解決了參數(shù)過多的問題家坎,但是一般我們都不建議直接使用Map傳遞參數(shù),因為沒有了強類型約束吝梅,編譯無法幫你保證程序的正確性和健壯虱疏,寫錯的風險依然存在,更致命的是服務消費端根本無法從這個API看出我到底可以傳遞哪些參數(shù)
Map<String, Object> userMap = new LinkedMultiValueMap();
userMap.put("user_id", 123);
userMap.put("user_name", "codingman1990");
@FeignClient("user", path = "user")
public interface UserFeign {
@GetMapping("search")
public List<User> search(Map<String, Object> userMap);
}
如何支持直接傳遞自定義對象
那么我們希望能有一種方式保持跟controller完全一致只需要傳遞自定義的對象憔涉,既讓服務提供端開發(fā)人員爽订框,也讓服務消費端開發(fā)人員爽,兩全其美兜叨。既然Feign官方不支持穿扳,那我們就自己動手擼源碼,自己來實現(xiàn)国旷。
-
AnnotatedParameterProcessor feign方法參數(shù)注解處理器矛物,總兩個方法:1.獲取當前參數(shù)注解類型;2.處理當前參數(shù)
image.png
除開第三個是我們自己的實現(xiàn)類外跪但,其余三個很明顯是分別處理@PathVariable,@Header以及@RequestParam注解的履羞,那么我們就可以依葫蘆畫瓢,再實現(xiàn)一個自己注解處理器
image.png -
@RequestObject 首先我們自定義這樣一個注解屡久,用于在feign方法上標記自定義對象
image.png -
RequestObjectParameterProcessor 自定義識別@RequestObject注解的處理器忆首。這里其實只做了一件事情,告訴context可以作為復雜查詢參數(shù)對象(可以是Map,@QueryMap被环,當然這里是我們自定義的@RequestObject)的參數(shù)下標糙及,后面讀取參數(shù)值的時候會用到。標紅的1是為了排除基本類型和包裝類型參數(shù)筛欢,它們是不可以作為復雜參數(shù)的
image.png -
QueryMapEncoder 就只有一個方法把參數(shù)對象轉(zhuǎn)換為Map
image.png -
RequestObjectQueryMapEncoder 自定義的map轉(zhuǎn)換器浸锨。具體實現(xiàn)里面做了很多細節(jié)優(yōu)化:
1.支持camel轉(zhuǎn)snake
2.支持Jackson的JsonProperty注解
3.支持枚舉序列化
4.支持JAVA8時間日期格式化
5.支持基本類型以及包裝類型數(shù)組
6.甚至還把分頁參數(shù)也兼容進來
以上細節(jié)可以根據(jù)自己的實際使用場景取舍,執(zhí)行完這些動作后版姑,放入Map中返回柱搜,等待feign構建request的時候直接使用
/**
* 把@RequestObject對象編碼為查詢參數(shù)Map對象(MethodMetadata.queryMapIndex是唯一可以自定義對象編碼的契機了)
*
* @author ty
*/
public class RequestObjectQueryMapEncoder implements QueryMapEncoder {
private final ConcurrentHashMap<Class<?>, List<Field>> fieldMap = new ConcurrentHashMap<>();
private final DateTimeFormatter LOCAL_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private final DateTimeFormatter LOCAL_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 專門應對{@link com.epet.microservices.common.web.Page}僅需要輸出的屬性
*/
private static final String[] PRESENT_FIELD_NAME = new String[]{"pageSize", "curPage"};
private static boolean JACKSON_PRESENT;
static {
try {
Class.forName("com.fasterxml.jackson.annotation.JsonProperty");
JACKSON_PRESENT = true;
} catch (ClassNotFoundException e) {
JACKSON_PRESENT = false;
}
}
@Override
public Map<String, Object> encode(Object object) {
if (ClassUtils.isPrimitiveOrWrapper(object.getClass())) {
throw new EncodeException("@ParamObject can't be primitive or wrapper type");
}
Class<?> clazz = object.getClass();
List<Field> fieldList = fieldMap.computeIfAbsent(clazz, this::fieldList);
/*List<Field> fieldList = fieldMap.get(clazz);
if (fieldList == null) {
fieldList = fieldList(clazz);
fieldMap.put(clazz, fieldList);
}*/
Map<String, Object> map = new HashMap<>(fieldList.size());
try {
for (Field field : fieldList) {
Object fieldObj = field.get(object);
if (fieldObj == null) {
continue;
}
Class<?> fieldClazz = field.getType();
String name;
// 支持@JsonProperty
if (JACKSON_PRESENT && field.getDeclaredAnnotation(JsonProperty.class) != null) {
name = field.getDeclaredAnnotation(JsonProperty.class).value();
} else {
// 默認camel轉(zhuǎn)snake
name = StringUtil.camel2Snake(field.getName());
}
// DeserializableEnum特殊處理
if (DeserializableEnum.class.isAssignableFrom(fieldClazz)) {
DeserializableEnum deserializableEnum = (DeserializableEnum) fieldObj;
map.put(name, deserializableEnum.getValue());
}
// LocalDate
else if (LocalDate.class.isAssignableFrom(fieldClazz)) {
String localDate = LOCAL_DATE_FORMATTER.format((LocalDate) fieldObj);
map.put(name, localDate);
}
// LocalDateTime
else if (LocalDateTime.class.isAssignableFrom(fieldClazz)) {
String localDateTime = LOCAL_DATE_TIME_FORMATTER.format((LocalDateTime) fieldObj);
map.put(name, localDateTime);
}
// 基本類型數(shù)組
else if (ClassUtil.isPrimitiveArray(fieldClazz)) {
// byte[]
if (ClassUtil.isByteArray(fieldClazz)) {
map.put(name, StringUtil.join((byte[]) fieldObj, ","));
}
// char[]
else if (ClassUtil.isCharArray(fieldClazz)) {
map.put(name, StringUtil.join((char[]) fieldObj, ","));
}
// short[]
else if (ClassUtil.isShortArray(fieldClazz)) {
map.put(name, StringUtil.join((short[]) fieldObj, ","));
}
// int[]
else if (ClassUtil.isIntArray(fieldClazz)) {
map.put(name, StringUtil.join((int[]) fieldObj, ","));
}
// float[]
else if (ClassUtil.isFloatArray(fieldClazz)) {
map.put(name, StringUtil.join((float[]) fieldObj, ","));
}
// long[]
else if (ClassUtil.isLongArray(fieldClazz)) {
map.put(name, StringUtil.join((long[]) fieldObj, ","));
}
// double[]
else if (ClassUtil.isDoubleArray(fieldClazz)) {
map.put(name, StringUtil.join((double[]) fieldObj, ","));
}
}
// 基本包裝類型數(shù)組
else if (ClassUtil.isPrimitiveWrapperArray(fieldClazz)) {
map.put(name, StringUtil.join((Object[]) fieldObj, ","));
}
// String[]
else if (String[].class.isAssignableFrom(fieldClazz)) {
map.put(name, StringUtil.join((String[]) fieldObj, ","));
} else {
map.put(name, fieldObj);
}
}
return map;
} catch (IllegalAccessException e) {
throw new EncodeException("Fail encode ParamObject into query Map", e);
}
}
private List<Field> fieldList(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
if (illegalField(field)) {
fields.add(field);
}
}
// 支持繼承的父類屬性
for (Class<?> superClazz : ClassUtils.getAllSuperclasses(clazz)) {
if (!Object.class.equals(superClazz)) {
// Page class
boolean isPage = superClazz.equals(Page.class);
Arrays.stream(superClazz.getDeclaredFields())
.filter(field -> !isPage || (isPage && Arrays.stream(PRESENT_FIELD_NAME).anyMatch(s -> s.equalsIgnoreCase(field.getName()))))
.forEach(field -> {
if (illegalField(field)) {
fields.add(field);
}
});
/*for (Field field : superClazz.getDeclaredFields()) {
if (illegalField(field)) {
fields.add(field);
}
}*/
}
}
return fields;
}
private boolean illegalField(Field field) {
Class<?> fieldType = field.getType();
// 暫時只能支持一層屬性編碼,所以必須是基礎類型或者包裝類型,基礎類型或者包裝類型數(shù)組,String,String[],DeserializableEnum類型
// 2019-3-8 fix:新增JAVA8 LocalDate和LocalDateTime支持
if (ClassUtils.isPrimitiveOrWrapper(fieldType)
|| ClassUtil.isPrimitiveOrWrapperArray(fieldType)
|| String.class.isAssignableFrom(fieldType) || String[].class.isAssignableFrom(fieldType)
|| DeserializableEnum.class.isAssignableFrom(fieldType)
|| LocalDateTime.class.isAssignableFrom(fieldType) || LocalDate.class.isAssignableFrom(fieldType)
// 2019-4-15 fix:新增BigDecimal和BigInteger支持
|| BigDecimal.class.isAssignableFrom(fieldType) || BigInteger.class.isAssignableFrom(fieldType)) {
if (!field.isAccessible()) {
field.setAccessible(true);
}
return true;
}
return false;
}
}
-
FeignRequestObjectAutoConfiguration 處理器和轉(zhuǎn)換器都寫好了,我們現(xiàn)在需要覆蓋feign默認的配置(查看FeignClientsConfiguration源碼即可理解)剥险,轉(zhuǎn)而使用我們自定義的聪蘸。兩個目的:
1.使用feign.request.object屬性可以開啟關閉,默認開啟
2.覆蓋默認的SpringMvcContract表制,內(nèi)部增加RequestObjectParameterProcessor
3.覆蓋默認Feign.Builder宇姚,使用我們自定義的RequestObjectQueryMapEncoder
/**
* 為支持復雜對象類型查詢參數(shù)自動配置類
*
* @author ty
*/
@Configuration
@ConditionalOnClass(Feign.class)
@ConditionalOnProperty(prefix = "feign.request", name = "object", havingValue = "true", matchIfMissing = true)
public class FeignRequestObjectAutoConfiguration {
/**
* 覆蓋FeignClientsConfiguration默認
*/
@Bean
public Contract feignContract(ConversionService feignConversionService) {
List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();
annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
// 新增的處理復雜對象類型查詢參數(shù)
annotatedArgumentResolvers.add(new RequestObjectParameterProcessor());
return new SpringMvcContract(annotatedArgumentResolvers, feignConversionService);
}
/**
* 覆蓋FeignClientsConfiguration默認
*/
@Configuration
@ConditionalOnClass({HystrixCommand.class, HystrixFeign.class})
protected static class HystrixFeignConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnProperty(name = "feign.hystrix.enabled")
public Feign.Builder feignHystrixBuilder() {
HystrixFeign.Builder builder = HystrixFeign.builder();
builder.queryMapEncoder(new RequestObjectQueryMapEncoder());
return builder;
}
}
}
- spring.factories 開啟自動配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.epet.microservices.common.feign.FeignRequestObjectAutoConfiguration
使用
對比之前的@RequestParam和Map用法,方法參數(shù)變少了夫凸,User對象復用了浑劳,對服務提供端和消費端都更方便了
@FeignClient("user", path = "user")
public interface UserFeign {
@GetMapping("search")
public List<User> search(@RequestObject User user);
}
后續(xù)
最近在調(diào)研spring cloud版本升級,發(fā)現(xiàn)新版的Feign也支持了自定義對象傳參夭拌,實現(xiàn)方式大同小異
-
@SpringQueryMap 等同于我們的@RequestObject
image.png -
QueryMapParameterProcessor 等同于我們的RequestObjectParameterProcessor
image.png -
FieldQueryMapEncoder和BeanQueryMapEncoder 等同于我們的RequestObjectQueryMapEncoder
image.png
個人覺得新版雖然官方支持了魔熏,但是功能卻是很弱,他只是簡單的反射獲取屬性名稱和值鸽扁,像我們前面提到的枚舉蒜绽,日期,camel轉(zhuǎn)snake等業(yè)務場景無法滿足桶现。只要能夠理解實現(xiàn)原理躲雅,其實實現(xiàn)自己的方案搭配自己的內(nèi)部框架使用起來會更方便和強大。