java:EasyExcel使用(一)读excel

java:EasyExcel使用(一)读excel

1 前言

EasyExcel相比于传统使用poi进行excel文件读写,编程使用操作上更加方便快捷,且对于内存溢出进行了优化处理。本文是EasyExcel读excel操作。

Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。

easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便。

EasyExcel官方文档:

https://easyexcel.opensource.alibaba.com/

进入maven.org,自动跳转如下进行maven仓库查询:

https://central.sonatype.com/?smo=true

搜索easyexcel:

在这里插入图片描述

pom文件添加如下依赖:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.3.4</version>
</dependency>

2 使用(Excel读取)

2.1 读Excel

GoodsDto:

package com.xiaoxu.excel.readE;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;

/**
 * @author xiaoxu
 * @date 2024-04-26 22:32
 * learn_java:com.xiaoxu.excel.readE.GoodsDto
 */
@Getter
@Setter
@EqualsAndHashCode
public class GoodsDto {
    private String name;
    private Date date;
    private long count;
    private Date produceDate;
    private double price;
}

在桌面准备的excel文件,test.xlsx:

在这里插入图片描述

excel读取方式一:

TestExcelRead,读取excel(JDK8.0+,不用额外写ReadListener,since: 3.0.0-beta1):

package com.xiaoxu.excel.readE;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.listener.PageReadListener;
import com.alibaba.fastjson.JSON;

import javax.swing.filechooser.FileSystemView;
import java.io.File;
import java.text.SimpleDateFormat;

/**
 * @author xiaoxu
 * @date 2024-04-27 14:53
 * learn_java:com.xiaoxu.excel.readE.TestExcelRead
 */
public class TestExcelRead {

    public static void main(String[] args) {
        // C:\Users\****\Desktop
        File homeDirectory = FileSystemView
                .getFileSystemView().getHomeDirectory();
        String fileName = "test";
        //  读取桌面路径excel文件
        String file = homeDirectory.getAbsolutePath()
                + File.separator + fileName + ".xlsx";
        EasyExcel.read(file, GoodsDto.class,
                new PageReadListener<GoodsDto>(dataList -> {
                    for (GoodsDto goodsDto : dataList) {
                        System.out.println("读取到excel数据:" +
                                JSON.toJSONString(goodsDto));
                        SimpleDateFormat sdf =
                                new SimpleDateFormat(
                                        "yyyy-MM-dd HH:mm:ss.SSS");
                        if (null != goodsDto.getDate()) {
                            System.out.println("日期:" +
                                    sdf.format(goodsDto.getDate()));
                        }
                        if (null != goodsDto.getProduceDate()) {
                            System.out.println("生产日期:" +
                                    sdf.format(goodsDto.getProduceDate()));
                        }
                    }
                })).sheet().doRead();
    }

}

执行excel读取结果如下:

读取到excel数据:
{"count":45,"date":1709099280000,
"name":"苹果","price":1.65,"produceDate":15278342400000}
日期:2024-02-28 13:48:00.000
生产日期:2454-02-25 00:00:00.000

读取到excel数据:{"count":13,"date":1713611400000,
"name":"梨子","price":2.4,"produceDate":15278515200000}
日期:2024-04-20 19:10:00.000
生产日期:2454-02-27 00:00:00.000

读取到excel数据:{"count":0,"name":"菠萝","price":9.9}

日期转换说明,需要按照excel的日期格式来写,

上述结果表明,如果是引用类型,那么excel的cell值为null时,不会为实体类赋值;如果实体类是primitive类型数据,同时excel的cell值为null时,会将null转换成该primitive的默认值,比如实体类的count属性为long时,默认值就是0,所以也会赋值(因为包装类的值为null时,直接转换成primitive类型值时,不可以为其赋值为null,必须转换成其对应的默认值,否则将会抛出异常)。

同时注意,上述的实体类的字段,和excel的每一列是按顺序一一对应定义的。否则可能会抛出ExcelDataConvertException转换异常(Excel的值类型需要和实体类字段类型一一对应)。

excel读取方式二:

TestExcelRead2,读取excel(定义监听器GoodsDtoListener读取excel):

GoodsDtoListener:

package com.xiaoxu.excel.readE;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.fastjson.JSON;

import java.util.List;

/**
 * @author xiaoxu
 * @date 2024-04-26 22:42
 * learn_java:com.xiaoxu.excel.readE.GoodsDtoListener
 */
public class GoodsDtoListener implements ReadListener<GoodsDto> {

    /**
     * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 100;

    /**
     * 缓存数据
     */
    private List<GoodsDto> cache = ListUtils
            .newArrayListWithExpectedSize(BATCH_COUNT);

    /**
     * 假设这个是一个DAO,
     * 当然有业务逻辑这个也可以是一个service。
     * 当然如果不用存储这个对象没用。
     */
    private GoodsDAO goodsDAO;

    public GoodsDtoListener() {
        //  这里是demo,所以随便new一个。
        //  实际使用如果到了spring,请使用下面的有参构造函数
        this.goodsDAO = new GoodsDAO();
    }

    public GoodsDtoListener(GoodsDAO goodsDAO) {
        //  如果使用了spring,请使用这个构造方法。
        //  每次创建Listener的时候需要把spring管理的类传进来
        this.goodsDAO = goodsDAO;
    }


    /**
     * @param goodsDto        one row value.
     *                        Is is same
     *                        as {@link AnalysisContext#readRowHolder()}
     * @param analysisContext 这个每一条数据解析都会来调用
     */
    @Override
    public void invoke(GoodsDto goodsDto, AnalysisContext analysisContext) {
        System.out.println(
                String.format(
                        "解析到数据: %s.", JSON.toJSONString(goodsDto)));
        cache.add(goodsDto);
        // 达到BATCH_COUNT了,需要去存储一次数据库,
        // 防止数据几万条数据在内存,容易OOM
        if (cache.size() >= BATCH_COUNT) {
            saveData();
            // cache置为空List,方便gc work
            cache = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }

    }

    private void saveData() {
        System.out.println("开始存储数据数数据:" + cache.size());
        goodsDAO.save(cache);
    }

