一、引言

先描述一下需求:

大前提在前面的文章将大文件存到Oracle数据库中已经描述过,不过又要新增一个微服务,数据库使用的Mysql,在编码的过程中,遇到几个坑,在此记录一下。

二、具体代码

1、几点说明

平台数据库:Mysql

数据库字段:file_content为存储文件的字段

mysql的data文件夹不全 mysql存储大文件_存储

2、代码实现

数据库的xml文件,注意file_content的类型为BINARY

<resultMap id="BaseResultMap" type="com.scorpio.bean.FileContent">
    <id column="file_tid" jdbcType="VARCHAR" property="fileTid" />
    <result column="file_path" jdbcType="VARCHAR" property="filePath" />
    <result column="file_path_md" jdbcType="VARCHAR" property="filePathMd" />
    <result column="file_content" jdbcType="BINARY" property="fileContent" />
  </resultMap>

  <select id="selectByFileId" parameterType="java.lang.String" 
  							resultMap="BaseResultMap">
    select
        file_tid, file_path, file_path_md, file_content
    from t_file_content
    where file_tid =  #{fileTid,jdbcType=VARCHAR}
  </select>

  <insert id="insertFileContent" parameterType="com.scorpio.bean.FileContent">
    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})
  </insert>

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容器;