Глубокое понимание метода Фейна имеет слишком много параметров тела.

Java

Идея этой статьи пришла от человека, которого я встретил в компании на прошлой неделе.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 и нашел два ответа:

  1. Параметры запроса в Feign нужно привести@RequestParamОбратите внимание, однако яpostПросьба, так что этот ответ ПРОЙДИТЕ.
  2. 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️⃣.

Ну а дальше можно примерно разобрать весь процесс:

  1. Feign использует этот метод для проверки всех методов прокси-класса Feign.
  2. Он инкапсулируется в объект метаданных с помощью объекта Method.
  3. Получите все аннотации параметров аннотации. Обратите внимание, что аннотация параметра представляет собой двумерный массив, а это означает, что для параметра может быть несколько аннотаций.
  4. пройти черезprocessAnnotationsOnParameterметод, чтобы определить, есть ли аннотация Http для этого параметра (isHttpAnnotation).
  5. В 4️⃣ оценивается, является ли параметр метода параметром типа URI, который не используется в качестве параметра типа URI в нашем проекте, поэтому вы можете сразу перейти к 5️⃣.
  6. Суждение здесь зависит от предыдущего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Подкласс:

  1. PathVariableParameterProcessor
  2. RequestHeaderParameterProcessor
  3. RequestParamParameterProcessor
  4. QueryMapParameterProcessor

Вы должны понять, глядя на имя.Эти четыре класса соответствуют четырем аннотациям в Spring.Когда аннотация нашего параметра не является одной из этих четырех, она сразу вернет false.

Если это одна из этих четырех аннотаций, вы можете просто подумать, что она вернет true.

В это время вы обнаружите, что в нем нет часто используемой карты.@RequestBody, то есть если параметры вашего метода принимают@RequestBodyТогда он вернет false напрямую.

С помощью кода на 5⃣️ выше мы можем узнать, что еслиisHttpAnnotation = true, то он никогда не выкинетMethod has too many Body parameters, то мы собираемся оглянуться назадisHttpAnnotation = falseЧто произойдет, когда?

  1. Посмотрим на суждение на 5⃣️, потому что оно нам в принципе не нужноRequest.Options.class, так когдаisHttpAnnotation = falseВы можете в основном думать, что это войдет в это еще.
  2. Затем посмотрите непосредственно на тело второго метода.checkState, так как первый не выбрасывает исключение, мы можем думать, что все в порядке, во втором checkState он судитbodyIndexЭтоnull, через код bodyIndex является переменной метаданных.Если результат суждения здесь будет ложным, сразу после помещения в checkState будет выброшено исключение.
  3. Далее смотрим на следующий шаг.Если две вышеуказанные проверки пройдены, bodyIndex будет установлено значение. . .

Увидев это, я моментально понял весь процесс проверки:

  1. Feign последовательно проверяет все параметры метода прокси-класса Feign.
  2. Если параметр содержит аннотации Http, он будет освобожден.
  3. Если параметр не содержит аннотаций Http, установите значение bodyIndex.
  4. Если первый параметр является аннотацией, отличной от HTTP, то значение будет установлено в bodyIndex.Если второй параметр также является аннотацией, отличной от HTTP, то он перейдет ко второмуcheckStateБудет выдано исключение, так как в это время bodyIndex уже имеет значение, и решение bodyIndex == null будет ложным.

После того, как я с этим разобрался, я понял исключение, которое часто встречается в проекте, то есть при одновременном использовании Feign@RequestBodyаннотации и@PageableDefaultПри аннотации будет выброшено исключение.

Поскольку, по мнению Feign, эти две аннотации не являются HTTP-аннотациями, Feign может разрешить только одному параметру каждого метода быть неHTTP-аннотациями, но не обоим сразу.

Но когда я думаю об этом, мне снова становится странно, почему я позже@RequestBodyа также@PathVariableДаже его использование вызовет исключение? По правилам здесь все должно быть в порядке.

С вопросами я снова отладил и нашел прокси-класс, который допустил ошибку.annotatedArgumentProcessors-MapЧетыре класса внутри стали двумя! ! ! :

  1. RequestHeaderParameterProcessor
  2. RequestParamParameterProcessor

то есть@PathVariableраспознается неправильно, поэтому он также распознается как аннотация, отличная от HTTP.

4. Два решения

Насчет того, почему на два меньше, я вникать не стал, просто знаю, что наверное есть два пути решения этой проблемы:

  1. переписатьprocessAnnotationsOnParameterметод, который возвращает true независимо от того, что происходит.
  2. повторно ввести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;
        }
    }

Эта статья здесь, если у вас есть какие-либо вопросы, вы можете оставить сообщение для обсуждения~