5.excel占位符替换

1.技术jxls

代码 https://gitee.com/J-summit/note-sty-blogs/tree/master/src/main/java/tech/cn/note/excel
可支持自定义函数,复杂字符替换,批量插入等功能

image.png

image.png

依赖引入

    implementation group: 'org.jxls', name: 'jxls', version: '2.9.0'
    implementation group: 'org.jxls', name: 'jxls-poi', version: '2.9.0'
    implementation group: 'org.jxls', name: 'jxls-jexcel', version: '1.0.9'
    implementation group: 'org.jxls', name: 'jxls-reader', version: '2.0.6'

2.代码实现

package tech.tongyu.bct.features.utils.poi.jxsl;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlEngine;
import org.jxls.common.Context;
import org.jxls.transform.TransformationConfig;
import org.jxls.transform.Transformer;
import org.jxls.util.JxlsHelper;
import org.springframework.stereotype.Service;
import tech.tongyu.bct.common.exception.CustomException;

import javax.annotation.PostConstruct;
import java.io.*;
import java.util.HashMap;
import java.util.Map;

@Service
public class JXLSTemplateDataFillServiceImpl implements JXLSTemplateDataFillService {

    private JxlsHelper jxlsHelper;

    private JexlEngine jexlEngine;

    @PostConstruct
    private void JXLSProcessInit() {
        jxlsHelper = JxlsHelper.getInstance();
        Map<String, Object> functionMap = new HashMap<>();
        functionMap.put(JXLSFunction.FUNCTION_NAME, JXLSFunction.getInstance());
        JexlBuilder jexlBuilder = new JexlBuilder();
        jexlBuilder.namespaces(functionMap);
        jexlEngine = jexlBuilder.create();
    }

    @Override
    public byte[] processToByte(File templateFile, Map<String, Object> dataSource) throws IOException, CustomException {
        FileInputStream inputStream = new FileInputStream(templateFile);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        doProcess(inputStream, outputStream, dataSource);
        return outputStream.toByteArray();
    }

    @Override
    public byte[] processToByte(InputStream inputStream, Map<String, Object> dataSource) throws CustomException, IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        doProcess(inputStream, outputStream, dataSource);
        return outputStream.toByteArray();
    }

    @Override
    public byte[] processToPdfByte(File templateFile, Map<String, Object> dataSource) throws Exception {
        byte[] dataByte = processToByte(templateFile, dataSource);
        return null; // todo FileUtil.doPdfProcessByAsposeOfExcel(dataByte);
    }

    @Override
    public void writeToFile(File templateFile, File outFile, Map<String, Object> dataSource) throws Exception {
        byte[] dataByte = processToByte(templateFile, dataSource);
        FileOutputStream fileOutputStream = new FileOutputStream(outFile);
        fileOutputStream.write(dataByte);
        fileOutputStream.close();
    }

    @Override
    public void writeToOutputStream(File templateFile, OutputStream outPut, Map<String, Object> dataSource)
            throws IOException, CustomException {
        FileInputStream inputStream = new FileInputStream(templateFile);
        doProcess(inputStream, outPut, dataSource);
    }

    @Override
    public void writeToOutputStream(InputStream inputStream, OutputStream outPut, Map<String, Object> dataSource)
            throws CustomException, IOException {
        doProcess(inputStream, outPut, dataSource);
    }

    /**
     * 根据模板将数据填充至输出流中
     *
     * @param inputStream
     * @param outputStream
     * @param dataSource
     * @throws CustomException
     */
    private synchronized void doProcess(InputStream inputStream,
                                        OutputStream outputStream,
                                        Map<String, Object> dataSource) throws CustomException, IOException {
        Context dataContext = new Context();
        if (MapUtils.isNotEmpty(dataSource)) {
            dataSource.forEach(dataContext::putVar);
        }
        Transformer transformer = jxlsHelper.createTransformer(inputStream, outputStream);
        transformer.getTransformationConfig().setExpressionEvaluator(new CustomJexlExpressionEvaluator());
        TransformationConfig transformationConfig = transformer.getTransformationConfig();
        CustomJexlExpressionEvaluator evaluator = (CustomJexlExpressionEvaluator) transformationConfig.getExpressionEvaluator();
        evaluator.setJexlEngine(jexlEngine);
        jxlsHelper.setUseFastFormulaProcessor(false).processTemplate(dataContext, transformer);
        IOUtils.closeQuietly(inputStream, outputStream);
    }
}

