基于Easyexcel实现不同场景的导出功能

基于Easyexcel实现不同场景的导出功能

导出的数据包含有图片

导出excel表格的数据包含有图片,这种场景比较少。通Easyexcel实现这样的需求,我认为最简便的方法就是使用前面提到的自定义转换器(com.alibaba.excel.converters.Converter);假如有这样一个场景,导出员工的信息,还要包括员工的一寸照。通常情况下,数据库并不会真的存一张图片,而是图片存储位置的相对路径。

1、新建一个类EmpHeadPhotoConverter,实现com.alibaba.excel.converters.Converter接口,并重写convertToExcelData()方法;

2、在重写convertToExcelData()方法中,根据图片的相同路径或网络地址读取出图片的字节流作为构建WriteCellData对象的参数然后返回;

3、在员工信息的实体类Employee的照片列引入自定义的类型转换器(EmpHeadPhotoConverter)

4、使用EasyExcel的工厂方法把数据写出到excel表格中;

注:这里是使用默认的样式,所以看起来可能点丑;@ContentRowHeight()用于调节数据行的高度;@ColumnWidth用于调节数据列的宽度;

java 复制代码
public class EmpHeadPhotoConverter implements Converter<String> {
    @Override
    public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        //根据图片的相对路径或网络地址读取图片到byte输出流(ByteArrayOutputStream)
        ByteArrayOutputStream byteArrayOutputStream = null;
        InputStream inputStream=null;
        try {
             inputStream = ClassLoader.getSystemResourceAsStream(value);
            byteArrayOutputStream = new ByteArrayOutputStream();
            int len=-1;
            byte[] bytes = new byte[1024];
            while ((len=inputStream.read(bytes))!=-1){
                byteArrayOutputStream.write(bytes);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (byteArrayOutputStream != null) {
                byteArrayOutputStream.close();
            }
            if (inputStream != null) {
                inputStream.close();
            }
        }
        //把输出流中的图片字节数组作为参数构建writeCellData对象并返回
        //WriteCellData是EasyExcel对单元格数据的封装
        return  new WriteCellData<>(byteArrayOutputStream.toByteArray());
    }
}
java 复制代码
@Data
@ContentRowHeight(50)//数据行的高度
public class Employee implements Serializable {
    @ExcelProperty("姓名")
    private String realName;
    @ExcelProperty("员工编号")
    private String empNo;
    @ExcelProperty("性别")
    private String sex;
    @ExcelProperty("家庭地址")
    private String address;
    @ExcelProperty("联系电话")
    private String phone;
    @ExcelProperty("电子邮箱")
    private String email;
    @ExcelProperty(value = "照片",converter = EmpHeadPhotoConverter.class)
    @ColumnWidth(10)//列宽
    private String headPhoto;
}
java 复制代码
@Test
public void writeImage(){
    String exportPath=this.getExportPath();
    String exportFile=exportPath+File.separator+"员工基本信息(包含照片).xlsx";
    List<Employee> list = new ArrayList<>();
    for (int i = 0; i < 3; i++) {
        Employee employee = new Employee();
        employee.setRealName("张三"+i);
        employee.setEmpNo("CH"+(i+1));
        employee.setAddress("北京");
        employee.setEmpNo("zhangsan@163.com");
        employee.setSex("女");
        employee.setPhone("17777xxxxxx");
        //这里是示例,因为我把示例图片放在了classpath下,后面可以根据图片名字在classpath下找到
        //在实际的业务开发过程中,这里应该是图片的相对路径或网络地址
        employee.setHeadPhoto("zhangsan.jpeg");
        list.add(employee);
    }
    EasyExcel.write(exportFile,Employee.class).sheet().doWrite(list);
}

导出表格指定列宽、行高

上面其实是已经说过了,通过注解可以简单的指定导出Excel表格的行、列的高度和宽度:

@ContentRowHeight()用于指定数据行的高度;

@HeadRowHeight()用于指定表头行的高度;

@ColumnWidth()用于指定数据列的宽度;

导出表格自定义样式

