前段时间完成了毕业设计课题——《基于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