    /**
     * @param analysisContext 所有数据处理完,调用这里
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        //  这里也要保存数据,
        //  确保最后遗留的数据也存储到数据库
        System.out.println("最终数据存储:");
        saveData();
        System.out.println("所有数据解析完成!");
    }

}

GoodsDAO :

public class GoodsDAO {

    public void save(List<GoodsDto> goodsDtoList) {
        // 如果是mybatis,尽量别直接调用多次insert,
        // 自己写一个mapper里面新增一个方法batchInsert,
        // 所有数据一次性插入
        // batchInsert的插入性能更好
        System.out.println("存储数据成功~");
    }

}

TestExcelRead2:

package com.xiaoxu.excel.readE;

import com.alibaba.excel.EasyExcel;

import javax.swing.filechooser.FileSystemView;
import java.io.File;

/**
 * @author xiaoxu
 * @date 2024-04-27 16:37
 * learn_java:com.xiaoxu.excel.readE.TestExcelRead2
 */
public class TestExcelRead2 {

    public static void main(String[] args) {
        // C:\Users\****\Desktop
        File homeDirectory = FileSystemView
                .getFileSystemView().getHomeDirectory();
        String fileName = "test";
        //  读取桌面路径excel文件
        String file = homeDirectory.getAbsolutePath()
                + File.separator + fileName + ".xlsx";

        //   有个很重要的点 GoodsDtoListener 不能被spring管理,
        //   要每次读取excel都要new,然后里面用到spring可以构造方法传进去
        //   excel有多个Sheet,可以通过 .sheet("Sheet1")指定sheet名称
        //   或者可以通过.sheet(0)指定读取第1个sheet   
        //   如果sheet名称不存在,那么不会报错,也不会读取到数据
        EasyExcel.read(file, GoodsDto.class, new GoodsDtoListener())
        .sheet("Sheet1").doRead();
    }

}

执行结果如下:

解析到数据: {"count":45,"date":1709099280000,
"name":"苹果","price":1.65,"produceDate":1746545644800000}.
解析到数据: {"count":13,"date":1713611400000,
"name":"梨子","price":2.4,"produceDate":1746563788800000}.
解析到数据: {"count":0,"name":"菠萝","price":9.9}.
最终数据存储:
开始存储数据数数据:3
存储数据成功~
所有数据解析完成!

注意:excel文件有多个sheet时,.sheet()表示读取第1个sheet的内容。若需要指定sheet名称读取,则使用.sheet(“Sheet1”)即可指定名称为Sheet1的sheet进行读取,若不知道sheet名称,那么可以通过.sheet(0)读取第1个sheet,.sheet(1)读取第2个sheet等等的方式来进行读取,下标从0开始。如果sheet的名称不存在,或者sheet的索引下标不存在(比如总共只有3个sheet,但是读取为.sheet(3)第4个sheet)时,不会抛出异常,只是无法读取到对应数据。并且EasyExcel读取sheet的数据时,表头不会进入Listener回调中,而是从第二行的内容数据开始进入回调。

excel读取方式三:

TestExcelRead3,读取excel(通过ExcelReader、ReadSheet读取excel文件):

package com.xiaoxu.excel.readE;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.metadata.ReadSheet;

import javax.swing.filechooser.FileSystemView;
import java.io.File;

/**
 * @author xiaoxu
 * @date 2024-04-27 19:32
 * learn_java:com.xiaoxu.excel.readE.TestExcelRead3
 */
public class TestExcelRead3 {

    public static void main(String[] args) {
        // C:\Users\****\Desktop
        File homeDirectory = FileSystemView
                .getFileSystemView().getHomeDirectory();
        String fileName = "test";
        //  读取桌面路径excel文件
        String file = homeDirectory.getAbsolutePath()
                + File.separator + fileName + ".xlsx";

        // 一个文件一个reader
        try (ExcelReader excelReader = EasyExcel
                .read(file, GoodsDto.class, 
                        new GoodsDtoListener()).build()) {
            // 构建一个sheet 这里可以指定名字或者no
            ReadSheet readSheet = EasyExcel.readSheet(0).build();
            // 读取一个sheet
            excelReader.read(readSheet);
        }
    }

}

读取结果同读取方式二一致。

2.2 指定列的下标或者列名

前面提到了,实体类的字段必须按照顺序和excel的列一一对应,否则可能出现转换异常或者赋值与实际字段不匹配的情况。但是这种情况也有解决方式,即使用EasyExcel提供的@ExcelProperty注解

GoodsDtoProperty:

package com.xiaoxu.excel.cols;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;

/**
 * @author xiaoxu
 * @date 2024-04-27 20:36
 * learn_java:com.xiaoxu.excel.cols.GoodsDtoPropertty
 */
@Getter
@Setter
@EqualsAndHashCode
public class GoodsDtoProperty {

    //  注解的默认值是value,这里表示excel的表头的列名称
    @ExcelProperty("售卖日期")
    private Date date;

    @ExcelProperty(index = 99)
    private long count;

    private Date produceDate;

    // index表示读取第几列,0表示第一列,1表示第二列...
    // 这里price属性是实体类第四个属性,没有 @ExcelProperty表示和第4列对应
    // 这里表示和excel的第5列对应
    @ExcelProperty(index = 4)
    private double price;

    @ExcelProperty(value = "名称")
    private String name;
}

excel数据如下:

在这里插入图片描述

TestExcelReadProp:

package com.xiaoxu.excel.cols;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.listener.PageReadListener;
import com.alibaba.fastjson.JSON;

import javax.swing.filechooser.FileSystemView;
import java.io.File;
import java.text.SimpleDateFormat;

/**
 * @author xiaoxu
 * @date 2024-04-27 20:46
 * learn_java:com.xiaoxu.excel.cols.TestExcelReadProp
 */
public class TestExcelReadProp {

