EasyExcel 实现国际化导入导出

发布于:2025-09-12 ⋅ 阅读:(15) ⋅ 点赞:(0)

新项目涉及国际化。不想每个语言都建一个pojo文件。就将字段信息放到了数据库中。因为数据体量小,都放在了一个表中,可以根据需要改成一个语言一个表,或者是一个类多个语言一个表。
思路:
下载模版: 查询导入模版的字段,使用EasyExcel实现文件导出
导入: 查询导入模版的字段,利用EasyExcel 回调监听,获取到excel的数据。使用反射将表头转换成字段名,构造对像并给对象赋值,获得对象数据。之后就可以根据实际需求对数据进行新增或修改。
导出:查询导出模版字段,查询导出数据,使用EasyExcel实现文件导出。比下载模版多一个查询导出数据。

新建数据库表

entity_header 表字段

字段名 描述
id 主键ID
entity 对象名,也可以认为是关键字,用来区分这个字段是属于谁
lang 语言,例如:en、zh-CN
field 字段名,实体类中的字段,如 name
translate 字段的翻, 例如: 姓名、Name
sort 排序,文件导出是字段的顺序。如果顺序要求严格,也可以用作导入的列字段判断
remark 备注信息
CREATE TABLE `entity_header` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `entity` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '对象',
  `lang` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '语言标识',
  `field` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '字段名称',
  `translate` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '翻译后的字段名称',
  `sort` int(11) DEFAULT NULL COMMENT '显示顺序',
  `remark` varchar(200) DEFAULT NULL COMMENT '备注',
  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='对象表头字段翻译';

后端实现

新建导入导出VO


import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

/**
 * 包装容器 Excel 导入 VO
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = false) // 设置 chain = false,避免导入有问题
public class PackingContainerImportExcelVO {

    private Long oemId;

    @ExcelProperty("代工厂")
    private String oemName;

    @ExcelProperty("包装容器号")
    private String code;

    @ExcelProperty("rfid")
    private String rfid;

    @ExcelProperty("重量下限")
    private String weightMin;

    @ExcelProperty("重量上限")
    private String weightMax;

    @ExcelProperty("皮重")
    private String tareWeight;

    @ExcelProperty("容器型号")
    private String packagingContainerType;

    @ExcelProperty("容器容积")
    private String packagingVolume;

}

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;

import java.util.List;
import java.util.Map;

@Schema(description = "包装容器导入 Response VO")
@Data
@Builder
public class PackingContainerImportRespVO {

    @Schema(description = "创建成功的包装容器数组", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<String> createCodes;

    @Schema(description = "更新成功的包装容器数组", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<String> updateCodes;

    @Schema(description = "导入失败的包装容器集合,key 为容器编号,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
    private Map<String, String> failureCodes;

}

Controller文件增加方法

	@GetMapping("/get-import-template")
    @Operation(summary = "获得导入包装容器模板")
    public void importTemplate(HttpServletResponse response,
                               @RequestAttribute(value = "lang", required = false) String lang ) throws IOException {

        // 输出
        List<EntityHeaderDO> entityHeaderList = entityHeaderService.getEntityHeaderList(PackingContainerImportExcelVO.class.getSimpleName(), lang);
        if(CollectionUtil.isEmpty(entityHeaderList)){
            throw exception(ENTITY_FIELDS_TEMPLATE_NOT_EXISTS);
        }
        List<String> headerList = entityHeaderList.stream()
                .map(EntityHeaderDO::getTranslate).collect(Collectors.toList());
        ExcelUtils.writeTemplate(response,headerList,"container_data_template.xls");
    }

    @PostMapping("/import")
    @Operation(summary = "导入包装容器")
    @Parameters({
            @Parameter(name = "file", description = "Excel 文件", required = true),
            @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
    })
    @PreAuthorize("@ss.hasPermission('packing-container-info:import')")
    @Transactional(rollbackFor = Exception.class)
    public CommonResult<PackingContainerImportRespVO> importExcel(@RequestParam("file") MultipartFile file,
               @RequestAttribute(value = "lang", required = false) String lang ,
               @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception {
        List<EntityHeaderDO> headerList = entityHeaderService.getEntityHeaderList(PackingContainerImportExcelVO.class.getSimpleName(), lang);
        if(CollectionUtil.isEmpty(headerList)){
            throw exception(ENTITY_FIELDS_TEMPLATE_NOT_EXISTS);
        }
        Map<String, String> headMap = headerList.stream().collect(Collectors.toMap(EntityHeaderDO::getTranslate, EntityHeaderDO::getField, (v1, v2) -> v1));
        List<PackingContainerImportExcelVO> list = excelImportService.importExcel(file, PackingContainerImportExcelVO.class, headMap);
        return success(packingContainerInfoService.importContainerList(list,updateSupport));
    }

	@GetMapping("/export-excel")
    @Operation(summary = "导出包装容器 Excel")
    @PreAuthorize("@ss.hasPermission('packing-container-info:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportInspectionStandardDetailExcel(@Valid PackingContainerPageReqVO pageReqVO,
              @RequestAttribute(value = "lang", required = false) String lang,
              HttpServletResponse response) throws IOException {
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<PackingContainerDO> list = packingContainerInfoService.getPackingContainerPage(pageReqVO).getList();
        // 输出
        List<EntityHeaderDO> entityHeaderList = entityHeaderService.getEntityHeaderList(InspectionStandardDetailExcelVO.class.getSimpleName(), lang);
        if(CollectionUtil.isEmpty(entityHeaderList)){
            throw exception(ENTITY_FIELDS_TEMPLATE_NOT_EXISTS);
        }
        List<String> headerList = entityHeaderList.stream()
                .map(EntityHeaderDO::getTranslate).collect(Collectors.toList());
        // 导出 Excel
        if(CollectionUtil.isNotEmpty(list) && LanguageEnum.ENGLISH.getLang().equals(lang)){
            list.forEach(this::setLangAttrEn);
        }
        ExcelUtils.writeExport(response, "包装容器.xls", "data", headerList,
                        BeanUtils.toBean(list, InspectionStandardDetailExcelVO.class));
    }

ExcelUtils 工具类

import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.converters.longconverter.LongStringConverter;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.mo.framework.common.util.http.HttpUtils;
import com.mo.framework.excel.core.handler.SelectSheetWriteHandler;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

/**
 * Excel 工具类
 *
 */
