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绑定枚举类型的参数了。



  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值