iT邦幫忙

2021 iThome 鐵人賽

DAY 17
1

經過上一篇Day 16 - Spring Boot 資料驗證的功能實作後,我們的業務邏輯層需要處理的判斷就變少了,可以讓我們的程式碼更加的簡潔,但還是有其他的問題,就是我們控制器層與業務邏輯層耦合的問題。

當我們在業務邏輯層遇到不理想的狀況時,還是會把資料回傳給呼叫的方法,再由呼叫的方法進行判斷,這樣會讓每個呼叫這個業務邏輯的方法,都需要判斷一次回傳的內容,所以實務上我們只會讓業務邏輯層回傳完整且正確執行後的理想結果其他不理想的狀況都直接拋出例外,由ExceptionHandler 統一處理。

統一例外處理

當我們要處理例外的時候,一般情況下都會使用try-catch 語法,但就像上面所說的,我們的業務邏輯方法只想要處理理想的結果,而Spring 有提供了一個非常方便的例外處理方案,也就是利用@ControllerAdvice@RestControllerAdvice ****註釋。

透過基於剖面技術實現的@ControllerAdvice@RestControllerAdvice ****註釋,可以對例外進行統一處理預設對所有的Controller 有效,若想要限定生效範圍則可以使用以下方法。

  1. 指定註釋 :

    // Target all Controllers annotated with @RestController
    @ControllerAdvice(annotations = RestController.class)
    public class ExampleAdvice1 {}
    
  2. 指定Package :

    // Target all Controllers within specific packages
    @ControllerAdvice("org.example.controllers")
    public class ExampleAdvice2 {}
    
  3. 指定類型 :

    // Target all Controllers assignable to specific classes
    @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
    public class ExampleAdvice3 {}
    

當我們使用@ControllerAdvice@RestControllerAdvice ****註釋開啟了對全域例外的捕捉,接下來就只需要做一件事情,那就是使用@ExceptionHandler 註釋定義捕捉例外的類型並對指定類型的例外進行統一處理,使用方法就跟下面展示的差不多。

@ControllerAdvice
public class ExampleAdviceDemo {

	// 統一處理NullPointerException 例外
	@ExceptionHandler(NullPointerException.class)
	public String handleNullPointerException() {
		// 忽略
	}

	// 統一處理陣列相關例外,如索引超出範圍、資料型別不相容等。
	@ExceptionHandler({ArrayIndexOutOfBoundsException.class, ArrayStoreException.class})
	public String handleArrayException() {
		// 忽略
	}

	// 統一處理其他非上述方法提及之例外
	@ExceptionHandler(Exception.class)
	public String handleException() {
		// 忽略
	}	
}

實作

自定義例外

在實務上,我們可以自己定義例外的類別,為各種狀況提供客製化的例外類別。

ServiceException

先定義一個ServiceException,再讓其他自定義的例外類別去繼承,這樣使用@ExceptionHandler ****接例外的時候比較方便。

package com.example.demo.service.ex;

public class ServiceException extends RuntimeException {

	// 略...

}

其他自定義例外類別

package com.example.demo.service.ex;

public class UsernameDuplicateException extends ServiceException {

	// 略...

}
package com.example.demo.service.ex;

public class PasswordNotMatchException extends ServiceException {

	// 略...

}
package com.example.demo.service.ex;

public class InsertException extends ServiceException {

	// 略...

}
package com.example.demo.service.ex;

public class MemberAccountNotFoundException extends ServiceException {

	// 略...

}
package com.example.demo.service.ex;

public class MemberNotFoundException extends ServiceException {

	// 略...

}

修改業務邏輯層的註冊方法

@Service
public class MemberAccountServiceImpl implements MemberAccountService {

	// 略...
	
	@Override
	public MemberAccountVO login(MemberAccount memberAccount) {
		// TODO Auto-generated method stub
		// 檢查帳號是否存在
		MemberAccount data = memberAccountDao.findMemberAccountByUsername(memberAccount.getUsername());
		if(data == null) throw new MemberAccountNotFoundException("帳號或密碼錯誤");

		// 使用資料庫鹽值對輸入密碼進行加密
		String md5Password = getMd5Password(memberAccount.getPassword(), data.getSalt());

		// 比對密碼是否相等
		if(!md5Password.equals(data.getPassword())) throw new PasswordNotMatchException("帳號或密碼錯誤");
		
		// 取得對應Member 資料
		Member member = memberService.findMemberByMa_id(data.getId());
		if(member == null) throw new MemberNotFoundException("帳號或密碼錯誤");
		
		// 組合資料為MemberAccountVO
		MemberAccountVO memberAccountVO = new MemberAccountVO();
		memberAccountVO.setUsername(memberAccount.getUsername());
		memberAccountVO.setName(member.getName());
		return memberAccountVO;
	}

	@Override
	public void register(MemberAccountVO memberAccountVO) {
		// TODO Auto-generated method stub
		// 驗證欄位是否填寫及格式
		if(!memberAccountVO.getPassword().equals(memberAccountVO.getCheckPassword())) throw new PasswordNotMatchException("兩次輸入密碼不相符");
		
		// 檢查帳號是否重複註冊
		MemberAccount data = memberAccountDao.findMemberAccountByUsername(memberAccountVO.getUsername());
		if(data != null) throw new UsernameDuplicateException("該帳號已被使用");
		
		// 產生鹽值
		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) throw new InsertException("新增會員帳號時發生錯誤");

		// 新增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) throw new InsertException("新增會員資料時發生錯誤");
	}

	// 略...

}

建立統一處理例外的控制器

package com.example.demo.controller;

import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.example.demo.service.ex.ServiceException;

@ControllerAdvice
public class ExceptionHandlerController {

	@ExceptionHandler({BindException.class})
	public String handleBindException(
			Throwable e, 
			RedirectAttributes redirectAttributes) {
		
		BindingResult results = ((BindException) e).getBindingResult();

		// 取得第一個例外訊息
		String message = results.getFieldErrors().get(0).getDefaultMessage();
		redirectAttributes.addFlashAttribute("MESSAGE", message);

		// 取得全部例外訊息
		// for(FieldError er :results.getFieldErrors()) {
		// 	System.err.println(er.getDefaultMessage());
		// 	System.err.println(er.getField());
		// }
		
		return "redirect:login";
	}
	
	@ExceptionHandler({ServiceException.class, Exception.class})
	public String handleServiceException(
			Throwable e, 
			RedirectAttributes redirectAttributes) {

		redirectAttributes.addFlashAttribute("MESSAGE", e.getMessage());
		return "redirect:login";
	}
	
}

Github

新增例外處理機制並將任何不符合預期情況改為拋出例外

參考網站

Controller Advice


上一篇
Day 16 - Spring Boot 資料驗證
下一篇
Day 18 - Spring Boot 日誌紀錄
系列文
誤打誤撞學了Spring Boot 還當了後端工程師30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言