在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);
}
}
}