5.excel占位符替換

1.技術(shù)jxls

代碼 https://gitee.com/J-summit/note-sty-blogs/tree/master/src/main/java/tech/cn/note/excel
可支持自定義函數(shù),復(fù)雜字符替換,批量插入等功能

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.代碼實現(xiàn)

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

    /**
     * 根據(jù)模板將數(shù)據(jù)填充至輸出流中
     *
     * @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模板填充自定義函數(shù)*/
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轉(zhuǎn))
            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表示數(shù)據(jù)中集合對象key值,
var表示對象的別名盲厌,

lastCell表示鎖定作用域范圍。

5.1項目實戰(zhàn)案例

image.png
    @Operation(summary = "導(dǎo)出持倉明細(xì)")
    @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

占位符配置 占位符說明
${對象.屬性} 常規(guī)數(shù)據(jù)填充
${Function:dateFormat(日期對象,"yyyy年MM月dd日")} 常規(guī)數(shù)據(jù)類型對象格式化輸出
${Function:dateFormat(日期對象,"yyyy-MM-dd")} 常規(guī)數(shù)據(jù)類型對象格式化輸出
${Function:dateFormat(日期對象,'yyyy')} 通用日期格式化輸出年份
${Function:dateFormat(日期對象, 'MM')} 通用日期格式化輸出月份
${Function:dateFormat(日期對象, 'dd')} 通用日期格式化輸出天數(shù)
${Function:numFormat(數(shù)值對象,小數(shù)位數(shù))} 數(shù)值輸出祸泪,小數(shù)位數(shù)不設(shè)置則以實際值輸出
${Function:thdNum(數(shù)值對象,小數(shù)位數(shù))} 數(shù)值千分位輸出吗浩,小數(shù)位數(shù)不設(shè)置則以實際值輸出
${Function:percent(數(shù)值對象,小數(shù)位數(shù))} 數(shù)值百分比輸出,小數(shù)位數(shù)不設(shè)置則保留實際的小數(shù)位數(shù)
${Function:chineseMoney(數(shù)值對象)} #金額轉(zhuǎn)換為中文
詳情配置可參考鏈接: http://www.reibang.com/p/1f821b519374
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末没隘,一起剝皮案震驚了整個濱河市懂扼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌右蒲,老刑警劉巖阀湿,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異瑰妄,居然都是意外死亡陷嘴,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進店門间坐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灾挨,“玉大人,你說我怎么就攤上這事眶诈≌谴祝” “怎么了?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵逝撬,是天一觀的道長浴骂。 經(jīng)常有香客問我,道長宪潮,這世上最難降的妖魔是什么溯警? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮狡相,結(jié)果婚禮上梯轻,老公的妹妹穿的比我還像新娘。我一直安慰自己尽棕,他們只是感情好喳挑,可當(dāng)我...
    茶點故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般伊诵。 火紅的嫁衣襯著肌膚如雪单绑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天曹宴,我揣著相機與錄音搂橙,去河邊找鬼。 笑死笛坦,一個胖子當(dāng)著我的面吹牛区转,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播版扩,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼废离,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了资厉?” 一聲冷哼從身側(cè)響起厅缺,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤蔬顾,失蹤者是張志新(化名)和其女友劉穎宴偿,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诀豁,經(jīng)...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡窄刘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了舷胜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片娩践。...
    茶點故事閱讀 40,872評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖烹骨,靈堂內(nèi)的尸體忽然破棺而出翻伺,到底是詐尸還是另有隱情,我是刑警寧澤沮焕,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布吨岭,位于F島的核電站,受9級特大地震影響峦树,放射性物質(zhì)發(fā)生泄漏辣辫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一魁巩、第九天 我趴在偏房一處隱蔽的房頂上張望急灭。 院中可真熱鬧,春花似錦谷遂、人聲如沸葬馋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽畴嘶。三九已至扫尖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間掠廓,已是汗流浹背换怖。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蟀瞧,地道東北人沉颂。 一個月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像悦污,于是被迫代替她去往敵國和親铸屉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,876評論 2 361

推薦閱讀更多精彩內(nèi)容