'那些年,我們見過的 Java 服務端亂象'

Java 移動互聯網 雙城記 創業 指尖上的代碼 2019-09-14
"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

關於插件:

阿里的 AliGenerator 是一款基於 MyBatis Generator 改造的 DAO 層代碼自動生成工具。利用 AliGenerator 生成的代碼,在執行復雜查詢的時候,需要在業務代碼中組裝查詢條件,使業務代碼顯得特別臃腫。

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

關於插件:

阿里的 AliGenerator 是一款基於 MyBatis Generator 改造的 DAO 層代碼自動生成工具。利用 AliGenerator 生成的代碼,在執行復雜查詢的時候,需要在業務代碼中組裝查詢條件,使業務代碼顯得特別臃腫。

那些年,我們見過的 Java 服務端亂象

個人不喜歡用 DAO 層代碼生成插件,更喜歡用原汁原味的 MyBatis XML 映射,主要原因如下:

  • 會在項目中導入一些不符合規範的代碼;
  • 只需要進行一個簡單查詢,也需要導入一整套複雜代碼;
  • 進行復雜查詢時,拼裝條件的代碼複雜且不直觀,不如在XML中直接編寫SQL語句;
  • 變更表格後需要重新生成代碼並進行覆蓋,可能會不小心刪除自定義函數。

當然,既然選擇了使用 DAO 層代碼生成插件,在享受便利的同時也應該接受插件的缺點。

3.3.把 Redis 代碼寫在 Service 中

現象描述:

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

關於插件:

阿里的 AliGenerator 是一款基於 MyBatis Generator 改造的 DAO 層代碼自動生成工具。利用 AliGenerator 生成的代碼,在執行復雜查詢的時候,需要在業務代碼中組裝查詢條件,使業務代碼顯得特別臃腫。

那些年,我們見過的 Java 服務端亂象

個人不喜歡用 DAO 層代碼生成插件,更喜歡用原汁原味的 MyBatis XML 映射,主要原因如下:

  • 會在項目中導入一些不符合規範的代碼;
  • 只需要進行一個簡單查詢,也需要導入一整套複雜代碼;
  • 進行復雜查詢時,拼裝條件的代碼複雜且不直觀,不如在XML中直接編寫SQL語句;
  • 變更表格後需要重新生成代碼並進行覆蓋,可能會不小心刪除自定義函數。

當然,既然選擇了使用 DAO 層代碼生成插件,在享受便利的同時也應該接受插件的缺點。

3.3.把 Redis 代碼寫在 Service 中

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

關於插件:

阿里的 AliGenerator 是一款基於 MyBatis Generator 改造的 DAO 層代碼自動生成工具。利用 AliGenerator 生成的代碼,在執行復雜查詢的時候,需要在業務代碼中組裝查詢條件,使業務代碼顯得特別臃腫。

那些年,我們見過的 Java 服務端亂象

個人不喜歡用 DAO 層代碼生成插件,更喜歡用原汁原味的 MyBatis XML 映射,主要原因如下:

  • 會在項目中導入一些不符合規範的代碼;
  • 只需要進行一個簡單查詢,也需要導入一整套複雜代碼;
  • 進行復雜查詢時,拼裝條件的代碼複雜且不直觀,不如在XML中直接編寫SQL語句;
  • 變更表格後需要重新生成代碼並進行覆蓋,可能會不小心刪除自定義函數。

當然,既然選擇了使用 DAO 層代碼生成插件,在享受便利的同時也應該接受插件的缺點。

3.3.把 Redis 代碼寫在 Service 中

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

關於插件:

阿里的 AliGenerator 是一款基於 MyBatis Generator 改造的 DAO 層代碼自動生成工具。利用 AliGenerator 生成的代碼,在執行復雜查詢的時候,需要在業務代碼中組裝查詢條件,使業務代碼顯得特別臃腫。

那些年,我們見過的 Java 服務端亂象

個人不喜歡用 DAO 層代碼生成插件,更喜歡用原汁原味的 MyBatis XML 映射,主要原因如下:

  • 會在項目中導入一些不符合規範的代碼;
  • 只需要進行一個簡單查詢,也需要導入一整套複雜代碼;
  • 進行復雜查詢時,拼裝條件的代碼複雜且不直觀,不如在XML中直接編寫SQL語句;
  • 變更表格後需要重新生成代碼並進行覆蓋,可能會不小心刪除自定義函數。

當然,既然選擇了使用 DAO 層代碼生成插件,在享受便利的同時也應該接受插件的缺點。

3.3.把 Redis 代碼寫在 Service 中

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

把一個 Redis 對象相關操作接口封裝為一個 DAO 類,符合面對對象的編程思想,也符合 SpringMVC 服務端三層架構規範,更便於代碼的管理和維護。

4.把數據庫模型類暴露給接口

4.1.現象描述

