Spring Boot的PostMapping:如果没有Content-Type,如何强制执行JSON解码

huangapple go评论62阅读模式
英文:

Spring Boot PostMapping: How to enforce JSON decoding if no Content-Type present

问题

我正在使用Spring Boot(2.3.2)实现一个简单的REST控制器,以通过POST从外部源接收数据。该源不附带Content-Type,但提供包含JSON的主体。

我的控制器非常简单

@RestController
@RequiredArgsConstructor
public class WaitTimeImportController {
    private final StorageService storageService;

    @PostMapping(value = "/endpoint")
    public void setWaitTimes(@RequestBody DataObject toStore) {
        storageService.store(toStore);
    }
}
@Data
@NoArgsConstructor
class DataObject {
    private String data;
}
@Service
class StorageService {
    void store(DataObject dataObject) {
        System.out.println("received");
    }
}

现在,如果我发送一个json字符串,它将不会触发该请求映射。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class WaittimesApplicationTests {

    @Test
    void happyPath() throws IOException {
        WebClient client = client();

        String content = "{ \"data\": \"test\" }";
        AtomicBoolean ab = new AtomicBoolean();
        await().atMost(Duration.FIVE_SECONDS).ignoreExceptions()
            .until(() -> {
                ClientResponse response = client.post()
                        // .header("Content-Type", "application/json")
                        .bodyValue(content)
                        .exchange()
                        .timeout(java.time.Duration.ofMillis(200))
                        .block();
                ab.set(response.statusCode() == HttpStatus.ACCEPTED);
                return ab.get();
            });
    }
}

除非我取消上面的"header"行的注释,否则此测试将失败;取消注释后,测试通过。

由于我无法影响提交POST请求的源,因此无法将application/json添加为请求头。

我已经在Spring中搜索了很多有关内容协商的内容,但是我尝试过的所有方法都没有奏效。

  • consumes="application/json"添加到@PostMapping注释中
  • consumes=MediaType.ALL_VALUE添加到@PostMapping注释中
  • 添加MappingJackson2HttpMessageConverter bean没有奏效
  • 添加ContentNegotiationStrategy bean无法编译,出现“cannot access javax.servlet.ServletException” - 是否值得追求这条路?

我该怎么做才能强制映射以接受非“Content-Type”请求并将其解码为JSON?

英文:

I'm implementing a simple rest controller with Spring Boot (2.3.2) to receive data from an external source via POST. That source doesn't send a Content-Type with it, but provides a body containing JSON.

My controller is really simple

@RestController
@RequiredArgsConstructor
public class WaitTimeImportController {
    private final StorageService storageService;

    @PostMapping(value = "/endpoint")
    public void setWaitTimes(@RequestBody DataObject toStore) {
        storageService.store(toStore);
    }
}
@Data
@NoArgsConstructor
class DataObject {
  private String data;
}
@Service
class StorageService {
    void store(DataObject do) {
        System.out.println("received");
    }
}

Now, if I send it a json String, it will not trigger that request mapping.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class WaittimesApplicationTests {

    @Test
    void happyPath() throws IOException {
        WebClient client = client();

        String content = "{ \"data\": \"test\"} ";
        AtomicBoolean ab = new AtomicBoolean();
        await().atMost(Duration.FIVE_SECONDS).ignoreExceptions()
            .until(() -> {
                ClientResponse response = client.post()
                        // .header("Content-Type", "application/json")
                        .bodyValue(content)
                        .exchange()
                        .timeout(java.time.Duration.ofMillis(200))
                        .block();
        ab.set(response.statusCode() == HttpStatus.ACCEPTED);
                return ab.get();
            });
    }
}

This test fails unless I uncomment the "header" line above; then it is fine.

Since I have no influence over the source submitting the POST request, I cannot add application/json as the request header.

I've been searching a lot about content negotiation in Spring, but nothing I've tried worked.

  • adding a consumes="application/json" to the @PostMapping annotation
  • adding a consumes=MediaType.ALL_VALUE to the @PostMapping annotation
  • adding a MappingJackson2HttpMessageConverter bean didn't work
  • adding a ContentNegotiationStrategy bean didn't compile "cannot access javax.servlet.ServletException" - is it a worthwhile route to pursue this?

What can I do to enforce the mapping to accept a non-"Content-Type" request and decode it as JSON?

答案1

得分: 0

Uggg. 因此,Lebecca的评论引发了我对如何使用Spring调整标题字段的思考(我正在使用webflux,因此链接的解决方案不适用),我找不到一个合适的教程,而且文档在搜索时在几个页面中有点分散。

有一些障碍,以下是精简版本。

清晰的方法:调整标题以注入Content-Type

所以你可以创建一个在请求实际达到映射之前被评估的WebFilter。只需定义一个实现它的@Component

要添加到传递的ServerWebExchange对象中,你需要将其包装在ServerWebExchangeDecorator中。

