背景
B/S架构项目,前后端分离开发,往往有很多枚举需要交代给前端做成下拉或者渲染
前端写死的弊端很多,比如失误造成的键、值错误,修改时要前后端都改等
为每个枚举写个接口,会好很多,但重复工作太多了,还是很蠢。
思路
前端做下拉或者渲染所需的源数据,可以全部调用同一个接口,形如:
@GetMapping("enum/{className}")
后端写好枚举后,把枚举的类名告诉前端,就可以了
实现
把枚举转为前端可用的源数据
一段很简单的反射代码,就可以把枚举变成List>这样的对象了:
List> getEnumByClassName(String className) {
List> list = new ArrayList<>();
// 1.得到枚举类对象
Class clz = optionalEnumMap.get(className);
Object[] objects = clz.getEnumConstants();
if (objects != null && objects.length != 0) {
Method[] methods = clz.getMethods();
for (Object obj : objects) {
Map map = new HashMap<>();
if (methods.length != 0) {
for (Method method : methods) {
String name = method.getName();
if ("getClass".equals(name) || "getDeclaringClass".equals(name)) {
continue;
}
if (name.startsWith("get")) {
try {
map.put(name.substring(3, 4).toLowerCase() + name.substring(4), method.invoke(obj));
} catch (IllegalAccessException | InvocationTargetException e) {
log.error("", e);
}
}
}
}
list.add(map);
}
}
return list;
}
然后,前端就可以看到这种风格报文了,大概会给你一个满意的眼神:
[
{
"template": "= %s",
"code": "eq",
"description": "等于"
},
{
"template": "> %s",
"code": "gt",
"description": "大于"
},
{
"template": "< %s",
"code": "lt",
"description": "小于"
},
{
"template": "BETWEEN %s AND %s",
"code": "between",
"description": "区间匹配"
}
]
那么,还有一个问题就是,根据类名怎么获取到Class clz咧?
有一个方法是,直接约定参数是全类名,但是这样显然很不优雅
我的示例代码中有个莫名其妙的Map> optionalEnumMap对吧?很方便对吧?
它是这样来的:它是这样来的:
package com.ygg.it.web.config;
import com.ygg.it.common.annotation.OptionalEnumScan;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* @author chenlu
*/
@Configuration
@Slf4j
@OptionalEnumScan({"com.ygg.it.common.enums", "com.ygg.it.form.enums", "com.ygg.it.flow.enums"})
public class EnumScanConfig {
@Bean
public Map> optionalEnumMap() {
HashMap> map = new HashMap<>(20);
//从注解中获取要扫描的包
OptionalEnumScan annotation = EnumScanConfig.class.getAnnotation(OptionalEnumScan.class);
String[] strings = annotation.value();
for (String pkgName : strings) {
Enumeration urls;
try {
String pkgPath = pkgName.replace(".", "/");
urls = Thread.currentThread().getContextClassLoader().getResources(pkgPath);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
if (url != null) {
/*
* 不同的运行方式,可能需要用不同的方法去寻找class文件;
* 比如运行jar包时,由于归档文件内的路径不可以new File,故需用JarFile来寻找class文件。
*/
if ("file".equals(url.getProtocol())) {
File path = new File(url.getPath());
if (path.exists() && path.isDirectory()) {
File[] classFiles = path.listFiles(file -> file.getName().endsWith("class"));
if (classFiles != null && classFiles.length != 0) {
for (File classFile : classFiles) {
String className = classFile.getName().substring(0, classFile.getName().length() - 6);
mapping(map, pkgName, className);
}
}
}
} else if ("jar".equals(url.getProtocol())) {
JarFile jarFile = ((JarURLConnection) url.openConnection()).getJarFile();
Enumeration entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
String name = jarEntry.getName();
if (name.startsWith("/" + pkgPath) || name.startsWith(pkgPath)) {
if (name.endsWith(".class") && !jarEntry.isDirectory()) {
String className = name.substring(name.lastIndexOf("/") + 1, name.length() - 6);
mapping(map, pkgName, className);
}
}
}
}
}
}
} catch (IOException e) {
log.error("optionalEnumMap", e);
}
}
return map;
}
private void mapping(HashMap> map, String pkgName, String className) {
try {
Class> aClass = Class.forName(pkgName + "." + className);
if (aClass.getSuperclass() == Enum.class) {
Class enumClass = (Class) aClass;
map.put(className, enumClass);
}
} catch (ClassNotFoundException e) {
log.error("optionalEnumMap", e);
}
}
}
后话
枚举反射时,getClass、getDeclaringClass两个方法忽略掉比较好,传给前端没有意义。
包扫描是这一套路能够优雅运作的重点。
**“怎样获取包名下所有的类/所有的枚举”**这个问题,在本文中也有答案(上面去找)。
由于上面那段代码同时考虑到了本地调试和jar包运行两种场景,也许可以是个相对更有价值的参考吧【只是也许】
@OptionalEnumScan这个注解就只是为了方便填写要扫描的包
虽然很没必要,但还是贴出来吧(万一有用呢):
package com.ygg.it.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author chenlu
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface OptionalEnumScan {
String[] value() default {};
}