JSON Converter
Jimmer实体并非POJO,但可以靠org.babyish.jimmer.jackson.ImmutableModule
支持Jackson序列化。
如果使用Jimmer的Spring Boot Starter,该ImmutableModule
会被自动注册,无需用户关注。
然而有的时候,我们需要对某些属性的Jackson序列化行为进行定制,我们以两种情况来举例。
-
雪花ID
-
字段敏感
1. 雪花ID
雪花id会导致数值很大的int64类型,遗憾的是,JavaScript的数字类型无法表达其他语言中的int64大数字。因此我们不得不让这类id属性被序列化为字符串。
用法
- Java
- Kotlin
@Entity
@public interface Book {
@Id
@GeneratedValue(generatorType = SnowflakeIdGenerator.class)
@JsonConverter(LongToStringConverter.class)
long id();
String name();
...省略其他属性...
}
@Entity
@public interface Book {
@Id
@GeneratedValue(generatorType = SnowflakeIdGenerator.class)
@JsonConverter(LongToStringConverter.class)
long id();
String name();
...省略其他属性...
}
其中SnowflakeIdGenerator
是用户提供的分布式雪花id生成器实现,不是我们讨论的重点。
这里,我们讨论被标记的那行代码:
-
@org.babyfish.jimmer.jackson.JsonConverter
表示自定义Book.id
属性的Jackson序列化/反序列化行为。 -
org.babyfish.jimmer.jackson.LongToStringConverter
表示将long类型的值按照String来序列化/反序列化。LongToStringConverter
是Jimmer内置的Converter,这里展示一下其源代码,我们就可以快熟了解如何自定义Converter
。package org.babyfish.jimmer.jackson;
public class LongToStringConverter implements Converter<Long, String> {
// 如何序列化
@Override
public String output(Long value) {
return Long.toString(value);
}
// 如何反序列化
@Override
public Long input(String jsonValue) {
return Long.parseLong(jsonValue);
}
}
这里,我们绕过ORM,通过模拟数据展示效果。
- Java
- Kotlin
Book book = Immutables.createBook(draft -> {
draft.setId(1546434604146774987L);
draft.setName("SQL in Action");
});
System.out.println(book);
val book = Book {
id = 1546434604146774987L
name = "SQL in Action"
}
println(book)
由于Jimmer实体的toString就是Jackson序列化,所以,我们可以直接打印并观察Jackson序列化的结果:
{
"id": "1546434604146774987",
"name": "SQL in Action"
}
可见,序列化后的id是字符串,而非数字,这样,就避开了JavaScript客户端无法解析大数字的问题。
自动应用于IdView属性
@IdView属性是进阶映射才会讲解的内容,读者也可以先跳过本节,以后再回来看。
- Java
- Kotlin
@Entity
public interface Author {
@ManyToMany(mappedBy = "authors")
List<Book> books();
@IdView("authors")
List<Long> bookIds();
...省略其他属性...
}
@Entity
interface Author {
@ManyToMany(mappedBy = "authors")
val books: List<Book>
@IdView("authors")
val bookIds: List<Long>
...省略其他属性...
}
这里,bookIds
属性并非普通的List<Long>
集合,而是所有关联的Book
对象的id的集合。
由于Book.id
属性已经被注解@JsonConverter
修饰,所以,无需为这里的bookIds
属性指定任何注解,该属性也有这种能力。
同样,绕过ORM,通过模拟数据展示效果。
- Java
- Kotlin
Author author = Immutables.createAuthor(draft -> {
draft.setBookIds(
Arrays.asList(
1546434604146774987L,
1546434604146774988L
)
);
});
System.out.println(book);
val book = Book {
bookIds = listOf(
1546434604146774987L,
1546434604146774988L
)
}
println(book)
由于Jimmer实体的toString就是Jackson序列化,所以,我们可以直接打印并观察Jackson序列化的结果:
{
"bookIds": [
"1546434604146774987",
"1546434604146774988"
]
}
可见,序列化后的bookIds
属性的所有元素都是字符串,而非数字,这样,就避开了JavaScript客户端无法解析大数字的问题。
自动应用于DTO
DTO是进阶映射才会讲解的内容,读者也可以先跳过本节,以后再回来看。
在src/main/dto
目录下新建任何一个扩展名为dto
的文件,编辑其代码如下
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
BookView {
id
name
}
编译项目,生成的Java/Kotlin类型如下
- Java
- Kotlin
@GeneratedBy(file = "<yourproject>/src/main/dto/Book.dto")
public class BookView {
private String id;
private String name;
...省略其他属性...
}
@GeneratedBy(file = "<yourproject>/src/main/dto/Book.dto")
class BookView(
val id: String,
val name: String
) {
...省略其他属性...
}
可见,编译后自动生成的DTO类型中id属性的类型为String,而非long。
最后,我们验证一下将实体转化为DTO对象,如下
- Java
- Kotlin
Book book = Immutables.createBook(draft -> {
draft.setId(1546434604146774987L);
draft.setName("SQL in Action");
});
System.out.println(new BookView(book));
val book = Book {
id = 1546434604146774987L
name = "SQL in Action"
}
println(BookView(book))
输出如下
BookView(
id = "1546434604146774987",
name = "SQL in Action"
)
2. 脱敏
用法
假如User
实体具备phone
属性,表示用户的电话号码,我们现在为该属性进行脱敏处理。
比如,手机号显示为135****6792
。
脱敏并不是一个单纯页面渲染问题,必须保证HTTP请求本身不返回敏感的数据
对于这个需求,没有内置的Converter可用,我们必须自定义Converter,编写UserPhoneConverter
,如下:
- Java
- Kotlin
public class UserPhoneConverter implements Converter<String, String> {
@Override
public String output(String value) {
return value.substring(0, 3) + "****" + value.substring(7);
}
}
class UserPhoneConverter : Converter<String, String> {
override fun output(value: String): String =
"${value.substring(0,3)}****${value.substring(7)}"
}
然后,简单修改User
实体即可
- Java
- Kotlin
@Entity
public interface User {
@JsonConverter(UserPhoneConverter.class)
String phone();
}
@Entity
interface User {
@JsonConverter(UserPhoneConverter.class)
val phone: String
}
兼顾输入
在上面的例子中,UserPhoneConverter
只覆盖了output
方法,没有覆盖input
方法。
由于input
方法的默认实现是抛出UnsupportedOperationException
异常,因此,无法支持反序列化。
然而,实际项目中,可以会基于实体对象生成Input DTO
以完成数据录入,而且这种情况下,客户端应该录入精确的手机号,因此,我们可以如此覆盖input
方法。
- Java
- Kotlin
@Override
public String input(String value) {
return value;
}
override fun input(value: String): String =
value