1. 程式人生 > >SpringBoot後端系統的基礎架構

SpringBoot後端系統的基礎架構

## 前言 前段時間完成了畢業設計課題——《基於Spring Boot + Vue的直播後臺管理系統》,專案名為LBMS,主要完成了對直播平臺數據的視覺化展示和分級的許可權管理。雖然相當順利地通過了答辯,但是由於時間以及本人水平的不足,其實後端系統的程式碼還僅僅停留在“能跑就行”。因此這篇文章主要也是為了反思一下專案中亟待完善的地方,我後續也會考慮在此基礎上編寫一個後端管理系統的通用架構模板。 2020/6/10 這個模板專案已經在做了:[common-MS](https://github.com/2511zzZ/common-MS) 2020/6/12 完成了日誌處理、異常處理、結果封裝、引數校驗模組 ## 日誌處理 **日誌框架** Java中可用的日誌框架有很多,並且通常都有著抽象層+實現層的結構,在實際應用中,只需要考慮抽象層提供的功能介面而不用瞭解實現層的具體結構。Spring Boot預設的日誌框架為Slf4j + logback。在我的畢設專案中,雖然引入了日誌框架,但是卻很少使用。 Slf4j的輸出級別有5種:trace、debug、info、warn、error,可以通過在properties或yml檔案中通過logging.level.root引數指定日誌輸出的級別,其中root代表配置對整個專案生效,可以修改為其他路徑進行自定義配置 **日誌程式碼的簡化** 使用lombok可以簡化程式碼的編寫: ```Java Logger logger = LoggerFactory.getLogger(MyLog.class); logger.info("logger info test"); ``` ```Java @Slf4j // ... log.info("lombok info test") ``` 對於日誌資訊中的變數,建議使用佔位符形式而非字串拼接 ```java log.info(time + " " + methodName + "is invoked"); ``` ```Java log.info("{} {} is invoked", time, methodName) ``` **將日誌輸出到檔案** 這裡用了某位大牛寫的logback-spring.xml進行配置(可以訪問我的[Github](https://github.com/2511zzZ/common-MS)獲取具體檔案),配置完成後可以將日誌按級別的不同輸出到指定目錄下的不同檔案,並且對每天的日誌分開儲存,日誌檔案大小超過100MB時,還可以自動分塊。 **基於AOP的日誌處理** 之前用DRF做一個專案時,發現它很貼心地在控制檯展示了每個請求的引數、返回狀態碼等資訊,SpringBoot當然也可以實現類似的功能。 想要實現上述需求,毫無疑問要在Controller層使用AOP了。對每個請求,我想要輸出對應的URL、請求方法、引數、返回狀態碼等資訊。 AOP的切點切面: ```Java @Pointcut("execution(* priv.zzz.controller..*.*(..))") public void controllerAspect() {} @Before("controllerAspect()") public void before(JoinPoint joinPoint){ log.info(getRequestMessage(joinPoint)); } @AfterReturning(pointcut = "controllerAspect()", returning = "returnValue") public void after(JoinPoint joinPoint, Object returnValue){ if (returnValue instanceof Result){ log.info(getResponseMessage(joinPoint, ((Result) returnValue).getStatus())); } if (returnValue instanceof ResultSet){ log.info(getResponseMessage(joinPoint, ((ResultSet) returnValue).getStatus())); } } ``` URL、rquestMethod: ```Java private String getBaseMessage(JoinPoint joinPoint) { HttpServletRequest request = ((ServletRequestAttributes)(Objects.requireNonNull(RequestContextHolder.getRequestAttributes()))).getRequest(); String url = request.getRequestURI(); String requestMethod = request.getMethod(); String datetime = DateFormatter.format(new Date()); return datetime + " " + url + " " + requestMethod; } ``` 請求引數: ```Java private String getRequestMessage(JoinPoint joinPoint) { MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature(); Object[] args = joinPoint.getArgs(); String[] parameters = methodSignature.getParameterNames(); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < Math.min(args.length, parameters.length); i++){ stringBuilder.append(parameters[i]).append(":").append(args[i]).append(" "); } String params = "{ "+stringBuilder.toString()+"}"; return this.getBaseMessage(joinPoint) + " " + params; } ``` ```Java private String getResponseMessage(JoinPoint joinPoint, int status) { return this.getBaseMessage(joinPoint) + " " + status; } ``` 最終效果: ```shell 2020-06-11 13:10:32 /log GET { name:test number:1 } 2020-06-11 13:10:32 /log GET 200 ``` ## 結果封裝 前後端分離的情況下前後端一般都是通過Json資料進行互動,使用`@RestController`註解可以將返回的物件轉為Json格式,在那之前,我們需要對返回的結果封裝為Result物件。Result中主要要包含的欄位有status、message和data,對於status和message,我使用列舉型別ResultCode進行封裝,其中包含SUCCESS、NOT_FOUND、UNAUTHORIZED等常見狀態碼。data要考慮返回的資料是否是一個列表,如果是列表,還需要實現分頁功能。 在LBMS中,我將這兩種結果集(單個物件和列表物件)封裝為同一個結果集,在新的模板專案中,我嘗試使用Result和ResultSet兩種結果集進行封裝。這樣做的好處是返回結果更加清晰,缺點是有些地方可能需要一些額外的處理,比如在日誌模組獲取controller返回的狀態碼時,具體的優劣有待更加深入的使用。 Result示例: ```js { "timestamp": "2020-06-12T15:44:02.106+08:00", "status": 200, "message": "success", "data": 123, "path": "/result" } ``` ResultSet示例: ```javascript { "timestamp": "2020-06-12T15:38:01.130+08:00", "total": 2, "status": 200, "message": "success", "list": [ { "username": "Alice", "age": 20, "sex": 0, "email": "[email protected]" }, { "username": "Eric", "age": 21, "sex": 1, "email": "[email protected]" } ], "path": "/result/set" } ``` 結果封裝還要考慮的一個問題是對異常的處理,這個我在**異常處理**章節會談到。 ## 引數校驗 上一個專案中的引數校驗做的相當有限,目前Spring Boot主流的引數校驗方式有hibernate-validator、Assert等。使用validator引數校驗的位置可以在實體類欄位處,也可以在Controller傳參處。 網上大部分文章說spring-boot-starter-web已經包含了hibernate-validator,但我不知道為什麼無法直接使用@NotNull等註解,因此手動引入validator: ```XML org.hibernate hibernate-validator 6.1.5.Final ``` 一個簡單的例子: ```Java @Data @AllArgsConstructor @NoArgsConstructor public class TestUser { @NotNull(message = "使用者名稱不能為空") @NotBlank(message = "使用者名稱不能為空") @Length(max = 20, message = "使用者名稱過長") private String username; @Min(0) private Integer age; @Range(min = 0, max = 1) private Integer sex; @Email(message = "郵箱格式錯誤") private String email; } ``` 使用Assert進行校驗: ```Java Assert.notNull(user.getUsername(), "使用者名稱不能為空"); ``` validator校驗失敗時,會丟擲`MethodArgumentNotValidException`異常。 Assert校驗失敗時會丟擲`IllegalArgumentException`。 實際應用中我們可以靈活使用這兩種校驗方式,並且可以通過ExceptionHandler對這些異常進行捕獲和統一處理。 ## 異常處理 LBMS中,我的異常處理採用的是自定義異常+@ResponseStatus註解的方式,在特定的地方丟擲異常,交給ResponseStatusExceptionResolver去處理。 ```Java @ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "無法識別的操作") public class BadOperationException extends Exception { public BadOperationException(){ super(); } public BadOperationException(String msg){ super(msg); } } ``` 在common-MS中,異常處理採用`@ControllerAdvice`+`@ExceptionHandler`實現,`@ControllerAdvice`將一個類標註為全域性的異常處理類,`@ExceptionHandler`用於捕獲不同的異常進行對應處理。同理,對於異常的返回結果也與正常返回結果格式保持一致,使用Result封裝。 例如,捕獲上述validator丟擲的`MethodArgumentNotValidException`異常並進行處理的程式碼為: ```Java @ExceptionHandler(value = { MethodArgumentNotValidException.class }) public Result validatorException(HttpServletResponse response, MethodArgumentNotValidException e) { // validator設定了message時返回message,未設定則返回“非法引數” FieldError error = e.getBindingResult().getFieldError(); String message = "非法引數"; if(error != null){ message = error.getField() + error.getDefaultMessage(); } response.setStatus(400); return Result.failure(400, message); } ``` 當提交的郵箱格式錯誤時返回: ```javascript { "timestamp": "2020-06-12T15:45:07.874+08:00", "status": 400, "message": "email郵箱格式錯誤", "data": null, "path": "/user" } ``` 同理,還可以對自定義的異常進行處理: ```Java public class ExampleException extends Exception{ public ExampleException() {super();} public ExampleException(String message) { super(message); } } ``` 使用時直接丟擲異常即可: ```Java @RequestMapping(value = "exception", method = RequestMethod.GET) public Result exampleException() throws ExampleException { throw new ExampleException("這是一個測試異常"); } ``` 如果需要修改Response的狀態碼而不僅僅是使用自定義的status,可以`@ExceptionHandler`方法內引入並使用 ```Java response.setStatus(400); ``` 待續~ todo:Shiro、分頁功能、Redis等。 完整程式碼移步Github:[common-MS](https://github.com/2511zzZ/co