导入图片引发出的对图片、视频、文档等上传的思考

导读

我们在开发的过程中,经常会遇到导入和导出:

  1. 从哪里导入到哪里?我们在客户端选择上传Excel文件,同时调用服务端的某个接口。服务端通过HttpServletRequest获取Excel的数据流,通过poi的相关操作获取单元格的数值,并填充到相应的javabean的实例化对象中。再调用事务的保存方法,利用hibernate框架或mybatis框架,将对象数据保存到数据库中。
  2. 从哪里导出到哪里?从服务器上的数据库的数据导出到本地,并以Excel文件的方式存储。我们在客户端选择导出Excel文件,同时调用服务端的某个接口。服务端通过HttpServletResponse响应客户端的请求,同时,调用事务层的查询方法,拿到待导出的数据源,过滤我们想要的数据。调用poi的相关操作,将数据填充到Excel表中。

导入

这里以导入材料为主,材料中存有图片,如图所示:

材料图片

你会发现,Excel表中存储的是图片在服务器上的路径,为什么存储的是图片在服务器的路径,而不是图片的字节码数据?

我们都知道任何文件都可以按照字节码的方式存储,比如视频文件、音乐文件、图片文件、GIF文件、文本框文件等。但是字节码的存储和读取都占用内存,如果在大批量的导入和导出的情况下,势必会占用JVM内存,造成资源阻塞。

因而,我们存储的是图片的路径,这还不是随随便便的路径,而是其所在服务器的路径。为什么选择路径。从上图中的图片路径来看,路径的字符比较短。占用的内存比较少,存储和读取相对来说快。因而,我们读入的图片的路径。比如上图中ENGINE_PLATFORM1TENANTTHUMBNAILTENANT-LOGO_1_1534415695498_1.jpg的图片:

示例中的图片

其所对应的服务器的图片地址是http://cw.rosunn.com/upload/i...。因而,我们只要在数据库中存储/ENGINE_PLATFORM/1/TENANT/THUMBNAIL/TENANT-LOGO_1_1534415695498_1.jpg这部分路径就可以的。我们的域名前缀http://cw.rosunn.com/是固定的,其所对应的图片的文件夹upload是固定的,该文件夹下有很多的图片文件夹。每个图片的文件夹都是不一样的。如图所示:

clipboard.png

因为我们是Windows服务器,所以服务器是Windows界面化操作。其实,一台电脑就是一台服务器,要不然,怎么说本地服务器呢?在该文件夹下,有三个子文件夹。

  1. attach文件夹,存储与附件相关的文件夹。
  2. image文件夹,存储图片的文件夹
  3. uEditor文件夹,前端会使用uEditor框架,这是多文本编辑器,可以上传图片、视频等。

以后,可能会有视频文件夹,如果做教学软件的话。不管是存储图片文件和附件文件,还是视频文件,我们在数据库中都只存储该文件路径。当我们从数据库中读出图片到前台页面,我门只要拿到其存储的路径,并在前端做如下配置即可:

http://域名/upload/图片在数据...

明白这一点,我们就好往下进行,当我们点击前端代码的导入按钮时,如图所示:

clipboard.png

其首先会进入到拦截器,然后再进入到服务器的三层架构中。


服务器的三层架构

我们常说服务器有三层架构,即dao层,service层,controller层。实际上这是个通俗的概念,然而,在真正的开发过程中,并非只有三层架构,其中还会有拦截器的概念。如果你用servlet开发,会涉及到过滤器。拦截器和过滤器的功能是一样的,只不过用法是不一样的。它俩到底有什么区别,我想网上的博客非常多。这里就不在细说了。也许,你可以参考这篇博客:拦截器(Interceptor)和过滤器(Filter)的执行顺序和区别

一般项目启动后,首先进入的不是controller层,而是拦截器,controller层只是针对接口而言的。

拦截器,听名知其意,主要做数据的过滤和拦截。对于数据库中不时常改变的数据,比如系统变量和数据字典等,我们可以放到拦截器的缓存中,当我们加载数据字典时,不必再从数据库中读取,而是读取缓存的数据字典。这样,减少了与数据库的连接,从而提升了效率。

曾经在实习时,有个老大教我,说影响服务端的效率一般是db操作、网络调用操作、线程、JVM优化等。至于,我们是用++i,还是i++,哪个效率高一点。当然,是++i效率高一点。i++内部会有一个临时变量,其存储的i改变前值,然后再执行i = i + 1,返回的也是临时变量。++i直接执行的是i = i + 1,并返回改变后的值。但是,不会考虑到这个问题,因为它的影响微乎其微。

