前言
不知不觉已经实习两个月了,记录一下 ‘美好’ 的实习生活
难点1. 在已有的excel表内追加数据不发生栈溢出(10w条数据,关键字:‘已有’,‘追加’)
之前没接触过excel,一上手就来个有意思的(公司用的poi)
在网上找了半圈只找到,读取大数据量的api(Stream流)和往空文件写入大量数据的api(SXSSFWorkbook)
他们俩的共同特点都是将内存里的数据用一段扔一段,于是用了一个很笨的办法实现了需求。
大概思路就是用Stream流读取数据,中间再对数据进行判断,如果符合新增数据的条件,就把要增加的数据添加进读取的数据中,最后再用 SXSSFWorkbook 向空文件写入。
缺点就是效率低,excel的样式丢失(读取的样式有问题)
贴一下代码
/**
* 根据map 对大批量数据的EXCEL 进行写操作
* @param map
* @param file
* @param sheetIndex
*/
public static void inserLargeExcelByMap(Map<String, Object> map, File file, int sheetIndex) {
//输出流
BufferedOutputStream bos = null;
try (
FileInputStream fis = new FileInputStream(file);
//原excel
Workbook workbook = StreamingReader.builder()
.rowCacheSize(100)
.bufferSize(4096)
.open(fis);
//新excel
SXSSFWorkbook sxssfWorkbook = new SXSSFWorkbook();
) {
//1.获取 sheet
Sheet oldSheet = workbook.getSheetAt(sheetIndex);
SXSSFSheet newSheet = sxssfWorkbook.createSheet(oldSheet.getSheetName());
//用于标记新增数据是否插入
boolean flag = false;
for (String key : map.keySet()) {
long columnIndex = transLetter2Num(key.replaceAll("[0-9]", ""));
long rowIndex = Long.parseLong(key.replaceAll("[a-zA-Z]", ""));
//2.1遍历原excel 填充数据到新 excel
for (Row oldRow : oldSheet) {
//2.2 创建新row
SXSSFRow newRow = newSheet.createRow(oldRow.getRowNum());
//2.3 判断插入数据的行 是否在所在行
if (rowIndex == oldRow.getRowNum()) {
for (Cell oldCell : oldRow) {
if (columnIndex == oldCell.getColumnIndex()) {
throw new RuntimeException("要插入数据的行列在excel已存在");
} else {
//2.4 填充原数据到新excel
newRow.createCell(oldCell.getColumnIndex()).setCellValue(oldCell.getStringCellValue());
}
}
//2.5 插入新增数据到新excel
newRow.createCell((int) columnIndex).setCellValue(map.get(key).toString());
//2.6 设置插入标记
flag = true;
} else {
// 3.1 新数据的行不在原excel 已有行时
for (Cell oldCell : oldRow) {
//3.2 填充原数据到新excel
newRow.createCell(oldCell.getColumnIndex()).setCellValue(oldCell.getStringCellValue());
}
}
}
//4 遍历完所有数据,新数据还没插入时,插入新数据
if (!flag) {
newSheet.createRow((int) rowIndex).createCell((int) columnIndex).setCellValue(map.get(key).toString());
}
}
//遍历完之后 将原excel删除
String filePath = file.getAbsolutePath();
file.delete();
bos = new BufferedOutputStream(new FileOutputStream(filePath));
sxssfWorkbook.write(bos);
sxssfWorkbook.dispose();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 将 excel 中的字母列索引转换为数字列索引
*
* @param letterColIdx 字母列索引
* @return rlt 返回字母列索引对应的数字索引 A -> 0 BA -> 52 CA -> 78
* */
public static long transLetter2Num(String letterColIdx) {
long rlt = 0;
letterColIdx = letterColIdx.toUpperCase();
String[] sts = letterColIdx.split("");
int length = sts.length;
for (int i = 0; i < length; i++) {
String st = sts[i];
char[] cs = st.toCharArray();
int idx = (int)cs[0];
// 如果字符不是 A-Z 之间的字符, 则抛错
if (idx < 65 || idx > 90) {
throw new ExcelAnalysisException("Characters["+ st +"] not between A and Z.");
}
idx -= 64;
long num = (long) (idx * Math.pow(26, length - i - 1));
rlt += num;
}
return --rlt;
}
难点2. 优化某个代码片段的执行速度
拿到代码跑了一下,好家伙,跑了半个小时抛出死锁异常(人生第一次遇到死锁…),仔细看了看代码逻辑,发现以下几个问题。
问题一: 有一条查询所有数据的sql大致是
select * from table where column = '某1' order by '某2'
查看设计表,只有一个 id 主键索引,这条sql大概运行了接近10s(公司电脑比较烂),查出来10w条多点的数据
然后我把 sql 改为下面的语句,用时4s,sql优化知识之前几乎是空白,只在B站上看过一次图灵学院的免费公开课。
select * from table a, (select id from table where column = '某1') b where a.id = b.id order by '某2'
最后采用的是这条语句,并给 ‘某一,某二’增加了联合索引,用时1.9s
select 需要的字段 from table where column = '某1' order by '某2'
本来想试试这条语句,结果报错了,没搞懂为啥
select 需要的字段 from table a, (select id from table where column = '某1') b where a.id = b.id order by '某2'
问题二: 代码中有一个逻辑是将上述sql查询的10w条数据遍历,并根据条件进行更新,每一次循环更新的语句大概有8条…遍历完对数据库进行的IO操作至少以万为单位,这代码能运行快那才见鬼了
解决思路:想起来Mybatis-Plus中有条批量删除的语句,于是百度搜索关键字 mysql 批量更新
找到这条语句
UPDATE mytable SET
myfield = CASE id
WHEN 1 THEN 'value'
WHEN 2 THEN 'value'
WHEN 3 THEN 'value'
END
WHERE id IN (1,2,3)
接下来就好办了,再循环的时候 加上拼接 sql 字符串的逻辑,最后跑代码只用了 10s不到的样子
难点3. 生成 PDF 报销信息
一开始是打算先手动生成一个pdf模板然后用 pdfbox 替换关键字,结果遇到了中文乱码的问题,试了好几个编码都不行,进官网只找到 api 文档 (弱鸡看不懂),最后百度找到了一个很好用的 jar 包 Spire
文档地址
用段落➕表格 生成了 需要的PDF
过程中遇到的问题:
- 中文乱码,需要使用支持中文的字体
- 使用了
宋体
但是 linux环境 好像没有这个字体,导致报错,于是使用了从网上下载了免费商用的字体
难点4. 将 2.5w 个 word合同关键信息 提取,并添加进excel进行汇总
这一下 Office 三件套一样没落下,难点在于各个 word 里面的信息有差异,关键字也不一样,目前没找到啥好的解决办法
难点5. 会议室预约的前端实现
技术栈是vue + ant deign of vue,这算是我真正意义上使用 vue 开发项目,之前在学校有开发过一款跑腿小程序(行云跑腿,服务器到期了…),算是写着练手玩的。
需求:
- 响应式布局,支持 手机 和 PC
- 外观样式,自己看着办…
之前开发的小程序就是仿的 UU跑腿界面,有经验… ,最后决定模仿挺好看的一个日历界面
接手的当天晚上就回家恶补 vue vuex vue router的知识(之前有学过,但是很久没用忘的差不多了)
难点:
- 根据会议时常,鼠标移动时,对应时间段的div背景色改变。解决思路:给每个div加上 开始时间和结束时间的属性
- 切换会议室,如何刷新页面并且保留 data 数据。解决思路:将关键数据和 父组件同步,切换会议室时,通知父组件 配合 v-if 销毁重建 子组建
- 切换会议室,点击上一页,下一页增加过渡动画,模仿的那个日历做的动画很自然,我做的动画幅度太大,之前一直想通过修改动画间隔粒度来让动画变得自然。最后通过修改 移动的距离,完成了动画的复刻,模仿的挺像~
后端主要就是同时抢占会议室锁的问题,由于只是公司的几个领导会使用这个功能,并且代码应该是部署到一台服务器的,所以就使用了 synchronized 代码块
如果部署到多台服务器,可以用redis 实现一个简单的分布式锁
思路:通过 redis 的原子性操作,给 redis增加键值对。
- 只有当键不存在时,才增加
- 为防止应用宕机,需设置过期时间,自动将锁归还
- 为防止应用任务堵塞后(超过key过期时间),归还锁时,将别的任务加的锁给删除,解决办法可以再开一个线程定时重置锁的过期时间。
分享一个优雅的参数校验方法:validation
maven坐标:
<!--参数检验-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
简单示例:
最后加一个异常拦截配置
具体 API 参考
记录一次奇怪的BUG
bug问题:提交数据时,数据库插入了两条一样的数据
debug调试时,发现运行一条sql语句的代码,打印出一堆sql日志,一开始以为是通用mapper的问题,
后来发现是前端循环发送了四条异步请求,造成的并发问题。
加锁后发现问题依然存在,看了很久的代码,觉得逻辑没问题(第一次请求新增,后三次请求更新)
最后发现@Transactional注解 会使 synchronized关键字失效 ( synchronized代码块里执行的内容是在事务里面,在事务commit前可能有多个线程进入代码块)
,@Transactional注解又加在了整个service类上,给controller加锁,又会导致锁的粒度太大,最后交给了一个的前端实习妹子,让她用promise发同步请求
总结
- 由于大部分的任务都是对老项目(OA项目)进行维护和升级,深刻的明白了项目分层的意义,老项目没有service层
一个Controller 快一万行代码,每个方法都看的头皮发麻 - 变量命名要规范!! 老项目前端代码甚至出现 a1,a2,a3 … a21 这样的神奇变量命名
- 参数校验!! 打开项目 总是点这点着就报错,点开控制台 熟悉的 java.lang.NullPointerException,不知道使用的员工是怎么受得了的…