/** 用戶DAO類 */
@Repository
public class UserDAO {
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {...}
}
/** 用戶服務類 */
@Service
public class UserService {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {
return userDAO.getUser(userId);
}
}
/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶服務 */
@Autowired
private UserService userService;
/** 獲取用戶函數 */
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserDO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
UserDO user = userService.getUser(userId);
return Result.success(user);
}
}

上面的代碼,看上去是滿足 SpringMVC 服務端三層架構的,唯一的問題就是把數據庫模型類 UserDO 直接暴露給了外部接口。

4.2.存在問題及解決方案

存在問題:

  • 間接暴露數據庫表格設計,給競爭對手競品分析帶來方便;
  • 如果數據庫查詢不做字段限制,會導致接口數據龐大,浪費用戶的寶貴流量;
  • 如果數據庫查詢不做字段限制,容易把敏感字段暴露給接口,導致出現數據的安全問題;
  • 如果數據庫模型類不能滿足接口需求,需要在數據庫模型類中添加別的字段,導致數據庫模型類跟數據庫字段不匹配問題;
  • 如果沒有維護好接口文檔,通過閱讀代碼是無法分辨出數據庫模型類中哪些字段是接口使用的,導致代碼的可維護性變差。

解決方案:

  • 從管理制度上要求數據庫和接口的模型類完全獨立;
  • 從項目結構上限制開發人員把數據庫模型類暴露給接口。

4.3.項目搭建的三種方式

下面,將介紹如何更科學地搭建 Java 項目,有效地限制開發人員把數據庫模型類暴露給接口。

第1種:共用模型的項目搭建

共用模型的項目搭建,把所有模型類放在一個模型項目(example-model)中,其它項目(example-repository、example-service、example-website)都依賴該模型項目,關係圖如下:


"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

關於插件:

阿里的 AliGenerator 是一款基於 MyBatis Generator 改造的 DAO 層代碼自動生成工具。利用 AliGenerator 生成的代碼,在執行復雜查詢的時候,需要在業務代碼中組裝查詢條件,使業務代碼顯得特別臃腫。

那些年,我們見過的 Java 服務端亂象

個人不喜歡用 DAO 層代碼生成插件,更喜歡用原汁原味的 MyBatis XML 映射,主要原因如下:

  • 會在項目中導入一些不符合規範的代碼;
  • 只需要進行一個簡單查詢,也需要導入一整套複雜代碼;
  • 進行復雜查詢時,拼裝條件的代碼複雜且不直觀,不如在XML中直接編寫SQL語句;
  • 變更表格後需要重新生成代碼並進行覆蓋,可能會不小心刪除自定義函數。

當然,既然選擇了使用 DAO 層代碼生成插件,在享受便利的同時也應該接受插件的缺點。

3.3.把 Redis 代碼寫在 Service 中

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

把一個 Redis 對象相關操作接口封裝為一個 DAO 類,符合面對對象的編程思想,也符合 SpringMVC 服務端三層架構規範,更便於代碼的管理和維護。

4.把數據庫模型類暴露給接口

4.1.現象描述

/** 用戶DAO類 */
@Repository
public class UserDAO {
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {...}
}
/** 用戶服務類 */
@Service
public class UserService {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {
return userDAO.getUser(userId);
}
}
/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶服務 */
@Autowired
private UserService userService;
/** 獲取用戶函數 */
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserDO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
UserDO user = userService.getUser(userId);
return Result.success(user);
}
}

上面的代碼,看上去是滿足 SpringMVC 服務端三層架構的,唯一的問題就是把數據庫模型類 UserDO 直接暴露給了外部接口。

4.2.存在問題及解決方案

存在問題:

  • 間接暴露數據庫表格設計,給競爭對手競品分析帶來方便;
  • 如果數據庫查詢不做字段限制,會導致接口數據龐大,浪費用戶的寶貴流量;
  • 如果數據庫查詢不做字段限制,容易把敏感字段暴露給接口,導致出現數據的安全問題;
  • 如果數據庫模型類不能滿足接口需求,需要在數據庫模型類中添加別的字段,導致數據庫模型類跟數據庫字段不匹配問題;
  • 如果沒有維護好接口文檔,通過閱讀代碼是無法分辨出數據庫模型類中哪些字段是接口使用的,導致代碼的可維護性變差。

解決方案:

  • 從管理制度上要求數據庫和接口的模型類完全獨立;
  • 從項目結構上限制開發人員把數據庫模型類暴露給接口。

4.3.項目搭建的三種方式

下面,將介紹如何更科學地搭建 Java 項目,有效地限制開發人員把數據庫模型類暴露給接口。

第1種:共用模型的項目搭建

共用模型的項目搭建,把所有模型類放在一個模型項目(example-model)中,其它項目(example-repository、example-service、example-website)都依賴該模型項目,關係圖如下:


那些年,我們見過的 Java 服務端亂象



"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

關於插件:

阿里的 AliGenerator 是一款基於 MyBatis Generator 改造的 DAO 層代碼自動生成工具。利用 AliGenerator 生成的代碼,在執行復雜查詢的時候,需要在業務代碼中組裝查詢條件,使業務代碼顯得特別臃腫。

那些年,我們見過的 Java 服務端亂象

個人不喜歡用 DAO 層代碼生成插件,更喜歡用原汁原味的 MyBatis XML 映射,主要原因如下:

  • 會在項目中導入一些不符合規範的代碼;
  • 只需要進行一個簡單查詢,也需要導入一整套複雜代碼;
  • 進行復雜查詢時,拼裝條件的代碼複雜且不直觀,不如在XML中直接編寫SQL語句;
  • 變更表格後需要重新生成代碼並進行覆蓋,可能會不小心刪除自定義函數。

當然,既然選擇了使用 DAO 層代碼生成插件,在享受便利的同時也應該接受插件的缺點。

3.3.把 Redis 代碼寫在 Service 中

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

把一個 Redis 對象相關操作接口封裝為一個 DAO 類,符合面對對象的編程思想,也符合 SpringMVC 服務端三層架構規範,更便於代碼的管理和維護。

4.把數據庫模型類暴露給接口

4.1.現象描述

/** 用戶DAO類 */
@Repository
public class UserDAO {
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {...}
}
/** 用戶服務類 */
@Service
public class UserService {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {
return userDAO.getUser(userId);
}
}
/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶服務 */
@Autowired
private UserService userService;
/** 獲取用戶函數 */
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserDO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
UserDO user = userService.getUser(userId);
return Result.success(user);
}
}

上面的代碼,看上去是滿足 SpringMVC 服務端三層架構的,唯一的問題就是把數據庫模型類 UserDO 直接暴露給了外部接口。

4.2.存在問題及解決方案

存在問題:

  • 間接暴露數據庫表格設計,給競爭對手競品分析帶來方便;
  • 如果數據庫查詢不做字段限制,會導致接口數據龐大,浪費用戶的寶貴流量;
  • 如果數據庫查詢不做字段限制,容易把敏感字段暴露給接口,導致出現數據的安全問題;
  • 如果數據庫模型類不能滿足接口需求,需要在數據庫模型類中添加別的字段,導致數據庫模型類跟數據庫字段不匹配問題;
  • 如果沒有維護好接口文檔,通過閱讀代碼是無法分辨出數據庫模型類中哪些字段是接口使用的,導致代碼的可維護性變差。

解決方案:

  • 從管理制度上要求數據庫和接口的模型類完全獨立;
  • 從項目結構上限制開發人員把數據庫模型類暴露給接口。

4.3.項目搭建的三種方式

下面,將介紹如何更科學地搭建 Java 項目,有效地限制開發人員把數據庫模型類暴露給接口。

第1種:共用模型的項目搭建

共用模型的項目搭建,把所有模型類放在一個模型項目(example-model)中,其它項目(example-repository、example-service、example-website)都依賴該模型項目,關係圖如下:


那些年,我們見過的 Java 服務端亂象



那些年,我們見過的 Java 服務端亂象

風險:表現層項目(example-webapp)可以調用業務層項目(example-service)中的任意服務函數,甚至於越過業務層直接調用持久層項目(example-repository)的DAO函數。

第2種:模型分離的項目搭建

模型分離的項目搭建,單獨搭建API項目(example-api),抽象出對外接口及其模型VO類。業務層項目(example-service)實現了這些接口,並向表現層項目(example-webapp)提供服務。表現層項目(example-webapp)只調用API項目(example-api)定義的服務接口。


"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

關於插件:

阿里的 AliGenerator 是一款基於 MyBatis Generator 改造的 DAO 層代碼自動生成工具。利用 AliGenerator 生成的代碼,在執行復雜查詢的時候,需要在業務代碼中組裝查詢條件,使業務代碼顯得特別臃腫。

那些年,我們見過的 Java 服務端亂象

個人不喜歡用 DAO 層代碼生成插件,更喜歡用原汁原味的 MyBatis XML 映射,主要原因如下:

  • 會在項目中導入一些不符合規範的代碼;
  • 只需要進行一個簡單查詢,也需要導入一整套複雜代碼;
  • 進行復雜查詢時,拼裝條件的代碼複雜且不直觀,不如在XML中直接編寫SQL語句;
  • 變更表格後需要重新生成代碼並進行覆蓋,可能會不小心刪除自定義函數。

當然,既然選擇了使用 DAO 層代碼生成插件,在享受便利的同時也應該接受插件的缺點。

3.3.把 Redis 代碼寫在 Service 中

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