同时,我们每打开一个页面,都要经过拦截器,有些页面需要登录才能看,有些页面可以不用登录。这就是拦截器的作用。

我们这个项目使用的是Apache Shiro框架。其是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理的拦截器框架。

除了,我们登录后台需要身份验证,需要shiro的拦截。或者,我们调用第三方支付接口,其要回调我们的接口。但是,我们对每个接口,都要进行拦截,防止其恶意攻击。此时,我们需要忽略第三方回调的我们的接口,也就是说,这个接口不在我们的拦截范围之内,如下代码:

备注,因为涉及到隐私,部分代码省略,或以 ** 代替,望请见谅。

    <!-- 基于url+角色的身份验证过滤器 -->
    <bean id="urlAuthFilter" class="com.**.UrlAuthFilter">
        <property name="ignoreCheckUriList">
            <list>
                。。。
                <value>/manager/**/lianPayFn</value>
                <!--个人开户回调接口-->
                <value>/manager/**/openAccountFn</value>
                <!--实名认证回调接口-->
                <value>/manager/**/walletFn</value>
                <!--提现回调接口-->
                <value>/manager/**/withdrawFn</value>
                <!--充值回调接口-->
                <value>/manager/**/rechargeFn</value>
                。。。
            </list>
        </property>
    </bean>

个人开户回调接口对应的controller层的接口为:

  /**
 * Created By zby on 11:14 2019/3/11
 * 钱包管理:
 * 0 支付密码修改
 * 1 绑定手机号码
 * 2 基本信息修改
 * 3 绑定银行卡修改
 * 4 收支明细查询
 * 5 实名认证
 */
@RequestMapping(value = "/wallet", method = RequestMethod.GET)
public Result wallet(String flagPara) {
    CommonUtil.requiredCheck(flagPara);
    JSONObject body = new JSONObject();
    body.put("version", "1.2");
    body.put("oid_partner","1212121212");
    body.put("timestamp", DateUtil.ISO_DATETIME_FORMAT_NONE.format(new Date()));
    body.put("sign_type", "RSA");
    body.put("userreq_ip", "127.0.0.1");
    body.put("user_id", "test001");
    body.put("flag_para", flagPara);
    body.put("url_return", "http://**/returnPage.html");
//        仅支持企业的字段:name_unit,notify_url
    body.put("name_unit", "ceshigongsi");
    body.put("notify_url", "http://**/walletFn");
    body.put("sign", SignUtil.genRSASign(body));
    return ResultUtil.buildSuccess(body);
}

/**
 * Created By zby on 11:14 2019/3/11
 * 实名认证。绑定银行卡等
 */
@RequestMapping(value = "/walletFn", method = RequestMethod.POST)
public Result walletFn(HttpServletRequest request) {
    String str = CommonUtil.parseInputStream2String(request);
    JSONObject json = JSONObject.parseObject(str);
    logger.info(json.getString("user_id") + "钱包管理回调信息:" + json);
    return ResultUtil.buildSuccess(json);
}

wallet签名方法中调用连连的修改银行卡的接口,并向其传递我们的回调接口。如果修改成功,其会回调我们的/walletFn接口。这个接口,我们就不需要拦截,而是任其调用的。

当我们的前端请求接口通过了拦截,然后其会进入我们的controller层,查找到我们的导入材料的接口。使用该方法List<JSONObject> jsonObjectList = PoiUtil.importSimpleExcel(request, 1, "P");,拿到Excel表中每行数据,每行数据都是一个JSONObject对象。因而,返回的是JSONObject集合。这个,会在下面讲到。

 /**
 * Created By zby on 17:35 2019/2/20
 * 导入
 */
