導讀
我們在開發的過程中,經常會遇到導入和導出:
- 從哪裏導入到哪裏?我們在客戶端選擇上傳Excel文件,同時調用服務端的某個接口。服務端通過HttpServletRequest獲取Excel的數據流,通過poi的相關操作獲取單元格的數值,並填充到相應的javabean的實例化對象中。再調用事務的保存方法,利用hibernate框架或mybatis框架,將對象數據保存到數據庫中。
- 從哪裏導出到哪裏?從服務器上的數據庫的數據導出到本地,並以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是固定的,該文件夾下有很多的圖片文件夾。每個圖片的文件夾都是不一樣的。如圖所示:
因爲我們是Windows服務器,所以服務器是Windows界面化操作。其實,一臺電腦就是一臺服務器,要不然,怎麼說本地服務器呢?在該文件夾下,有三個子文件夾。
- attach文件夾,存儲與附件相關的文件夾。
- image文件夾,存儲圖片的文件夾
- uEditor文件夾,前端會使用uEditor框架,這是多文本編輯器,可以上傳圖片、視頻等。
以後,可能會有視頻文件夾,如果做教學軟件的話。不管是存儲圖片文件和附件文件,還是視頻文件,我們在數據庫中都只存儲該文件路徑。當我們從數據庫中讀出圖片到前臺頁面,我門只要拿到其存儲的路徑,並在前端做如下配置即可:
明白這一點,我們就好往下進行,當我們點擊前端代碼的導入按鈕時,如圖所示:
其首先會進入到攔截器,然後再進入到服務器的三層架構中。
服務器的三層架構
我們常說服務器有三層架構,即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到注入文件對象中。
總結
我們在開發過程中,要知其然,知其所以然。