项目场景:
最近接到一个需求对大数据进行Excel导出,打算多线程分sheet页导出
问题描述
测试过程中发现声明的静态成员变量用 @Autowired 注入 bean 为 null
(因为是在工具类中我定义的工具类方法是静态的 所以成员变量也需要定义为静态)
错误demo部分代码如下
package com.ruoyi.common.utils.poi;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
@Component
public class ExcelXSSFNewUtil {
@Autowired
private static ThreadPoolTaskExecutor threadPoolTaskExecutor;
public static void exportXlsxBigInfo(String[] headName, String[] headId, List list, String fileName, HttpServletResponse response) throws Exception {
//测试默认两条一页
int num = 2;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
int dataCount = list.size();
//任务数(向下取整)
int taskNext = dataCount / num;
//余数
int modeCount = dataCount % taskNext;
//每个线程有个sheet用开始结束分割数据
int start = 0;
int end = 0;
SXSSFWorkbook workbook = new SXSSFWorkbook();
CountDownLatch countDownLatch = new CountDownLatch(taskNext);
for (int t = 0; t < taskNext; t++) {
start = t * num;
//最后一页加上余数
if (t == taskNext - 1) {
end = (t + 1) * num + modeCount;
} else {
end = (t + 1) * num;
}
List subList = list.subList(start, end);
//这个方法线程不安全放外面
SXSSFSheet sheet = workbook.createSheet(start + 1 + "-" + end);
threadPoolTaskExecutor.execute(() -> {
try {
Row row1 = sheet.createRow(0);
for (int j = 0; j < headName.length; j++) {
Cell cell = row1.createCell(j);
cell.setCellValue(headName[j]);
}
Object obj = null;
Map map;
for (int i = 1; i <= subList.size(); i++) {
Row row = sheet.createRow(i);
obj = subList.get(i - 1);
map = objectToMap(obj);
for (int j = 0; j < headId.length; j++) {
Cell cell = row.createCell(j);
XSSFRichTextString text = null;
String head = null;
String dictName = null;
head =String.valueOf(map.get(headId[j]));
if ("".equals(head) || head == null) {
text = new XSSFRichTextString("");
} else {
Field field = obj.getClass().getDeclaredField(headId[j]);
boolean b = field.isAccessible();
field.setAccessible(true);
if (field.get(obj) instanceof Date) {
String a = sdf.format(field.get(obj));
text = new XSSFRichTextString(a);
} else {
text = new XSSFRichTextString(head);
}
field.setAccessible(b);
}
cell.setCellValue(text);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
File file = new File("C:\\Users\\14377\\Videos\\Captures\\" + fileName);
FileOutputStream os = new FileOutputStream(file);
workbook.write(os);
os.flush();
os.close();
// response.setContentType("application/octet-stream;charset=UTF-8");
// response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
// response.setCharacterEncoding("UTF-8");
// ServletOutputStream outputStream = response.getOutputStream();
// workbook.write(outputStream);
// outputStream.flush();
// outputStream.close();
}
private static void addValidate(SXSSFSheet sheet, List<String> list, String head, int column) {
DataValidationHelper helper = sheet.getDataValidationHelper();
//区域
CellRangeAddressList regions = new CellRangeAddressList(3, 5000, column, column);
// 生成下拉框内容
DataValidationConstraint constraint = helper.createFormulaListConstraint(String.valueOf(list));
// 绑定下拉框和作用区域
DataValidation validation = helper.createValidation(constraint, regions);
validation.setShowErrorBox(true);
validation.createErrorBox("警告", head + "选择有误");
sheet.addValidationData(validation);
}
public static Map<String, Object> objectToMap(Object object) {
Map<String, Object> dataMap = new HashMap<>();
Class<?> clazz = object.getClass();
for (Field field : clazz.getDeclaredFields()) {
try {
field.setAccessible(true);
dataMap.put(field.getName(), field.get(object));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return dataMap;
}
}
@PostMapping("/export")
public void export(HttpServletResponse response, SysUser user) throws Exception {
List<SysUser> list = userService.selectUserList(user);
ExcelXSSFNewUtil.exportXlsxBigInfo(USER_NAME,USER_ID,list,"用户信息.xlsx",response);
}
private static final String[] USER_NAME = new String[]{"用户序号","部门编号","登录名称","用户名称","用户邮箱","手机号码","用户性别","帐号状态","最后登录IP","最后登录时间"};
private static final String[] USER_ID = new String[]{"userId","deptId","userName","nickName","email","phonenumber","sex","status","loginIp","loginDate"};
原因分析:
这篇文章写的比较好
https://blog.csdn.net/asd43211234/article/details/106824230?spm=1001.2014.3001.5506
原因大概有一下两点
1.在Java中,针对static静态成员,我们有一些最基本的常识:静态变量(成员)它是属于类的,而非属于实例对象的属性;同样的静态方法也是属于类的,普通方法(实例方法)才属于对象。而Spring容器管理的都是实例对象,包括它的@Autowired依赖注入的均是容器内的对象实例,所以对于static成员是不能直接使用@Autowired注入的。
就是 类成员的初始化较早,那时候实例对象还没有 就不存在注入成功了
2.看源码
扫描Class类需要注入的元数据的时候,直接选择忽略掉了static成员(包括属性和方法)。
解决方案:
间接实现static成员注入 延迟为static成员属性赋值
方法一:set方法注入
@Component
public class ExcelXSSFNewUtil {
private static ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Autowired
public void setMbsTaskExecutor(@Qualifier("threadPoolTaskExecutor") ThreadPoolTaskExecutor taskExecutor) {
ExcelXSSFNewUtil.threadPoolTaskExecutor = taskExecutor;
}
方法二:使用@PostConstruct注解
@Component
public class ExcelXSSFNewUtil {
private static ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Resource
private ThreadPoolTaskExecutor taskExecutor;
@PostConstruct
public void init() {
ExcelXSSFNewUtil.threadPoolTaskExecutor = taskExecutor;
}
@PostConstruct注解介绍:
初始化方式一:@PostConstruct注解
假设类UserController有个成员变量UserService被@Autowired修饰,那么UserService的注入是在UserController的构造方法之后执行的。
如果想在UserController对象生成时候完成某些初始化操作,而偏偏这些初始化操作又依赖于依赖注入的对象,那么就无法在构造函数中实现(ps:spring启动时初始化异常),例如:
public class UserController {
@Autowired
private UserService userService;
public UserController() {
// 调用userService的自定义初始化方法,此时userService为null,报错
userService.userServiceInit();
}
}
因此,可以使用@PostConstruct注解来完成初始化,@PostConstruct注解的方法将会在UserService注入完成后被自动调用。
public class UserController {
@Autowired
private UserService userService;
public UserController() {
}
// 初始化方法
@PostConstruct
public void init(){
userService.userServiceInit();
}
}
总结:类初始化调用顺序:
(1)构造方法Constructor
(2)@Autowired
(3)@PostConstruct
初始化方式二:实现InitializingBean接口
除了采用注解完成初始化,也可以通过实现InitializingBean完成类的初始化
public class UserController implements InitializingBean {
@Autowired
private UserService userService;
public UserController() {
}
// 初始化方法
@Override
public void afterPropertiesSet() throws Exception {
userService.userServiceInit();
}
}