• [技术干货] Java创建Excel文件
     public synchronized static int writeExcel(JSONArray jsonArray,int k) {         //创建Excel文件薄         HSSFWorkbook workbook=new HSSFWorkbook();         //创建工作表sheeet         HSSFSheet sheet=workbook.createSheet();         //创建第一行         HSSFRow row=sheet.createRow(0);         String[] title={"货号","中文名","英文名","CAS","分子式","分子量","图片网址","svg网址"};         HSSFCell cell_title = null;         for (int i=0;i<title.length;i++){             cell_title=row.createCell(i);             cell_title.setCellValue(title[i]);         }         //追加数据         for (int i=0;i<jsonArray.size();i++){             JSONObject jsonObject =(JSONObject) jsonArray.get(i);             HSSFRow nextrow=sheet.createRow(i+1);             HSSFCell cell_value=nextrow.createCell(0);             cell_value.setCellValue("");             cell_value=nextrow.createCell(1);             cell_value.setCellValue(TransApi.EN_to_ZH(jsonObject.get("name").toString()));             cell_value=nextrow.createCell(2);             cell_value.setCellValue(jsonObject.get("name").toString());             cell_value=nextrow.createCell(3);             cell_value.setCellValue(jsonObject.get("CAS").toString());             cell_value=nextrow.createCell(4);             cell_value.setCellValue(jsonObject.get("FCS").toString());             cell_value=nextrow.createCell(5);             cell_value.setCellValue(jsonObject.get("MW").toString());             cell_value=nextrow.createCell(6);             cell_value.setCellValue(jsonObject.get("img").toString());             cell_value=nextrow.createCell(7);             cell_value.setCellValue(jsonObject.get("svg").toString());         }         //创建一个文件         try {             File file=new File("D:/pacong/poi_test_"+k+".xls");             file.createNewFile();             FileOutputStream stream= FileUtils.openOutputStream(file);             workbook.write(stream);             stream.close();         }catch (Exception e){             e.printStackTrace();         }finally {             return 1;         }     }原文链接:https://blog.csdn.net/qq_31122833/article/details/123449030
  • [技术干货] java代码生成excel表格
    在很多项目中,都需要我们把数据库查询出来的数据导出成excel表格,导出就算了,还给我弄个模板,大标题要写在合并表格里还要居中,标题要有底色,还要加粗,等等,下面我就来讲一下我用到的表格操作:  首先要进行表格操作,要添加相关依赖  这里我重点说明一下,我用4.3及以上版本就不行,可能是方法有升级  <dependency>             <groupId>org.apache.poi</groupId>             <artifactId>poi-ooxml</artifactId>             <version>4.0.1</version>         </dependency>         <dependency>             <groupId>org.apache.poi</groupId>             <artifactId>poi</artifactId>             <version>4.0.1</version>         </dependency> 创建excel文件  如果想生成.xls文件:  Workbook workbook = new HSSFWorkbook(); 如果想生成.xlsx文件  Workbook workbook = new XSSFWorkbook(); 创建工作簿  也就是我们说的Sheet页,sheet页是根据Workbook对象生成的,也就是sheet页依赖excel文件而存在。  Sheet sheet = workbook.createSheet("Sheet1");  // Sheet1为要生成的sheet页的名称 写入操作  完成以上操作,在不考虑样式的前提下,我们就可以写数据了。写数据的话是按行进行写入的。 首先声明行变量 行是根据sheet声明的,参数为该行的位置(从0开始算)  Row titleRow = sheet.createRow(0); 写入数据 有了行对象,那么我们就可以对该行的某列进行操作了 首先要创建该行某列对象,然后用getCell方法获取单元格,参数为单元格索引位置(从0开始算),然后用setCellValue方法进行内容写入,参数为内容  titleRow.createCell(0); titleRow.getCell(0).setCellValue("序号"); 写完之后,返回workbook对象就可以了。 按这样操作,一份没有样式的表格文件就生成好了。 样式操作  首先要声明样式对象 因为创建的workbook对象方式不同,样式对象的类型也不一样,不过方法是类似的  XSSFCellStyle cellStyle = workbook.createCellStyle();  背景颜色  这里修改只需要改new java.awt.Color里的参数值即可,也支持输入0xff000000(默认值)16进制数 需要与设置填充格式一起使用才生效,使用无先后顺序  cellStyle.setFillForegroundColor(new XSSFColor(new java.awt.Color(89, 214, 255),new DefaultIndexedColorMap())); 字体居中  cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);  // 垂直居中 cellStyle.setAlignment(HorizontalAlignment.CENTER); // 水平居中 设置边框  BorderStyle.THIN为宽度属性  cellStyle.setBorderBottom(BorderStyle.THIN); // 下边框 cellStyle.setBorderRight(BorderStyle.THIN);  // 右边框 cellStyle.setBorderLeft(BorderStyle.THIN);   // 左边框 cellStyle.setBorderTop(BorderStyle.THIN);    // 上边框 设置填充格式  cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); 自动换行  cellStyle.setWrapText(true); 字体设置  Font titleFont = workbook.createFont();  // 声明字体对象 titleFont.setBold(true);        // 字体加粗 titleFont.setFontName("华文彩云");  // 字体类型 titleFont.setFontHeightInPoints((short) 20);  // 字体大小 cellStyle.setFont(titleFont);   // 赋值样式对象 设置列宽  sheet.setColumnWidth(0, 4200);  // 第0列列宽设置为4200 合并单元格  sheet.addMergedRegion(new CellRangeAddress(1, 3, 1, 3));  // 参数分别为起始行,结束行,起始列,结束列 1 返回前端的话,写法是这样的,方法是没有返回的,也就是void  public void index(HttpServletResponse response) {         XSSFWorkbook workbook = service.test();         if (workbook!=null){             String fileName = "1.xlsx";             try {                 fileName = URLEncoder.encode(fileName, "UTF-8");                 //设置ContentType请求信息格式                 response.setContentType("application/vnd.ms-excel;charset=utf-8");                 response.setHeader("Content-disposition", "attachment;filename=" + fileName);                 OutputStream outputStream = response.getOutputStream();                 workbook.write(outputStream);                 outputStream.flush();                 outputStream.close();             } catch (Exception e) {                 System.out.println(e);             }         }     }  我这边用到的基本是这些,如果有其他的操作可以评论我或者私信我,我这边在做补充 ———————————————— 原文链接:https://blog.csdn.net/weixin_45494557/article/details/132088533 
  • [技术干货] Java8让Excel的读写变得更加简单高效
    Apache POI 在业务开发中我们经常会遇到Excel的导入导出,而 Apache POI 是Java开发者常用的API。 【https://poi.apache.org/components/spreadsheet/index.html】  GridExcel Universal solution for reading and writing simply Excel based on functional programming and POI EventModel  GridExcel是基于Java8函数式编程和POI EventModel实现的用于Excel简单读写的通用解决方案。  基于POI EventModel,在读写数据量非常大的Excel时,降低内存占用避免OOM与频繁FullGC 基于函数编程,支持关联对象等多种复杂情况的处理,学习成本低 支持流式API,使代码编写和理解更简单,更直观 EventModel 什么是EventModel?在POI FAQ(常见问题解答)【https://poi.apache.org/help/faq.html#faq-N100C2】官方给出解释:  The SS eventmodel package is an API for reading Excel files without loading the whole spreadsheet into memory. It does require more knowledge on the part of the user, but reduces memory consumption by more than tenfold. It is based on the AWT event model in combination with SAX. If you need read-only access, this is the best way to do it.  SS eventmodel包是一个用于读取Excel文件而不将整个电子表格加载到内存中的API。 它确实需要用户掌握更多知识,但是将内存消耗减少了十倍以上。 它基于AWT(Abstract Window Toolkit)event model与SAX的结合。 如果您需要只读访问权限,这是最好的方式。  函数编程 说到函数编程,就不得不提Lambda表达式,如果对Java8中的Lambda不了解或理解不深刻,可以看下甲骨文官网给出的这篇文章,【https://www.oracle.com/technetwork/articles/java/architect-lambdas-part1-2080972.html】,个人认为这是Java8 Lambda从入门到进阶最好的文章之一。  其中函数编程的目的就是实现代码块传递,即,将方法作为参数在方法间传递。为此,随着Java语言的发展,不断出现一些解决方案:  Java 1.0, 使用Abstract Window Toolkit (AWT) EventModel来实现,但笨拙且不可行 Java 1.1,提出一系列“Listeners” 后来使用内部类和匿名内部类来实现,但是大多数情况下,它们只是被用作事件处理。 再后来发现更多地方将代码块作为对象(实际上是数据)不仅有用而且是必要的,但是Java中函数编程还是很笨拙,它需要成长。 直到Java 1.7,Java引入了java.lang.invoke包,提供一 种新的动态确定目标方法的机制(可以不用再单纯依靠固化在虚拟机中的字节码调用指令),称为MethodHandle,模拟字节码的方法指针调用,类似于C/C++的Function Pointer(函数指针)并引入第5条方法调用的字节码指令invokedynamic。 直到Java 1.8,基于Java 1.7提出的字节码指令invokedynamic,实现了Lamda技术,将函数作为参数在方法间传递,Java开始更好的支持函数式编程。 用反射不是早就可以实现了吗?Reflection API 重量级,性能低。 注意: 5、6、7参考《深入理解Java虚拟机》第2版,8.3.3 动态类型语言支持。  在POI的使用过程中,对大多数API User来说经常面临两个问题,这也是GridExcel致力解决的问题。  问题1. 仅使用简单的导入导出功能,但每次业务的数据对象结构不同,需要重新编写处理方法,很麻烦! 解决方法 将Excel处理逻辑抽取出来,封装成工具类。  封装条件 与大多数Java API一样,POI把更多的精力放在高级功能的处理上,比如Formula(公式)、Conditional Formatting(条件格式)、Zoom(缩放)等。对于仅仅做数据导入导出功能的API User,很少使用这些高级特性,这允许API用户对POI的使用进行简单的封装。  封装方式 无论是读是写,我们都需要解决Excel中的Columns(列)与Java数据对象Fields(字段)的映射关系,将这种映射关系作为参数(Map对象HashMap或LinkedHashMap),传递给工具类。  对于Columns不难理解,它可以是有序的数字或字母,也可以是其它字符串用来作为首行,表示该列数据的含义。  对于Fields,它的处理需要兼容复杂情况,如下:  查询字段时出现异常 字段或单元格的值为null 该列的值可能对应关联对象、甚至是关联集合中的某个字段值 字段或单元格的值需要做特殊处理,例如value == true?完成:失败; 反射 首先想到,也是大多数封装者都在使用的方式是就是Reflection API,从上文 函数编程 章节我们了解到,反射重量级,会降低代码的性能,同时对复杂情况的处理支持性不够好。  反射+注解 这种方式可以更好的支持复杂情况,但是反射依然会降低性能,同时注解对数据对象会造成代码侵入,而且对该工具类封装者的其他使用者无疑会增加学习成本。  匿名内部类 这种方式也可以很好的支持复杂情况,但是使用匿名内部类的语法显然患有“垂直问题”(这意味着代码需要太多的线条来表达基本概念),太过冗杂。至于性能,应该也不如直接传递函数来的快吧。  函数接口(Lambda) 这种方式是基于第5条方法调用的字节码指令invokeDynamic实现的,直接传递函数代码块,很好的支持复杂情况,性能较高,代码编写更简单结构更加简洁,而且对数据对象代码零侵入。  问题2. Excel导入或导出数据量比较大,造成内存溢出或频繁的Full GC,该如何解决? 解决方法 读Excel —— eventmodel 写Excel —— streaming.SXSSFWorkbook 原理 POI的使用对我们来说很常见,对下面两个概念应该并不陌生:  HSSFWorkbook(处理97(-2007) 的.xls) XSSFWorkbook(处理2007 OOXML (.xlsx) ) 但是对于eventmodel和streaming.SXSSFWorkbook就很少接触了,它们是POI提供的专门用来解决内存占用问题的low level API(低级API),使用它们可以读写数据量非常大的Excel,同时可以避免内存溢出或频繁的Full GC。【https://poi.apache.org/components/spreadsheet/how-to.html】  eventmodel,用来读Excel,并没有将Excel整个加载到内存中,而是允许用户从InputStream每读取一些信息,就交给回调函数或监听器,至于丢弃,存储还是怎么处理这些内容,都交由用户。 streaming.SXSSFWorkbook,用来写Excel(是对XSSFWorkbook的封装,仅支持.xlsx),通过滑动窗口来实现,只在内存中保留滑动窗口允许存在的行数,超出的行Rows被写出到临时文件,当调用write(OutputStream stream)方法写出内容时,再直接从临时内存写出到目标OutputStream。SXSSFWorkbook的使用会产生一些局限性。 Only a limited number of rows are accessible at a point in time. Sheet.clone() is not supported. Formula evaluation is not supported 解决途径 https://github.com/liuhuagui/gridexcel 基于Java函数编程(Lambda),支持流式API,使用环境Java1.8或更高,学习成本:Lambda 实际上POI官网已经给了用户使用示例,而上述工具只是做了自己的封装实现,让使用更方便。  快速使用 <dependency>     <groupId>com.github.liuhuagui</groupId>     <artifactId>gridexcel</artifactId>     <version>2.2</version> </dependency> GridExcel.java GridExcel.java提供了多种静态方法,可以直接使用,具体式例可参考测试代码(提供了测试数据和测试文件):  https://github.com/liuhuagui/gridexcel/blob/master/src/test/java/ReadTest.java https://github.com/liuhuagui/gridexcel/blob/master/src/test/java/WriteTest.java 流式API /**   * 业务逻辑处理方式三选一:   * 1.启用windowListener,并将业务逻辑放在该函数中。   * 2.不启用windowListener,使用get()方法取回全部数据集合,做后续处理。   * 3.readFunction函数,直接放在函数中处理 或 使用final or effective final的局部变量存放这写数据,做后续处理。   * 注意:使用EventModel时readFunction函数的输入为每行的cell值集合List<String>。   * @throws Exception   */  @Test  public void readXlsxByEventModel() throws Exception {      InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("2007.xlsx");      GridExcel.readByEventModel(resourceAsStream,TradeOrder.class,ExcelType.XLSX)              .window(2,ts -> System.out.println(JSON.toJSONString(ts)))//推荐在这里执行自己的业务逻辑              .process(cs ->{                  TradeOrder tradeOrder = new TradeOrder();                  tradeOrder.setTradeOrderId(Long.valueOf(cs.get(0)));                  Consultant consultant = new  Consultant();                  consultant.setConsultantName(cs.get(3));                  tradeOrder.setConsultant(consultant);                  tradeOrder.setPaymentRatio(cs.get(16));                  return tradeOrder;              },1);  }  /**   * 使用Streaming UserModel写出数据到Excel   * @throws Exception   */  @Test  public void writeExcelByStreaming() throws Exception {      GridExcel.writeByStreaming(TradeOrder.class)              .head(writeFunctionMap())//对象字段到Excel列的映射              .createSheet()              .process(MockData.data())//模拟数据。在这里设置业务数据集合。              .write(FileUtils.openOutputStream(new File("/excel/test.xlsx")));  }  ReadExcel ReadExcelByUserModel Use user model to read excel file. userModel ——  缺点:内存消耗大,会将excel信息全部加载到内存再进行处理。 优点:现成的API,使用和理解更简单。 使用场景:可以处理数据量较小的Excel。 ReadExcelByEventModel Use event model to read excel file. eventModel ——  缺点:没有现成的API,使用和理解较为复杂,适合中高级程序员(GridExcel的目标之一就是让EventModel的使用变得简单) 优点:非常小的内存占用,并没有在一开始就将所有内容加载到内存中,而是把主体内容的处理(存储,使用,丢弃)都交给了用户,用户可以自定义监听函数来处理这些内容。 使用场景:可以处理较大数据量的Excel,避免OOM和频繁FullGC WriteExcel WriteExcelByUserModel Use user model to write excel file. userModel ——  缺点:会将产生的spreadsheets对象整个保存在内存中,所以write Excel的大小受到堆内存(Heap space)大小限制。 优点:使用和理解更简单。 使用场景:可以写出数据量较小的Excel。 WriteExcelByStreaming Use API-compatible streaming extension of XSSF to write very large excel file. streaming userModel——  缺点: 仅支持XSSF; Sheet.clone() is not supported; Formula evaluation is not supported; Only a limited number of rows are accessible at a point in time. 优点:通过滑动窗口来实现,内存中只保留指定size of rows的内容,超出部分被写出到临时文件,write Excel的大小不再受到堆内存(Heap space)大小限制。 使用场景:可以写出非常大的Excel。 Issues 在使用工具过程中出现问题,有功能添加或改动需求的可以向作者提Issue:https://github.com/liuhuagui/gridexcel/issues 比如说,想要增加对首行以外的行列做样式扩展 ————————————————  原文链接:https://blog.csdn.net/qq_32331073/article/details/97650960 
  • [技术干货] 小型java easy Excel导出工具通类(可拓展) jdk8函数式接口
    小型java easy Excel导出工具类(可拓展) jdk8函数式接口 首先需求是前端传入对象list,指定后端配置的Excel导出实体类package地址(根据需求可脱离前端根据业务需求定义接口直接调工具类方法即可,传参数方式由指向地址改为Excel导出实体类Class对象)并且以application/vnd.ms-excel类型的回执返回给前端download(实现逻辑简单不喜勿喷,希望得到各路神仙🐮的指点OwO)。讲一下实现思路首先后端拿到list和实体类映射地址或者类Class定义文件输出流定义两个方法一个作为创建列头和数据,另一个作为导出操作(这一步操作是由Lambda表达式写入提交)直接写入到流中返回给前端。  1.引入maven poi依赖以及其他应用到的依赖         <dependency>            <groupId>org.apache.poi</groupId>            <artifactId>poi</artifactId>        </dependency>        <dependency>            <groupId>org.apache.poi</groupId>            <artifactId>poi-ooxml</artifactId>        </dependency>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-web</artifactId>         </dependency> 2.结构目录  annotations:字段型自定义注解(列头/日期格式/字典式数字替换文字/列头宽度……后期拓展字段交给你们OwO🐮) as:函数接口/自定义接口/定义接口实现 entity:Excel导出行数据转换实体 model:Excel导出实体类 uitls:Excel导出工具类  3.定义字段型注解(解释都在注释里了) package 这个你们放你们自己的包吧;  import java.lang.annotation.*;  @Documented @Target({ElementType.FIELD}) //注解应用类型(应用到方法的注解,还有类的可以自己试试) @Retention(RetentionPolicy.RUNTIME) // 注解的类型 public @interface Excel {      /**      * Excel列头      **/     String name() default "";      /**      * 日期格式转换      **/     String format() default "yyyy/MM/dd";      /** 文字替换根据 ":" 提供两种方式如下      * 1.普通数组替换: {"文本1:文本2",……}      * 2.反射方法替换: "${无参数方法返回值类型为List<Object>}"      * 且方法必须在同一实体类      **/     String[] filterText() default {};      /**      * 列头宽度      **/     int width() default 30;      /**      * 函数配置      **/     String func() default ""; }  4.自定义一个消费型函数式接口 这里着重讲一下含义所谓消费,其实就是将数据提交到接口里由方法重写由Lambda表达式的方式处理数据优化代码避免性能浪费  package 这个你们放你们自己的包吧;  /**  有且只有一个的函数式接口,称之为函数接口,接口可以包含其他方法(静态和默认以及私有)  * @Author du_zy  * @Description //TODO  * @Date 3:18 PM 2022/3/8  **/ @FunctionalInterface public interface ExportConsumer<ByteArrayOutputStream, Workbook, List>{      /** 自定义导出数据拼装      * @Author du_zy      * @Description //TODO      * @Date 2:46 PM 2022/3/8      * @Param [fileOut, wk, list]      * fileOut文件输出流最后是将文件写入到这个里传给前端的,wk是poi处理Excel样式以及数据的APi里面有很多强大的功能可以参考官方有兴趣可以看看,list是前端传入的数据      * @return void      **/     public abstract void accept(ByteArrayOutputStream fileOut, Workbook wk, List list) throws Exception; } 5.定义数据转换类实体 这里是对list的属性的一些定义因为要对list里的数据标记那些要日期格式化那些要文字替换下文在注释里有详解  package 这个你们放你们自己的包吧;  /**  * @ClassName FieldExcel  * @Description TODO  * @Author du_zy  * @Date 2022/3/11 10:09 AM  * @Version 1.0  **/ public class FieldExcel {      private String[] fieldArr;      private String fieldName;      private String fieldType;      private String fieldFormat;      public String[] getFieldArr() {         return fieldArr;     }      public void setFieldArr(String[] fieldArr) {         this.fieldArr = fieldArr;     }      public String getFieldName() {         return fieldName;     }      public void setFieldName(String fieldName) {         this.fieldName = fieldName;     }      public String getFieldType() {         return fieldType;     }      public void setFieldType(String fieldType) {         this.fieldType = fieldType;     }      public String getFieldFormat() {         return fieldFormat;     }      public void setFieldFormat(String fieldFormat) {         this.fieldFormat = fieldFormat;     }      public FieldExcel() {         this.fieldArr = new String[]{};         this.fieldName = "null_flag";     } } 6.导出列头实体类(这个类里的属性根据业务需求制作) 主要需要看Excel注解  package 这个你们放你们自己的包吧;  import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import 这个你们放你们自己的包吧.CfmRepairCodeEnum; import 这个你们放你们自己的包吧.annotations.Excel; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component;  import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Map;  /**  *  实体类  * @author du  * @since 2021-12-07 15:23:53  */  @Data @EqualsAndHashCode(callSuper = false) @NoArgsConstructor @AllArgsConstructor @Component public class FitMentExcelModel implements Serializable {      private static final long serialVersionUID = 1L;     /**      * 装店装修业务单主键id      */     private Long cfmRepairId;     /**      * 装店装修单R单单号      */     @Excel(name = "装店装修单R单单号")     private String cfmRepairCode;     /**      * 申请日期      */     @Excel(name = "申请日期")     private LocalDateTime applyTime;     /**      * 申请人      */     @Excel(name = "申请人")     private String applyName;     /**      * 申请人电话      */     @Excel(name = "申请人电话")     private String applyMobile;     /**      * 城市经理名称      */     @Excel(name = "城市经理名称")     private String cityManager;     /**      * 城市经理电话      */     @Excel(name = "城市经理电话")     private String cityManagerPhone;     /**      * 装修负责人      */     @Excel(name = "装修负责人")     private String repairLeader;     /**      * 负责人电话      */     @Excel(name = "负责人电话")     private String leaderPhone;     /**      * 省份      */     @Excel(name = "省份")     private String province;     /**      * 城市      */     @Excel(name = "城市")     private String city;     /**      * 区      */     @Excel(name = "区")     private String area;     /**      * 店面名称      */     @Excel(name = "店面名称")     private String storeName;     /**      * 店面类别      */     @Excel(name = "店面类别")     private String storeType;     /**      * 店面类型      */     @Excel(name = "店面类型")     private String storeCategory;     /**      * 店面性质      */     @Excel(name = "店面性质")     private String storeProperty;     /**      * 店面面积      */     @Excel(name = "店面面积")     private BigDecimal storeArea;     /**      * 店面所属商圈      */     @Excel(name = "店面所属商圈")     private String storeBusinessArea;     /**      * 地级/县级      */     @Excel(name = "地级/县级", filterText = {"1:地级", "2:县级"})     private String cityLevel;     /**      * 老店历史装修时间      */     @Excel(name = "老店历史装修时间")     private LocalDate decorateDate;     /**      * 装修性质      */     @Excel(name = "装修性质")     private String repairLevelName;     /**      * 装修模式      */     @Excel(name = "装修模式")     private String repairModelName;     /**      * 预计开业时间      */     @Excel(name = "预计开业时间")     private LocalDate openDate;     /**      * 店面专区备注      */     @Excel(name = "店面专区备注")     private String storeTeamRemark;     /**      * 店面专区面积      */     @Excel(name = "店面专区面积")     private BigDecimal storeTeamArea;     /**      * 开始设计时间      */     @Excel(name = "开始设计时间")     private LocalDate designingDate;     /**      * 设计完成时间      */     @Excel(name = "设计完成时间")     private LocalDate designFinishDate;     /**      * 设计周期      */     @Excel(name = "设计周期")     private Integer designCycle;     /**      * 铺地砖日期      */     @Excel(name = "铺地砖日期")     private LocalDate brickDate;     /**      * 木工日期      */     @Excel(name = "木工日期")     private LocalDate woodDate;     /**      * 油工日期      */     @Excel(name = "油工日期")     private LocalDate oilDate;     /**      * 安装样品日期      */     @Excel(name = "安装样品日期")     private LocalDate installDate;     /**      * 软装摆场日期      */     @Excel(name = "软装摆场日期")     private LocalDate softDate;     /**      * 暂不装修时间      */     @Excel(name = "暂不装修时间")     private LocalDate notRepairDate;     /**      * 暂不装修原因      */     @Excel(name = "暂不装修原因")     private String notRepairReason;     /**      * 不装修时间      */     @Excel(name = "不装修时间")     private LocalDate noRepairDate;     /**      * 不装修原因      */     @Excel(name = "不装修原因")     private String noRepairReason;     /**      * 装修完成时间      */     @Excel(name = "装修完成时间")     private LocalDate repairFinishDate;     /**      * 装修周期      */     @Excel(name = "装修周期")     private Integer repairCycle;     /**      * 验收完成时间      */     @Excel(name = "验收完成时间")     private LocalDate checkDate;     /**      * 验收周期      */     @Excel(name = "验收周期")     private Integer checkCycle;     /**      * 验收状态 0:不合格 1:合格 2:无验收      */     @Excel(name = "验收状态", filterText = {"0:不合格", "1:合格", "2:无验收"})     private Integer checkState;     /**      * 装修单状态      */     @Excel(name = "装修单状态", filterText = "${getEnumRepairSate}")     private Integer repairState;     @Excel(name = "状态")     private String state;     /**      * 支付时间      */     @Excel(name = "支付时间")     private LocalDateTime payTime;     /**      * 支付单号      */     @Excel(name = "支付单号")     private String cfmPayCode;     /**      * 设计费金额(分)      */     @Excel(name = "设计费金额(分)")     private Long sumDesignCost;     /**      * 支付方式      */     @Excel(name = "支付方式")     private String payMethod;     /**      * 评价状态 0:未评价 1:已评价      */     @Excel(name = "评价状态", filterText = {"0:未评价", "1:已评价"})     private Integer evaluateState;     /**      * 评价信息  图纸设计      */     @Excel(name = "评价信息-图纸设计")     private BigDecimal evaluateDesign;     /**      * 评价信息  装修效果      */     @Excel(name = "评价信息-装修效果")     private BigDecimal evaluateRepair;     /**      * 评价信息  服务沟通      */     @Excel(name = "评价信息-服务沟通")     private BigDecimal evaluateCommunication;     /**      * 评价内容      */     @Excel(name = "评价内容")     private String evaluateContent;     /**      * 设计师负责人名称      */     @Excel(name = "设计师负责人名称")     private String responseName;     /**      * 设计师名称      */     @Excel(name = "设计师名称")     private String designName;     /**      * 表单备注      */     @Excel(name = "表单备注")     private String formRemark;     /**      * 备注      */     @Excel(name = "备注")     private String remark;     /**      * 关联店面id      */     private String storeId;     /**      * 店面编码      */     private String storeCode;     /**      * 申请人id      */     private Long applyId;       /**将一串key-value数据转换成key:value数据并返回成list      * @Author du_zy      * @Description //TODO       * @Date 4:12 PM 2022/3/23      * @Param []      * @return java.util.List<java.lang.Object>      **/     @Bean     public List<Object> getEnumRepairSate() {         List<Object> resultList = new ArrayList<>();         for (Map.Entry<String, Object> stringObjectEntry : CfmRepairCodeEnum.getEnum().entrySet()) {             JSONObject j = JSONObject.parseObject(JSON.toJSONString(stringObjectEntry.getValue()));             resultList.add(j.get("code") + ":" + j.get("msg"));         }       return resultList;     }          /**      * 装修性质code      */     private String repairLevelCode;     /**      * 联系电话      */     private String mobile;     /**      * 省份编码      */     private String provinceCode;     /**      * 城市编码      */     private String cityCode;     /**      * 区编码      */     private String areaCode;     /**      * 装修模式code      */     private String repairModelCode;     /**      * 设计费单价(分)      */     private Long designCost;     /**      * 设计部数据员id      */     private Long dataId;     /**      * 设计部数据员名称      */     private String dataName;     /**      * 设计师负责人id      */     private Long responseId;     /**      * 设计师id      */     private Long designId;     /**      * 验收图片 0:未上传 1:已上传      */     private Integer checkPhotoState;     /**      * 关闭时间      */     private LocalDateTime closeTime;     /**      * 是否删除(0:未删除、1:删除)      */     private String delFg;     /**      * 创建人      */     private String createBy;     /**      * 创建人编码      */     private String createCode;     /**      * 创建时间      */     private LocalDateTime createDt;     /**      * 更新人      */     private String updateBy;     /**      * 更新人编码      */     private String updateCode;     /**      * 更新时间      */     private LocalDateTime updateDt;     /**      * 时间戳      */     private Long timeStamp;  }  7.导出工具类(创建列头和数据以及提交写入流方法)都在注释里了 package 这个你们放你们自己的包吧;  import com.alibaba.fastjson.JSONObject; import 这个你们放你们自己的包吧; import 这个你们放你们自己的包吧; import 这个你们放你们自己的包吧; import 这个你们放你们自己的包吧; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.WorkbookUtil; import org.springframework.beans.TypeMismatchException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils;  import java.io.ByteArrayOutputStream; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import java.util.regex.Pattern;  /**  * @ClassName ExportUtils  * @Description TODO  * @Author du_zy  * @Date 2022/3/8 11:03 AM  * @Version 1.0  **/ public class ExportUtils {      /** 导出Excel      * @Author du_zy      * @Description //TODO      * @Date 3:04 PM 2022/3/8      * @Param [fileOut, wb, listData, fileName, consumer]      * @return org.springframework.http.ResponseEntity<byte[]>      **/     public static <T> ResponseEntity<byte[]> exportExcel(ByteArrayOutputStream fileOut,                                                          Workbook wb,                                                          List<T> listData,                                                          String fileName,                                                          ExportConsumer<ByteArrayOutputStream, Workbook, List<T>> consumer) {         HttpHeaders headers = new HttpHeaders();         try {             consumer.accept(fileOut, wb, listData); // 函数接口数据配置             headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); // 配置请求头类型             headers.setContentDispositionFormData("attachment", new String((fileName + ".xlsx").getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1)); //配置表单信息,字符转换             return new ResponseEntity<>(fileOut.toByteArray(), headers, HttpStatus.OK);         } catch (Exception e) {             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);          }      }      /** 创建标头和数据      * @Author du_zy      * @Description //TODO      * @Date 2:59 PM 2022/3/8      * @Param [wb, data, className]      * @return org.apache.poi.ss.usermodel.Workbook      **/     public static <T> Workbook createCell(Workbook wb, List<T> data, Class<?> className) {         try {             String safeName = WorkbookUtil.createSafeSheetName("sheet1");             Sheet sheet = wb.createSheet(safeName);  // 创建工作空间             //第一行标题             Row title = sheet.createRow(0);             List<Map<String, Object>> cols = new LinkedList<>();             for (int i = 0; i < className.getDeclaredFields().length; i++) { // 循环如果反射里的类里有注解name配置就存入到标头list里                 boolean is = className.getDeclaredFields()[i].isAnnotationPresent(Excel.class);                 if (is) {                     Map<String, Object> coverMap = new HashMap<>();                     Excel annotation = className.getDeclaredFields()[i].getAnnotation(Excel.class);                     coverMap.put("name", annotation.name());                     coverMap.put("width", annotation.width());                     cols.add(coverMap);                 }             }              for (int i = 0; i < cols.size(); i++) { //第一行放标题                 title.createCell(i).setCellValue(cols.get(i).get("name").toString()); //                sheet.autoSizeColumn(i, true);                 sheet.setColumnWidth(i,  (int)((Integer.parseInt(cols.get(i).get("width").toString()) + 0.72) * 256));              } //            //如果list里有数组数据,加入到list里 //            String dataJson = JSONArray.toJSONString(data);// 先把list转换成json str数组格式 //            JSONArray objects = JSONArray.parseArray(dataJson); // 在转成json 数组 //            for (Object o : objects) { //                String s = JSONObject.toJSONString(o); //Object 对象转成jsonString //                JSONObject jsonObject = JSONObject.parseObject(s); //                jsonObject.forEach( (key, value) -> { //                    if (jsonObject.get(key) instanceof List) { // 如果这一行数据里有list数据,那么循环他并给给他放初始list里一并导出 //                        JSONArray jsonArray = jsonObject.getJSONArray(key); //                        for (Object o1 : jsonArray) { //                            data.add((T) o1); //                        } //                        System.out.println("是list类型"); //                    } else { //                        System.out.println("不是list类型"); //                    } //                }); //            } //            if (1==1) { //                throw new NullPointerException(); //            }             Field[] fields = className.getDeclaredFields();// 获取到反射类里所有的属性             Map<Integer, FieldExcel> fieldMap = new HashMap<>(); // integer: 存放的Excel列下标, map:存放的属性名称、类型、日期格式转换             data.forEach( o -> { // 数据拼接                 Row row = sheet.createRow(data.indexOf(o) + 1);// 创建一行数据                 String s = JSONObject.toJSONString(o); //Object 对象转成jsonString                 JSONObject jsonObject = JSONObject.parseObject(s); //jsonString转成JSONObject                 int mapIndex = 0; // 定义Excel列下标                 for (int i = 0; i < fields.length; i++) {// 循环找出如果有注解配置的字段添加到数组里                     boolean is = fields[i].isAnnotationPresent(Excel.class);                     if (is) {                         Excel annotation = fields[i].getAnnotation(Excel.class); // 获取注解里的属性                         fields[i].setAccessible(true); // 设置私有属性可见                         FieldExcel fieldExcel = new FieldExcel();                         if (!StringUtils.isEmpty(jsonObject.get(fields[i].getName()))) { // 如果反射里的字段可以在传入list里json对象找到就存找的数据,没有存空                             if (annotation.filterText().length > 0) { // 如果注解里配置了数组类型属性,将会被替换自负分割0 替换 1                                 String s1 = annotation.filterText()[0];                                 if (s1.startsWith("${") && s1.endsWith("}")) {// 用第一个元素做校验                                     try {                                         int startIndex = s1.indexOf("${") + 2;                                         int endIndex = s1.indexOf("}");                                         String methodName = s1.substring(startIndex, endIndex); // 获取到反射类里的方法名称这个是在注解上定义的名称不包含El表达式的名称                                         List<String> beanList = SpringUtils.getBean(methodName, List.class); // 获取实体里的list方法方式1:通过spring bean装载方式实现映射方法返回(实体类规范: 1.类必须交给spring管理 2.方法必须加注解Bean(name = "方法名")) //                                        Method getEnumRepairSate = className.getMethod(methodName); // 获取实体里的list方法方式2: 获取到方法实例通过java反射机制的方式加载射射方法返回 //                                        Object obj = className.newInstance(); // 生成反射加载类实例(调用实体类里带有无参数的构造方法) 从jvm角度看此类必须保证是加载了才能够使用此方法,而new不同new可以不用加载 //                                        List<Object> fieldCoverList = (List<Object>) getEnumRepairSate.invoke(obj); // 指定类实例转List<Object>类型                                         String[] fieldArr = beanList.toArray(new String[beanList.size()]); // List 转 数组                                         fieldExcel.setFieldArr(fieldArr);                                     } catch (Exception e) {                                         e.printStackTrace();                                     }                                 } else {                                     fieldExcel.setFieldArr(annotation.filterText());                                 }                             }                             fieldExcel.setFieldName(fields[i].getName());                             if (fields[i].getGenericType().toString().contains("Date")) { //如果属性类型包含Date说明是日期类型放入注解的format的属性                                 fieldExcel.setFieldFormat(annotation.format());                             }                         }                         fieldExcel.setFieldType(fields[i].getGenericType().toString());                         fieldMap.put(mapIndex, fieldExcel);                         mapIndex++;                     }                 }                 for (Map.Entry<Integer, FieldExcel> i : fieldMap.entrySet()) { //循环带有注解的字段依次排放数据(在加个循环的原因是:第一次循环有注解的for拼接数据会导致数据顺序错乱,因为注解的顺序不一定每个属性都有)                     FieldExcel value = i.getValue();                     if (value.getFieldArr().length > 0) {                         for (String s1 : value.getFieldArr()) {                             String[] split = s1.split(":"); // ":"号分割                             if (split[0].equals(jsonObject.get(value.getFieldName()).toString())) { // 0 替换 1                                 row.createCell(i.getKey()).setCellValue(split[1]);                             }                         }                     } else if (!StringUtils.isEmpty(value.getFieldFormat()) && value.getFieldType().contains("Date")) { // 如果日期格式转换属性不为空并且字段类型包含Date进行格式转换,如果没有就正常赋值                         SimpleDateFormat dateFormat = getDateFormat(jsonObject.get(value.getFieldName()).toString()); // 验证日期格式这里做了一个正则日期校验 斜杠和- 任意转换加时间或者不加时间                         Date fieldNameDate = null;                         try {                              fieldNameDate = dateFormat.parse(jsonObject.get(value.getFieldName()).toString());                         } catch (ParseException e) {                             e.printStackTrace();                         }                         SimpleDateFormat format = new SimpleDateFormat(value.getFieldFormat()); // 转换成注解定义的格式 yyyy/MM/dd HH:mm:ss yyyy-MM-dd HH:mm:ss ……                         String strFieldName = format.format(fieldNameDate);                         row.createCell(i.getKey()).setCellValue(strFieldName);                     } else {                         row.createCell(i.getKey()).setCellValue(value.getFieldName().equals("null_flag") ? "" : jsonObject.get(value.getFieldName()).toString());                     }                  }                 mapIndex = 0;                 fieldMap.clear();// 下标清0 map清空 进行下一次循环             });             return wb;         }catch (Exception e) {             e.printStackTrace();             return null;         }     }      private static SimpleDateFormat getDateFormat(String source) {         SimpleDateFormat sdf = new SimpleDateFormat();         sdf.setLenient(false);         if(Pattern.matches("^\\d{4}-\\d{2}-\\d{2}$",source)){             sdf = new SimpleDateFormat("yyyy-MM-dd");         }else if(Pattern.matches("^\\d{4}/\\d{2}/\\d{2}$",source)){             sdf = new SimpleDateFormat("yyyy/MM/dd");         }else if(Pattern.matches("^\\d{4}\\d{2}\\d{2}$",source)){             sdf = new SimpleDateFormat("yyyyMMdd");         }else if(Pattern.matches("^\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2}$",source)) {             sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");         }else if(Pattern.matches("^\\d{4}/\\d{2}/\\d{2}\\s\\d{2}:\\d{2}:\\d{2}$",source)) {             sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");         }else if(Pattern.matches("^\\d{4}\\d{2}\\d{2}\\s\\d{2}:\\d{2}:\\d{2}$",source)) {             sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss");         }else {             //这里抛出一个异常,             throw new TypeMismatchException("",Date.class);         }         return sdf;     } } 8.自定义接口调用 到了这一步这个方法已经注入到spring的管辖范围内了,就可以根据业务需求处理list然后导出啦!  package  这个你们放你们自己的包吧;  import  这个你们放你们自己的包吧; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Workbook; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service;  import javax.annotation.Resource; import java.io.ByteArrayOutputStream; import java.util.List;  @Service public class OrdCfmExport implements IOrdCfmExport {      @Override     public ResponseEntity<byte[]> export(List<Object> list, String className){         try {             ByteArrayOutputStream fileout = new ByteArrayOutputStream();             Workbook wb = new HSSFWorkbook();             Class<?> aClass = Class.forName(className); // 反射加载类实体             return ExportUtils.exportExcel(                     fileout,                     wb,                     list,                     "",                     (out, workbook, data) -> ExportUtils.createCell(                             wb,                             data,                             aClass).write(fileout));         } catch (Exception e) {             e.printStackTrace();             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);         }     }          @Override     public ResponseEntity<byte[]> export(List<Object> list, Class<?> className)                    {         try {             ByteArrayOutputStream fileout = new ByteArrayOutputStream();             Workbook wb = new HSSFWorkbook();             Class<?> aClass = className; // 反射加载类实体             return ExportUtils.exportExcel(                     fileout,                     wb,                     list,                     "",                     (out, workbook, data) -> ExportUtils.createCell(                             wb,                             data,                             aClass).write(fileout));         } catch (Exception e) {             e.printStackTrace();             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);         }     } }  9.最后贴一下vue代码         export() {             let that = this             let params = {}             params.data = that.exportBatchWrapper             params.baseUrl = that.url.baseUrlExport             framework.save(that.url.data.saveExport, params, 'EXPORT_POST').then((res) => {                 let blob = new Blob([res.data], { type: 'application/vnd.ms-excel' })                 // 其他浏览器                 let link = document.createElement('a') // 创建a标签                 link.style.display = 'none'                 let objectUrl = URL.createObjectURL(blob)                 link.href = objectUrl                 link.click()                 URL.revokeObjectURL(objectUrl)             }).catch((e) => {                 that.$message.error('导出失败,内部服务器错误!')             })         }      save: function (url, params, method) {         if (method === 'GET') {             return http.get(url, params.data, null, params.baseUrl)         } else if (method === 'POST') {             return http.post(url, params.data, null, params.baseUrl)         } else if (method === 'EXPORT_POST') {             return http.post(url, params.data, { responseType: 'blob' }, params.baseUrl)         }     } /**  * http post请求  * @param url请求的url地址  * @param params 基于post请求参数,格式必须是json,例如{id: '1111'}  * @param config 其他配置,一般是用于配置其他属性  */ export function post(url, data, config, baseUrl) {     let options = { url: url, data: data, method: 'post' }     let opt = config ? Object.assign(config, options) : options     if (baseUrl === '' || baseUrl === null || baseUrl === undefined) {         axios = new HttpRequest(getBaseUrl())         return axios.request(opt)     } else {         if (nodeEnv === 'production') {             axios = new HttpRequest(getBaseUrl() + baseUrl)         } else {             axios = new HttpRequest(baseUrl)         }         return axios.request(opt)     } } axios配置这里不做展示了,相信到这里就已经能明白了。后期如果有代码技术上的问题请后台联系我发起技术讨论大家相互学习。最后希望大家学无止境,欲穷千里目,更上一层楼。———————————————— 原文链接:https://blog.csdn.net/weixin_44556981/article/details/123677796 
  • [技术干货] Java中操作Excel的3种方法,太好用了!
    一、介绍在平时的业务系统开发中,少不了需要用到导出、导入excel功能,今天我们就一起来总结一下,如果你正为此需求感到困惑,那么阅读完本文,你一定会有所收获!二、poi大概在很久很久以前,微软的电子表格软件 Excel 以操作简单、存储数据直观方便,还支持打印报表,在诞生之初,可谓深得办公室里的白领青睐,极大的提升了工作的效率,不久之后,便成了办公室里的必备工具。随着更多的新语言的崛起,例如我们所熟悉的 java,后来便有一些团队开始开发一套能与 Excel 软件无缝切换的操作工具!这其中就有我们所熟悉的 apache 的 poi,其前身是 Jakarta 的 POI Project项目,之后将其开源给 apache 基金会!当然,在java生态体系里面,能与Excel无缝衔接的第三方工具还有很多,因为 apache poi 在业界使用的最广泛,因此其他的工具不做过多介绍!话不多说,直接开撸!2.1、首先引入apache poi的依赖<dependencies>    <!--xls(03)-->    <dependency>        <groupId>org.apache.poi</groupId>        <artifactId>poi</artifactId>        <version>4.1.2</version>    </dependency>    <!--xlsx(07)-->    <dependency>        <groupId>org.apache.poi</groupId>        <artifactId>poi-ooxml</artifactId>        <version>4.1.2</version>    </dependency>    <!--时间格式化工具-->    <dependency>        <groupId>joda-time</groupId>        <artifactId>joda-time</artifactId>        <version>2.10.6</version>    </dependency></dependencies>2.2、导出excel导出操作,即使用 Java 写出数据到 Excel 中,常见场景是将页面上的数据导出,这些数据可能是财务数据,也可能是商品数据,生成 Excel 后返回给用户下载文件。在 poi 工具库中,导出 api 可以分三种方式HSSF方式:这种方式导出的文件格式为office 2003专用格式,即.xls,优点是导出数据速度快,但是最多65536行数据XSSF方式:这种方式导出的文件格式为office 2007专用格式,即.xlsx,优点是导出的数据不受行数限制,缺点导出速度慢SXSSF方式:SXSSF 是 XSSF API的兼容流式扩展,主要解决当使用 XSSF 方式导出大数据量时,内存溢出的问题,支持导出大批量的excel数据2.2.1、HSSF方式导出HSSF方式,最多只支持65536条数据导出,超过这个条数会报错!public class ExcelWrite2003Test {    public static String PATH = "/Users/hello/Desktop/";    public static void main(String[] args) throws Exception {        //时间        long begin = System.currentTimeMillis();        //创建一个工作簿        Workbook workbook = new HSSFWorkbook();        //创建表        Sheet sheet = workbook.createSheet();        //写入数据        for (int rowNumber = 0; rowNumber < 65536; rowNumber++) {            //创建行            Row row = sheet.createRow(rowNumber);            for (int cellNumber = 0; cellNumber < 10; cellNumber++) {                //创建列                Cell cell = row.createCell(cellNumber);                cell.setCellValue(cellNumber);            }        }        System.out.println("over");        FileOutputStream fileOutputStream = new FileOutputStream(PATH + "用户信息表2003BigData.xls");        workbook.write(fileOutputStream);        fileOutputStream.close();        long end = System.currentTimeMillis();        System.out.println((double) (end - begin) / 1000);//4.29s    }}2.2.2、XSSF方式导出XSSF方式支持大批量数据导出,所有的数据先写入内存再导出,容易出现内存溢出!public class ExcelWrite2007Test {    public static String PATH = "/Users/hello/Desktop/";    public static void main(String[] args) throws Exception {        //时间        long begin = System.currentTimeMillis();        //创建一个工作簿        Workbook workbook = new XSSFWorkbook();        //创建表        Sheet sheet = workbook.createSheet();        //写入数据        for (int rowNumber = 0; rowNumber < 65537; rowNumber++) {            Row row = sheet.createRow(rowNumber);            for (int cellNumber = 0; cellNumber < 10; cellNumber++) {                Cell cell = row.createCell(cellNumber);                cell.setCellValue(cellNumber);            }        }        System.out.println("over");        FileOutputStream fileOutputStream = new FileOutputStream(PATH + "用户信息表2007BigData.xlsx");        workbook.write(fileOutputStream);        fileOutputStream.close();        long end = System.currentTimeMillis();        System.out.println((double) (end - begin) / 1000);//15.87s    }}2.2.3、SXSSF方式导出SXSSF方式是XSSF方式的一种延伸,主要特性是低内存,导出的时候,先将数据写入磁盘再导出,避免报内存不足,导致程序运行异常,缺点是运行很慢!public class ExcelWriteSXSSFTest {    public static String PATH = "/Users/hello/Desktop/";    public static void main(String[] args) throws Exception {        //时间        long begin = System.currentTimeMillis();        //创建一个工作簿        Workbook workbook = new SXSSFWorkbook();        //创建表        Sheet sheet = workbook.createSheet();        //写入数据        for (int rowNumber = 0; rowNumber < 100000; rowNumber++) {            Row row = sheet.createRow(rowNumber);            for (int cellNumber = 0; cellNumber < 10; cellNumber++) {                Cell cell = row.createCell(cellNumber);                cell.setCellValue(cellNumber);            }        }        System.out.println("over");        FileOutputStream fileOutputStream = new FileOutputStream(PATH + "用户信息表2007BigDataS.xlsx");        workbook.write(fileOutputStream);        fileOutputStream.close();        long end = System.currentTimeMillis();        System.out.println((double) (end - begin) / 1000);//6.39s    }}2.3、导入excel导入操作,即将 excel 中的数据采用java工具库将其解析出来,进而将 excel 数据写入数据库!同样,在 poi 工具库中,导入 api 也分三种方式,与上面的导出一一对应!五、小结总体来说,easypoi和easyexcel都是基于apache poi进行二次开发的。不同点在于:1、easypoi 在读写数据的时候,优先是先将数据写入内存,优点是读写性能非常高,但是当数据量很大的时候,会出现oom,当然它也提供了 sax 模式的读写方式,需要调用特定的方法实现。2、easyexcel 基于sax模式进行读写数据,不会出现oom情况,程序有过高并发场景的验证,因此程序运行比较稳定,相对于 easypoi 来说,读写性能稍慢!easypoi 与 easyexcel 还有一点区别在于,easypoi 对定制化的导出支持非常的丰富,如果当前的项目需求,并发量不大、数据量也不大,但是需要导出 excel 的文件样式千差万别,那么我推荐你用 easypoi;反之,使用 easyexcel !六、参考1、apache poi - 接口文档2、easypoi - 接口文档3、easyexcel - 接口文档原文链接:https://blog.csdn.net/sufu1065/article/details/115301974
  • [技术干货] Java操作Excel三种方式POI、Hutool、EasyExcel
    Java操作Excel三种方式POI、Hutool、EasyExcel 1. Java操作Excel概述 1.1 Excel需求概述 1.2 Excel操作三种方式对比 2. ApachePOIExcel 2.1 ApachePOI简介 2.2 ApachePOI功能结构 2.3 ApachePOI官网说明 2.4 ApachePOI实现验证 3. HutoolExcel 3.1 Hutool简介 3.2 Hutool组件 3.1 HutoolExcel实现验证 4. EasyExcel 4.1 EasyExcel简介 4.2 HutoolExcel实现验证 5. 源码地址 1. Java操作Excel概述 1.1 Excel需求概述 Excel作为常用的报表文件,项目开发中经常用到Excel操作功能,对于Excel操作目前常见的有三种方式,Apache的POI,Hutool的ExcelUtil,Ali的EasyExcel。  1.2 Excel操作三种方式对比 Apache的POI相对比较原生,功能强大,使用复杂繁琐,内存占用比较大。 Hutool的ExcelUtil在POI基础上进行封装,简单易用,功能不太全,对于一些特殊操作,如标注等不具备。 Ali的EasyExcel也是在POI基础上进行封装,简单易用,功能相对比较全面,且性能十分强悍,最为推荐。  2. ApachePOIExcel 2.1 ApachePOI简介 Apache POI [1] 是用Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java程式对Microsoft Office格式档案读和写的功能。POI为“Poor Obfuscation Implementation”的首字母缩写,意为“简洁版的模糊实现”。  2.2 ApachePOI功能结构 HSSF [1]  - 提供读写Microsoft Excel XLS格式档案的功能。 XSSF [1]  - 提供读写Microsoft Excel OOXML XLSX格式档案的功能。 HWPF [1]  - 提供读写Microsoft Word DOC格式档案的功能。 HSLF [1]  - 提供读写Microsoft PowerPoint格式档案的功能。 HDGF [1]  - 提供读Microsoft Visio格式档案的功能。 HPBF [1]  - 提供读Microsoft Publisher格式档案的功能。 HSMF [1]  - 提供读Microsoft Outlook格式档案的功能。 2.3 ApachePOI官网说明 POI官网地址:https://poi.apache.org/index.html POI下载地址:https://poi.apache.org/download.html POI文档说明:https://poi.apache.org/apidocs/index.html  2.4 ApachePOI实现验证 Maven依赖配置          <!-- poi 相关 -->         <dependency>             <groupId>org.apache.poi</groupId>             <artifactId>poi</artifactId>             <version>4.0.1</version>         </dependency>         <dependency>             <groupId>org.apache.poi</groupId>             <artifactId>poi-ooxml</artifactId>             <version>4.0.1</version>         </dependency>         <dependency>             <groupId>org.apache.poi</groupId>             <artifactId>poi-ooxml-schemas</artifactId>             <version>4.0.1</version>         </dependency>  代码测试  package com.zrj.easyexcel.excel;  import com.google.common.collect.Lists; import org.junit.Test;  import java.io.File; import java.util.HashMap; import java.util.List; import java.util.Map;  /**  * 原生Excel测试类  *  * @author zrj  * @since 2021/12/29  **/ public class EasyExcelPoi {      @Test     public void createExcelTest() throws Exception {         //文件的基础路径         String fileBasePath = System.getProperty("user.dir") + File.separator + "excel" + File.separator;         System.out.println("文件的基础路径:" + fileBasePath);          String filepath = fileBasePath + "writeexcel.xlsx";         String sheetName = "模板名称";         List<String> titles = Lists.newArrayList("name", "sex", "age");         List<Map<String, Object>> values = Lists.newArrayList();          Map map2 = new HashMap(6);         map2.put("name", "王五");         map2.put("sex", "女");         map2.put("age", "20");         values.add(map2);          Map map3 = new HashMap(6);         map3.put("name", "阿萨");         map3.put("sex", "女");         map3.put("age", "66");         values.add(map3);          ExcelUtils.writeExcel(filepath, sheetName, titles, values);         System.out.println("创建Excel完成");     }      @Test     public void copyExcelTest() throws Exception {         //文件的基础路径         String fileBasePath = System.getProperty("user.dir") + File.separator + "excel" + File.separator;         System.out.println("文件的基础路径:" + fileBasePath);          String filepath = fileBasePath + "writeexcel.xlsx";         String fileNewPath = fileBasePath + "writeexcel2.xlsx";         ExcelUtils.writeExcel(filepath, fileNewPath);         System.out.println("复制Excel完成");     }      @Test     public void readExcelTest() throws Exception {         //文件的基础路径         String fileBasePath = System.getProperty("user.dir") + File.separator + "excel" + File.separator;         String filepath = fileBasePath + "writeexcel.xlsx";         System.out.println("文件路径:" + fileBasePath);          String readExcel = ExcelUtils.readExcel(filepath);         System.out.println(readExcel);         System.out.println("读取Excel完成");     }  } 3. HutoolExcel 3.1 Hutool简介 Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。 Hutool中的工具方法来自每个用户的精雕细琢,它涵盖了Java开发底层代码中的方方面面,它既是大型项目开发中解决小问题的利器,也是小型项目中的效率担当; Hutool是项目中“util”包友好的替代,它节省了开发人员对项目中公用类和公用工具方法的封装时间,使开发专注于业务,同时可以最大限度的避免封装不完善带来的bug。  项目中使用非常广泛的一个小工具,没有依赖,非常干净好用。 官网地址:https://www.hutool.cn/  参考文档:https://www.hutool.cn/docs/#/ API文档:https://apidoc.gitee.com/dromara/hutool/  GITEE:https://gitee.com/dromara/hutool/ GITHUB:https://github.com/dromara/hutool/  视频介绍:https://player.bilibili.com/player.html?aid=710062843&bvid=BV1bQ4y1M7d9&cid=170377135&page=2  3.2 Hutool组件 3.1 HutoolExcel实现验证 Mave依赖配置  <dependency>     <groupId>cn.hutool</groupId>     <artifactId>hutool-all</artifactId>     <version>5.7.18</version> </dependency> 代码实现验证  package com.zrj.easyexcel.excel;  import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IoUtil; import cn.hutool.poi.excel.ExcelReader; import cn.hutool.poi.excel.ExcelUtil; import cn.hutool.poi.excel.ExcelWriter; import com.zrj.easyexcel.utils.FileUtils; import org.junit.Test;  import java.io.ByteArrayOutputStream; import java.io.File; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map;  /**  * HutoolExcel测试类  * 注意:不要到错包了,easyexcel与hutool的类名一样!!!  *  * @author zrj  * @since 2021/12/29  **/ public class EasyExcelHutool {      /*******************************Excel读取-ExcelReader*******************************************/      /**      * 1. 读取Excel中所有行和列,都用列表表示      */     @Test     public void readExcelObjectTest() {         //定义文件路径         String filepath = System.getProperty("user.dir") + File.separator + "excel" + File.separator + "writeBeanKeyTest.xlsx";         //通过工具类创建ExcelReader         ExcelReader reader = ExcelUtil.getReader(filepath);         List<List<Object>> readAll = reader.read();         System.out.println("Excel读取结果:" + readAll);     }      /**      * 2. 读取为Map列表,默认第一行为标题行,Map中的key为标题,value为标题对应的单元格值。      */     @Test     public void readExcelMapTest() {         //定义文件路径         String filepath = System.getProperty("user.dir") + File.separator + "excel" + File.separator + "writeBeanKeyTest.xlsx";         //通过工具类创建ExcelReader         ExcelReader reader = ExcelUtil.getReader(filepath);         List<Map<String, Object>> readAll = reader.readAll();         System.out.println("Excel读取结果:" + readAll);     }      /**      * 3. 读取为Bean列表,Bean中的字段名为标题,字段值为标题对应的单元格值。      */     @Test     public void readExcelBeanTest() {         //定义文件路径         String filepath = System.getProperty("user.dir") + File.separator + "excel" + File.separator + "writeBeanKeyTest.xlsx";         //通过工具类创建ExcelReader         ExcelReader reader = ExcelUtil.getReader(filepath);         List<UserBean> readAll = reader.readAll(UserBean.class);         System.out.println("Excel读取结果:" + readAll);     }      /*******************************Excel生成-ExcelWriter*******************************************/      /**      * 1. 将行列对象写出到Excel      */     @Test     public void writeExcelObjectTest() {         //定义一个嵌套的List,List的元素也是一个List,内层的一个List代表一行数据,每行都有4个单元格,最终list对象代表多行数据。         List<String> row1 = CollUtil.newArrayList("aa", "bb", "cc", "dd");         List<String> row2 = CollUtil.newArrayList("aa1", "bb1", "cc1", "dd1");         List<String> row3 = CollUtil.newArrayList("aa2", "bb2", "cc2", "dd2");         List<String> row4 = CollUtil.newArrayList("aa3", "bb3", "cc3", "dd3");         List<String> row5 = CollUtil.newArrayList("aa4", "bb4", "cc4", "dd4");          List<List<String>> rows = CollUtil.newArrayList(row1, row2, row3, row4, row5);          //定义文件路径         String filepath = System.getProperty("user.dir") + File.separator + "excel" + File.separator + "writeTest.xlsx";         //通过工具类创建writer         ExcelWriter writer = ExcelUtil.getWriter(filepath);         //通过构造方法创建writer         //ExcelWriter writer = new ExcelWriter("d:/writeTest.xls");          //跳过当前行,既第一行,非必须,在此演示用         writer.passCurrentRow();          //合并单元格后的标题行,使用默认标题样式         writer.merge(row1.size() - 1, "测试标题");         //一次性写出内容,强制输出标题         writer.write(rows, true);         //关闭writer,释放内存         writer.close();         System.out.println("创建完成,文件路径:" + filepath);     }      /**      * 2. 写出Map数据      */     @Test     public void writeExcelMapTest() {         Map<String, Object> row1 = new LinkedHashMap<>();         row1.put("姓名", "张三");         row1.put("年龄", 23);         row1.put("成绩", 88.32);         row1.put("是否合格", true);         row1.put("考试日期", DateUtil.date());          Map<String, Object> row2 = new LinkedHashMap<>();         row2.put("姓名", "李四");         row2.put("年龄", 33);         row2.put("成绩", 59.50);         row2.put("是否合格", false);         row2.put("考试日期", DateUtil.date());          ArrayList<Map<String, Object>> rows = CollUtil.newArrayList(row1, row2);          //定义文件路径         String filepath = System.getProperty("user.dir") + File.separator + "excel" + File.separator + "writeMapTest.xlsx";         // 通过工具类创建writer         ExcelWriter writer = ExcelUtil.getWriter(filepath);         // 合并单元格后的标题行,使用默认标题样式         writer.merge(row1.size() - 1, "一班成绩单");         // 一次性写出内容,使用默认样式,强制输出标题         writer.write(rows, true);         // 关闭writer,释放内存         writer.close();         System.out.println("创建完成,文件路径:" + filepath);     }      /**      * 3. 写出Bean数据      */     @Test     public void writeExcelBeanTest() {         UserBean bean1 = UserBean.builder().name("张三").age(22).isPass(true).score(66.30).examDate(DateUtil.date()).build();         UserBean bean2 = UserBean.builder().name("李四").age(30).isPass(false).score(38.50).examDate(DateUtil.date()).build();          List<UserBean> rows = CollUtil.newArrayList(bean1, bean2);          //定义文件路径         String filepath = System.getProperty("user.dir") + File.separator + "excel" + File.separator + "writeBeanTest.xlsx";         // 通过工具类创建writer         ExcelWriter writer = ExcelUtil.getWriter(filepath);         // 合并单元格后的标题行,使用默认标题样式         writer.merge(4, "一班成绩单");         // 一次性写出内容,使用默认样式,强制输出标题         writer.write(rows, true);         // 关闭writer,释放内存         writer.close();         System.out.println("创建完成,文件路径:" + filepath);     }      /**      * 4. 自定义Bean的key别名(排序标题)      * 注意:与3的区别是,3中标题是英文,4中标题是自定义的别名      */     @Test     public void writeExcelBeanKeyTest() {         UserBean bean1 = UserBean.builder().name("张三").age(22).isPass(true).score(66.30).examDate(DateUtil.date()).build();         UserBean bean2 = UserBean.builder().name("李四").age(30).isPass(false).score(38.50).examDate(DateUtil.date()).build();          List<UserBean> rows = CollUtil.newArrayList(bean1, bean2);          //定义文件路径         String filepath = System.getProperty("user.dir") + File.separator + "excel" + File.separator + "writeBeanKeyTest.xlsx";         // 通过工具类创建writer         ExcelWriter writer = ExcelUtil.getWriter(filepath);          //自定义标题别名         writer.addHeaderAlias("name", "姓名");         writer.addHeaderAlias("age", "年龄");         writer.addHeaderAlias("score", "分数");         writer.addHeaderAlias("isPass", "是否通过");         writer.addHeaderAlias("examDate", "考试时间");          // 默认的,未添加alias的属性也会写出,如果想只写出加了别名的字段,可以调用此方法排除之         writer.setOnlyAlias(true);          // 合并单元格后的标题行,使用默认标题样式         writer.merge(4, "一班成绩单");         // 一次性写出内容,使用默认样式,强制输出标题         writer.write(rows, true);         // 关闭writer,释放内存         writer.close();         System.out.println("创建完成,文件路径:" + filepath);     }      /**      * 5. 写出到流      */     @Test     public void writeExcelStreamTest() {         //定义内容         UserBean bean1 = UserBean.builder().name("张三").age(22).isPass(true).score(66.30).examDate(DateUtil.date()).build();         UserBean bean2 = UserBean.builder().name("李四").age(30).isPass(false).score(38.50).examDate(DateUtil.date()).build();         List<UserBean> rows = CollUtil.newArrayList(bean1, bean2);          //定义输出流         ByteArrayOutputStream out = FileUtils.cloneInputStream(IoUtil.toUtf8Stream(rows.toString()));          //定义文件路径         String filepath = System.getProperty("user.dir") + File.separator + "excel" + File.separator + "writeStreamTest.xlsx";         if (!FileUtil.isFile(filepath)) {             FileUtil.touch(filepath);         }         //通过工具类创建writer         ExcelWriter writer = ExcelUtil.getWriter();         //创建xlsx格式的         //ExcelWriter writer = ExcelUtil.getWriter(true);         //一次性写出内容,使用默认样式,强制输出标题         writer.write(rows, true);         //out为OutputStream,需要写出到的目标流         writer.flush(out);         //关闭writer,释放内存         writer.close();         System.out.println("创建完成,文件路径:" + filepath);     }      /**      * 6. 写出到客户端下载(写出到Servlet)      */     @Test     public void writeExcelDownloadTest() {         //定义内容         UserBean bean1 = UserBean.builder().name("张三").age(22).isPass(true).score(66.30).examDate(DateUtil.date()).build();         UserBean bean2 = UserBean.builder().name("李四").age(30).isPass(false).score(38.50).examDate(DateUtil.date()).build();         List<UserBean> rows = CollUtil.newArrayList(bean1, bean2);          // 通过工具类创建writer,默认创建xls格式         ExcelWriter writer = ExcelUtil.getWriter();         // 一次性写出内容,使用默认样式,强制输出标题         writer.write(rows, true);         //out为OutputStream,需要写出到的目标流          //response为HttpServletResponse对象         //response.setContentType("application/vnd.ms-excel;charset=utf-8");         //test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码         //response.setHeader("Content-Disposition", "attachment;filename=test.xls");         //ServletOutputStream out = response.getOutputStream();          //writer.flush(out, true);         //关闭writer,释放内存         writer.close();         //此处记得关闭输出Servlet流         //IoUtil.close(out);     }  } 4. EasyExcel 4.1 EasyExcel简介 EasyExcel是阿里巴巴开源的一款Excel操作工具,简单易用,功能强大,节省内存,性能彪悍。  语雀地址:https://www.yuque.com/easyexcel/doc/easyexcel Github地址: https://github.com/alibaba/easyexcel mvn依赖地址:https://mvnrepository.com/artifact/com.alibaba/easyexcel  4.2 HutoolExcel实现验证 Mave依赖配置          <dependency>             <groupId>com.alibaba</groupId>             <artifactId>easyexcel</artifactId>             <version>3.0.5</version>         </dependency> EasyExcel基本实现案例 Github案例地址:https://github.com/alibaba/easyexcel 项目路径:test -> read/write -> ReadTest与WriteTest类,实现基本Excel各种读写功能。  EasyExcel批量添加批注 ByteToInputStreamUtil  package com.zrj.easyexcel.excel;  import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream;  /**  * 输入流与字节转换  *  * @author zrj  * @since 2021/12/27  **/ public class ByteToInputStreamUtil {     /**      * 字节转输入流      *      * @param buf      * @return java.io.InputStream      */     public static InputStream byte2Input(byte[] buf) {         return new ByteArrayInputStream(buf);     }      /**      * 输入流转字节      *      * @param inStream      * @return byte[]      */     public static byte[] input2byte(InputStream inStream) throws Exception {         ByteArrayOutputStream swapStream = new ByteArrayOutputStream();         byte[] buff = new byte[100];         int rc = 0;         while ((rc = inStream.read(buff, 0, 100)) > 0) {             swapStream.write(buff, 0, rc);         }         byte[] in2b = swapStream.toByteArray();         return in2b;     } } CommentWriteHandler  package com.zrj.easyexcel.excel;  import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.excel.write.handler.AbstractRowWriteHandler; import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; import com.alibaba.excel.write.metadata.holder.WriteTableHolder; import org.apache.poi.hssf.usermodel.HSSFClientAnchor; import org.apache.poi.hssf.usermodel.HSSFRichTextString; import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFClientAnchor; import org.apache.poi.xssf.usermodel.XSSFRichTextString;  import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors;  /**  * 批注处理器  *  * @author zrj  * @since 2021/12/29  **/ public class CommentWriteHandler extends AbstractRowWriteHandler {      /**      * sheet名称KEY      */     public static final String SHEETNAME_NAME = "sheetName";      /**      * 文档后缀名      */     private String extension;      /**      * 列索引key      */     public static final String COLINDEX_NAME = "colIndex";      /**      * 行索引key      */     public static final String ROWINDEX_NAME = "rowIndex";      /**      * 批注内容key      */     public static final String COMMENTCONTENT_NAME = "commentContent";      /**      * sheet页名称列表      */     private List<String> sheetNameList;      /**      * 批注集合      */     List<Map<String, String>> commentList = new ArrayList<>(10);      /**      * CommentWriteHandler      *      * @param commentList      * @param extension      */     public CommentWriteHandler(List<Map<String, String>> commentList, String extension) {         this.commentList = commentList != null && commentList.size() > 0                 ? commentList.stream().filter(x ->                 x.keySet().contains(SHEETNAME_NAME) == true && x.get(SHEETNAME_NAME) != null && StrUtil.isNotBlank(x.get(SHEETNAME_NAME).toString())                         && x.keySet().contains(COLINDEX_NAME) == true && x.get(COLINDEX_NAME) != null && StrUtil.isNotBlank(x.get(COLINDEX_NAME).toString())                         && x.keySet().contains(ROWINDEX_NAME) == true && x.get(ROWINDEX_NAME) != null && StrUtil.isNotBlank(x.get(ROWINDEX_NAME).toString())                         && x.keySet().contains(COMMENTCONTENT_NAME) == true && x.get(COMMENTCONTENT_NAME) != null && StrUtil.isNotBlank(x.get(COMMENTCONTENT_NAME).toString())         ).collect(Collectors.toList()) : new ArrayList<>();         sheetNameList = this.commentList.stream().map(x -> x.get(SHEETNAME_NAME).toString()).collect(Collectors.toList());         this.extension = extension;     }      /**      * 生成批注信息      *      * @param sheetName      sheet页名称      * @param rowIndex       行号      * @param columnIndex    列号      * @param commentContent 批注内容      * @return      */     public static Map<String, String> createCommentMap(String sheetName, int rowIndex, int columnIndex, String commentContent) {         Map<String, String> map = new HashMap<>();         //sheet页名称         map.put(SHEETNAME_NAME, sheetName);         //行号         map.put(ROWINDEX_NAME, rowIndex + "");         //列号         map.put(COLINDEX_NAME, columnIndex + "");         //批注内容         map.put(COMMENTCONTENT_NAME, commentContent);         return map;     }      /**      * 功能描述      * @param writeSheetHolder      * @param writeTableHolder      * @param row      * @param relativeRowIndex      * @param isHead      * @return void      */     @Override     public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,                                 Integer relativeRowIndex, Boolean isHead) {         Sheet sheet = writeSheetHolder.getSheet();         //不需要添加批注,或者当前sheet页不需要添加批注         if (commentList == null || commentList.size() <= 0 || sheetNameList.contains(sheet.getSheetName()) == false) {             return;         }         //获取当前行的批注信息         List<Map<String, String>> rowCommentList = commentList.stream().filter(x ->                 StrUtil.equals(x.get(SHEETNAME_NAME).toString(), sheet.getSheetName())                         && relativeRowIndex == Integer.parseInt(x.get(ROWINDEX_NAME))).collect(Collectors.toList());         //当前行没有批注信息         if (rowCommentList == null || rowCommentList.size() <= 0) {             return;         }         List<String> colIndexList = rowCommentList.stream().map(x -> x.get(COLINDEX_NAME)).distinct().collect(Collectors.toList());         for (String colIndex : colIndexList) {             //同一单元格的批注信息             List<Map<String, String>> cellCommentList = rowCommentList.stream().filter(x ->                     StrUtil.equals(colIndex, x.get(COLINDEX_NAME))).collect(Collectors.toList());             if (CollectionUtil.isEmpty(cellCommentList)) {                 continue;             }             //批注内容拼成一条             String commentContent = cellCommentList.stream().map(x -> x.get(COMMENTCONTENT_NAME)).collect(Collectors.joining());             Cell cell = row.getCell(Integer.parseInt(colIndex));             addComment(cell, commentContent, extension);         }         //删除批注信息         commentList.remove(rowCommentList);         //重新获取要添加的sheet页姓名         sheetNameList = commentList.stream().map(x -> x.get(SHEETNAME_NAME).toString()).collect(Collectors.toList());     }      /**      * 给Cell添加批注      *      * @param cell      单元格      * @param value     批注内容      * @param extension 扩展名      */     public static void addComment(Cell cell, String value, String extension) {         Sheet sheet = cell.getSheet();         cell.removeCellComment();         if ("xls".equals(extension)) {             ClientAnchor anchor = new HSSFClientAnchor();             // 关键修改             anchor.setDx1(0);             anchor.setDx2(0);             anchor.setDy1(0);             anchor.setDy2(0);             anchor.setCol1(cell.getColumnIndex());             anchor.setRow1(cell.getRowIndex());             anchor.setCol2(cell.getColumnIndex() + 5);             anchor.setRow2(cell.getRowIndex() + 6);             // 结束             Drawing drawing = sheet.createDrawingPatriarch();             Comment comment = drawing.createCellComment(anchor);             // 输入批注信息             comment.setString(new HSSFRichTextString(value));             // 将批注添加到单元格对象中             cell.setCellComment(comment);         } else if ("xlsx".equals(extension)) {             ClientAnchor anchor = new XSSFClientAnchor();             // 关键修改             anchor.setDx1(0);             anchor.setDx2(0);             anchor.setDy1(0);             anchor.setDy2(0);             anchor.setCol1(cell.getColumnIndex());             anchor.setRow1(cell.getRowIndex());             anchor.setCol2(cell.getColumnIndex() + 5);             anchor.setRow2(cell.getRowIndex() + 6);             // 结束             Drawing drawing = sheet.createDrawingPatriarch();             Comment comment = drawing.createCellComment(anchor);             // 输入批注信息             comment.setString(new XSSFRichTextString(value));             // 将批注添加到单元格对象中             cell.setCellComment(comment);         }     } } EasyExcelUtils  package com.zrj.easyexcel.excel;  import com.alibaba.excel.EasyExcel; import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.write.builder.ExcelWriterBuilder; import com.alibaba.excel.write.metadata.WriteSheet; import com.zrj.easyexcel.entity.DemoData; import com.zrj.easyexcel.excel.read.ReadTest; import com.zrj.easyexcel.excel.write.WriteTest; import org.junit.Test;  import java.io.File; import java.util.*;  /**  * EasyExcel测试类  *  * @author zrj  * @since 2021/12/29  **/ public class EasyExcelUtils {     /**      * EasyExcel读写测试类      */     @Test     public void easyExcelReadWriteTest() {         //EasyExcel读测试类         ReadTest readTest = new ReadTest();         //EasyExcel写测试类         WriteTest writeTest = new WriteTest();     }     /**      * EasyExcel 批量添加批注      */     @Test     public void batchAddCommentTest() {         try {             //构建数据             List<DemoData> demoDataList = new ArrayList<>(10);             demoDataList.add(DemoData.builder().string("张三").date(new Date()).doubleData(3.14).build());             demoDataList.add(DemoData.builder().string("王五").date(new Date()).doubleData(6.68).build());             demoDataList.add(DemoData.builder().string("赵六").date(new Date()).doubleData(8.32).build());             demoDataList.add(DemoData.builder().string("李四").date(new Date()).doubleData(8.66).build());              //定义文件路径             String filepath = System.getProperty("user.dir") + File.separator + "excel" + File.separator + "EasyExcelCommentWriteTest3.xlsx";             String sheetName = "批注模板";             List<Map<String, String>> commentList = new ArrayList<>();             commentList.add(CommentWriteHandler.createCommentMap(sheetName, 0, 1, "第一条批注。"));             commentList.add(CommentWriteHandler.createCommentMap(sheetName, 1, 1, "第二条批注。"));              // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭             // 这里要注意inMemory 要设置为true,才能支持批注。目前没有好的办法解决 不在内存处理批注。这个需要自己选择。             EasyExcel.write(filepath, DemoData.class).inMemory(Boolean.TRUE)                     .registerWriteHandler(new CommentWriteHandler(commentList, "xlsx"))                     .sheet(sheetName).doWrite(demoDataList);              System.out.println("批注模板完成,模板地址:" + filepath);         } catch (Exception e) {             e.printStackTrace();         }     } } 5. 源码地址 测试源码地址:https://gitee.com/rjzhu/opencode/tree/master/easyexcel ———————————————— 原文链接:https://blog.csdn.net/m0_37583655/article/details/122233640 
  • [技术干货] Java Stream流详解-转载
     学习目标 看玩这篇将会: 1.了解stream流 2.学会使用stream流 3.掌握stream流的使用场景 每个方法通过举例子的形式学习!  Stream API主要提供了两种类型的操作:中间操作 和 终止操作。  中间操作 中间操作是返回一个新的流,并在返回的流中包含所有之前的操作结果。它们总是延迟计算,这意味着它们只会在终止操作时执行,这样可以最大限度地优化资源使用。  Filter(过滤)  filter()方法接受一个谓词(一个返回boolean值的函数),并返回一个流,其中仅包含通过该谓词的元素。 建一个数组,帅选出长度大于4的元素 eg: public class Main {     public static void main(String[] args) {         List<String> names = Arrays.asList("Alex", "Brian", "Charles", "David");         List<String> collect = names.stream().filter(item -> item.length() > 4).collect(Collectors.toList());         System.out.println(collect);     } } 这段代码创建了一个包含4个字符串的List集合,然后使用Stream()方法将其转化为一个Stream流。接下来使用filter()方法筛选出长度大于4的字符串,返回一个新的包含符合条件元素的Stream流collect。最后使用collect()方法将筛选后的结果转换成一个List集合。 使用Stream流中的filter()方法可以对流中的元素进行筛选过滤。在这段代码中,lambda表达式item -> item.length() > 4指定了筛选判断条件,即只保留长度大于4的字符串。collect(Collectors.toList())则将筛选后的结果转换成一个List集合返回。 通过这段代码,开发人员可以对包含字符串的数据进行快速的筛选和过滤,并且返回结果是一个新的可操作的集合,方便后续进行处理或展示。  Map(转换)  map()方法可将一个流的元素转换为另一个流。它接受一个函数,该函数映射流中的每个元素到另一个元素。 public class Main {     public static void main(String[] args) {         List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);         List<Integer> collect = numbers.stream().map(n ->         {             n = n * 2;             return n;         }).collect(Collectors.toList());         for (Integer integer : collect) {             System.out.println("integer = " + integer);         }     } } 这段代码使用了 Java 8 中的 Stream API 实现了一种对数字列表中的每个元素进行乘以 2 的操作,并将操作后的结果保存到新的列表中。  首先创建了一个包含数字 1~5 的列表。 然后利用 stream() 方法将列表转换成 Stream 对象。 接下来调用 map() 方法对每个元素进行操作,这里使用了 lambda 表达式对每个元素进行了乘以 2 的操作。 最后调用 collect() 方法将结果收集起来,并转换成 List。   转换为Map public class Main {     public static void main(String[] args) {         List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);         Map<Byte, Integer> collect = numbers.stream().collect(Collectors.toMap(Integer::byteValue, item -> item*2, (val1, val2) -> val2));         for (Map.Entry<Byte, Integer> byteIntegerEntry : collect.entrySet()) {             Byte key = byteIntegerEntry.getKey();             System.out.println("key = " + key);             System.out.println("Value = " + byteIntegerEntry.getValue());         }     } }  这段代码使用了 Java 8 中的 Stream API 实现了一种将数字列表转换成字节-整数键值对的方式。具体来说,代码中: 首先创建了一个包含数字 1~5 的列表。 然后利用 stream() 方法将列表转换成 Stream 对象。 接下来调用 collect(Collectors.toMap(…)) 方法将 Stream 转换成 Map<Byte, Integer>。 在 toMap 方法中,我们以每个整数的字节值为键,该整数乘以 2 为值,当遇到重复的键时取最后一个值。(这里实际上可以用任何能区分不同键的方式作为第一个参数,而不一定是 Integer::byteValue) 最后,在 for 循环中遍历了这个 Map 并打印出每个键值对的内容。 总的来说,通过 Stream API 可以方便地实现对集合数据进行筛选、映射、分组、统计等各种操作,相对于传统的循环遍历方式更为简洁、可读性更高,可以提高开发效率。  Sorted(排序)  sorted()方法可对流进行排序。它可以接受一个Comparator参数,也可以使用自然排序Ordering.natural()。默认排序是按升序排序。 public class Main {     public static void main(String[] args) {         int[] numbers = { 5, 2, 8, 3, 7 };         int[] sortedNumbers = Arrays.stream(numbers).sorted().toArray();         System.out.println(Arrays.toString(sortedNumbers));     } } 这段代码创建了一个包含整数的数组numbers,然后使用Arrays.stream()方法将其转化为一个IntStream流。接下来使用sorted()方法对流中的元素进行排序操作,返回一个新的排序后的IntStream流。最后,使用toArray()方法将排序后的结果转换为一个新的int类型数组sortedNumbers,并使用Arrays.toString()方法将结果输出到控制台。 使用Stream流可以简化代码,提高效率和可读性,方便开发人员对数据进行快速处理和排序。  Distinct(去重)  distinct()方法从流中返回所有不同的元素。在内部,它使用equals()方法来比较元素是否相同。因此,我们需要确保equals()方法已正确实现。 public class Main {     public static void main(String[] args) {         List<Integer> numbers = Arrays.asList(1, 2, 3, 2, 1);         List<Integer> collect = numbers.stream().distinct().collect(Collectors.toList());         System.out.println(collect);     } } 这段代码创建了一个包含整数的List集合numbers,其中包含了若干个重复的整数。接下来使用Stream()方法将其转化为一个Stream流。使用distinct()方法对流中的元素进行去重操作,返回一个新的不包含重复元素的Stream流collect。最后使用collect()方法将去重后的结果转换成一个List集合,并使用System.out.println()方法输出到控制台。 使用Stream流中的distinct()方法可以快速地对集合中的重复元素进行去重处理。在这段代码中,集合中的元素都是整数,使用distinct()方法去除了所有重复的整数,返回一个新的元素不重复且顺序不变的List集合。 运行该示例代码,输出结果为:[1, 2, 3],即去重后的不包含重复元素的整数List集合。  Limit(限制)  limit()方法可以将流限制为指定的元素数。 public class Main {     public static void main(String[] args) {         List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);         List<Integer> collect = numbers.stream().limit(3).collect(Collectors.toList());         System.out.println(collect);     } } 这段代码创建了一个包含整数的List集合numbers,其中包含了5个整数。接下来使用Stream()方法将其转化为一个Stream流。使用limit()方法对流中的元素进行限制操作,仅保留前3个元素,返回一个新的只包含前3个元素的Stream流collect。最后使用collect()方法将限制操作后的结果转化为一个新的List集合,并使用System.out.println()方法输出到控制台。 使用Stream流中的limit()方法可以快速地对集合中的元素进行截取操作,仅保留前N个元素。在这段代码中,集合中包含了5个整数,使用limit(3)方法仅保留了前3个整数,返回一个新的只包含前3个元素的List集合。 运行该示例代码,输出结果为:[1, 2, 3],即仅包含前3个元素的整数List集合。  Skip(跳过) public class Main {     public static void main(String[] args) {         List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);         List<Integer> collect = numbers.stream().skip(2).collect(Collectors.toList());         System.out.println(collect);     } }  skip()方法可跳过前N个元素。  这段代码创建了一个包含整数的List集合numbers,其中包含了5个整数。接下来使用Stream()方法将其转化为一个Stream流。使用skip()方法对流中的元素进行跳过操作,跳过前2个元素,返回一个新的不包含前2个元素的Stream流collect。最后使用collect()方法将跳过操作后的结果转化为一个新的List集合,并使用System.out.println()方法输出到控制台。 使用Stream流中的skip()方法可以快速地对集合中的元素进行跳过操作,跳过前N个元素。在这段代码中,集合中包含了5个整数,使用skip(2)方法跳过前2个元素,返回一个新的不包含前2个元素的List集合。 运行该示例代码,输出结果为:[3, 4, 5],即不包含前2个元素的整数List集合。  Peek(展示)  peek()方法可以用于在Stream流中获取元素同时执行一些操作,如打印、调试、观察等。通常会与其他的方法联合使用。 public class Main {     public static void main(String[] args) {         List<String> names = Arrays.asList("Alex", "Brian", "Charles", "David");         List<String> filteredNames = names.stream()                 .peek(System.out::println)                 .filter(name -> name.startsWith("C"))                 .peek(name -> System.out.println("Filtered value: " + name))                 .collect(Collectors.toList());         System.out.println("-----------------------------------------------------------------");         System.out.println(filteredNames);     } } 代码中创建了一个列表 names 包含四个字符串元素,然后使用流式操作处理这个列表。首先使用 peek() 方法将每个元素打印到控制台,然后使用 filter() 方法过滤掉不符合条件的元素,即不以字母 C 开头的字符串。接下来再次使用 peek() 方法将符合条件的字符串打印到控制台,以便验证过滤操作的效果。最后使用 collect() 方法将符合条件的字符串收集到一个新的列表 filteredNames 中,并输出该列表。 注意到,控制台上先输出了列表中的四个字符串,但只有以字母 C 开头的字符串 Charles 才符合筛选条件,因此仅仅 Charles 被保存在了 filteredNames 列表中。第二个 peek() 方法也被用来打印筛选出的元素 Charles。  终止操作 终止操作返回一个结果或副作用(例如:显示控制台输出),并将流关闭。  forEach(循环)  forEach()方法可将给定的方法应用于流中的每个元素。该方法是一种消费流的方式,不会返回值。 public class Main {     public static void main(String[] args) {         List<String> names = Arrays.asList("Alex", "Brian", "Charles", "David");         names.stream().forEach(System.out::println);     } } 这段代码创建了一个包含四个字符串元素的列表 names,使用流式操作将每个元素打印到控制台。具体来说,它使用 forEach() 方法遍历列表中的所有元素,并对每个元素执行打印操作。 其中,四个字符串元素按顺序打印到了控制台上。注意到,使用 forEach() 方法时并没有指定任何条件或谓词,因此它会对列表中的所有元素进行操作,以达到遍历、打印等目的。  Collect(收集)  collect()方法可以将流中的元素收集到一个集合中。一般与其他方法配合使用。 public class Main {     public static void main(String[] args) {         List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);         List<Integer> evenNumbers = numbers.stream().filter(n -> n % 2 == 0).collect(Collectors.toList());         System.out.println(evenNumbers);     } } 这段代码创建了一个包含整数的列表 numbers,使用流式操作筛选出所有偶数,然后将它们收集到一个新的列表 evenNumbers 中,并打印输出。具体来说,它使用了 filter() 方法过滤掉所有奇数元素,只保留所有偶数元素,并使用 collect() 方法将它们收集到一个新的列表 evenNumbers 中。 注意到,只有偶数元素被保留在了新列表 evenNumbers 中,而奇数元素全部被过滤掉了。而且,在筛选偶数元素时,使用了 lambda 表达式 n -> n % 2 == 0,其中 % 表示取模操作,判断当前数是否为偶数。如果 n % 2 的结果是 0,就把 n 这个数保留下来,否则就过滤掉。  Count(计数)  count()方法可以返回流中的元素数。 public class Main {     public static void main(String[] args) {         List<String> names = Arrays.asList("Alex", "Brian", "Charles", "David");         long count = names.stream().count();         System.out.println(count);     } } 这段代码创建了一个包含四个字符串元素的列表 names,使用流式操作计算出它包含的元素数量(即列表大小),并将该数量打印到控制台。具体来说,它使用了 count() 方法统计列表中元素的个数。 注意到,count() 方法返回的是一个 long 类型的值,表示列表中元素的个数。因为列表 names 包含了四个元素,所以 count() 方法返回值为 4,最终被打印输出到了控制台。  Reduce(聚合)  reduce()方法可以将流元素聚合为单个结果。它接受一个BinaryOperator参数作为累加器。 public class Main {     public static void main(String[] args) {         List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);         Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);         System.out.println(sum);     } } 这段代码创建了一个包含整数的列表 numbers,使用流式操作将它们累加起来得到总和,并将结果打印输出。具体来说,它使用了 reduce() 方法对列表中的所有元素进行累加操作。reduce() 方法接收一个 BinaryOperator 函数作为参数,用于指定如何处理相邻的两个元素并返回一个新的结果值。 注意到,reduce() 方法返回的是一个 Optional 类型的值,表示结果可能存在也可能不存在(例如当列表为空时)。由于列表 numbers 包含 1 到 5 共五个元素,因此 reduce() 方法的操作过程如下: 1 + 2 = 3 3 + 3 = 6 6 + 4 = 10 10 + 5 = 15 最终得到的结果 15 被包装成 Optional 类型的对象并打印输出到控制台。  AnyMatch(任意匹配)  anyMatch()方法如果至少有一个元素与给定的谓词匹配,则返回true。 public class Main {     public static void main(String[] args) {         List<String> names = Arrays.asList("Alex", "Brian", "Charles", "David");         boolean anyStartsWithB = names.stream().anyMatch(name -> name.startsWith("B"));         System.out.println(anyStartsWithB);     } } 这段代码创建了一个包含四个字符串元素的列表 names,使用流式操作检查其中是否有任意一个元素以字母 “B” 开头,并将检查结果(布尔值)打印输出。具体来说,它使用了 anyMatch() 方法匹配列表中的所有元素,并依次对每个元素执行指定的谓词操作(这里是以 “B” 开头),只要有一个元素符合条件,就返回 true,否则返回 false。 注意到,列表 names 中包含了一个以字母 “B” 开头的元素 “Brian”,因此 anyMatch() 方法返回 true,最终被打印输出到了控制台。  AllMatch(全部匹配)  allMatch()方法如果所有元素都与给定谓词匹配,则返回true。 public class Main {     public static void main(String[] args) {         List<String> names = Arrays.asList("Alex", "Brian", "Charles", "David");         boolean allStartsWithB = names.stream().allMatch(name -> name.startsWith("B"));         System.out.println(allStartsWithB);     } } 这段代码创建了一个包含四个字符串元素的列表 names,使用流式操作检查其中是否所有元素都以字母 “B” 开头,并将检查结果(布尔值)打印输出。具体来说,它使用了 allMatch() 方法匹配列表中的所有元素,并依次对每个元素执行指定的谓词操作(这里是以 “B” 开头),只有当所有元素都符合条件时,才返回 true,否则返回 false。 注意到,虽然列表 names 中包含了一个以字母 “B” 开头的元素 “Brian”,但是它不是所有元素都以 “B” 开头,因此 allMatch() 方法返回 false,最终被打印输出到了控制台。  NoneMatch(无匹配)  noneMatch()方法,如果没有任何元素与给定谓词匹配,则返回true。 public class Main {     public static void main(String[] args) {         List<String> names = Arrays.asList("Alex", "Brian", "Charles", "David");         boolean noneStartsWithB = names.stream().noneMatch(name -> name.startsWith("E"));         System.out.println(noneStartsWithB);     } } 这段代码创建了一个包含四个字符串元素的列表 names,使用流式操作检查其中是否没有任意一个元素以字母 “E” 开头,并将检查结果(布尔值)打印输出。具体来说,它使用了 noneMatch() 方法匹配列表中的所有元素,并依次对每个元素执行指定的谓词操作(这里是以 “E” 开头),只有当所有元素都不符合条件时,才返回 true,否则返回 false。 注意到,列表 names 中不包含任何一个以字母 “E” 开头的元素,因此 noneMatch() 方法返回 true,最终被打印输出到了控制台。  以上就是Java Stream流的基础知识和操作方式。Stream API可以使Java程序员编写出高效,干净,紧凑的代码,使得代码易于阅读和维护。建议初学者多加练习,掌握基本操作。中级和高级程序员则需要深入研究Stream API的实现原理和运作机制,进一步提高代码的质量和效率。  使用Stream流的优缺点: 优点: Stream流可以帮助简化代码,减少样板代码,从而提高代码质量和可读性。 Stream流充分利用了现代多核处理器的优势,在多线程场景下可以获得更好的性能表现。 Stream流提供了丰富的操作方法,可以轻松地处理各种集合和数组的数据,从而降低程序员的编码难度和心理负担。 Stream流可以帮助开发人员更容易地写出函数式风格的代码,使代码更加健壮可维护。  缺点: Stream流有时候会让代码变得复杂,反而降低了可读性,因此在某些简单的情况下可能不需要使用Stream流。 Stream流可能会对程序的性能产生一定影响,尤其是在大型数据集或者复杂的业务逻辑的情况下,程序员需要根据具体的情况进行测试和分析,选择最优解。 Stream流可能会造成资源浪费,例如创建中间操作的临时对象,这些对象将占用存储空间,导致效率降低。 在实际开发中,应该根据具体情况来决定是否使用Stream流。一般建议在数据集较大或者需要进行复杂的数据处理操作时使用Stream流,而在一些简单的操作中则可以直接使用循环和传统的集合操作方法。此外,如果代码可读性受到影响,也可以考虑使用传统的集合操作方法来实现代码。 ———————————————— 版权声明:本文为CSDN博主「小尘要自信」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_54796785/article/details/131122376 
  • [技术干货] 【JavaScript】JS能力测试题:数组扁平化 | 判断质数 | 获取字符串的长度-转载
     数组扁平化 题目描述 请补全JavaScript代码,要求将数组参数中的多维数组扩展为一维数组并返回该数组。 注意:  数组参数中仅包含数组类型和数字类型 题解 递归  遍历数组 并 判断遍历的当前元素的数据类型,分为以下两种情况:  类型为数值:添加该元素到输出数组中。 类型为数组:重复上诉操作。   const _flatten = arr => {          return arr.reduce((target, item) => {     return target.concat(Array.isArray(item) ? _flatten(item) : item);  }, []) } 其中参数 target为输出数组,item为遍历的当前元素  字符串法  a. 利用toString()方法,将数组转为字符串,在使用split()方法,以","为分割变为数组。  b. 利用join()方法将数组转为字符串且用","分割,在利用上诉split()方法以","为分割变为数组。  const _flatten = arr => {     return arr.toString().split(",") } const _flatten = arr => {     return arr.join(",").split(",") } 简单粗暴法  思路:  使用while循环检测数组,只要数组中存在元素的数据类型为数组,就利用扩展运算符将该元素的可遍历属性追加到输出数组中展开直到数组元素的数据类型均为数值为止。  const _flatten = arr => {     while(arr.some(item=>{Array.isArray(item)})){         arr = [].concat(...arr)     }     return arr } some() :测试数组中是否至少有一个元素通过了由提供的函数实现的测试,在本题中用于测试数组中是否存在元素的数据类型为数组。  扩展运算符(…):取出参数对象中的所有可遍历属性,拷贝到当前对象之中。在本题中扩展运算符(…)用于取出被检测到元素类型为数组的对象内的所有可遍历属性,并使用concat方法拷贝到输出数组中。  判断质数 题目描述 请补全JavaScript代码,要求在Number对象的原型对象上添加"_isPrime"函数,该函数判断调用的对象是否为一个质数,是则返回true,否则返回false。  题解 质数的定义,不能被除了1和它本身的数字因式分解的数字,对应到数学的概念就是取余为0  双指针  Number.prototype._isPrime = function(){     let j=this-1;     for(let i=2; i<j;i++){                  if(i>=j)break         if(!(this%i) || !(this%j)){             return false         }         j--            }     return true } 平方根  假设Number数值为c, 若a * b = c 且(a<=b)则a,b需满足 a<=Math.sqrt©<=b,因此只用依次让[2,Math.sqrt©]范围内的数值对number进行取余数检验。  Number.prototype._isPrime = function(){          for(let i=2; i <= Math.sqrt(this); i++){         if(!(this%i)){             return false         }     }     return true } 获取字符串的长度 题目描述 如果第二个参数 bUnicode255For1 === true,则所有字符长度为 1 否则如果字符 Unicode 编码 > 255 则长度为 2  示例:  输入:  'hello world, 牛客', false 1 输出:  17 1 题解 function strLength(s, bUnicode255For1) {     if(bUnicode255For1){        return s.length      }else{         let length = 0         for(let i=0; i<s.length; i++){             if(s[i].codePointAt() > 255){                 length += 2             }else{                 length += 1             }         }         return length     } } codePointAt(index)  作用:返回一个非负整数,该整数是从给定索引开始的字符的 Unicode 码位值。  参数:index ———————————————— 版权声明:本文为CSDN博主「阿选不出来」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/m0_63300737/article/details/133319227 
  • [技术干货] 【JAVA】为什么要使用封装以及如何封装-转载
     前言 Java的封装指的是在一个类中将数据和方法进行封装,使其可以保护起来,只能在该类内部访问,而不允许外部直接访问和修改。这是Java面向对象编程的三个基本特性之一,另外两个是继承和多态。在此之前我们已经学习过关于继承的概念,今天我们来学习有关封装的内容。  封装的含义 封装是面向对象编程的一种重要概念,是将数据和对数据的操作封装在一个类中,使得数据对外部的访问受到限制,只能通过类中的公共方法来访问或操作。封装的目的是隐藏类的实现细节,并且保护数据不被随意修改,从而增强了代码的安全性和可维护性。另外,封装也使得代码的扩展和修改更加方便,只需在类内部进行修改而不需要修改其他代码。  通过封装,可以提高类的安全性和可维护性,使得类的实现细节被隐藏,只暴露出有限的接口和功能给外部使用,从而减少了类之间的耦合性。同时,封装还可以实现数据隐藏和数据保护,提高程序的可靠性和可扩展性。  四种访问控制符 Java中,采用访问控制符来控制类中数据成员和方法的访问权限,主要有四种访问控制符:public、private、protected和默认访问控制符(即不写访问控制符)。  public:表示该数据成员或方法可以被其他任何类访问。 private:表示该数据成员或方法只能在当前类内部访问。 protected:表示该数据成员或方法可以在当前类和其子类中访问。 默认访问控制符:表示该数据成员或方法只能在同一个包内的其他类中访问。 封装的两种经典实例 银行账户类 该类的主要属性包括账户名、账号、账户余额等。其中账户余额是一个私有属性,外部无法直接访问。类中提供了一系列操作账户的方法,例如存款、取款、查询余额等。这些方法都能够在保证账户余额正确的情况下,修改账户余额。  public class BankAccount {     private String accountNumber;     private double balance;      public BankAccount(String accountNumber, double balance) {         this.accountNumber = accountNumber;         this.balance = balance;     }      public String getAccountNumber() {         return accountNumber;     }      public double getBalance() {         return balance;     }      public void deposit(double amount) {         balance += amount;     }      public void withdraw(double amount) {         if (balance >= amount) {             balance -= amount;         } else {             System.out.println("Insufficient balance.");         }     } } accountNumber: 表示账户号码的字符串。 balance: 表示账户余额的双精度浮点数。 BankAccount(String accountNumber, double balance): 构造函数,用于创建一个新的银行账户对象。 getAccountNumber(): 返回账户号码的方法。 getBalance(): 返回账户余额的方法。 deposit(double amount): 存款方法,用于向账户中添加资金。 withdraw(double amount): 取款方法,用于从账户中扣除资金。如果账户余额不足,将输出一条错误信息。 学生类 public class Student {     // 属性     private String name;     private int age;     private String gender;     private String id;      // 构造方法     public Student(String name, int age, String gender, String id) {         this.name = name;         this.age = age;         this.gender = gender;         this.id = id;     }      // 方法     public void study() {         System.out.println("学生正在学习");     }      public void showInfo() {         System.out.println("姓名:" + name);         System.out.println("年龄:" + age);         System.out.println("性别:" + gender);         System.out.println("学号:" + id);     }      // Getter和Setter方法     public String getName() {         return name;     }      public void setName(String name) {         this.name = name;     }      public int getAge() {         return age;     }      public void setAge(int age) {         this.age = age;     }      public String getGender() {         return gender;     }      public void setGender(String gender) {         this.gender = gender;     }      public String getId() {         return id;     }      public void setId(String id) {         this.id = id;     } } 属性:学生类具有姓名、年龄、性别和学号这些属性。 构造方法:学生类具有一个带四个参数的构造方法,用于初始化学生对象。 方法:学生类具有学习方法和展示学生信息的方法。 Getter和Setter方法:学生类具有获取和设置属性值的Getter和Setter方法,用于保护属性的私有性。 ———————————————— 版权声明:本文为CSDN博主「许思王」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_73602725/article/details/133498865 
  • [技术干货] Java 超高频常见字符操作【建议收藏】-转载
     前言 为了巩固所学的知识,作者尝试着开始发布一些学习笔记类的博客,方便日后回顾。当然,如果能帮到一些萌新进行新技术的学习那也是极好的。作者菜菜一枚,文章中如果有记录错误,欢迎读者朋友们批评指正。 (博客的参考源码可以在我主页的资源里找到,如果在学习的过程中有什么疑问欢迎大家在评论区向我提出)  1. 字符串拼接 1. 使用 + 操作符:  String str1 = "Hello"; String str2 = ", "; String str3 = "World";  String result = str1 + str2 + str3; 这种方法是最简单的,通过使用 + 操作符可以将多个字符串连接在一起。Java会自动处理字符串拼接并创建一个新的字符串对象。  2. 使用 concat() 方法:  String str1 = "Hello"; String str2 = ", "; String str3 = "World";  String result = str1.concat(str2).concat(str3); concat() 方法用于将一个字符串连接到另一个字符串的末尾。可以连续调用 concat() 方法来连接多个字符串。  3. 使用 StringBuilder 类:  StringBuilder 是一个可变的字符串类,适用于需要频繁修改字符串的情况,因为它不会创建多个中间字符串对象,从而提高了性能。  StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("Hello"); stringBuilder.append(", "); stringBuilder.append("World");  String result = stringBuilder.toString(); 在这种情况下,我们首先创建一个 StringBuilder 对象,然后使用 append() 方法来添加需要拼接的字符串,最后通过 toString() 方法将 StringBuilder 转换为不可变的 String。  4. 使用 StringBuffer 类:  StringBuffer 类与 StringBuilder 类 类似,但是是线程安全的,适用于多线程环境。  StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("Hello"); stringBuffer.append(", "); stringBuffer.append("World");  String result = stringBuffer.toString(); 与 StringBuilder 一样,我们可以使用 append() 方法来构建最终的字符串,然后通过 toString() 方法获取不可变的 String。  5. 使用 String.join() 方法(Java 8+):  这种方法更加简洁,特别适用于将多个字符串与指定的分隔符连接起来的情况。  String.join()  String str1 = "Hello"; String str2 = ", "; String str3 = "World";  String result = String.join(str2, str1, str3); 2. 字符串查找 1.使用 indexOf() 方法查找子字符串的位置:  indexOf() 方法用于查找一个字符串是否包含另一个子字符串,并返回第一次出现的位置索引。如果未找到,它将返回 -1。  String mainString = "Hello, World!"; String subString = "World"; int position = mainString.indexOf(subString);  if (position != -1) {    System.out.println("子字符串在主字符串中的位置是:" + position); } else {     System.out.println("子字符串未找到!"); } 2.使用 lastIndexOf() 方法查找最后一次出现的位置:  lastIndexOf() 方法与 indexOf() 方法类似,但它返回最后一次出现的位置索引。  String mainString = "Java is a powerful programming language. Java is also fun!"; String subString = "Java"; int position = mainString.lastIndexOf(subString);  if (position != -1) {     System.out.println("最后一次出现的位置是:" + position); } else {     System.out.println("子字符串未找到!"); } 3.使用 contains() 方法检查字符串是否包含子字符串:  contains() 方法用于检查一个字符串是否包含另一个子字符串,它返回一个布尔值。  String mainString = "This is a Java example."; String subString = "Java"; boolean contains = mainString.contains(subString);  if (contains) {     System.out.println("主字符串包含子字符串!"); } else {     System.out.println("主字符串不包含子字符串!"); } 4.使用正则表达式进行高级搜索:  Java 也支持使用正则表达式来进行复杂的字符串搜索。你可以使用 Pattern 和 Matcher 类来实现这一点。这允许你执行更复杂的模式匹配操作。  import java.util.regex.*;  String text = "The quick brown fox jumps over the lazy dog."; String pattern = "fox";  Pattern compiledPattern = Pattern.compile(pattern); Matcher matcher = compiledPattern.matcher(text);  while (matcher.find()) {     System.out.println("找到匹配字符串: " + matcher.group()); } 3. 字符串截取 1.使用 substring() 方法:  substring() 方法用于从一个字符串中截取子串,你可以指定截取的起始位置和结束位置。这个方法有两种形式:一种只传入起始位置,另一种传入起始位置和结束位置。  String mainString = "Hello, World!";  // 截取从索引2到索引5之间的子串(包括索引2,但不包括索引5) String subString1 = mainString.substring(2, 5); System.out.println("截取子串1: " + subString1); // 输出 "llo"  // 从索引7开始截取到字符串末尾 String subString2 = mainString.substring(7); System.out.println("截取子串2: " + subString2); // 输出 "World!" 2.使用 split() 方法分割字符串:  split() 方法允许你根据某个分隔符将字符串拆分为子串,然后选择需要的子串。  String text = "apple,banana,grape";  // 使用逗号作为分隔符拆分字符串 String[] fruits = text.split(",");  for (String fruit : fruits) {     System.out.println("水果: " + fruit); } 3.使用正则表达式进行高级截取:  Java 的正则表达式库允许你根据复杂的模式来截取字符串。  import java.util.regex.*;  String text = "The quick brown fox jumps over the lazy dog."; String pattern = "\\b\\w+\\b"; // 匹配单词  Pattern compiledPattern = Pattern.compile(pattern); Matcher matcher = compiledPattern.matcher(text);  while (matcher.find()) {     System.out.println("匹配到的单词: " + matcher.group()); } 4. 字符串替換 1.使用 replace() 方法:  replace() 方法用于将指定的字符或子字符串替换为新的字符或子字符串。  String originalString = "Hello, World!"; String searchString = "World"; String replacementString = "Java";  String modifiedString = originalString.replace(searchString, replacementString); System.out.println("替换后的字符串: " + modifiedString); 输出:  替换后的字符串: Hello, Java! 1 2.使用 replaceAll() 方法进行正则表达式替换:  replaceAll() 方法允许你使用正则表达式进行更灵活的替换操作。  String originalString = "The quick brown fox jumps over the lazy dog."; String pattern = "fox"; String replacementString = "cat";  String modifiedString = originalString.replaceAll(pattern, replacementString); System.out.println("替换后的字符串: " + modifiedString); 输出:    替换后的字符串: The quick brown cat jumps over the lazy dog. 1 3.使用 StringBuilder 进行替换:  如果需要进行多次替换或性能要求较高,可以使用 StringBuilder 类。  StringBuilder stringBuilder = new StringBuilder("Java is easy. Java is fun."); String searchString = "Java"; String replacementString = "Python"; int index = stringBuilder.indexOf(searchString); while (index != -1) {     stringBuilder.replace(index, index + searchString.length(), replacementString);     index = stringBuilder.indexOf(searchString); }  String modifiedString = stringBuilder.toString(); System.out.println("替换后的字符串: " + modifiedString); 输出:    替换后的字符串: Python is easy. Python is fun. 1 5. 字符串分割 1. 使用 split() 方法:  split() 方法是Java中最常用的字符串分割方法。它使用指定的正则表达式作为分隔符,将字符串分割成一个字符串数组。  String inputString = "apple,orange,banana,grape"; String[] fruits = inputString.split(",");  System.out.println("分割后的水果:"); for (String fruit : fruits) {      System.out.println(fruit); } 输出:  分割后的水果: apple orange banana grape 2. 使用 StringTokenizer 类:  StringTokenizer 类是Java中另一种进行字符串分割的方式,它使用指定的分隔符将字符串分割成标记。  String inputString = "Java is a powerful programming language"; StringTokenizer tokenizer = new StringTokenizer(inputString);  System.out.println("分割后的单词:"); while (tokenizer.hasMoreTokens()) {      System.out.println(tokenizer.nextToken()); } 输出: 分割后的单词: Java is a powerful programming language 3. 使用正则表达式: 你也可以使用正则表达式作为分隔符,以实现更灵活的字符串分割。 String inputString = "Java123is456a789powerful"; String[] parts = inputString.split("\\d+"); System.out.println("分割后的部分:"); for (String part : parts) {      System.out.println(part); } 输出: 分割后的部分: Java is a 4. 使用 Apache Commons Lang 库:  Apache Commons Lang 库提供了 StringUtils 类,其中有一个方便的 split() 方法,可以更容易地处理字符串分割。  import org.apache.commons.lang3.StringUtils;  String inputString = "Java;C;C++;Python"; String[] languages = StringUtils.split(inputString, ';');  System.out.println("分割后的编程语言:"); for (String language : languages) {      System.out.println(language); 输出: 分割后的编程语言: Java C C++ 字符串比较 1. 使用 equals() 方法进行内容比较: equals() 方法用于比较两个字符串的内容是否相同。它比较字符串的每个字符,而不仅仅是比较引用是否相等。 String str1 = "Hello"; String str2 = "World"; String str3 = "Hello"; boolean isEqual1 = str1.equals(str2); // 返回 false boolean isEqual2 = str1.equals(str3); // 返回 true System.out.println("str1 和 str2 是否相等:" + isEqual1); 2. 使用 equalsIgnoreCase() 方法进行忽略大小写的内容比较: equalsIgnoreCase() 方法与 equals() 方法类似,但它会忽略字符串的大小写。 String str1 = "Hello"; String str2 = "hello"; boolean isEqualIgnoreCase = str1.equalsIgnoreCase(str2); // 返回 true 3. 使用 compareTo() 方法进行字典顺序比较: compareTo() 方法用于比较两个字符串的字典顺序。它返回一个整数,表示两个字符串之间的比较结果。 String str1 = "apple"; String str2 = "banana"; String str3 = "cherry";  int result1 = str1.compareTo(str2); // 返回负数 int result2 = str2.compareTo(str1); // 返回正数 int result3 = str1.compareTo(str3); // 返回负数  System.out.println("str1 和 str2 的比较结果:" + result1); System.out.println("str2 和 str1 的比较结果:" + result2); System.out.println("str1 和 str3 的比较结果:" + result3); 4. 使用 startsWith() 和 endsWith() 方法检查前缀和后缀: startsWith() 方法用于检查字符串是否以指定的前缀开头,而 endsWith() 方法用于检查字符串是否以指定的后缀结尾。 Strng str = "Hello, World"; boolean startsWithHello = str.startsWith("Hello"); // 返回 true boolean endsWithWorld = str.endsWith("World");     // 返回 false System.out.println("字符串是否以 Hello 开头:" + startsWithHello); System.out.println("字符串是否以 World 结尾:" + endsWithWorld); 5. 使用 compareToIgnoreCase() 方法进行忽略大小写的字典顺序比较: compareToIgnoreCase() 方法与 compareTo() 方法类似,但它会忽略字符串的大小写。 String str1 = "apple"; String str2 = "Banana"; int result = str1.compareToIgnoreCase(str2); // 返回正数 System.out.println("str1 和 str2 的比较结果(忽略大小写):" + result); 7. 字符串格式化 1. 使用 String.format() String.format() 方法允许您创建格式化的字符串,类似于C语言中的 printf() 函数。它使用占位符来指定要插入的数据以及它们的格式。占位符由百分号 (%) 后跟一个字符组成,该字符表示插入数据的类型。以下是一些常见的占位符及其用法: %s :字符串。 %d :整数。 %f :浮点数。 %n :换行符。 示例: String name = "Alice"; int age = 30; double salary = 50000.50; String formattedString = String.format("Name: %s, Age: %d, Salary: %.2f", name, age, salary); System.out.println(formattedString); 输出: Name: Alice, Age: 30, Salary: 50000.50 在上面的示例中,我们使用了 %s 、 %d 和 %.2f 占位符来插入字符串、整数和浮点数,并指定了浮点数保留两位小数。 2. 使用 printf() printf() 方法是 System.out 对象的一个方法,它用于将格式化的字符串输出到控制台。与 String.format() 类似,它使用相同的占位符来格式化输出。 示例: String name = "Bob"; int age = 25; double height = 1.75; 输出: Name: Bob, Age: 25, Height: 1.75 8. 字符串空格处理 1. 删除空格:  .使用 String 类的 trim() 方法删除字符串前后的空格。 trim() 返回一个新的字符串,其中删除了前导和尾随的空格。 String text = "  This is a text with spaces  "; String trimmedText = text.trim(); System.out.println(trimmedText); // 输出: "This is a text with spaces" 2. 替换空格:  使用 String 类的 replace() 方法替换字符串中的空格。 你可以将空格替换为其他字符或字符串。 String text = "Hello, World!"; String replacedText = text.replace(" ", "_"); System.out.println(replacedText); // 输出: "Hello,_World!" 3. 分割字符串:  使用 split() 方法将字符串拆分成字符串数组。 默认情况下,split() 使用空格作为分隔符,但你可以指定自定义分隔符。 String sentence = "This is a sample sentence"; String[] words = sentence.split(" "); // 使用空格分割成单词数组 4. 检查空格:  使用 contains() 方法检查字符串是否包含空格。 String text = "This has spaces"; boolean hasSpaces = text.contains(" "); // 返回 true 5. 统计空格的数量:  使用循环遍历字符串并计算空格的数量。 String text = "Count the spaces in this text"; int spaceCount = 0; for (char c : text.toCharArray()) {     if (c == ' ') {         spaceCount++;     } } System.out.println("空格数量:" + spaceCount); // 输出: "空格数量:5" 6. 替换多个连续空格:  使用正则表达式来替换连续的多个空格为单个空格。 String text = "Replace  multiple   spaces  with one."; String replacedText = text.replaceAll("\\s+", " "); System.out.println(replacedText); // 输出: "Replace multiple spaces with one." 7. 处理制表符和换行符:  空白字符不仅包括空格,还包括制表符 (\t) 和换行符 (\n) 等。 你可以使用 replaceAll() 来处理它们,就像处理空格一样。 String textWithTabs = "This\tis\ta\ttab\tseparated\ttext"; String textWithNewlines = "This\nis\na\nnewline\nseparated\ntext"; 总结 欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下。 (博客的参考源码可以在我主页的资源里找到,如果在学习的过程中有什么疑问欢迎大家在评论区向我提出) ———————————————— 版权声明:本文为CSDN博主「东离与糖宝」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/HHX_01/article/details/132499313 
  • [其他问题] .so动态库出现undefined symbol
    使用了一个第三方的动态库libgmssl.so来编写生成了一个新的动态库tlcp.so,但是使用时出现undefined symbol后面使用ldd -r tlcp.so查看,显示如下内容undefined symbol: tls_send    (./tlcp.so) undefined symbol: tls_ctx_cleanup    (./tlcp.so) undefined symbol: tls_socket_accept    (./tlcp.so) undefined symbol: tls_do_handshake    (./tlcp.so) undefined symbol: tls_ctx_set_certificate_and_key    (./tlcp.so) undefined symbol: tls_socket_connect    (./tlcp.so) 这些函数的实现在libgmssl.so内已经实现,且使用nm libgmssl.so显示
  • [技术干货] Java生成UUID的常用方式-转载
     java.util.UUID类来生成UUID import java.util.UUID;  public class UUIDGenerator {     public static void main(String[] args) {                  //随机生成一个UUID对象         UUID uuid = UUID.randomUUID();         System.out.println("生成的UUID为:" + uuid.toString());                  //通过给定的字符串名称和命名空间生成UUID对象         UUID uuid2 = UUID.nameUUIDFromBytes("example_name".getBytes());         System.out.println("生成的UUID2为:" + uuid2.toString());     } } /*优点: Java自带,无需引入额外的库和依赖; 简单易用,一行代码就可以生成UUID。  缺点: 生成的UUID可能会重复,虽然重复的概率较小,但是在高并发的情况下还是有可能发生; 无法控制生成的UUID的格式,只能生成标准的UUID*/  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Apache Commons IO库中的UUIDUtils类 import org.apache.commons.io.UUIDUtils;  public class UUIDGenerator {     public static void main(String[] args) {                  //随机生成一个UUID字符串         String uuid = UUIDUtils.randomUUID().toString();         System.out.println("生成的UUID为:" + uuid);     } } /* 三方库优缺点 优点: 可以生成唯一的UUID; 很多开源库和框架都提供了UUID生成的支持。  缺点: 会增加项目的依赖和复杂度; 不同的库实现方式不同,可能会影响生成的UUID的格式和唯一性。 */  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 使用Google Guice库中的UUIDGenerator类生成UUID import com.google.inject.Inject; import com.google.inject.name.Named; import java.util.UUID;  public class UUIDGenerator {     private final UUID uuid;      @Inject     public UUIDGenerator(@Named("randomUUID") UUID uuid) {         this.uuid = uuid;     }      public UUID getUUID() {         return uuid;     }          public static void main(String[] args) {         UUIDGenerator generator = new UUIDGenerator(UUID.randomUUID());         System.out.println("生成的UUID为:" + generator.getUUID().toString());     } }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 使用JDK的MessageDigest类和SecureRandom类:可以通过Hash算法和随机数生成UUID 写法一: import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.UUID;  public class UUIDGenerator {     public static void main(String[] args) throws NoSuchAlgorithmException {         SecureRandom secureRandom = new SecureRandom();         byte[] seed = secureRandom.generateSeed(16);         MessageDigest md5 = MessageDigest.getInstance("MD5");         md5.update(seed);         UUID uuid = UUID.nameUUIDFromBytes(md5.digest());         System.out.println("生成的UUID为:" + uuid.toString());     } } 写法二: import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Random;  public class UUIDGenerator {      public static String generateUUID() {         String result = "";         try {             MessageDigest md = MessageDigest.getInstance("MD5");             byte[] messageDigest = md.digest((System.currentTimeMillis() + new Random().nextInt(99999999) + "").getBytes());             StringBuilder sb = new StringBuilder();             for (byte b : messageDigest) {                 sb.append(String.format("%02x", b));             }             result = sb.toString();         } catch (NoSuchAlgorithmException e) {             e.printStackTrace();         }         return result;     }  }  /* 优点: 可以通过Hash算法和随机数生成唯一的UUID,具有较高的唯一性; 实现简单,无需引入额外的库和依赖。  缺点: 重复的概率比较难以预测,取决于生成的Hash值的分布情况; 无法控制生成的UUID的格式,只能生成基于MD5或SHA-1的UUID。 */  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 使用Snowflake算法生成UUID Snowflake算法是Twitter开源的分布式ID生成算法,可以在多个节点上生成唯一的ID  import com.github.f4b6a3.uuid.UuidCreator; import com.github.f4b6a3.uuid.enums.UuidVariant; import com.github.f4b6a3.uuid.enums.UuidVersion; import com.github.f4b6a3.uuid.impl.TimeBasedUuidCreator;  import java.time.Instant;  public class UUIDGenerator {     public static void main(String[] args) {         UuidCreator creator = TimeBasedUuidCreator.withRandomNodeId();         Instant now = Instant.now();         long timestamp = now.getEpochSecond() * 1000 + now.getNano() / 1000000;         String uuid = creator.create(UuidVersion.VERSION_TIME_BASED, timestamp).toString();         System.out.println("生成的UUID为:" + uuid);     } } /* 优点: 可以在分布式系统中生成唯一的ID,具有较高的唯一性和可读性; 可以控制生成的ID的格式和信息。  缺点: 实现相对复杂,需要实现一个全局唯一的时钟服务; 只适用于分布式系统,不适用于独立的单机系统。 */ Snowflake算法第二种:  public class UUIDGenerator {      /** 开始时间截 (2017-01-01) */     private final long twepoch = 1483200000000L;     /** 机器id所占的位数 */     private final long workerIdBits = 5L;     /** 数据标识id所占的位数 */     private final long datacenterIdBits = 5L;     /** 支持的最大机器id,结果是31 */     private final long maxWorkerId = -1L ^ (-1L << workerIdBits);     /** 支持的最大数据标识id,结果是31 */     private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);     /** 序列在id中占的位数 */     private final long sequenceBits = 12L;     /** 机器ID向左移12位 */     private final long workerIdShift = sequenceBits;     /** 数据标识id向左移17位(12+5) */     private final long datacenterIdShift = sequenceBits + workerIdBits;     /** 时间截向左移22位(5+5+12) */     private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;     /** 生成序列的掩码,这里为4095 */     private final long sequenceMask = -1L ^ (-1L << sequenceBits);     /** 工作机器id(0~31) */     private long workerId = 0L;     /** 数据中心id(0~31) */     private long datacenterId = 0L;     /** 毫秒内序列(0~4095) */     private long sequence = 0L;     /** 上次生成ID的时间截 */     private long lastTimestamp = -1L;      /**      * 构造函数      *      * @param workerId     工作ID (0~31)      * @param datacenterId 数据中心ID (0~31)      */     public UUIDGenerator(long workerId, long datacenterId) {         if (workerId > maxWorkerId || workerId < 0) {             throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));         }         if (datacenterId > maxDatacenterId || datacenterId < 0) {             throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));         }         this.workerId = workerId;         this.datacenterId = datacenterId;     }      /**      * 获得下一个ID (该方法是线程安全的)      *      * @return SnowflakeId      */     public synchronized long nextId() {         long timestamp = timeGen();         // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过,此时应当抛出异常         if (timestamp < lastTimestamp) {             throw new RuntimeException(                     String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));         }          // 如果是同一时间生成的,则进行毫秒内序列         if (lastTimestamp == timestamp) {             sequence = (sequence + 1) & sequenceMask;             // 毫秒内序列溢出             if (sequence == 0) {                 // 阻塞到下一个毫秒,获得新的时间戳                 timestamp = tilNextMillis(lastTimestamp);             }         }         // 时间戳改变,毫秒内序列重置         else {             sequence = 0L;         }          // 上次生成ID的时间截         lastTimestamp = timestamp;          // 移位并通过或运算拼到一起组成64位的ID         return ((timestamp - twepoch) << timestampLeftShift) //                 | (datacenterId << datacenterIdShift) //                 | (workerId << workerIdShift) //                 | sequence;     }      /**      * 阻塞到下一个毫秒,直到获得新的时间戳      *      * @param lastTimestamp 上次生成ID的时间截      * @return 当前时间戳      */     protected long tilNextMillis(long lastTimestamp) {         long timestamp = timeGen();         while (timestamp <= lastTimestamp) {             timestamp = timeGen();         }         return timestamp;     }      /**      * 返回以毫秒为单位的当前时间      *      * @return 当前时间(毫秒)      */     protected long timeGen() {         return System.currentTimeMillis();     }  } 将时间戳和随机数作为种子生成UUID import java.util.UUID;  public class UUIDGenerator {     public static void main(String[] args) {         long time = System.currentTimeMillis();         int random = (int) (Math.random() * Integer.MAX_VALUE);         UUID uuid = new UUID(time, random);         System.out.println("生成的UUID为:" + uuid.toString());     } 使用Redis集群的redisson框架提供的RUID类生成UUID import org.redisson.api.RUID;  public class UUIDGenerator {     public static void main(String[] args) {         RUID ruid = RUID.randomUID();         System.out.println("生成的UUID为:" + ruid.toString());     } } 利用SecureRandom类生成 import java.security.SecureRandom; import java.util.UUID;  public class UUIDGenerator {      public static String generateUUID() {         return UUID.randomUUID().toString();     }      public static String generateSecureUUID() {         SecureRandom random = new SecureRandom();         byte[] bytes = new byte[16];         random.nextBytes(bytes);         return UUID.nameUUIDFromBytes(bytes).toString();     }  } 三方库详细版 Apache Commons: 引入以下Maven依赖 <dependency>     <groupId>commons-lang</groupId>     <artifactId>commons-lang</artifactId>     <version>2.6</version> </dependency> java示例代码:  import org.apache.commons.lang3.StringUtils; import java.util.UUID;  public class GenerateUUID {     public static void main(String[] args) {         UUID uuid = UUID.randomUUID();         String uuidStr = StringUtils.remove(uuid.toString(), '-');         System.out.println("UUID:" + uuidStr);     } } Google Guava: Google Guava库可以使用它的UUID类来生成UUID。需要引入以下Maven依赖:  <dependency>     <groupId>com.google.guava</groupId>     <artifactId>guava</artifactId>     <version>30.0-jre</version> </dependency> java示例:  import com.google.common.base.CharMatcher; import java.util.UUID;  public class GenerateUUID {     public static void main(String[] args) {         UUID uuid = UUID.randomUUID();         String uuidStr = CharMatcher.is('-').removeFrom(uuid.toString());         System.out.println("UUID:" + uuidStr);     } } 注意事项 之前提到了 Apache Commons 的 UUIDUtils 工具类,但是这个工具类实际上是用于字符串格式与 UUID 转化的,而不是生成 UUID。 如果你想要使用 Apache Commons 中的工具类来生成 UUID ,可以使用 RandomStringUtils 类中的 randomUUID() 方法。下面是一个简单示例:  import org.apache.commons.lang3.RandomStringUtils;  public class GenerateUUID {     public static void main(String[] args) {         String uuid = RandomStringUtils.randomNumeric(8) + "-" +                       RandomStringUtils.randomNumeric(4) + "-" +                       RandomStringUtils.randomNumeric(4) + "-" +                       RandomStringUtils.randomNumeric(4) + "-" +                       RandomStringUtils.randomNumeric(12);         System.out.println("UUID:" + uuid);     } } /* 上述代码中,RandomStringUtils的randomNumeric  方法用于生成指定长度的数字字符串,然后通过字符串拼接的方式生成UUID。 需要注意的是,这种方式所生成的UUID并不是符合UUID标准规范的。 */  ———————————————— 版权声明:本文为CSDN博主「潮流coder」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_45699990/article/details/129814826 
  • [技术干货] Post 和 Get 两种方式实现数据导出Excel文件并下载 -转载
     Post 和 Get 两种方式实现数据导出Excel文件并下载  前端使用Vue,后端使用Springboot。  一般而言,使用post方式是比较方便的。但有时候,需要下载的数据在后端进行查询的时候很比较复杂的查询条件,而这个查询条件是前端进行下载请求的时候传递的参数,如果参数比较复杂或参数量比较大,超出了get方式的参数限制,就需要使用post方式进行下载请求。  Post方式: 后端将查询到的数据作一些处理之后,将数据写入到请求的响应体中,前端在请求接口之后,在回调函数中,将响应体中的二进制流转化为Blob对象,然后创建一个下载链接进行下载。  后端代码:      @PostMapping("/export")     public void export(HttpServletResponse response, @RequestParam("className") String className) {         try {             // 根据班级名获取该班级的所有学生的信息             List<Student> list = StudentService.lists(className);             // 为了导出的表格中有序号,在定义实体类的时候加了一个serialNo属性             for(int i = 0; i < list.size(); i++) {                 list.get(i).setSerialNo(i + 1);             }             // 设置excel文件的名字,这边可以不用,在前端重新设置了文件名             String name = "学生信息列表.xlsx";             String fileName = new String(name.getBytes("gb2312"), "ISO8859-1");             response.setContentType("application/octet-stream");             response.setHeader("Content-Disposition","attachment;filename=" + fileName);                          // 这里注册的 CustomCellWriteHandler工具类是用来在excel中适应内容自动调整表格宽度的             ExcelWriter writer = EasyExcel.write(response.getOutputStream()).registerWriteHandler(new CustomCellWriteHandler()).build();             WriteSheet sheet = EasyExcel.writerSheet(0,"sheet").head(BillList.class).build();             writer.write(list,sheet);             writer.finish();         } catch (Exception e) {             log.info("jjj", e);         }     }  Student 实体类:  @Data public class Student{     // ExcelIgnore 注册表示该属性不导出到excel     @ExcelIgnore     private Integer id;     @ExcelProperty(value = "班级", index = 1)     private String className;     @ExcelProperty(value = "学号", index = 2)     private String studentNumber;     @ExcelProperty(value = "姓名", index = 3)     private String name;     @ExcelProperty(value = "性别", index = 4)     private String gender;     @       // 导出excel的序号列, index值表示在导出的excel中的第几列,从0开始     @ExcelProperty(value = "序号", index = 0)     private Integer serialNo; }  CustomCellWriteHandler工具类:  import com.alibaba.excel.enums.CellDataTypeEnum; import com.alibaba.excel.metadata.CellData; import com.alibaba.excel.metadata.Head; import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy; import org.apache.commons.collections4.CollectionUtils; import org.apache.poi.ss.usermodel.Cell;   import java.util.HashMap; import java.util.List; import java.util.Map;   /**  * Excel 导出列宽度自适应  */ public class CustomCellWriteHandler extends AbstractColumnWidthStyleStrategy {       private Map<Integer, Map<Integer, Integer>> CACHE = new HashMap<>();       @Override     protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<CellData> cellDataList, Cell cell, Head head, Integer integer, Boolean isHead) {         boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);         if (needSetWidth) {             Map<Integer, Integer> maxColumnWidthMap = CACHE.get(writeSheetHolder.getSheetNo());             if (maxColumnWidthMap == null) {                 maxColumnWidthMap = new HashMap<>();                 CACHE.put(writeSheetHolder.getSheetNo(), maxColumnWidthMap);             }               Integer columnWidth = this.dataLength(cellDataList, cell, isHead);             if (columnWidth >= 0) {                 if (columnWidth > 255) {                     columnWidth = 255;                 }                   Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());                 if (maxColumnWidth == null || columnWidth > maxColumnWidth) {                     maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);                     writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);                 }               }         }     }       private Integer dataLength(List<CellData> cellDataList, Cell cell, Boolean isHead) {         if (isHead) {             return cell.getStringCellValue().getBytes().length;         } else {             CellData cellData = cellDataList.get(0);             CellDataTypeEnum type = cellData.getType();             if (type == null) {                 return -1;             } else {                 switch (type) {                     case STRING:                         return cellData.getStringValue().getBytes().length;                     case BOOLEAN:                         return cellData.getBooleanValue().toString().getBytes().length;                     case NUMBER:                         return cellData.getNumberValue().toString().getBytes().length;                     default:                         return -1;                 }             }         }     } }  前端代码:  // 导入接口文件,一个controller的接口都配置在一个js/ts文件中 import studentApi from '@/api/studentAPI'   // exportExcel 绑定了下载按钮的点击事件 function exportExcel() {   studentApi.exportExcelUrl(className).then(res=>{     const url = window.URL.createObjectURL(new Blob([res]))     let link = document.createElement('a')     link.style.display = 'none'     link.href = url     link.setAttribute('download', '学生信息列表.xlsx')     document.body.appendChild(link)     link.click()     URL.revokeObjectURL(link.href) // 释放URL 对象     document.body.removeChild(link)     link = null   }) }  studentAPI.ts:  import AxiosInstance from "@/plugins/axiosInstance";   const exportExcelUrl = (className:string) => {     return AxiosInstance.post('/rent/export', {responseType:'arraybuffer', params: {className: className}}) }   export default {     exportExcelUrl } axiosInstance.ts:  import axios from 'axios'   // 配置后端接口地址前缀 const ConfigBaseURL = 'http://localhost:8080/'     // 本地环境 const Axios = axios.create({     timeout: 5000,      baseURL: ConfigBaseURL,     headers: {         'Content-Type': 'application/json;charset=UTF-8'     } })   Axios.interceptors.request.use(config => {     return config }, error => {     return Promise.reject(error) })   Axios.interceptors.response.use(response => {     var data = response.data     return response.data }, error => {       return Promise.reject(error) })   export default Axios ;  Get方式: 实现方式和post基本一致,后端只需要修改一下接口的请求方式和response的contentType;前端就只需要提供一个链接即可。  后端代码:      @GetMapping("/export")     public void export(HttpServletResponse response, @Param("className") String className) {         try {             // 根据班级名获取该班级的所有学生的信息             List<Student> list = StudentService.lists(className);             // 为了导出的表格中有序号,在定义实体类的时候加了一个serialNo属性             for(int i = 0; i < list.size(); i++) {                 list.get(i).setSerialNo(i + 1);             }             // 设置Excel文件名             String name = "学生信息列表.xlsx";             String fileName = new String(name.getBytes("gb2312"), "ISO8859-1");             response.setContentType("application/vnd.ms-excel;chartset=utf-8");             response.setHeader("Content-Disposition","attachment;filename=" + fileName);               // 这里注册的 CustomCellWriteHandler工具类是用来在excel中适应内容自动调整表格宽度的             ExcelWriter writer = EasyExcel.write(response.getOutputStream()).registerWriteHandler(new CustomCellWriteHandler()).build();             WriteSheet sheet = EasyExcel.writerSheet(0,"sheet").head(BillList.class).build();             writer.write(list,sheet);             writer.finish();         } catch (Exception e) {             log.info("jjj", e);         }     }  除了请求方式从Post改为了Get,还有一个变化之处就是:  response.setContentType("application/vnd.ms-excel;chartset=utf-8"); 前端代码:  // exportExcel 绑定了下载按钮的点击事件 function exportExcel() {   // 本来后面的接口地址也是配置在接口文件中的,这边为了方便阅读,直接放在了此处。   var className = 2205   window.location.href = "http://localhost:8080/student/export?className=" + className } 参考:  使用VUE+SpringBoot+EasyExcel 整合导入导出demo_娜乌西卡lxm的博客-CSDN博客  通过get或post请求下载excel表格的解决办法_get请求下载excel_一键写代码的博客-CSDN博客  Vue2 导出Excel + 解决乱码问题 —— axios (下载后台传过来的流文件(excel)后乱码问题)_Shimeng_1989的博客-CSDN博客 ———————————————— 版权声明:本文为CSDN博主「CHEN HANGJUN」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/chen1030416518/article/details/129973905 
  • [技术干货] JAVA编程中常用到的线程和注意点总结
    一般的创建线程的方式有两种:继承 Thread 类(extends Thread)或者实现Runnable 接口(implements Runnable)1)  继承 Thread 类    实现步骤:  继承 Thread 类, 覆盖run()方法, 提供并发运程的过程  创建这个类的实例  使用 start() 方法启动线程2)  实现 Runnable 接口  实现步骤:  实现 Runnable 接口, 实现run()方法, 提供并发运程的过程  创建这个类的实例, 用这个实例作为Thread 构造器参数,创建Thread 类  使用 start() 方法启动线程      线程都会出现最基本的五种状态:1)  New      新建状态    当程序使用new关键字创建了一个线程后,该线程就处于新建状态,此时线程还未启动,  当线程对象调用start()方法时,线程启动,进入Runnable 状态2)  Runnable    可运行(就绪)状态  当线程处于Runnable 状态时,表示线程准备就绪,等待获取CPU3)  Running    运行(正在运行)状态  假如该线程获取了CPU,则进入Running 状态,开始执行线程体,即run()方法中的内容  注意:   (1)如果系统叧有1个CPU,那么在仸意时间点则叧有1条线程处于Running 状态;   (2)如果是双核系统,那么同一时间点会有2条线程处于Running 状态但是,当线程数大于处理器数时,依然会是多条线程在同一个CPU 上轮换执行   (3)当一条线程开始运行时,如果它不是一瞬间完成,那么它不可能一直处于Running 状态, 线程在执行过程中会被中断,目的是让其它线程获得执行的机会,像这样线程调度的策略取决于底层平台。对于抢占式策略的平台而言,系统系统会给每个可执行的线程一小段时间来处理仸务,当该时间段(时间片)用完,系统会剥夺该线程所占资源(CPU), 让其他线程获得运行机会。  (4) 调用yield()方法,可以使线程由Running 状态进入Runnable 状态4)  Block      阻塞(挂起)状态  当如下情冴下,线程会进入阻塞状态:  线程调用了sleep()方法主动放弃所占CPU 资源  线程调用了一个阻塞式IO 方法(比如控制台输入方法),在该方法返回前,该线程被阻塞  当正在执行的线程被阻塞时,其它线程就获得执行机会了。需要注意的是,当阻塞结束时,该线程将进入Runnable 状态,而非直接进入Running 状态5)  Dead      死亡状态    当线程的run()方法执行结束,线程进入Dead 状态  需要注意的是,不要试图对一个已经死亡的线程调用start()方法,线程死亡后将不能再次作为线程执行,系统会抛出IllegalThreadStateException 异常注:1)  new运算创建线程后,线程进入New状态(初始状态)2)  调用  start()方法后,线程从New状态进入Runnable 状态(就绪状态)          start()方法是在main()方法(Running 状态)中调用的3)  线程结束后,进入Dead 状态(死亡状态),被对象垃圾回收4)  线程进入Dead 状态后,叧能被垃圾回收,不能再开始5)  如果线程在运行过程中,自己调用了yield()方法,则主动由 Running 状态进入Runnable 状态
  • [技术干货] Spring MVC请求处理流程和九大组件-转载
    需求:前端浏览器请求url: http://localhost:8080/demo/handle01,前端⻚⾯显示后台服务器的时间 开发过程 配置DispatcherServlet前端控制器 开发处理具体业务逻辑的Handler(@Controller、@RequestMapping) xml配置⽂件配置controller扫描,配置springmvc三⼤件 将xml⽂件路径告诉springmvc(DispatcherServlet)  流程说明 第⼀步:⽤户发送请求⾄前端控制器DispatcherServlet 第⼆步:DispatcherServlet收到请求调⽤HandlerMapping处理器映射器 第三步:处理器映射器根据请求Url找到具体的Handler(后端控制器),⽣成处理器对象及处理器拦截器(如果 有则⽣成)⼀并返回DispatcherServlet 第四步:DispatcherServlet调⽤HandlerAdapter处理器适配器去调⽤Handler 第五步:处理器适配器执⾏Handler 第六步:Handler执⾏完成给处理器适配器返回ModelAndView 第七步:处理器适配器向前端控制器返回 ModelAndView,ModelAndView 是SpringMVC 框架的⼀个底层对象,包括 Model 和 View 第⼋步:前端控制器请求视图解析器去进⾏视图解析,根据逻辑视图名来解析真正的视图。 第九步:视图解析器向前端控制器返回View 第⼗步:前端控制器进⾏视图渲染,就是将模型数据(在 ModelAndView 对象中)填充到 request 域 第⼗⼀步:前端控制器向⽤户响应结果。  Spring MVC 九⼤组件 HandlerMapping(处理器映射器) HandlerMapping 是⽤来查找 Handler 的,也就是处理器,具体的表现形式可以是类,也可以是⽅法。⽐如,标注了@RequestMapping的每个⽅法都可以看成是⼀个Handler。Handler负责具体实际的请求处理,在请求到达后,HandlerMapping 的作⽤便是找到请求相应的处理器Handler 和 Interceptor。  HandlerAdapter(处理器适配器) HandlerAdapter 是⼀个适配器。因为 Spring MVC 中 Handler 可以是任意形式的,只要能处理请求即可。但是把请求交给 Servlet 的时候,由于 Servlet 的⽅法结构都是doService(HttpServletRequest req,HttpServletResponse resp)形式的,要让固定的 Servlet 处理⽅法调⽤ Handler 来进⾏处理,便是HandlerAdapter 的职责。  HandlerExceptionResolver(Handler异常解析器) HandlerExceptionResolver ⽤于处理 Handler 产⽣的异常情况。它的作⽤是根据异常设置ModelAndView,之后交给渲染⽅法进⾏渲染,渲染⽅法会将 ModelAndView 渲染成⻚⾯。  ViewResolver(视图解析器) ViewResolver即视图解析器,⽤于将String类型的视图名和Locale解析为View类型的视图,只有⼀个resolveViewName()⽅法。从⽅法的定义可以看出,Controller层返回的String类型视图名viewName 最终会在这⾥被解析成为View。View是⽤来渲染⻚⾯的,也就是说,它会将程序返回的参数和数据填⼊模板中,⽣成html⽂件。ViewResolver 在这个过程主要完成两件事情: ViewResolver 找到渲染所⽤的模板(第⼀件⼤事)和所⽤的技术(第⼆件⼤事,其实也就是找到视图的类型,如JSP)并填⼊参数。默认情况下,Spring MVC会⾃动为我们配置⼀个InternalResourceViewResolver,是针对 JSP 类型视图的。  RequestToViewNameTranslator(ViewName翻译器) RequestToViewNameTranslator 组件的作⽤是从请求中获取 ViewName。因为 ViewResolver 根据ViewName 查找 View,但有的 Handler 处理完成之后,没有设置 View,也没有设置 ViewName,便要通过这个组件从请求中查找 ViewName。  LocaleResolver(区域设置解析器) ViewResolver 组件的 resolveViewName ⽅法需要两个参数,⼀个是视图名,⼀个是 Locale。LocaleResolver ⽤于从请求中解析出 Locale,⽐如中国 Locale 是 zh-CN,⽤来表示⼀个区域。这个组件也是 i18n 的基础。  ThemeResolver(主题解析器) ThemeResolver 组件是⽤来解析主题的。主题是样式、图⽚及它们所形成的显示效果的集合。Spring MVC 中⼀套主题对应⼀个 properties⽂件,⾥⾯存放着与当前主题相关的所有资源,如图⽚、CSS样式等。创建主题⾮常简单,只需准备好资源,然后新建⼀个“主题名.properties”并将资源设置进去,放在classpath下,之后便可以在⻚⾯中使⽤了。SpringMVC中与主题相关的类有ThemeResolver、ThemeSource和Theme。ThemeResolver负责从请求中解析出主题名,ThemeSource根据主题名找到具体的主题,其抽象也就是Theme,可以通过Theme来获取主题和具体的资源。  MultipartResolver(上传处理器) MultipartResolver ⽤于上传请求,通过将普通的请求包装成 MultipartHttpServletRequest 来实现。MultipartHttpServletRequest 可以通过 getFile() ⽅法 直接获得⽂件。如果上传多个⽂件,还可以调⽤ getFileMap()⽅法得到Map<FileName,File>这样的结构,MultipartResolver 的作⽤就是封装普通的请求,使其拥有⽂件上传的功能。  FlashMapManager(传输参数管理器) FlashMap ⽤于重定向时的参数传递,⽐如在处理⽤户订单时候,为了避免重复提交,可以处理完post请求之后重定向到⼀个get请求,这个get请求可以⽤来显示订单详情之类的信息。这样做虽然可以规避⽤户重新提交订单的问题,但是在这个⻚⾯上要显示订单的信息,这些数据从哪⾥来获得呢?因为重定向时么有传递参数这⼀功能的,如果不想把参数写进URL(不推荐),那么就可以通过FlashMap来传递。只需要在重定向之前将要传递的数据写⼊请求(可以通过ServletRequestAttributes.getRequest()⽅法获得)的属性OUTPUT_FLASH_MAP_ATTRIBUTE中,这样在重定向之后的Handler中Spring就会⾃动将其设置到Model中,在显示订单信息的⻚⾯上就可以直接从Model中获取数据。FlashMapManager 就是⽤来管理 FalshMap 的。 ———————————————— 版权声明:本文为CSDN博主「共饮一杯无」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_35427589/article/details/132873827 
总条数:692 到第
上滑加载中