分批导出,从模板方法模式到代理模式

分批导出,从模板方法模式到代理模式

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

上一篇:TkMybatis用法总结


下一篇:【MySQL】二十、limit的使用(重点),通用SQL分页