Dan*_*lli 27 java rest json spring-mvc jackson
我试图在Spring Rest Controller中使用PUT请求方法部分更新实体时,区分空值和未提供的值.
以下面的实体为例:
@Entity
private class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/* let's assume the following attributes may be null */
private String firstName;
private String lastName;
/* getters and setters ... */
}
Run Code Online (Sandbox Code Playgroud)
我的人员库(Spring Data):
@Repository
public interface PersonRepository extends CrudRepository<Person, Long> {
}
Run Code Online (Sandbox Code Playgroud)
我使用的DTO:
private class PersonDTO {
private String firstName;
private String lastName;
/* getters and setters ... */
}
Run Code Online (Sandbox Code Playgroud)
我的Spring RestController:
@RestController
@RequestMapping("/api/people")
public class PersonController {
@Autowired
private PersonRepository people;
@Transactional
@RequestMapping(path = "/{personId}", method = RequestMethod.PUT)
public ResponseEntity<?> update(
@PathVariable String personId,
@RequestBody PersonDTO dto) {
// get the entity by ID
Person p = people.findOne(personId); // we assume it exists
// update ONLY entity attributes that have been defined
if(/* dto.getFirstName is defined */)
p.setFirstName = dto.getFirstName;
if(/* dto.getLastName is defined */)
p.setLastName = dto.getLastName;
return ResponseEntity.ok(p);
}
}
Run Code Online (Sandbox Code Playgroud)
要求遗失财产
{"firstName": "John"}
Run Code Online (Sandbox Code Playgroud)
预期行为:更新firstName= "John"(lastName保持不变).
请求null属性
{"firstName": "John", "lastName": null}
Run Code Online (Sandbox Code Playgroud)
预期的行为:更新firstName="John"和设置lastName=null.
我无法区分这两种情况,因为lastNameDTO总是null由杰克逊设定.
注意:我知道REST最佳实践(RFC 6902)建议使用PATCH而不是PUT进行部分更新,但在我的特定场景中,我需要使用PUT.
有一个更好的选择,它不涉及更改您的 DTO 或自定义您的设置器。
它涉及让 Jackson 将数据与现有数据对象合并,如下所示:
MyData existingData = ...
ObjectReader readerForUpdating = objectMapper.readerForUpdating(existingData);
MyData mergedData = readerForUpdating.readValue(newData);
Run Code Online (Sandbox Code Playgroud)
中不存在的任何字段newData都不会覆盖 中的数据existingData,但如果存在字段,它将被覆盖,即使它包含null.
演示代码:
ObjectMapper objectMapper = new ObjectMapper();
MyDTO dto = new MyDTO();
dto.setText("text");
dto.setAddress("address");
dto.setCity("city");
String json = "{\"text\": \"patched text\", \"city\": null}";
ObjectReader readerForUpdating = objectMapper.readerForUpdating(dto);
MyDTO merged = readerForUpdating.readValue(json);
Run Code Online (Sandbox Code Playgroud)
结果是 {"text": "patched text", "address": "address", "city": null}
在 Spring Rest Controller 中,您需要获取原始 JSON 数据,而不是让 Spring 反序列化它来执行此操作。所以像这样改变你的端点:
@Autowired ObjectMapper objectMapper;
@RequestMapping(path = "/{personId}", method = RequestMethod.PATCH)
public ResponseEntity<?> update(
@PathVariable String personId,
@RequestBody JsonNode jsonNode) {
RequestDto existingData = getExistingDataFromSomewhere();
ObjectReader readerForUpdating = objectMapper.readerForUpdating(existingData);
RequestDTO mergedData = readerForUpdating.readValue(jsonNode);
...
)
Run Code Online (Sandbox Code Playgroud)
按照杰克逊作者的建议使用布尔标志。
class PersonDTO {
private String firstName;
private boolean isFirstNameDirty;
public void setFirstName(String firstName){
this.firstName = firstName;
this.isFirstNameDirty = true;
}
public void getFirstName() {
return firstName;
}
public boolean hasFirstName() {
return isFirstNameDirty;
}
}
Run Code Online (Sandbox Code Playgroud)
实际上,如果忽略验证,您可以像这样解决您的问题。
public class BusDto {
private Map<String, Object> changedAttrs = new HashMap<>();
/* getter and setter */
}
Run Code Online (Sandbox Code Playgroud)
另一个选择是使用java.util.Optional。
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.Optional;
@JsonInclude(JsonInclude.Include.NON_NULL)
private class PersonDTO {
private Optional<String> firstName;
private Optional<String> lastName;
/* getters and setters ... */
}
Run Code Online (Sandbox Code Playgroud)
如果未设置firstName,则该值为null,@ JsonInclude批注将忽略该值。否则,如果在请求对象中隐式设置,则firstName将不为null,而firstName.get()将为null。我在此浏览了@laffuste的解决方案,该解决方案在另一条注释中链接到较低的位置(garretwilson的最初评论说它不起作用,结果证明行得通)。
您还可以使用Jackson的ObjectMapper将DTO映射到Entity,它将忽略请求对象中未传递的属性:
import com.fasterxml.jackson.databind.ObjectMapper;
class PersonController {
// ...
@Autowired
ObjectMapper objectMapper
@Transactional
@RequestMapping(path = "/{personId}", method = RequestMethod.PUT)
public ResponseEntity<?> update(
@PathVariable String personId,
@RequestBody PersonDTO dto
) {
Person p = people.findOne(personId);
objectMapper.updateValue(p, dto);
personRepository.save(p);
// return ...
}
}
Run Code Online (Sandbox Code Playgroud)
使用java.util.Optional验证DTO也有所不同。 它记录在这里,但是花了我一段时间才找到:
// ...
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
// ...
private class PersonDTO {
private Optional<@NotNull String> firstName;
private Optional<@NotBlank @Pattern(regexp = "...") String> lastName;
/* getters and setters ... */
}
Run Code Online (Sandbox Code Playgroud)
在这种情况下,可能根本不会设置firstName,但是如果已设置,则在验证PersonDTO的情况下可能不会将其设置为null。
//...
import javax.validation.Valid;
//...
public ResponseEntity<?> update(
@PathVariable String personId,
@RequestBody @Valid PersonDTO dto
) {
// ...
}
Run Code Online (Sandbox Code Playgroud)
还可能值得一提的是,对Optional的使用似乎受到了激烈的争论,而在撰写本文之时,Lombok的维护者将不支持它(例如,参见此问题)。这意味着在具有带有约束的Optional字段的类上使用lombok.Data/lombok.Setter无效(尝试创建具有完整约束的setter),因此使用@ Setter / @ Data会引发异常,因为setter和member变量已设置约束。写不带Optional参数的Setter似乎也是更好的形式,例如:
//...
import lombok.Getter;
//...
@Getter
private class PersonDTO {
private Optional<@NotNull String> firstName;
private Optional<@NotBlank @Pattern(regexp = "...") String> lastName;
public void setFirstName(String firstName) {
this.firstName = Optional.ofNullable(firstName);
}
// etc...
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
5211 次 |
| 最近记录: |