把一個 Redis 對象相關操作接口封裝為一個 DAO 類,符合面對對象的編程思想,也符合 SpringMVC 服務端三層架構規範,更便於代碼的管理和維護。

4.把數據庫模型類暴露給接口

4.1.現象描述

/** 用戶DAO類 */
@Repository
public class UserDAO {
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {...}
}
/** 用戶服務類 */
@Service
public class UserService {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {
return userDAO.getUser(userId);
}
}
/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶服務 */
@Autowired
private UserService userService;
/** 獲取用戶函數 */
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserDO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
UserDO user = userService.getUser(userId);
return Result.success(user);
}
}

上面的代碼,看上去是滿足 SpringMVC 服務端三層架構的,唯一的問題就是把數據庫模型類 UserDO 直接暴露給了外部接口。

4.2.存在問題及解決方案

存在問題:

  • 間接暴露數據庫表格設計,給競爭對手競品分析帶來方便;
  • 如果數據庫查詢不做字段限制,會導致接口數據龐大,浪費用戶的寶貴流量;
  • 如果數據庫查詢不做字段限制,容易把敏感字段暴露給接口,導致出現數據的安全問題;
  • 如果數據庫模型類不能滿足接口需求,需要在數據庫模型類中添加別的字段,導致數據庫模型類跟數據庫字段不匹配問題;
  • 如果沒有維護好接口文檔,通過閱讀代碼是無法分辨出數據庫模型類中哪些字段是接口使用的,導致代碼的可維護性變差。

解決方案:

  • 從管理制度上要求數據庫和接口的模型類完全獨立;
  • 從項目結構上限制開發人員把數據庫模型類暴露給接口。

4.3.項目搭建的三種方式

下面,將介紹如何更科學地搭建 Java 項目,有效地限制開發人員把數據庫模型類暴露給接口。

第1種:共用模型的項目搭建

共用模型的項目搭建,把所有模型類放在一個模型項目(example-model)中,其它項目(example-repository、example-service、example-website)都依賴該模型項目,關係圖如下:


那些年,我們見過的 Java 服務端亂象



那些年,我們見過的 Java 服務端亂象

風險:表現層項目(example-webapp)可以調用業務層項目(example-service)中的任意服務函數,甚至於越過業務層直接調用持久層項目(example-repository)的DAO函數。

第2種:模型分離的項目搭建

模型分離的項目搭建,單獨搭建API項目(example-api),抽象出對外接口及其模型VO類。業務層項目(example-service)實現了這些接口,並向表現層項目(example-webapp)提供服務。表現層項目(example-webapp)只調用API項目(example-api)定義的服務接口。


那些年,我們見過的 Java 服務端亂象



"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

關於插件:

阿里的 AliGenerator 是一款基於 MyBatis Generator 改造的 DAO 層代碼自動生成工具。利用 AliGenerator 生成的代碼,在執行復雜查詢的時候,需要在業務代碼中組裝查詢條件,使業務代碼顯得特別臃腫。

那些年,我們見過的 Java 服務端亂象

個人不喜歡用 DAO 層代碼生成插件,更喜歡用原汁原味的 MyBatis XML 映射,主要原因如下:

  • 會在項目中導入一些不符合規範的代碼;
  • 只需要進行一個簡單查詢,也需要導入一整套複雜代碼;
  • 進行復雜查詢時,拼裝條件的代碼複雜且不直觀,不如在XML中直接編寫SQL語句;
  • 變更表格後需要重新生成代碼並進行覆蓋,可能會不小心刪除自定義函數。

當然,既然選擇了使用 DAO 層代碼生成插件,在享受便利的同時也應該接受插件的缺點。

3.3.把 Redis 代碼寫在 Service 中

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

把一個 Redis 對象相關操作接口封裝為一個 DAO 類,符合面對對象的編程思想,也符合 SpringMVC 服務端三層架構規範,更便於代碼的管理和維護。

4.把數據庫模型類暴露給接口

4.1.現象描述

/** 用戶DAO類 */
@Repository
public class UserDAO {
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {...}
}
/** 用戶服務類 */
@Service
public class UserService {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {
return userDAO.getUser(userId);
}
}
/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶服務 */
@Autowired
private UserService userService;
/** 獲取用戶函數 */
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserDO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
UserDO user = userService.getUser(userId);
return Result.success(user);
}
}

上面的代碼,看上去是滿足 SpringMVC 服務端三層架構的,唯一的問題就是把數據庫模型類 UserDO 直接暴露給了外部接口。

4.2.存在問題及解決方案

存在問題:

  • 間接暴露數據庫表格設計,給競爭對手競品分析帶來方便;
  • 如果數據庫查詢不做字段限制,會導致接口數據龐大,浪費用戶的寶貴流量;
  • 如果數據庫查詢不做字段限制,容易把敏感字段暴露給接口,導致出現數據的安全問題;
  • 如果數據庫模型類不能滿足接口需求,需要在數據庫模型類中添加別的字段,導致數據庫模型類跟數據庫字段不匹配問題;
  • 如果沒有維護好接口文檔,通過閱讀代碼是無法分辨出數據庫模型類中哪些字段是接口使用的,導致代碼的可維護性變差。

