我有一个项目,我使用Spring MVC + Jackson来构建REST服务。假设我有以下java实体
public class MyEntity {
private Integer id;
private boolean aBoolean;
private String aVeryBigString;
//getter & setters
}
有时候,我只是想更新布尔值,我不认为用更大的字符串发送整个对象只是为了更新一个简单的布尔值。所以,我考虑过使用PATCH HTTP方法只发送需要更新的字段。所以,我在我的控制器中声明了以下方法:
@RequestMapping(method = RequestMethod.PATCH)
public void patch(@RequestBody MyVariable myVariable) {
//calling a service to update the entity
}
问题是:我如何知道哪些字段需要更新?例如,如果客户端只想更新布尔值,我将得到一个空的“aVeryBigString”对象。我怎么知道用户只想更新布尔值,但不想清空字符串?
我通过构建自定义URL“解决”了这个问题。例如,以下URL:POST / myentities / 1 / aboolean / true将映射到只允许更新布尔值的方法。此解决方案的问题在于它不符合REST。我不希望100%兼容REST,但我不愿意提供自定义URL来更新每个字段(特别是考虑到当我想更新多个字段时它会导致问题)。
另一个解决方案是将“MyEntity”拆分为多个资源并只更新这些资源,但我觉得它没有意义:“MyEntity”是一个普通资源,它不是由其他资源组成的。
那么,有一种解决这个问题的优雅方式吗?
您可以将布尔值更改为布尔值,并为您不想更新的所有字段分配空值。唯一一个非null值将定义您要更新的字段客户端。
这是使用谷歌GSON的补丁命令的实现。
package de.tef.service.payment;
import com.google.gson.*;
class JsonHelper {
static <T> T patch(T object, String patch, Class<T> clazz) {
JsonElement o = new Gson().toJsonTree(object);
JsonObject p = new JsonParser().parse(patch).getAsJsonObject();
JsonElement result = patch(o, p);
return new Gson().fromJson(result, clazz);
}
static JsonElement patch(JsonElement object, JsonElement patch) {
if (patch.isJsonArray()) {
JsonArray result = new JsonArray();
object.getAsJsonArray().forEach(result::add);
return result;
} else if (patch.isJsonObject()) {
System.out.println(object + " => " + patch);
JsonObject o = object.getAsJsonObject();
JsonObject p = patch.getAsJsonObject();
JsonObject result = new JsonObject();
o.getAsJsonObject().entrySet().stream().forEach(e -> result.add(e.getKey(), p.get(e.getKey()) == null ? e.getValue() : patch(e.getValue(), p.get(e.getKey()))));
return result;
} else if (patch.isJsonPrimitive()) {
return patch;
} else if (patch.isJsonNull()) {
return patch;
} else {
throw new IllegalStateException();
}
}
}
实现是recursiv来处理嵌套结构。数组未合并,因为它们没有合并的键。
“补丁”JSON直接从String转换为JsonElement而不是转换为对象,以使未填充的字段与填充NULL的字段分开。
我注意到许多提供的答案都是JSON补丁或不完整的答案。以下是功能真实代码所需内容的完整说明和示例
完整的补丁功能:
@ApiOperation(value = "Patch an existing claim with partial update")
@RequestMapping(value = CLAIMS_V1 + "/{claimId}", method = RequestMethod.PATCH)
ResponseEntity<Claim> patchClaim(@PathVariable Long claimId, @RequestBody Map<String, Object> fields) {
// Sanitize and validate the data
if (claimId <= 0 || fields == null || fields.isEmpty() || !fields.get("claimId").equals(claimId)){
return new ResponseEntity<>(HttpStatus.BAD_REQUEST); // 400 Invalid claim object received or invalid id or id does not match object
}
Claim claim = claimService.get(claimId);
// Does the object exist?
if( claim == null){
return new ResponseEntity<>(HttpStatus.NOT_FOUND); // 404 Claim object does not exist
}
// Remove id from request, we don't ever want to change the id.
// This is not necessary,
// loop used below since we checked the id above
fields.remove("claimId");
fields.forEach((k, v) -> {
// use reflection to get field k on object and set it to value v
// Change Claim.class to whatver your object is: Object.class
Field field = ReflectionUtils.findField(Claim.class, k); // find field in the object class
field.setAccessible(true);
ReflectionUtils.setField(field, claim, v); // set given field for defined object to value V
});
claimService.saveOrUpdate(claim);
return new ResponseEntity<>(claim, HttpStatus.OK);
}
对于某些人而言,上述情况可能会令人困惑,因为较新的开发人员通常不会像这样处理反射。基本上,无论您在正文中传递此函数,它都会使用给定的ID找到关联的声明,然后仅将您传入的字段更新为键值对。
示例正文:
PATCH / Claims / 7
{
"claimId":7,
"claimTypeId": 1,
"claimStatus": null
}
以上内容将claimTypeId和claimStatus更新为权利要求7的给定值,保持所有其他值不变。
所以回报将是这样的:
{
"claimId": 7,
"claimSrcAcctId": 12345678,
"claimTypeId": 1,
"claimDescription": "The vehicle is damaged beyond repair",
"claimDateSubmitted": "2019-01-11 17:43:43",
"claimStatus": null,
"claimDateUpdated": "2019-04-09 13:43:07",
"claimAcctAddress": "123 Sesame St, Charlotte, NC 28282",
"claimContactName": "Steve Smith",
"claimContactPhone": "777-555-1111",
"claimContactEmail": "[email protected]",
"claimWitness": true,
"claimWitnessFirstName": "Stan",
"claimWitnessLastName": "Smith",
"claimWitnessPhone": "777-777-7777",
"claimDate": "2019-01-11 17:43:43",
"claimDateEnd": "2019-01-11 12:43:43",
"claimInvestigation": null,
"scoring": null
}
正如您所看到的,完整对象将返回而不会更改除要更改的内容之外的任何数据。我知道这里的解释有点重复,我只是想清楚地勾勒出来。
我的答案可能会迟到,但如果有人仍然面临同样的问题。我已经使用所有可能的解决方案来解决PATCH问题,但无法管理部分更新对象的字段。所以我切换到POST并使用post,我可以更新特定字段而不更改未更改字段的值。
这可能很晚,但为了新手和遇到同样问题的人,让我分享一下我自己的解决方案。
在我过去的项目中,为了简单起见,我只使用原生的Java Map。它将捕获所有新值,包括客户端显式设置为null的空值。此时,很容易确定哪些java属性需要设置为null,与使用相同的POJO作为域模型不同,您将无法区分客户端设置的哪些字段为null和它们不包含在更新中,但默认情况下为null。
此外,您必须要求http请求发送要更新的记录的ID,并且不要将其包含在修补程序数据结构中。我所做的是,将URL中的ID设置为路径变量,将补丁数据设置为PATCH主体。然后使用ID,您将首先通过域模型获取记录,然后使用HashMap,您可以使用映射器服务或实用程序,用于修补相关域模型的更改。
更新
您可以使用这种通用代码为您的服务创建抽象超类,您必须使用Java Generics。这只是可能实现的一部分,我希望你能得到这个想法。使用Orika或Dozer等mapper框架也更好。
public abstract class AbstractService<Entity extends BaseEntity, DTO extends BaseDto> {
@Autowired
private MapperService mapper;
@Autowired
private BaseRepo<Entity> repo;
private Class<DTO> dtoClass;
private Class<Entity> entityCLass;
public AbstractService(){
entityCLass = (Class<Entity>) SomeReflectionTool.getGenericParameter()[0];
dtoClass = (Class<DTO>) SomeReflectionTool.getGenericParameter()[1];
}
public DTO patch(Long id, Map<String, Object> patchValues) {
Entity entity = repo.get(id);
DTO dto = mapper.map(entity, dtoClass);
mapper.map(patchValues, dto);
Entity updatedEntity = toEntity(dto);
save(updatedEntity);
return dto;
}
}
正确的方法是在JSON PATCH RFC 6902提出的方法
请求示例如下:
PATCH http://example.com/api/entity/1 HTTP/1.1
Content-Type: application/json-patch+json
[
{ "op": "replace", "path": "aBoolean", "value": true }
]
在挖掘了一下后,我找到了一个可接受的解决方案,使用Spring MVC DomainObjectReader
目前使用的相同方法,另见:JsonPatchHandler
@RepositoryRestController
public class BookCustomRepository {
private final DomainObjectReader domainObjectReader;
private final ObjectMapper mapper;
private final BookRepository repository;
@Autowired
public BookCustomRepository(BookRepository bookRepository,
ObjectMapper mapper,
PersistentEntities persistentEntities,
Associations associationLinks) {
this.repository = bookRepository;
this.mapper = mapper;
this.domainObjectReader = new DomainObjectReader(persistentEntities, associationLinks);
}
@PatchMapping(value = "/book/{id}", consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<?> patch(@PathVariable String id, ServletServerHttpRequest request) throws IOException {
Book entityToPatch = repository.findById(id).orElseThrow(ResourceNotFoundException::new);
Book patched = domainObjectReader.read(request.getBody(), entityToPatch, mapper);
repository.save(patched);
return ResponseEntity.noContent().build();
}
}
PATCH
的重点是你没有发送整个实体表示,所以我不理解你对空字符串的评论。你必须处理某种简单的JSON,例如:
{ aBoolean: true }
并将其应用于指定的资源。我们的想法是,所接收的是所需资源状态和当前资源状态的差异。
Spring会/不能使用PATCH
修补你的对象,因为你已经遇到了同样的问题:JSON反序列化器创建了一个带有空字段的Java POJO。
这意味着你必须为修补实体提供一个自己的逻辑(即仅在使用PATCH
但不使用POST
时)。
要么你知道你只使用非原始类型,或者一些规则(空字符串是null
,这对每个人都不起作用),或者你必须提供一个额外的参数来定义被覆盖的值。最后一个适用于我:JavaScript应用程序知道除了列出到服务器的JSON主体之外哪些字段已被更改和发送。例如,如果字段description
被命名为更改(补丁)但未在JSON正文中给出,则它被置为空。
难道你不能只发送一个包含已更新字段的对象吗?
脚本调用:
var data = JSON.stringify({
aBoolean: true
});
$.ajax({
type: 'patch',
contentType: 'application/json-patch+json',
url: '/myentities/' + entity.id,
data: data
});
Spring MVC控制器:
@PatchMapping(value = "/{id}")
public ResponseEntity<?> patch(@RequestBody Map<String, Object> updates, @PathVariable("id") String id)
{
// updates now only contains keys for fields that was updated
return ResponseEntity.ok("resource updated");
}
在控制器的path
member中,遍历updates
映射中的键/值对。在上面的例子中,"aBoolean"
key将保持值true
。下一步是通过调用实体setter来实际分配值。但是,这是一个不同的问题。
您可以使用Optional<>
:
public class MyEntityUpdate {
private Optional<String> aVeryBigString;
}
这样,您可以按如下方式检查更新对象:
if(update.getAVeryBigString() != null)
entity.setAVeryBigString(update.getAVeryBigString().get());
如果字段aVeryBigString
不在JSON文档中,则POJO aVeryBigString
字段将为null
。如果它在JSON文档中,但是具有null
值,则POJO字段将是包含值Optional
的null
。此解决方案允许您区分“无更新”和“设置为空”的情况。
我修复了这样的问题,因为我无法改变服务
public class Test {
void updatePerson(Person person,PersonPatch patch) {
for (PersonPatch.PersonPatchField updatedField : patch.updatedFields) {
switch (updatedField){
case firstname:
person.setFirstname(patch.getFirstname());
continue;
case lastname:
person.setLastname(patch.getLastname());
continue;
case title:
person.setTitle(patch.getTitle());
continue;
}
}
}
public static class PersonPatch {
private final List<PersonPatchField> updatedFields = new ArrayList<PersonPatchField>();
public List<PersonPatchField> updatedFields() {
return updatedFields;
}
public enum PersonPatchField {
firstname,
lastname,
title
}
private String firstname;
private String lastname;
private String title;
public String getFirstname() {
return firstname;
}
public void setFirstname(final String firstname) {
updatedFields.add(PersonPatchField.firstname);
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(final String lastname) {
updatedFields.add(PersonPatchField.lastname);
this.lastname = lastname;
}
public String getTitle() {
return title;
}
public void setTitle(final String title) {
updatedFields.add(PersonPatchField.title);
this.title = title;
}
}
杰克逊只在价值存在时才打电话所以你可以保存调用哪个setter。