Service层服务层
service层
//文件上传
void importData(MultipartFile multipartFile);
//文件上传2(同时添加上传文件时给数据库添加的通道id)
void importData1(MultipartFile multipartFile, Long id);
//文件下载
void exportData(HttpServletResponse response);
serviceImpl层
文件上传
//文件上传
@Override
public void importData(MultipartFile multipartFile) {
if (multipartFile.isEmpty()) {
throw new GuiguException("0x500","文件为空");
}
ExcelListener<FunctionExcelVo> excelListener = new ExcelListener<>(functionMapper);
try {
EasyExcel.read(multipartFile.getInputStream(), FunctionExcelVo.class, excelListener)
.sheet().doRead();
} catch (Exception e) {
e.printStackTrace();
throw new GuiguException("0x501","文件格式错误");
}
}
//文件上传(同时添加上传文件时给数据库添加的通道id)
@Override
public void importData1(MultipartFile multipartFile, Long id) {
if (multipartFile.isEmpty()) {
throw new GuiguException("0x500","文件为空");
}
// 读取文件数据
List<FunctionExcelVo1> dataList;
try {
dataList = EasyExcel.read(multipartFile.getInputStream()).head(FunctionExcelVo1.class).sheet().doReadSync();
} catch (Exception e) {
e.printStackTrace();
throw new GuiguException("0x501","文件格式错误");
}
// 为每条数据添加通道 ID
for (FunctionExcelVo1 functionExcelVo1 : dataList) {
functionExcelVo1.setChannelId(id);
}
// 保存数据到数据库
functionMapper.saveData1(dataList);
}
逐句解释(文件上传)
@Override
public void importData(MultipartFile multipartFile) {
这是一个java方法的声明,该方法名为'importData',它接收一个‘MultipartFile’类型的参数。‘@Override’注解表明该方法是覆盖(或实现)父类或接口中的同名方法。
if (multipartFile.isEmpty()) {
throw new GuiguException("0x500","文件为空");
}
这段代码是用来检查文件是否为空。如果为空,就会抛出一个自定义异常‘GuiguException’,异常代码为“0x500”,异常信息为“文件为空”
ExcelListener<FunctionExcelVo> excelListener = new ExcelListener<>(functionMapper);
这里创建一个'ExcelListener'对象,用来监听Excel文件的读取。‘< FunctionExcelVo >’指定了读取的Excel行的类型,而“functionMapper”是在构造‘ExcelListenter’对象时传入的。
try {
EasyExcel.read(multipartFile.getInputStream(), FunctionExcelVo.class, excelListener)
.sheet().doRead();
} catch (Exception e) {
e.printStackTrace();
throw new GuiguException("0x501","文件格式错误");
}
这段带代码使用了EasyExcel库来读取Excel文件。它从‘multipartFile’中获取输入流,并将其传递给EasyExcel的‘read’方法。‘FunctionExcelVo.class’指定了Excel中每一行的数据类型。‘excelListener’则作为监听器传递给EasyExcel,以便处理读取的数据。
-
在EasyExcel库中,
.sheet().doRead()
用于指定读取Excel文件的操作。让我们来详细解释它的用途:-
.sheet():这个方法用于指定的读取的Excel工作表(sheet)。在大多数情况下,Excel文件包含多个工作表,通过‘.sheet’方法可以指定要读取的特定工作表。如果不指定,EasyExcel默认读取第一个工作表。
-
.doRead():这个方法执行实际的读取操作。它会启动Excel文件的读取,并将读取的数据通过之前设置的监听器进行处理。通过ExcelListenter监听器处理读取到的数据。
-
-
综合起来,.sheet(),.doRead()的作用是指定要读取的Excel工作表,并执行读取操作。它会将读取到的数据传递给之前设置的监听器进行处理
如果读取过程中出现异常,捕获并打印异常信息,然后抛出一个自定义异常‘GuiguException’,异常代码‘0x500’,异常信息为“文件格式错误”。整体来说,这段代码的作用是从一个上传的Excel文件中读取数据,并将其保存到系统中。它确保文件不为空,并且通过EasyExcel库来读取Excel数据,同时处理可能出现的异常情况。
逐句解释(文件上传2+id)
@Override
public void importData1(MultipartFile multipartFile, Long id) {
这是一个Java方法的声明,名为importData1,它接受两个参数:一个是MultipartFile类型的上传文件,另一个是一个长整型的通道ID。
if (multipartFile.isEmpty()) {
throw new GuiguException("0x500","文件为空");
}
这段代码检查上传的文件是否为空,如果为空,则抛出自定义异常GuiguException
,异常代码为0x500
,异常信息为"文件为空"。
List<FunctionExcelVo1> dataList;
try {
dataList = EasyExcel.read(multipartFile.getInputStream()).head(FunctionExcelVo1.class).sheet().doReadSync();
} catch (Exception e) {
e.printStackTrace();
throw new GuiguException("0x501","文件格式错误");
}
这里使用EasyExcel都读取上传的Excel文件数据,并将其转换为‘FunctionExcelVo1’对象的列表,.head(FunctionExcelVo1.class)
指定了Excel文件的表头。.sheet()
用于指定要读取的工作表(默认为第一个工作表),.doReadSync()
表示同步读取文件数据。如果读取过程中出现异常,将会捕获异常并抛出自定义异常GuiguException
,异常代码为0x501
,异常信息为"文件格式错误"。
for (FunctionExcelVo1 functionExcelVo1 : dataList) {
functionExcelVo1.setChannelId(id);
}
这个循环遍历读取到的数据列表,并为每一条数据设置通道ID,以确保每条数据都与指定的通道相关联。
functionMapper.saveData1(dataList);
最后,将处理过的数据列表保存到数据库中,通过functionMapper
对象调用saveData1
方法来实现。
整体来说,这段代码的作用是将上传的Excel文件数据保存到数据库中,并在保存数据之前为每条数据设置指定的通道ID。
(文件上传)与(文件上传2+id)的不同
.doRead()与.doReadSync()区别
-
异步读取 (
doRead()
):-
doRead()
方法是异步执行的,意味着它会在后台线程中读取数据,并通过回调函数来处理读取到的数据。 -
在使用
doRead()
方法时,你需要提供一个实现了ReadListener
接口的监听器,用于处理读取到的数据。 -
异步读取适用于大型数据集,可以提高读取性能,但相对复杂一些,因为需要处理多线程和异步回调。
-
-
同步读取 (
doReadSync()
):-
doReadSync()
方法是同步执行的,即它会阻塞当前线程,直到读取完整个Excel文件并返回所有数据。 -
同步读取不需要使用监听器,读取完成后会直接返回数据列表。
-
同步读取适用于数据量较小的情况,代码相对简单,但可能会影响性能,特别是在读取大型文件时,可能会导致程序响应变慢或阻塞。
-
因此,区别主要在于执行方式和是否需要使用监听器。如果需要在读取过程中执行一些自定义的逻辑或处理大量数据,推荐使用异步读取,并提供相应的监听器。如果只是简单地读取数据并将其保存到数据库,可以选择同步读取。
那是否可以调换
能,你可以将 .doReadSync()
替换为 .doRead()
。但是,需要考虑以下几点:
-
性能影响:
-
使用
.doRead()
而不是.doReadSync()
将读取行为改为异步模式。异步读取可以在读取文件时不阻塞当前线程,从而潜在地提高性能。但是,它也会增加复杂性,特别是在处理每一行数据的回调时。
-
-
错误处理:
-
异步读取可能需要不同的错误处理机制。在同步读取(
.doReadSync()
)中,异常可以在try-catch
块中立即捕获和处理。然而,在异步读取(.doRead()
)中,错误处理通常涉及到回调或监听完成事件,这可能根据具体情况更为复杂。
-
-
线程注意事项:
-
异步读取可能涉及多线程,如果处理不当可能会引入并发问题。确保在异步上下文中访问的任何共享资源都是线程安全的。
-
如果决定使用 .doRead()
而不是 .doReadSync()
,请确保适当处理异步行为,包括错误处理和潜在的线程注意事项。
dataList = EasyExcel.read(multipartFile.getInputStream()).head(FunctionExcelVo1.class).sheet().doReadSync();与 EasyExcel.read(multipartFile.getInputStream(), FunctionExcelVo.class, excelListener).sheet().doRead();不同
这两种读取方式之间的主要区别在于同步与异步读取。(文件上传2+id)主要是有添加id的原因,需要用到数据列表。
文件下载
//文件导出(下载)
@Override
public void exportData(HttpServletResponse response) {
//1.设置响应头信息和其他信息
try {
// 设置响应结果类型
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("分类数据", "UTF-8");
//设置响应头
response.setHeader("Content-disposition","attachment;filename=" + fileName + ".xlsx");
response.setHeader("Access-Control-Expose-Headers","Content-disposition");
//查询所有分类,返回list集合
List<Stu> stuList = functionMapper.findAll();
//最终数据list集合
List<FunctionExcelBo> functionExcelBoList = new ArrayList<>();
for (Stu stu : stuList) {
FunctionExcelBo functionExcelBo = new FunctionExcelBo();
BeanUtils.copyProperties(stu, functionExcelBo);
functionExcelBoList.add(functionExcelBo);
}
//写入操作
EasyExcel.write(response.getOutputStream(), FunctionExcelBo.class)
.sheet("数据").doWrite(functionExcelBoList);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
逐句讲解(文件下载)
这段代码是一个Java方法,用于将数据导出为Excel文件并通过HTTP响应下载。
-
response.setContentType("application/vnd.ms-excel");
- 设置响应的内容类型为Excel文件。 -
response.setCharacterEncoding("utf-8");
- 设置响应的字符编码为UTF-8,以支持中文字符。 -
String fileName = URLEncoder.encode("分类数据", "UTF-8");
- 对文件名进行URL编码,以防止中文乱码。这里的文件名是“分类数据”。 -
response.setHeader("Content-disposition","attachment;filename=" + fileName + ".xlsx");
- 设置响应头,指定文件名并声明为附件,告知浏览器以附件形式下载。 -
response.setHeader("Access-Control-Expose-Headers","Content-disposition");
- 设置响应头,暴露Content-disposition
字段,以便客户端可以读取并处理该响应头信息。 -
List<Stu> stuList = functionMapper.findAll();
- 从数据库中查询所有分类数据,返回学生列表。 -
List<FunctionExcelBo> functionExcelBoList = new ArrayList<>();
- 创建一个用于存储Excel数据对象的列表。 -
BeanUtils.copyProperties(stu, functionExcelBo);
- 将每个学生对象(Stu
)转换为对应的Excel数据对象(FunctionExcelBo
)。 -
EasyExcel.write(response.getOutputStream(), FunctionExcelBo.class).sheet("数据").doWrite(functionExcelBoList);
- 使用EasyExcel将Excel数据对象列表写入到输出流中,并生成Excel文件。.sheet("数据")
表示在Excel中创建一个名为“数据”的工作表。
总的来说,这段代码实现了从数据库查询数据到生成Excel文件并提供下载的功能。
response.setHeader("Content-disposition","attachment;filename=" + fileName + ".xlsx");
response.setHeader("Access-Control-Expose-Headers","Content-disposition");
这两行代码分别设置了HTTP响应头中的两个字段:
-
Content-disposition
:这个响应头字段告诉浏览器如何处理服务器端返回的响应数据。在这段代码中,通过设置Content-disposition
为attachment
,告诉浏览器将响应内容作为附件下载,而不是在浏览器中直接显示。filename
参数指定了下载的文件名,.xlsx
表示下载的文件是一个Excel文件。这样设置之后,浏览器会提示用户下载文件,并将文件保存到本地。 -
Access-Control-Expose-Headers
:这个响应头字段用于CORS(跨域资源共享)设置。在这段代码中,通过设置Access-Control-Expose-Headers
为Content-disposition
,告诉浏览器在处理跨域请求时,允许前端JavaScript代码访问Content-disposition
字段。这是因为某些情况下,跨域请求的JavaScript代码默认是无法访问所有的响应头字段的,但通过设置Access-Control-Expose-Headers
,可以显式地将指定的响应头字段暴露给JavaScript代码。
Mapper层
//插入数据到数据库(上传文件)
void saveData(List<FunctionExcelVo> cachedDataList);
//插入数据到数据库(文件上传2+id)
void saveData1(List<FunctionExcelVo1> cachedDataList);
//查询所有(文件下载)
List<Stu> findAll();
mapper.xml层
<!-- 插入数据到数据库(上传文件) -->
<insert id="saveData">
INSERT INTO
student
(stu_id,stu_name)
VALUES
<foreach collection="list" item="cachedDataList" separator=",">
(#{cachedDataList.stuId}, #{cachedDataList.stuName})
</foreach>
</insert>
<insert id="saveData1" parameterType="java.util.List">
INSERT INTO student (stu_id, stu_name, channel_id)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.stuId}, #{item.stuName}, #{item.channelId})
</foreach>
</insert>
<!-- 查询所有学生(用于文件下载) -->
<select id="findAll" resultType="com.tlm.people.entity.Stu">
SELECT
id,stu_id,stu_name,status
FROM
student
</select>
Foreach知识点
<foreach>
标签是 MyBatis 中用于循环遍历集合或数组的标签,用于在 SQL 语句中动态生成多个相似的 SQL 片段。以下是 <foreach>
标签的一些关键知识点:
-
遍历集合或数组:
-
<foreach>
标签通过collection
属性指定要遍历的集合或数组的属性名或表达式。MyBatis 将会根据这个属性从对应的对象中获取数据进行遍历。
-
-
别名设置:
-
使用
item
属性可以指定在每次迭代中当前元素的别名。在 SQL 语句中可以使用这个别名引用当前遍历到的元素。
-
-
分隔符设置:
-
通过
separator
属性可以指定在迭代过程中每个元素之间的分隔符。这个分隔符会在生成的 SQL 片段中的每两个元素之间插入。
-
-
索引变量:
-
如果遍历的是数组,可以通过
index
属性设置索引变量的别名。索引变量是当前遍历到的元素在数组中的索引值。
-
-
集合参数:
-
MyBatis 在执行 SQL 语句时会将整个集合作为参数传递给 SQL 语句,而不是每次迭代只传递一个元素。因此,在 SQL 语句中可以直接引用整个集合,而不需要在迭代过程中处理单个元素。
-
-
动态 SQL 生成:
-
<foreach>
标签通常用于在 SQL 语句中动态生成多个 SQL 片段,例如批量插入或更新数据,动态 IN 查询等场景。
-
使用 <foreach>
标签可以灵活处理集合或数组数据,动态生成符合需求的 SQL 片段,从而实现更加灵活和高效的 SQL 操作。
案例
以下是一个示例展示如何在 MyBatis 中使用 <foreach>
标签进行动态 SQL 生成的案例:
假设有一个 User
实体类,包含 id
、name
和 age
属性。现在我们想要根据一组用户 ID 批量查询用户信息。
首先,我们需要定义一个 Mapper 接口,包含一个查询方法:
public interface UserMapper {
List<User> findUsersByIds(List<Long> userIds);
}
然后,在对应的 Mapper XML 文件中编写 SQL 语句:
<select id="findUsersByIds" parameterType="java.util.List" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="list" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
</select>
在这个示例中,我们使用了 <foreach>
标签来动态生成 IN
子句中的用户 ID 列表。关键点如下:
-
collection="list"
:指定了要遍历的集合属性名为list
,这个集合包含了一组用户 ID。 -
item="userId"
:指定了在每次迭代中当前元素的别名为userId
,即当前遍历到的用户 ID。 -
open="("
、separator=","
、close=")"
:这三个属性分别定义了在迭代过程中 SQL 语句的开头、元素之间的分隔符、结尾,用于构建完整的IN
子句。
这样,当调用 findUsersByIds
方法时,传入一组用户 ID,MyBatis 将会根据传入的 ID 动态生成 SQL 语句,并查询匹配的用户信息。
插入数据到数据库(上传文件)
-
<insert id="saveData">...</insert>
: 这是一个MyBatis的插入语句,它的ID为"saveData",意味着可以通过这个ID在代码中调用这段插入语句。 -
INSERT INTO student (stu_id, stu_name) VALUES
: 这是插入语句的标准格式,用于向名为"student"的表中插入数据,数据列为"stu_id"和"stu_name"。
其中
<foreach collection="list" item="cachedDataList" separator=","> (#{cachedDataList.stuId}, #{cachedDataList.stuName}) </foreach>
-
<foreach>
: 这是MyBatis提供的一个迭代标签,用于遍历集合中的元素,并在SQL语句中动态生成相应的语句。-
collection="list"
: 这里的"list"是指在调用插入语句时传入的参数列表,通常是一个包含多个对象的列表。 -
item="cachedDataList"
: 这里的"cachedDataList"是遍历过程中每个对象的别名,在每次迭代时代表当前遍历到的对象。 -
separator=","
: 这个属性指定了在迭代过程中每个对象之间的分隔符,通常是逗号。
-
-
(#{cachedDataList.stuId}, #{cachedDataList.stuName})
: 这是插入语句中的值部分,通过MyBatis的占位符#{}
来引用对象的属性。cachedDataList.stuId
和cachedDataList.stuName
分别表示遍历到的对象中的"stuId"和"stuName"属性。
插入数据到数据库(文件上传2+id)
-
<insert id="saveData1" parameterType="java.util.List">
:这里定义了一个名为 "saveData1" 的插入操作,它接受一个名为 "list" 的参数,参数类型为java.util.List
,这个列表中包含了要插入数据库的对象。 -
INSERT INTO student (stu_id, stu_name, channel_id) VALUES
:这是 SQL 插入语句的开始部分,指定了要插入数据的表名以及列名。
其中
<foreach collection="list" item="item" separator=",">
(#{item.stuId}, #{item.stuName}, #{item.channelId})
</foreach>
-
<foreach collection="list" item="item" separator=",">
:这是 MyBatis 中用于遍历集合的标签。collection="list"
指定了要遍历的集合对象名为 "list",item="item"
表示在每次迭代中当前元素的别名为 "item",separator=","
表示在迭代过程中每个元素之间使用逗号分隔。 -
(#{item.stuId}, #{item.stuName}, #{item.channelId})
:这是插入语句中的值部分,通过 MyBatis 的占位符#{}
来引用对象的属性。#{item.stuId}
、#{item.stuName}
和#{item.channelId}
分别表示每个对象中的 "stuId"、"stuName" 和 "channelId" 属性。
监听器:
//监听器
public class ExcelListener<T> implements ReadListener<T> {
/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;
/**
* 缓存的数据
*/
private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
//构造传递mapper,操作数据库
private FunctionMapper functionMapper;
public ExcelListener(FunctionMapper functionMapper) {
this.functionMapper = functionMapper;
}
public List<T> getCachedDataList() {
return cachedDataList;
}
public ExcelListener() {
}
/**
* 解析数据
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context analysis context
*/
@Override
public void invoke(T data, AnalysisContext context) {
//把每行数据放到集合中
cachedDataList.add(data);
//达到临界值,就去存储依次,防止数据库内存溢出
if(cachedDataList.size() >= BATCH_COUNT) {
//批量把数据添加到数据库
saveData();
//清理集合
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
//保存数据
saveData();
}
//保存的方法
void saveData() {
functionMapper.saveData((List<FunctionExcelVo>)cachedDataList);
}
}
逐句解释
//监听器
public class ExcelListener<T> implements ReadListener<T> {
这里是定义一个监听器的类“ExcelListener”,它实现了EasyExcel库中的“ReadListener”接口,泛型‘T’表示读取的数据类型。
private static final int BATCH_COUNT = 100;
这是一个常量,表示每读取100条数据就会执行一次批量保存操作。
private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
这里创建了一个缓存数据的列表,用于存储读取的数据。
ListUtils.newArrayListWithExpectedSize(BATCH_COUNT):用于创建一个初始容量为’BATCH_COUNT‘的空列表
private FunctionMapper functionMapper;
这是一个数据访问的mybits中的mapper,用于操作数据库。
public ExcelListener(FunctionMapper functionMapper) {this.functionMapper = functionMapper;}
这是一个监听器的构造方法,用于传入一个数据访问对象,以便于在监听器中进行数据库操作。
@Override
public void invoke(T data, AnalysisContext context) {
//把每行数据放到集合中
cachedDataList.add(data);
//达到临界值,就去存储依次,防止数据库内存溢出
if(cachedDataList.size() >= BATCH_COUNT) {
//批量把数据添加到数据库
saveData();
//清理集合(只是创建一个新的列表)
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
在这段代码中,为什么使用cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
而不是cachedDataList.clear();
的主要原因是为了提高性能。
解释:
-
性能考虑:
-
cachedDataList.clear()
方法会清空列表中的所有元素,但他并不会减少列表的容量。这意味着,虽然列表中的元素被清空了,但列表仍然占用相同的内存空间。 -
如果不重新创建一个新的列表实例,而是使用
cachedDataList.clear()
来清空列表,那么在之后的数据添加操作中,当列表的大小再次达到临界值时,会触发的扩容操作。这会导致重新分配内存空间,可能会引起性能损失。
-
-
减少内存分配开销:
-
相比之下,
ListUtils.newArrayListWithExpectedSize(BATCH_COUNT)
会创建一个初始容量为BATCH_COUNT
的新列表实例。由于预先知道了列表的初始容量,这可以避免在之后添加元素时的内部扩容操作,从而减少了内存分配的开销。
-
-
更好的内存管理:
-
重新创建一个新的列表实例,可以确保之前的列表实例及其所持有的对象被及时释放,从而更好地管理内存,防止内存泄漏。
-
因此,为了避免潜在的性能问题和更好地管理内存,代码选择重新创建一个新的列表实例来清理集合。
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
这里是当缓存数据列表‘cachedDataList’达到了一定的数量(即‘BATCH_COUNT’),就会执行批量保存操作,并清除列表以释放内存。这里使用`ListUtils.newArrayListWithExpectedSize(BATCH_COUNT)‘重新创建一个新的列表实例,其初始容量为’(BATCH_COUNT)‘,以便重新开始缓存新的数据。
原因如下:
-
存储管理:随者程序运行,缓存的数据列表会不断增长,可能占用大量内存。通过清空列表,可以释放已经使用的内存,防止内存占用过高导致内存溢出或性能下降。
-
避免内存泄漏:如果不清空列表,而是一直是用列表实例,即使其中的数据被处理完毕,但由于列表对象仍然存在于内存中,并持有数据对象的引用,这可能导致内存泄漏问题。
-
提高性能:重新创建一个新的列表实例,可以避免频繁的内存扩容操作,从而提高程序的性能。
清理集合的操作旨在有效地管理内存,避免内存泄漏,并提高程序的性能。
这个方法是在读取Excel文件时调用的,他将每一行的数据添加到缓存数据列表中,并在达到一定数量时执行批量保存操作,以防止数据内存溢出。
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
//保存数据
saveData();
}
这个方法在所有数据解析完毕后调用,他确保最后一批次数据保存到数据库中。
void saveData() {
functionMapper.saveData((List<FunctionExcelVo>)cachedDataList);
}
这是一个私有方法,用于将缓存的数据批量保存到数据库中。
整体上,这个监听器的作用是在读取Excel文件时对数据进行处理,并在达到一定数量时批量保存到数据库中,以防止内存溢出。