前后端字典类枚举使用方案

在web开发中,常常涉及到一些字典类的信息编号存储和展示。由于规则比较单一,常常会抽象和封装为一个公共的方法和组件。为了保证前后端的定义统一,且考虑到易于维护和扩展。一般都采用后端定义和维护枚举常量,暴露接口给前端,前端可以进行一定封装。后端也会采用一些方式保证容易使用和增改字典类数据。

字段类枚举的使用

字典类枚举

字典类的枚举,通常是编码和名称,比如性别:

0 -> 女
1 -> 男

或者:

F -> 女
M -> 男

在前端展示的时候会转为名称,而存储到数据库通常存储为编码;

前端使用

前端会在展示和操作中使用常量数组和对象来维护。

// dict list json
{
  "level":[
    {"code": "high", "name": "高"},
    {"code": "middle", "name": "中"},
    {"code": "low", "name": "低"}
  ],
  "sex": [
    {"code": "0", "name": "女"},
    {"code": "1", "name": "男"}
  ]
  //...
}

在展示时,会从code转化为name,在一些编辑操作时,需要转化为下拉列表select来进行选择。

<select name="level">
	<option value="high">option>
  <option value="middle">option>
  <option value="low">option>
select>

在使用vue2.x的时候,通定义全局变量和初始接口来加载系统的所有字典类枚举。

后端使用

后端开发目前一般习惯使用枚举类enum来维护和使用

@AllArgsConstructor
@Getter
public enum LevelEnum {

    HIGH("high", "高"),
    MIDDLE("middle", "中"),
    LOW("low", "低");

    private String code;
    private String name;
}

前端使用后端提供的接口来统一使用

通常前后端会约定接口返回这些字典类枚举:

返回指定类型的枚举
GET /getDict?type=level

返回所有枚举
GET /getAllDict

格式同前述的dict list json

这样可以保证前后端对枚举是统一的,便于维护和管理。

后端接口通常是这么定义的:

@RestController
public class DictController {

    @GetMapping("/getAllDict")
    public Map<String, List<CodeNameDTO>> getAllDict() {
        return DictUtils.getAllDict();
    }

    @GetMapping("/getDict")
    public List<CodeNameDTO> getAllDict(@RequestParam String type) {
        return DictUtils.getAllDict().get(type);
    }
}

实体类CodeNameDTO的定义:

@Getter
@Setter
public class CodeNameDTO {
    private String code;
    private String name;
}

后端如何方便扩展

使用接口来统一这样枚举类常量

最简单直接的方式就是维护这一类枚举的转化为常量的结构;

为了避免使用反射方式转化枚举到实体对象,定义一个接口ICodeName

public interface ICodeName {
    String getCode();
    String getName();
}

所有的字典类枚举实现改接口,这样获取编码和名称的方法就可以统一了;

@AllArgsConstructor
@Getter
// 加入了接口来统一获取编码和名称的方法
public enum LevelEnum implements ICodeName {
    // 代码省略,同前
}

这样的好处,可以避免使用反射来转化实体对象:

try{
  Method getCodeMethod = clazz.getMethod("getCode");
  Method getNameMethod = clazz.getMethod("getName");
  CodeNameDTO codeNameDTO = new CodeNameDTO();
  codeNameDTO.setCode(getCodeMethod.invoke(enumObj));
  codeNameDTO.setName(getNameMethod.invoke(enumObj));
}catch(Exception e) {
  // 这里可能会有NoSuchMethodException
}

于是可以这样的编辑加载的逻辑:

public class DictUtils {
  	// 使用类常量
    private static final Map<String, List<CodeNameDTO>> dictCacheMap;
		// 在类初始化时加载,避免反复加载
    static {
        Map<String, List<CodeNameDTO>> map = new HashMap<>();
        map.put("level", dict(LevelEnum.values()));
        //map.put("sex", dict(SexEnum.values()));
        dictCacheMap = Collections.unmodifiableMap(map);

    }

		/**
		 * 获取所有字典类枚举
		 */
    public static Map<String, List<CodeNameDTO>> getAllDict() {
        return dictCacheMap;
    }

    /**
     * 字典类枚举接口转化为实体对象
     */
    private static List<CodeNameDTO> dict(ICodeName[] codeNames) {
        List<CodeNameDTO> list = Arrays.stream(codeNames).map(i -> {
            CodeNameDTO codeNameDTO = new CodeNameDTO();
            codeNameDTO.setCode(i.getCode());
            codeNameDTO.setName(i.getName());
            return codeNameDTO;
        }).collect(Collectors.toList());
        // 这里使用unmodifiable*方法避免后续操作时,修改全局常量
        return Collections.unmodifiableList(list);
    }
}

