前言
前段時間完成了畢業設計課題——《基於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