解決方案:

  • 從管理制度上要求數據庫和接口的模型類完全獨立;
  • 從項目結構上限制開發人員把數據庫模型類暴露給接口。

4.3.項目搭建的三種方式

下面,將介紹如何更科學地搭建 Java 項目,有效地限制開發人員把數據庫模型類暴露給接口。

第1種:共用模型的項目搭建

共用模型的項目搭建,把所有模型類放在一個模型項目(example-model)中,其它項目(example-repository、example-service、example-website)都依賴該模型項目,關係圖如下:


那些年,我們見過的 Java 服務端亂象



那些年,我們見過的 Java 服務端亂象

風險:表現層項目(example-webapp)可以調用業務層項目(example-service)中的任意服務函數,甚至於越過業務層直接調用持久層項目(example-repository)的DAO函數。

第2種:模型分離的項目搭建

模型分離的項目搭建,單獨搭建API項目(example-api),抽象出對外接口及其模型VO類。業務層項目(example-service)實現了這些接口,並向表現層項目(example-webapp)提供服務。表現層項目(example-webapp)只調用API項目(example-api)定義的服務接口。


那些年,我們見過的 Java 服務端亂象



那些年,我們見過的 Java 服務端亂象

風險:表現層項目(example-webapp)仍然可以調用業務層項目(example-service)提供的內部服務函數和持久層項目(example-repository)的DAO函數。為了避免這種情況,只好管理制度上要求表現層項目(example-webapp)只能調用API項目(example-api)定義的服務接口函數。

第3種:服務化的項目搭建

服務化的項目搭,就是把業務層項目(example-service)和持久層項目(example-repository)通過 Dubbo 項目(example-dubbo)打包成一個服務,向業務層項目(example-webapp)或其它業務項目(other-service)提供API項目(example-api)中定義的接口函數。


"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

關於插件:

阿里的 AliGenerator 是一款基於 MyBatis Generator 改造的 DAO 層代碼自動生成工具。利用 AliGenerator 生成的代碼,在執行復雜查詢的時候,需要在業務代碼中組裝查詢條件,使業務代碼顯得特別臃腫。

那些年,我們見過的 Java 服務端亂象

個人不喜歡用 DAO 層代碼生成插件,更喜歡用原汁原味的 MyBatis XML 映射,主要原因如下:

  • 會在項目中導入一些不符合規範的代碼;
  • 只需要進行一個簡單查詢,也需要導入一整套複雜代碼;
  • 進行復雜查詢時,拼裝條件的代碼複雜且不直觀,不如在XML中直接編寫SQL語句;
  • 變更表格後需要重新生成代碼並進行覆蓋,可能會不小心刪除自定義函數。

當然,既然選擇了使用 DAO 層代碼生成插件,在享受便利的同時也應該接受插件的缺點。

3.3.把 Redis 代碼寫在 Service 中

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

把一個 Redis 對象相關操作接口封裝為一個 DAO 類,符合面對對象的編程思想,也符合 SpringMVC 服務端三層架構規範,更便於代碼的管理和維護。

4.把數據庫模型類暴露給接口

4.1.現象描述

/** 用戶DAO類 */
@Repository
public class UserDAO {
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {...}
}
/** 用戶服務類 */
@Service
public class UserService {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {
return userDAO.getUser(userId);
}
}
/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶服務 */
@Autowired
private UserService userService;
/** 獲取用戶函數 */
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserDO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
UserDO user = userService.getUser(userId);
return Result.success(user);
}
}

上面的代碼,看上去是滿足 SpringMVC 服務端三層架構的,唯一的問題就是把數據庫模型類 UserDO 直接暴露給了外部接口。

4.2.存在問題及解決方案

存在問題:

  • 間接暴露數據庫表格設計,給競爭對手競品分析帶來方便;
  • 如果數據庫查詢不做字段限制,會導致接口數據龐大,浪費用戶的寶貴流量;
  • 如果數據庫查詢不做字段限制,容易把敏感字段暴露給接口,導致出現數據的安全問題;
  • 如果數據庫模型類不能滿足接口需求,需要在數據庫模型類中添加別的字段,導致數據庫模型類跟數據庫字段不匹配問題;
  • 如果沒有維護好接口文檔,通過閱讀代碼是無法分辨出數據庫模型類中哪些字段是接口使用的,導致代碼的可維護性變差。

解決方案:

  • 從管理制度上要求數據庫和接口的模型類完全獨立;
  • 從項目結構上限制開發人員把數據庫模型類暴露給接口。

4.3.項目搭建的三種方式

