SpringMVC體系下各組件的功能邊界及重構建議

来源:https://www.cnblogs.com/mcmoon/archive/2018/08/30/9560142.html
-Advertisement-
Play Games

最近在重構後端代碼,很多同學對Spring體系下的後端組件如Controller、Service、Repository、Component等認識不夠清晰,導致代碼里常常會出現Controller里直接使用RestTemplate、直接訪問資料庫的情況。下麵談談我對這些組件功能邊界的認識,一家之言,歡 ...


最近在重構後端代碼,很多同學對Spring體系下的後端組件如Controller、Service、Repository、Component等認識不夠清晰,導致代碼里常常會出現Controller里直接使用RestTemplate、直接訪問資料庫的情況。下麵談談我對這些組件功能邊界的認識,一家之言,歡迎討論。

1. Controller

Controller是整個後端服務的門面,他向外暴露了可用服務。你不關心Dispatcher、HandleMapping如何作用,但你肯定關心Controller中暴露的介面的HttpMethod、URL Path等。

官方對Controller的說明:

Indicates that an annotated class is a "Controller" (e.g. a web controller).

This annotation serves as a specialization of Component,
allowing for implementation classes to be autodetected through classpath scanning.
It is typically used in combination with annotated handler methods based on the annotation.

Component的一種、會被自動掃描、與RequestMapping合用--其實並沒涉及到Controller的功能邊界的說明。

雖然官方說明沒有涉及,但大量的最佳實踐還是告訴我們,Controller只做三件事:

  • 校驗輸入:PathVariable\RequestBody\RequestParam合法性校驗
  • 業務邏輯:Service層代碼調用,並且只調用單個service的單個方法(儘量一行代碼搞定),複雜的業務邏輯組裝需放在service中
  • 控制輸出:根據校驗、業務邏輯給出合適的response

1.1 輸入校驗

對於特殊的輸入可以用一個if搞定;對於通用輸入的校驗(如介面的授權校驗),可以通過自定義Filter或者自定義切麵完成。

自定義Filter示例:

 1 @Order(1)
 2 @WebFilter(filterName = "authorizationFilter", urlPatterns = "/*")
 3 public class AuthorizationFilter implements Filter
 4 {
 5     @Override
 6     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
 7             throws IOException, ServletException
 8     {
 9         HttpServletRequest req = (HttpServletRequest)request;
10         HttpServletResponse res = (HttpServletResponse)response;
11         String path = req.getServletPath();
12         if (!isWhiteList(path))
13         {
14             String token = req.getHeader(AUTHORIZATION_HEADER_NAME);
15             if (isValidate(token))
16             {
17                 res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
18                 return;
19             }
20         }
21         chain.doFilter(request, response);
22     }

 自定義切麵示例:

 1 @Target(ElementType.METHOD)
 2 @Retention(RetentionPolicy.RUNTIME)
 3 @Documented
 4 public @interface AuthInfo
 5 {
 6     String[] authId() default {"0001"};
 7 }
 8 
 9 @Aspect
10 @Component
11 public class AuthService
12 {
13     @Around("within(com.company.product..*) && @annotation(authInfo)")
14     public Object aroundMethod(ProceedingJoinPoint joinPoint, AuthInfo authInfo) throws Throwable
15     {
16         String[] token = authInfo.authId();
17         if (isValidate(token))
18         {
19             return joinPoint.proceed();
20         }
21         HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getResponse();
22         response.setStatus(401);
23         return null;
24     }
25 
26     private boolean isValidate(String[] tokenArray)
27     {
28         return true;
29     }
30 }
31 
32 @RestController
33 public class TestController
34 {
35     @GetMapping("/test/{userId}")
36     @AuthInfo   //通用校驗
37     public String test(HttpServletRequest req, @PathVariable(value = "userId") String userId)
38     {
39         if(!isValidateUser(userId)){   //個別校驗
40             throw new MyException("Illegal userId");
41         }
42         ... ...
43     }
44 }

以上兩種都屬於AOP的應用,如果不希望Controller內包含了大量的if校驗,可以考慮用上述兩種方法抽出來。推薦使用Filter,自定義切麵會造成額外的負擔。

1.2 業務邏輯

輸入校驗完成後,到了真正處理業務邏輯的地方,推薦的做法是一行代碼搞定。

 1 @RestController
 2 public class TestController
 3 {
 4     @Autowired
 5     TestService testService;
 6 
 7     @GetMapping("/test/{userId}")
 8     @AuthInfo(authId = {"token"})
 9     public ResponseEntity test(HttpServletRequest req, @PathVariable(value = "userId") String userId)
10     {
11         if (!isValidateUser(userId))
12         {
13             throw new MyException("Illegal userId");
14         }
15         Object result = testService.getResult(userId);
16         return ResponseEntity.ok(result);
17     }
18 }

有人會問:我的實際業務邏輯中需要調用多個service怎麼辦?我的意見是,controller中不要涉及業務邏輯組裝,組裝的工作應該新建一個Service,在這個Service中完成。

1.3 控制輸出

 在上面的示例中已經涉及到了一些輸出控制:自定義ResponseEntity和拋出異常。這兩種方法可以靈活運用,自定義返回比較直接,可以很直接的返回status和消息體。

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(message);

而拋出異常則需要與ControllerAdvice相配合:在Controller中拋出的異常可被ControllerAdvice捕獲,並根據異常的內容和種類,定製不同的返回。

1 @ControllerAdvice
 2 public class MyExceptionHandler
 3 {
 4     private static Logger logger = LoggerFactory.getLogger(MyExceptionHandler.class);
 5 
 6     @ExceptionHandler(MyException.class)
 7     public ResponseEntity<ExceptionResponse> handleMyException(HttpServletRequest request, MyException ex)
 8     {
 9         String message = String.format("Request to %s failed, detail: %s", getUrl(request), ex.getMessage());
10         logger.error(message);
11         HttpStatus status = getHttpStatus(ex);
12         if (ExceptionCode.PARAM_CHECK_ERROR.equals(ex.getCode()))
13         {
14             status = HttpStatus.BAD_REQUEST;
15         }
16         return generateErrorResponse(status, getMessageDetail(ex));
17     }
18 
19     @ExceptionHandler(JsonMappingException.class)
20     public ResponseEntity<ExceptionResponse> handleJsonMappingException(HttpServletRequest request, Exception ex)
21     {
22         String message = String.format("Parse response failed, url: %s, detail: %s", getUrl(request), ex.getMessage());
23         logger.error(message, ex);
24         return generateErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, getMessageDetail(ex));
25     }
26 
27     @ExceptionHandler(Exception.class)
28     public ResponseEntity<ExceptionResponse> handleException(HttpServletRequest request, Exception ex)
29     {
30         String message = String.format("Request to %s failed, detail: %s", getUrl(request), ex.getMessage());
31         logger.error(message, ex);
32         return generateErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, getMessageDetail(ex));
33     }
34 
35     private ResponseEntity<ExceptionResponse> generateErrorResponse(HttpStatus httpStatus, String message)
36     {
37         ExceptionResponse response = new ExceptionResponse();
38         response.setCode(String.valueOf(httpStatus.value()));
39         response.setMessage(message);
40         return ResponseEntity.status(httpStatus).body(response);
41     }
42 }

