基於springboot搭建的web系統架構

基於springboot搭建的web系統架構
從接觸springboot開始,便深深的被它的簡潔性深深的折服了,精簡的配置,方便的集成,使我再也不想用傳統的ssm框架來搭建項目,一大堆的配置文件,維護起來很不方便,集成的時候也要費力不少。從第一次使用springboot開始,一個簡單的main方法,甚至一個配置文件也不需要(當然我是指的沒有任何數據交互,沒有任何組件集成的情況),就可以把一個web項目啓動起來,下面總結一下自從使用springboot依賴,慢慢完善的自己的一個web系統的架構,肯定不是最好的,但平時自己用着很舒服。

  1. 配置信息放到數據庫裏邊
      個人比較不喜歡配置文件,因此有一個原則,配置文件能不用就不用,配置信息能少些就少些,配置內容能用代碼寫堅決不用xml,因此我第一個想到的就是,能不能把springboot的配置信息寫到數據庫裏,在springboot啓動的時候自動去加載,而在application.properties裏邊只寫一個數據源。最終找到了方法:

  注意圖中箭頭指向的兩行,構造了一個properties對象,然後將這個對象放到了springboot的啓動對象application中,properties是一個類似map的key-value容器,springboot可以將其中的東西當做成原來application.properties中的內容一樣,因此在properties對象的內容也就相當於寫在了application.properties文件中。知道了這個之後就簡單了,我們將原本需要寫在application.properties中的所有配置信息寫在數據庫中,在springboot啓動的時候從數據庫中讀取出來放到properties對象中,然後再將這個對象set到application中即可。上圖中PropertyConfig.loadProperties()方法就是進行了這樣的操作,代碼如下:

PropertyConfig.java
  代碼中,首先使用古老的jdbc技術,讀取數據庫t_config表,將表中的key-value加載到properties中,代碼中profile是爲了區分開發環境和生產環境,以便於確定從那張表中加載配置文件,數據庫中的配置信息如下:

  這樣以後,application.properties中就不用再寫很多的配置信息,而且,如果將這些配置信息放到數據庫中之後,如果起多個應用可是公用這一張表,這樣也可以做到配置信息的公用的效果,這樣修改以後,配置文件中就只有數據源的信息了:

  profile代表使用哪個環境,代碼中可以根據這個信息來從開發表中加載配置信息還是從生產表中加載配置信息。

  1. 統一返回結果
      一般web項目中,大多數都是接口,以返回json數據爲主,因此統一一個返回格式很必要。在本示例中,建了一個BaseController,所有的Controller都需要繼承這個類,在這個BaseController中定義了成功的返回和失敗的返回,在其他業務的Controller中,返回的時候,只需要return super.success(xxx)或者return super.fail(xxx, xxx)即可,例:

  說到這裏,返回給前臺的狀態碼,建議也是封裝成一個枚舉類型,不建議直接返回200、400之類的,不方便維護也不方便查詢。那麼BaseController裏做了什麼呢?如下:

  定義一個ResultInfo類,該類只有兩個屬性,一個是Integer類型的狀態碼,一個是泛型,用於成功時返回給前臺的數據,和失敗時返回給前臺的提示信息。

  1. 統一異常捕獲
      在上一步中的Controller代碼中看到拋出了一個自定義的異常,在Controller中,屬於最外層的代碼了,這個時候如果有異常就不能直接拋出去了,這裏再拋出去就沒有人處理了,服務器只能返回給前臺一個錯誤,用戶體驗不好。因此,建議所有的Controller代碼都用try-catch包裹,捕獲到異常後統一進行處理,然後再給前臺一個合理的提示信息。在上一步中拋出了一個自定義異常:

throw new MyException(ResultEnum.DELETE_ERROR.getCode(), "刪除員工出錯,請聯繫網站管理人員。", e);
  該自定義異常有三個屬性,分別是異常狀態碼,異常提示信息,以及捕獲到的異常對象,接下來定義一個全局的異常捕獲,統一對異常進行處理:

