在工作中,我们经常有这样的业务情况,实体间通过id实现数据业务上的关联,比如订单和用户,订单的创建人id、商品id等,在页面查询时我们需要将对应的id信息转换成对应的中文描述,比如用户中文名称,商品中文名称等。如果是单条数据的展示还好,但是设计到列表查询,如何高效、优雅地实现这个效果呢?
现在接口返回的数据基本都是JSON格式,比如spring中使用了jackson,在controller层对结果进行json序列化,而我们要做的就是在序列化的过程中,实现id的转换。
使用方式
定义实体转换接口
由于需要对订单实体中的创建人id进行转换:
public interface UserConvert {
String USER_CACHE = "USER_CACHE";
String userId();
default ConvertItem getUserConvert(){
if( userId() == null ){
return null;
}
return new ConvertItem(userId(), USER_CACHE);
}
}
定义接口转换适配器
基于上面UserConvert的处理,基于缓存实现,同时支持一个实体中多个,比如商品名称、商品分类等!
public class UserConvertProvider extends CacheItemConvertAdapter {
private static String name = UserConvert.USER_CACHE;
public UserConvertProvider() {
super(name, User.class);
}
@Override
public boolean support(ConvertItem convertItem) {
return convertItem != null && convertItem.getName().equals(name);
}
@Override
public String convert(ConvertItem convertItem) {
if( convertItem == null ){
return null;
}
User user = (User) fromCache(convertItem.getId());
return user != null ? user.getCaption() : null;
}
}
需要转换的数据缓存
该实现依赖缓存,需要优先对需要转换的数据进行缓存,因此示例中添加了缓存示例:
public void init(){
Cache cache = cacheManager.getCache(UserConvert.USER_CACHE);
if( cache != null ){
cache.put("u1", new User("u1","Tom"));
}
}
实体定义
实体中需要通过实现接口UserConvert,这样对多个数据项转换时可以继续扩展。
public class Order implements UserConvert {
private String id;
private String name;
private LocalDateTime createTime = LocalDateTime.now();
/**
* 创建用户
*/
private String creator;
@Override
public String userId() {
return creator;
}
}
实现效果
可以看到,在输出json中,多了一列userConvert,也就是接口中定义的get*方法:
{
"id": "1",
"name": "测试订单",
"createTime": "2024-05-08T21:55:51.5747507",
"creator": "u1",
"userConvert": "Tom"
}
实现原理
上面说的,主要实现基于缓存,在web查询结果进行json序列化时,依赖于jackson的扩展,对输出结果匹配的类型进行转换。
@EnableCaching
@Configuration
public class JacksonCustomConfiguration{
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(){
return jacksonObjectMapperBuilder -> configureMapperBuilder(jacksonObjectMapperBuilder);
}
private void configureMapperBuilder(Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder) {
jackson2ObjectMapperBuilder.serializers(convertSerializer());
}
@Bean
public ItemConvertSerializer convertSerializer(){
return new ItemConvertSerializer(ConvertItem.class);
}
}
- 在配置文件中基于Jackson2ObjectMapperBuilderCustomizer对jackson进行扩展。
- 定义ItemConvertSerializer对ConvertItem类型的属性进行处理,该类主要继承于StdSerializer。
- 在ItemConvertSerializer中基于ConvertItem的name属性来匹配对应的缓存并进行转换。
- 注意开启spring缓存*@EnableCaching*。
- 最后基于spring特性,定义*/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports*来实现自动注入配置。
- ConvertItem示例:
@Getter
@Setter
public class ConvertItem {
private String id;
private String text;
private String name;
public ConvertItem() {
}
public ConvertItem(String id, String name) {
this.id = id;
this.name = name;
}
}
- ItemConvertAdapter扩展适配器,主要于ConvertItem搭配扩展。
public interface ItemConvertAdapter {
/**
* @param convertItem
* @return
*/
boolean support(ConvertItem convertItem);
/**
*
* @param convertItem
* @return
*/
String convert(ConvertItem convertItem);
}
- ItemConvertSerializer示例:
public class ItemConvertSerializer extends StdSerializer<ConvertItem> implements ApplicationContextAware {
private List<ItemConvertAdapter> itemConvertAdapters;
public ItemConvertSerializer(Class<ConvertItem> t) {
super(t);
}
@Override
public void serialize(ConvertItem value, JsonGenerator gen, SerializerProvider provider) throws IOException {
String text = "";
if(!CollectionUtils.isEmpty(itemConvertAdapters)){
for (ItemConvertAdapter itemConvertAdapter : itemConvertAdapters) {
if( itemConvertAdapter.support(value) ){
text = itemConvertAdapter.convert(value);
break;
}
}
}
gen.writeString(text);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, ItemConvertAdapter> itemConvertAdapterMap
= BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, ItemConvertAdapter.class, true, false);
if( !itemConvertAdapterMap.isEmpty() ){
itemConvertAdapters = new ArrayList<>(itemConvertAdapterMap.values());
itemConvertAdapters.sort(OrderComparator.INSTANCE);
}
}
}
优缺点
- 使用了jackson序列化的扩展,如果使用其他序列化工具,需要单独支持。
- 依赖于数据缓存,一般针对通用数据才有数据转换的需要,比如用户、部门数据等,一般这些数据更适合缓存。