出于对Mysql数据库减负的想法,我们决定将一些经常读的数据放在自己的json文件服务器中,当然也可以选择redis,但是可能会有较多数据不会读到但必须要存的情况比较耗内存。这里对于json文件的更新就成了一种问题,这里我们介绍下我们使用的SpringBoot+HttpPatch+JsonPatch。
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方法的无结构实体相比就是最大的区别。
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的大概思路,希望对大家有帮助!!!