@Component
class MyWebFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(new MyServerExchangeDecorator(exchange));
    }
}

该装饰器可以同时操纵请求和响应;只需重写适当的方法。当然,请求也需要被装饰 - 不能直接在原始对象上工作(如果尝试设置其属性,实际上会抛出异常,因为它是只读的)。

class MyServerExchangeDecorator extends ServerWebExchangeDecorator {

    protected MyServerExchangeDecorator(ServerWebExchange delegate) {
        super(delegate);
    }
    @Override
    public ServerHttpRequest getRequest() {
        return new ContentRequestDecorator(super.getRequest());
    }
}

现在,你只需要实现ContentRequestDecorator,就可以继续了。

class ContentRequestDecorator extends ServerHttpRequestDecorator {

    public ContentRequestDecorator(ServerHttpRequest delegate) {
        super(delegate);
    }
    @Override
    public HttpHeaders getHeaders() {
        HttpHeaders httpHeaders = HttpHeaders.writableHttpHeaders(super.getHeaders());
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);

        return httpHeaders;
    }
}

试图直接在传递的对象上调用setContentType会导致OperationNotSupportedException,因为它是只读的;因此使用了HttpHeaders.writableHttpHeaders()调用。

当然,这会调整所有传入请求的头部,所以如果你打算这样做并且不想要“核选项”,可以在之前检查请求。

简单的方法

所以上述方法会触发Spring将传入请求映射到consumes="application/json"的端点。

然而,如果你没有将我之前的描述视为带有轻微讽刺意味,我应该更直接。这是一个对于一个非常简单问题来说非常复杂的解决方案。

我最终采取的方法是

    @Autowired
    private ObjectMapper mapper;

    @PostMapping(value = "/endpoint")
    public void setMyData(HttpEntity<String> json) {
        try {
            MyData data = mapper.readValue(json.getBody(), MyData.class);
            storageService.setData(data);
        } catch (JsonProcessingException e) {
            return ResponseEntity.unprocessableEntity().build();
        }
    }
英文:

Uggg. So Lebecca's comment sent on a wild run through how to adjust header field with Spring (I'm using webflux, so the linked solution was not applicable as such), and I couldn't find a proper tutorial on it and the documentation is a bit distributed amongst several pages you come across when searching.

There are a few roadblocks, so here's a condensed version.

Clean way: Adjusting the header to inject the Content-Type`

So what you can do is create a WebFilter that gets evaluated before the request actually reaches the mapping. Just define a @Component that implements it.

To add to the ServerWebExchange object that gets passed, you wrap it in a ServerWebExchangeDecorator.

@Component
class MyWebFilter implements WebFilter {

    @Override
    public Mono&lt;Void&gt; filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(new MyServerExchangeDecorator(exchange));
    }
}

That decorator can manipulate both the request and the response; just override the appropriate method. Of course, the request needs to be decorated as well - can't go around working on the original object (it actually throws an exception if you try to set its properties because it's read only).

class MyServerExchangeDecorator extends ServerWebExchangeDecorator {

    protected MyServerExchangeDecorator(ServerWebExchange delegate) {
        super(delegate);
    }
    @Override
    public ServerHttpRequest getRequest() {
        return new ContentRequestDecorator(super.getRequest());
    }
}

Now you just implement the ContentRequestDecorator and you're good to go.

class ContentRequestDecorator extends ServerHttpRequestDecorator {

    public ContentRequestDecorator(ServerHttpRequest delegate) {
        super(delegate);
    }
    @Override
    public HttpHeaders getHeaders() {
        HttpHeaders httpHeaders = HttpHeaders.writableHttpHeaders(super.getHeaders());
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);

        return httpHeaders;
    }
}

Trying to just call setContentType on the object being passed in results in an OperationNotSupportedException because, again, it is read only; hence the HttpHeaders.writableHttpHeaders() call.

Of course, this will adjust all headers for all incoming requests, so if you plan on doing this and don't want the nuclear option, inspect the request beforehand.

The simple way

So the way above triggers Springs mapping of the incoming request to a consumes=&quot;application/json&quot; endpoint.

However, if you didn't read my previous description as containing a slightly sarcastic undertone, I should have been less subtle. It's a very involved solution for a very simple problem.

The way I actually ended up doing it was

    @Autowired
    private ObjectMapper mapper;

    @PostMapping(value = &quot;/endpoint&quot;)
    public void setMyData(HttpEntity&lt;String&gt; json) {
        try {
            MyData data = mapper.readValue(json.getBody(), MyData.class);
            storageService.setData(data);
        } catch (JsonProcessingException e) {
            return ResponseEntity.unprocessableEntity().build();
        }
    }

huangapple
  • 本文由 发表于 2020年9月16日 22:35:32
  • 转载请务必保留本文链接:https://go.coder-hub.com/63922374.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定