开发笔记

1.导入百万级别的Excel数据

这里有2个要解决的问题:

  • 如何将百万数据从Excel文件写入内存
  • 如何将百万数据从内存写入数据库

1.1 Excel读取文件的写法

Tips

先说结论,能用EasyExcel,直接用

  1. 最原始的,通过APACHE POI读取
<dependencies>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>4.1.2</version>
    </dependency>

    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>4.1.2</version>
    </dependency>
</dependencies>
public class PoiExample {
    public void importExcel(InputStream inputStream){
        try{
            Workbook workbook = WorkbookFactory.create(inputStream);
            for(Sheet sheet:workbook){
                for(Row row:sheet){
                    for(Cell cell:row){
                        System.out.println(cell.getStringCellValue());
                        //...
                    }
                }
            }
        }catch (Exception e){

        }
    }
}

Note

解析效率低。
处理大文件容易OOM。

  1. 通过Hutool的ExcelReader
<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.8.29</version>
</dependency>
public class HutoolExample {

    public void importExcel(InputStream inputStream){
        ExcelReader reader = ExcelUtil.getReader(inputStream);
        //方式1:读取后,每一行数据封装成Map,默认第一行为标题行,Map中的key为标题,value为对应的单元格值
        List<Map<String,Object>> data1 = reader.readAll();
        
        //方式2:读取后,每一行数据封装成Bean,默认第一行为标题行,Bean中的字段名为标题
        List<Student> data2 = reader.readAll(Student.class);
    }
}

Note

简单地对POI进行封装,方便用户开发

  1. 通过EasyExcel
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>easyexcel</artifactId>
  <version>${easyexcel.version}</version>
</dependency>

示例代码见:https://easyexcel.opensource.alibaba.com/open in new window

Note

解决了POI大文件内存容易溢出的问题

  1. 通过StreamingReader
<dependency>
  <groupId>com.monitorjbl</groupId>
  <artifactId>xlsx-streamer</artifactId>
  <version>2.2.0</version>
</dependency>
public class MonitorJBLExample {
    /**
     * 导入示例代码
     * @param inputStream
     */
    public void importExcel(InputStream inputStream){
        int rowCacheSize = 200; //缓存到内存中的行数(默认10行)
        int bufferSize = 10240; //缓存到内存中的默认大小(默认1024字节)
        Workbook book = StreamingReader.builder().rowCacheSize(rowCacheSize).bufferSize(bufferSize).open(inputStream);
        for(Sheet sheet:book){
            for(Row row:sheet){
                for(Cell cell:row){
                    System.out.println(cell.getStringCellValue());
                    //...
                }
            }
        }
    }
}

Note

解决了POI大文件内存容易溢出的问题

1.2 批量插入数据库(mybatis)

  1. 多次调用 void insert(Student)
START TRANSACTION;
insert into Student() values();
COMMIT;

START TRANSACTION;
insert into Student() values();
COMMIT;

...

Note

效率低,每次插入都会自动开启新事务,比较耗时

  1. 增加一个 void batchInsert(List)方法
START TRANSACTION;
insert into Student() values(),(),()...;
COMMIT;

Note

效率比上面高,但sql容易超长。(mybatis默认sql长度不能超过4m)

  1. 将JDBC改成批处理(ExecutorType.BATCH)
START TRANSACTION;
insert into Student() values();
insert into Student() values();
insert into Student() values();
...
COMMIT;
  1. 2+3结合使用
START TRANSACTION;
insert into Student() values(),(),()...;
insert into Student() values(),(),()...;
insert into Student() values(),(),()...;
...
COMMIT;

2. 异步/多线程 的多种写法

JDK1.8之前

Note

异步执行:直接新建一个Thread对象