public class ExcelUtils {

    /**
     * 将列表以 Excel 响应给前端
     *
     * @param response  响应
     * @param filename  文件名
     * @param sheetName Excel sheet 名
     * @param head      Excel head 头
     * @param data      数据列表哦
     * @param <T>       泛型,保证 head 和 data 类型的一致性
     * @throws IOException 写入失败的情况
     */
    public static <T> void write(HttpServletResponse response, String filename, String sheetName,
                                 Class<T> head, List<T> data) throws IOException {
        // 输出 Excel
        EasyExcel.write(response.getOutputStream(), head)
                .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
                .registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框
                .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
                .sheet(sheetName).doWrite(data);
        // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
        response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
    }

    public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
        return EasyExcel.read(file.getInputStream(), head, null)
                .autoCloseStream(false)  // 不要自动关闭,交给 Servlet 自己处理
                .doReadAllSync();
    }

    public static void writeTemplate(HttpServletResponse response,List<String> headerList,String fileName) throws IOException{
        // 将映射信息转换为表头列表
        List<List<String>> head = new ArrayList<>();
        for(String field : headerList){
            List<String> headerRow = new ArrayList<>();
            headerRow.add(field);
            head.add(headerRow);
        }
        // 设置响应头
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
        response.setCharacterEncoding("utf-8");
        fileName = StrUtil.isEmpty(fileName)?"import_template.xlsx":fileName;
        fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);

        // 使用 EasyExcel 写入表头并生成模板文件
        EasyExcel.write(response.getOutputStream())
                .head(head)
                .sheet("data")
                .doWrite(new ArrayList<>());
    }

    public static <T> void writeExport(HttpServletResponse response, String filename, String sheetName,
                                       List<String> headerList, List<T> data) throws IOException {
        // 将映射信息转换为表头列表
        List<List<String>> head = new ArrayList<>();
        for(String field : headerList){
            List<String> headerRow = new ArrayList<>();
            headerRow.add(field);
            head.add(headerRow);
        }
        // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
        response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
        // 输出 Excel
        EasyExcel.write(response.getOutputStream())
                .head(head)
                .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
                .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
                .sheet(sheetName).doWrite(data);
    }

}

导入功能利用反射将excel数据转成对象数据

使用表头信息与翻译信息匹配,获取字段名称

import com.alibaba.excel.EasyExcel;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Service
public class ExcelImportService {

    public <T> List<T> importExcel(MultipartFile file, Class<T> beanClass, Map<String,String> headerFieldMap) throws Exception{
        ExcelDataListener listener = new ExcelDataListener();

        // 使用 EasyExcel 读取 Excel 数据
        EasyExcel.read(file.getInputStream()).registerReadListener(listener).sheet().doRead();

        List<T> result = new ArrayList<>();

        for (Map<String,String> row : listener.getDataList()) {
            T bean = beanClass.getDeclaredConstructor().newInstance();
            for (Map.Entry<String, String> entry : row.entrySet()) {
                String header = entry.getKey();
                String fieldName = headerFieldMap.get(header); // 表头 → 字段名
                if (fieldName != null) {
                    Field field = beanClass.getDeclaredField(fieldName);
                    field.setAccessible(true);
                    field.set(bean, entry.getValue());
                }
            }
            result.add(bean);
        }

        return result;
    }

    public <T> List<T> convertToBeanList(List<Map<String, Object>> dataList, Class<T> beanClass) throws Exception {
        List<T> beans = new ArrayList<>();
        for (Map<String, Object> data : dataList) {
            T bean = beanClass.getDeclaredConstructor().newInstance();
            for (Map.Entry<String, Object> entry : data.entrySet()) {
                String fieldName = entry.getKey();
                Object value = entry.getValue();
                try {
                    Field field = beanClass.getDeclaredField(fieldName);
                    field.setAccessible(true);
                    if (field.getType() == Integer.class || field.getType() == int.class) {
                        field.set(bean, value == null ? 0 : Integer.parseInt((String) value));
                    } else if (field.getType() == Double.class || field.getType() == double.class) {
                        field.set(bean, value == null ? 0.0 : Double.parseDouble((String) value));
                    } else {
                        field.set(bean, value);
                    }
                } catch (NoSuchFieldException | IllegalAccessException ignored) {
                }
            }
            beans.add(bean);
        }
        return beans;
    }
}
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import lombok.Getter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * excel 回调监听
 */
public class ExcelDataListener extends AnalysisEventListener<Map<Integer, String>> {
    private final Map<Integer, String> headers = new HashMap<>();
    @Getter
    private final List<Map<String, String>> dataList = new ArrayList<>();

    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {
        // 构建 header -> value 映射
        Map<String, String> row = new HashMap<>();
        for (Map.Entry<Integer, String> entry : data.entrySet()) {
            String header = headers.get(entry.getKey());
            if (header != null) {
                row.put(header, entry.getValue());
            }
        }
        dataList.add(row);

    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 所有数据解析完成
    }

    // excel头部数据
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        // 存到全局的map中
        headers.putAll(headMap);
    }
}