Record the "xx is not a type supported by this encoder" caused by the upload configuration of feign file once error

Here is the correct configuration:

There is no need to add an additional configuration Encoder (most of the Internet will let you configure a SpringFormEncoder, which will have hidden problems, which will be described in detail below). The PageableSpringEncoder in the spring default FeignClientsConfiguration has supported file upload.

public interface UserService {
    @PostMapping(value = "/user/upload", headers = "content-type=" + MediaType.MULTIPART_FORM_DATA_VALUE)
    String uploadPic(@RequestPart("file") MultipartFile file);
}

Scene reproduction

Initial configuration

The development framework uses the spring cloud microservice system, and the calls between microservices use the feign interface. Due to a demand in the early stage, some colleagues need to use the feign interface to upload files. At that time, it may be because of the urgent time, so they got a piece of configuration on the Internet and put it in the project. The approximate code is as follows:

@Configuration
public class MultipartConfig {
    @Bean
    public Encoder encoder(){
        return new SpringFormEncoder();
    }
}

@FeignClient(name = "xxx-provider")
public interface UserService {
    @PostMapping(value = "/user/upload",
        produces = MediaType.APPLICATION_JSON_UTF8_VALUE,
        consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    String uploadPic(@RequestPart("file") MultipartFile file);
}

After restarting the project, it is found that the problem has been solved and the file can be uploaded (there are great problems in the above code, which is a mine buried in the later stage).

Preliminary exposure problems

According to the above configuration, a new feign interface micro service is introduced in the later development. When making a post request and @ requestbody transferring parameters, it is found that the error is always reported: xxx is not a type supported by this encoder There is no problem with the get request

Encoder: the local transparent call of feign interface needs to encode and serialize java objects for http network transmission, so an encoder is required. Conversely, a decoder is required

First stage troubleshooting

Considering the introduction of spring formencoder, we start with this class to find the problem.

View the core source code of this class

public void encode (Object object, Type bodyType, RequestTemplate template) throws EncodeException {
    if (bodyType.equals(MultipartFile[].class)) {
      val files = (MultipartFile[]) object;
      val data = new HashMap<String, Object>(files.length, 1.F);
      for (val file : files) {
        data.put(file.getName(), file);
      }
      super.encode(data, MAP_STRING_WILDCARD, template);
    } else if (bodyType.equals(MultipartFile.class)) {
      val file = (MultipartFile) object;
      val data = singletonMap(file.getName(), object);
      super.encode(data, MAP_STRING_WILDCARD, template);
    } else if (isMultipartFileCollection(object)) {
      val iterable = (Iterable<?>) object;
      val data = new HashMap<String, Object>();
      for (val item : iterable) {
        val file = (MultipartFile) item;
        data.put(file.getName(), file);
      }
      super.encode(data, MAP_STRING_WILDCARD, template);
    } else {
      super.encode(object, bodyType, template);
    }
  }

It can be seen from the encode method that if it is not a Multipart related operation, go directly to the encode method of the parent class's {FormEncoder:

public void encode (Object object, Type bodyType, RequestTemplate template) throws EncodeException {
    String contentTypeValue = getContentTypeValue(template.headers());
    val contentType = ContentType.of(contentTypeValue);
    if (!processors.containsKey(contentType)) {
      // There is no header in the project. The configured contentTypeValue is null. The code will eventually come here and call the encode of the parent class
      delegate.encode(object, bodyType, template);
      return;
    }

    Map<String, Object> data;
    if (MAP_STRING_WILDCARD.equals(bodyType)) {
      data = (Map<String, Object>) object;
    } else if (isUserPojo(bodyType)) {
      data = toMap(object);
    } else {
      delegate.encode(object, bodyType, template);
      return;
    }

    val charset = getCharset(contentTypeValue);
    processors.get(contentType).process(template, charset, data);
  }

Finally, we will go to the Default implementation of the top-level Encoder:

class Default implements Encoder {

