導入圖片引發出的對圖片、視頻、文檔等上傳的思考

導讀

我們在開發的過程中,經常會遇到導入和導出:

  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到注入文件對象中。


總結

我們在開發過程中,要知其然,知其所以然。

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