Идея этой статьи пришла от человека, которого я встретил в компании на прошлой неделе.Feignисключение запуска.
Сначала небольшой вывод: это исключение возникает из-за того, что Feign позволяет вам иметь только один параметр в методе POST прокси-класса Feign.
1. Не можете запустить проект?
В настоящее время наша команда разработчиков использует Feign в качестве компонента вызова микросервиса.Я думаю, что многие другие компании должны быть такими же, поэтому, учитывая эту проблему, я столкнулсяMethod has too many Body parametersВы также можете столкнуться с этим, и ни Google, ни Baidu не объяснили это подробно, поэтому я разобрался и поделился с вами.
Началось все с прошлой недели.Я получил задание на миграцию сервиса.После переноса этого сервиса в новый проект я ввел два jar пакета,которые этот сервис должен использовать в новом проекте,а потом обнаружил что проект нельзя использовать. запускается и бросает мнеIllegalStateExceptionаномальный:
Я последовал подсказке сообщения об исключении и обнаружил, что возникла проблема с методом прокси-класса Feign, определенным в одном из пакетов jar.Метод, вероятно, такой:
@PostMapping(value = PLAN_PAGE)
@ApiOperation(value = "分页查询计划列表", notes = "计划列表")
JsonResult<Pagination<PlanInfo>> queryPlans(@RequestBody @Validated Query query,
@PageableParam
@PageableDefault(sort = "id",
direction = Sort.Direction.DESC,
size = 100) Pageable pageable);
На первый взгляд вполне нормальный метод, метод Post с условиями запроса и пагинацией.
Я не почувствовал ничего необычного, поэтому искал эту аномалию в Baidu и Google и нашел два ответа:
- Параметры запроса в Feign нужно привести
@RequestParamОбратите внимание, однако яpostПросьба, так что этот ответ ПРОЙДИТЕ. - Feign используется одновременно
@RequestBodyаннотации и@PageableDefaultПри аннотировании вам необходимо добавить обработчик аннотаций в конфигурацию проекта,
ок, после исключения первого ответа, надо настроить по второму ответу.Для перестраховки я все же попросил коллег по группе спросить о соответствующей конфигурации.
Вскоре он дал мне ответ и помог настроить его конфигурация, вероятно, такова:
public static class ConsultantPageableParamProcessor implements AnnotatedParameterProcessor {
private static final Class<com.xingren.consultant.consultant.contract.annotation.PageableParam> ANNOTATION =
com.xingren.consultant.consultant.contract.annotation.PageableParam.class;
@Override
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}
@Override
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
int parameterIndex = context.getParameterIndex();
MethodMetadata data = context.getMethodMetadata();
data.queryMapIndex(parameterIndex);
return true;
}
}
Определите внутренний класс и объявите этот класс как Bean.
Из этого кода также видно, что он предназначен для вышеуказанного метода сообщения об ошибках.@PageableParamПроведена специальная обработка.
После того, как он помог мне разобраться с этим, он сказал мне, что все в порядке, я взволнованно вытащил кодrunВстало, но опять сообщило об ошибке, но оно было сообщено в коде других сегментов:
@PostMapping(value = PLAN_UPDATE)
@ApiOperation(value = "更新计划对象", notes = "更新计划对象")
JsonResult<Pagination<PlanInfo>> queryPlans(@RequestBody @Validated PlanForm form,
@PathVariable("id") Long id);
Этот фрагмент кода, вероятно, похож на приведенный выше, обновляя объект с помощью идентификатора.
Потом я снова погуглил@RequestBodyа также@PathVariableВ случае с Feign, но на этот раз ничего, только один вроде бы связанный, но бесполезныйOpenFeign`s issue.
После того, как я безрезультатно поискал в Интернете, я снова спросил Вэньцюсина, но на этот раз он сказал, что никогда не сталкивался с такой ситуацией, что меня немного обеспокоило.
Поскольку это исключение надолго задержало мой прогресс миграции, можно сказать, что миграция сервиса не на полпути, а средний путь рушится.Поразмыслив, я решил подробно изучить причину этого исключения, а затем искать решение.
2. Углубленный исходный код DEBUG
Прежде всего, я нашел местонахождение кода, выбрасывающего исключение.По картинке в начале этой статьи мы можем знать, где выбрасывается исключение.feign.Util.checkState(Util.java:130):
public static void checkState(boolean expression,
String errorMessageTemplate,
Object... errorMessageArgs) {
if (!expression) {
throw new IllegalStateException(
format(errorMessageTemplate, errorMessageArgs));
}
}
Прочитав его, вы обнаружите, что здесь выносится суждение.Если выражение ложно, будет выброшено это исключение.
Получив этот вывод, нам просто нужно найти место для его вызова и найти способ изменить выражение на истинное.
Затем я пришел к тому месту, где он был назван.Из картинки в начале этой статьи мы можем знать, что он находится вfeign.Contract$BaseContract.parseAndValidateMetadata(Contract.java:117), в следующем коде я вырезал бесполезный код и сделал его проще для понимания и короче:
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
// 1️⃣
MethodMetadata data = new MethodMetadata();
data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType()));
data.configKey(Feign.configKey(targetType, method));
Class<?>[] parameterTypes = method.getParameterTypes();
Type[] genericParameterTypes = method.getGenericParameterTypes();
// 2️⃣
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
int count = parameterAnnotations.length;
for (int i = 0; i < count; i++) {
boolean isHttpAnnotation = false;
if (parameterAnnotations[i] != null) {
// 3️⃣
isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
}
// 4️⃣
if (parameterTypes[i] == URI.class) {
data.urlIndex(i);
// 5️⃣
} else if (!isHttpAnnotation && parameterTypes[i] != Request.Options.class) {
checkState(data.formParams().isEmpty(),
"Body parameters cannot be used with form parameters.");
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
data.bodyIndex(i);
data.bodyType(Types.resolve(targetType, targetType, genericParameterTypes[i]));
}
}
return data;
}
Из названия этого метода мы можем знать, что он выполняет некоторую проверку метода. Поскольку это проверка метода, это должны быть те методы, которые сообщают об ошибках. При проверке методов в прокси-классе Feign некоторые методы не пройти проверку, поэтому было выдано исключение.
Я грубо делю этот код на две части. Первая часть заключается в инкапсуляции объекта метаданных через объект Method.Этот объект метаданных содержит параметры и типы параметров метода, а также тип возвращаемого значения и другую информацию.
Часть 2️⃣ находится в центре нашего внимания. Она получает все аннотации к методу и выполняет некоторую обработку аннотаций одну за другой. Мы ясно видим, что есть метод с именемprocessAnnotationsOnParameter, именно этот метод является главным приоритетом нашего анализа в этой статье.
Чтобы подчеркнуть его суть, я пометил эту строку кода знаком 3️⃣.
Ну а дальше можно примерно разобрать весь процесс:
- Feign использует этот метод для проверки всех методов прокси-класса Feign.
- Он инкапсулируется в объект метаданных с помощью объекта Method.
- Получите все аннотации параметров аннотации. Обратите внимание, что аннотация параметра представляет собой двумерный массив, а это означает, что для параметра может быть несколько аннотаций.
- пройти черезprocessAnnotationsOnParameterметод, чтобы определить, есть ли аннотация Http для этого параметра (isHttpAnnotation).
- В 4️⃣ оценивается, является ли параметр метода параметром типа URI, который не используется в качестве параметра типа URI в нашем проекте, поэтому вы можете сразу перейти к 5️⃣.
- Суждение здесь зависит от предыдущегоisHttpAnnotation, и можно обнаружить, что именно в этом суждении и появится наша сегодняшняя тема: «Метод имеет слишком много параметров Тела».
С приведенным выше расчесыванием мы должны знать, что нам нужно сделать более глубокоеprocessAnnotationsOnParameterТеперь, если вы это понимаете, вы можете найти основную причину этого исключения.
3. Важный процесс AnnotationsOnParameter
нажмите вprocessAnnotationsOnParameterПозже я обнаружил, что это абстрактный метод, и есть два класса, которые реализуют его абстрактные методы, предоставляемые Feign.Класс контрактаи Весна реализованаSpringMvcContract, поскольку мы являемся проектом SpringCloud, поэтому здесь нам следует обратить внимание наSpringMvcContract.
@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) {
// 6️⃣
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, method);
}
}
return isHttpAnnotation;
}
Код здесь на первый взгляд довольно проблемный, но мы уже знаем из предыдущей статьи, что этот метод в основном используется для проверки того, являются ли аннотации к параметрам метода аннотациями Http, поэтому мой принцип — смотреть только на переменную sumisHttpAnnotationсоответствующие места.
Здесь мы можем в основном смотреть на код в 6️⃣, использовать переменную-член Map этого класса, чтобы получить обработчик параметра аннотации, и напрямую возвращать fasle, если он не найден.
Если здесь нет DEBUG, вы ничего не видите.После DEBUG вы можете обнаружить, что этот ключ имеет тип аннотации.annotatedArgumentProcessors-MapВнутри четыре элемента элементы в этом всеAnnotatedParameterProcessorПодкласс:
- PathVariableParameterProcessor
- RequestHeaderParameterProcessor
- RequestParamParameterProcessor
- QueryMapParameterProcessor
Вы должны понять, глядя на имя.Эти четыре класса соответствуют четырем аннотациям в Spring.Когда аннотация нашего параметра не является одной из этих четырех, она сразу вернет false.
Если это одна из этих четырех аннотаций, вы можете просто подумать, что она вернет true.
В это время вы обнаружите, что в нем нет часто используемой карты.@RequestBody, то есть если параметры вашего метода принимают@RequestBodyТогда он вернет false напрямую.
С помощью кода на 5⃣️ выше мы можем узнать, что еслиisHttpAnnotation = true, то он никогда не выкинетMethod has too many Body parameters, то мы собираемся оглянуться назадisHttpAnnotation = falseЧто произойдет, когда?
- Посмотрим на суждение на 5⃣️, потому что оно нам в принципе не нужно
Request.Options.class, так когдаisHttpAnnotation = falseВы можете в основном думать, что это войдет в это еще. - Затем посмотрите непосредственно на тело второго метода.
checkState, так как первый не выбрасывает исключение, мы можем думать, что все в порядке, во втором checkState он судитbodyIndexЭтоnull, через код bodyIndex является переменной метаданных.Если результат суждения здесь будет ложным, сразу после помещения в checkState будет выброшено исключение. - Далее смотрим на следующий шаг.Если две вышеуказанные проверки пройдены, bodyIndex будет установлено значение. . .
Увидев это, я моментально понял весь процесс проверки:
- Feign последовательно проверяет все параметры метода прокси-класса Feign.
- Если параметр содержит аннотации Http, он будет освобожден.
- Если параметр не содержит аннотаций Http, установите значение bodyIndex.
- Если первый параметр является аннотацией, отличной от HTTP, то значение будет установлено в bodyIndex.Если второй параметр также является аннотацией, отличной от HTTP, то он перейдет ко второму
checkStateБудет выдано исключение, так как в это время bodyIndex уже имеет значение, и решение bodyIndex == null будет ложным.
После того, как я с этим разобрался, я понял исключение, которое часто встречается в проекте, то есть при одновременном использовании Feign@RequestBodyаннотации и@PageableDefaultПри аннотации будет выброшено исключение.
Поскольку, по мнению Feign, эти две аннотации не являются HTTP-аннотациями, Feign может разрешить только одному параметру каждого метода быть неHTTP-аннотациями, но не обоим сразу.
Но когда я думаю об этом, мне снова становится странно, почему я позже@RequestBodyа также@PathVariableДаже его использование вызовет исключение? По правилам здесь все должно быть в порядке.
С вопросами я снова отладил и нашел прокси-класс, который допустил ошибку.annotatedArgumentProcessors-MapЧетыре класса внутри стали двумя! ! ! :
- RequestHeaderParameterProcessor
- RequestParamParameterProcessor
то есть@PathVariableраспознается неправильно, поэтому он также распознается как аннотация, отличная от HTTP.
4. Два решения
Насчет того, почему на два меньше, я вникать не стал, просто знаю, что наверное есть два пути решения этой проблемы:
- переписать
processAnnotationsOnParameterметод, который возвращает true независимо от того, что происходит. - повторно ввестиSpringMvcContractи настроить его
annotatedArgumentProcessors-Map, вы можете завершить класс обработки аннотаций в нем.
Вот код второго метода:
@Resource
private ConversionService feignConversionService;
public static class RequestBodyParameterProcessor implements AnnotatedParameterProcessor {
private static final Class<RequestBody> ANNOTATION =
RequestBody.class;
@Override
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}
@Override
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
int parameterIndex = context.getParameterIndex();
MethodMetadata data = context.getMethodMetadata();
data.queryMapIndex(parameterIndex);
return true;
}
}
@Bean
@Primary
@ConditionalOnMissingBean
public Contract feignContract() {
// 用于解决偶发性Feign调用异常
// 方法:将AnnotatedParameterProcessor下所有子类加入list,重写SpringMvcContract
List<AnnotatedParameterProcessor> parameterProcessors = Lists.newArrayList();
parameterProcessors.add(new PathVariableParameterProcessor());
parameterProcessors.add(new RequestHeaderParameterProcessor());
parameterProcessors.add(new QueryMapParameterProcessor());
parameterProcessors.add(new RequestParamParameterProcessor());
parameterProcessors.add(new RequestBodyParameterProcessor());
return new SpringMvcContract(parameterProcessors, feignConversionService);
}
В Карте я вручную поместил исходные четыре процессора аннотаций, а затем вручную определил одинRequestBodyОбработчик аннотаций, чтобы после обработки мы могли убедиться, что наш метод прокси-класса Feign может принимать@RequestBodyпараметр и дополнительный параметр, гарантируя при этомannotatedArgumentProcessors-MapНекоторые процессоры аннотаций не исчезают необъяснимым образом.
При этом, если есть новый элемент, добавить его в параметр пейджинга уже нельзя.@PageableParam, исходный проект определяет@PageableParamЯ определил еще один обработчик аннотаций, чтобы он возвращал истину, чтобы пропустить проверку, и теперь он мне не нужен~ (если вы здесь не понимаете, посмотрите решение моего коллеги в начале)
Тогда я подумал об этомMethod has too many Body parameters, перевод такой: в методе слишком много параметров тела.Также я видел, как кто-то в Интернете говорил, что Feign рекомендует, чтобы в методе POST был только один параметр.Оказывается, все это сделано Feign намеренно.
2021-02-28 Обновление
После реального боя моего проекта я обнаружил, что второй метод невозможен, эта конфигурация заставит Feign думать, что вы принесли@RequestBodyМетод параметра представляет собой метод типа GET, который заставляет его принимать ваши@RequestBodyВсе параметры размещаются после URL запроса (то есть сращивание параметров наподобие GET-запроса), поэтому выложу решение первого способа:
@Bean
@Primary
@ConditionalOnMissingBean
public Contract feignContract() {
return new FeignContract();
}
public static class FeignContract extends SpringMvcContract {
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
return true;
}
}
Эта статья здесь, если у вас есть какие-либо вопросы, вы можете оставить сообщение для обсуждения~