通过调用公共接口导出列表excel
这篇文章不讲述如何使用easypoi导出excel,主要讲述的是如何通过公共接口调用其他接口拿数据。
现在系统有个需求是 对所有列表 都增加一个导出excel功能,为了不对现有的业务带来倾入性 以及 对导出方法方便管理。设计为一个公共接口,前端列表 统一调用该接口,传参 有 要导出的列,要导出的数据(导出当前页数据,导出选中的数据,全部数据则传入数据接口的URL),导出excel集成的是easypoi,这个设置好表头List modelList = new ArrayList<>()和对应的数据List<Map<String, Object>> dataList = new ArrayList<>(),再调用ExcelExportUtil.exportExcel(exportParams, entities, list)就好了。codeSet 代码集是用于这个系统数据转换的。
//表头
List<ExcelExportEntity> modelList = new ArrayList<>();
//数据行
List<Map<String, Object>> dataList = new ArrayList<>();
//代码集
Map<String,Map<String,String>> codeSet = new HashMap<>();
//设置表头
setTableTitle( tableDefinition, modelList, codeSet, requestWrapper, response);
//设置对应数据
setTableData(rowList, tableDefinition, dataList, codeSet, requestWrapper, response);
log.debug("=====>组装数据完成:"+(System.currentTimeMillis()-start));
//组装下载excel文件
ExcelUtils.exportExcel(modelList,dataList,"导出列表.xls",response);
//testExcel(tableDefinition,rowList,response);
log.debug("=====>导出全部数据完成:"+(System.currentTimeMillis()-start));
以为很简单,但是遇到不少坑啊,现在就将遇到的问题做一个记录。
主要问题是导出所有数据,如何通过前台传入的url调用指定的接口拿数据。我是通过在后台维护了一套url和方法对象的映射关系。通过下面的方法:
@Slf4j
@Configuration
public class ActionManager {
public static Map<String, MethodEntity> actionMap = new HashMap();
/**
* 扫描包
*/
@PostConstruct
private void scan() {
doAction();
}
public void doAction() {
// 包名且不可忘记,不然扫描全部项目包,包括引用的jar,填写所在的包名称!!!!
Reflections reflections = new Reflections("cn.***.***.app");
// 获取带Service注解的类
Set<Class<?>> typesAnnotatedWith = reflections.getTypesAnnotatedWith(RestController.class);
//遍历restcontroller类中的url映射方法方法
searchUrlMethod(typesAnnotatedWith);
Set<Class<?>> typesAnnotatedWith2 = reflections.getTypesAnnotatedWith(Controller.class);
//遍历controller类中的url映射方法方法
searchUrlMethod(typesAnnotatedWith2);
}
private void searchUrlMethod(Set<Class<?>> typesAnnotatedWith) {
for (Class clazz : typesAnnotatedWith) {
String[] clazzReq = null;
if(clazz.isAnnotationPresent(RequestMapping.class)){
clazzReq = ((RequestMapping) clazz.getAnnotation(RequestMapping.class)).value();
}
String clazzUrl = "";
if(clazzReq!=null&&clazzReq.length>0){
clazzUrl = clazzReq[0];
}
Map<String, MethodEntity> methodMap = new HashMap<>();
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
String[] methUrl = null;
if (method.isAnnotationPresent(RequestMapping.class)) {
methUrl = ((RequestMapping) method.getAnnotation(RequestMapping.class)).value();
}else if(method.isAnnotationPresent(PostMapping.class)){
methUrl = ((PostMapping) method.getAnnotation(PostMapping.class)).value();
}else if(method.isAnnotationPresent(GetMapping.class)){
methUrl = ((GetMapping) method.getAnnotation(GetMapping.class)).value();
}else{
continue;
}
if (methUrl.length > 0) {
try {
//执行方法记录
MethodEntity methodEntity = MethodEntity.builder().annotation(methUrl[0]).classpath(clazz.getName()).classname(clazz.getSimpleName()).methodname(method.getName()).build();
actionMap.put(clazzUrl+methUrl[0], methodEntity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
}
这个方法中 @PostConstruct 表示初始化bean时执行一次注解的方法,扫描指定的包 cn.*.*.app ,找到包含 @RestController 和 @Controller 注解的类,然后遍历方法,将方法的信息封装到 MethodEntity 中,
@Data
@Builder
public class MethodEntity {
/**
* 类名称
*/
private String classname;
/**
* 类路径
*/
private String classpath;
/**
* 方法名称
*/
private String methodname;
/**
* 注解写的value
*/
private String annotation;
}
最后将拼接的 url 和封装好的 MethodEntity 放入actionMap 中。
然后就是通过前台传入 url 调用对应的接口了,直接上代码
@Slf4j
public class CallUrlUtil {
//前台没有传入类似json参数
public static Object getResultByAction(HttpServletRequest request, HttpServletResponse response) {
return getResultByAction(request,response,null);
}
//前台有传入类似json参数 通过JSONObject[]接收
public static Object getResultByAction(HttpServletRequest request, HttpServletResponse response, JSONObject[] requestbody) {
String actionName = RequestUtil.getString(request, "actionName");
if (StringUtils.isBlank(actionName)) {
actionName = (String) request.getAttribute("actionName");
}
if (StringUtils.isBlank(actionName)) {
throw new BusinessException("actionName is null!", BaseStatusCode.SYSTEM_ERROR);
}
String beanName = "";
String methodName = "";
Class<?> clazz = null;
try {
//通过指定url找到对应的方法
MethodEntity methodEntity = ActionManager.actionMap.getOrDefault(actionName, null);
if (methodEntity == null) {
return null;
}
//获取方法所在类的bean名称
beanName = LowerUpperCaseFirstLetter.lowerCaseFirstLetter(methodEntity.getClassname());
//方法名称
methodName = methodEntity.getMethodname();
//方法所在的类的字节码对象
//clazz = SpringContextUtil.getBean(beanName).getClass(); //此方法获取的字节码是代理过的 没含有编译保留的信息
clazz = Class.forName(methodEntity.getClasspath());
String finalMethodName = methodName;
//找到对应的方法对象
Method mathod = Arrays.stream(clazz.getMethods()).filter(method -> method.getName().equals(finalMethodName)).findAny().get();
//装配参数
//获取方法上的参数
Parameter[] parameters = mathod.getParameters();
if (parameters.length == 0) {
return mathod.invoke(SpringContextUtil.getBean(beanName));
} else {
List<Object> args = new ArrayList<>(parameters.length);
assembleParam(request, response, requestbody, parameters, args);
return mathod.invoke(SpringContextUtil.getBean(beanName), args.toArray());
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
log.debug("============>call controller interface encounter error:" + actionName);
return null;
}
//装配参数
private static void assembleParam(HttpServletRequest request, HttpServletResponse response, JSONObject[] requestbody, Parameter[] parameters, List<Object> args) {
for (Parameter parameter : parameters) {
if (parameter.getType().getSimpleName().equals("HttpServletRequest")) {
args.add(request);
} else if (parameter.getType().getSimpleName().equals("HttpServletResponse")) {
args.add(response);
} else {
//string || 基本数据类型
if (parameter.getType().getSimpleName().equals("String") || parameter.getType().isPrimitive()) {
String value = "";
//优先尝试获取参数注解里的值
RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
if (requestParam != null && StringUtils.isNoneBlank(requestParam.value())) {
value = request.getParameter(requestParam.value());
}
if(StringUtils.isBlank(value)){
value = request.getParameter(parameter.getName());
}
args.add(value);
} else {
try {
//非基本数据类型尝试装配复杂数据类型
//判断是否是通过@RequestBody注解
if (parameter.isAnnotationPresent(RequestBody.class)) {
if (requestbody == null || requestbody.length == 0) {
//如果是简单的pojo则尝试通过requestParameter注入
if(!parameter.getType().getSimpleName().equals("List")){
args.add(PackObject.getObject(request, parameter.getType()));
}else{
args.add(null);
}
continue;
}
//list的复杂类型
if (parameter.getType().getSimpleName().equals("List")) {
List<Object> list = new ArrayList<>();
//获取list的泛型类型
ParameterizedType parameterizedType = (ParameterizedType) parameter.getParameterizedType();
Class<?> actualTypeArgument = (Class<?>) parameterizedType.getActualTypeArguments()[0];
//通过requestbody构造list
for (JSONObject jsonObject : requestbody) {
list.add(JSONObject.parseObject(jsonObject.toJSONString(), actualTypeArgument));
}
args.add(list);
} else {
//简单的pojo类型
args.add(JSONObject.parseObject(requestbody[0].toJSONString(), parameter.getType()));
}
} else {
args.add(PackObject.getObject(request, parameter.getType()));
}
} catch (Exception e) {
args.add(null);
}
}
}
}
}
}
这里面有个问题是 获取方法所在的类的字节码对象,使用 clazz = Class.forName(methodEntity.getClasspath()); 而不能用 clazz = SpringContextUtil.getBean(beanName).getClass(); 因为此方法获取的字节码是代理过的 没含有编译保留的信息,后面获取方法参数时 Parameter[] parameters = mathod.getParameters(); 则获取到的参数名是没有实际意义的 为arg0,arg1 这样就没办法绑定对应的参数数据了。
最麻烦的就是绑定参数数据这一块了,情况太多肯定没考虑周全,后面再优化了……
最后还有一个 PackObject.getObject(request, parameter.getType()) 这个方法是通过request 封装指定class的对象,代码如下:
public class PackObject<T> {
Class<T> c;
public static <T> T getObject(HttpServletRequest request, Class<T> c) {
T t = null;
try {
t = c.newInstance(); // 实例化参数对象
} catch (InstantiationException e1) {
e1.printStackTrace();
} catch (IllegalAccessException e1) {
e1.printStackTrace();
}
@SuppressWarnings("rawtypes")
Enumeration e = request.getParameterNames(); // 所有请求参数
Method[] methods = c.getDeclaredMethods(); // 参数对象的所有方法
// 根据对象的set方法的参数类型去将请求的值做相应转换
while (e.hasMoreElements()) {
String paramName = e.nextElement().toString();
String setParamName = reverseParamName(paramName); //将参数名字转换成set方法名字,如:id 转换成 setId
for (Method method : methods) {
if (setParamName.equals(method.getName())) {
try {
Class<?> paramType = (method.getParameterTypes())[0]; //得到set方法参数类型
String value = request.getParameter(paramName);
adapter(t, method, paramType, value); //通过适配器将值注入进POJO里面
} catch (IllegalArgumentException e1) {
e1.printStackTrace();
} catch (IllegalAccessException e1) {
e1.printStackTrace();
} catch (InvocationTargetException e1) {
e1.printStackTrace();
} catch (SecurityException e1) {
e1.printStackTrace();
}
}
}
}
return t;
}
private static String reverseParamName(String paramName) {
char firstChar = paramName.charAt(0);
char toUpper = Character.toUpperCase(firstChar);
String setParamName = "set" + String.valueOf(toUpper)
+ paramName.substring(1);
return setParamName;
}
private static <T> void adapter(T t, Method method, Class<?> paramType,
String value) throws IllegalAccessException,
InvocationTargetException {
if (paramType == String.class) {
method.invoke(t, value);
} else if (paramType == Integer.class || paramType == int.class) {
method.invoke(t, Integer.parseInt(value));
} else if (paramType == Long.class || paramType == long.class) {
method.invoke(t, Long.parseLong(value));
} else if (paramType == Boolean.class || paramType == boolean.class) {
method.invoke(t, Boolean.parseBoolean(value));
} else if (paramType == Short.class || paramType == short.class) {
method.invoke(t, Short.parseShort(value));
} else if (paramType == Float.class || paramType == float.class) {
method.invoke(t, Float.parseFloat(value));
} else if (paramType == Double.class || paramType == double.class) {
method.invoke(t, Double.parseDouble(value));
} else if (paramType == Character.class || paramType == char.class) {
char[] cs = value.toCharArray();
if (cs.length > 1) {
throw new IllegalArgumentException("参数长度太大");
}
method.invoke(t, value.toCharArray()[0]);
}
}