#SpringCloud feign 服务访问 404问题 SpringCloud微服务分布式 311106863 ##一、问题 ###1.问题说明 feignClient默认只能在方法的RequestMapping 上加全部url路径,
类的RequestMapping,不能合并到方法上;
http://www.jianshu.com/p/191d45210d16 详见问题十 ###2.错误及代码示例 如:访问全路径url时/sys/user/login时,导致找不到服务,出现404错误。
api服务网关或消费者返回错误信息
Caused by: feign.FeignException: status 404 reading UserClient#login(String); content:
{"timestamp":1484022879514,"status":404,"error":"Not Found","message":"No message available","path":"/login"}
at feign.FeignException.errorStatus(FeignException.java:62)
示例代码
@FeignClient(value = "sys-service",configuration = FeignClientsConfiguration.class)
@RequestMapping("/sys/user")
public interface UserClient extends BaseFeignClient<User, Long> {
@CustomScript
/**
* 根据用户名,查询用户信息
*/
@RequestMapping(value = "/login")
@RequiresPermissions(PermissionUtils.VIEW)
@ResponseBody
BaseResponse<User> login(@RequestParam("loginName")String loginName...);
}
##二、解决方案 ###1. 合并类和方法的url
SpringMvcContractCustom 拷贝自SpringMvcContract,增加类和方法url合并
###2. 增加自定义配置类
FeignClientsConfigurationCustom 拷贝自FeignClientsConfiguration
修改feignContract 方法 return new SpringMvcContractCustom
###3. feign客户端增加自定义配置类
@FeignClient(value = "sys-service",configuration = FeignClientsConfigurationCustom.class)
##三、修改代码示意 SpringMvcContractCustom 拷贝自SpringMvcContract
去掉类的url path
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
//hongliangpan remove this line
//data.template().insert(0, pathValue);
合并方法和类的path
//note hongliangpan add merge RequestMapping path and method path
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation,
Method method) {
//note hongliangpan add merge class path and method path
RequestMapping anno = method.getDeclaringClass().getAnnotation(RequestMapping.class);
if (anno != null && anno.value().length > 0) {
String basePath = anno.value()[0];
if (!Strings.isNullOrEmpty(basePath) && !pathValue.startsWith(basePath)) {
pathValue = basePath + pathValue;
}
}
data.template().append(pathValue);
配置类FeignClientsConfigurationCustom
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
return new SpringMvcContractCustom(this.parameterProcessors, feignConversionService);
}
##四、完整代码
###1. FeignClientsConfigurationCustom
import java.util.ArrayList;
import java.util.List;
import com.glodon.cloud.feign.SpringMvcContractCustom;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
import org.springframework.cloud.netflix.feign.DefaultFeignLoggerFactory;
import org.springframework.cloud.netflix.feign.FeignFormatterRegistrar;
import org.springframework.cloud.netflix.feign.FeignLoggerFactory;
import org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder;
import org.springframework.cloud.netflix.feign.support.SpringDecoder;
import org.springframework.cloud.netflix.feign.support.SpringEncoder;
import org.springframework.cloud.netflix.feign.support.SpringMvcContract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import com.netflix.hystrix.HystrixCommand;
import feign.Contract;
import feign.Feign;
import feign.Logger;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.hystrix.HystrixFeign;
/**
* @author Dave Syer
* @author Venil Noronha
*/
@Configuration
public class FeignClientsConfigurationCustom {
@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 SpringMvcContractCustom(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 = true)
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
}
}
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public Feign.Builder feignBuilder() {
return Feign.builder();
}
@Bean
@ConditionalOnMissingBean(FeignLoggerFactory.class)
public FeignLoggerFactory feignLoggerFactory() {
return new DefaultFeignLoggerFactory(logger);
}
}
###2. SpringMvcContractCustom
import com.google.common.base.Strings;
import feign.Contract;
import feign.Feign;
import feign.MethodMetadata;
import feign.Param;
import org.springframework.beans.BeanUtils;
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
import org.springframework.cloud.netflix.feign.annotation.PathVariableParameterProcessor;
import org.springframework.cloud.netflix.feign.annotation.RequestHeaderParameterProcessor;
import org.springframework.cloud.netflix.feign.annotation.RequestParamParameterProcessor;
import org.springframework.cloud.netflix.feign.support.SpringMvcContract;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.*;
import static feign.Util.checkState;
import static feign.Util.emptyToNull;
import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;
/**
* @author Spencer Gibb
*/
//@Component
public class SpringMvcContractCustom extends Contract.BaseContract
implements ResourceLoaderAware {
private static final String ACCEPT = "Accept";
private static final String CONTENT_TYPE = "Content-Type";
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
private final Map<Class<? extends Annotation>, AnnotatedParameterProcessor> annotatedArgumentProcessors;
private final Map<String, Method> processedMethods = new HashMap<>();
private final ConversionService conversionService;
private final Param.Expander expander;
private ResourceLoader resourceLoader = new DefaultResourceLoader();
public SpringMvcContractCustom() {
this(Collections.<AnnotatedParameterProcessor>emptyList());
}
public SpringMvcContractCustom(
List<AnnotatedParameterProcessor> annotatedParameterProcessors) {
this(annotatedParameterProcessors, new DefaultConversionService());
}
public SpringMvcContractCustom(
List<AnnotatedParameterProcessor> annotatedParameterProcessors,
ConversionService conversionService) {
Assert.notNull(annotatedParameterProcessors,
"Parameter processors can not be null.");
Assert.notNull(conversionService, "ConversionService can not be null.");
List<AnnotatedParameterProcessor> processors;
if (!annotatedParameterProcessors.isEmpty()) {
processors = new ArrayList<>(annotatedParameterProcessors);
} else {
processors = getDefaultAnnotatedArgumentsProcessors();
}
this.annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
this.conversionService = conversionService;
this.expander = new SpringMvcContract.ConvertingExpander(conversionService);
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
if (clz.getInterfaces().length == 0) {
RequestMapping classAnnotation = findMergedAnnotation(clz,
RequestMapping.class);
if (classAnnotation != null) {
// Prepend path from class annotation if specified
if (classAnnotation.value().length > 0) {
String pathValue = emptyToNull(classAnnotation.value()[0]);
pathValue = resolve(pathValue);
if (!pathValue.startsWith("/")) {
pathValue = "/" + pathValue;
}
//hongliangpan remove this line
//data.template().insert(0, pathValue);
}
}
}
}
@Override
public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
this.processedMethods.put(Feign.configKey(targetType, method), method);
MethodMetadata md = super.parseAndValidateMetadata(targetType, method);
RequestMapping classAnnotation = findMergedAnnotation(targetType,
RequestMapping.class);
if (classAnnotation != null) {
//hongliangpan add
//md = super.parseAndValidateMetadata(targetType, method);
// produces - use from class annotation only if method has not specified this
if (!md.template().headers().containsKey(ACCEPT)) {
parseProduces(md, method, classAnnotation);
}
// consumes -- use from class annotation only if method has not specified this
if (!md.template().headers().containsKey(CONTENT_TYPE)) {
parseConsumes(md, method, classAnnotation);
}
// headers -- class annotation is inherited to methods, always write these if
// present
parseHeaders(md, method, classAnnotation);
}
return md;
}
@Override
//note hongliangpan add merge RequestMapping path and method path
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation,
Method method) {
if (!(methodAnnotation instanceof RequestMapping)) {
return;
}
RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
// HTTP Method
RequestMethod[] methods = methodMapping.method();
if (methods.length == 0) {
methods = new RequestMethod[]{RequestMethod.GET};
}
checkOne(method, methods, "method");
data.template().method(methods[0].name());
// path
checkAtMostOne(method, methodMapping.value(), "value");
if (methodMapping.value().length > 0) {
String pathValue = emptyToNull(methodMapping.value()[0]);
if (pathValue != null) {
pathValue = resolve(pathValue);
// Append path from @RequestMapping if value is present on method
if (!pathValue.startsWith("/")
&& !data.template().toString().endsWith("/")) {
pathValue = "/" + pathValue;
}
//note hongliangpan add merge class path and method path
RequestMapping anno = method.getDeclaringClass().getAnnotation(RequestMapping.class);
if (anno != null && anno.value().length > 0) {
String basePath = anno.value()[0];
if (!Strings.isNullOrEmpty(basePath) && !pathValue.startsWith(basePath)) {
pathValue = basePath + pathValue;
}
}
data.template().append(pathValue);
}
}
// produces
parseProduces(data, method, methodMapping);
// consumes
parseConsumes(data, method, methodMapping);
// headers
parseHeaders(data, method, methodMapping);
data.indexToExpander(new LinkedHashMap<Integer, Param.Expander>());
}
private String resolve(String value) {
if (StringUtils.hasText(value)
&& this.resourceLoader instanceof ConfigurableApplicationContext) {
return ((ConfigurableApplicationContext) this.resourceLoader).getEnvironment()
.resolvePlaceholders(value);
}
return value;
}
private void checkAtMostOne(Method method, Object[] values, String fieldName) {
checkState(values != null && (values.length == 0 || values.length == 1),
"Method %s can only contain at most 1 %s field. Found: %s",
method.getName(), fieldName,
values == null ? null : Arrays.asList(values));
}
private void checkOne(Method method, Object[] values, String fieldName) {
checkState(values != null && values.length == 1,
"Method %s can only contain 1 %s field. Found: %s", method.getName(),
fieldName, values == null ? null : Arrays.asList(values));
}
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data,
Annotation[] annotations, int paramIndex) {
boolean isHttpAnnotation = false;
AnnotatedParameterProcessor.AnnotatedParameterContext context = new SimpleAnnotatedParameterContext(
data, paramIndex);
Method method = this.processedMethods.get(data.configKey());
for (Annotation parameterAnnotation : annotations) {
AnnotatedParameterProcessor processor = this.annotatedArgumentProcessors
.get(parameterAnnotation.annotationType());
if (processor != null) {
Annotation processParameterAnnotation;
// synthesize, handling @AliasFor, while falling back to parameter name on
// missing String #value():
processParameterAnnotation = synthesizeWithMethodParameterNameAsFallbackValue(
parameterAnnotation, method, paramIndex);
isHttpAnnotation |= processor.processArgument(context,
processParameterAnnotation);
}
}
if (isHttpAnnotation && data.indexToExpander().get(paramIndex) == null
&& !isMultiValued(method.getParameterTypes()[paramIndex])
&& this.conversionService.canConvert(
method.getParameterTypes()[paramIndex], String.class)) {
data.indexToExpander().put(paramIndex, this.expander);
}
return isHttpAnnotation;
}
private boolean isMultiValued(Class<?> type) {
// Feign will deal with each element in a collection individually (with no
// expander as of 8.16.2, but we'd rather have no conversion than convert a
// collection to a String (which ends up being a csv).
return Collection.class.isAssignableFrom(type);
}
private void parseProduces(MethodMetadata md, Method method,
RequestMapping annotation) {
checkAtMostOne(method, annotation.produces(), "produces");
String[] serverProduces = annotation.produces();
String clientAccepts = serverProduces.length == 0 ? null
: emptyToNull(serverProduces[0]);
if (clientAccepts != null) {
md.template().header(ACCEPT, clientAccepts);
}
}
private void parseConsumes(MethodMetadata md, Method method,
RequestMapping annotation) {
checkAtMostOne(method, annotation.consumes(), "consumes");
String[] serverConsumes = annotation.consumes();
String clientProduces = serverConsumes.length == 0 ? null
: emptyToNull(serverConsumes[0]);
if (clientProduces != null) {
md.template().header(CONTENT_TYPE, clientProduces);
}
}
private void parseHeaders(MethodMetadata md, Method method,
RequestMapping annotation) {
// TODO: only supports one header value per key
if (annotation.headers() != null && annotation.headers().length > 0) {
for (String header : annotation.headers()) {
int index = header.indexOf('=');
md.template().header(resolve(header.substring(0, index)),
resolve(header.substring(index + 1).trim()));
}
}
}
private Map<Class<? extends Annotation>, AnnotatedParameterProcessor> toAnnotatedArgumentProcessorMap(
List<AnnotatedParameterProcessor> processors) {
Map<Class<? extends Annotation>, AnnotatedParameterProcessor> result = new HashMap<>();
for (AnnotatedParameterProcessor processor : processors) {
result.put(processor.getAnnotationType(), processor);
}
return result;
}
private List<AnnotatedParameterProcessor> getDefaultAnnotatedArgumentsProcessors() {
List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();
annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
return annotatedArgumentResolvers;
}
private Annotation synthesizeWithMethodParameterNameAsFallbackValue(
Annotation parameterAnnotation, Method method, int parameterIndex) {
Map<String, Object> annotationAttributes = AnnotationUtils
.getAnnotationAttributes(parameterAnnotation);
Object defaultValue = AnnotationUtils.getDefaultValue(parameterAnnotation);
if (defaultValue instanceof String
&& defaultValue.equals(annotationAttributes.get(AnnotationUtils.VALUE))) {
Type[] parameterTypes = method.getGenericParameterTypes();
String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
if (shouldAddParameterName(parameterIndex, parameterTypes, parameterNames)) {
annotationAttributes.put(AnnotationUtils.VALUE,
parameterNames[parameterIndex]);
}
}
return AnnotationUtils.synthesizeAnnotation(annotationAttributes,
parameterAnnotation.annotationType(), null);
}
private boolean shouldAddParameterName(int parameterIndex, Type[] parameterTypes, String[] parameterNames) {
// has a parameter name
return parameterNames != null && parameterNames.length > parameterIndex
// has a type
&& parameterTypes != null && parameterTypes.length > parameterIndex
// and it is a simple property
&& BeanUtils.isSimpleProperty(parameterTypes[parameterIndex].getClass());
}
private class SimpleAnnotatedParameterContext
implements AnnotatedParameterProcessor.AnnotatedParameterContext {
private final MethodMetadata methodMetadata;
private final int parameterIndex;
public SimpleAnnotatedParameterContext(MethodMetadata methodMetadata,
int parameterIndex) {
this.methodMetadata = methodMetadata;
this.parameterIndex = parameterIndex;
}
@Override
public MethodMetadata getMethodMetadata() {
return this.methodMetadata;
}
@Override
public int getParameterIndex() {
return this.parameterIndex;
}
@Override
public void setParameterName(String name) {
nameParam(this.methodMetadata, name, this.parameterIndex);
}
@Override
public Collection<String> setTemplateParameter(String name,
Collection<String> rest) {
return addTemplatedParam(rest, name);
}
}
public static class ConvertingExpander implements Param.Expander {
private final ConversionService conversionService;
public ConvertingExpander(ConversionService conversionService) {
this.conversionService = conversionService;
}
@Override
public String expand(Object value) {
return this.conversionService.convert(value, String.class);
}
}
}