Spring Boot中json-patch不可变字段修改限制的实现问询
我之前在Spring Boot项目里用json-patch处理PATCH请求时,也碰到过要限制修改id、creation_time这类不可变字段的需求,下面给你分享几个实用的Java实现方案:
方案1:自定义JsonPatch验证器(推荐)
核心思路是遍历JsonPatch里的所有操作,检查每个操作的路径是否指向不可变字段,提前拦截非法请求。
先写一个通用的验证工具类:
import com.github.fge.jsonpatch.JsonPatch; import com.github.fge.jsonpatch.JsonPatchOperation; import java.util.Arrays; import java.util.List; public class JsonPatchValidator { // 定义不可修改的字段路径(注意用JsonPointer格式,以/开头) private static final List<String> IMMUTABLE_FIELDS = Arrays.asList("/id", "/creation_time"); public static void validatePatch(JsonPatch patch) { for (JsonPatchOperation operation : patch.getOperations()) { String targetPath = operation.getPath().toString(); // 检查是否直接修改不可变字段 if (IMMUTABLE_FIELDS.contains(targetPath)) { throw new IllegalArgumentException(String.format("不可修改字段:%s", targetPath)); } // 可选:拦截修改不可变字段的子字段(比如嵌套场景下的/user/id) boolean isNestedImmutable = IMMUTABLE_FIELDS.stream() .anyMatch(immutablePath -> targetPath.startsWith(immutablePath + "/")); if (isNestedImmutable) { throw new IllegalArgumentException(String.format("不可修改字段或其子字段:%s", targetPath)); } } } }
然后在Controller里调用验证:
@RestController @RequestMapping("/api/entities") public class EntityController { @PatchMapping("/{id}") public ResponseEntity<Entity> updateEntity(@PathVariable Long id, @RequestBody JsonPatch patch, EntityRepository repository) { // 先验证PATCH请求的合法性 try { JsonPatchValidator.validatePatch(patch); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body(null); // 也可以返回自定义错误信息 } // 执行正常的PATCH逻辑 Entity existingEntity = repository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("实体不存在")); ObjectMapper objectMapper = new ObjectMapper(); JsonNode patchedNode = patch.apply(objectMapper.valueToTree(existingEntity)); Entity updatedEntity = objectMapper.treeToValue(patchedNode, Entity.class); // 双重保险:强制保留不可变字段的原有值 updatedEntity.setId(existingEntity.getId()); updatedEntity.setCreationTime(existingEntity.getCreationTime()); repository.save(updatedEntity); return ResponseEntity.ok(updatedEntity); } }
方案2:用Spring AOP统一拦截验证
如果多个Controller都需要做同样的验证,可以用AOP实现全局拦截,避免重复代码:
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import com.github.fge.jsonpatch.JsonPatch; @Aspect @Component public class JsonPatchValidationAspect { // 拦截所有接收JsonPatch作为RequestBody的Controller方法 @Before("execution(* com.yourpackage.controller.*.*(.., @RequestBody com.github.fge.jsonpatch.JsonPatch, ..))") public void validatePatchRequest(JsonPatch patch) { JsonPatchValidator.validatePatch(patch); } }
方案3:自定义JsonPatch实现(进阶)
如果需要更灵活的控制,可以继承JsonPatch类,在应用补丁前自动触发验证:
import com.fasterxml.jackson.databind.JsonNode; import com.github.fge.jsonpatch.JsonPatch; import com.github.fge.jsonpatch.JsonPatchException; import com.github.fge.jsonpatch.JsonPatchOperation; import java.util.List; public class ValidatedJsonPatch extends JsonPatch { public ValidatedJsonPatch(List<JsonPatchOperation> operations) { super(operations); } @Override public JsonNode apply(JsonNode node) throws JsonPatchException { // 应用补丁前先验证 JsonPatchValidator.validatePatch(this); return super.apply(node); } }
然后在Controller里直接接收这个自定义类型:
@PatchMapping("/{id}") public ResponseEntity<Entity> updateEntity(@PathVariable Long id, @RequestBody ValidatedJsonPatch patch, EntityRepository repository) { // 此时已经完成验证,直接执行后续逻辑 // ... }
额外注意点
- 双重保险:即使验证通过,保存实体时也要手动覆盖不可变字段的原有值,防止特殊场景下绕过验证。
- 错误处理:可以自定义异常类(比如
ImmutableFieldModificationException),再用@ExceptionHandler统一返回标准化的错误响应。 - 嵌套字段:如果实体有嵌套对象,需要根据实际场景调整不可变字段的路径匹配规则。
内容的提问来源于stack exchange,提问作者shoaib1992
相关产品推荐
相关产品推荐

