分批导出,从模板方法模式到代理模式
1. 普通分页导出的写法
service里共有3个方法
export为导出的方法
getTotal是根据查询条件查询数据的总条数
getData是根据查询条件分页查询数据
其中getTotal和getData为业务方法,自己模拟了一些数据
@Service
public class Service1 {
//每页查询的数量
private final int PAGESIZE = 20;
//最大的页码
private final int MAX_PAGENO = 5;
protected static Logger log = LoggerFactory.getLogger(Service1.class);
/**
* 分页导出数据
* @param response
*/
public void export(HttpServletResponse response) {
String fileName = "文件名"+".xlsx";
Set<String> includeColumnFiledNames = ExampleDTO.getIncludeColumnFiledNames(false);
HorizontalCellStyleStrategy strategy = ExcelUtils.getCellStyleStrategy(false);
Class<ExampleDTO> clazz = ExampleDTO.class;
ExampleQuery query = new ExampleQuery();
long total = getTotal(query);
log.info("导出的数量为:{}", total);
if (total>Integer.MAX_VALUE){
System.out.println("超出导出限制");
}
//从第一页开始导出
Integer pageNo = 1;
//每次查询20条数据
Integer pageSize = PAGESIZE;
//分页导出的总页数
long pageNum = total % pageSize==0? (total/pageSize) : (total/pageSize)+1;
ExcelWriter excelWriter = null;
try {
response.setContentType("application/octet-stream;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename*=utf-8'zh_cn'" + URLEncoder.encode(fileName, "UTF-8"));
ServletOutputStream outputStream = response.getOutputStream();
excelWriter = EasyExcel.write(outputStream, clazz)
.registerWriteHandler(strategy)
.includeColumnFiledNames(includeColumnFiledNames)
.build();
WriteSheet writeSheet = EasyExcel.writerSheet(fileName).build();
while (true) {
log.info("开始查询第{}页:", pageNo);
List<ExampleDTO> dto = getData(query,pageNo,pageSize);
log.info("完成查询第{}页:", pageNo);
pageNo++;
excelWriter.write(dto, writeSheet);
if (pageNo > pageNum ) {
break;
}
}
} catch (Exception e) {
log.error("导出失败:", e);
} finally {
if (excelWriter != null) {
try {
excelWriter.finish();
} catch (Exception e) {
log.error("关闭导出流失败:", e);
}
}
}
}
/**
* 根据查询条件 查询数据的总条数
* 方法内为自己定的条数
* @param query
* @return
*/
public long getTotal(ExampleQuery query){
return PAGESIZE*MAX_PAGENO;
}
/**
* 根据查询条件 查询数据
* 方法内为自己造的数据
* @param query 查询条件
* @param pageNo 页码
* @param pageSize 每页的数量
* @return
*/
public List<ExampleDTO> getData(ExampleQuery query,Integer pageNo,Integer pageSize){
List<ExampleDTO> dtos = new ArrayList<>();
if (pageNo<=MAX_PAGENO){
for (int i = 1; i <= PAGESIZE; i++) {
ExampleDTO dto = new ExampleDTO();
dto.setCode(pageNo+" 页code_"+i);
dto.setName("name "+i);
dtos.add(dto);
}
}
return dtos;
}
}
ExampleDTO为需要导出记录的模型
@Data
@ContentRowHeight(60)
public class ExampleDTO {
/**
* 工号
*/
@ExcelProperty(value = "工号", index = 0)
private String code;
/**
* 姓名
*/
@ExcelProperty(value = "姓名", index = 1)
private String name;
/**
* 导入错误 的 错误原因
*/
@ColumnWidth(30)
@ExcelProperty(value = "错误原因", index = 18)
private String errMsg;
public static Set<String> getIncludeColumnFiledNames(Boolean hasErrMsg) {
Set<String> includeColumnFiledNames = new HashSet<String>();
includeColumnFiledNames.add("code");
includeColumnFiledNames.add("name");
if (hasErrMsg){
includeColumnFiledNames.add("errMsg");
}
return includeColumnFiledNames;
}
public static List<String> listHead(Boolean hasErrMsg) {
List<String> headList = new ArrayList<>(Arrays.asList("工号","姓名"));
if (hasErrMsg) {
headList.add("导入错误信息");
}
return headList;
}
}
缺点:当需要再添加一个导出方法,导出另外的模型时,会产生重复代码
例如,想要导出ExampleDTO2模型时,需要在相应的service里添加如下方法
public void export2(HttpServletResponse response) {
String fileName = "文件名2"+".xlsx";
Set<String> includeColumnFiledNames = ExampleDTO.getIncludeColumnFiledNames(false);
HorizontalCellStyleStrategy strategy = ExcelUtils.getCellStyleStrategy(false);
Class<ExampleDTO2> clazz = ExampleDTO2.class;
ExampleQuery2 query = new ExampleQuery2();
long total = getTotal(query);
log.info("导出的数量为:{}", total);
if (total>Integer.MAX_VALUE){
System.out.println("超出导出限制");
}
//从第一页开始导出
Integer pageNo = 1;
//每次查询20条数据
Integer pageSize = PAGESIZE;
//分页导出的总页数
long pageNum = total % pageSize==0? (total/pageSize) : (total/pageSize)+1;
ExcelWriter excelWriter = null;
try {
response.setContentType("application/octet-stream;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename*=utf-8'zh_cn'" + URLEncoder.encode(fileName, "UTF-8"));
ServletOutputStream outputStream = response.getOutputStream();
excelWriter = EasyExcel.write(outputStream, clazz)
.registerWriteHandler(strategy)
.includeColumnFiledNames(includeColumnFiledNames)
.build();
WriteSheet writeSheet = EasyExcel.writerSheet(fileName).build();
while (true) {
log.info("开始查询第{}页:", pageNo);
List<ExampleDTO2> dto = getData(query,pageNo,pageSize);
log.info("完成查询第{}页:", pageNo);
pageNo++;
excelWriter.write(dto, writeSheet);
if (pageNo > pageNum ) {
break;
}
}
} catch (Exception e) {
log.error("导出失败:", e);
} finally {
if (excelWriter != null) {
try {
excelWriter.finish();
} catch (Exception e) {
log.error("关闭导出流失败:", e);
}
}
}
}
2. 采用模板方法模式抽取相同代码
定义抽象模板
/**
* 分页导出模板
* <p>
* T 为查询参数 需要有 PageNo 和 PageSize
* R 为查询结果,导出的对象
*/
public abstract class PagingExportTemplate<T extends BasicQueryEntity, R> {
protected static final Logger log = LoggerFactory.getLogger(PagingExportTemplate.class);
/**
* 获取查询总数
*
* @param param
* @return
*/
protected abstract long getTotal(T param);
/**
* 获取一页的数据
*
* @param param
* @return
*/
protected abstract List<R> getPageData(T param,Integer pageNo,Integer pageSize);
/**
* 分页导出
*
* @param param 查询参数
* @param pageSize 每页的数量
* @param response
* @param fileName 文件名
* @param includeColumnFiledNames 导出对象的表头字段
* @param clazz 导出对象类
* @param strategy 导出策略
*/
public void pagingExport(T param, Integer pageSize, HttpServletResponse response,
String fileName, Set<String> includeColumnFiledNames, Class clazz, HorizontalCellStyleStrategy strategy) {
long total = getTotal(param);
log.info("导出的数量为:{}", total);
if (total>Integer.MAX_VALUE){
System.out.println("超出导出限制");
}
//分页导出的总页数
long pageNum = total % pageSize==0? (total/pageSize) : (total/pageSize)+1;
//从第一页开始导出
Integer pageNo = 1;
ExcelWriter excelWriter = null;
try {
response.setContentType("application/octet-stream;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename*=utf-8'zh_cn'" + URLEncoder.encode(fileName, "UTF-8"));
ServletOutputStream outputStream = response.getOutputStream();
excelWriter = EasyExcel.write(outputStream, clazz)
.registerWriteHandler(strategy)
.includeColumnFiledNames(includeColumnFiledNames)
.build();
WriteSheet writeSheet = EasyExcel.writerSheet(fileName).build();
while (true) {
log.info("开始查询第{}页:", pageNo);
List<R> dto = getPageData(param,pageNo,pageSize);
log.info("完成查询第{}页:", pageNo);
pageNo++;
excelWriter.write(dto, writeSheet);
if (pageNo > pageNum ) {
break;
}
}
} catch (Exception e) {
log.error("导出失败:", e);
} finally {
if (excelWriter != null) {
try {
excelWriter.finish();
} catch (Exception e) {
log.error("关闭导出流失败:", e);
}
}
}
}
}
当想要添加一个导出方法时,需添加一个实现类,实现具体的查询逻辑。将其注入到service。
@Component
public class ExampleExport extends PagingExportTemplate<ExampleQuery, ExampleDTO> {
@Resource
Service2 service;
@Override
protected long getTotal(ExampleQuery param) {
return service.getTotal(param);
}
@Override
protected List<ExampleDTO> getPageData(ExampleQuery param,Integer pageNo,Integer pageSize) {
return service.getData(param,pageNo,pageSize);
}
}
service的导出方法调用具体实现类的模板方法
@Service
public class Service2 {
//每页查询的数量
private final int PAGESIZE = 20;
//最大的页码
private final int MAX_PAGENO = 5;
@Resource
ExampleExport exampleExport;
public void export(HttpServletResponse response) {
String fileName = "文件名"+".xlsx";
Set<String> includeColumnFiledNames = ExampleDTO.getIncludeColumnFiledNames(false);
HorizontalCellStyleStrategy strategy = ExcelUtils.getCellStyleStrategy(false);
Class<ExampleDTO> clazz = ExampleDTO.class;
ExampleQuery query = new ExampleQuery();
exampleExport.pagingExport(query,PAGESIZE, response,fileName,includeColumnFiledNames, clazz,strategy);
}
public long getTotal(ExampleQuery query){
return PAGESIZE*MAX_PAGENO;
}
public List<ExampleDTO> getData(ExampleQuery query,Integer pageNo,Integer pageSize){
List<ExampleDTO> dtos = new ArrayList<>();
if (pageNo<=MAX_PAGENO){
for (int i = 1; i <= PAGESIZE; i++) {
ExampleDTO dto = new ExampleDTO();
dto.setCode(pageNo+" 页code_"+i);
dto.setName("name "+i);
dtos.add(dto);
}
}
return dtos;
}
}
优点:将相同的代码抽取到抽象类中,不同的代码延迟到具体的子类实现。减少了重复的代码
缺点:增加了设计的复杂度,每增加一个导出方法,都需添加一个具体的实现类
3. 采用注解+代理模式
大致思路:仿造mybatis的思路,定义接口,接口中只有抽象的导出方法,方法上带有自定义注解。容器启动时扫描自定义的接口,为其生成代理对象,在执行代理方法时读取方法上的注解信息,获取相应的service的bean和查询数据的方法名。代理方法执行导出的逻辑,当需要查询数据时,可通过反射执行service里的查询方法。
先定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExportMethodTab {
String beanName();
String getTotal();
String getData();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExportProxyTab {
}
定义InvocationHandler,导出的代码写在这里
class ServiceProxy implements InvocationHandler {
protected static Logger log = LoggerFactory.getLogger(ServiceProxy.class);
private ApplicationContext applicationContext;
ServiceProxy(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
ExportMethodTab annotation = method.getAnnotation(ExportMethodTab.class);
if (annotation==null){
return null;
}
String beanName = annotation.beanName();
String getTotal = annotation.getTotal();
String getData = annotation.getData();
Object bean = applicationContext.getBean(beanName);
if (bean==null){
return null;
}
//获取执行的方法
Method getTotalMethod = getMethod(bean.getClass(), getTotal);
Method getDataMethod = getMethod(bean.getClass(), getData);
invokeMethod(bean,getTotalMethod,getDataMethod,args);
return null;
}
/**
* 获取目标方法
* @param proxyObject
* @param methodStr
* @return
*/
private Method getMethod(Class proxyObject, String methodStr) {
Method[] methods = proxyObject.getMethods();
for(Method method : methods) {
if(method.getName().equalsIgnoreCase(methodStr)) {
return method;
}
}
return null;
}
private void invokeMethod(Object bean, Method getTotalMethod, Method getDataMethod, Object[] args) throws Exception{
Object arg = args[0];
if (!(arg instanceof ExportParam)){
return;
}
ExportParam param = (ExportParam) arg;
long total = (long)getTotalMethod.invoke(bean, param.getQueryParam());
log = LoggerFactory.getLogger(bean.getClass());
log.info("导出的数量为:{}", total);
if (param.getMax()!=null && total>param.getMax()){
throw new Exception("超出导出的最大限制");
}
Integer pageSize = param.getPageSize();
HttpServletResponse response = param.getResponse();
Class clazz = param.getClazz();
String fileName = param.getFileName();
Set<String> includeColumnFiledNames = param.getIncludeColumnFiledNames();
HorizontalCellStyleStrategy strategy = param.getStrategy();
//分页导出的总页数
long pageNum = total % pageSize==0? (total/pageSize) : (total/pageSize)+1;
ExcelWriter excelWriter = null;
try {
response.setContentType("application/octet-stream;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename*=utf-8'zh_cn'" + URLEncoder.encode(fileName, "UTF-8"));
ServletOutputStream outputStream = response.getOutputStream();
excelWriter = EasyExcel.write(outputStream, clazz)
.registerWriteHandler(strategy)
.includeColumnFiledNames(includeColumnFiledNames)
.build();
WriteSheet writeSheet = EasyExcel.writerSheet(fileName).build();
Object[] array = new Object[3];
//查询参数
array[0] = param.getQueryParam();
//查询的页数
array[1] = 1;
//每页的数量
array[2] = param.getPageSize();
while (true) {
log.info("开始查询第{}页:", (int)array[1]);
List list = (List) getDataMethod.invoke(bean, array);
log.info("完成查询第{}页:", (int)array[1]);
array[1] = (int)array[1]+1;
excelWriter.write(list, writeSheet);
if ((int)array[1] > pageNum ) {
break;
}
}
} catch (Exception e) {
log.error("导出失败:", e);
} finally {
if (excelWriter != null) {
try {
excelWriter.finish();
} catch (Exception e) {
log.error("关闭导出流失败:", e);
}
}
}
}
}
定义工厂类
class ServiceProxyFactoryBean<T> implements FactoryBean<T> , ApplicationContextAware {
private Class<T> interfaces;
private ApplicationContext applicationContext;
public ServiceProxyFactoryBean(Class<T> interfaces) {
this.interfaces = interfaces;
}
@Override
@SuppressWarnings("unchecked")
public T getObject() throws Exception {
return (T) Proxy.newProxyInstance(interfaces.getClassLoader(), new Class[]{interfaces},
new ServiceProxy(applicationContext));
}
@Override
public Class<?> getObjectType() {
return interfaces;
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
定义扫描器,在这里定义如何处理扫描到的类
class ServiceInterfacesScanner extends ClassPathBeanDefinitionScanner {
ServiceInterfacesScanner(BeanDefinitionRegistry registry) {
//false表示不使用ClassPathBeanDefinitionScanner默认的TypeFilter
super(registry, false);
}
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
this.addFilter();
Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
this.createBeanDefinition(beanDefinitionHolders);
return beanDefinitionHolders;
}
/**
* 该类必须是接口
* 并且这个类 没有 其他的实现类
* 并且有ExportProxyTab注解
*
* @param beanDefinition bean定义
* @return boolean
*/
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
AnnotationMetadata metadata = beanDefinition.getMetadata();
MergedAnnotation<ExportProxyTab> annotation = metadata.getAnnotations().get(ExportProxyTab.class);
return metadata.isInterface() && metadata.isIndependent() && annotation!=null;
}
/**
* 扫描所有类
*/
private void addFilter() {
addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
}
/**
* 为扫描到的接口创建代理对象
*
* @param beanDefinitionHolders beanDefinitionHolders
*/
private void createBeanDefinition(Set<BeanDefinitionHolder> beanDefinitionHolders) {
for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
GenericBeanDefinition beanDefinition = ((GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition());
//将bean的真实类型改变为FactoryBean
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
beanDefinition.setBeanClass(ServiceProxyFactoryBean.class);
beanDefinition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
}
注册扫描器
public class ProxyRegister implements BeanDefinitionRegistryPostProcessor {
private String basePackage;
public ProxyRegister(String basePackage) {
this.basePackage = basePackage;
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (basePackage==null) {
return;
}
ServiceInterfacesScanner scanner = new ServiceInterfacesScanner(registry);
scanner.doScan(basePackage);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
}
指定扫描的路径
@Configuration
public class DemoConfiguration {
@Bean
public ProxyRegister proxyRegister() {
return new ProxyRegister("com.jxf.demo.pageExport.proxy");
}
}
定义导出方法的参数对象
@Data
public class ExportParam {
/**
* 查询参数
*/
private Object queryParam;
/**
* 每页的数量
*/
private Integer pageSize;
/**
* http响应对象
*/
private HttpServletResponse response;
/**
* 文件名
*/
private String fileName;
/**
* 导出对象的表头字段
*/
private Set<String> includeColumnFiledNames;
/**
* 导出对象类
*/
private Class clazz;
/**
* 导出策略
*/
private HorizontalCellStyleStrategy strategy;
/**
* 允许导出的最大数量
*/
private Long max;
}
使用方法:
在指定的包下定义接口,添加类注解@ExportProxyTab
当需要添加一个导出方法时,只需添加一个抽象方法。该方法需添加@ExportMethodTab注解,beanName指定对应的service的名字,getData指定查询数据的方法,getTotal指定查询总条数的方法
@ExportProxyTab
public interface ExportProxy {
@ExportMethodTab(beanName = "service3",getData ="getData", getTotal = "getTotal")
void exportA(ExportParam param);
}
service调用代理对象的方法
@Service
public class Service3 {
//每页查询的数量
private final int PAGESIZE = 20;
//最大的页码
private final int MAX_PAGENO = 5;
@Resource
ExportProxy exportProxy;
public void export(HttpServletResponse response) {
String fileName = "文件名"+".xlsx";
Set<String> includeColumnFiledNames = ExampleDTO.getIncludeColumnFiledNames(false);
HorizontalCellStyleStrategy strategy = ExcelUtils.getCellStyleStrategy(false);
Class<ExampleDTO> clazz = ExampleDTO.class;
ExampleQuery query = new ExampleQuery();
ExportParam param = new ExportParam();
param.setQueryParam(query);
param.setPageSize(20);
param.setResponse(response);
param.setFileName(fileName);
param.setIncludeColumnFiledNames(includeColumnFiledNames);
param.setClazz(clazz);
param.setStrategy(strategy);
exportProxy.exportA(param);
}
public long getTotal(ExampleQuery query){
return PAGESIZE*MAX_PAGENO;
}
public List<ExampleDTO> getData(ExampleQuery query,Integer pageNo,Integer pageSize){
List<ExampleDTO> dtos = new ArrayList<>();
if (pageNo<=MAX_PAGENO){
for (int i = 1; i <= PAGESIZE; i++) {
ExampleDTO dto = new ExampleDTO();
dto.setCode(pageNo+" 页code_"+i);
dto.setName("name "+i);
dtos.add(dto);
}
}
return dtos;
}
}