    public static void main(String[] args) {
        // C:\Users\****\Desktop
        File homeDirectory = FileSystemView
                .getFileSystemView().getHomeDirectory();
        String fileName = "test";
        //  读取桌面路径excel文件
        String file = homeDirectory.getAbsolutePath()
                + File.separator + fileName + ".xlsx";
        EasyExcel.read(file, GoodsDtoProperty.class,
                new PageReadListener<GoodsDtoProperty>(dataList -> {
                    for (GoodsDtoProperty goodsDtoProp : dataList) {
                        System.out.println("读取到excel数据:" +
                                JSON.toJSONString(goodsDtoProp));
                        SimpleDateFormat sdf =
                                new SimpleDateFormat(
                                        "yyyy-MM-dd HH:mm:ss.SSS");
                        if (null != goodsDtoProp.getDate()) {
                            System.out.println("日期:" +
                                    sdf.format(goodsDtoProp.getDate()));
                        }
                        if (null != goodsDtoProp.getProduceDate()) {
                            System.out.println("生产日期:" +
                                    sdf.format(goodsDtoProp.getProduceDate()));
                        }
                    }
                })).sheet().doRead();
    }

}

执行结果:

读取到excel数据:{"count":0,"name":"苹果",
"price":1.65,"produceDate":1709099280000}
生产日期:2024-02-28 13:48:00.000

读取到excel数据:{"count":0,"name":"梨子",
"price":2.4,"produceDate":1713611400000}
生产日期:2024-04-20 19:10:00.000

读取到excel数据:{"count":0,"name":"菠萝","price":9.9}

上述表明,如果实体类中使用了@ExcelProperty注解,那么最好是全部属性都加上,这样方便和excel需要的属性列一一对应,否则可能出现部分值胡乱匹配excel列值的情况。同时,如果注解的value值随意填写,即excel的匹配列名不存在,或者index超越索引上限,都不会报错。同时index索引也是从0开始,0表示excel的第1列,1表示excel的第2列,以此类推。

2.3 读多个sheet

2.3.1 读取全部sheet

准备excel文件中存在多个Sheet,文件为test3.xlsx:

Sheet1:

在这里插入图片描述

Sheet2:

在这里插入图片描述

提取读取桌面文件工具类:

package com.xiaoxu.excel;

import javax.swing.filechooser.FileSystemView;
import java.io.File;

/**
 * @author xiaoxu
 * @date 2024-04-27 22:00
 * learn_java:com.xiaoxu.excel.DeskTool
 */
public class DeskTool {

    public static String getPath(String fileName) {
        // C:\Users\****\Desktop
        File homeDirectory = FileSystemView
                .getFileSystemView().getHomeDirectory();
        //  读取桌面路径excel文件
        return homeDirectory.getAbsolutePath()
                + File.separator + fileName + ".xlsx";
    }

}

GoodsDtoNew:

package com.xiaoxu.excel.multiSht;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;

/**
 * @author xiaoxu
 * @date 2024-04-27 22:17
 * learn_java:com.xiaoxu.excel.multiSht.GoodsDtoNew
 */
@Getter
@Setter
@EqualsAndHashCode
public class GoodsDtoNew {
    //  注解的默认值是value,这里表示excel的表头的列名称
    @ExcelProperty("日期")
    private Date date;

    @ExcelProperty("生产日期")
    private Date produceDate;

    @ExcelProperty("价格")
    private double price;

    @ExcelProperty("数量")
    private long count;

    @ExcelProperty(value = "名称")
    private String name;
}

GoodsDtoNewListener:

package com.xiaoxu.excel.multiSht;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.fastjson.JSON;

/**
 * @author xiaoxu
 * @date 2024-04-27 22:20
 * learn_java:com.xiaoxu.excel.multiSht.GoodsDtoNewListener
 */
public class GoodsDtoNewListener implements ReadListener<GoodsDtoNew> {
    @Override
    public void invoke(GoodsDtoNew goodsDtoNew, AnalysisContext analysisContext) {
        System.out.println("扫描数据:"
                + JSON.toJSONString(goodsDtoNew));
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        System.out.println("所有数据解析完成!");
    }
}

TestExcelReadMultiSheetAll:

package com.xiaoxu.excel.multiSht;

import com.alibaba.excel.EasyExcel;
import com.xiaoxu.excel.DeskTool;

/**
 * @author xiaoxu
 * @date 2024-04-27 21:57
 * learn_java:com.xiaoxu.excel.multiSht.TestExcelReadMultiSheetAll
 */
public class TestExcelReadMultiSheetAll {

    public static void main(String[] args) {
        String file = DeskTool.getPath("test3");

        // 读取全部sheet
        // 这里需要注意 GoodsDtoNewListener的doAfterAllAnalysed
        // 会在每个sheet读取完毕后调用一次。
        // 然后所有sheet都会往同一个GoodsDtoNewListener里面写(或者读)
        EasyExcel.read(file,
                GoodsDtoNew.class,
                new GoodsDtoNewListener())
                .doReadAll();
    }

}

执行结果:

扫描数据:{"count":45,"date":1709099280000,
"name":"苹果","price":1.65,"produceDate":1746545644800000}
扫描数据:{"count":13,"date":1713611400000,
"name":"梨子","price":2.4,"produceDate":1746563788800000}
扫描数据:{"count":0,"name":"菠萝","price":9.9}
所有数据解析完成!

扫描数据:{"count":45,"date":1709349480000,
"name":"水蜜桃","price":3.5,"produceDate":1746535536000000}
扫描数据:{"count":13,"date":1713805800000,
"name":"菠萝蜜","price":9.5,"produceDate":1746546508800000}
扫描数据:{"count":88,"date":1714227300000,
"name":"百香果","price":7.8,"produceDate":1746552816000000}
所有数据解析完成!

所有数据解析完成!

上述表明,doReadAll()方法会处理全部的Sheet数据,且全部sheet的数据是同一个listener处理的,并且doAfterAllAnalysed()最终读取完毕的回调方法,3个sheet分别调用了3次(因为上述的excel文件还有一个空的Sheet3)。

2.3.2 读取部分sheet

准备excel文件中存在多个Sheet,多个Sheet结构不同(对应不同监听器),文件为test2.xlsx:

Sheet1:

在这里插入图片描述

Sheet2:

在这里插入图片描述

MerchantDto:

package com.xiaoxu.excel.multiSht;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

/**
 * @author xiaoxu
 * @date 2024-04-28 8:23
 * learn_java:com.xiaoxu.excel.multiSht.MerchantDto
 */
@Getter
@Setter
@EqualsAndHashCode
public class MerchantDto {

    @ExcelProperty("商户名称")
    private String merchantName;

    @ExcelProperty("商户库存")
    private long merchantInventory;

}

MerchantDtoListener:

package com.xiaoxu.excel.multiSht;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.fastjson.JSON;

/**
 * @author xiaoxu
 * @date 2024-04-28 8:25
 * learn_java:com.xiaoxu.excel.multiSht.MerchantDtoListener
 */
public class MerchantDtoListener implements ReadListener<MerchantDto> {
    @Override
    public void invoke(MerchantDto merchantDto, AnalysisContext analysisContext) {
        System.out.println("扫描商户数据:"
                + JSON.toJSONString(merchantDto));
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        System.out.println("商户所有数据解析完成!");
    }
}

TestExcelReadMultiSheet:

package com.xiaoxu.excel.multiSht;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.xiaoxu.excel.DeskTool;

/**
 * @author xiaoxu
 * @date 2024-04-27 21:56
 * learn_java:com.xiaoxu.excel.multiSht.TestExcelReadMultiSheet
 */
public class TestExcelReadMultiSheet {

