java实现excel导入、下载模板方法

导入excel:

1. 引入 EasyExcel 依赖

如果你还没有引入 EasyExcel,请先在 pom.xml 中加上:

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.3.4</version> <!-- 建议使用较新的稳定版本 -->
</dependency>

2. 为 Entity 加上 EasyExcel 注解

为了让 EasyExcel 能够自动把 Excel 的列和对象的属性对应起来 row-by-row 地解析,建议在你的 ProductEntity 属性上加上 @ExcelProperty 注解(匹配 Excel 的表头名称):

java 复制代码
import com.alibaba.excel.annotation.ExcelProperty;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.util.Date;

@Data
@TableName("product_new")
public class ProductEntity {

    @TableId(type = IdType.AUTO)
    private Long productId;

    @ExcelProperty("一级分类(业务板块)")
    private String firstCategory;

    @ExcelProperty("一级分类说明")
    private String firstDesc;

    @ExcelProperty("二级分类-行业/能源类型")
    private String secondIndustry;

    @ExcelProperty("二级分类-阶段")
    private String secondStage;

    @ExcelProperty("二级分类-业务类型")
    private String secondType;

    @ExcelProperty("二级分类说明")
    private String secondDesc;

    @ExcelProperty("三级分类")
    private String thirdCategory;

    @ExcelProperty("三级分类说明")
    private String thirdDesc;

    @ExcelProperty("四级分类(标段级别)")
    private String fourthCategory;

    @ExcelProperty("采购方式")
    private String procurementMethod;

    @ExcelProperty("评审办法")
    private String reviewMethod;

    @ExcelProperty("对应招标文件范本")
    private String bidTemplate;

    @ExcelProperty("对应合同范本")
    private String contractTemplate;

    private Date createTime;
    private Date updateTime;
}

3. 核心:编写 Excel 解析监听器

EasyExcel 是基于事件驱动的,需要一个监听器来边读边处理数据。为了防止一次性读入十几万条数据撑爆内存,我们采用分批插入(每满 100 条存一次数据库)的方式。

java 复制代码
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;

@Slf4j
public class ProductExcelListener implements ReadListener<ProductEntity> {

    /**
     * 每隔100条存储数据库,实际使用中可以1000条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 100;
    private final List<ProductEntity> cachedDataList = new ArrayList<>(BATCH_COUNT);
    
    // 注入 service 用来写入数据库
    private final ProductService productService;

    public ProductExcelListener(ProductService productService) {
        this.productService = productService;
    }

    /**
     * 每一条数据解析都会进来
     */
    @Override
    public void invoke(ProductEntity data, AnalysisContext context) {
        log.info("解析到一条品类数据: {}", data);
        cachedDataList.add(data);
        // 达到BATCH_COUNT了,需要去存储一次数据库,防止几万条数据在内存中导致OOM
        if (cachedDataList.size() >= BATCH_COUNT) {
            saveData();
            // 存储完成清理 list
            cachedDataList.clear();
        }
    }

    /**
     * 所有数据解析完成了都会来调用
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 这里也要保存一下,确保最后遗留的数据(不满 BATCH_COUNT 条)也被存入数据库
        saveData();
        log.info("所有数据解析完成!");
    }

    /**
     * 加上存储数据库
     */
    private void saveData() {
        if (!cachedDataList.isEmpty()) {
            log.info("{}条数据,开始存储数据库!", cachedDataList.size());
            productService.saveBatch(cachedDataList);
            log.info("存储数据库成功!");
        }
    }
}

4. Service 实现类 (ServiceImpl)

在 ProductServiceImpl 中编写调用 EasyExcel 的核心逻辑:

java 复制代码
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, ProductEntity> implements ProductService {

    @Override
    public void importProductExcel(MultipartFile file) {
        try {
            // 这里 需要指定用哪个监听器去读,并且传入当前 service 实例
            EasyExcel.read(file.getInputStream(), ProductEntity.class, new ProductExcelListener(this))
                     .sheet()
                     .doRead();
        } catch (IOException e) {
            log.error("读取 Excel 文件失败", e);
            throw new RuntimeException("文件解析异常,请稍后再试");
        }
    }
}

(ProductService 接口中声明了 void importProductExcel(MultipartFile file); )


5. 控制层 (Controller)

最后,暴露出一个 HTTP POST 接口,供前端上传文件。

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    @PostMapping("/import")
    public ResponseEntity<String> importExcel(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            return ResponseEntity.badRequest().body("上传的文件不能为空");
        }
        
        // 简单校验下文件后缀,防止传错格式
        String fileName = file.getOriginalFilename();
        if (fileName == null || (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx"))) {
            return ResponseEntity.badRequest().body("请上传正确的 Excel 文件 (.xls 或 .xlsx)");
        }

        try {
            productService.importProductExcel(file);
            return ResponseEntity.ok("品类数据导入成功!");
        } catch (Exception e) {
            return ResponseEntity.status(500).body("导入失败: " + e.getMessage());
        }
    }
}

  1. 关于时间字段:数据库中的 create_time 和 update_time 已经设置了 DEFAULT CURRENT_TIMESTAMP。通过这种批量插入时,如果 Excel 里没有这两列,MyBatis-Plus 插入后数据库会自动生成当前时间,无需在前端或后端代码里手动 Set。
  2. 监听器不能交由 Spring 管理 :注意到 ProductExcelListener 是每次读取时由 new 出来的。不要把它声明为 @Component 类似单例,因为它内部持有 cachedDataList。如果并发导入,单例会导致数据错乱。用 new 并通过构造器把 Service 传进去是 EasyExcel 的标准推荐写法