3.自定义方法

package tech.tongyu.bct.features.utils.poi.jxsl;

import cn.hutool.core.convert.NumberChineseFormatter;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import tech.tongyu.bct.common.util.BigDecimalUtil;
import tech.tongyu.bct.common.util.DateTimeUtils;
import tech.tongyu.bct.common.util.EnumUtils;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import static cn.hutool.core.text.CharSequenceUtil.NULL;
import static tech.tongyu.bct.common.util.BigDecimalUtil.isZero;

/***JXLS模板填充自定义函数*/
public class JXLSFunction {

    public static final String FUNCTION_NAME = "Function";

    private static final JXLSFunction jxlsFunction = new JXLSFunction();

    private JXLSFunction() {
    }

    public static JXLSFunction getInstance() {
        return jxlsFunction;
    }

    public String doNothing(Object value) {
        return value == null ? "" : String.valueOf(value);
    }

    public String dateFormat(Object value) {
        LocalDate resultDate = DateTimeUtils.toLocalDate(value);
        return resultDate == null ? "" : resultDate.toString();
    }

    public String dateFormat(Object value, String formatter) {
        DateTimeFormatter dateTimeFormatter = DatePattern.NORM_DATE_FORMATTER;
        if (StringUtils.isNotBlank(formatter)) {
            dateTimeFormatter = DateTimeFormatter.ofPattern(formatter);
        }
        LocalDate resultDate = DateTimeUtils.toLocalDate(value);
        return resultDate == null ? "" : resultDate.format(dateTimeFormatter);
    }

    public Integer daysBetween(Object value1, Object value2) {
        LocalDate startLocalDate = DateTimeUtils.toLocalDate(value1);
        LocalDate endLocalDate = DateTimeUtils.toLocalDate(value2);
        if (Objects.isNull(startLocalDate) || Objects.isNull(endLocalDate)) {
            return null;
        }
        return (int) (startLocalDate.toEpochDay() - endLocalDate.toEpochDay());
    }

    public static Object nvl(Object value1, Object value2) {
        if (Objects.isNull(value1) || StrUtil.isEmpty(value1.toString())) {
            return value2;
        } else {
            return value1;
        }
    }

    public static String divide(Object value1, Object value2) {
        return divide(value1, value2, 100);
    }

    public static String divide(Object value1, Object value2, Object value3) {
        BigDecimal dividend = BigDecimalUtil.parse(value1);
        BigDecimal divisor = BigDecimalUtil.parse(value2);
        if (Objects.isNull(dividend) || isZero(divisor)) {
            return "0";
        }

        int scale = 100;
        if (Objects.nonNull(value3)) {
            scale = (int) value3;
        }
        return dividend.divide(divisor, scale, RoundingMode.HALF_UP).toPlainString();
    }

    public String numNegate(Object value) {
        return BigDecimalUtil.parseOrZero(value).negate().toPlainString();
    }

    public String numFormat(Object value) {
        BigDecimal num = BigDecimalUtil.parseOrZero(value);
        return num.stripTrailingZeros().toPlainString();
    }

    public String numFormat(Object value, int pointNum) {
        BigDecimal num = BigDecimalUtil.parseOrZero(value);
        return num.setScale(pointNum, RoundingMode.HALF_UP).toPlainString();
    }

    public String numFormat(Object value, int pointNum, int tag) {
        BigDecimal num = BigDecimalUtil.parseOrZero(value);
        BigDecimal numTag = BigDecimal.valueOf(tag);
        return BigDecimalUtil.multiply(num, numTag).setScale(pointNum, RoundingMode.HALF_UP).toPlainString();
    }

    public String thdNum(Object value) {
        BigDecimal num = BigDecimalUtil.parseOrZero(value);
        return BigDecimalUtil.formatThdByPara(num);
    }

