You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:10:57