Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1471,11 +1471,22 @@ interface PizzaClient {
);
}

Encoder jackson3Encoder = new ConditionalEncoder(
new Jackson3Encoder(),
EncoderPredicate.forContentType(MediaType.APPLICATION_JSON_VALUE)
);
PartBodyFactory partBodyFactory = new PartBodyFactory(encoders -> encoders.add(0, jackson3Encoder));
PartFactory partFactory = PartFactory.builder()
.partBodyFactory(partBodyFactory)
.build();
MultipartFormBodyFactory multipartFormBodyFactory = MultipartFormBodyFactory.builder()
.partFactory(partFactory)
.build();
MultipartFormEncoder multipartFormEncoder = MultipartFormEncoder.builder()
.multipartFormBodyFactory(multipartFormBodyFactory)
.build();
PizzaClient pizzaClient = Feign.builder()
.encoder(MultipartFormEncoder.builder()
.partBodyEncoders(List.of(new Jackson3Encoder()))
.build()
)
.encoder(multipartFormEncoder)
.target(PizzaClient.class, "https://api.pizza.com");

Pizza pizza = new Pizza();
Expand Down Expand Up @@ -1601,25 +1612,38 @@ public interface DownloadClient {
#### Streaming Spring MultipartFile (Feign 14+)

If you are using Spring Cloud OpenFeign and need to stream large `MultipartFile` payloads without consuming significant
JVM memory, you can use `feign.form.spring.MultipartFileEncoder`. This encoder processes files as streams instead of
loading their entire content into memory.
JVM memory, configure `MultipartFormEncoder` with:

- `MultipartFilePartContextResolver` to unwrap Spring `MultipartFile` metadata
- `MultipartFileEncoder` as the part body encoder

```java
@Configuration
public class StreamingMultipartConfig {

@Bean
public Encoder feignFormEncoder() {
var partFactory = PartFactory.builder()
.partBodyFactory(new PartBodyFactory(List.of(new MultipartFileEncoder())))
.build();
var multipartFormBodyFactory = new MultipartFormBodyFactory(
new PartContextResolverChain(resolvers -> resolvers.addFirst(new MultipartFilePartContextResolver())),
partFactory
);
return MultipartFormEncoder.builder()
.partEncoders(encoders -> encoders.addFirst(new MultipartFileEncoder()))
.multipartFormBodyFactory(multipartFormBodyFactory)
.build();
}
}

@FeignClient(name = "large-file-upload-service", configuration = StreamingMultipartConfig.class)
public interface LargeFileUploadClient {

@RequestMapping(value = "/upload", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@RequestMapping(
value = "/upload",
method = RequestMethod.POST,
consumes = MediaType.MULTIPART_FORM_DATA_VALUE
)
void uploadLargeFile(@RequestBody MultipartFile file);
}
```
12 changes: 12 additions & 0 deletions api/src/main/java/feign/Request.java
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,18 @@ public boolean isRepeatable() {
return true;
}

@Override
public boolean equals(Object object) {
if (!(object instanceof BodyImpl)) return false;
BodyImpl body = (BodyImpl) object;
return Objects.deepEquals(content, body.content) && Objects.equals(charset, body.charset);
}

@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(content), charset);
}

