当前位置: 首页 > 工具软件 > JSON Patch > 使用案例 >

SpringBoot+HttpPatch+JsonPatch实现Json文件的更新

凤凡
2023-12-01

引言

出于对Mysql数据库减负的想法,我们决定将一些经常读的数据放在自己的json文件服务器中,当然也可以选择redis,但是可能会有较多数据不会读到但必须要存的情况比较耗内存。这里对于json文件的更新就成了一种问题,这里我们介绍下我们使用的SpringBoot+HttpPatch+JsonPatch。

HttpPatch

Http的【RFC2616】原本定义用于上传数据的方法只有POST和PUT,但是考虑到两者的不足,就增加了PATCH方法。

用PATCH方法,默认是以x-www-form-urlencoded的contentType来发送信息,并且信息内容是放在request的body里。

PUT方法和PATCH方法的提交目的地都是直接指向资源,而POST方法提交的数据的目的地是一个行为处理器。

PUT方法用来替换资源,而patch方法用来更新部分资源,然而PATCH和POST都是非幂等的,POST请求服务器执行一个动作,多次请求会多次执行。PATCH提供的实体则需要根据程序或其它协议的定义,解析后在服务器上执行,以此来修改服务器上的数据。也就是说,PATCH请求是会执行某个程序的,如果重复提交,程序可能执行多次,对服务器上的资源就可能造成额外的影响POST方法和PATCH方法它们的实体部分都是结构化的数据,所以PAtch也是非幂等的。POST方法的实体结构一般是 multipart/form-data或 application/x-www-form-urlencoded而PATCH方法的实体结构则随其它规范定义。这和PUT方法的无结构实体相比就是最大的区别。

JsonPatch

JSON Patch是一种用于描述对JSON文档所做的更改的格式(JSON Patch本身也是JSON结构)。当只更改了一部分时,可用于避免发送整个文档。可以与HTTP PATCH方法结合使用时,它允许以符合标准的方式对HTTP API进行部分更新。

JSON Patch是在IETF的RFC 6902中指定的。如果了解过linux上的diff、patch,就非常容易理解JSON Patch了,前者是针对普通文本文件的,后者是指针对JSON结构。(前者更通用)

实战

1.首先来看看需要导入哪些依赖,为了方便直接贴出全部依赖直接复制即可

     <properties>
        <!-- Source encoding -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <!-- Dependency versions -->
        <lombok.version>1.18.18</lombok.version>
        <guava.version>27.1-jre</guava.version>
        <mapstruct.version>1.3.0.Final</mapstruct.version>
        <jackson.version>2.9.8</jackson.version>
        <javax-json.version>1.1.4</javax-json.version>
        <spring-boot.version>2.1.5.RELEASE</spring-boot.version>
        <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- Spring Boot Starter Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Boot Developer Tools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Spring Boot Starter Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
        <!-- MapStruct -->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${mapstruct.version}</version>
        </dependency>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>
        <!-- JSR-353 (JSON Processing): API -->
        <dependency>
            <groupId>javax.json</groupId>
            <artifactId>javax.json-api</artifactId>
            <version>${javax-json.version}</version>
        </dependency>
        <!-- JSR-353 (JSON Processing): Johnzon implementation -->
        <dependency>
            <groupId>org.apache.johnzon</groupId>
            <artifactId>johnzon-core</artifactId>
        </dependency>
        <!-- Jackson module for the JSR-353 (JSON Processing) -->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr353</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <!-- Apache HTTP Components -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Jayway JsonPath Assert -->
        <dependency>
            <groupId>com.jayway.jsonpath</groupId>
            <artifactId>json-path-assert</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Spring Boot -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <!-- Maven Compiler -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven-compiler-plugin.version}</version>
                <configuration>
                    <release>11</release>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${lombok.version}</version>
                        </path>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${mapstruct.version}</version>
                        </path>
                    </annotationProcessorPaths>
                    <compilerArgs>
                        <arg>
                            -Amapstruct.suppressGeneratorTimestamp=true
                        </arg>
                        <arg>
                            -Amapstruct.suppressGeneratorVersionInfoComment=true
                        </arg>
                        <arg>
                            -Amapstruct.defaultComponentModel=spring
                        </arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>

2.准备一个实体类和一个请求信息类

实体类

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BookInput {
    private Long id;
    private String bookName;
    private String url;
}

请求信息类

public final class PatchMediaType {

    public static final String APPLICATION_JSON_PATCH_VALUE = "application/json-patch+json";

    public static final String APPLICATION_MERGE_PATCH_VALUE = "application/merge-patch+json";

    public static final MediaType APPLICATION_JSON_PATCH;

    public static final MediaType APPLICATION_MERGE_PATCH;

