前段時間完成了畢業設計課題——《基於Spring Boot + Vue的直播後臺管理系統》,項目名爲LBMS,主要完成了對直播平臺數據的可視化展現和分級的權限管理。雖然至關順利地經過了答辯,可是因爲時間以及本人水平的不足,其實後端系統的代碼還僅僅停留在「能跑就行」。所以這篇文章主要也是爲了反思一下項目中亟待完善的地方,我後續也會考慮在此基礎上編寫一個後端管理系統的通用架構模板。javascript
2020/6/10 這個模板項目已經在作了:common-MS
2020/6/12 完成了日誌處理、異常處理、結果封裝、參數校驗模塊java
日誌框架git
Java中可用的日誌框架有不少,而且一般都有着抽象層+實現層的結構,在實際應用中,只須要考慮抽象層提供的功能接口而不用瞭解實現層的具體結構。Spring Boot默認的日誌框架爲Slf4j + logback。在個人畢設項目中,雖然引入了日誌框架,可是卻不多使用。github
Slf4j的輸出級別有5種:trace、debug、info、warn、error,能夠經過在properties或yml文件中經過logging.level.root參數指定日誌輸出的級別,其中root表明配置對整個項目生效,能夠修改成其餘路徑進行自定義配置web
日誌代碼的簡化spring
使用lombok能夠簡化代碼的編寫:shell
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時,還能夠自動分塊。app
基於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