先描述一下需求:
大前提在前面的文章将大文件存到Oracle数据库中已经描述过,不过又要新增一个微服务,数据库使用的Mysql,在编码的过程中,遇到几个坑,在此记录一下。
二、具体代码 1、几点说明平台数据库:Mysql
数据库字段:file_content为存储文件的字段
数据库的xml文件,注意file_content的类型为BINARY
select
file_tid, file_path, file_path_md, file_content
from t_file_content
where file_tid = #{fileTid,jdbcType=VARCHAR}
insert into t_file_content (file_tid, file_path, file_path_md, file_content)
values (#{fileTid,jdbcType=VARCHAR},#{filePath,jdbcType=VARCHAR},
#{filePathMd,jdbcType=VARCHAR},#{fileContent,jdbcType=BINARY})
Service层代码,注意file_content的POJO类型为byte[]数组
@Service
public class FileContentServiceImpl implements FileContentService {
private static final org.slf4j.Logger LOG =
LoggerFactory.getLogger(FileContentServiceImpl.class);
@Autowired
FileContentMapper contentMapper;
@Override
public boolean readResourceFromDB(String fileTid) {
FileOutputStream fos = null;
ByteArrayInputStream bis = null;
try{
// 查询资源
FileContent fileContent = contentMapper.selectByFileId(fileTid);
if(fileContent == null){
return false;
}
String filePath = fileContent.getFilePath();
File file = new File(filePath);
// 创建资源路径
if (!file.getParentFile().exists()) {
boolean succ = file.getParentFile().mkdirs();
if (!succ) {
throw new Exception("mkdir failed: " +
file.getParentFile().getAbsolutePath());
}
}
// 获取资源内容的byte[]
bis = new ByteArrayInputStream(fileContent.getFileContent());
// 将byte[]输出到文件
fos = new FileOutputStream(file);
int len = 0;
byte[] buf = new byte[1024];
while ((len = bis.read(buf)) != -1) {
fos.write(buf, 0, len);
}
return true;
} catch (Exception e) {
LOG.error("readResourceFromDB 异常:", e);
return false;
} finally {
LOG.debug("readResourceFromDB() enter finally");
if (null != fos) {
try {
fos.close();
} catch (IOException e) {
}
}
if (null != bis) {
try {
bis.close();
} catch (IOException e) {
}
}
}
}
@Override
public void saveResourceToDB(FileContent fileContent) throws Exception {
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
fileContent.setFilePathMd(Md5Tool.getMd5(fileContent.getFilePath()));
try {
File file = new File(fileContent.getFilePath());
if(!file.exists()) {
return;
}
fis = new FileInputStream(file);
byte[] buffer = null;
// 此处用字节输出流
bos = new ByteArrayOutputStream();
byte[] temp = new byte[1024];
int n;
while ((n = fis.read(temp)) != -1) {
bos.write(temp, 0, n);
}
// 将字节输出流转化为byte[],保存到数据库中
buffer = bos.toByteArray();
fileContent.setFileContent(buffer);
int result = contentMapper.insertFileContent(fileContent);
}catch (Exception e){
LOG.error("saveResourceToDB 异常:", e);
throw e;
}finally {
LOG.debug("saveResourceToDB enter finally");
if (null != bos) {
try {
bos.close();
} catch (IOException e) {
}
}
if (null != fis) {
try {
fis.close();
} catch (IOException e) {
}
}
}
}
}
三、小结
在将文件以流的形式存入Mysql数据库时,我遇到了下面几个问题:
(1)、是将文件以byte[]数组存入还是以String存入?
我一开始以String存入,程序报了heap space异常,很明显,堆内存溢出,这个问题不能通过改变堆内存的大小来解决,所以我放弃String,而是以byte[]数组。此处要注意的是jdbcType类型为BINARY。
(2)、如何获取byte[]数组?
起初,我使用下面的方式来获取byte[]数组,即将文件先放入StringBuffer中,再用getBytes()转为byte[],此种方式将数据存入数据库没有问题,但文件会变大,我原本存储14M的文件,入库后变成了24M,这肯定是有问题的。原因感兴趣的,可以研究一下,这种方式获取byte[]数组,果断放弃。使用的方法是ByteArrayInputStream,具体看上面代码。
FileInputStream fis = new FileInputStream(file);
byte[] buf = new byte[1024];
StringBuffer sb = new StringBuffer();
while ((fis.read(buf)) != -1) {
sb.append(new String(buf));
buf = new byte[1024];// 重新生成,避免和上次读取的数据重复
}
byte[] fileContent = sb.toString().getBytes()
(3)、LongBlob 和LongText ?
解决上面的两个问题后,原本以为问题就迎刃而解了,但并不是。我一开始设置file_content的Mysql字段是longtext,就报了下面这个错,这问题困惑了我2个小时,因为各种资料都是说字符编码有问题,用的不是utf8,我检查了一遍又一遍啊,最后怀疑是字段类型学的问题,于是使用了longblob,问题解决。这两个类型的具体区别如下:
ERROR 1366 (HY000): Incorrect string value: '\xE8\x8B\xB1\xE5\xAF\xB8...'
(4)、LongBlob 和LongText 主要区别
TEXT与BLOB的主要差别就是BLOB保存二进制数据,TEXT保存字符数据。
BLOB有4种类型:TINYBLOB、BLOB、MEDIUMBLOB和LONGBLOB。它们只是可容纳值的最大长度不同。
TEXT也有4种类型:TINYTEXT、TEXT、MEDIUMTEXT和LONGTEXT。这些类型同BLOB类型一样,有相同的最大长度和存储需求。
MySQL的四种 BLOB 类型(同TEXT): (单位:字节)
TinyBlob : 最大 255
Blob : 最大 65K
MediumBlob : 最大 16M
LongBlob : 最大 4G
补充!!!! 上面的方法经过测试小文件入库,没有大问题,但是当文件稍大以后,就会报OutOfMemoryError: Java heap space。
问题描述,如这篇文章所示: https://www.v2ex.com/t/562447
其中的原因是,将文件以byte[]数组的方式存入数据库,byte[]数组里要存整个文件,仔细查看下面的代码,可以看出,需要2倍的文件大小,一个是存放文件全部字节的byte[]数组,一个是要存放文件流,这样会非常占用内存。如果一个文件200M,内存将消耗极大,我们也可以通过增加JVM的堆内存大小来解决,但此方法并没有从根本上解决问题,还是不妥。
fis = new FileInputStream(file);
byte[] buffer = null;
// 此处用字节输出流
bos = new ByteArrayOutputStream();
byte[] temp = new byte[1024];
int n;
while ((n = fis.read(temp)) != -1) {
bos.write(temp, 0, n);
}
// 将字节输出流转化为byte[],保存到数据库中
buffer = bos.toByteArray();
分析了上述的问题,下面提出改进的方法,我们直接将文件流存入mysql数据库中。
public class FileContentServiceImpl implements FileContentService, ApplicationContextAware {
private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(FileContentServiceImpl.class);
private ApplicationContext applicationContext;
private DruidDataSource dataSource;
@Autowired
FileContentMapper contentMapper;
@Override
public boolean readResourceFromDB(String fileTid) {
FileOutputStream fos = null;
DruidPooledConnection connection = null;
try{
connection = dataSource.getConnection();
String sql = "select file_tid,file_path,file_path_md,file_content from t_file_content where file_tid=?";
PreparedStatement pst = connection.prepareStatement(sql);
pst.setString(1,fileTid);
ResultSet result = pst.executeQuery();
if(result != null && result.next()){
String filePath = result.getString(2);
File file = new File(filePath);
// 创建资源路径
if (!file.getParentFile().exists()) {
boolean succ = file.getParentFile().mkdirs();
if (!succ) {
throw new Exception("mkdir failed: " + file.getParentFile().getAbsolutePath());
}
}
InputStream in = result.getBinaryStream(4);
fos = new FileOutputStream(file);
int len = 0;
byte[] buf = new byte[1024];
while ((len = in.read(buf)) != -1) {
fos.write(buf, 0, len);
}
fos.flush();
in.close();
connection.close();
return true;
}
return false;
} catch (Exception e) {
LOG.error("readResourceFromDB 异常:", e);
return false;
} finally {
LOG.debug("readResourceFromDB() enter finally");
if (null != fos) {
try {
fos.close();
} catch (IOException e) {
}
}
}
}
@Override
public void saveResourceToDB(FileContent fileContent) {
DruidPooledConnection connection = null;
try {
String filePath = fileContent.getFilePath();
fileContent.setFilePathMd(Md5Tool.getMd5(filePath));
File file = new File(filePath);
if(!file.exists()) {
return;
}
String sql = "insert into t_file_content (file_tid, file_path, file_path_md, file_content) values (?,?,?,?)";
connection = dataSource.getConnection();
PreparedStatement pst = connection.prepareStatement(sql);
pst.setString(1, fileContent.getFileTid());
pst.setString(2, fileContent.getFilePath());
pst.setString(3, fileContent.getFilePathMd());
InputStream in = new FileInputStream(file);
pst.setBinaryStream(4,in);
pst.execute();
pst.close();
connection.close();
} catch (Exception e) {
LOG.error("saveResourceToDB 异常:", e);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
try {
this.applicationContext = applicationContext;
LOG.info("get applicationContext is : " + applicationContext);
dataSource = (DruidDataSource) applicationContext.getBean("dataSource");
LOG.info("get dataSource is : " + dataSource);
}catch (Exception e){
LOG.info("get applicationContext exception!");
}
}
}
说明: 此处用的是Mybatis和DruidDataSource数据源,我们需要从Spring容器中拿到数据源,所以实现了ApplicationContextAware接口,该接口可以让我们拿到Spring容器;