歡迎關(guān)注我的github,以后所有文章源碼都會(huì)陸續(xù)更新上去
遇到的困境
現(xiàn)我們服務(wù)提供端有如下的根據(jù)用戶查詢條件獲取滿足條件的用戶列表controller接口
@RestController
@RequestMapping("user")
public class UserController {
@GetMaping("search")
public List<User> search(User user) {
// ...
return list;
}
}
我們?cè)谑褂肍eign構(gòu)建遠(yuǎn)程服務(wù)請(qǐng)求客戶端的時(shí)候,會(huì)發(fā)現(xiàn)Feign官方版本是不支持GET請(qǐng)求傳遞自定義的對(duì)象,當(dāng)我們的請(qǐng)求參數(shù)很多的時(shí)候,我們只能選擇以下兩種方式:
- @RequestParam注解方式,這種方式缺點(diǎn)很明顯,查詢條件越多,feign方法參數(shù)越多,而且我們是要求每一個(gè)微服務(wù)必須提供一個(gè)API jar包給其他小組使用的,這樣的話User對(duì)象完全沒(méi)法復(fù)用,而且純手寫(xiě)@RequestParam增加了多余的開(kāi)發(fā)量和出錯(cuò)的風(fēng)險(xiǎn)
@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ù)過(guò)多的問(wèn)題,但是一般我們都不建議直接使用Map傳遞參數(shù),因?yàn)闆](méi)有了強(qiáng)類(lèi)型約束,編譯無(wú)法幫你保證程序的正確性和健壯,寫(xiě)錯(cuò)的風(fēng)險(xiǎn)依然存在,更致命的是服務(wù)消費(fèi)端根本無(wú)法從這個(gè)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);
}
如何支持直接傳遞自定義對(duì)象
那么我們希望能有一種方式保持跟controller完全一致只需要傳遞自定義的對(duì)象,既讓服務(wù)提供端開(kāi)發(fā)人員爽,也讓服務(wù)消費(fèi)端開(kāi)發(fā)人員爽,兩全其美。既然Feign官方不支持,那我們就自己動(dòng)手?jǐn)]源碼,自己來(lái)實(shí)現(xiàn)。
-
AnnotatedParameterProcessor feign方法參數(shù)注解處理器,總兩個(gè)方法:1.獲取當(dāng)前參數(shù)注解類(lèi)型;2.處理當(dāng)前參數(shù)
image.png
除開(kāi)第三個(gè)是我們自己的實(shí)現(xiàn)類(lèi)外,其余三個(gè)很明顯是分別處理@PathVariable,@Header以及@RequestParam注解的,那么我們就可以依葫蘆畫(huà)瓢,再實(shí)現(xiàn)一個(gè)自己注解處理器
image.png -
@RequestObject 首先我們自定義這樣一個(gè)注解,用于在feign方法上標(biāo)記自定義對(duì)象
image.png -
RequestObjectParameterProcessor 自定義識(shí)別@RequestObject注解的處理器。這里其實(shí)只做了一件事情,告訴context可以作為復(fù)雜查詢參數(shù)對(duì)象(可以是Map,@QueryMap,當(dāng)然這里是我們自定義的@RequestObject)的參數(shù)下標(biāo),后面讀取參數(shù)值的時(shí)候會(huì)用到。標(biāo)紅的1是為了排除基本類(lèi)型和包裝類(lèi)型參數(shù),它們是不可以作為復(fù)雜參數(shù)的
image.png -
QueryMapEncoder 就只有一個(gè)方法把參數(shù)對(duì)象轉(zhuǎn)換為Map
image.png -
RequestObjectQueryMapEncoder 自定義的map轉(zhuǎn)換器。具體實(shí)現(xiàn)里面做了很多細(xì)節(jié)優(yōu)化:
1.支持camel轉(zhuǎn)snake
2.支持Jackson的JsonProperty注解
3.支持枚舉序列化
4.支持JAVA8時(shí)間日期格式化
5.支持基本類(lèi)型以及包裝類(lèi)型數(shù)組
6.甚至還把分頁(yè)參數(shù)也兼容進(jìn)來(lái)
以上細(xì)節(jié)可以根據(jù)自己的實(shí)際使用場(chǎng)景取舍,執(zhí)行完這些動(dòng)作后,放入Map中返回,等待feign構(gòu)建request的時(shí)候直接使用
/**
* 把@RequestObject對(duì)象編碼為查詢參數(shù)Map對(duì)象(MethodMetadata.queryMapIndex是唯一可以自定義對(duì)象編碼的契機(jī)了)
*
* @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");
/**
* 專(zhuān)門(mén)應(yīng)對(duì){@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 {
// 默認(rèn)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);
}
// 基本類(lèi)型數(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, ","));
}
}
// 基本包裝類(lèi)型數(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);
}
}
// 支持繼承的父類(lèi)屬性
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í)只能支持一層屬性編碼,所以必須是基礎(chǔ)類(lèi)型或者包裝類(lèi)型,基礎(chǔ)類(lèi)型或者包裝類(lèi)型數(shù)組,String,String[],DeserializableEnum類(lèi)型
// 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ě)好了,我們現(xiàn)在需要覆蓋feign默認(rèn)的配置(查看FeignClientsConfiguration源碼即可理解),轉(zhuǎn)而使用我們自定義的。兩個(gè)目的:
1.使用feign.request.object屬性可以開(kāi)啟關(guān)閉,默認(rèn)開(kāi)啟
2.覆蓋默認(rèn)的SpringMvcContract,內(nèi)部增加RequestObjectParameterProcessor
3.覆蓋默認(rèn)Feign.Builder,使用我們自定義的RequestObjectQueryMapEncoder
/**
* 為支持復(fù)雜對(duì)象類(lèi)型查詢參數(shù)自動(dòng)配置類(lèi)
*
* @author ty
*/
@Configuration
@ConditionalOnClass(Feign.class)
@ConditionalOnProperty(prefix = "feign.request", name = "object", havingValue = "true", matchIfMissing = true)
public class FeignRequestObjectAutoConfiguration {
/**
* 覆蓋FeignClientsConfiguration默認(rèn)
*/
@Bean
public Contract feignContract(ConversionService feignConversionService) {
List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();
annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
// 新增的處理復(fù)雜對(duì)象類(lèi)型查詢參數(shù)
annotatedArgumentResolvers.add(new RequestObjectParameterProcessor());
return new SpringMvcContract(annotatedArgumentResolvers, feignConversionService);
}
/**
* 覆蓋FeignClientsConfiguration默認(rèn)
*/
@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 開(kāi)啟自動(dòng)配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.epet.microservices.common.feign.FeignRequestObjectAutoConfiguration
使用
對(duì)比之前的@RequestParam和Map用法,方法參數(shù)變少了,User對(duì)象復(fù)用了,對(duì)服務(wù)提供端和消費(fèi)端都更方便了
@FeignClient("user", path = "user")
public interface UserFeign {
@GetMapping("search")
public List<User> search(@RequestObject User user);
}
后續(xù)
最近在調(diào)研spring cloud版本升級(jí),發(fā)現(xiàn)新版的Feign也支持了自定義對(duì)象傳參,實(shí)現(xiàn)方式大同小異
-
@SpringQueryMap 等同于我們的@RequestObject
image.png -
QueryMapParameterProcessor 等同于我們的RequestObjectParameterProcessor
image.png -
FieldQueryMapEncoder和BeanQueryMapEncoder 等同于我們的RequestObjectQueryMapEncoder
image.png
個(gè)人覺(jué)得新版雖然官方支持了,但是功能卻是很弱,他只是簡(jiǎn)單的反射獲取屬性名稱和值,像我們前面提到的枚舉,日期,camel轉(zhuǎn)snake等業(yè)務(wù)場(chǎng)景無(wú)法滿足。只要能夠理解實(shí)現(xiàn)原理,其實(shí)實(shí)現(xiàn)自己的方案搭配自己的內(nèi)部框架使用起來(lái)會(huì)更方便和強(qiáng)大。