    public static void main(String[] args) {
        String file = DeskTool.getPath("test2");

        // 一个文件一个reader
        try (ExcelReader excelReader = EasyExcel
                .read(file).build()) {
            //  可以注册同样的head和Listener 自己使用功能使用不同的Listener
            // 构建一个sheet 这里可以指定名字或者no
            ReadSheet readSheet = EasyExcel.readSheet(0)
                    .head(GoodsDtoNew.class)
                    .registerReadListener(new GoodsDtoNewListener())
                    .build();
            ReadSheet readSheet2 = EasyExcel.readSheet(1)
                    .head(MerchantDto.class)
                    .registerReadListener(new MerchantDtoListener())
                    .build();
            // 这里注意 一定要把sheet1 sheet2 一起传进去,
            // 不然有个问题就是03版的excel 会读取多次,浪费性能
            excelReader.read(readSheet, readSheet2);
        }
    }

}

执行结果:

扫描数据:{"count":45,"date":1709099280000,
"name":"苹果","price":1.65,"produceDate":1746545644800000}
扫描数据:{"count":13,"date":1713611400000,
"name":"梨子","price":2.4,"produceDate":1746563788800000}
扫描数据:{"count":0,"name":"菠萝","price":9.9}
所有数据解析完成!

扫描商户数据:{"merchantInventory":150,"merchantName":"天天水果"}
扫描商户数据:{"merchantInventory":500,"merchantName":"只爱水果"}
商户所有数据解析完成!

2.4 日期、数字或者自定义格式转换

test4.xlsx:

在这里插入图片描述

ConverterData:

package com.xiaoxu.excel.format;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.format.NumberFormat;
import lombok.Getter;
import lombok.Setter;

/**
 * @author xiaoxu
 * @date 2024-04-29 20:01
 * learn_java:com.xiaoxu.excel.format.ConverterData
 */
@Setter
@Getter
public class ConverterData {

    /**
     * 定义 转换器
     */
    @ExcelProperty(value = "信息",
            converter = CustomStringConverter.class)
    private String message;

    /**
     * 这里用string 去接日期才能格式化。接收年月日格式
     */
    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒SSS毫秒")
    @ExcelProperty("日期")
    private String date;

    /**
     * 接收百分比的数字
     */
    @NumberFormat("#.##%")
    @ExcelProperty("价格")
    private String price;

}

CustomStringConverter,日期、数字或者自定义格式转换:

package com.xiaoxu.excel.format;

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.converters.ReadConverterContext;
import com.alibaba.excel.converters.WriteConverterContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.data.WriteCellData;

/**
 * @author xiaoxu
 * @date 2024-04-29 20:05
 * learn_java:com.xiaoxu.excel.format.CustomStringConverter
 */
public class CustomStringConverter implements Converter<String> {

    @Override
    public Class<?> supportJavaTypeKey() {
        return String.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    /**
     * @param context
     * @return
     * @throws Exception 读的时候调用
     */
    @Override
    public String convertToJavaData(
            ReadConverterContext<?> context) 
            throws Exception {
        String str = context.getReadCellData().getStringValue();
        char[] chars = str.toCharArray();
        int left = 0;
        int right = chars.length - 1;
        while (left < right) {
            char temp = chars[left];
            chars[left] = chars[right];
            chars[right] = temp;
            left++;
            right--;
        }
        return new String(chars);
    }

    /**
     * @param context
     * @return
     * @throws Exception 写的时候调用,这里不涉及写
     */
    @Override
    public WriteCellData<?> convertToExcelData(
            WriteConverterContext<String> context)
            throws Exception {
        return new WriteCellData<>(context.getValue());
    }
}

TestExcelConverter:

public class TestExcelConverter {

    public static void main(String[] args) {
        String file = DeskTool.getPath("test4");
        EasyExcel.read(file, ConverterData.class,
                // 这里注意 我们也可以registerConverter来指定自定义转换器, 
                // 但是这个转换变成全局了,所有java为string,
                // excel为string的都会用这个转换器。
                // 如果就想单个字段使用请使用@ExcelProperty 指定converter
                // .registerConverter(new CustomStringStringConverter())
                new PageReadListener<ConverterData>
                        ((list) -> {
                            for (ConverterData c : list) {
                                System.out.println("读取到excel数据:" +
                                        JSON.toJSONString(c));
                            }
                        })).sheet().doRead();

    }
}

执行结果:

读取到excel数据:{"date":"2024年02月28日13时48分00秒000毫秒",
"message":"ebab好你","price":"980%"}
读取到excel数据:{"date":"2024年04月08日11时20分00秒000毫秒",
"message":"EBABEFAC","price":"570%"}

上述convertToJavaData对String类型数据,进行字符串反转。

上述字符串反转,还可以使用下面的方式,效果一致:

String str = context.getReadCellData().getStringValue();
char[] chars = str.toCharArray();
int left = 0;
int right = chars.length - 1;
while (left < right) {
    chars[left] ^= chars[right];
    chars[right] ^= chars[left];
    chars[left] ^= chars[right];
    left++;
    right--;
}
return new String(chars);

2.5 多行头

test5.xlsx:

在这里插入图片描述

TestMultiHead:

public class TestMultiHead {

    public static void main(String[] args) {
        String file = DeskTool.getPath("test5");
        EasyExcel.read(file, ConverterData.class,
                new PageReadListener<ConverterData>
                        ((list) -> {
                            for (ConverterData c : list) {
                                System.out.println("读取到excel数据:" +
                                        JSON.toJSONString(c));
                            }
                        })).sheet().doRead();
    }

}

执行结果:

读取到excel数据:{"date":"日期","message":"息信","price":"价格"}
读取到excel数据:{"date":"日期","message":"息信","price":"价格"}
读取到excel数据:{"date":"2024年02月28日13时48分00秒000毫秒",
"message":"ebab好你","price":"980%"}
读取到excel数据:{"date":"2024年04月08日11时20分00秒000毫秒",
"message":"EBABEFAC","price":"570%"}

默认就是如下的.headRowNumber(1),表头一般是1行:

String file = DeskTool.getPath("test5");
EasyExcel.read(file, ConverterData.class,
        new PageReadListener<ConverterData>
                ((list) -> {
                    for (ConverterData c : list) {
                        System.out.println("读取到excel数据:" +
                                JSON.toJSONString(c));
                    }
                })).sheet().headRowNumber(1).doRead();

如果上述有3行表头,我们希望都去掉,那么修改如下:

String file = DeskTool.getPath("test5");
EasyExcel.read(file, ConverterData.class,
        new PageReadListener<ConverterData>
                ((list) -> {
                    for (ConverterData c : list) {
                        System.out.println("读取到excel数据:" +
                                JSON.toJSONString(c));
                    }
                })).sheet().headRowNumber(3).doRead();

3行表头场景,希望都去掉表头,使用.headRowNumber(3)即可,执行结果如下:

读取到excel数据:{"date":"2024年02月28日13时48分00秒000毫秒",
"message":"ebab好你","price":"980%"}
读取到excel数据:{"date":"2024年04月08日11时20分00秒000毫秒",
"message":"EBABEFAC","price":"570%"}

2.6 同步的返回

test6.xlsx:

在这里插入图片描述

PeopleDto:

@Setter
@Getter
public class PeopleDto {

