英文:
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<Void> 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="application/json"
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 = "/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();
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论