我比较喜欢使用easyexcel的一个很重要的原因就是,easyexcel在poi的基础上,封装的比较友好。就比如,在导出的时候,很多情况下需要自定义表格的样式,easyexcel就提供了多种的实现方式。主要有三种:

1、通过注解;

2、编程式;

3、自定义类型转换器。

不同的方式,侧重的场景也有所不同,下面通过示例梳理一下使用方法:

编程式自定义样式

通过编程式来自定义导出表格的样式中,有一个非常关键类HorizontalCellStyleStrategy。

1、通过HorizontalCellStyleStrategy可以配置好表头的样式和数据行的样式;

2、使用Easyexcel的工厂方法写出数据前,把通过HorizontalCellStyleStrategy构建好的样式策略注册到表格写出构建器里;

3、使用使用Easyexcel的工厂方法写出数据;

总结:这种方法的优点就是比较简单,且容易理解构建样式的过程;缺点就是不太灵活;比较适合那些导出表格前可以明确知道导出的表格的样式特点;

java 复制代码
@Test
public void writeWithStyle2() {
    String exportPath = this.getExportPath();
    String exportFile = exportPath + File.separator + "员工基本信息v2.xlsx";
    List<Employee> list = new ArrayList<>();
    Employee employee = new Employee();
    employee.setRealName("张三");
    employee.setEmpNo("CH001");
    employee.setSex("女");
    Employee employee2 = new Employee();
    employee2.setRealName("李四");
    employee2.setEmpNo("CH002");
    employee2.setSex("男");
    list.add(employee);
    list.add(employee2);
    // 头的策略
    WriteCellStyle headWriteCellStyle = new WriteCellStyle();
    // 背景设置为红色
    headWriteCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
    WriteFont headWriteFont = new WriteFont();
    headWriteFont.setFontHeightInPoints((short)20);
    headWriteCellStyle.setWriteFont(headWriteFont);
    // 内容的策略
    WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
    // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定
    contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
    // 背景绿色
    contentWriteCellStyle.setFillForegroundColor(IndexedColors.GREEN.getIndex());
    WriteFont contentWriteFont = new WriteFont();
    // 字体大小
    contentWriteFont.setFontHeightInPoints((short)20);
    contentWriteCellStyle.setWriteFont(contentWriteFont);
    // 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
    HorizontalCellStyleStrategy horizontalCellStyleStrategy =
            new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);

    // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
    EasyExcel.write(exportFile, Employee.class)
            .registerWriteHandler(horizontalCellStyleStrategy)
            .sheet()
            .doWrite(list);
}

注解形式自定义样式

通过注解的形式来自定义导出表格的样式,和编程式的比较类似,明显的区别是一个需要通过在编写代码来定义样式并应用这个样式;另一个是需要 使用注解定义好表格样式,应用样式的过程easyexcel内部已经实现了,不用自己编写代码来实现。总体上特点是一样的:使用简单,且比较好理解,缺点就是不灵活,不能动态的设置导出表格内单元格的样式。比较常用的注解有以下(有的是作用在类上,有的是作用在属性上,注解需要设置哪些属性可以点以注解的源里一看就很清楚):

作用在类上:

java 复制代码
@HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 10)
// 头字体设置成20
@HeadFontStyle(fontHeightInPoints = 20)
// 内容的背景设置成绿色 IndexedColors.GREEN.getIndex()
@ContentStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 17)
// 内容字体设置成20
@ContentFontStyle(fontHeightInPoints = 20)

作用在属性上

java 复制代码
// 字符串的头背景设置成粉红 IndexedColors.PINK.getIndex()
@HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 14)
// 字符串的头字体设置成20
@HeadFontStyle(fontHeightInPoints = 30)
// 字符串的内容的背景设置成天蓝 IndexedColors.SKY_BLUE.getIndex()
@ContentStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 40)
// 字符串的内容字体设置成20
@ContentFontStyle(fontHeightInPoints = 30)

自定义类型转换器形式定义样式