    @ExcelProperty("名称")
    private String name;

    @ExcelProperty("年龄")
    private int age;

}

TestSyncRead:

String file = DeskTool.getPath("test6");

//  synchronous,同步的返回,不推荐使用,
//  如果数据量大会把数据放到内存里面
List<PeopleDto> peopleDtos = EasyExcel.read(file).head(PeopleDto.class)
        .sheet().doReadSync();
for (PeopleDto p : peopleDtos) {
    System.out.println("读取到excel数据:" +
            JSON.toJSONString(p));
}
//  这里 也可以不指定class,返回一个list,
//  然后读取第一个sheet 同步读取会自动finish
List<Map<Integer, String>> listMap = EasyExcel.read(file).sheet().doReadSync();
for (Map<Integer, String> data : listMap) {
    // 返回每条数据的键值对 表示所在的列 和所在列的值
    System.out.println("读取到数据:" + JSON.toJSONString(data));
}

执行结果:

读取到excel数据:{"age":66,"name":"小徐"}
读取到excel数据:{"age":22,"name":"小李"}

读取到数据:{0:"小徐",1:"66"}
读取到数据:{0:"小李",1:"22"}

2.7 读取表头数据

监听器重写invokeHeadMap方法即可:

PeopleDtoListener监听器:

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ConverterUtils;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;

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

/**
 * @author xiaoxu
 * @date 2024-04-30
 * java_demo:com.xiaoxu.test.excel.sync.PeopleDtoListener
 */
public class PeopleDtoListener implements ReadListener<PeopleDto> {

    List<PeopleDto> peopleDtos = Lists.newArrayListWithCapacity(2);

    @Override
    public void invoke(PeopleDto peopleDto, AnalysisContext analysisContext) {
        peopleDtos.add(peopleDto);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {

    }

    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
        Map<Integer, String> head = ConverterUtils.convertToStringMap(headMap, context);
        System.out.println("读取到表头数据:" + JSON.toJSONString(head));
        // 如果想转成成 Map<Integer,String>
        // 方案1: 不要implements ReadListener 而是 extends AnalysisEventListener
        // 方案2: 调用 ConverterUtils.convertToStringMap(headMap, context) 自动会转换
    }
}

excel的head表头数据处理:

String file = DeskTool.getPath("test6");

//  synchronous,同步的返回,不推荐使用,
//  如果数据量大会把数据放到内存里面
//  表头数据读取      
List<PeopleDto> peopleDtos = EasyExcel.read(file).head(PeopleDto.class)
        .registerReadListener(new PeopleDtoListener())
        .sheet().doReadSync();
for (PeopleDto p : peopleDtos) {
    System.out.println("读取到excel数据:" +
            JSON.toJSONString(p));
}

执行结果:

读取到表头数据:{0:"名称",1:"年龄"}
读取到excel数据:{"age":66,"name":"小徐"}
读取到excel数据:{"age":22,"name":"小李"}

2.8 额外信息(批注、超链接、合并单元格信息读取)

since 2.0.0-beta1

在这里插入图片描述

对象:

@Getter
@Setter
@EqualsAndHashCode
public class DemoExtraData {

    private String row1;

    private String row2;
}

监听器:

@Slf4j
public class DemoExtraListener implements ReadListener<DemoExtraData> {

    @Override
    public void invoke(DemoExtraData data, AnalysisContext context) {}

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {}

    @Override
    public void extra(CellExtra extra, AnalysisContext context) {
        log.info("读取到了一条额外信息:{}", JSON.toJSONString(extra));
        switch (extra.getType()) {
            case COMMENT:
                log.info("额外信息是批注,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(), extra.getColumnIndex(),
                    extra.getText());
                break;
            case HYPERLINK:
                if ("Sheet1!A1".equals(extra.getText())) {
                    log.info("额外信息是超链接,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(),
                        extra.getColumnIndex(), extra.getText());
                } else if ("Sheet2!A1".equals(extra.getText())) {
                    log.info(
                        "额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{},"
                            + "内容是:{}",
                        extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
                        extra.getLastColumnIndex(), extra.getText());
                } else {
                    Assert.fail("Unknown hyperlink!");
                }
                break;
            case MERGE:
                log.info(
                    "额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{}",
                    extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
                    extra.getLastColumnIndex());
                break;
            default:
        }
    }
}
/**
 * 额外信息(批注、超链接、合并单元格信息读取)
 * <p>
 * 由于是流式读取,没法在读取到单元格数据的时候直接读取到额外信息,所以只能最后通知哪些单元格有哪些额外信息
 *
 * <p>
 * 1. 创建excel对应的实体对象 参照{@link DemoExtraData}
 * <p>
 * 2. 由于默认异步读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoExtraListener}
 * <p>
 * 3. 直接读即可
 *
 * @since 2.2.0-beat1
 */
@Test
public void extraRead() {
    String fileName = TestFileUtil.getPath() + "demo" + File.separator + "extra.xlsx";
    // 这里 需要指定读用哪个class去读,然后读取第一个sheet
    EasyExcel.read(fileName, DemoExtraData.class, new DemoExtraListener())
        // 需要读取批注 默认不读取
        .extraRead(CellExtraTypeEnum.COMMENT)
        // 需要读取超链接 默认不读取
        .extraRead(CellExtraTypeEnum.HYPERLINK)
        // 需要读取合并单元格信息 默认不读取
        .extraRead(CellExtraTypeEnum.MERGE).sheet().doRead();
}

test7.xlsx:

在这里插入图片描述

ConverterData:

@Setter
@Getter
public class ConverterData {