    static {
        APPLICATION_JSON_PATCH = MediaType.valueOf(APPLICATION_JSON_PATCH_VALUE);
        APPLICATION_MERGE_PATCH = MediaType.valueOf(APPLICATION_MERGE_PATCH_VALUE);
    }

    private PatchMediaType() {
        throw new AssertionError("No instances of PatchMediaType for you!");
    }
}

3.写一个配置类用来对json进行配置

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
                .setDefaultPropertyInclusion(Include.NON_NULL)
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .findAndRegisterModules();
    }
}

注:如果没有这个配置类会报错

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `javax.json.JsonStructure` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

4.这些准备好后重点来了

@Component
@RequiredArgsConstructor
public class PatchHelper {

    private final ObjectMapper mapper;

    private final Validator validator;

    /**
     * Performs a JSON Patch operation.
     *
     * @param patch      JSON Patch document
     * @param targetBean object that will be patched
     * @param beanClass  class of the object the will be patched
     * @param <T>
     * @return patched object
     */
    public <T> T patch(JsonPatch patch, T targetBean, Class<T> beanClass) {
        JsonStructure target = mapper.convertValue(targetBean, JsonStructure.class);
        JsonValue patched = applyPatch(patch, target);
        return convertAndValidate(patched, beanClass);
    }

    /**
     * Performs a JSON Merge Patch operation
     *
     * @param mergePatch JSON Merge Patch document
     * @param targetBean object that will be patched
     * @param beanClass  class of the object the will be patched
     * @param <T>
     * @return patched object
     */
    public <T> T mergePatch(JsonMergePatch mergePatch, T targetBean, Class<T> beanClass) {
        JsonValue target = mapper.convertValue(targetBean, JsonValue.class);
        JsonValue patched = applyMergePatch(mergePatch, target);
        return convertAndValidate(patched, beanClass);
    }

    private JsonValue applyPatch(JsonPatch patch, JsonStructure target) {
        try {
            return patch.apply(target);
        } catch (Exception e) {
            throw new UnprocessableEntityException(e);
        }
    }

    private JsonValue applyMergePatch(JsonMergePatch mergePatch, JsonValue target) {
        try {
            return mergePatch.apply(target);
        } catch (Exception e) {
            throw new UnprocessableEntityException(e);
        }
    }

    private <T> T convertAndValidate(JsonValue jsonValue, Class<T> beanClass) {
        T bean = mapper.convertValue(jsonValue, beanClass);
        validate(bean);
        return bean;
    }

    private <T> void validate(T bean) {
        Set<ConstraintViolation<T>> violations = validator.validate(bean);
        if (!violations.isEmpty()) {
            throw new ConstraintViolationException(violations);
        }
    }
}

这是封装好的JsonPatch方法直接调用就可以用了。另外两个异常处理类要放在下面

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {

}
@ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY)
public class UnprocessableEntityException extends RuntimeException {

    public UnprocessableEntityException(Throwable cause) {
        super(cause);
    }
    
}

5.测试

@RestController
@RequestMapping("/patch")
public class PatchController {

    private final PatchHelper patchHelper;
    public PatchController(PatchHelper patchHelper) {
        this.patchHelper = patchHelper;
    }

    @PatchMapping(consumes = PatchMediaType.APPLICATION_JSON_PATCH_VALUE)
    public void test() {
        /**
         * [{"op":"replace","path":"/id","value":3},
         * {"op":"replace","path":"/bookName","value":"HAPPYBEAR"},
         * {"op":"remove","path":"/url"},
         * {"op":"add","path":"/url","value":"https://baike.baidu.com"}]
         */
        JsonPatch patch = Json.createPatchBuilder()
                //将id替换为3
                .replace("/id",3)
                //将bookName替换为HAPPYBEAR
                .replace("/bookName", "HAPPYBEAR")
                //移除路径字段
                .remove("/url")
                //添加路径字段并赋值https://baike.baidu.com
                .add("/url", "https://baike.baidu.com")
                .build();
        //拿到的json转换为对象
        BookInput bookInput =new BookInput(2L,"大话设计模式","https://baike.baidu.com/item/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/85262?fr=aladdin");
        /**
         * 开始处理
         * patch:补丁  对源json需要做的操作
         * bookInput:源json转换后的对象
         * BookInput.class:处理过后的数据转换成什么对象
         */
        BookInput input = patchHelper.patch(patch, bookInput, BookInput.class);
        System.out.println(input);
    }
}

6.运行结果

BookInput(id=3, bookName=HAPPYBEAR, url=https://baike.baidu.com)

获取或上传json文件这边就不阐述了,这边只提供SpringBoot+HttpPatch+JsonPatch的大概思路,希望对大家有帮助!!!

 类似资料: