在之前的文章《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的配置如下:
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绑定枚举类型的参数了。