1. 程式人生 > >Spring Cloud Feign繫結列舉型別引數

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繫結列舉型別的引數了。