使用注解类来标识枚举的分类

上述的方法每次在新增枚举时,都需要修改工具类,比较麻烦;

定义一个注解,在运行期可以拿到对枚举的分类标识:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DictLabel {
    String value() default "";
}

后端定义的枚举定义了编码和对应的名称:

@AllArgsConstructor
@Getter
// 加入了注解来标识分类
@DictLabel("level")
public enum LevelEnum implements ICodeName {
    // 代码省略,同前
}

这样一来可以结合反射方法,DictUtils就不用逐个指定和编写枚举了,可以修改为:

public class DictUtils {

    private static final Map<String, List<CodeNameDTO>> dictCacheMap;

    static {
      	// 加载指定包路径下的
        dictCacheMap = loadDict("com.xx.xxx");
    }

    public static void main(String[] args) throws IOException {
        System.out.println(getAllDict());
    }

    /**
     * 获取所有字典类枚举
     */
    public static Map<String, List<CodeNameDTO>> getAllDict() {
        return dictCacheMap;
    }

    /**
     * 从指定包路径加载字典类枚举
     */
    private static Map<String, List<CodeNameDTO>> loadDict(String packageName) {
        Map<String, List<CodeNameDTO>> map = new HashMap<>();

        try {
            List<String> classNames = scanClasses(packageName);
            for(String className : classNames) {
                Class clazz = Class.forName(className);
                // 如果是枚举类,且是定义的字典类枚举接口ICodeName
                if(clazz.isEnum() && ICodeName.class.isAssignableFrom(clazz)) {
                    DictLabel dictLabel = (DictLabel) clazz.getAnnotation(DictLabel.class);
                    // 默认使用类名称作为字典类型名称
                    String label = clazz.getSimpleName();
                    // 如果有DictLabel注解,则用注解中的分类名称
                    if(dictLabel != null && !dictLabel.value().isEmpty()) {
                        label = dictLabel.value();
                    }
                    List<CodeNameDTO> list = new ArrayList<>();
                    Object[] values = clazz.getEnumConstants();
                    CodeNameDTO codeNameDTO = null;
                    for(Object value : values) {
                        ICodeName en = (ICodeName) value;
                        codeNameDTO = new CodeNameDTO();
                        codeNameDTO.setCode(en.getCode());
                        codeNameDTO.setName(en.getName());
                        list.add(codeNameDTO);
                    }
                    map.put(label, Collections.unmodifiableList(list));
                }
            }

        }catch (Exception e) {
            e.printStackTrace();
        }
        return Collections.unmodifiableMap(map);
    }
    

    /**
     * 扫描包下所有类名称
     * @param packageName 包名称
     * @return 类名称列表
     * @throws IOException
     */
    private static List<String> scanClasses(String packageName) throws IOException {
        List<String> list = new ArrayList<>();
        String packageDirName = packageName.replace('.', '/');
        Enumeration<URL> urlEnumeration = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
        while (urlEnumeration.hasMoreElements()) {
            URL url = urlEnumeration.nextElement();
            String protocol = url.getProtocol();
            if("jar".equals(protocol)){
                JarFile jar = ((JarURLConnection) url.openConnection()).getJarFile();
                Enumeration<JarEntry> entries = jar.entries();
                while (entries.hasMoreElements()) {
                    JarEntry entry = entries.nextElement();
                    String jarEntryName = entry.getName();
                    if(jarEntryName.startsWith(packageDirName) && jarEntryName.endsWith(".class")) {
                        String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replace("/", ".");
                        list.add(className);
                    }
                }

            }else if("file".equals(protocol)){
                // 递归查找所有路径下的类
                findClass(packageName, new File(url.getFile()), list);
            }
        }
        return list;
    }

    /**
     * 找到路径下的所有类
     * @param packageName 当前路径对应的包名
     * @param file 当前路径的File对象
     * @param classNames 输出的类名称列表
     */
    private static void findClass(String packageName, File file, List<String> classNames) {
        String name = file.getName();
        if(file.isDirectory()) {
            File[] subFiles = file.listFiles();
            for(File subFile : subFiles) {
                String subPackageName = packageName + (subFile.isDirectory() ? "." + subFile.getName() : "");
                findClass(subPackageName, subFile, classNames);
            }
        }else if(name.endsWith(".class")){
            String className = packageName + "." + name.substring(0, name.length() - 6);
            classNames.add(className);
        }
    }

}

你可能感兴趣的:(java)