前两种的样式定义方法,都比较固定,不能根据导出数据的规则来动态设置表格内单元格的样式,如导出员工信息表格里如果性别是女的员工数据行,则使用红色字体,如果是男性,则是使用蓝色;因为导出数据通常是从数据源里动态取出的,在导出前没办法确定哪些行是男性,哪些员工是女性,所以需要动态的设置导出表格的样式,自定义类型转换器就可以很好的解决这个问题。

1、新建一个类SexColourConverter,实现com.alibaba.excel.converters.Converter接口,并重写convertToExcelData()方法;

2、重写convertToExcelData()方法中,根据单元格的内容设置不同的样式,示例中:如果性别是男,则字体为蓝色;如果性别是女,则字体颜色是红色;

3、在员工信息的实体类Employee的性别(sex)属性上,通过@ExcelProperty()引入自定义的类型转换器SexColourConverter;

4、使用Easyexcel的工厂方法实现表格数据的写出;

总结:这种方法很灵活,非常适合一些报表中,要求导出表格中数据达到在不同规则就要求有不同的样式的场景。在我做过的项目里,有不少导出都有类似的规则:某个属性达到预警阈值时,要导出数据的单元格格式标成红色。

java 复制代码
public class SexColourConverter implements Converter<String> {
    @Override
    public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        WriteCellStyle writeCellStyle = new WriteCellStyle();
        WriteFont writeFont = new WriteFont();
        if ("男".equals(value)) {
            writeFont.setColor((short) 12);
        }
        if ("女".equals(value)) {
            writeFont.setColor((short) 10);
        }
        writeCellStyle.setWriteFont(writeFont);
        WriteCellData<Object> cellData = new WriteCellData<>();
        cellData.setWriteCellStyle(writeCellStyle);
        cellData.setType(CellDataTypeEnum.STRING);
        cellData.setStringValue(value);
        return cellData;
    }
}
java 复制代码
@Data
public class Employee implements Serializable {
    @ExcelProperty("姓名")
    private String realName;
    @ExcelProperty("员工编号")
    private String empNo;
    @ExcelProperty(value = "性别",converter = SexColourConverter.class)
    private String sex;
}
@Test
public void writeWithStyle() {
    String exportPath = this.getExportPath();
    String exportFile = exportPath + File.separator + "员工基本信息v2.xlsx";
    List<Employee> list = new ArrayList<>();
    Employee employee = new Employee();
    employee.setRealName("张三");
    employee.setEmpNo("CH001");
    employee.setSex("女");
    Employee employee2 = new Employee();
    employee2.setRealName("李四");
    employee2.setEmpNo("CH002");
    employee2.setSex("男");
    list.add(employee);
    list.add(employee2);
    EasyExcel.write(exportFile, Employee.class).sheet().doWrite(list);
}

在设置单元格背景或字体的颜色的时候,颜色会对应一个short类型的数据

复杂表头的导出

特别是一些财务类型的报表导出,表头往往是复合表头,比较复杂

easyexcel对于这种场景提供了两种方法,一种是注解;另外一种是通过一种特殊的数据结构List<list>;根据我的感受,我是推荐使用注解的;

第一种:

如果是所在列是复合表头,则使用@ExcelProperty()注解,从上到下标明表头组成;如果是普通表头,按普通的用法标明表头名称即可;是不是很简单?

java 复制代码
@Data
public class EmpSalary  {
    @ExcelProperty({"基本信息","姓名"})
    private String realName;
    @ExcelProperty({"基本信息","员工编号"})
    private String empNo;
    //,converter = SalaryDateConverter.class
    @ExcelProperty(value = "工资日期")
    private String salaryDate;
    @ExcelProperty({"工资构成","基本工资"})
    private Float baseAmount;
    @ExcelProperty({"工资构成","全勤奖"})
    private Float fullAttendAmount;
    @ExcelProperty({"工资构成","五险一金"})
    private Float insurance;
    //特别资金
    @ExcelIgnore
    private Float specialAmount;
}
java 复制代码
@Test
public void writeHead() {
    String exportPath = this.getExportPath();
    String exportFile = exportPath + File.separator + "员工工资表v3.xlsx";
    EasyExcel.write(exportFile, EmpSalary.class).sheet().doWrite(this.empSalaryData(10));
}

