Идея этой статьи пришла от человека, которого я встретил в компании на прошлой неделе.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;
}
}
Эта статья здесь, если у вас есть какие-либо вопросы, вы можете оставить сообщение для обсуждения~