實(shí)戰(zhàn) - 業(yè)務(wù)實(shí)現(xiàn) 2
這一小節(jié),是 Java 基礎(chǔ)教程的最后一節(jié),很感謝大家能夠堅(jiān)持看到這里。本小節(jié)我將帶領(lǐng)大家優(yōu)化用戶鑒權(quán)服務(wù),并完成商品模塊的實(shí)現(xiàn)。為了檢驗(yàn)大家的學(xué)習(xí)成果,分類模塊的實(shí)現(xiàn)將交給大家自行來(lái)完成。
1. 用戶密碼加密
上一小節(jié)的最后,我們提到用戶鑒權(quán)服務(wù)是需要優(yōu)化的。大家可以看到我們數(shù)據(jù)庫(kù)存儲(chǔ)的是明文密碼,這是非常不推薦的,在實(shí)際的項(xiàng)目中,明文存儲(chǔ)用戶的密碼是非常不安全的,也是不負(fù)責(zé)任的行為。我們?cè)谠O(shè)計(jì) imooc_user
表時(shí),給password
設(shè)置的類型為固定長(zhǎng)度類型char(32)
,32 位正好是MD5
算法加密后的長(zhǎng)度。
本系統(tǒng)使用 MD5
算法對(duì)密碼進(jìn)行加密,下面在 util
包下新建一個(gè) MD5Util
類并寫入如下內(nèi)容(可直接復(fù)制粘貼代碼):
package com.colorful.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MD5Util {
public static String md5(String source) {
StringBuilder stringBuilder = new StringBuilder();
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
// 將一個(gè)byte數(shù)組進(jìn)行加密操作,返回的是一個(gè)加密的byte數(shù)組,二進(jìn)制的哈西計(jì)算,md5加密的第一步
byte[] digest = messageDigest.digest(source.getBytes());
for (byte b : digest) {
int result = b & 0xff;
// 將得到的int類型的值轉(zhuǎn)化為16進(jìn)制的值
String hexString = Integer.toHexString(result);
if (hexString.length() < 2) {
//系統(tǒng)會(huì)自動(dòng)把0省略,所以添加0
stringBuilder.append("0");
}
stringBuilder.append(hexString);
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return stringBuilder.toString();
}
public static void main(String[] args) {
String password = "123456";
String s = MD5Util.md5(password);
System.out.println(s);
}
}
在主方法中,我們編寫了調(diào)用md5()
加密方法的邏輯,運(yùn)行代碼,屏幕上得到123456
加密后的字符串:
e10adc3949ba59abbe56e057f20f883e
下面我們將imooc_user
表中存儲(chǔ)的明文密碼,更新為上面的結(jié)果,大家可以使用SQL
語(yǔ)句來(lái)進(jìn)行更新:
UPDATE `imooc_user` SET `password` = 'e10adc3949ba59abbe56e057f20f883e' WHERE `id` = 1;
這里我直接通過(guò) MySQL
客戶端進(jìn)行更新,如下是操作過(guò)程的截圖:
數(shù)據(jù)庫(kù)存儲(chǔ)的密碼更新后,我們就無(wú)法直接通過(guò)原本的驗(yàn)證邏輯來(lái)驗(yàn)證密碼了,需要修改用戶鑒權(quán)邏輯 —— 將用戶輸入的密碼加密后,再與數(shù)據(jù)庫(kù)的密碼進(jìn)行對(duì)比。那么這段邏輯要寫在service
層還是dao
層呢?答案肯定是service
層,此時(shí)service
層用于處理業(yè)務(wù)的特性得到了體現(xiàn),修改UserService
下的login
方法,將參數(shù)password
加密:
public User login(String username, String password) {
String md5Password = MD5Util.md5(password);
return userDAO.selectByUserNameAndPassword(username, md5Password);
}
再次啟動(dòng)應(yīng)用程序,驗(yàn)證改寫的邏輯是否正確:

至此,我們就完成了對(duì)用戶鑒權(quán)服務(wù)的優(yōu)化。
2. 控制臺(tái)(儀表盤)
用戶登錄成功后,應(yīng)該顯示控制臺(tái)面板,我們下面稱之為儀表盤,它主要包含 3 個(gè)選項(xiàng),分別是管理商品、管理分類以及退出登錄。下面我們編寫一個(gè)dashboard()
方法,該方法用來(lái)打印儀表盤的相關(guān)操作提示,以及根據(jù)用戶的輸入來(lái)執(zhí)行相應(yīng)的操作。如下是部分代碼:
/**
* 主流程方法
*/
public static void run() {
// ... 已省略前面的鑒權(quán)代碼
// 登錄成功后,跳轉(zhuǎn)到儀表盤頁(yè)面
dashboard();
}
/**
* 儀表盤操作
*/
private static void dashboard() {
Scanner scanner = new Scanner(System.in);
int code1 = 0, code2 = 0;
while (true) {
printDashboardTips();
code1 = scanner.nextInt();
if (code1 == 0) {
System.out.println("您已退出登錄");
break;
}
switch (code1) {
case 1:
System.out.println("正在查詢商品列表...");
// TODO 實(shí)現(xiàn)商品模塊
break;
case 2:
System.out.println("正在查詢分類列表...");
// TODO 實(shí)現(xiàn)分類模塊
break;
default:
System.out.println("不存在您輸入的選項(xiàng),請(qǐng)重新輸入");
break;
}
}
}
/**
* 輸出儀表盤操作提示
*/
private static void printDashboardTips() {
System.out.println("請(qǐng)輸入對(duì)應(yīng)數(shù)字以進(jìn)行操作:");
System.out.println("(1. 管理商品 | 2. 管理分類 | 0. 退出登錄)");
}
我們把向控制臺(tái)輸出的操作提示,封裝成了一個(gè)方法printDashboardTips()
,這樣使代碼更簡(jiǎn)潔易讀。
在dashboard()
方法內(nèi)部,實(shí)例化了一個(gè)Scanner
類,初始化的code1
變量接收用戶的輸入,根據(jù)輸入的數(shù)值用來(lái)操作儀表盤,關(guān)于code2
變量,我們將在實(shí)現(xiàn)商品模塊代碼的時(shí)候使用。緊接著有一個(gè)while
循環(huán),其條件始終為true
,當(dāng)用戶輸入的code
登錄 0
的時(shí)候,就跳出循環(huán),也就是退出了應(yīng)用程序。
完成上面的代碼編寫后,我們啟動(dòng)應(yīng)用程序,來(lái)驗(yàn)證一下:

至此,我們已實(shí)現(xiàn)展示儀表盤以及退出登錄的代碼編寫。
3. 商品模塊實(shí)現(xiàn)
3.1 商品管理主流程
當(dāng)用戶輸入的code1
變量為數(shù)字 1
的時(shí)候,就要顯示商品管理相關(guān)的操作。我們?cè)俜庋b一個(gè)printGoodsListTips()
方法,用于打印商品管理模塊的相關(guān)操作提示。方法的代碼如下:
/**
* 輸出商品列表頁(yè)操作提示
*/
private static void printGoodsListTips() {
System.out.println("請(qǐng)輸入對(duì)應(yīng)數(shù)字以進(jìn)行操作:");
System.out.println("(1. 新增商品 | 2. 編輯商品 | 3. 查看商品詳情 | 4. 刪除商品 | 5. 搜索商品 | 6. 按分類查詢商品 | 0. 返回上一級(jí)菜單)");
}
向屏幕打印這些提示后,下面還是一個(gè)條件始終為true
的while
循環(huán),當(dāng)用戶輸入的code
登錄 0
的時(shí)候,就跳出當(dāng)前層循環(huán),也就是返回上一級(jí)儀表盤的菜單。
已知了商品管理模塊的所有操作,下面我們?cè)?code>switch(code1)的case 1
條件分支加入如下邏輯代碼(部分偽代碼):
case 1:
while (true) {
System.out.println("正在查詢商品列表...");
// TODO 查詢并顯示商品列表
printGoodsListTips();
code2 = scanner.nextInt();
if (code2 == 0) {
// 返回上一級(jí),即跳出本層循環(huán)
System.out.println("返回上一級(jí)");
break;
}
switch (code2) {
case 1:
System.out.println("新增商品");
break;
case 2:
System.out.println("編輯商品");
break;
case 3:
System.out.println("商品詳情");
break;
case 4:
System.out.println("刪除商品");
break;
case 5:
System.out.println("搜索商品");
break;
case 6:
System.out.println("按分類查詢");
break;
default:
System.out.println("不存在您輸入的選項(xiàng),請(qǐng)重新輸入");
}
}
break;
上面我們提到,code2
變量用于接收用戶對(duì)于管理商品操作的輸入,此處又是一個(gè)switch case
結(jié)構(gòu),每一個(gè)條件分支,都對(duì)應(yīng)到用戶輸入的數(shù)字,如果用戶輸入的數(shù)字找不到對(duì)應(yīng)的分支,那么就重復(fù)執(zhí)行循環(huán)體中的代碼。
接下來(lái)我們就要實(shí)現(xiàn)這些操作。
3.2 查詢商品列表
在dao
包下新建一個(gè)GoodsDAO
類,并寫入一下內(nèi)容:
package com.colorful.dao;
import com.colorful.model.Goods;
import com.colorful.util.JDBCUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
public class GoodsDAO {
private Connection connection = null;
private PreparedStatement preparedStatement = null;
private ResultSet resultSet = null;
boolean executeResult;
public List<Goods> selectGoodsList() {
List<Goods> goodsList = new ArrayList<>();
try {
// 獲得鏈接
connection = JDBCUtil.getConnection();
// 編寫 SQL 語(yǔ)句
String sql = "SELECT `id`, `name`, `price` FROM `imooc_goods` where `delete_time` is null";
// 預(yù)編譯 SQL
preparedStatement = connection.prepareStatement(sql);
resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
Goods goods = new Goods();
goods.setId(resultSet.getInt("id"));
goods.setName(resultSet.getString("name"));
goods.setPrice(resultSet.getDouble("price"));
goodsList.add(goods);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 釋放資源
JDBCUtil.release(resultSet, preparedStatement, connection);
}
return goodsList;
}
}
selectGoodsList()
方法就用于查詢商品列表(由于數(shù)據(jù)量不大,此處我沒(méi)有對(duì)列表數(shù)據(jù)進(jìn)行分頁(yè)查詢,大家也可以自行加入)。
在service
包下新建GoodsService
,并調(diào)用dao
層下封裝好的方法:
package com.colorful.service;
import com.colorful.dao.GoodsDAO;
import com.colorful.model.Goods;
import java.util.List;
public class GoodsService {
private final GoodsDAO goodsDAO = new GoodsDAO();
/**
* 獲取商品列表
* @return 商品列表
*/
public List<Goods> getGoodsList() {
return goodsDAO.selectGoodsList();
}
}
這樣,我們就完成了查詢商品列表的服務(wù)層代碼編寫。
3.3 刪除商品
新增商品、刪除商品、查看商品詳情等功能都是簡(jiǎn)單的SQL
語(yǔ)句,這里不再具體寫出實(shí)現(xiàn),大家可以參考源碼自行實(shí)現(xiàn)。但關(guān)于刪除商品,我要特殊說(shuō)明一下。對(duì)于實(shí)際的項(xiàng)目,往往不用對(duì)數(shù)據(jù)執(zhí)行DELETE
操作,對(duì)于數(shù)據(jù)的刪除往往是更新操作,這也是我們?cè)O(shè)置了一個(gè)公用字段delete_time
的意義,當(dāng)這個(gè)delete_time
字段不為null
的時(shí)候,才會(huì)被查詢出來(lái)。在GoodsDAO
類下,新增如下方法:
public boolean deleteGoodsById(Integer id) {
try {
// 獲得鏈接
connection = JDBCUtil.getConnection();
// 編寫 SQL 語(yǔ)句
String sql = "UPDATE `imooc_goods` set `delete_time` = ? WHERE id = ?";
// 預(yù)編譯 SQL
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
preparedStatement.setInt(2, id);
executeResult = preparedStatement.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 釋放資源
JDBCUtil.release(preparedStatement, connection);
}
return executeResult;
}
大家可以看到,我們的代碼實(shí)現(xiàn)沒(méi)有使用DELETE
語(yǔ)句,而是使用了UPDATE
語(yǔ)句,更新了指定id
記錄的delete_time
字段為系統(tǒng)當(dāng)前時(shí)間。
dao
層方法編寫完成后,就可以在service
層調(diào)用該方法了:
/**
* 刪除商品
* @param id 商品id
*/
public void removeGoodsById(Integer id) {
goodsDAO.deleteGoodsById(id);
}
3.4 搜索商品
除了刪除商品的實(shí)現(xiàn),搜索商品的實(shí)現(xiàn)我們也要特殊講解一下。上面我們提到,由于商品的數(shù)據(jù)量不大,在查詢商品列表時(shí),沒(méi)有使用LIMIT
關(guān)鍵字進(jìn)行分頁(yè)查詢。正是由于數(shù)據(jù)量不大的原因,對(duì)于搜索商品,我們沒(méi)有使用LIKE
關(guān)鍵字進(jìn)行模糊查詢,而是使用Stream API
直接對(duì)商品列表進(jìn)行過(guò)濾,希望通過(guò)這里的實(shí)現(xiàn)來(lái)協(xié)助讓大家理解Stream API
,直接在GoodsService
下添加如下方法:
/**
* 根據(jù)商品名稱搜索商品
* @param name 商品名稱
* @return 商品列表
*/
public List<Goods> searchGoodsByName(String name) {
List<Goods> goodsList = this.getGoodsList();
return goodsList.stream().filter(
goods -> goods.getName().contains(name)
).collect(Collectors.toList());
}
該方法先是調(diào)用了getGoodsList()
方法獲取了商品列表,然后使用Stream API
中的filter()
中間操作,對(duì)商品進(jìn)行過(guò)濾,filter()
接收一個(gè)斷言型接口,由于是一個(gè)函數(shù)式接口,我們可通過(guò)lambda
表達(dá)式來(lái)進(jìn)行表示。最后調(diào)用collect()
終止操作,將流轉(zhuǎn)化為列表。
服務(wù)層的接口完成后,大家就可以在對(duì)應(yīng)的case
分支編寫的具體的邏輯了,每個(gè)分支的邏輯大體相同,主要是接收用戶的輸入,以及服務(wù)層方法的調(diào)用。大家可參考github
倉(cāng)庫(kù)的源碼來(lái)補(bǔ)全自己的代碼。
4. 作業(yè) - 分類模塊實(shí)現(xiàn)
上面,我們已經(jīng)實(shí)現(xiàn)了較為復(fù)雜的商品模塊,對(duì)于分類模塊的實(shí)現(xiàn)也大同小異,甚至更加簡(jiǎn)單,剩下的功能 ——分類的增刪改查就交由同學(xué)們自行實(shí)現(xiàn)。希望大家能夠按照我們項(xiàng)目的架構(gòu),將合理的代碼寫到合適的位置,對(duì)每個(gè)功能點(diǎn)都要將細(xì)節(jié)考慮周全,這將有助于降低大家后續(xù)對(duì)框架學(xué)習(xí)的上手成本。
5. 小結(jié)
通過(guò)實(shí)戰(zhàn)階段的學(xué)習(xí),我們知道了數(shù)據(jù)表中的密碼字段,是不能夠明文存儲(chǔ)的,通常使用一些加密算法進(jìn)行加密,也復(fù)習(xí)了switch case
條件結(jié)構(gòu)的使用,對(duì)于商品模糊查詢,我們使用了 Java 8 中的 Stream API
。
中間還講解了項(xiàng)目的分層技術(shù)、MySQL 的增刪改查操作、JDBC API 的封裝與使用以及Scanner
類的使用等知識(shí),實(shí)際的項(xiàng)目基本不會(huì)使用Scanner
來(lái)與用戶進(jìn)行交互,都是通過(guò)優(yōu)美的前端界面與用戶進(jìn)行交互的,建議大家可以去看看Lin CMS的示例demo
,它是一個(gè)能夠達(dá)到企業(yè)級(jí)應(yīng)用標(biāo)準(zhǔn)的內(nèi)容管理系統(tǒng)開(kāi)發(fā)框架。
當(dāng)然,想要上手使用Lin CMS
,大家還有很長(zhǎng)的一段路要走,但是請(qǐng)記住,莫要浮空建高樓,Java 的基礎(chǔ)知識(shí)在任何時(shí)候都是不能忽視的,希望大家反復(fù)學(xué)習(xí)。Java 基礎(chǔ)的學(xué)習(xí)到此也就結(jié)束了,再次感謝大家能夠堅(jiān)持看完!