  @Override
  public void encode(Object object, Type bodyType, RequestTemplate template) {
    if (bodyType == String.class) {
      template.body(object.toString());
    } else if (bodyType == byte[].class) {
      template.body((byte[]) object, null);
    } else if (object != null) {
    // An exception is thrown here
      throw new EncodeException(
          format("%s is not a type supported by this encoder.", object.getClass()));
    }
  }
}

The reason for the problem here is that after adding the file upload configuration, the global use of spring formencoder causes that there is no appropriate encoder for the post @requestbody request.

Preliminary solution

The configuration uploaded from the file cannot take effect globally, but only in the current feign. Therefore, the following configuration is available.

@FeignClient(name = "xxx-provider",configuration = MultipartConfig.class)
public interface UserService {
    @PostMapping(value = "/user/upload",
        produces = MediaType.APPLICATION_JSON_UTF8_VALUE,
        consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    String uploadPic(@RequestPart("file") MultipartFile file);

    public static class MultipartConfig {
        @Bean
        public Encoder encoder(){
            return new SpringFormEncoder();
          }
    }
}

According to the updated configuration, different feign interface configuration isolation can be solved without affecting the encoders of other feign interfaces. I thought the problem had been solved here and I could rest easy. In fact, there is still a big hidden danger.

The problem is exposed again

In the later development, a post method call of feign interface was added to the micro service where feign was uploaded from the original file to save some information. In the call, it was found that {xxx is not a type supported by this encoder Question.

Phase II troubleshooting

With the above process, the encoder problem occurs again. It is considered that only a feign interface is added to the original microservice, and the feign calls of other microservices are normal at this time, that is, only the microservice where the feign of spring formencoder is uploaded with the configuration file has a problem (the feign interface is separated from the jar package, corresponding to XX resource API jar in the project).

At this time, I think: the MultipartConfig configuration is introduced in the specific UserService feign interface. It should only take effect for the current interface. How can it affect the other feign interfaces of the current microservice?  

In fact, according to the source code debugging, this configuration does affect other feign interfaces of the current microservice XXX provider, and the newly added feign interface also uses the encoder SpringFormEncoder.

Sort out the questions and conjectures:

@The configuration specified by feignclient (name = "XXX provider", configuration = multipartconfig. Class) will take effect in all feigns with name = "XXX provider". The configuration is shared, but different names, that is, different microservices, will not affect each other.

Analyze the source code and verify the conjecture

feign interface call is implemented based on JDK dynamic proxy. The core class: FeignClientFactoryBean. spring based FactoryBean is not analyzed here. Those unfamiliar with it can understand the concept of FactoryBean separately.

Since it is based on FactoryBean, getObject() is the core method

@Override
	public Object getObject() {
		return getTarget();
	}

	/**
	 * @param <T> the target type of the Feign client
	 * @return a {@link Feign} client created with the specified data and the context
	 * information
	 */
	<T> T getTarget() {
		FeignContext context = beanFactory != null
				? beanFactory.getBean(FeignContext.class)
				: applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);

		if (!StringUtils.hasText(url)) {
			if (url != null && LOG.isWarnEnabled()) {
				LOG.warn(
						"The provided URL is empty. Will try picking an instance via load-balancing.");
			}
			else if (LOG.isDebugEnabled()) {
				LOG.debug("URL not provided. Will use LoadBalancer.");
			}
			if (!name.startsWith("http")) {
				url = "http://" + name;
			}
			else {
				url = name;
			}
			url += cleanPath();
			return (T) loadBalance(builder, context,
					new HardCodedTarget<>(type, name, url));
		}
		.....ellipsis....
        .............
		return (T) targeter.target(this, builder, context,
				new HardCodedTarget<>(type, name, url));
	}

Here, only the source code involved in the current problem is analyzed as follows:

	protected Feign.Builder feign(FeignContext context) {
		FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
		Logger logger = loggerFactory.create(type);

		// @formatter:off
		Feign.Builder builder = get(context, Feign.Builder.class)
				// required values
				.logger(logger)
                // Encoder correlation
				.encoder(get(context, Encoder.class))
				.decoder(get(context, Decoder.class))
				.contract(get(context, Contract.class));
		// @formatter:on

		configureFeign(context, builder);
		applyBuildCustomizers(context, builder);

		return builder;
	}

Focus on the get(context, Encoder.class) method

.encoder(get(context, Encoder.class))

//get into
	protected <T> T get(FeignContext context, Class<T> type) {
		T instance = context.getInstance(contextId, type);
		if (instance == null) {
			throw new IllegalStateException(
					"No bean found of type " + type + " for " + contextId);
		}
		return instance;
	}

//Enter context getInstance(contextId, type);  That is, getInstance of NamedContextFactory