    /**
     * 定义 转换器
     */
    @ExcelProperty(value = "信息",
            converter = CustomStringConverter.class)
    private String message;

    /**
     * 这里用string 去接日期才能格式化。接收年月日格式
     */
    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒SSS毫秒")
    @ExcelProperty("日期")
    private String date;

    /**
     * 接收百分比的数字
     */
    @NumberFormat("#.##%")
    @ExcelProperty("价格")
    private String price;

}

MergeDataListener:

package com.xiaoxu.excel.merge;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.metadata.CellExtra;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.fastjson.JSON;
import com.xiaoxu.excel.format.ConverterData;

import java.text.MessageFormat;

/**
 * @author xiaoxu
 * @date 2024-04-30 22:13
 * learn_java:com.xiaoxu.excel.merge.MergeDataListener
 */
public class MergeDataListener implements ReadListener<ConverterData> {
    @Override
    public void invoke(ConverterData converterData, AnalysisContext analysisContext) {
        System.out.println("读取正常数据:" +
                JSON.toJSONString(converterData));
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {

    }

    @Override
    public void extra(CellExtra extra, AnalysisContext context) {
        System.out.println("读取额外数据:" +
                JSON.toJSONString(extra));
        switch (extra.getType()) {
            case MERGE:
                System.out.println(MessageFormat
                        .format("额外信息是合并单元格," +
                                        "覆盖区间:firstRowIndex:{0}," +
                                        "firstColumnIndex:{1}," +
                                        "lastRowIndex:{2},lastColumnIndex:{3}.",
                                extra.getFirstRowIndex(),
                                extra.getFirstColumnIndex(),
                                extra.getLastRowIndex(),
                                extra.getLastColumnIndex()));
        }
    }
}

TestMerge:

// 这种写法只会输出正常读取的数据,而不会输出合并的单元格等数据
// 批注、超链接、合并单元格默认不读取
//        EasyExcel.read(file).head(ConverterData.class)
//                .registerReadListener(new MergeDataListener())
//                .sheet().headRowNumber(1).doRead();
EasyExcel.read(file).head(ConverterData.class)
        .registerReadListener(new MergeDataListener())
        .extraRead(CellExtraTypeEnum.MERGE)
        .sheet().headRowNumber(1).doRead();

执行结果:

读取正常数据:{"date":"2024年02月28日13时48分00秒000毫秒",
"message":"ebab好你","price":"980%"}
读取正常数据:{"date":"2024年04月08日11时20分00秒000毫秒",
"message":"EBABEFAC","price":"570%"}

读取额外数据:{"columnIndex":0,"firstColumnIndex":0,
"firstRowIndex":2,"lastColumnIndex":0,
"lastRowIndex":3,"rowIndex":2,"type":"MERGE"}
额外信息是合并单元格,覆盖区间:firstRowIndex:2,firstColumnIndex:0,
lastRowIndex:3,lastColumnIndex:0.

2.9 读取公式和单元格类型

test8.xlsx:

在这里插入图片描述

其中金额是公式生成的,是单价*数量计算出来的。

公式操作如下,在第一列数据的金额列下,输入=号,然后选中苹果的单价列,输入*表示乘,再选中苹果的数量列,按下enter即公式配置成功,下拉苹果列的金额,即可自动填充后续列的公式:

在这里插入图片描述

CellDataReadData:

package com.xiaoxu.excel.formula;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.metadata.data.CellData;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;

/**
 * @author xiaoxu
 * @date 2024-05-01 10:22
 * learn_java:com.xiaoxu.excel.formula.CellDataReadData
 */
@Getter
@Setter
@EqualsAndHashCode
public class CellDataReadData {
    @ExcelProperty("品名")
    private CellData<String> name;
    @ExcelProperty("单价")
    private CellData<Long> price;
    @ExcelProperty("数量")
    private CellData<Long> number;
    @ExcelProperty("金额")
    private CellData<Long> money;
    @ExcelProperty("日期")
    private CellData<Date> date;
}

CellDataListener:

public class CellDataListener implements ReadListener<CellDataReadData> {
    @Override
    public void invoke(CellDataReadData cellDataReadData, AnalysisContext analysisContext) {
        System.out.println("读取正常数据:" +
                JSON.toJSONString(cellDataReadData, true));
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {

    }
}

TestCellData:

String file = DeskTool.getPath("test8");
EasyExcel.read(file).head(CellDataReadData.class)
        .registerReadListener(new CellDataListener())
        .extraRead(CellExtraTypeEnum.MERGE)
        .sheet().headRowNumber(1).doRead();

执行结果:

读取正常数据:{
	"date":{
		"data":1709099280000,
		"dataFormatData":{
			"format":"yyyy-m-d h:mm",
			"index":22
		},
		"numberValue":45350.575,
		"originalNumberValue":45350.575,
		"type":"NUMBER"
	},
	"money":{
		"data":24,
		"dataFormatData":{
			"format":"General",
			"index":0
		},
		"formulaData":{
			"formulaValue":"B2*C2"
		},
		"numberValue":24,
		"originalNumberValue":24,
		"type":"NUMBER"
	},
	"name":{
		"data":"苹果",
		"dataFormatData":{
			"format":"General",
			"index":0
		},
		"stringValue":"苹果",
		"type":"STRING"
	},
	"number":{
		"data":12,
		"dataFormatData":{
			"format":"General",
			"index":0
		},
		"numberValue":12,
		"originalNumberValue":12,
		"type":"NUMBER"
	},
	"price":{
		"data":2,
		"dataFormatData":{
			"format":"General",
			"index":0
		},
		"numberValue":2,
		"originalNumberValue":2,
		"type":"NUMBER"
	}
}
读取正常数据:{
	"date":{
		"data":1712546400000,
		"dataFormatData":{
			"format":"yyyy-m-d h:mm",
			"index":22
		},
		"numberValue":45390.4722222222,
		"originalNumberValue":45390.4722222222,
		"type":"NUMBER"
	},
	"money":{
		"data":57,
		"dataFormatData":{
			"format":"General",
			"index":0
		},
		"formulaData":{
			"formulaValue":"B3*C3"
		},
		"numberValue":57,
		"originalNumberValue":57,
		"type":"NUMBER"
	},
	"name":{
		"data":"西瓜",
		"dataFormatData":{
			"format":"General",
			"index":0
		},
		"stringValue":"西瓜",
		"type":"STRING"
	},
	"number":{
		"data":19,
		"dataFormatData":{
			"format":"General",
			"index":0
		},
		"numberValue":19,
		"originalNumberValue":19,
		"type":"NUMBER"
	},
	"price":{
		"data":3,
		"dataFormatData":{
			"format":"General",
			"index":0
		},
		"numberValue":3,
		"originalNumberValue":3,
		"type":"NUMBER"
	}
}
读取正常数据:{
	"date":{
		"data":1714533600000,
		"dataFormatData":{
			"format":"yyyy-m-d h:mm",
			"index":22
		},
		"numberValue":45413.4722222222,
		"originalNumberValue":45413.4722222222,
		"type":"NUMBER"
	},
	"money":{
		"data":84,
		"dataFormatData":{
			"format":"General",
			"index":0
		},
		"formulaData":{
			"formulaValue":"B4*C4"
		},
		"numberValue":84,
		"originalNumberValue":84,
		"type":"NUMBER"
	},
	"name":{
		"data":"土豆",
		"dataFormatData":{
			"format":"General",
			"index":0
		},
		"stringValue":"土豆",
		"type":"STRING"
	},
	"number":{
		"data":21,
		"dataFormatData":{
			"format":"General",
			"index":0
		},
		"numberValue":21,
		"originalNumberValue":21,
		"type":"NUMBER"
	},
	"price":{
		"data":4,
		"dataFormatData":{
			"format":"General",
			"index":0
		},
		"numberValue":4,
		"originalNumberValue":4,
		"type":"NUMBER"
	}
}

2.10 数据转换等异常处理

test9.xlsx:

在这里插入图片描述

数据转换等异常处理,在监听器中重写onException方法即可:

ErrorListener,在转换异常 获取其他异常下会调用本接口,抛出异常则停止读取,如果这里不抛出异常则继续读取下一行:

package com.xiaoxu.excel.exp;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.fastjson.JSON;

/**
 * @author xiaoxu
 * @date 2024-05-01 11:07
 * learn_java:com.xiaoxu.excel.exp.ErrorListener
 */
public class ErrorListener implements ReadListener<ErrorObj> {
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        System.out.println("解析失败,继续下一行数据解析:"
                + exception.getMessage());
        if (exception instanceof ExcelDataConvertException) {
            ExcelDataConvertException exp
                    = (ExcelDataConvertException) exception;
            System.out.println(String.format("格式转换异常," +
                            "第%s行,第%s列解析异常,数据为:%s.",
                    exp.getRowIndex(), exp.getColumnIndex(),
                    exp.getCellData().getStringValue()));
        }
    }

