SpringBoot后端系统的基础架构
前言
前段时间完成了毕业设计课题——《基于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": 3,
"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
SpringBoot后端系统的基础架构的更多相关文章
- 用c#开发微信 (11) 微统计 - 阅读分享统计系统 1 基础架构搭建
微信平台自带的统计功能太简单,有时我们需要统计有哪些微信个人用户阅读.分享了微信公众号的手机网页,以及微信个人用户访问手机网页的来源:朋友圈分享访问.好友分享消息访问等.本系统实现了手机网页阅读.分享 ...
- FastAPI(六十五)实战开发《在线课程学习系统》基础架构的搭建
在之前三篇,我们分享的就是需求的分析,基本接口的整理,数据库链接的配置.这次我们分享项目的基本框架,目录结构如下: common目录 通用的目录,一些通用的处理放在这里 models目录 数据库相关的 ...
- IT基础架构规划方案三(IT基础软件和系统规划)
IT基础软件和系统规划 操作系统选型规划方案 根据对某集团的实际调研,获取了企业业务应用系统的建设情况,随着企业信息化建设的推进,需要对各种信息化管理系统和应用系统的服务器选型进行选型规划,根据不同的 ...
- 基于SpringBoot搭建应用开发框架(一) —— 基础架构
目录 Spring的简史 零.开发技术简介 一.创建项目 1.创建工程 2.创建Starter 3.启动项目 4.Spring Boot 配置 5.项目结构划分 二.基础结构功能 1.web支持 2. ...
- 使用Spring Boot搭建应用开发框架(一) —— 基础架构
Spring的简史 第一阶段:XML配置,在Spring1.x时代,使用Spring开发满眼都是xml配置的Bean,随着项目的扩大,我们需要把xml配置文件分放到不同的配置文件里,那时候需要频繁的在 ...
- 面向服务体系架构(SOA)和数据仓库(DW)的思考基于 IBM 产品体系搭建基于 SOA 和 DW 的企业基础架构平台
面向服务体系架构(SOA)和数据仓库(DW)的思考 基于 IBM 产品体系搭建基于 SOA 和 DW 的企业基础架构平台 当前业界对面向服务体系架构(SOA)和数据仓库(Data Warehouse, ...
- b2c项目基础架构分析(二)前端框架 以及补漏的第一篇名词解释
继续上篇,上篇里忘记了也很重要的前端部分,今天的网站基本上是以一个启示页,然后少量的整页切换,大量的浏览器后台调用web服务局部.动态更新页面显示状态这种方式在运作的,从若干年前简单的ajax流行起来 ...
- b2c项目基础架构分析(一)b2c 大型站点方案简述 已补充名词解释
我最近一直在找适合将来用于公司大型bs,b2b b2c的基础架构. 实际情况是要建立一个bs架构b2b.b2c的网站,当然还包括wap站点.手机app站点. 一.现有公司技术人员现状: 1.熟悉asp ...
- Ceph基础知识和基础架构认识
1 Ceph基础介绍 Ceph是一个可靠地.自动重均衡.自动恢复的分布式存储系统,根据场景划分可以将Ceph分为三大块,分别是对象存储.块设备存储和文件系统服务.在虚拟化领域里,比较常用到的是Cep ...
随机推荐
- GreenPlum-数据存储目录迁移及常用操作
一.环境介绍 Greenplum5 3节点集群,Centos7.2虚拟机, 二.需求 因为/home目录磁盘空间已满,需要将Greenplum的数据存储目录转移到新的分区/opt目录下,虚拟机磁盘管理 ...
- A == B ?(hdu2054)
输入格式:直接循环,同时输入两个不带空格未知长度的字符串. 思考:不带空格未知长度且同时输入,用两个char s[maxsize]定义两个字符数组,再用scanf_s()函数同时输入两个字符串. 注意 ...
- 【XSS-labs】Level 1-5
写在前面: 这个闯关游戏旨在理解XSS的原理并运用各种姿势绕过对于XSS攻击的过滤和限制. 这个练习只要弹出弹框即可过关 ,每一关我也会附上payload和源代码的解析 Level 1 观察源码 &l ...
- 【python爬虫】scrapy实战1--百万微博任性采集
原文: https://blog.csdn.net/qq_36936730/article/details/105132550
- 基本sql语法
SQL 语句主要可以划分为以下 3 个类别. DDL(Data Definition Languages)语句:数据定义语言,这些语句定义了不同的数据段.数据库.表.列.索引等数据库对象的定义.常用 ...
- Python的多线程锁跟队列
一.互斥锁: 1.线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁. 2.互斥锁为资源引入一个状态:锁定.非锁定 3.某个线程要更改共享数据是,先将其锁定.此时资源的状态为锁定, ...
- PIC单片机的for定时
看到公司的一个项目上的用的for定时 但是网上查找看到<PIC16系列单片机C程序设计与PROTEUS仿真> 上有关于for语句的定时 void Delay(unsigned int n) ...
- [PHP学习教程 - 网络]002.$_SERVER["SCRIPT_NAME"]、$_SERVER["PHP_SELF"]、$_SERVER["QUERY_STRING"]、$_SERVER["REQUEST_URI"]介绍($_SERVER URL Infomation)
引言:在使用原生PHP的时候,对于URL路径的切割,如:域名,查询参数等等的提取,通常绝大多数兄弟会忽略$_SERVER中定义的内置常量的关系,这里为大家讲解一下. 常用的URL请求路径$_SERVE ...
- Parrot os安装docker及docker-compose
一.安装docker 1.添加Docker pgp key curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key ...
- 01 . RabbitMQ简介及部署
RabbitMQ简介 MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法.应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它 ...