複製代碼
1 @Slf4j
2 @ResponseBody
3 @ControllerAdvice
4 public class GlobalExceptionHandle {
5
6 /**
7 * 處理捕獲的異常
8 */
9 @ExceptionHandler(value = Exception.class)
10 public Object handleException(Exception e, HttpServletRequest request, HttpServletResponse resp) throws IOException {
11 log.error(AppConst.ERROR_LOG_PREFIX + "請求地址:" + request.getRequestURL().toString());
12 log.error(AppConst.ERROR_LOG_PREFIX + "請求方法:" + request.getMethod());
13 log.error(AppConst.ERROR_LOG_PREFIX + "請求者IP:" + request.getRemoteAddr());
14 log.error(AppConst.ERROR_LOG_PREFIX + "請求參數:" + ParametersUtils.getParameters(request));
15 if (e instanceof MyException) {
16 MyException myException = (MyException) e;
17 log.error(AppConst.ERROR_LOG_PREFIX + myException.getMsg(), myException.getE());
18 if (myException.getCode().equals(ResultEnum.SEARCH_PAGE_ERROR.getCode())) {
19 JSONObject result = new JSONObject();
20 result.put("code", myException.getCode());
21 result.put("msg", myException.getMsg());
22 return result;
23 } else if (myException.getCode().equals(ResultEnum.ERROR_PAGE.getCode())) {
24 resp.sendRedirect("/err");
25 return "";
26 } else {
27 return new ResultInfo<>(myException.getCode(), myException.getMsg());
28 }
29 } else if (e instanceof UnauthorizedException) {
30 resp.sendRedirect("/noauth");
31 return "";
32 } else {
33 log.error(AppConst.ERROR_LOG_PREFIX + "錯誤信息:", e);
34 }
35 resp.sendRedirect("/err");
36 return "";
37 }
38
39 }
複製代碼
  統一捕獲異常之後,可以進行相應的處理,我這裏沒有進行特殊的處理,只是進行了一下區分,獲取數據的接口拋出的異常,前臺肯定是使用的ajax請求,因此返回前臺一個json格式的信息,提示出錯誤內容。如果是跳轉頁面拋出的異常,類似404之類的,直接跳轉到自定義的404頁面。補充一點,springboot項目默認是有/error路由的,返回的就是error頁面,所以,如果你在你的項目中定義一個error.html的頁面,如果報404錯誤,會自動跳轉到該頁面。
  補充,統一異常處理類中使用了一個註解@Slf4j,該註解是lombok包中的,項目中加入了該依賴後,再也不用寫繁瑣的get、set等代碼,當然類似的像上邊的聲明log對象的代碼也不用寫了:

  1. 日誌配置文件區分環境
      本示例使用的是logback日誌框架。需要在resources目錄中添加logback.xml配置文件,這是一個比較頭疼的地方,我本來想一個配置文件也沒有的,奈何我也不知道怎麼講這個日誌的配置文件放到數據庫中,所以暫時先這麼着了,好在幾乎沒有需要改動它的時候。

  我在項目中添加了兩個日誌的配置文件,分別是logback-dev.xml和logback-pro.xml可以根據不同的環境決定使用哪個配置文件,在數據庫配置表中(相當於寫在了application.properties中)添加一條配置logging.config=classpath:logback-dev.xml來區分使用哪個文件作爲日誌的配置文件,配置文件內容如下:

logback.xml
  在配置文件中,定義了三個級別的日誌,info、debug和error分別輸出到三個文件中,便於查看。在生成日誌文件的時候,進行了按照日誌進行拆分的配置,每一個級別的日誌每一天都會重新生成一個,根據日期進行命名,超過180天的日誌將自動會刪除。當然你還可以按照日誌大小進行拆分,我這裏沒有進行這項的配置。

  1. 全局接口請求記錄
      進行全局的接口請求記錄,可以記錄接口的別調用情況,然後進行一些統計和分析,在本示例中,只是將全局的接口調用情況記錄到了info日誌中,沒有進行相應的分析操作:

複製代碼
1 @Slf4j
2 @Aspect
3 @Component
4 public class WebLogAspect {
5
6 @Pointcut("execution(public com.oven.controller..*(..))")
7 public void webLog() {
8 }
9
10 @Before("webLog()")
11 public void doBefore() {
12 // 獲取請求
13 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
14 @SuppressWarnings("ConstantConditions") HttpServletRequest request = attributes.getRequest();
15 // 記錄請求內容
16 log.info(AppConst.INFO_LOG_PREFIX + "請求地址:" + request.getRequestURL().toString());
17 log.info(AppConst.INFO_LOG_PREFIX + "請求方法:" + request.getMethod());
18 log.info(AppConst.INFO_LOG_PREFIX + "請求者IP:" + request.getRemoteAddr());
19 log.info(AppConst.INFO_LOG_PREFIX + "請求參數:" + ParametersUtils.getParameters(request));
20 }
21
22 @AfterReturning(returning = "ret", pointcut = "webLog()")
23 public void doAfterReturning(Object ret) {
24 // 請求返回的內容
25 if (ret instanceof ResultInfo) {
26 log.info(AppConst.INFO_LOG_PREFIX + "返回結果:" + ((ResultInfo) ret).getCode().toString());
27 }
28 }
29
30 }
複製代碼

  1. 集成shiro實現權限校驗
      集成shirl,輕鬆的實現了權限的管理,如果對shiro不熟悉朋友,還需要先把shiro入門一下才好,shiro的集成一般都需要自定義一個realm,來進行身份認證和授權,因此先來一個自定義realm:

MyShiroRealm.java
  自定義完realm後需要一個配置文件但自定義的realm配置到shiro裏:

ShiroConfig.java
  身份認證如果簡單的理解的話,你可以理解爲登錄的過程。授權就是授予你權利,代表你在這個系統中有權限做什麼動作,具體shiro的內容小夥伴們自行去學習吧。

  1. 登錄校驗,安全攔截
      在集成了shiro之後,登錄操作就需要使用到自定義的realm了,具體的登錄代碼如下:

