Java 未通过所有参数构造函数设置多部分/表单数据MutlipartFile 背景
这个问题是在我的日常工作中出现的,当时我正在为带有一些元信息的文件上传实现一个Java 未通过所有参数构造函数设置多部分/表单数据MutlipartFile 背景,java,spring-boot,rest,Java,Spring Boot,Rest,这个问题是在我的日常工作中出现的,当时我正在为带有一些元信息的文件上传实现一个POST multipart/form data端点。我不是春靴生态系统的专家;这个问题很可能是通过一个简单的修复来解决的,而我只是缺少了一个合适的搜索词 问题陈述 为了实现带有附加元信息的文件上传端点,我编写了以下@RestController: @RestController @RequestMapping(Resource.ROOT) @AllArgsConstructor(onConstructor = @__
POST multipart/form data
端点。我不是春靴生态系统的专家;这个问题很可能是通过一个简单的修复来解决的,而我只是缺少了一个合适的搜索词
问题陈述
为了实现带有附加元信息的文件上传端点,我编写了以下@RestController
:
@RestController
@RequestMapping(Resource.ROOT)
@AllArgsConstructor(onConstructor = @__({@Inject}))
public class Resource {
public static final String ROOT = "/test";
private final Logger logger;
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA, produces = MediaType.APPLICATION_JSON)
public ResponseEntity<Void> test(@Valid final Request request) {
logger.info("request = {}", request);
return ResponseEntity.ok().build();
}
}
还有一个小的快乐路径测试:
@SpringBootTest
@AutoConfigureMockMvc
class TestCase {
@Autowired
private MockMvc mockMvc;
@Test
void shouldReturnOk() throws Exception {
// GIVEN
final byte[] content = Files.readAllBytes(Path.of(".", "src/test/resources/PenPen.png"));
final String name = "name";
// WHEN
// @formatter:off
mockMvc.perform(MockMvcRequestBuilders
.multipart(Resource.ROOT)
.file("file", content)
.param("name", name))
// THEN
.andExpect(status().isOk());
// @formatter:on
}
}
完整的MRE可在上找到
运行测试(/mvnw test)
时,测试失败,端点返回的是400错误请求,而不是200 OK
。读取日志会发现请求参数文件
为null
:
...
Content type = text/plain;charset=UTF-8
Body = file: must not be null.
...
我部分理解为什么它是空的。有了这部分知识,我就能够通过使请求中的文件字段变为可变字段来规避这个问题:
@ToString
@Getter
@AllArgsConstructor
public class Request {
@NotNull
private final String name;
@Setter
@NotNull
private MultipartFile file;
}
可以在上找到“修复”问题的代码
然而,这使得请求
是可变的,这是我想要阻止的。为了进一步调查,我在Request
上展开了lombok注释,并添加了一些日志记录:
public class Request {
private static final Logger LOGGER = LoggerFactory.getLogger(Request.class);
@NotNull
private final String name;
@NotNull
private MultipartFile file;
public Request(final String name, final MultipartFile file) {
this.name = name;
this.setFile(file);
}
public @NotNull String getName() {
return this.name;
}
public @NotNull MultipartFile getFile() {
return this.file;
}
public String toString() {
return "Request(name=" + this.getName() + ", file=" + this.getFile() + ")";
}
public void setFile(final MultipartFile file) {
LOGGER.info("file = {}", file);
this.file = file;
}
}
可在上找到展开版本的代码
当查看现在成功的测试的日志语句时,我们可以看到Request::setFile
被调用了两次:
2020-09-05 09:42:31.049 INFO 11012 --- [ main] d.turing85.springboot.multipart.Request : file = null
2020-09-05 09:42:31.056 INFO 11012 --- [ main] d.turing85.springboot.multipart.Request : file = org.springframework.mock.web
第一个调用来自构造函数调用。我想,第二个调用来自Spring的表单参数映射机制中的某个地方
我知道有可能在端点上单独定义表单参数,并在方法中构造请求
实例:
public class Resource {
...
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA, produces = MediaType.APPLICATION_JSON)
public ResponseEntity<Void> test(
@RequestPart(name = "name") final String name,
@RequestPart(name = "file") final MultipartFile file) {
final Request request = new Request(name, file);
logger.info("request = {}", request);
return ResponseEntity.ok().build();
}
}
公共类资源{
...
@PostMapping(使用=MediaType.MULTIPART\u FORM\u数据,生成=MediaType.APPLICATION\u JSON)
公众反应测试(
@RequestPart(name=“name”)最终字符串名称,
@RequestPart(name=“file”)最终多部分文件{
最终请求=新请求(名称、文件);
info(“request={}”,request);
返回ResponseEntity.ok().build();
}
}
然而,这将导致其他问题。例如,我们必须为MissingServletRequestPartException
添加一个额外的异常映射器,并将返回的HTTP响应与BindException
的现有响应对齐。如果可能的话,我希望避免这种情况
出现了对该主题的搜索。然而,这个解决方案并不适合我,因为我不使用MVC(我想)
问题:
是否有可能保持请求
不可变,以便Spring能够将多部分文件
传递给all args构造函数,而不是事后通过setter进行设置?编写自定义映射器/转换器是可以接受的,但我没有发现为特定端点或特定类型编写映射器的可能性
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> test(@Valid @ModelAttribute final RequestDto request) {
return ResponseEntity.ok().build();
}
上面的代码使用ModelAttribute
@SpringBootTest
@AutoConfigureMockMvc
class FileUploadControllerIT {
@Autowired
private MockMvc mockMvc;
@Test
void shouldReturnOk() throws Exception {
// GIVEN
final byte[] content = Files.readAllBytes(Paths.get(Thread.currentThread().getContextClassLoader().getResource("text.txt").toURI()));
final String name = "name";
// WHEN
// @formatter:off
mockMvc.perform(MockMvcRequestBuilders
.multipart("/context/api/v1")
.file("multipartFile", content)
.param("name", name))
// THEN
.andExpect(status().isOk());
// @formatter:on
}
}
你也给出了绝对路径,我猜这是错误的。您可以使用类加载器获取文件
上面的代码使用ModelAttribute
@SpringBootTest
@AutoConfigureMockMvc
class FileUploadControllerIT {
@Autowired
private MockMvc mockMvc;
@Test
void shouldReturnOk() throws Exception {
// GIVEN
final byte[] content = Files.readAllBytes(Paths.get(Thread.currentThread().getContextClassLoader().getResource("text.txt").toURI()));
final String name = "name";
// WHEN
// @formatter:off
mockMvc.perform(MockMvcRequestBuilders
.multipart("/context/api/v1")
.file("multipartFile", content)
.param("name", name))
// THEN
.andExpect(status().isOk());
// @formatter:on
}
}
你也给出了绝对路径,我猜这是错误的。您可以使用类加载器获取文件。是否需要包装多部分文件
?似乎如果您将其分离,您将不会遇到可变性问题。我尝试使用ModelAttribute,它可以工作,但您必须为多部分数据定义setter,否则它将为空。@limido是的,它是空的。我在我的帖子中解释了为什么我希望它出现在Request
中。@Gurkanİlleez定义的二传手不符合这一点。我已经演示了为文件提供setter
“修复”了问题,而没有添加@modeldattribute
。当然,它不能解决这一点,但转换器正在使用setter转换多部分文件数据是否需要包装多部分文件
?似乎如果您将其分离,您将不会遇到可变性问题。我尝试使用ModelAttribute,它可以工作,但您必须为多部分数据定义setter,否则它将为空。@limido是的,它是空的。我在我的帖子中解释了为什么我希望它出现在Request
中。@Gurkanİlleez定义的二传手不符合这一点。我已经演示了为文件提供setter
“修复”了问题,而没有添加@modeldattribute
。当然,它不能解决这一点,但是转换器正在使用setter为我转换多部分文件数据,测试仍然失败@ModelAttriibute
。这并不能回答问题。至于我为什么想要不变性:对于JSON请求,我们可以对DTO进行注释,以便使用bulder。对于表单数据,情况似乎并非如此。这反过来导致DTO的异属结构,这反过来又引入了不必要的复杂性。对我来说,使用@modelatriibute
测试仍然失败。这并不能回答问题。至于我为什么想要不变性:对于JSON请求,我们可以对DTO进行注释,以便使用bulder。对于表单数据,情况似乎并非如此。这反过来导致DTO的异属结构,这反过来又引入了不必要的复杂性。