最近做了一個前後端分離高併發的秒殺書城 ,對項目的代碼結構有了新的認識。具體的後臺代碼實踐在這裏。對於這個項目,我總結了四點比較重要的項目結構要點,希望對小夥伴們以後的開發中有新的啓發。
1. 一定要有返回類型
如今較大型的項目都會用到前後端分離的技術,此時,接口和數據的定義就會顯得尤爲重要。爲了給前端返回統一的用戶數據,在一般情況下,我們會爲返回值定義一個實體類,其中的屬性包括返回碼,返回描述,返回實體,一個例子如下:
{
"code":200,
"msg":"ok",
"data":{
"this is a response data"
}
}
-
我們在代碼中可以這樣編寫
public class ReturnType { /** * 狀態碼 */ private int code; /** * 表明對應請求的返回處理結果 "ok" 或 "fail" */ private String status; /** * 若status=success,則data內返回前端需要的json數據 * 若status=fail,則data內使用通用的錯誤碼格式 */ private Object data; /** * 定義一個通用的創建方法,用於返回值爲空的情況 */ public static ReturnType create() { return ReturnType.create("ok",200); } public static ReturnType create(Object result,int code){ return ReturnType.create(result,"ok",200); } public static ReturnType create(Object result,String status,int code){ ReturnType type = new ReturnType(); type.setStatus(status); type.setData(result); type.setCode(code); return type; } public int getCode() { return code; } public int setData(Object code) { this.data = code; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }
2. 異常拋出的姿勢
有過面向對象基礎的人都知道,異常是面向對象必不可少的一部分,就Java來說,有ERROR
和Exception
兩大類,由於本片文章主要說的是異常拋出的一些技巧,所以這些基礎性的知識可以看這篇文章:Java中的異常
在做Java開發的時候,常常需要我們拋出異常。尤其是前後端分離的項目,在正常情況下,前臺給後臺發送請求,後臺通過處理再把數據返回給前臺。但在非正常情況下,譬如庫存不夠,插入的主鍵已經存在等異常情況下,我們必須要把這種情況告訴前臺,這就用到了我們的自定義異常。
舉個例子,如果是庫存不夠的異常,我應該給前臺這樣返回:
{
"code":502,
"msg":"apple的庫存不夠",
"data":{
"this response shouldn't have response"
}
}
但是有一個問題,我們如何才能給前臺拋出這個優雅的異常呢?
可以用裝飾者模式解決這個問題
-
首先定義一個異常接口,作爲抽象組件
// 裝飾者模式中的抽象組件 public interface ReturnException { /** * 得到錯誤代碼 * @return ErrCode */ int getErrCode(); /** * 得到錯誤信息 * @return ErrMsg */ String getErrMsg(); /** * 設置錯誤信息 * @param errMsg 錯誤信息 */ void setErrMsg(String errMsg); }
-
然後再定義一個異常實現類,作爲具體裝飾者
// 具體裝飾者 public class ReturnExceptionImpl extends Exception implements ReturnException { private ReturnException returnException; /** * 直接接收EmException的傳參用於構造業務異常 * @param returnPtin 錯誤類型 */ public ReturnExceptionImpl(ReturnException returnException) { super(); this.returnException = returnException; } /** * 接收自定義errMsg的方式構造業務異常 * @param returnException 錯誤類型 * @param errMsg 錯誤信息 */ public ReturnExceptionImpl(ReturnException returnException,String errMsg){ this.returnException = returnException; this.returnError.setErrMsg(errMsg); } @Override public int getErrCode() { return this.returnError.getErrCode(); } @Override public String getErrMsg() { return this.returnError.getErrMsg(); } @Override void ReturnException setErrMsg(String errMsg) { this.returnException.setErrMsg(errMsg); } }
-
還有一個枚舉類型,作爲裝飾者中的具體構件
// 具體構件 public enum EmException implements ReturnError { /** * 通用錯誤類型 999 */ PARAMETER_VALIDATION_ERROR(999, "參數不合法"), /** * 未知錯誤 888 */ UNKNOWN_ERROR(888, "未知異常"), /** * 10000開頭爲用戶相關信息錯誤定義 */ STOCK_NOT_ENOUGH(502,"庫存不夠"); private int errCode; private String errMsg; EmException(int errCode, String errMsg) { this.errCode = errCode; this.errMsg = errMsg; } @Override public int getErrCode() { return this.errCode; } @Override public String getErrMsg() { return this.errMsg; } @Override public ReturnException setErrMsg(String errMsg) { this.errMsg = errMsg; return this; } }
-
之後,我們碰到異常就可以這樣拋出
if (!itemService.decreaseStock(orderDTO.getItemId(),orderDTO.getAmount())){ throw new ReturnExceptionImpl(EmException.STOCK_NOT_ENOUGH); }
-
最後一步,我們在SpringBoot中定義全局異常處理,用來接收全局的異常拋出:
@ControllerAdvice public class GlobalExceptionHandler{ @ExceptionHandler(Exception.class) @ResponseBody public ReturnType doError(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Exception ex) { String msg; int code; if( ex instanceof ReturnException){ ReturnException businessException = (ReturnException)ex; code = EmReturnError.UNKNOWN_ERROR.getErrCode(); msg = businessException.getErrMsg()); }else if(ex instanceof ServletRequestBindingException){ code = EmReturnError.UNKNOWN_ERROR.getErrCode(); msg = "url綁定路由問題"; }else if(ex instanceof NoHandlerFoundException){ code = EmReturnError.UNKNOWN_ERROR.getErrCode(); msg = "沒有找到對應的訪問路徑"; }else if (ex instanceof IllegalArgumentException) { code = EmReturnError.PARAMETER_VALIDATION_ERROR.getErrCode msg = "輸入參數不完整"; }else{ code = EmReturnError.UNKNOWN_ERROR.getErrCode(); msg = EmReturnError.UNKNOWN_ERROR.getErrMsg(); } return ReturnType.create(msg,"fail",code); } }
3. 實體類三劍客
所謂的實體類三劍客,即是DO,DTO和VO了。
- DO,即是DataObject,數據對象。是和數據庫的表一對一的,一般情況下由Mybatis的逆向插件直接生成的
- DTO,即是DataTransferObject,數據轉換對象。是對DO層的二次抽象,一般前端的數據會填充到DTO中,即Controller的參數實體一般是DTO
- VO,即是ViewObject,視圖對象。顧名思義,我們後臺返回給前臺的實體即是VO
對於這三者的關係,我舉個例子:大型項目中,用戶其他信息和密碼在數據庫中分開存放的,因爲在實際業務中,密碼和用戶其他信息的讀取和修改概率是不一樣的。在這種業務場景下,DO就會把用戶其他信息和密碼存成兩個實體類,這是和數據庫一一對應的。但是DTO就會把用戶其他信息和密碼合成一個實體類,因爲在業務邏輯中,他們的意義一樣重要。當我們把這些數據返回時,前端可能不需要諸如Id一類的數據,我們就會重新把他們封裝爲VO實體類
以我Github中的秒殺項目爲例,如下所示:
把他們抽象起來來說,就像下圖一樣:
此時,又有一個問題誕生了,DO,DTO和VO怎麼轉化呢?
一般情況下,我們會在它們的所在的層級進行轉化:譬如,我們在Service層中把DTO和DO進行相互轉化,在Controller層中把DTO和VO進行相互轉化。在此我們一定要注意不要手動轉化,太過麻煩,我們要善於利用API,Spring給我們提供了BeanUtils.copyProperties()
方法,十分方便快捷。如下:
private CategoryDTO convertDtoFromDO(CategoryInfoDO categoryInfoDO, List<ItemCategoryDO> itemCategoryDOList) {
CategoryDTO categoryDTO = new CategoryDTO();
BeanUtils.copyProperties(categoryInfoDO,categoryDTO);
if (itemCategoryDOList != null) {
categoryDTO.setItemIds(itemCategoryDOList.stream()
.map(ItemCategoryDO::getItemId).collect(Collectors.toList()));
}
return categoryDTO;
}
更近一步去思考,如此多的convertxxx
,我們有沒有可能去簡化一下,當然可以!我們可以定義一個反型接口把這些都抽象出來,但是這個就是後話了。
同時,我們還要利用好工具,譬如Lombok。它是通過註解的形式來減輕我們編程的重複勞動。如下
@Data
public class UserDTO() {
private int id;
private String name;
private String pwd;
}
最明顯的一個功能就是它可以幫助我們省略實體類中的getter和setter方法,甚至是toString和hashCode,equals等等各種方便的操作。雖然IDEA可以自動生成這些方法,但是IDEA絕對沒有它的註解方便省事!
4. 項目包的命名
這個不是絕對的,包括上面三種都不是絕對的。
在我一年多的項目開發過程中,結合我自己和大牛項目結構的包名,我覺得有基本的命名和機構有以下這麼幾種:
- config:這個是一些配置,譬如Redis的配置,Druid的配置等等
- constant:這個會放置常量類,如枚舉,常量接口等等
- controller:這個是控制層。有時候VO實體類所在的vo包也在該包下
- dao:這個是用來操作數據庫的包。mybaits中的mapper也可以存放在這裏面
- entity:這個包存放DO實體類
- log:存放日誌相關操作
- service:這個是服務層。有時候會把DTO實體類所在的model包也放在該包下
- util:這個就是防止工具類了,一般情況下,工具類的方法都是靜態的,同時該工具類的構造方法也是私有的