下载模板:


1. Service 接口

首先在 ProductService 接口中声明下载模板的方法:

java 复制代码
import javax.servlet.http.HttpServletResponse;

public interface ProductService extends IService<ProductEntity> {
    // 前面已实现的导入方法
    void importProductExcel(MultipartFile file);
    
    // 新增:下载模板方法
    void downloadTemplate(HttpServletResponse response);
}

2. ServiceImpl 实现类

ProductServiceImpl 中实现该方法。我们通过 EasyExcel.write() 并传入 ProductEntity.class,它会自动根据类上配置的 @ExcelProperty("表头名称") 注解来动态生成 Excel 的表头。

java 复制代码
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, ProductEntity> implements ProductService {

    @Override
    public void importProductExcel(MultipartFile file) {
        // ... 前面已实现的导入逻辑
    }

    @Override
    public void downloadTemplate(HttpServletResponse response) {
        try {
            // 1. 设置响应内容类型和字符集
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            
            // 2. 防止中文文件名乱码
            String fileName = URLEncoder.encode("品类数据导入模板", "UTF-8").replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
            
            // 3. 使用 EasyExcel 将空列表(或者你可以放一条示例数据)写入响应流
            // 传入 ProductEntity.class 后,EasyExcel 会自动根据 @ExcelProperty 生成表头
            EasyExcel.write(response.getOutputStream(), ProductEntity.class)
                    .sheet("品类模板")
                    .doWrite(new ArrayList<>()); // 传入空列表,只生成表头;如果需要示例数据,可以New一个对象加进去
                    
        } catch (IOException e) {
            log.error("下载模板失败", e);
            throw new RuntimeException("生成下载模板异常,请稍后再试");
        }
    }
}

3. Controller 控制层

ProductController 中暴露出一个 GET 请求接口,供前端(或浏览器直接访问)下载:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;

@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    @PostMapping("/import")
    public ResponseEntity<String> importExcel(@RequestParam("file") MultipartFile file) {
        // ... 前面已实现的导入接口
    }

    /**
     * 下载品类导入模板
     * 注意:因为是直接往 response 流里写数据,所以方法返回值设为 void 即可
     */
    @GetMapping("/download-template")
    public void downloadTemplate(HttpServletResponse response) {
        productService.downloadTemplate(response);
    }
}

💡 提示:如何添加"示例数据"?

如果你希望用户下载打开模板时,里面自带一行模拟数据 作为参考,只需要修改 ServiceImpl.doWrite() 传入的参数即可。例如:

java 复制代码
// 在 ServiceImpl 的 downloadTemplate 方法中:
List<ProductEntity> demoData = new ArrayList<>();

ProductEntity demo = new ProductEntity();
demo.setFirstCategory("新能源板块");
demo.setFirstDesc("包含风电、光伏等业务");
demo.setSecondIndustry("陆上风电");
demo.setSecondStage("前期阶段");
demo.setSecondType("服务");
demo.setFourthCategory("某风电场一期标段");
// ... 可以根据需要继续补充其他字段的示例
demoData.add(demo);

// 写入时传入 demoData 即可
EasyExcel.write(response.getOutputStream(), ProductEntity.class)
        .sheet("品类模板")
        .doWrite(demoData);
相关推荐
眠りたいです2 小时前
现代C++:C++14中的新语言特性和库特性
c语言·开发语言·c++
段ヤシ.2 小时前
回顾Java知识点,面试题汇总Day12(持续更新)
java·mybatis
java1234_小锋3 小时前
Spring AI 2.0 开发Java Agent智能体 - MCP(模型上下文协议)
java·人工智能·spring·spring ai
seven97_top3 小时前
两小时入门Sentinel
java·sentinel
叶小鸡3 小时前
Java 篇-项目实战-AI 天机学堂(从 0 到 1)-day1
java·开发语言
bigbearxyz3 小时前
Caused by: java.net.SocketException: Connection reset问题排查
java·keepalived·proxysql
楼田莉子4 小时前
C++17新特性:__had_include/属性/求值顺序规则
开发语言·c++·后端
香蕉鼠片5 小时前
Python进阶学习
开发语言·python
500845 小时前
昇腾 CANN 的五层架构,到底分了哪五层
java·人工智能·分布式·架构·ocr·wpf