ControllerAdvice需要自定義異常MyException和自定義返回ExceptionResponse配合,定製自由度比較大,各微服務之間統一格式即可。

在SpringBoot應用里,ControllerAdvice是必備的,主要原因:

  • RestTemplate大量使用,RestTemplate預設的ResponseErrorHandler中,非2XX的返回一律拋出異常
  • Service或其他組件中拋出的RuntimeException易被忽略;
  • 異常返回統一在ControllerAdvice中定製,避免各個程式猿在各自的Controller中返回千奇百怪的Response。

 2.Service

Service是真正的業務邏輯層,這一層的功能邊界:

  • 基於單一職責的原則,每一個Service只處理單一事務;
  • 如果某個業務需要調用多個業務事務,建議在Service上再擴展一層,專門用於組裝各個Service的調用;
  • Service層不做任何形式的持久化工作:資料庫訪問、遠程調用等。

3.Repository

微服務不贊同任何形式的狀態如緩存,在多實例下,存在於各自JVM中的緩存由於互相不感知,可能會造成多實例之間的溝通問題。這就是為什麼Eureka核心功能只是個RestTemplate的Inteceptor,缺花費了大力氣做實例間的緩存同步的原因。

持久層Repository的功能是花樣百出的持久化:

  • 資料庫訪問
  • 本地文件
  • HTTP調用
  • ... ...

可以看出,Repository層做的工作實際上是對網路上各種資源的訪問。

4.Component

Controller、Service、Repository都是繼承自Component,當你實在不好註解你的類但又希望Spring上下文去管理它時,可以暫時將其註解為Component。

個人認為出現這種尷尬問題的主要原因是因為類的功能不夠單一,只要能夠拆分重構,是可以確切的找到合適的註解的。

5.Resource

將Resource列舉在此實際是不合適的,因為Resource是JDK的註解,但使用時確實易與其他幾個註解造成混淆。

Resouce的使用場景時這樣的:

你在微服務中將User信息持久化在MySQL中,並依此寫了一個UserMySQLRepository去進行交互;

