SpringBoot後端系統的基礎架構

週週zzz發表於2020-06-12

前言

前段時間完成了畢業設計課題——《基於Spring Boot + Vue的直播後臺管理系統》,專案名為LBMS,主要完成了對直播平臺資料的視覺化展示和分級的許可權管理。雖然相當順利地通過了答辯,但是由於時間以及本人水平的不足,其實後端系統的程式碼還僅僅停留在“能跑就行”。因此這篇文章主要也是為了反思一下專案中亟待完善的地方,我後續也會考慮在此基礎上編寫一個後端管理系統的通用架構模板。

2020/6/10 這個模板專案已經在做了:common-MS
2020/6/12 完成了日誌處理、異常處理、結果封裝、引數校驗模組

日誌處理

日誌框架

Java中可用的日誌框架有很多,並且通常都有著抽象層+實現層的結構,在實際應用中,只需要考慮抽象層提供的功能介面而不用瞭解實現層的具體結構。Spring Boot預設的日誌框架為Slf4j + logback。在我的畢設專案中,雖然引入了日誌框架,但是卻很少使用。

Slf4j的輸出級別有5種:trace、debug、info、warn、error,可以通過在properties或yml檔案中通過logging.level.root引數指定日誌輸出的級別,其中root代表配置對整個專案生效,可以修改為其他路徑進行自定義配置

日誌程式碼的簡化

使用lombok可以簡化程式碼的編寫:

Logger logger = LoggerFactory.getLogger(MyLog.class);
logger.info("logger info test");
@Slf4j
// ...
log.info("lombok info test")

對於日誌資訊中的變數,建議使用佔位符形式而非字串拼接

log.info(time + " " + methodName + "is invoked");
log.info("{} {} is invoked", time, methodName)

將日誌輸出到檔案

這裡用了某位大牛寫的logback-spring.xml進行配置(可以訪問我的Github獲取具體檔案),配置完成後可以將日誌按級別的不同輸出到指定目錄下的不同檔案,並且對每天的日誌分開儲存,日誌檔案大小超過100MB時,還可以自動分塊。

基於AOP的日誌處理

之前用DRF做一個專案時,發現它很貼心地在控制檯展示了每個請求的引數、返回狀態碼等資訊,SpringBoot當然也可以實現類似的功能。

想要實現上述需求,毫無疑問要在Controller層使用AOP了。對每個請求,我想要輸出對應的URL、請求方法、引數、返回狀態碼等資訊。

AOP的切點切面:

@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:

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;
}

請求引數:

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;
}
private String getResponseMessage(JoinPoint joinPoint, int status) {
    return this.getBaseMessage(joinPoint) + " " + status;
}

最終效果:

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示例:

{
  "timestamp": "2020-06-12T15:44:02.106+08:00",
  "status": 200,
  "message": "success",
  "data": 123,
  "path": "/result"
}

ResultSet示例:

{
  "timestamp": "2020-06-12T15:38:01.130+08:00",
  "total": 2,
  "status": 200,
  "message": "success",
  "list": [
    {
      "username": "Alice",
      "age": 20,
      "sex": 0,
      "email": "12345@qq.com"
    },
    {
      "username": "Eric",
      "age": 21,
      "sex": 1,
      "email": "12345@163.com"
    }
  ],
  "path": "/result/set"
}

結果封裝還要考慮的一個問題是對異常的處理,這個我在異常處理章節會談到。

引數校驗

上一個專案中的引數校驗做的相當有限,目前Spring Boot主流的引數校驗方式有hibernate-validator、Assert等。使用validator引數校驗的位置可以在實體類欄位處,也可以在Controller傳參處。

網上大部分文章說spring-boot-starter-web已經包含了hibernate-validator,但我不知道為什麼無法直接使用@NotNull等註解,因此手動引入validator:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.5.Final</version>
</dependency>

一個簡單的例子:

@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進行校驗:

Assert.notNull(user.getUsername(), "使用者名稱不能為空");

validator校驗失敗時,會丟擲MethodArgumentNotValidException異常。

Assert校驗失敗時會丟擲IllegalArgumentException

實際應用中我們可以靈活使用這兩種校驗方式,並且可以通過ExceptionHandler對這些異常進行捕獲和統一處理。

異常處理

LBMS中,我的異常處理採用的是自定義異常+@ResponseStatus註解的方式,在特定的地方丟擲異常,交給ResponseStatusExceptionResolver去處理。

@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異常並進行處理的程式碼為:

@ExceptionHandler(value = { MethodArgumentNotValidException.class })
public Result<String> 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);
}

當提交的郵箱格式錯誤時返回:

{
  "timestamp": "2020-06-12T15:45:07.874+08:00",
  "status": 400,
  "message": "email郵箱格式錯誤",
  "data": null,
  "path": "/user"
}

同理,還可以對自定義的異常進行處理:

public class ExampleException extends Exception{

    public ExampleException() {super();}

    public ExampleException(String message) {
        super(message);
    }
}

使用時直接丟擲異常即可:

@RequestMapping(value = "exception", method = RequestMethod.GET)
public Result exampleException() throws ExampleException {
    throw new ExampleException("這是一個測試異常");
}

如果需要修改Response的狀態碼而不僅僅是使用自定義的status,可以@ExceptionHandler方法內引入並使用

response.setStatus(400);

待續~

todo:Shiro、分頁功能、Redis等。

完整程式碼移步Github:common-MS

相關文章