public class Test{
    public static void one(){

        Thread thread = new Thread(){
            @Override
            public void run() {
                try {
                    //停留1s,展示异步执行成功
                    Thread.sleep(1000);
                    System.out.println("线程ID:"+Thread.currentThread().getId()+"  输出:haha");
                    //
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        thread.start();
        System.out.println("线程ID:"+Thread.currentThread().getId()+"  输出:heihei");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Note

异步执行:新建1个Runnable对象,放到线程池执行

public class Test{
    public static void two(){
        //定义一个任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                //停留1s,展示异步执行成功
                try {
                    Thread.sleep(1000);
                    System.out.println("线程ID:"+Thread.currentThread().getId()+"  输出:haha");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        //定义一个线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(1);
        threadPool.execute(runnable);
        System.out.println("线程ID:"+Thread.currentThread().getId()+"  输出:heihei");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //记得关闭线程池
        threadPool.shutdown();
    }
    
}

Note

多线程执行:新建N个Runnable对象,放到线程池执行

public class Test{
    public static void three() {
        //需要定义:
        // 1个计数器(CountDownLatch)
        // 1个线程池(ExecutorService)
        // 10个无返回值的任务(Runnable)

        int TASK_NUM = 10;
        //计数器
        CountDownLatch cdl = new CountDownLatch(TASK_NUM);
        //定义10个任务
        List<Runnable> taskList = new ArrayList<>();
        for(int i=0;i<TASK_NUM;i++){
            int finalNum = i;
            taskList.add(() -> {
                try {
                    Thread.sleep(finalNum * 1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("==========执行完毕!" + finalNum);
                cdl.countDown();
            });
        }
        //多线程执行任务
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        for(Runnable task:taskList){
            threadPool.execute(task);
        }
        //等待所有任务执行完毕
        System.out.println("==========wait!");
        try {
            cdl.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //最后记得关闭线程池
        threadPool.shutdown();
        System.out.println("==========finish!");
    }
}

Note

多线程执行&有返回值:新建N个FutureTask对象,放到线程池执行

public class Test{
    public static void four() {
        //需要定义:
        // 1个计数器(CountDownLatch)
        // 1个线程池(ExecutorService)
        // 10个有返回值的任务(FutureTask + Callable)


        int TASK_NUM = 10;
        //计数器
        CountDownLatch cdl = new CountDownLatch(TASK_NUM);
        //定义10个任务
        List<FutureTask<Integer>> taskList = new ArrayList<>();
        for(int i=0;i<TASK_NUM;i++){
            int finalNum = i;
            taskList.add(new FutureTask<>(() -> {
                try {
                    Thread.sleep(finalNum * 1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("==========执行完毕!" + finalNum);
                cdl.countDown();
                return finalNum;
            }));
        }
        //多线程执行任务
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        for(FutureTask task:taskList){
            threadPool.execute(task);
        }
        //等待所有任务执行完毕
        System.out.println("==========wait!");
        try {
            cdl.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //取出计算结果并汇总。这里只是简单地求和。
        int sum = 0;
        for(FutureTask<Integer> task:taskList){
            try {
                sum+=task.get();
            } catch (Exception e) {
                throw new RuntimeException(e);
            } 
        }
        System.out.println("==========result:"+sum);
        //最后记得关闭线程池
        threadPool.shutdown();
        System.out.println("==========finish!");
    }
}

Note

多线程执行&有返回值2:无需计数器

public class Test{
    public static void five() {
        //需要定义:
        // 不用计数器(CountDownLatch)
        // 1个线程池(CompletionService代替ExecutorService)
        // 10个有返回值的任务(Callable)
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        CompletionService<Integer> cService = new ExecutorCompletionService<Integer>(threadPool);
        int TASK_NUM = 10;
        //定义10个任务
        List<Callable<Integer>> taskList = new ArrayList<>();
        for(int i=0;i<TASK_NUM;i++){
            int finalNum = i;
            taskList.add(() -> {
                return finalNum;
            });
        }

        //执行任务
        for(Callable task:taskList){
            cService.submit(task);
        }

        //不用等到所有任务出结果才汇总
        //先计算出结果的就可以先汇总
        int sum = 0;
        for(int i=0;i<TASK_NUM;i++){
            try {
                int result = cService.take().get();
                System.out.println("==========执行完毕!" + result);
                sum+= result;
            } catch (Exception e) {
                throw new RuntimeException(e);
            } 
        }
        System.out.println("==========result:"+sum);
        //最后记得关闭线程池
        threadPool.shutdown();
        System.out.println("==========finish!");
    }
}

JDK1.8后

Note

异步执行,可使用CompletableFuture类来执行异步方法。写法简单

public class Test{
    public static void one(){
        //开启一个线程
        CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
            System.out.println("线程ID:"+Thread.currentThread().getId()+"  输出:haha");
        });
        System.out.println("线程ID:"+Thread.currentThread().getId()+"  输出:heihei");
        future.join();
    }
}

Note

多线程执行&有返回值

public class Test{
    public static void two() {
        //需要定义:
        // 不用计数器(CountDownLatch)
        // 不用线程池(CompletableFuture自带线程池(ForkJoinPool.commonPool()))
        // 10个有返回值的任务(Callable)
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        int TASK_NUM = 10;
        //定义10个任务
        AtomicInteger sum = new AtomicInteger();
        List<CompletableFuture<Integer>> taskList = new ArrayList<>();
        for(int i=0;i<TASK_NUM;i++){
            int finalNum = i;
            //执行任务
            taskList.add(CompletableFuture.supplyAsync(() -> {
                try {
                    Thread.sleep(finalNum * 1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程ID:"+Thread.currentThread().getId()+"==========执行完毕!结果:" + finalNum);
                return finalNum;
            }).thenApply(sum::addAndGet));
        }


        System.out.println("==========等待汇总结果!");
        CompletableFuture
                .allOf(taskList.toArray(new CompletableFuture[taskList.size()]))
                .join();
        System.out.println("==========result:"+sum);
        //最后记得关闭线程池
        threadPool.shutdown();
        System.out.println("==========finish!");
    }
}

3. 手工开启数据库事务

@Service
@Slf4j
public class TestServiceImpl implements TestService{
    @Autowired
    private TransactionTemplate transactionTemplate;
    
    @Autowired
    private StudentMapper studentMapper;

    @Autowired
    private TeacherMapper teacherMapper;
    
    public void test(List<StudentDto> studentDtoList,List<TeacherDto> teacherDtoList){
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                try{
                    studentMapper.batchInsert(studentDtoList);
                    teacherMapper.batchInsert(teacherDtoList);
                }catch (Exception e){
                    log.error("保存报错",e);
                    transactionStatus.setRollbackOnly();
                }
            }
        });
    }
}

4. LocalDate、LocalTime、LocalDateTime的使用

日期时间类说明
LocalDate只包含年月日
LocalTime只包含时分秒
LocalDateTime包含年月日、时分秒
  • 获取年月日、时分秒
LocalDate now = LocalDate.now(); //获取当前的年月日
System.out.println(now); //2024-05-28
System.out.println(now.getYear()); //2024
System.out.println(nowdata.getMonth().getValue());//4 获取当前月份
System.out.println(nowdata.getDayOfMonth());//6 获取今天几号
System.out.println(nowdata.getDayOfWeek().getValue());//2 获取今天星期几
System.out.println(nowdata.getDayOfYear());//96 获取今天是今年的第几天

Date nowDate = Date.from(now.atStartOfDay(ZoneId.systemDefault()).toInstant()); //LocalDate转成Date
System.out.println(DateTimeFormatter.ofPattern("yyyy-MM").format(nextMonthFirstDay)); //按yyyy-MM转成字符串
//字符串按yyyy-MM格式转为LocalDate

LocalTime nowTime = LocalTime.now();
System.out.println(nowTime);//09:51:11.987 获取当前的时间
System.out.println(nowTime.getHour());//9 获取当前时间的小时
System.out.println(nowTime.getMinute());//51 获取当前时间的分钟
System.out.println(nowTime.getSecond());//11 获取当前时间的秒


5. Map <--> Object 转换示例代码

// Map 转 Bean
cn.hutool.core.bean.BeanUtil.mapToBean(map,StudentDto.class,false,null);

// Bean 转 Map
cn.hutool.core.bean.BeanUtil.beanToMap(studentDto);

// List<Map> 转 List<Bean>

6. Java8 Stream Collectors的常见用法

6.1 集合转换


//1.1 list转成set(可以用来去重)
Set<String> personNameSet = personNameList.stream().collect(Collectors.toSet());

//1.2 list转成map(如果有重名,则取第一个)
Map<String,Person> personMap = personList.stream().collect(Collectors
  .toMap(e->e.getPersonName(),e->e,(e1,e2)->e1));

6.2 分组

//按姓名分组
Map<String,List<Person>> personMap = personList.stream().collect(Collectors
  .groupingBy(Person::getName);
  
//按班级+姓名分组
Map<String,List<Person>> personMap = personList.stream().collect(Collectors
  .groupingBy(e->e.getClassName()+e.getName());

6.3 分组后操作

//按班级分组,并计数出每班总人数
Map<String,Long> classPersonCountMap = personList.stream().collect(Collectors
  .groupingBy(Person::getClassName,Collectors.counting());

7. Java8 Stream Comparator的常见用法

//对person List进行排序
//先根据名字升序排序
//再根据年龄降序排序
List<Person> sortedPersons = persons.stream()
                .sorted(Comparator.comparing(Person::getName)
                .thenComparing(Comparator.comparing(Person::getAge).reversed()))
                .collect(Collectors.toList());

8. 时间是怎样定义的

GMT(格林威治标准时间)

1884年,国际经度会决定以经过格林尼治天文台(旧址) 的经线为本初子午线(0 度经线)。同时也将全球划分为了 24 个时区。0 度经线所在的时区为 0 时区。

UT(世界时)

UT在格林威治标准时间的基础上,增加了衡量一秒、一小时、一天究竟有多长的标准。

这个标准就是,UT会基于地球自转,来衡量时间的长度:

  • 地球自转1圈定义为1天;
  • 1小时为地球自转周期的1/24
  • 1秒则被定义为地球自转周期的1/86400

UTC(世界协调时)

后来,人们意识到地球自转不是一个完全恒定的过程,地球会转得越来越慢

地球的减速会导致每天的长度变长,每年大约增加1.7毫秒。

这样下去,会导致1秒的时间会越来越长。

为了追求1秒时间基本恒定,在不断寻找新的计时手段的努力下,人们发明了原子钟

国际计量协会结合了全球 400 多个原子钟,规定 1 秒为铯-133 原子基态两个超精细能级间跃迁辐射震荡 9,192,631,770 周所持续的时间
这个定义就叫国际原子时(International Atomic Time,TAI)。这样,我们钟表里指针应该转多快也有了一个统一的标准。

国际原子时的秒长以格林威治时间 1958 年 1 月 1 日 0 时的秒长为基准。也就是规定, 在这一瞬间,国际原子时的秒长和世界时的秒长是一样的。

闰秒

同时,引入闰秒的概念,来控制UT和UTC越来越大的误差。

所谓闰秒,也就是让在某个时间点上,人为规定这一分钟比普通的分钟多一秒,它有 61 秒。

img.png
img.png

9.时间的格式有哪些

ISO 8601

格式1:yyyy-MM-ddThh:mm:ssZ
示例1:2022-09-03T14:13:00Z ------- 用T隔开日期和时间,Z代表这是UTC或GMT时间,0时区。

格式2:yyyy-MM-ddThh:mm:ss.SSS+0000
示例2:2023-02-23T11:03:11.000+0000 --------用T隔开日期和时间,有3位代表微秒,加号后面4位表示时区

UNIX时间戳

Unix 时间戳只表示从特定时间点到现在的秒数。

GMT

格式:EEE,DD MMM YYYY HH:mm:ss GMT
示例1:Tue, 29 Nov 2022 03:30:28 GMT(0时区时间)
示例2:Tue, 29 Nov 2022 03:30:28 GMT+0800(中国标准时间)

CST

中国标准时间,跟GMT格式很相似。
注意,CST可以表示多个时区的时间:

  • 美国中部时间(Central Standard Time)
  • 澳大利亚中部时间(Central Standard Time)
  • 中国标准时间(China Standard Time)
  • 古巴标准时间(Cuba Standard Time)
    因此使用CST格式的日期转成GMT可能会有错误。

格式:EEE MM DD HH:mm:ss CST YYYY
示例:Thu Aug 18 20:38:54 CST 2016

10. PageHelper的常见用法

10.1 最常见的分页(先count再查询)

PageHelper.startPage(pageNum,pageSize);
List<YourEntity> result = findByExample(example);
return new PageInfo(result);

10.2 分页,但不执行count

Boolean isCount = false;
PageHelper.startPage(pageNum,pageSize,isCount);
List<YourEntity> result = findByExample(example);
return new PageInfo(result);

10.3 只count,不查询数据

// 使用PageHelper.startPage方法来设置分页参数,但不传入具体的pageSize和pageNum
PageHelper.startPage(1, 0);

List<YourEntity> result = findByExample(example);
// 使用PageInfo来获取总数
PageInfo<YourEntity> pageInfo = new PageInfo<>(result);
long total = pageInfo.getTotal(); // 这里获取的就是count的结果

11. 字符串操作

带参数的字符串---方法1:String.format()

// %s 字符串
// %d 整数
String formattedString = String.format("My name is %s and I am %d years old.", name, age);

带参数的字符串---方法2:MessageFormat.format()

String greeting = MessageFormat.format("你好,{0},你今年{1}岁,是一名{2}。", name, age, job);

下划线转驼峰:cn.hutool.core.util.StrUtil.toCamelCase()

String camelStr = StrUtil.toCamelCase(underLineStr);

EasyExcel拦截器的运用

  • AbstractWorkbookWriteHandler
  • AbstractRowWriteHandler
  • AbstractCellWriteHandler(已废弃)
  • CellWriteHandler接口
  • HorizontalCellStyleStrategy
  • AbstractVerticalCellStyleStrategy

ExecutorService

ExecutorService是一个线程池接口,有2种方式可以初始化

//方法1:直接new一个对象(推荐)
ExecutorService executorService = new ThreadPoolExecutor(5, 50, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
//方法2:使用Executors工具类
ExecutorService threadPool = Executors.newFixedThreadPool(5);

通过下面的方法,往线程池添加一个任务。

ExecutorService.submit(()->{});

添加完后,任务会自动在后台运行,主线程不会被阻塞。

Last Updated:
Contributors: dongyz8