doLogin
  身份認證的操作交給了shiro,利用用戶名和密碼構造一個身份的令牌,調用shiro的login方法,這個時候就會進入自定義reaml的身份認證方法中,也就是上一步中的doGetAuthenticationInfo方法,具體的認證操作看上一步的代碼,無非就是賬號密碼的校驗等。身份認證的時候,通過拋出異常的方式給登錄操作返回信息,從而在登錄方法中判斷身份認證失敗後的信息,從而返回給前臺進行提示。
  在身份認證通過後,拿到當前登錄用戶的信息,首先放到session中,便於後續的使用。其次在放到application對象中,防止同一個賬號的多次登錄。
  有了身份任何和授權自然就少不了安全校驗,在本示例中使用了一個攔截器來實現安全校驗的工作:

SecurityInterceptor.java
  在攔截器中,首先對一些不需要校驗的請求進行放行,例如登錄動作、登錄頁面請求以及錯誤頁面等。然後獲取當前登錄的用戶,如果沒有登錄則自動跳轉到登錄頁面。在返回前臺的時候,判斷請求屬於同步請求還是異步請求,如果是同步請求,直接進行頁面的跳轉,跳轉到登錄頁面。如果是異步請求,則返回前臺一個json數據,提示前臺登錄信息失效。這裏補充一點,前臺可以使用ajaxhook進行異步請求的捕獲,相當於一個前端的全局攔截器,攔截所有的異步請求,可以監視所有異步請求的返回結果,如果返回的是登錄失效,則進行跳轉到登錄頁面的操作。具體ajaxhook的使用方法請自行學習,本示例中暫時沒有使用。
  下面是判斷同一個賬號有沒有多次登錄,具體方法就是使用當前的sessionId,將當前登錄用戶和請求sissionId作爲一個key-value放到了application中,如果該用戶的sessionId發生了變化,說明又有一個人登錄了該賬號,然後就進行相應的提示操作。

  1. 配置虛擬路徑
      web項目中免不了並上傳的操作,圖片或者文件,如果上傳的是圖片,一般還要進行回顯的操作,我們不想將上傳的文件直接存放在項目的目錄中,而是放在一個自定義的目錄,同時項目還可以訪問:

  這樣在進行上傳操作的時候,就可以將上傳的文件放到項目以外的目錄中,然後外部訪問的時候,通過虛擬路徑進行映射訪問。

  1. 集成redis緩存
      springboot的強悍就是集成一個東西太方便了,如果你不想做任何配置,只需要加入redis的依賴,然後在配置文件(本示例中配置是在數據庫中)中添加redis的鏈接信息,就可以在項目中使用redis了。

  本示例中使用redis做緩存,首先寫了一個緩存的類,代碼有些長不做展示。然後在service層進行緩存的操作:

  代碼中使用了double check的騷操作,防止高併發下緩存失效的問題(雖然我的示例不可能有高併發,哈哈)。另外就是緩存更新的問題,網上說的有很多,先更新數據再更新緩存,先更新緩存再更新數據庫等等,具體要看你是做什麼,本示例中沒有什麼需要特殊注意的地方,因此就先更新數據庫,然後再移除緩存:

  1. 項目代碼和依賴以及靜態資源分別打包
      之前遇到一個問題,springboot打包之後是一個jar文件,如果將所有依賴也打到這個jar包中的話,那麼這個jar包動輒幾十兆,來回傳輸不說,如果想改動其中的一個配置內容,還異常的繁瑣,因此,將項目代碼,就是自己寫的代碼打成一個jar包(一般只有幾百k),然後將所有的依賴打包到一個lib目錄,然後再將所有的配置信息以及靜態文件打包到resources目錄,這樣,靜態文件可以直接進行修改,瀏覽器清理緩存刷新即可出現改動效果,而且打包出來的項目代碼也小了很多,至於依賴,一般都是不變的,所以也沒必要每次都打包它。具體操作就是在pom.xml中增加一個插件即可,代碼如下:

代碼太長,不做展示

  1. 項目啓動
      到現在都沒有貼一個項目的目錄結構,先來一張。目錄中項目跟目錄下的demo.sh就是啓動腳本,當時從網上抄襲改裝過來的,源代碼出自那位大師之手我就不知道了,先行謝過。在部署到服務器的時候,如果服務器上安裝好了jdk、maven、git,每次修改完代碼,直接git pull下來,然後mvn package打包,然後直接./demo.sh start就可以啓動項目,方便快速。慢着,忘記了,如果你提交到github中的application.properties中的數據源配置信息是開發環境的話,那麼你在打包之後,target/resources中的application.properties中的數據源需要改成開發環境纔可以啓動。當然如果你嫌麻煩,可以直接將開發環境的數據源配置push到github中,安不安全就要你自己考慮了。
  1. 總結
    示例中可能還有一些細節沒有說到,總之這個項目是慢慢的添磚添瓦弄出來的,自己在寫很多其他的項目的時候,都是以此項目爲模板進行改造出來的,個人感覺很實用很方便,用着也很舒服。github地址:https://github.com/503612012/demo歡迎收藏。

作者:Oven
個人網站:http://www.vhzsqm.com

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章