    public String thdNum(Object value, int pointNum) {
        BigDecimal num = BigDecimalUtil.parseOrZero(value);
        return BigDecimalUtil.formatThd(num, pointNum);
    }

    public String thdNum(Object value, int pointNum, int tag) {
        BigDecimal num = BigDecimalUtil.parseOrZero(value);
        BigDecimal numTag = BigDecimal.valueOf(tag);
        return BigDecimalUtil.formatThd(BigDecimalUtil.multiply(num, numTag), pointNum);
    }

    public String chineseMoney(Object value) {
        return value == null ? "" : NumberChineseFormatter.format(Double.parseDouble(value.toString()), true, true);
    }

    public String percent(Object value) {
        BigDecimal num = BigDecimalUtil.parse(value);
        return num == null ? "" : BigDecimalUtil.percentFormat(num, num.scale());
    }

    public String percent(Object value, int pointNum) {
        BigDecimal num = BigDecimalUtil.parse(value);
        return num == null ? "" : BigDecimalUtil.percentFormat(num, pointNum);
    }

    public String percentWithoutZero(Object value, int pointNum) {
        BigDecimal num = BigDecimalUtil.parse(value);
        return isZero(num) ? "" : BigDecimalUtil.percentFormat(num, pointNum);
    }

    public String thousandth(Object value) {
        BigDecimal num = BigDecimalUtil.parse(value);
        return num == null ? "" : BigDecimalUtil.thousandthFormat(num, num.scale());
    }

    public String thousandth(Object value, int pointNum) {
        BigDecimal num = BigDecimalUtil.parse(value);
        return num == null ? "" : BigDecimalUtil.thousandthFormat(num, pointNum);
    }

    public String getEnumDesc(String enumPath, String name) throws ClassNotFoundException {
        if (StrUtil.hasBlank(enumPath, name)) {
            return null;
        }
        return EnumUtils.getDesc((Class<? extends Enum<?>>) Class.forName(enumPath), name);
    }

    public String beanToList(Object data, String field, String delimiter) {
        List<String> list = obj2List(data, field);
        return String.join(StrUtil.blankToDefault(delimiter, ","), list);
    }

    public String beanToList(Object data, String field) {
        List<String> list = obj2List(data, field);
        return String.join(",", list);
    }

    private static List<String> obj2List(Object data, String field) {
        List<String> list = Lists.newArrayList();
        // data为list
        if (data instanceof Collection) {
            list = ((List<?>) data).stream().map(v -> {
                try {
                    if (v instanceof Map) {
                        Map<String, Object> map = (Map<String, Object>) v;
                        Object mapField = map.get(field);
                        return mapField instanceof BigDecimal
                                ? ((BigDecimal) mapField).toPlainString()
                                : String.valueOf(mapField);
                    } else {
                        Field declaredField = v.getClass().getDeclaredField(field);
                        declaredField.setAccessible(true);
                        return declaredField.get(v).toString();
                    }
                } catch (NoSuchFieldException | IllegalAccessException ignored) {
                    // 不打日志了 避免刷屏
                    return null;
                }
            }).filter(v -> CharSequenceUtil.isNotBlank(v) && !NULL.equals(v)).collect(Collectors.toList());
        } else if (data instanceof String) {
            // data为字符串(由list转)
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                List<Map<String, Object>> mapList = objectMapper.readValue(data.toString(),
                        new TypeReference<List<Map<String, Object>>>() {
                        });
                list = mapList.stream()
                        .filter(map -> map.containsKey(field))
                        .map(map -> map.get(field) instanceof BigDecimal
                                ? ((BigDecimal) map.get(field)).toPlainString()
                                : String.valueOf(map.get(field))
                        )
                        .filter(v -> CharSequenceUtil.isNotBlank(v) && !NULL.equals(v))
                        .collect(Collectors.toList());
            } catch (JsonProcessingException e) {
                // 不打日志了 避免刷屏
            }
        }
        return list;
    }
}

4.自定义异常处理

package tech.tongyu.bct.features.utils.poi.jxsl;

import java.util.HashMap;
import java.util.Map;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.commons.jexl3.MapContext;
import org.jxls.expression.JexlExpressionEvaluator;

