Spring Cloud Feign繫結列舉型別引數
在之前的文章《Spring Boot繫結列舉型別引數》中,我們討論了Spring中的Converter和ConverterFactory,以及如何與Spring Boot整合以使得WebMVC能夠接收列舉型別的引數。現在Spring Cloud已經逐漸流行了起來,其中最流行的要數Spring Cloud Netflix系列了。Netflix有個很重要的服務治理中介軟體Eureka,Feign由於其聲名式的使用方式,使用@RequestMapping, @RequestParam, @PathVariable等傳統的Spring Web MVC註解得以廣泛使用。不過正如Spring Web MVC一樣,在繫結引數的時候也會出現繫結自定義型別(例如列舉)的需求,在程式碼中手動進行轉換當然能解決問題,但是這樣終究不夠優雅。經過稍加研究之後,分享給大家。
我們都知道,Feign是Spring Cloud的眾多實現之一,自然也可以使用Spring的功能進行配置。於是Spring的Converter又可以派上用場了(詳見《Spring Boot繫結列舉型別引數》)。那麼怎麼才能將已經寫好的Converter用到Feign當中呢?
很顯然,我們需要重新配置一下Feign。關於Feign的配置,可以點開@FeignClient註解,在這裡發現這樣一段程式碼:
清單1 @FeignClient中的Configuration註解
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface FeignClient { // ... /** * A custom <code>@Configuration</code> for the feign client. Can contain override * <code>@Bean</code> definition for the pieces that make up the client, for instance * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}. * * @see FeignClientsConfiguration for the defaults */ Class<?>[] configuration() default {}; // ... }
可以看到,只要寫一個配置類,讓@FeignClient的configuration欄位指向這個配置類就可以解決這個問題,而且官方的原始碼中還給出了一個預設的配置類FeignClientsConfiguration。既然要寫配置類,那毫無疑問要看看這個預設配置類FeignClientsConfiguration怎麼寫。
清單2 官方的FeignClientsConfiguration實現
@Configuration public class FeignClientsConfiguration { @Autowired private ObjectFactory<HttpMessageConverters> messageConverters; @Autowired(required = false) private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>(); @Autowired(required = false) private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>(); @Autowired(required = false) private Logger logger; @Bean @ConditionalOnMissingBean public Decoder feignDecoder() { return new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)); } @Bean @ConditionalOnMissingBean public Encoder feignEncoder() { return new SpringEncoder(this.messageConverters); } @Bean @ConditionalOnMissingBean public Contract feignContract(ConversionService feignConversionService) { return new SpringMvcContract(this.parameterProcessors, feignConversionService); } @Bean public FormattingConversionService feignConversionService() { FormattingConversionService conversionService = new DefaultFormattingConversionService(); for (FeignFormatterRegistrar feignFormatterRegistrar : feignFormatterRegistrars) { feignFormatterRegistrar.registerFormatters(conversionService); } return conversionService; } @Configuration @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class }) protected static class HystrixFeignConfiguration { @Bean @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = false) public Feign.Builder feignHystrixBuilder() { return HystrixFeign.builder(); } } @Bean @ConditionalOnMissingBean public Retryer feignRetryer() { return Retryer.NEVER_RETRY; } @Bean @Scope("prototype") @ConditionalOnMissingBean public Feign.Builder feignBuilder(Retryer retryer) { return Feign.builder().retryer(retryer); } @Bean @ConditionalOnMissingBean(FeignLoggerFactory.class) public FeignLoggerFactory feignLoggerFactory() { return new DefaultFeignLoggerFactory(logger); } }
重點關注程式碼的28-41行。可以看到,Feign的ApplicationContxt中配置了一個FormattingConversionService,Feign就是通過這個FormattingConversionService來進行型別轉換的。在這個FormattingConversionService的父類(已經差了兩輩了)GenericConversionService中可以找到:
清單3 GenericConversionService中配置Converter和ConverterFactory
public class GenericConversionService implements ConfigurableConversionService {
// ...
@Override
public void addConverter(Converter<?, ?> converter) {
// ...
}
@Override
public <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter) {
// ...
}
@Override
public void addConverter(GenericConverter converter) {
// ...
}
@Override
public void addConverterFactory(ConverterFactory<?, ?> factory) {
// ...
}
@Override
public void removeConvertible(Class<?> sourceType, Class<?> targetType) {
// ...
}
// ...
}
嘿嘿,你發現了什麼?正是那熟悉的addConverter和addConverterFactory!有了這個,只要在新寫好的Feign配置類中,在這個FormattingConversionService中呼叫這些方法就可以把寫好的Converter和ConverterFactory註冊進去。不過在清單2的feignContract()方法中的入參是ConversionService,並且還指定了@ConditionalOnMissingBean註解,而這個feignConversionService()方法則沒有指定@ConditionalOnMissingBean註解,說明官方並不想讓我們直接覆蓋FormattingConversionService,而是通過呼叫feignContract()方法註冊Converter和ConverterFactory。
因此最後的思路就是,寫一個Feign的配置類,裡面要配置一個返回型別為Contract(就是官方FeignClientsConfiguration中的feignContract()返回型別)的Bean,在這個Bean中利用ConversionService註冊Converter和ConverterFactory,寫好之後再把這個配置類寫到@FeignClient註解的configuration欄位中,這樣就能夠實現自定義型別轉換。這裡面有一個小坑,就是這個配置類所在的包不能在@ComponentScan能夠掃到的路徑中。參考官方文件:
The FooConfiguration has to be @configuration but take care that it is not in a @componentscan for the main application context, otherwise it will be used for every @feignclient. If you use @componentscan (or @springbootapplication) you need to take steps to avoid it being included (for instance put it in a separate, non-overlapping package, or specify the packages to scan explicitly in the @componentscan).
好了,既然思路清晰了,就開始幹活。
首先,列舉和實現介面定義如下
清單4 列舉的定義
public enum Language implements NamedEnum {
UNLIMITED("--"),
// 英語
ENGLISH("en"),
// 簡體中文
CHINESE_SIMPLIFIED("zh-CN"),
// 繁體中文
CHINESE_TRADITIONAL("zh-TW");
//語言的ISO_639-1縮寫
private String name;
public String getName() {
return name;
}
Language(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
清單5 介面NamedEnum的定義
package org.fhp.springclouddemo.enums;
import org.fhp.springclouddemo.exceptions.NoMatchedEnumException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
public interface NamedEnum extends Serializable {
String getName();
Map<Class, Map> ENUM_MAP = new HashMap<>();
static <E extends Enum & NamedEnum> E getByName(String name, Class<E> clazz) throws NoMatchedEnumException {
// Class<E> clazz = E.class;
Map enumMap = ENUM_MAP.get(clazz);
if(null == enumMap) {
E[] enums = clazz.getEnumConstants();
enumMap = new HashMap<String, E>();
for(E current : enums) {
enumMap.put(current.getName(), current);
}
}
E result = (E) enumMap.get(name);
if(result != null) {
return result;
} else {
throw new NoMatchedEnumException("No element matches " + name);
}
}
}
其中,NoMatchEnumException為自定義異常,可自行實現。接下來我們實現Converter。這個無需實現ConverterFactory,只實現Converter即可。
清單6 列舉轉換Converter
package org.fhp.springclouddemo.common;
import org.fhp.springclouddemo.enums.NamedEnum;
import org.springframework.core.convert.converter.Converter;
public class UniversalReversedEnumConverter implements Converter<NamedEnum, String> {
@Override
public String convert(NamedEnum source) {
return source.getName();
}
}
現在Converter和列舉都有了,我們就可以上主菜了。
清單7 Feign Client的配置
package org.fhp.springclouddemo.service;
import com.alibaba.fastjson.JSONObject;
import org.fhp.springclouddemo.enums.Language;
import org.fhp.springclouddemo.feignconfig.UDFeignClientsConfiguration;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "interpatch", configuration = MyFeignClientsConfiguration.class)
public interface HelloFeignService {
@RequestMapping(method = RequestMethod.GET, value = "/api/patch/search")
JSONObject hello(@RequestParam(value="word") String word,
@RequestParam(value="from") Language from,
@RequestParam(value="to") Language to);
}
其中,MyFeignClientsConfiguration的配置如下:清單8 Feign配置檔案的實現
package org.fhp.springclouddemo.feignconfig;
import feign.Contract;
import org.fhp.springclouddemo.common.UniversalReversedEnumConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
import org.springframework.cloud.netflix.feign.support.SpringMvcContract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.support.FormattingConversionService;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class MyFeignClientsConfiguration {
@Autowired(required = false)
private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();
@Bean
public Contract feignContract(FormattingConversionService feignConversionService) {
//在原配置類中是用ConversionService型別的引數,但ConversionService介面不支援addConverter操作,使用FormattingConversionService仍然可以實現feignContract配置。
feignConversionService.addConverter(new UniversalReversedEnumConverter());
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}
}
大功告成!現在可以用Feign繫結列舉型別的引數了。