    @Override
    public void invoke(ErrorObj errorObj, AnalysisContext analysisContext) {
        System.out.println("读取数据:" +
                JSON.toJSONString(errorObj, true));
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {

    }
}

ErrorObj:

@Setter
@Getter
public class ErrorObj {
    @ExcelProperty("数目")
    private long count;
}

TestErrorObj:

String file = DeskTool.getPath("test9");
EasyExcel.read(file).head(ErrorObj.class)
        .registerReadListener(new ErrorListener())
        .sheet().headRowNumber(1).doRead();

执行结果:

解析失败,继续下一行数据解析:
Convert data com.alibaba.excel.metadata.data.ReadCellData@dc8f2848 to long error 
格式转换异常,1,0列解析异常,数据为:苹果.
读取数据:{
	"count":120
}

2.11 不创建对象的读

不创建对象的场景下读取数据,可以使用Map<Integer, String>来存储读取的数据:

@Slf4j
public class NoModelDataListener extends AnalysisEventListener<Map<Integer, String>> {
    /**
     * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 5;
    private List<Map<Integer, String>> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSON.toJSONString(data));
        cachedDataList.add(data);
        if (cachedDataList.size() >= BATCH_COUNT) {
            saveData();
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        saveData();
        log.info("所有数据解析完成!");
    }

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

不创建对象读取:

/**
 * 不创建对象的读
 */
@Test
public void noModelRead() {
    String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
    // 这里 只要,然后读取第一个sheet 同步读取会自动finish
    EasyExcel.read(fileName, new NoModelDataListener()).sheet().doRead();
}

2.12 web中的读

参考EasyExcel的读取文件方法,可以传入InputStream进行读取,那么可以结合Spring的web读取excel数据:

public static ExcelReaderBuilder read(InputStream inputStream, Class head, ReadListener readListener) {
    ExcelReaderBuilder excelReaderBuilder = new ExcelReaderBuilder();
    excelReaderBuilder.file(inputStream);
    if (head != null) {
        excelReaderBuilder.head(head);
    }

    if (readListener != null) {
        excelReaderBuilder.registerReadListener(readListener);
    }

    return excelReaderBuilder;
}

web读取如下:

/**
 * 文件上传
 * <p>
 * 1. 创建excel对应的实体对象 参照{@link UploadData}
 * <p>
 * 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UploadDataListener}
 * <p>
 * 3. 直接读即可
 */
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
    EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener(uploadDAO)).sheet().doRead();
    return "success";
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/587249.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

24.什么是跨域?解决方案有哪些?

为什么会出现跨域问题 存在浏览器同源策略&#xff0c;所以才会有跨域问题。那么浏览器是出于何种原因会有跨域的限制呢。其实不难想到&#xff0c;跨域限制主要的目的就是为了用户的上网安全。 同源策略导致的跨域是浏览器单方面拒绝响应数据&#xff0c;服务器端是处理完毕…

【C++】学习笔记——string_3

文章目录 六、string类5. string类的操作6. string类的转换7. string类的模拟实现 未完待续 搭配文档食用 六、string类 5. string类的操作 上面的函数中&#xff0c;有些是不常用的&#xff0c;咱们只挑几个重要的进行讲解。 c_str 就是将字符串转换成 C语言 字符串的格式。…

(附源码)超级简单的SSM图书交易系统,包含技术栈、架构图、设计图、教程

先看首页效果&#xff0c;包含买家、卖家、管理员三个端口。启动有问题可以联系我解决&#xff0c;微信&#xff1a;keepgoing4u 架构图&#xff1a; 用到软件 Jdk1.8 Mysql IntelliJ IDEA Maven 项目技术&#xff1a; Spring Boot SSM JSP mybatis Maven B/S模式 配置…

区块链技术:DAPP开发

随着科技的飞速发展&#xff0c;区块链技术逐渐渗透到各个领域&#xff0c;其中DAPP&#xff08;去中心化应用&#xff09;的发展尤为引人注目。作为一种新型的应用程序&#xff0c;DAPP正在重塑未来商业生态&#xff0c;其潜力无可估量。 一、DAPP的定义和特点 DAPP是指基于…

CSS高级选择器

一、属性选择器 以value开头的att属性的E元素&#xff1a;E[att^"value"]{ ;} a[href^http]{background-color"red";} css a[href^http]{background-color"red"; } html <!DOCTYPE html> <html lang"en"> <head&…

运营版游戏陪玩平台源码/tt语音聊天/声优服务/陪玩系统源码开黑/约玩源码

简述 一个人的游戏叫孤独&#xff0c;一群人的游戏才是乐趣&#xff0c;随着电竞产业在国内的快速发展&#xff0c;游戏陪练行业也迅速成长&#xff0c;现在很多游戏玩家为了追求更高质量的游戏体验感&#xff0c;往往会在玩游戏的过程中找陪练&#xff0c;通过陪玩系统进行预…

前端开发工程师——Vue

Vue学习笔记&#xff08;尚硅谷天禹老师&#xff09;_尚硅谷天禹老师vue2021讲课笔记下载-CSDN博客 模板语法 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" co…

Springboot+Vue+小程序+基于微信小程序电影票网购系统

Java电影票购买管理系统&#xff0c;Maven管理工具&#xff0c;MyBatis数据库操作&#xff0c;idea平台开发&#xff0c;后台的前端为Vue&#xff0c;前台客户端为小程序&#xff0c;功能丰富&#xff0c;还有电影周边购买功能&#xff0c;请在最下方二维码处联系我即可&#x…

如何判断第三方软件测试公司是否具有资质

在软件开发的过程中&#xff0c;软件测试是确保软件质量、稳定性和用户体验的关键环节。许多企业选择将软件测试工作交给专业的第三方软件测试公司来完成&#xff0c;以确保测试的准确性和公正性。但是&#xff0c;如何判断一个第三方软件测试公司是否具有资质呢&#xff1f;以…

【软件开发规范篇】JAVA后端开发编码格式规范

作者介绍&#xff1a;本人笔名姑苏老陈&#xff0c;从事JAVA开发工作十多年了&#xff0c;带过大学刚毕业的实习生&#xff0c;也带过技术团队。最近有个朋友的表弟&#xff0c;马上要大学毕业了&#xff0c;想从事JAVA开发工作&#xff0c;但不知道从何处入手。于是&#xff0…

基于FPGA的数字信号处理(7)--如何确定Verilog表达式的位宽

一般规则 很多时候&#xff0c;Verilog中表达式的位宽都是被隐式确定的&#xff0c;即使你自己设计了位宽&#xff0c;它也是根据规则先确定位宽后&#xff0c;再扩展到你的设计位宽&#xff0c;这常常会导致结果产生意想不到的错误。比如&#xff1a; timescale 1ns/1ns mod…

GD32F470内存不能设置512KB

最近研究了下GD32F470芯片手册&#xff0c;发现标称的内存512KB&#xff0c; 其实是连TCMSRAM一起算的&#xff0c;即TCMSRAMSRAM0SRAM1SRAM2ADDSRAM 512KB, 其中SRAM0 - ADDSRAM 这段内存地址是连续的&#xff0c;从地址0x20000000开始。而TCMSRAM地址却是从0x10000000 - 0x1…

新建stm32工程模板步骤

1.先使用keil新建一个project的基本代码 2.stm32启动文件添加 将stm32的启动文件&#xff0c;在原工程当中新建一个Start文件夹把相关的启动文件放到文件夹当中 然后还需要找到下面三个文件 stm32f10x.h是stm32的外设寄存器的声明和定义&#xff0c;后面那两个文件用于配置系…

Unity 实现新手引导遮罩

Unity 复写OnPopulateMesh 实现新手引导遮罩、包含点击事件触发区域判断 https://download.csdn.net/download/shenliang34/89247117

git 第一次安装设置用户名密码

git config --global user.name ljq git config --global user.email 15137659164qq.com创建公钥命令 输入后一直回车 ssh-keygen -t rsa下面这样代表成功 这里是公钥的 信息输入gitee 中 输入下面命令看是否和本机绑定成功 ssh -T gitgitee.com如何是这样&#xff0c;恭喜…

【算法系列】哈希表

目录 哈希表总结 leetcode题目 一、两数之和 二、判定是否互为字符重排 三、存在重复元素 四、存在重复元素 II 五、字母异位词分组 六、在长度2N的数组中找出重复N次的元素 七、两个数组的交集 八、两个数组的交集 II 九、两句话中的不常见单词 哈希表总结 1.存储数…

HC-SR04超声波测距

什么是超声波 超声波是频率高于20000赫兹的声波。 超声波是一种特定频率范围内的声波&#xff0c;其特点在于频率高于人耳能够感知的上限&#xff0c;即超过20千赫兹&#xff08;Hz&#xff09;。这种高频率的声波具有一些独特的性质&#xff1a; 方向性好&#xff1a;超声波…

托普利兹矩阵(T矩阵)及其应用(Matlab demo测试)

托普利兹矩阵&#xff08;T矩阵&#xff09;及其应用&#xff08;Matlab demo测试&#xff09; 1. 概念2. Matlab简单测试2.1 生成测试2.2 基本性质及原理2.3 性质验证 3. 其他应用总结3.1 其他性质3.2 文献阅读看到的 参考资料 1. 概念 托普利兹矩阵&#xff0c;简称为T型矩阵…

可视化大屏在真实场景的效果,绝对震撼,不要再嘲笑其作用了

hello&#xff0c;我是大千UI工场&#xff0c;本地带来一批可视化大屏现场效果图&#xff0c;非常震撼&#xff0c;给大家带来身临其境的感受&#xff0c;欢迎关注点赞&#xff0c;有需求请私信。 有人可能会认为可视化大屏没有太多价值&#xff0c;可能是因为以下几个原因&am…

npm 安装 pnpm 时 报错 npm ERR! Unexpected token ‘.‘

问题 一个项目用的是 pnpm 安装的依赖&#xff0c;node 的版本是 16.16.0&#xff0c;nvm 的版本是 1.1.7&#xff0c;然后全局安装 pnpm 报错如下&#xff1a; 解决 我看网上的一些解决方案是说 nvm 版本过低导致&#xff0c;下面我们按照这个方向处理。 实首先下载 nvm-up…