下面,將介紹如何更科學地搭建 Java 項目,有效地限制開發人員把數據庫模型類暴露給接口。

第1種:共用模型的項目搭建

共用模型的項目搭建,把所有模型類放在一個模型項目(example-model)中,其它項目(example-repository、example-service、example-website)都依賴該模型項目,關係圖如下:


那些年,我們見過的 Java 服務端亂象



那些年,我們見過的 Java 服務端亂象

風險:表現層項目(example-webapp)可以調用業務層項目(example-service)中的任意服務函數,甚至於越過業務層直接調用持久層項目(example-repository)的DAO函數。

第2種:模型分離的項目搭建

模型分離的項目搭建,單獨搭建API項目(example-api),抽象出對外接口及其模型VO類。業務層項目(example-service)實現了這些接口,並向表現層項目(example-webapp)提供服務。表現層項目(example-webapp)只調用API項目(example-api)定義的服務接口。


那些年,我們見過的 Java 服務端亂象



那些年,我們見過的 Java 服務端亂象

風險:表現層項目(example-webapp)仍然可以調用業務層項目(example-service)提供的內部服務函數和持久層項目(example-repository)的DAO函數。為了避免這種情況,只好管理制度上要求表現層項目(example-webapp)只能調用API項目(example-api)定義的服務接口函數。

第3種:服務化的項目搭建

服務化的項目搭,就是把業務層項目(example-service)和持久層項目(example-repository)通過 Dubbo 項目(example-dubbo)打包成一個服務,向業務層項目(example-webapp)或其它業務項目(other-service)提供API項目(example-api)中定義的接口函數。


那些年,我們見過的 Java 服務端亂象



"

查爾斯·狄更斯在《雙城記》中寫道:“這是一個最好的時代,也是一個最壞的時代。”

移動互聯網的快速發展,出現了許多新機遇,很多創業者伺機而動;隨著行業競爭加劇,互聯網紅利逐漸消失,很多創業公司九死一生。筆者在初創公司摸爬滾打數年,接觸了各式各樣的 Java 微服務架構,從中獲得了一些優秀的理念,但也發現了一些不合理的現象。現在,筆者總結了一些創業公司存在的 Java 服務端亂象,並嘗試性地給出了一些不成熟的建議。

1.使用Controller基類和Service基類

1.1.現象描述

1.1.1.Controller 基類

常見的 Controller 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Controller 基類主要包含注入服務、靜態常量和靜態函數等,便於所有的Controller 繼承它,並在函數中可以直接使用這些資源。

1.1.2. Service 基類

常見的 Service 基類如下:

那些年,我們見過的 Java 服務端亂象

常見的 Service 基類主要包括注入 DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等,便於所有的 Service 繼承它,並在函數中可以直接使用這些資源。

1.2.論證基類必要性

首先,瞭解一下里氏替換原則:

里氏代換原則(Liskov Substitution Principle,簡稱LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。

其次,瞭解一下基類的優點:

  • 子類擁有父類的所有方法和屬性,從而減少了創建子類的工作量;
  • 提高了代碼的重用性,子類擁有父類的所有功能;
  • 提高了代碼的擴展性,子類可以添加自己的功能。

所以,我們可以得出以下結論:

  • Controller 基類和 Service 基類在整個項目中並沒有直接被使用,也就沒有可使用其子類替換基類的場景,所以不滿足里氏替換原則;
  • Controller 基類和 Service 基類並沒有抽象接口函數或虛函數,即所有繼承基類的子類間沒有相關共性,直接導致在項目中仍然使用的是子類;
  • Controller 基類和 Service 基類只關注了重用性,即子類能夠輕鬆使用基類的注入DAO、注入服務、注入參數、靜態常量、服務函數、靜態函數等資源。但是,忽略了這些資源的必要性,即這些資源並不是子類所必須的,反而給子類帶來了加載時的性能損耗。

綜上所述,Controller 基類和 Service 基類只是一個雜湊類,並不是一個真正意義上的基類,需要進行拆分。

1.3.拆分基類的方法

由於 Service 基類比 Controller 基類更典型,本文以 Service 基類舉例說明如何來拆分“基類”。

1.3.1.把注入實例放入實現類

根據“使用即引入、無用則刪除”原則,在需要使用的實現類中注入需要使用的DAO、服務和參數。

那些年,我們見過的 Java 服務端亂象

1.3.2.把靜態常量放入常量類

對於靜態常量,可以把它們封裝到對應的常量類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

1.3.3.把服務函數放入服務類

對於服務函數,可以把它們封裝到對應的服務類中。在別的服務類使用時,可以注入該服務類實例,然後通過實例調用服務函數。

那些年,我們見過的 Java 服務端亂象

1.3.4.把靜態函數放入工具類

對於靜態函數,可以把它們封裝到對應的工具類中,在需要時直接使用即可。

那些年,我們見過的 Java 服務端亂象

2.把業務代碼寫在 Controller 中

2.1.現象描述

我們會經常會在 Controller 類中看到這樣的代碼:

/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
// 獲取用戶信息
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}

