前面幾篇已經完成了資料庫的基本操作跟使用Thymeleaf 呈現頁面,接下來才真正要踏入Spring Boot 的世界,我們會圍繞在註冊、登入與會員中心三個基本功能,從最簡單的資料庫操作,逐步加上資料驗證、例外處理、登入日誌等功能。
記得相關的註釋在Day 09 - Spring Boot 常用註釋(下)都有提到,由於篇幅的關係,這邊就不闡述開發時的測試程式,主要講Controller 和Service 這兩層的內容,對測試有興趣的話可以去看Github。
首先,要在控制器層的類別上新增註釋@Controller
,宣告這是SpringMVC 的Controller 物件,才繼續往下設定請求的路徑,其實在上一篇Day 14 - Spring Boot & Thymeleaf已經建立了顯示註冊與登入頁面的Controller,就在裡面新增兩個POST 的方法,用來接收註冊與登入時的送出的表單。
@Controller
public class MemberAccountController {
@Autowired
private MemberAccountService memberAccountService;
@RequestMapping(value = {"/", "/login"}, method = RequestMethod.GET)
public String login(
@ModelAttribute MemberAccount memberAccount,
@ModelAttribute(value = "MESSAGE") String message) {
return "login";
}
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String doLogin(
@ModelAttribute MemberAccount memberAccount,
HttpSession session,
RedirectAttributes redirectAttributes) {
MemberAccountVO memberAccountVO = memberAccountService.login(memberAccount);
if(memberAccountVO == null) {
String message = memberAccountVO == null ? "帳號或密碼錯誤" : "";
redirectAttributes.addFlashAttribute("MESSAGE", message);
return "redirect:login";
}
session.setAttribute("member", memberAccountVO);
return "redirect:information";
}
@RequestMapping(value = "/register", method = RequestMethod.GET)
public String register(@ModelAttribute MemberAccountVO memberAccountVO) {
return "register";
}
@RequestMapping(value = "/register", method = RequestMethod.POST)
public String doRegister(
@ModelAttribute MemberAccountVO memberAccountVO,
RedirectAttributes redirectAttributes) {
Optional<String> optional = memberAccountService.register(memberAccountVO);
String message = optional.orElse("註冊成功");
redirectAttributes.addFlashAttribute("MESSAGE", message);
return "redirect:login";
}
}
業務邏輯層的地方跟持久層都一樣,先定義好介面,再撰寫實作該介面的具體類別,或許有些人會覺得這樣很麻煩,不能直接寫個類別嗎,這個原因其實並不複雜,推薦可以看一下Java 為什麼在Service層要使用Interface 這篇文章,作者有詳細解釋及實作講解為什麼要使用介面。
在Day 06 - MVC 與三層架構已經提過的,三層架構的業務邏輯層與持久層都是對應MVC 架構中的M的部分,在業務邏輯層的介面這邊,可以用註解分類方法的用途。
public interface MemberAccountService {
// 業務邏輯
public MemberAccountVO login(MemberAccount memberAccount);
public Optional<String> register(MemberAccountVO memberAccountVO);
// 資料庫操作
public MemberAccount findMemberAccountByUsername(String username);
}
在業務邏輯層的實作類別上,記得先新增註釋@Service
,宣告這是業務處理類別,再依照邏輯需求使用@Autowired
注入所需要的物件,而實際上這邊應該要在每個方法上面都寫上很多註解,標明這個方法的用途、引數以及回傳值,但這邊為了不讓文章太長就不寫了,就先記得在撰寫方法的邏輯前,要列出該方法的邏輯順序。
@Override
public MemberAccountVO login(MemberAccount memberAccount) {
// TODO Auto-generated method stub
// 檢查帳號是否存在
// 使用資料庫鹽值對輸入密碼進行加密
// 比對密碼是否相等
// 取得對應Member 資料
// 組合資料為MemberAccountVO
return null;
}
@Service
public class MemberAccountServiceImpl implements MemberAccountService {
@Autowired
private MemberAccountDao memberAccountDao;
@Autowired
private MemberService memberService;
private String getMd5Password(String password, String salt) {
// 對password + salt 進行三次加密
String str = password + salt;
for (int i = 0; i < 3; i++) {
str = DigestUtils.md5DigestAsHex(str.getBytes()).toUpperCase();
}
return str;
}
@Override
public MemberAccountVO login(MemberAccount memberAccount) {
// TODO Auto-generated method stub
// 檢查帳號是否存在
MemberAccount data = memberAccountDao.findMemberAccountByUsername(memberAccount.getUsername());
if(data == null) return null;
// 使用資料庫鹽值對輸入密碼進行加密
String md5Password = getMd5Password(memberAccount.getPassword(), data.getSalt());
// 比對密碼是否相等
if(!md5Password.equals(data.getPassword())) return null;
// 取得對應Member 資料
Member member = memberService.findMemberByMa_id(data.getId());
if(member == null) return null;
// 組合資料為MemberAccountVO
MemberAccountVO memberAccountVO = new MemberAccountVO();
memberAccountVO.setUsername(memberAccount.getUsername());
memberAccountVO.setName(member.getName());
return memberAccountVO;
}
@Override
public Optional<String> register(MemberAccountVO memberAccountVO) {
// TODO Auto-generated method stub
// 驗證欄位是否填寫及格式
if(!ValidFormat.isEmail(memberAccountVO.getUsername())) return Optional.of("帳號必須是Email 格式");
if(!ValidFormat.isPassword(memberAccountVO.getPassword())) return Optional.of("密碼必須為長度6~16位碼大小寫英文加數字");
if(!memberAccountVO.getPassword().equals(memberAccountVO.getCheckPassword())) return Optional.of("兩次輸入密碼不相符");
// 檢查帳號是否重複註冊
MemberAccount data = memberAccountDao.findMemberAccountByUsername(memberAccountVO.getUsername());
if(data != null) return Optional.of("該帳號已被使用");
// 產生鹽值
String salt = UUID.randomUUID().toString().toUpperCase().replaceAll("-", "");
// 密碼加密
String md5Password = getMd5Password(memberAccountVO.getPassword(), salt);
// 新增MemberAccount 資料
MemberAccount memberAccount = new MemberAccount();
memberAccount.setUsername(memberAccountVO.getUsername());
memberAccount.setPassword(md5Password);
memberAccount.setSalt(salt);
memberAccount.setCreate_by(memberAccountVO.getUsername());
memberAccount.setUpdate_by(memberAccountVO.getUsername());
Integer id = memberAccountDao.insert(memberAccount);
if(id == 0) return Optional.of("新增會員帳號時發生錯誤");
// 新增Member 資料
Member member = new Member();
member.setMa_id(String.valueOf(id));
member.setName(memberAccountVO.getName());
member.setCreate_by(memberAccountVO.getUsername());
member.setUpdate_by(memberAccountVO.getUsername());
Integer result = memberService.insert(member);
if(result == 0) return Optional.of("新增會員資料時發生錯誤");
return Optional.empty();
}
@Override
public MemberAccount findMemberAccountByUsername(String username) {
// TODO Auto-generated method stub
return memberAccountDao.findMemberAccountByUsername(username);
}
}