第二种:

使用List<list>结构来组织表头数据,开始的时候还不是很理解表头的数据结构为什么这么奇怪,到这里是不是明白了。对于普通表头List肯定就可以了,但是在一些复杂的场景就不行了。这么制定的话,什么场景都不在话下,关键还可以动态生成表头,这一点确实比注解的方式要灵活一些;所以具体使用哪种,要根据业务场景决定了。

java 复制代码
@Test
public void writeHead() {
    String exportPath = this.getExportPath();
    String exportFile = exportPath + File.separator + "员工工资表v4.xlsx";
    List<List<String>> headList = new ArrayList<>();
    List<String> head1 = new ArrayList<>();
    head1.add("基本信息");
    head1.add("姓名");
    headList.add(head1);
    List<String> head2 = new ArrayList<>();
    head2.add("基本信息");
    head2.add("员工编号");
    headList.add(head2);
    List<String> head3 = new ArrayList<>();
    head3.add("工资日期");
    headList.add(head3);
    List<String> head4 = new ArrayList<>();
    head4.add("工资构成");
    head4.add("基本工资");
    headList.add(head4);
    List<String> head5 = new ArrayList<>();
    head5.add("工资构成");
    head5.add("全勤奖");
    headList.add(head5);
    List<String> head6 = new ArrayList<>();
    head6.add("工资构成");
    head6.add("保险");
    headList.add(head6);
    EasyExcel.write(exportFile).head(headList).sheet().doWrite(this.empSalaryData(10));
}

日期、数字、自定义格式的导出

自定义格式的导出可以参考上一篇《Spring Boot:基于Easyexcel实现导入功能》中的日期、数字及其他自定义格式的转换部分,SalaryDateConverter#convertToExcelData(),导出时候的数据格式转换逻辑可以写在这里面;SalaryDateConverter#convertToJavaData()导入时候的数据格式转换的实现逻辑可以写在这里;SalaryDateConverter是com.alibaba.excel.converters.Converter的实现类;

除了实现com.alibaba.excel.converters.Converterr接口,easyexcel也预置了一些常用的注解来实现格式转换,导入导出的时候都能用,如@DateTimeFormat、@NumberFormat;

这里特别注意别导错类了:

com.alibaba.excel.annotation.format.DateTimeFormat;

com.alibaba.excel.annotation.format.NumberFormat;

java 复制代码
@Data
public class EmpSalary  {
    @ExcelProperty({"基本信息","姓名"})
    private String realName;
    @ExcelProperty({"基本信息","员工编号"})
    private String empNo;
    @DateTimeFormat("yyyy年MM月")
    @ExcelProperty(value = "工资日期")
    private Date salaryDate;
    @ExcelProperty({"工资构成","基本工资"})
    private Float baseAmount;
    @ExcelProperty({"工资构成","全勤奖"})
    private Float fullAttendAmount;
    @ExcelProperty({"工资构成","五险一金"})
    private Float insurance;
    //特别资金
    @ExcelIgnore
    @NumberFormat
    private Float specialAmount;
    @NumberFormat("#.##%")
    @ExcelProperty("绩效完成百分比")
    private Double jixiao;
}
java 复制代码
@Test
public void writeByConverter(){
    String exportPath = this.getExportPath();
    String exportFile = exportPath + File.separator + "员工工资表v5.xlsx";
    List<EmpSalary> list=new ArrayList<>();
    EmpSalary empSalary = new EmpSalary();
    empSalary.setEmpNo("CH" + ( 1));
    empSalary.setRealName("张三" + ( 1));
    empSalary.setSalaryDate(new Date());
    empSalary.setBaseAmount(5000f);
    empSalary.setFullAttendAmount(500f);
    empSalary.setInsurance(300f);
    empSalary.setJixiao(0.9877);
    list.add(empSalary);
    EasyExcel.write(exportFile, EmpSalary.class).sheet("12月").doWrite(list);
}

读取全部的sheet页

