字典注解:用于在属性上添加注解,可自动查询字典数据

问题

  1. 很多业务数据中,存的都是字典代码,但是 API 查询返回时,需要将原始字典名称 Title 返回
  2. 关联查询其实很简单,仅仅是增加一个字典表,条件为筛选字典类型字典代码
  3. 但是存在很多业务数据都需要这么关联,有时一次查询,可能要关联很多次字典表,导致 SQL 臃肿

解决方案

  • 源码:登录 · 极狐GitLab
    • spring-boot-3 分支
  • 可以利用自定义序列化时,增加字段来解决此问题,示例:
    1. 查询用户数据时,返回的数据格式
      [
        {
          "id": 1,
          "username": "Alice",
          "sex": "2"
        }
      ]
      
    2. 我们希望,返回数据增加字段:sexTitle,并且无需在原始对象中增加属性(如果返回对象是 domain/entity,并且使用了 MyBatis Plus,增加字段还要使用 @TableField(exist = false) 排除属性,防止 MyBatis Plus 反射时,查询数据库异常:字段不存在),返回数据格式
      [
        {
          "id": 1,
          "username": "Alice",
          "sex": "2",
          "sexTitle": "女"
        }
      ]
      
    3. 其他字典类型同理,以下功能,可以实现返回任意字典 Title,仅需两行代码
      @DictAnnotation(type = "字典类型", fieldName = "字典代码对应的 title 字段名称")
      @JsonSerialize(using = DictJsonSerializer.class)
      

DictAnnotation 注解

package cn.com.xuxiaowei.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 字典注解:用于在属性上添加注解,可自动查询字典数据
 *
 * @author xuxiaowei
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DictAnnotation {

	/**
	 * 字典类型
	 */
	String type();

	/**
	 * 字典代码对应的 title 字段名称
	 */
	String fieldName();

}

User 接口返回数据的对象

package cn.com.xuxiaowei.domain;

import cn.com.xuxiaowei.annotation.DictAnnotation;
import cn.com.xuxiaowei.serializer.DictJsonSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;

import java.io.Serializable;

/**
 * @author xuxiaowei
 */
@Data
public class User implements Serializable {

	private static final long serialVersionUID = 1L;

	private Long id;

	private String username;

	/**
	 * @see DictAnnotation 字典注解
	 * @see JsonSerialize 自定义序列化,用于增加 {@link DictAnnotation#fieldName()} 字段、查询数据库的核心逻辑
	 */
	@DictAnnotation(type = "sex", fieldName = "sexTitle")
	@JsonSerialize(using = DictJsonSerializer.class)
	private String sex;

}

DictMapper 字典查询

package cn.com.xuxiaowei.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * @author xuxiaowei
 */
@Mapper
public interface DictMapper {

	/**
	 * 根据 字典类型、字典代码 查询 字典 Title
	 * @param type 字典类型
	 * @param code 字典代码
	 * @return 字典 Title
	 */
	String selectTitleByTypeAndCode(@Param("type") String type, @Param("code") String code);

}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.com.xuxiaowei.mapper.DictMapper">

    <resultMap id="UserResultMap" type="Dict">
        <result column="dict_type" property="dictType"/>
        <result column="dict_code" property="dictCode"/>
        <result column="dict_title" property="dictTitle"/>
    </resultMap>

    <select id="selectTitleByTypeAndCode" resultType="java.lang.String">
        select dict_title
        from dict
        where dict_type = #{type}
          and dict_code = #{code}
    </select>

</mapper>

DictJsonSerializer 自定义序列化,增加字典代码对应的 Title

package cn.com.xuxiaowei.serializer;

import cn.com.xuxiaowei.annotation.DictAnnotation;
import cn.com.xuxiaowei.mapper.DictMapper;
import cn.com.xuxiaowei.utils.SpringContextUtils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;

/**
 * 自定义序列化,用于增加 {@link DictAnnotation#fieldName()} 字段、查询数据库的核心逻辑
 *
 * @author xuxiaowei
 */
@Slf4j
public class DictJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {

	private final DictAnnotation dictAnnotation;

	public DictJsonSerializer() {
		this.dictAnnotation = null;
	}

	public DictJsonSerializer(DictAnnotation dictAnnotation) {
		this.dictAnnotation = dictAnnotation;
	}

	@Override
	public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
			throws IOException {

		// 字典代码:原始值,无需处理
		jsonGenerator.writeString(value);

		// 自定义字典注解存在 && 字典代码不为空
		if (dictAnnotation != null && value != null) {

			// 字典类型
			String type = dictAnnotation.type();
			// 字典代码对应的 title 字段名称
			String fieldName = dictAnnotation.fieldName();

			// TODO 增加 Redis,减少数据库压力

			// 获取查询数据库的 Bean
			DictMapper dictMapper = SpringContextUtils.getBean(DictMapper.class);
			// 查询数据库,获取 字典 title
			String title = dictMapper.selectTitleByTypeAndCode(type, value);

			// 序列化时,增加 字典 title 字段
			jsonGenerator.writeStringField(fieldName, title);
		}

	}

	@Override
	public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
			throws JsonMappingException {
		if (property == null) {
			// return this;
			return new DictJsonSerializer();
		}

		// 获取 @JsonSerialize 序列化字段上的其他注解
		DictAnnotation annotation = property.getAnnotation(DictAnnotation.class);

		return new DictJsonSerializer(annotation);
	}

}

SpringContextUtils 用于普通类获取 Bean

package cn.com.xuxiaowei.utils;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * 用于普通类获取 Bean
 *
 * @author xuxiaowei
 */
@Component
public class SpringContextUtils implements ApplicationContextAware {

	private static ApplicationContext applicationContext;

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		SpringContextUtils.applicationContext = applicationContext;
	}

	/**
	 * 通过名称获取 Bean
	 */
	public static Object getBean(String name) {
		return applicationContext.getBean(name);
	}

	/**
	 * 通过类型获取 Bean
	 */
	public static <T> T getBean(Class<T> clazz) {
		return applicationContext.getBean(clazz);
	}

	/**
	 * 通过名称和类型获取 Bean
	 */
	public static <T> T getBean(String name, Class<T> clazz) {
		return applicationContext.getBean(name, clazz);
	}

}
1 个赞

关于为何使用 SpringContextUtils 获取 BeanJsonSerializer 不是能直接注入 Bean 吗?

  1. 如果不实现 ContextualSerializer 接口,确实可以使用 Autowired 获取 Bean
    • 说明:在 JsonSerializer 中,IDEA 无法识别,但是运行正常
  2. 如果实现了 ContextualSerializer,需要使用静态方法获取 Bean,使用 Autowired 无法注入
    • 实现 ContextualSerializer 的目的,是为了获取除 JsonSerialize 以外的该属性的其他注解