// 拷貝並返回用戶
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}

編寫人員給出的理由是:一個簡單的接口函數,這麼寫也能滿足需求,沒有必要去封裝成一個服務函數。

2.2.一個特殊的案例

案例代碼如下:

那些年,我們見過的 Java 服務端亂象

訪問結果如下:

那些年,我們見過的 Java 服務端亂象

為什麼參數systemName(系統名稱)沒有被注入值?《Spring Documentation》給出的解釋是:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

意思是說:@Value是通過BeanPostProcessor來處理的,而WebApplicationContex和ApplicationContext是單獨處理的,所以WebApplicationContex 不能使用父容器的屬性值。

所以,Controller 不滿足 Service 的需求,不要把業務代碼寫在 Controller 類中。

2.3.服務端三層架構

SpringMVC 服務端採用經典的三層架構,即表現層、業務層、持久層,分別採用@Controller、@Service、@Repository進行類註解。

那些年,我們見過的 Java 服務端亂象

表現層(Presentation):又稱控制層(Controller),負責接收客戶端請求,並向客戶端響應結果,通常採用HTTP協議。

業務層(Business):又稱服務層(Service),負責業務相關邏輯處理,按照功能分為服務、作業等。

持久層(Persistence):又稱倉庫層(Repository),負責數據的持久化,用於業務層訪問緩存和數據庫。

所以,把業務代碼寫入到Controller類中,是不符合SpringMVC服務端三層架構規範的。

3.把持久層代碼寫在 Service 中

把持久層代碼寫在 Service 中,從功能上來看並沒有什麼問題,這也是很多人欣然接受的原因。

3.1.引起以下主要問題

  • 業務層和持久層混雜在一起,不符合SpringMVC服務端三層架構規範;
  • 在業務邏輯中組裝語句、主鍵等,增加了業務邏輯的複雜度;
  • 在業務邏輯中直接使用第三方中間件,不便於第三方持久化中間件的替換;
  • 同一對象的持久層代碼分散在各個業務邏輯中,背離了面對對象的編程思想;
  • 在寫單元測試用例時,無法對持久層接口函數直接測試。

3.2.把數據庫代碼寫在Service中

這裡以數據庫持久化中間件 Hibernate 的直接查詢為例。

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

關於插件:

阿里的 AliGenerator 是一款基於 MyBatis Generator 改造的 DAO 層代碼自動生成工具。利用 AliGenerator 生成的代碼,在執行復雜查詢的時候,需要在業務代碼中組裝查詢條件,使業務代碼顯得特別臃腫。

那些年,我們見過的 Java 服務端亂象

個人不喜歡用 DAO 層代碼生成插件,更喜歡用原汁原味的 MyBatis XML 映射,主要原因如下:

  • 會在項目中導入一些不符合規範的代碼;
  • 只需要進行一個簡單查詢,也需要導入一整套複雜代碼;
  • 進行復雜查詢時,拼裝條件的代碼複雜且不直觀,不如在XML中直接編寫SQL語句;
  • 變更表格後需要重新生成代碼並進行覆蓋,可能會不小心刪除自定義函數。

當然,既然選擇了使用 DAO 層代碼生成插件,在享受便利的同時也應該接受插件的缺點。

3.3.把 Redis 代碼寫在 Service 中

現象描述:

那些年,我們見過的 Java 服務端亂象

建議方案:

那些年,我們見過的 Java 服務端亂象

那些年,我們見過的 Java 服務端亂象

把一個 Redis 對象相關操作接口封裝為一個 DAO 類,符合面對對象的編程思想,也符合 SpringMVC 服務端三層架構規範,更便於代碼的管理和維護。

4.把數據庫模型類暴露給接口

4.1.現象描述

/** 用戶DAO類 */
@Repository
public class UserDAO {
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {...}
}
/** 用戶服務類 */
@Service
public class UserService {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 獲取用戶函數 */
public UserDO getUser(Long userId) {
return userDAO.getUser(userId);
}
}
/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶服務 */
@Autowired
private UserService userService;
/** 獲取用戶函數 */
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserDO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
UserDO user = userService.getUser(userId);
return Result.success(user);
}
}

上面的代碼,看上去是滿足 SpringMVC 服務端三層架構的,唯一的問題就是把數據庫模型類 UserDO 直接暴露給了外部接口。

4.2.存在問題及解決方案