这里需要注意两个地方:1、读取shee页的数据结构是一样的;2、excel的列与接收数据类的属性是一一对应的,如果不对应,可参考读取到指定列部分,使用@ExcelProperty(index=xx)显性的指定对应关系;

java 复制代码
@Data
public class Student implements Serializable {
    private Integer id;
    private String stuCode;
    private String stuName;
    private String sex;
    private String born;
    private Integer age;
    private String address;
    private String motherName;
    private String fatherName;
    private Integer grade;
    private Integer classNum;
}
java 复制代码
@Test
public void readAllSheet(){
    String userDir = System.getProperty("user.dir");
    String importPath = userDir + File.separator + "import";
    File dir = new File(importPath);
    if (!dir.exists()) {
        dir.mkdirs();
    }
    String importFile = importPath + File.separator + "学生信息表.xlsx";
    StudentReadListener studentReadListener = new StudentReadListener();
    EasyExcel.read(importFile, Student.class, studentReadListener).doReadAll();
    List<Student> students = studentReadListener.getStudents();
    for (Student student : students) {
        System.out.println(student.getStuName());
    }
}

日期、数字及其他自定义格式的转换

在导入或者导出excel的时候,如果想对某一列的数据格式作调整转换,可以自定义一个转换器(com.alibaba.excel.converters.Converter),然后这个个转换器通过@ExcelProperty(converter=xxxxx.class)标记在接收参数的类型的属性上;

这种转换数据格式的需求,有时候是主动的,有时候是被动的。什么是主动的的呢?假如前数据为库存储的日期格式是yyyyMMdd,导出的时候想要的是xxxx年xx月xx日,然后你就可以实现一个类型转换器(Converter)主动完成这个事。下面举个被动的例子,excel中关于日期的一个坑,绕不过的坑,所以是"被动"滴。

excel中单元格式格式是日期的,easyexcel解析后是一个数字,这不是解析错误了,而是excel中对于日期存储的格式就是数字,这个数字代表的是1900年1月1日,到单元格式内日期的天数,所以解析结果中是一个数字并不难理解,但是这不是我我们想要的结果呀。更恶心的是,java中的Date的时间起点1970年1月1日,所以被动的需求就产生了,需要把一个以1900-1-1为起天的天数代表的日期,转换为以1970-1-1为起点的java.util.Date。标准的不统一,产生的结果就是这么恶心。

java 复制代码
public class SalaryDateConverter implements Converter<String> {
    @Override
    public Class<?> supportJavaTypeKey() {
        return String.class;
    }
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }
    //导入的时候会走这个方法,导入的转换逻辑可以在这个方法里实现
    @Override
    public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        BigDecimal numberValue = cellData.getNumberValue();
        //平时不要动不动就搞个util工具类,我曾经目睹一个新同事,用上用不上的也不管,上来在工程里导入了几十个工具类,搞得maven依赖冲突
        // org.apache.poi.ss.usermodel.DateUtil是POI的工具类,
        // DateUtil.getJavaDate()的功能就是把以1900-1-1为起点的日期天数转换成java.util.Date,直接拿来用就好了,基本不用担心里面有bug
        Date javaDate = DateUtil.getJavaDate(numberValue.doubleValue());
        //com.alibaba.excel.util.DateUtils是easyexcel封装的日期转换工具类,能用就用上呗,基本也不用担心有bug
        String format = DateUtils.format(javaDate, DateUtils.DATE_FORMAT_10);
        return format;
    }
    //导出的时候会走这个方法,导出的转换逻辑可以在这个方法里实现
    @Override
    public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return null;
    }
}
java 复制代码
@Data
public class EmpSalary  {
    @ExcelProperty("姓名")
    private String realName;
    @ExcelProperty("员工编号")
    private String empNo;
    @ExcelProperty(value = "工资日期",converter = SalaryDateConverter.class)
    private String salaryDate;
    @ExcelProperty("工资数额")
    private Float amount;
}
@Test
public void readByConvert(){
    String userDir = System.getProperty("user.dir");
    String importPath = userDir + File.separator + "import";
    File dir = new File(importPath);
    if (!dir.exists()) {
        dir.mkdirs();
    }
    String importFile = importPath + File.separator + "员工工资表.xlsx";
    EmpSalaryReadListener empSalaryReadListener = new EmpSalaryReadListener();
    EasyExcel.read(importFile, EmpSalary.class, empSalaryReadListener).sheet().doRead();
    List<EmpSalary> empSalaries = empSalaryReadListener.getEmpSalaries();
    System.out.println(empSalaries.size());
}

