在新版本SpringCloud中,增加了契约验证,当一个类上同时使用@RequestMapping 和 @FeignClient 注解时,会抛出此异常信息:java.lang.IllegalArgumentException: @RequestMapping annotation not allowed on @FeignClient interfaces
Caused by: java.lang.IllegalArgumentException: @RequestMapping annotation not allowed on @FeignClient interfaces at org.springframework.cloud.openfeign.support.SpringMvcContract.processAnnotationOnClass(SpringMvcContract.java:180) at feign.Contract$BaseContract.parseAndValidateMetadata(Contract.java:91) at org.springframework.cloud.openfeign.support.SpringMvcContract.parseAndValidateMetadata(SpringMvcContract.java:187) at feign.Contract$BaseContract.parseAndValidateMetadata(Contract.java:62) at feign.ReflectiveFeign$ParseHandlersByName.apply(ReflectiveFeign.java:151) at feign.ReflectiveFeign.newInstance(ReflectiveFeign.java:49) at feign.Feign$Builder.target(Feign.java:269) at org.springframework.cloud.openfeign.DefaultTargeter.target(DefaultTargeter.java:30) at org.springframework.cloud.openfeign.FeignClientFactoryBean.loadBalance(FeignClientFactoryBean.java:373) at org.springframework.cloud.openfeign.FeignClientFactoryBean.getTarget(FeignClientFactoryBean.java:421) at org.springframework.cloud.openfeign.FeignClientFactoryBean.getObject(FeignClientFactoryBean.java:396) at org.springframework.cloud.openfeign.FeignClientsRegistrar.lambda$registerFeignClient$0(FeignClientsRegistrar.java:235) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1249) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1191) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ... 28 common frames omitted
方案一
将类上的@RequestMapping注解删掉,将路径更改到每个方法的路径上即可,然后使用@FeignClient自己的path属性指定路径
方案二
通过分析源码,替换源码相关约定,进行兼容处理
2.1 根据异常提示,定位错误提示位置:
1. 新版本实现:
// org.springframework.cloud.openfeign.support.SpringMvcContract#processAnnotationOnClass @Override protected void processAnnotationOnClass(MethodMetadata data, Class> clz) { RequestMapping classAnnotation = findMergedAnnotation(clz, RequestMapping.class); if (classAnnotation != null) { LOG.error("Cannot process class: " + clz.getName() + ". @RequestMapping annotation is not allowed on @FeignClient interfaces."); throw new IllegalArgumentException("@RequestMapping annotation not allowed on @FeignClient interfaces"); } }
2. 旧版本实现:
protected void processAnnotationOnClass(MethodMetadata data, Class> clz) { if (clz.getInterfaces().length == 0) { RequestMapping classAnnotation = (RequestMapping)AnnotatedElementUtils.findMergedAnnotation(clz, RequestMapping.class); if (classAnnotation != null && classAnnotation.value().length > 0) { String pathValue = Util.emptyToNull(classAnnotation.value()[0]); pathValue = this.resolve(pathValue); if (!pathValue.startsWith("/")) { pathValue = "/" + pathValue; } data.template().insert(0, pathValue); } } }
3. Contract契约接口:
4. processAnnotationOnClass接口方法实现:
public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware {
5. 解决方案:覆盖SpringMvcContract的processAnnotationOnClass接口方法实现
5.1 确定SpringMvcContract加载点
@ConditionalOnMissingBean 源码中的这个注解很关键,意思是我们可以替换Contract 的实现。所以目标很明确,就是要用自己的契约机制替换新版本中的契约
#org.springframework.cloud.openfeign.FeignClientsConfiguration#feignContract@Bean@ConditionalOnMissingBeanpublic Contract feignContract(ConversionService feignConversionService) {boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash();return new SpringMvcContract(this.parameterProcessors, feignConversionService, decodeSlash);}
5.2 编写自己的SvcSpringContract契约
主要是更改processAnnotationOnClass方法的实现为老版本的实现方式(微调一下即可,具体看下面的代码)。
package xxxx.basic.config;import feign.*;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;import org.springframework.cloud.openfeign.CollectionFormat;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.cloud.openfeign.annotation.*;import org.springframework.cloud.openfeign.encoding.HttpEncoding;import org.springframework.context.ConfigurableApplicationContext;import org.springframework.context.ResourceLoaderAware;import org.springframework.core.DefaultParameterNameDiscoverer;import org.springframework.core.MethodParameter;import org.springframework.core.ParameterNameDiscoverer;import org.springframework.core.ResolvableType;import org.springframework.core.annotation.AnnotatedElementUtils;import org.springframework.core.annotation.AnnotationUtils;import org.springframework.core.convert.ConversionService;import org.springframework.core.convert.TypeDescriptor;import org.springframework.core.convert.support.DefaultConversionService;import org.springframework.core.io.DefaultResourceLoader;import org.springframework.core.io.ResourceLoader;import org.springframework.http.InvalidMediaTypeException;import org.springframework.http.MediaType;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.Parameter;import java.lang.reflect.Type;import java.util.*;import static feign.Util.checkState;import static feign.Util.emptyToNull;import static java.util.Optional.ofNullable;import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;@ConditionalOnBeanpublic class SvcSpringContract extends Contract.BaseContract implements ResourceLoaderAware { private static final Log LOG = LogFactory.getLog(SvcSpringContract.class); private static final String ACCEPT = "Accept"; private static final String CONTENT_TYPE = "Content-Type"; private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class); private static final TypeDescriptor ITERABLE_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Iterable.class); private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); private final Map, AnnotatedParameterProcessor> annotatedArgumentProcessors; private final Map processedMethods = new HashMap<>(); private final ConversionService conversionService; private final ConvertingExpanderFactory convertingExpanderFactory; private ResourceLoader resourceLoader = new DefaultResourceLoader(); private boolean decodeSlash; public SvcSpringContract() { this(Collections.emptyList()); } public SvcSpringContract(List annotatedParameterProcessors) { this(annotatedParameterProcessors, new DefaultConversionService()); } public SvcSpringContract(List annotatedParameterProcessors, ConversionService conversionService) { this(annotatedParameterProcessors, conversionService, true); } public SvcSpringContract(List annotatedParameterProcessors, ConversionService conversionService, boolean decodeSlash) { Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null."); Assert.notNull(conversionService, "ConversionService can not be null."); List processors = getDefaultAnnotatedArgumentsProcessors(); processors.addAll(annotatedParameterProcessors); annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors); this.conversionService = conversionService; convertingExpanderFactory = new ConvertingExpanderFactory(conversionService); this.decodeSlash = decodeSlash; } @Override public void processAnnotationOnClass(MethodMetadata data, Class> clz) { if (data != null && clz.getInterfaces().length == 0) { RequestMapping classAnnotation = AnnotatedElementUtils.findMergedAnnotation(clz, RequestMapping.class); if (classAnnotation != null && classAnnotation.value().length > 0) { String pathValue = Util.emptyToNull(classAnnotation.value()[0]); pathValue = this.resolve(pathValue); if (!pathValue.startsWith("/")) { pathValue = "/" + pathValue; } data.template().uri(pathValue); } } } @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } @Override public MethodMetadata parseAndValidateMetadata(Class> targetType, Method method) { processedMethods.put(Feign.configKey(targetType, method), method); return super.parseAndValidateMetadata(targetType, method); } @Override public void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { if (CollectionFormat.class.isInstance(methodAnnotation)) { CollectionFormat collectionFormat = findMergedAnnotation(method, CollectionFormat.class); data.template().collectionFormat(collectionFormat.value()); } if (!RequestMapping.class.isInstance(methodAnnotation) && !methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) { 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(Request.HttpMethod.valueOf(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().path().endsWith("/")) { pathValue = "/" + pathValue; } data.template().uri(pathValue, true); if (data.template().decodeSlash() != decodeSlash) { data.template().decodeSlash(decodeSlash); } } } // produces parseProduces(data, method, methodMapping); // consumes parseConsumes(data, method, methodMapping); // headers parseHeaders(data, method, methodMapping); data.indexToExpander(new LinkedHashMap<>()); } @Override public boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { boolean isHttpAnnotation = false; AnnotatedParameterProcessor.AnnotatedParameterContext context = new SimpleAnnotatedParameterContext(data, paramIndex); Method method = processedMethods.get(data.configKey()); for (Annotation parameterAnnotation : annotations) { AnnotatedParameterProcessor processor = 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, method); } } if (!isMultipartFormData(data) && isHttpAnnotation && data.indexToExpander().get(paramIndex) == null) { TypeDescriptor typeDescriptor = createTypeDescriptor(method, paramIndex); if (conversionService.canConvert(typeDescriptor, STRING_TYPE_DESCRIPTOR)) { Param.Expander expander = convertingExpanderFactory.getExpander(typeDescriptor); if (expander != null) { data.indexToExpander().put(paramIndex, expander); } } } return isHttpAnnotation; } private String resolve(String value) { if (StringUtils.hasText(value) && resourceLoader instanceof ConfigurableApplicationContext) { return ((ConfigurableApplicationContext) resourceLoader).getEnvironment().resolvePlaceholders(value); } return value; } 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)); } 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 parseProduces(MethodMetadata md, Method method, RequestMapping annotation) { 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) { 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) { if (annotation.headers() != null && annotation.headers().length > 0) { for (String header : annotation.headers()) { int index = header.indexOf('='); if (!header.contains("!=") && index >= 0) { md.template().header(resolve(header.substring(0, index)),resolve(header.substring(index + 1).trim())); } } } } private Annotation synthesizeWithMethodParameterNameAsFallbackValue(Annotation parameterAnnotation, Method method, int parameterIndex) { Map 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; } private boolean isMultipartFormData(MethodMetadata data) { Collection contentTypes = data.template().headers().get(HttpEncoding.CONTENT_TYPE); if (contentTypes != null && !contentTypes.isEmpty()) { String type = contentTypes.iterator().next(); try { return Objects.equals(MediaType.valueOf(type), MediaType.MULTIPART_FORM_DATA); } catch (InvalidMediaTypeException ignored) { return false; } } return false; } private List getDefaultAnnotatedArgumentsProcessors() { List annotatedArgumentResolvers = new ArrayList<>(); annotatedArgumentResolvers.add(new MatrixVariableParameterProcessor()); annotatedArgumentResolvers.add(new PathVariableParameterProcessor()); annotatedArgumentResolvers.add(new RequestParamParameterProcessor()); annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor()); annotatedArgumentResolvers.add(new QueryMapParameterProcessor()); annotatedArgumentResolvers.add(new RequestPartParameterProcessor()); return annotatedArgumentResolvers; } private Map, AnnotatedParameterProcessor> toAnnotatedArgumentProcessorMap( List processors) { Map, AnnotatedParameterProcessor> result = new HashMap<>(); for (AnnotatedParameterProcessor processor : processors) { result.put(processor.getAnnotationType(), processor); } return result; } private static TypeDescriptor createTypeDescriptor(Method method, int paramIndex) { Parameter parameter = method.getParameters()[paramIndex]; MethodParameter methodParameter = MethodParameter.forParameter(parameter); TypeDescriptor typeDescriptor = new TypeDescriptor(methodParameter); // Feign applies the Param.Expander to each element of an Iterable, so in those // cases we need to provide a TypeDescriptor of the element. if (typeDescriptor.isAssignableTo(ITERABLE_TYPE_DESCRIPTOR)) { TypeDescriptor elementTypeDescriptor = getElementTypeDescriptor(typeDescriptor); checkState(elementTypeDescriptor != null, "Could not resolve element type of Iterable type %s. Not declared?", typeDescriptor); typeDescriptor = elementTypeDescriptor; } return typeDescriptor; } private static TypeDescriptor getElementTypeDescriptor(TypeDescriptor typeDescriptor) { TypeDescriptor elementTypeDescriptor = typeDescriptor.getElementTypeDescriptor(); // that means it's not a collection but it is iterable, gh-135 if (elementTypeDescriptor == null && Iterable.class.isAssignableFrom(typeDescriptor.getType())) { ResolvableType type = typeDescriptor.getResolvableType().as(Iterable.class).getGeneric(0); if (type.resolve() == null) { return null; } return new TypeDescriptor(type, null, typeDescriptor.getAnnotations()); } return elementTypeDescriptor; } private static class ConvertingExpanderFactory { private final ConversionService conversionService; ConvertingExpanderFactory(ConversionService conversionService) { this.conversionService = conversionService; } Param.Expander getExpander(TypeDescriptor typeDescriptor) { return value -> { Object converted = conversionService.convert(value, typeDescriptor, STRING_TYPE_DESCRIPTOR); return (String) converted; }; } } private class SimpleAnnotatedParameterContext implements AnnotatedParameterProcessor.AnnotatedParameterContext { private final MethodMetadata methodMetadata; private final int parameterIndex; SimpleAnnotatedParameterContext(MethodMetadata methodMetadata, int parameterIndex) { this.methodMetadata = methodMetadata; this.parameterIndex = parameterIndex; } @Override public MethodMetadata getMethodMetadata() { return methodMetadata; } @Override public int getParameterIndex() { return parameterIndex; } @Override public void setParameterName(String name) { nameParam(methodMetadata, name, parameterIndex); } @Override public Collection setTemplateParameter(String name, Collection rest) { return addTemplateParameter(rest, name); } Collection addTemplateParameter(Collection possiblyNull, String paramName) { Collection params = ofNullable(possiblyNull).map(ArrayList::new).orElse(new ArrayList<>()); params.add(String.format("{%s}", paramName)); return params; } }}
5.3 增加自动配置:
package xxxxx.basic.config;import feign.Contract;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;import org.springframework.cloud.openfeign.support.SpringMvcContract;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.annotation.Order;import org.springframework.core.convert.support.DefaultConversionService;import java.util.ArrayList;import java.util.List;@Configuration@ConditionalOnClass(SpringMvcContract.class)@Order(Integer.MIN_VALUE)public class SvcSpringContractConfig { @Autowired(required = false) private List parameterProcessors = new ArrayList<>(); @Bean public Contract feignContract() { return new SvcSpringContract(parameterProcessors, new DefaultConversionService(), true); }}
来源地址:https://blog.csdn.net/qq_44734154/article/details/128624881