存在問題:

  • 間接暴露數據庫表格設計,給競爭對手競品分析帶來方便;
  • 如果數據庫查詢不做字段限制,會導致接口數據龐大,浪費用戶的寶貴流量;
  • 如果數據庫查詢不做字段限制,容易把敏感字段暴露給接口,導致出現數據的安全問題;
  • 如果數據庫模型類不能滿足接口需求,需要在數據庫模型類中添加別的字段,導致數據庫模型類跟數據庫字段不匹配問題;
  • 如果沒有維護好接口文檔,通過閱讀代碼是無法分辨出數據庫模型類中哪些字段是接口使用的,導致代碼的可維護性變差。

解決方案:

  • 從管理制度上要求數據庫和接口的模型類完全獨立;
  • 從項目結構上限制開發人員把數據庫模型類暴露給接口。

4.3.項目搭建的三種方式

下面,將介紹如何更科學地搭建 Java 項目,有效地限制開發人員把數據庫模型類暴露給接口。

第1種:共用模型的項目搭建

共用模型的項目搭建,把所有模型類放在一個模型項目(example-model)中,其它項目(example-repository、example-service、example-website)都依賴該模型項目,關係圖如下:


那些年,我們見過的 Java 服務端亂象



那些年,我們見過的 Java 服務端亂象

風險:表現層項目(example-webapp)可以調用業務層項目(example-service)中的任意服務函數,甚至於越過業務層直接調用持久層項目(example-repository)的DAO函數。

第2種:模型分離的項目搭建

模型分離的項目搭建,單獨搭建API項目(example-api),抽象出對外接口及其模型VO類。業務層項目(example-service)實現了這些接口,並向表現層項目(example-webapp)提供服務。表現層項目(example-webapp)只調用API項目(example-api)定義的服務接口。


那些年,我們見過的 Java 服務端亂象



那些年,我們見過的 Java 服務端亂象

風險:表現層項目(example-webapp)仍然可以調用業務層項目(example-service)提供的內部服務函數和持久層項目(example-repository)的DAO函數。為了避免這種情況,只好管理制度上要求表現層項目(example-webapp)只能調用API項目(example-api)定義的服務接口函數。

第3種:服務化的項目搭建

服務化的項目搭,就是把業務層項目(example-service)和持久層項目(example-repository)通過 Dubbo 項目(example-dubbo)打包成一個服務,向業務層項目(example-webapp)或其它業務項目(other-service)提供API項目(example-api)中定義的接口函數。


那些年,我們見過的 Java 服務端亂象



那些年,我們見過的 Java 服務端亂象


說明:Dubbo 項目(example-dubbo)只發布 API 項目(example-api)中定義的服務接口,保證了數據庫模型無法暴露。業務層項目(example-webapp)或其它業務項目(other-service)只依賴了 API 項目(example-api),只能調用該項目中定義的服務接口。

4.4.一條不太建議的建議

有人會問:接口模型和持久層模型分離,接口定義了一個查詢數據模型VO類,持久層也需要定義一個查詢數據模型DO類;接口定義了一個返回數據模型VO類,持久層也需要定義一個返回數據模型DO類……這樣,對於項目早期快速迭代開發非常不利。能不能只讓接口不暴露持久層數據模型,而能夠讓持久層使用接口的數據模型?

如果從SpringMVC服務端三層架構來說,這是不允許的,因為它會影響三層架構的獨立性。但是,如果從快速迭代開發來說,這是允許的,因為它並不會暴露數據庫模型類。所以,這是一條不太建議的建議。

\t/** 用戶DAO類 */
@Repository
public class UserDAO {
/** 統計用戶函數 */
public Long countByParameter(QueryUserParameterVO parameter) {...}
/** 查詢用戶函數 */
public List<UserVO> queryByParameter(QueryUserParameterVO parameter) {...}
}
/** 用戶服務類 */
@Service
public class UserService {
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/** 查詢用戶函數 */
public PageData<UserVO> queryUser(QueryUserParameterVO parameter) {
Long totalCount = userDAO.countByParameter(parameter);
List<UserVO> userList = null;
if (Objects.nonNull(totalCount) && totalCount.compareTo(0L) > 0) {
userList = userDAO.queryByParameter(parameter);
}
return new PageData<>(totalCount, userList);
}
}
/** 用戶控制器類 */
@Controller
@RequestMapping("/user")
public class UserController {
/** 用戶服務 */
@Autowired
private UserService userService;
/** 查詢用戶函數(parameter中包括分頁參數startIndex和pageSize) */
@RequestMapping(path = "/queryUser", method = RequestMethod.POST)
public Result<PageData<UserVO>> queryUser(@Valid @RequestBody QueryUserParameterVO parameter) {
PageData<UserVO> pageData = userService.queryUser(parameter);
return Result.success(pageData);
}
}


後記

“仁者見仁、智者見智”,每個人都有自己的想法,而文章的內容也只是我的一家之言。

謹以此文獻給那些我工作過的創業公司,是您們曾經放手讓我去整改亂象,讓我從中受益頗深並得以技術成長。

"

相關推薦

推薦中...