表头有多行的读取

easyexcel在读取表格内容的时候,默认是从第二行开始读的,因为第一行通常是表头,所以上面没有指定从第几行开始读也没有问题。但是遇到下图样式的复合表头的时候,表头是占了两行,数据是从第三行开始的,那么在读取的时候读取监听器、接收数据的类没有变化,而是在读取的时候要显性指定从第几行开始读,实际指定的时候是索引,从0开始,第三行的索引就是2;

java 复制代码
@Test
public void readManyRow(){
    String userDir = System.getProperty("user.dir");
    String importPath = userDir + File.separator + "import";
    File dir = new File(importPath);
    if (!dir.exists()) {
        dir.mkdirs();
    }
    String importFile = importPath + File.separator + "员工工资表 - 副本.xlsx";
    EmpSalaryReadListener empSalaryReadListener = new EmpSalaryReadListener();
    //数据从第三行开始,索引是2
    EasyExcel.read(importFile, EmpSalary.class, empSalaryReadListener).sheet().headRowNumber(2).doRead();
    List<EmpSalary> empSalaries = empSalaryReadListener.getEmpSalaries();
    System.out.println(empSalaries.size());
}
java 复制代码
@Data
public class EmpSalary  {
    private String realName;
    private String empNo;
    @ExcelProperty(value = "工资日期",converter = SalaryDateConverter.class)
    private String salaryDate;
    private Float baseAmount;
    private Float fullAttendAmount;
    private Float insurance;
}

表头的读取

有时候也会有这样的需求,就是除了读取表格的数据外,表头的数据也要读取出来,easyexcel的读取监听器里的实现类里重写invokeHead()方法即可,下面以读取多行表头,写一个示例:

java 复制代码
public class EmpSalaryReadListener implements ReadListener<EmpSalary> {
    private List<EmpSalary> empSalaries=new ArrayList<>();
    public List<EmpSalary> getEmpSalaries() {
        return empSalaries;
    }
    @Override
    public void invoke(EmpSalary data, AnalysisContext context) {
        empSalaries.add(data);
    }
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
    }
    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
        for (Integer key : headMap.keySet()) {
            System.out.println("key:"+key+","+headMap.get(key).getStringValue());
        }
        System.out.println("---------");
    }
}

表头信息也是逐行读取的,即每读取一行就会回调一下监听器的表头读取回调方法(invokeHead()),表头信息结果是存储在一个map中,map的key为excel表格列上的索引,value是表头信息。对于多行合并单元格后,合并单元格后的内容在第一个格里,其他单元格也会占一个位置但是是空的;

相关推荐
杨充几秒前
13.观察者模式设计思想
java·redis·观察者模式
Lizhihao_2 分钟前
JAVA-队列
java·开发语言
喵叔哟12 分钟前
重构代码之移动字段
java·数据库·重构
喵叔哟12 分钟前
重构代码之取消临时字段
java·前端·重构
fa_lsyk14 分钟前
maven环境搭建
java·maven
Daniel 大东33 分钟前
idea 解决缓存损坏问题
java·缓存·intellij-idea
wind瑞40 分钟前
IntelliJ IDEA插件开发-代码补全插件入门开发
java·ide·intellij-idea
HappyAcmen40 分钟前
IDEA部署AI代写插件
java·人工智能·intellij-idea
马剑威(威哥爱编程)1 小时前
读写锁分离设计模式详解
java·设计模式·java-ee
鸽鸽程序猿1 小时前
【算法】【优选算法】前缀和(上)
java·算法·前缀和