但是boss突然覺得MySQL一點也不好,希望你改成Redis的同時,保持對MySQL的支持以免有問題時能夠回退。

這樣你的微服務中就有了兩個IUserRepository的實現類:UserMySQLRepository和UserRedisRepository。

在Service中如何調用它呢,如果還是使用以前的代碼調用:

@Autowired
IUserRepository userRepo;

這樣UserMySQLRepository和UserRedisRepository是要打架的:我也是IUserRepository,憑什麼你上?

如果你這樣調用:

@Autowired
UserMySQLRepository userMSRepo;

@Autowired
UserRedisRepository userRedisRepo;

代碼的擴展性被破壞的一干二凈:你的方法中必須用額外的代碼去判斷使用哪個repository;萬一哪天boss覺得redis又不好了,難道再加一個Autowired?

這時候Resource可以閃亮登場了,最佳的實踐如下:

@Repository("mysql")
public class UserMySQLRepository implements IUserRepository
{}

@Repository("redis")
public class UserRedisRepository implements IUserRepository
{}

@Service
public class UserService implements IUserService
{
    @Resource(name = "${user.persistence.type}")
    private IUserRepository userRepo;
    ... ...
}

在application.properties中,可以添加一個配置去控制持久層到底使用MySQL還是Redis。

user.persistence.type=redis
#user.persistence.type=mysql

如果想切回MySQL,只要將user.persistence.type的值改回mysql即可。

至於Resource可以做到而Autowired做不到的原因,網上也有很多解釋,做簡單說明:

  • Resource優先按照名稱(註解中的value:mysql和redis)裝配註入,也支持按照類型
  • Autowired按照類型(class名)裝配註入

這篇文章也是想到哪寫到哪,不符合單一職責,有時間重構,裡面的很多點可以單獨成文。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • fastjson這一工具包幫助我們進行java對象和json格式的字元串之間的相互轉換。對象到字元串的過程,我們稱之為序列化;反之,我們稱為反序列化。 現在我們就來談談fastjson提供的反序列化方法,本篇只討論按照指定的位元組碼返回相應對象的的反序列化方法,該方法有多種重載形式,按照重疊構造的模式 ...
  • VB6畢竟是很老的產品了,它的代碼編輯器垂直滾動條並不能隨滑鼠的滾輪而滾動,這個問題會讓我們在編寫代碼的時候覺得很不方便,不過還是有一種方法可以解決這個問題的。 先下載一個微軟發佈的“VB6IDEMouseWheelAddin.dll”文件(此文件已經上傳到百度網盤,網址:http://pan.ba ...
  • 今日內容介紹 1、Map介面 2、模擬鬥地主洗牌發牌 01Map集合概述 A:Map集合概述: 我們通過查看Map介面描述,發現Map介面下的集合與Collection介面下的集合,它們存儲數據的形式不同  a:Collection中的集合,元素是孤立存在的(理解為單身),向集合中存儲元素採用一個 ...
  • 講解微擎安裝使用及插件模塊的安裝,解決下載插件模塊後不知道怎麼使用的情況。以及安裝失敗,忘記密碼的解決方法 ...
  • 和諧數組是指一個數組裡元素的最大值和最小值之間的差別正好是1。 現在,給定一個整數數組,你需要在所有可能的子序列中找到最長的和諧子序列的長度。 示例 1: 輸入: [1,3,2,2,5,2,3,7] 輸出: 5 原因: 最長的和諧數組是:[3,2,2,2,3]. 說明: 輸入的數組長度最大不超過20 ...
  • 一、java基礎:1、抽象類與介面的區別:2、set集合和map集合在除去重覆時,分別調用的是哪種方法?結果是否相同?3、把D:\\java文件夾中內容複製到E:\\中4、sleep()和wait()的區別5、線程的關閉方式有幾種二、web基礎1、servlet為什麼被設計成單例多線程。2、jsp的 ...
  • 註:大家如果沒有VB6.0的安裝文件,可自行百度一下下載,一般文件大小在200M左右的均為完整版的軟體,可以使用。 特別提示:安裝此軟體的時候最好退出360殺毒軟體(包括360安全衛士,電腦管家等,如果電腦上有這些軟體的話),因為現如今的360殺毒軟體直接會對VB6.0軟體誤報,這樣的話就可能會在安 ...
  • 題目:平安果 題目介紹:給出一個m*n的格子,每個格子里有一定數量的平安果,現在要求從左上角頂點(1,1)出發,每次走一格並拿走那一格的所有平安果,且只能向下或向右前進,最終到達右下角頂點(m,n),要求求出能拿走的平安果的最大數值。 輸入:第一行有兩個數值m,n,然後是m行n列數值。 輸出:一個數 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...