/**
 * jxls填充占位符时,JexlExpressionEvaluator在catch到Exception后处理方式为
 * throw new EvaluationException("An error occurred when evaluating expression " + expression, e);
 * 错误日志会刷屏,为了避免刷屏,重写JexlExpressionEvaluator的异常处理
 */
@Slf4j
public class CustomJexlExpressionEvaluator extends JexlExpressionEvaluator {
    private static final ThreadLocal<Map<String, JexlExpression>> expressionMapThreadLocal = ThreadLocal.withInitial(HashMap::new);

    @Override
    public Object evaluate(String expression, Map<String, Object> context) {
        JexlContext jexlContext = new MapContext(context);
        try {
            Map<String, JexlExpression> expressionMap = expressionMapThreadLocal.get();
            JexlExpression jexlExpression = expressionMap.get(expression);
            if (jexlExpression == null) {
                jexlExpression = getJexlEngine().createExpression(expression);
                expressionMap.put(expression, jexlExpression);
            }
            return jexlExpression.evaluate(jexlContext);
        } catch (Exception e) {
            log.warn("An error occurred when evaluating expression[{}]:{}", expression, e.getMessage());
            return null;
        }
    }

}

5.占位符填充

1.在Excel表格第一单元格即A1,添加批注如下:jx:area(lastCell="I2"),锁定填充模板作用域范围


image.png

2.在输出行第一个单元格,添加批注如下: jx:each(items="contractList1" var="itemValue" lastCell="I2"),


image.png

items表示数据中集合对象key值,
var表示对象的别名,

lastCell表示锁定作用域范围。

5.1项目实战案例

image.png
    @Operation(summary = "导出持仓明细")
    @PostMapping("/export")
    public ResponseEntity<byte[]> exportToExcel(@RequestBody @Validated ReportQueryDTO reportDTO) throws Exception {
        List<CustomPositionReportDTO> positionReportList = positionReportService.listPositionReport(reportDTO);
        List<String> tradeIds = positionReportList.stream().map(CustomPositionReportDTO::getTradeId).collect(Collectors.toList());
        TradeCriteria build = TradeCriteria.builder().tradeIds(tradeIds).build();
        List<TradeDTO> trades = tradeQueryExternalClient.listTrade(build);
        Map<String, TradeDTO> tradeMap = trades.stream().collect(Collectors.toMap(TradeDTO::getTradeId, Function.identity(), (k1, k2) -> k1));
        for (CustomPositionReportDTO prl : positionReportList) {
            prl.setTrade(tradeMap.get(prl.getTradeId()));
        }
        HttpHeaders headers = new HttpHeaders();
        String fileName = String.format("日终-%s.xlsx", reportDTO.getValuationDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        headers.add("Content-Disposition", String.format("attachment; filename=%s", UriUtils.encode(fileName, "UTF-8")));
        InputStream stream = Files.newInputStream(Paths.get(tmpPath + positionFileName));
        Map<String, Object> map = new HashMap<>();
        map.put("list", positionReportList);
        byte[] bytes = jxlsTemplateDataFillService.processToByte(stream, map);
        return ResponseEntity.ok().headers(headers).body(bytes);
    }

6.其他占位符demo

占位符配置 占位符说明
${对象.属性} 常规数据填充
${Function:dateFormat(日期对象,"yyyy年MM月dd日")} 常规数据类型对象格式化输出
${Function:dateFormat(日期对象,"yyyy-MM-dd")} 常规数据类型对象格式化输出
${Function:dateFormat(日期对象,'yyyy')} 通用日期格式化输出年份
${Function:dateFormat(日期对象, 'MM')} 通用日期格式化输出月份
${Function:dateFormat(日期对象, 'dd')} 通用日期格式化输出天数
${Function:numFormat(数值对象,小数位数)} 数值输出,小数位数不设置则以实际值输出
${Function:thdNum(数值对象,小数位数)} 数值千分位输出,小数位数不设置则以实际值输出
${Function:percent(数值对象,小数位数)} 数值百分比输出,小数位数不设置则保留实际的小数位数
${Function:chineseMoney(数值对象)} #金额转换为中文
详情配置可参考链接: //www.greatytc.com/p/1f821b519374
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容