	public <T> T getInstance(String name, Class<T> type) {
        // A crucial class AnnotationConfigApplicationContext appears 
		AnnotationConfigApplicationContext context = getContext(name);
		try {
			return context.getBean(type);
		}
		catch (NoSuchBeanDefinitionException e) {
			// ignore
		}
		return null;
	}

As you can see above, an AnnotationConfigApplicationContext class appears. The students who are familiar with the source code of spring container will brighten up. Why is it not the web container of springboot, but the annotation configuration container class?

Continue to click in the getContext method

	protected AnnotationConfigApplicationContext getContext(String name) {
		if (!this.contexts.containsKey(name)) {
			synchronized (this.contexts) {
				if (!this.contexts.containsKey(name)) {
					this.contexts.put(name, createContext(name));
				}
			}
		}
		return this.contexts.get(name);
	}

When you see this method, you will cache the context. If you miss the cache, you will call the createContext method. From the naming, you can see that it is to create a new IOC container. containsKey is the current container ID, which is the name corresponding to the microservice, @ feignclient (name = "XXX provider", configuration = multipartconfig. Class) is XXX provider.

The following is the code to create a container and associate it with the parent container. The parent container is the main web container of the current springboot, that is, the container stored by the controller, service and dao objects in our project.

protected AnnotationConfigApplicationContext createContext(String name) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		if (this.configurations.containsKey(name)) {
			for (Class<?> configuration : this.configurations.get(name)
					.getConfiguration()) {
				context.register(configuration);
			}
		}
		for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
			if (entry.getKey().startsWith("default.")) {
				for (Class<?> configuration : entry.getValue().getConfiguration()) {
					context.register(configuration);
				}
			}
		}
		context.register(PropertyPlaceholderAutoConfiguration.class,
				this.defaultConfigType);
		context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
				this.propertySourceName,
				Collections.<String, Object>singletonMap(this.propertyName, name)));
		if (this.parent != null) {
			// Uses Environment from parent as well as beans
			context.setParent(this.parent);
			// jdk11 issue
			// https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
			context.setClassLoader(this.parent.getClassLoader());
		}
		context.setDisplayName(generateDisplayName(name));
		context.refresh();
		return context;
	}

After reading the above source code, it is concluded that all interfaces corresponding to @ FeignClient with the same name will create an AnnotationConfigApplicationContext container, which is associated with the web container through the child parent container, and the feign interface proxy object is saved in the current container. This confirms the above conjecture that the configuration specified by @ FeignClient (name = "XXX provider", configuration = multipartconfig. Class) will take effect in all feigns with name = "XXX provider". The configuration will be shared, but different names, that is, different microservices, will not affect each other.

Continue to find the solution of "the same microservice file upload and common interface coexist"

Think back: did spring cloud not consider the problem of file upload by default? Impossible? Let's take a closer look at the default coder spring encoder through the source code.

PageableSpringEncoder is a decorative enhancement of SpringEncoder. The internal call is still SpringEncoder. Let's just look at SpringEncoder directly here.

The encode method of SpringEncoder is as follows: sure enough, it can be seen that the MultipartType file upload has been configured, isMultipartType judgment logic, request headers(). get(HttpEncoding.CONTENT_TYPE);

public void encode(Object requestBody, Type bodyType, RequestTemplate request)
			throws EncodeException {
		// template.body(conversionService.convert(object, String.class));
		if (requestBody != null) {
			Collection<String> contentTypes = request.headers()
					.get(HttpEncoding.CONTENT_TYPE);

			MediaType requestContentType = null;
			if (contentTypes != null && !contentTypes.isEmpty()) {
				String type = contentTypes.iterator().next();
				requestContentType = MediaType.valueOf(type);
			}

			if (isMultipartType(requestContentType)) {
				this.springFormEncoder.encode(requestBody, bodyType, request);
				return;
			}
			else {
				if (bodyType == MultipartFile.class) {
					log.warn(
							"For MultipartFile to be handled correctly, the 'consumes' parameter of @RequestMapping "
									+ "should be specified as MediaType.MULTIPART_FORM_DATA_VALUE");
				}
			}
			encodeWithMessageConverter(requestBody, bodyType, request,
					requestContentType);
		}
	}


    

	private boolean isMultipartType(MediaType requestContentType) {
		return Arrays.asList(MediaType.MULTIPART_FORM_DATA, MediaType.MULTIPART_MIXED,
				MediaType.MULTIPART_RELATED).contains(requestContentType);
	}

Draw a conclusion: you only need to add the content of file upload in the request header_ Type:

conten-type=multipart/form-data

 

 

 

Tags: Spring Spring Cloud

Posted by mudkicker on Thu, 14 Apr 2022 14:26:27 +0930