@RequestMapping(value = "/import", method = RequestMethod.POST)
public Result importMaterials(HttpServletRequest request) {
    JSONObject body = new JSONObject();
    int totalNum = 0, successNum = 0;
    synchronized (this) {
        try {
            List<JSONObject> jsonObjectList = PoiUtil.importSimpleExcel(request, 1, "P");
            if (null != jsonObjectList || jsonObjectList.size() > 0) {
                totalNum = jsonObjectList.size();
                for (JSONObject json : jsonObjectList) {
                    Long materialId = CommonUtil.getExcelLongVal(json.getBigDecimal("A"));
                    Material dbMaterial = null;
                    //【1】当项目存在时,更新项目及附属表属性,否则,就保存新对象
                    if (isNotNull(materialId) && materialId > 0) {
                        dbMaterial = materialService.get(materialId).getResultData();
                        if (null == dbMaterial) {
                            dbMaterial = new Material();
                            dbMaterial.setId(materialId);
                        }
                    }
                    if (null == materialId || materialId <= 0) {
                        dbMaterial = new Material();
                    }
                    //所属品类的id
                    Long categoryId = CommonUtil.getExcelLongVal(json.getBigDecimal("B"));
                    if (isNotNull(categoryId) && categoryId > 0) {
                        dbMaterial.setMaterialCategory(materialCategoryService.get(categoryId).getResultData());
                    }
                    //供应商的id
                    Long supplierId = CommonUtil.getExcelLongVal(json.getBigDecimal("C"));
                    if (isNotNull(supplierId) && supplierId > 0) {
                        dbMaterial.setSupplier(supplierService.get(supplierId).getResultData());
                    }
                    dbMaterial.setMaterialName(json.getString("D"));
                    dbMaterial.setVersion(json.getString("E"));
                    dbMaterial.setBrand(json.getString("F"));
                    dbMaterial.setChroma(json.getString("G"));
                    dbMaterial.setSpecifications(json.getString("H"));
                    //单位
                    String unitValue = json.getString("I");
                    if (isNotNull(unitValue)) {
                        List<DataDict> units = dataDictService.getDataDictList("unit").getResultData();
                        if (null != units && units.size() > 0) {
                            for (DataDict unit : units) {
                                if (unit.getValue().equals(unitValue)) {
                                    dbMaterial.setUnit(unit);
                                    break;
                                }
                            }
                        }
                    }
                    dbMaterial.setRetailPrice(json.getBigDecimal("J"));
                    dbMaterial.setCostPrice(json.getBigDecimal("K"));
                    dbMaterial.setStock(json.getBigDecimal("L"));
                    //状态
                    String state = json.getString("M");
                    if (StringUtils.isNotBlank(state)) {
                        for (MaterialStateEnum stateEnum : MaterialStateEnum.class.getEnumConstants()) {
                            if (stateEnum.getTitle().equals(state)) {
                                dbMaterial.setStatus(stateEnum);
                                break;
                            }
                        }
                    }
                    //系列
                    String tags = json.getString("N");
                    if (isNotNull(tags)) {
                        String[] tagArr = StringUtils.split(tags, ",");
                        List<DataDict> tagDicts = new ArrayList<>();
                        for (String tag : tagArr) {
                            tagDicts.add(dataDictService.valueToDictObject("material_tag", tag));
                        }
                        dbMaterial.setTagList(tagDicts);
                    }
                    //图片,输入格式为:/image/system_engine/1/TENANT/ORG/TENANT-LOGO_1_1509329787954_1.jpg
                    String imgNames = json.getString("O");
                    if (isNotNull(imgNames)) {
                        String imgUrl = CommonUtil.getTomcatRootPath() + "/upload/";
                        String[] imgUrlSuffixes = imgNames.split(",");
                        List<Picture> pictureList = new ArrayList<>();
                        for (String imgUrlSuffix : imgUrlSuffixes) {
                            File file = new File(imgUrl + imgUrlSuffix);
                            if (file.exists()) {
                                Picture picture = new Picture();
                                picture.setRemoteRelativeUrl("/upload/" + imgNames);
                                picture.setName(imgNames.substring(imgNames.lastIndexOf("/"), imgNames.length()));
                                pictureList.add(pictureService.save(picture).getResultData());
                            }
                        }
                        dbMaterial.setPictureList(pictureList);
                    }
                    dbMaterial.setNote(json.getString("P"));
                    materialService.saveUpdateMaterialAccount(dbMaterial);
                    successNum++;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    body.put("totalNum", totalNum);
    body.put("successNum", successNum);
    body.put("errNum", totalNum - successNum);
    return ResultUtil.buildSuccess(body);
}

我们从中调用了保存图片的这段代码:

  //图片,excel的图片格式为:/image/system_engine/1/TENANT/ORG/TENANT-LOGO_1_1509329787954_1.jpg
String imgNames = json.getString("O");
if (isNotNull(imgNames)) {
    String imgUrl = CommonUtil.getTomcatRootPath() + "/upload/";
    String[] imgUrlSuffixes = imgNames.split(",");
    List<Picture> pictureList = new ArrayList<>();
    for (String imgUrlSuffix : imgUrlSuffixes) {
        File file = new File(imgUrl + imgUrlSuffix);
        if (file.exists()) {
            Picture picture = new Picture();
            picture.setRemoteRelativeUrl("/upload/" + imgNames);
            picture.setName(imgNames.substring(imgNames.lastIndexOf("/"), imgNames.length()));
            pictureList.add(pictureService.save(picture).getResultData());
        }
    }
    dbMaterial.setPictureList(pictureList);

我们首先找到类路径下的字节码文件的的根路径,如代码所示:

 /**
 * Created By zby on 15:24 2019/3/21
 * 通过spring自带的方法找到字节码classes包的路径,
 * 再往上找三级目录,到Tomcat的webApps下目录,
 * 当前目录加上 /upload才是图片路径
 */
public static String getTomcatRootPath() {
    File file = null;
    String basePath = null;
    try {
        file = getFile(CLASSPATH_URL_PREFIX);
        basePath = file.toPath().getParent().getParent().getParent().toString();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }
    return basePath;
}

其返回一个文件对象,通过三次调用getParent获取Tomcat的根路径,在根路径下加上该路径名“upload”,封装成这样的存储方式http://域名/upload//image/sys...。如果其在服务器中存在,我们就创建图片的对象,图片的属性remoteRelativeUrl,存储该路径的后半部分,也就是相对路径,并保存到数据库中,返回一个图片对象。并把图片对象放到集合中,然后保存到材料对象中。材料对象再保存到数据中,这就完成一次导入。但是jsonObjectList可能有多个对象,再遍历一次,直到遍历所有的对象。

如下是材料的java类:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "zq_material")
public class Material extends BaseObj {

/**
 * 材料名称
 */
@Column(name = "material_name")
private String materialName;

/**
 * 单位
 */
@ManyToOne
@JoinColumn(name = "unit_code")
private DataDict unit;

/**
 * 零售价
 */
@Column(name = "retail_price", precision = 12, scale = 2)
private BigDecimal retailPrice;

/**
 * 状态
 */
@Enumerated(EnumType.STRING)
private MaterialStateEnum status;

/**
 * 浏览量
 */
@Column(name = "page_view")
private Long pageView;

/**
 * 图片
 */
@ManyToMany(fetch = FetchType.EAGER)
@Fetch(FetchMode.SELECT)
@JoinTable(
        name = "zq_material_pictures",
        joinColumns = {@JoinColumn(name = "zq_material_id")},
        inverseJoinColumns = @JoinColumn(name = "core_picture_id")
)
@JSONField(serialize = false)
private List<Picture> pictureList = new ArrayList<>();

/**
 * 材料标签
 */
@ManyToMany(fetch = FetchType.EAGER)
@Fetch(FetchMode.SELECT)
@JoinTable(
        name = "zq_material_tag",
        joinColumns = {@JoinColumn(name = "zq_material_id")},
        inverseJoinColumns = @JoinColumn(name = "core_data_dict_code")
)
@JSONField(serialize = false)
private List<DataDict> tagList = new ArrayList<>();

/**
 * 备注
 */
@Column(name = "note", columnDefinition = "longtext")
private String note;

}

为何向上遍历三次

我们看一下字节码文件在服务器的位置,如图所示:

图片所在的服务器的位置

由图可知。一次次的向上遍历,只为找到根路径,也就是http://域名。这是Tomcat的配置。然后再配置upload文件夹,即http://域名/upload


导入的执行效率

以上导入可以分为两种方式。一种是如果导入的数据中,但凡有一条数据不成功,所有的数据都无法导入。这就涉及到了事务一致性的问题。因而,我们需要放在事务层,也就是service层。为什么spring直到service层是事务层,这和我们的框架配置有关,把service层定义为事务层。如果某一条数据导入失败,并不影响其他数据的导入,我们可以放在controller层。

但是,如果处理的不当,便影响导入的执行效率。为什么这么说?比如,我们现在导入的是材料,材料有单位。单位放置在数据字典中,假设单位有16条数据,如图所示:

材料单位图

假如jsonObjectList的集合有1000条数据。我们每次遍历jsonObjectList集合,都要创建一次查询,也就是与数据库创建一次连接,保存之后再关掉连接,势必会减低导入效率:

 //单位
String unitValue = json.getString("I");
if (isNotNull(unitValue)) {
    List<DataDict> units = dataDictService.getDataDictList("unit").getResultData();
    if (null != units && units.size() > 0) {
        for (DataDict unit : units) {
            if (unit.getValue().equals(unitValue)) {
                dbMaterial.setUnit(unit);
                break;
            }
        }
    }
}

我们从数据查找出当前单位的行数据,封装成我们想要的数据字典的对象。此时与数据库建立连接和释放数据库的连接,最多需要16000次,这势必会会增加服务器的资源,降低导入的执行效率。最少也需要1000次。

同时,系列也是来源于数据字典,然而,系列是以逗号分割的字符串,也就是说,我们需要将字符串分割成数组,再遍历这个数组,获取数据字典的对象,此时,最少语句数据库的连接数为16000,最多就不大清楚了。因而,严重降低导入的效率。

我们为什么不采用最少的呢?因而,我们在遍历jsonObjectList 之前,就从数据库中的加载出所有的单位的数据字典的集合,同时,也加载出系列的集合。放置在map的键值对当中,根据key值来取value值,如代码所示:

根据数据字典的父code值加载出所有的子code对象。

 /**
 * Created By zby on 14:12 2019/3/24
 * 将dict封装成map
 */
private Map<String, DataDict> dict2Map(String parentCode) {
    Map<String, DataDict> dictMap = null;
    if (StringUtils.isNotBlank(parentCode)) {
        List<DataDict> units = dataDictService.getDataDictList(parentCode).getResultData();
        dictMap = new HashMap<>();
        if (!CollectionUtils.isEmpty(dictMap)) {
            for (DataDict dict : units) {
                dictMap.put(dict.getValue(), dict);
            }
        }
    }
    return dictMap;
}

对于上面的一串代码,我们省略其他的代码,只加载和数据字典相关的代码,于是乎,得到:

/**
 * Created By zby on 17:35 2019/2/20
 * 导入
 */
@RequestMapping(value = "/import", method = RequestMethod.POST)
public Result importMaterials(HttpServletRequest request) {
    JSONObject body = new JSONObject();
    int totalNum = 0, successNum = 0;
//        单位
    Map<String, DataDict> unitDict = dict2Map("unit");
//        系列
    Map<String, DataDict> tagDict = dict2Map("material_tag");

    synchronized (this) {
        try {
            List<JSONObject> jsonObjectList = PoiUtil.importSimpleExcel(request, 1, "P");
            if (null != jsonObjectList || jsonObjectList.size() > 0) {
                totalNum = jsonObjectList.size();
                for (JSONObject json : jsonObjectList) {
                     //单位
                    String unitValue = json.getString("I");
                    if (StringUtils.isNotBlank(unitValue)) {
                        dbMaterial.setUnit(unitDict.get(unitValue));
                    }
                    //系列
                    String tags = json.getString("N");
                    if (isNotNull(tags)) {
                        String[] tagArr = StringUtils.split(tags, ",");
                        List<DataDict> tagDicts = new ArrayList<>();
                        for (String tag : tagArr) {
                            if (StringUtils.isNotBlank(tag)) {
                                tagDicts.add(tagDict.get(tag));
                            }
                        }
                        dbMaterial.setTagList(tagDicts);         
                    }
                    materialService.saveUpdateMaterialAccount(dbMaterial);
                    successNum++;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    body.put("totalNum", totalNum);
    body.put("successNum", successNum);
    body.put("errNum", totalNum - successNum);
    return ResultUtil.buildSuccess(body);
}

这就是数据库导入优化,但是导入图片,和我们上传图片、视频、文档有关系吗?


图片、视频、附件上传

我们在做java开发时,势必会涉及到文件操作。我们一般会上传图片、文件、视频等,但它们以什么样的格式存储。正如上面提到的,我们上传图片、视频、附件等,会在服务器上创建一个文件夹,他们存储在该文件夹中。我们只要获取文件夹的相对路径即可,就能将其加载出来,这样比较节省数据库的资源。如图所示:

删除图片、视频、附件等

这一般都是异步上传,先将文件的路径以对象的保存到数据库中,再返回文件被保存后的带有主键id的对象。我们拿到持久态的文件对象后,在前端页面展示出来。因而,我们在保存材料时,前端只要向后端传输文件的id,或者是文件.id即可,比如 logo.id。spring会自动创建该文件对象,并将id到注入文件对象中。


总结

我们在开发过程中,要知其然,知其所以然。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章