@Override
public String toString() {
return new String(content, charset);
Expand Down
15 changes: 1 addition & 14 deletions api/src/main/java/feign/codec/Encoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
* void create(User user);
* </pre>
*
* <p>Example implementation: <br>
* Example implementation: <br>
*
* <p>
*
Expand Down Expand Up @@ -66,7 +66,6 @@
* Session login(@Param(&quot;username&quot;) String username, @Param(&quot;password&quot;) String password);
* </pre>
*/
@FunctionalInterface

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

public interface Encoder {
/** Type literal for {@code Map<String, ?>}, indicating the object to encode is a form. */
Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;
Expand All @@ -81,16 +80,4 @@ public interface Encoder {
* @throws EncodeException when encoding failed due to a checked exception.
*/
void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;

/**
* Indicates whether this encoder supports encoding the given content type. Default implementation
* returns {@code true} for all content types.
*
* @param contentType the content type to check for support
* @return {@code true} if this encoder supports encoding the given content type, {@code false}
* otherwise
*/
default boolean supports(String contentType) {
return true;
}
}
13 changes: 1 addition & 12 deletions api/src/main/java/feign/codec/JsonEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,4 @@
import feign.Experimental;

@Experimental
public interface JsonEncoder extends Encoder {
/**
* {@inheritDoc}
*
* @param contentType {@inheritDoc}
* @return {@code true} if the given {@code contentType} is a JSON media type, {@code false}
*/
@Override
default boolean supports(String contentType) {
return contentType != null && contentType.trim().matches("(?i)\\w+/(?:[\\w._-]+\\+)?json.*");
}
}
public interface JsonEncoder extends Encoder {}
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,30 @@
package feign.form.spring;

import feign.Request;
import feign.form.multipart.AbstractPartEncoder;
import feign.form.multipart.Part;
import feign.form.multipart.PartMetadata;
import feign.RequestTemplate;
import feign.form.multipart.ConditionalEncoder;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Type;
import lombok.NonNull;
import org.springframework.web.multipart.MultipartFile;

/** Encoder for {@link MultipartFile} instances. */
public class MultipartFileEncoder extends AbstractPartEncoder {
public class MultipartFileEncoder extends ConditionalEncoder {
/**
* Encodes the content of the part using given {@link MultipartFile}.
*
* @param partMetadata {@inheritDoc}
* @return {@inheritDoc}
* Creates a new instance of {@link MultipartFileEncoder} that encodes {@link MultipartFile}
* instances.
*/
@Override
public Request.Body encode(PartMetadata partMetadata) {
var multipartFile = (MultipartFile) partMetadata.content().orElseThrow();
public MultipartFileEncoder() {
super(MultipartFileEncoder::doEncode, MultipartFileEncoder::isMultipartFile);
}

return new Part(
createHeaders(
multipartFile.getName(),
multipartFile.getOriginalFilename(),
multipartFile.getContentType()),
new MultipartFileBody(multipartFile));
private static void doEncode(Object object, Type bodyType, RequestTemplate template) {
template.body(new MultipartFileBody((MultipartFile) object));
}

/**
* {@inheritDoc}
*
* @param partMetadata {@inheritDoc}
* @return {@code true} if the content of the part is an instance of {@link MultipartFile}, {@code
* false} otherwise
*/
@Override
public boolean supports(PartMetadata partMetadata) {
return partMetadata.content().filter(MultipartFile.class::isInstance).isPresent();
private static boolean isMultipartFile(Object object, Type bodyType, RequestTemplate template) {
return object instanceof MultipartFile;
}

private record MultipartFileBody(@NonNull MultipartFile multipartFile) implements Request.Body {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright © 2012 The Feign Authors (feign@commonhaus.dev)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign.form.spring;

import feign.form.multipart.PartContext;
import feign.form.multipart.PartContextResolver;
import feign.form.multipart.PartContextResolverChain;
import java.util.stream.Stream;
import org.springframework.web.multipart.MultipartFile;

/**
* A {@link PartContextResolver} implementation that resolves a {@link PartContext} containing a
* {@link MultipartFile} into a {@link PartContext} with the unwrapped content of the {@link
* MultipartFile}.
*/
public class MultipartFilePartContextResolver implements PartContextResolver {
/**
* {@inheritDoc}
*
* @param partContext {@inheritDoc}
* @param chain {@inheritDoc}
* @return {@inheritDoc}
*/
@Override
public Stream<PartContext> resolve(PartContext partContext, PartContextResolverChain chain) {
var resolvedPartContext =
partContext
.content()
.filter(MultipartFile.class::isInstance)
.map(
content -> {
var multipartFile = (MultipartFile) content;

return partContext.toBuilder()
.name(multipartFile.getName())
.filename(multipartFile.getOriginalFilename())
.contentType(multipartFile.getContentType())
.build();
})
.orElse(partContext);

return chain.resolve(resolvedPartContext);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,14 @@
import feign.Util;
import feign.codec.Encoder;
import feign.form.MultipartFormEncoder;
import feign.form.multipart.MultipartFormBodyFactory;
import feign.form.multipart.PartBodyFactory;
import feign.form.multipart.PartContextResolverChain;
import feign.form.multipart.PartFactory;
import feign.form.spring.MultipartFileEncoder;
import feign.form.spring.MultipartFilePartContextResolver;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
Expand Down Expand Up @@ -91,8 +97,17 @@ private interface SpringStreamingMultipartFormTestClient {
private static class SpringStreamingMultipartFormTestConfiguration {
@Bean
public Encoder feignEncoder() {
var partFactory =
PartFactory.builder()
.partBodyFactory(new PartBodyFactory(List.of(new MultipartFileEncoder())))
.build();
var multipartFormBodyFactory =
new MultipartFormBodyFactory(
new PartContextResolverChain(
resolvers -> resolvers.addFirst(new MultipartFilePartContextResolver())),
partFactory);
return MultipartFormEncoder.builder()
.partEncoders(encoders -> encoders.addFirst(new MultipartFileEncoder()))
.multipartFormBodyFactory(multipartFormBodyFactory)
.build();
}
}
Expand Down
Loading