网站基础架构实现-小小架构入门

1
2
3
4
5
6
作者: 夜泊1990
企鹅: 1611756908
Q 群: 948233848
邮箱: hd1611756908@163.com
博客: https://hs-an-yue.github.io/
B 站: https://space.bilibili.com/514155929/

第一章 我理解的架构

第1节 架构是什么

1
架构是一个软件的基础,后期所有的业务都要建立在此架构基础之上,而架构是为了后期业务服务。

第2节 架构设计是为了什么

1
2
3
4
软件随着用户量的增长和功能的多样性会变得越来越复杂,一个好的软件架构可以很好的实现横向扩展
1. 业务上的扩展
2. 容量上的扩展
架构设计不合理就会局限软件的发展(业务扩展和容量扩展),还会增加开发和运维成本。

第二章 架构的发展

第1节 架构发展

1
2
3
4
5
6
架构发展

单点架构 -- 分布式架构 -- 云计算 -- ...

1. 可扩展架构(前后端分离)
2. 难扩展架构(前后端不分离)

第2节 分布式和集群思考

1
2
分布式概念 : 一拆多称之为分布式(一个复杂的应用服务拆成多个不相同的单体服务)
集群概念: 相同的服务放在一起称之为集群(MYSQL集群,Redis集群等)

第三章 单点架构的思考

1
2
3
4
5
6
7
8
9
单点架构也可以扛起高并发,高并发的软件也是由一个个单体应用撑起的,很多公司采用分布式架构设计是不明智的.

单点
优点: 结构简单,开发和运维成本低
缺点: 不能抗住很高的并发,但是不是不能抗住大量用户访问

分布式
优点: 容量大,扩展能力强,拆分合理的话,单体系统简单
缺点: 架构复杂,开发和运维成本高,程序员技术参差不齐容易出现一些深度BUG不易维护

第1节 确定架构

1
2
3
单点架构
1. 前后端不分离
2. 前后端分离 --- 方便后期扩展

第2节 确定技术

1
2
3
4
5
6
7
8
9
基于SpringBoot2.7.x的单点前后端分离项目(只有后端)

1. SpringBoot2.7.6 基础框架
2. MySQL8.x 数据库
3. MyBatis 持久层
4. knife4j4.4.0 在线API文档
5. jwt4.4.0 JWT token管理工具
6. SpringSecurity 权限管理
7. Druid 数据库连接池

第四章 单点基础架构实现

第1节 创建项目添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
创建项目 略
注意: 使用阿里巴巴项目创建脚手架,Spring官方脚手架创建不出基于JDK8的SpringBoot项目
脚手架地址: https://start.aliyun.com/

<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.7.6</spring-boot.version>
<hutool.version>5.8.26</hutool.version>
<knife4j.version>4.4.0</knife4j.version>
<easyexcel.version>4.0.2</easyexcel.version>
<pagehelper.version>1.4.7</pagehelper.version>
<jwt.version>4.4.0</jwt.version>
<mybatis.version>2.3.0</mybatis.version>
<jthinking.version>2.1.7</jthinking.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
</dependency>
<!-- 离线IP地址定位 -->
<dependency>
<groupId>com.jthinking.common</groupId>
<artifactId>ip-info</artifactId>
<version>${jthinking.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

第2节 基础数据库表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
-- 采用基于RBAC的权限,所以下面除了部门表,其他的表都是设计权限的表
-- 部门表
CREATE TABLE sys_department(
dept_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '部门ID,主键自增',
dept_name VARCHAR(100) NOT NULL COMMENT '部门名称',
parent_id INT NOT NULL DEFAULT 0 COMMENT '父级ID,顶层为0,自关联部门主键',
ancestors VARCHAR(100) NOT NULL COMMENT '当前部门的祖级列表 例:0,100 0和100都是当前部门的祖级',
status INT DEFAULT 1 COMMENT '部门状态 1: 可用 0:禁用',
is_delete INT DEFAULT 0 COMMENT '是否删除 1:删除 0:未删除',
create_time DATETIME COMMENT '数据创建时间',
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
)DEFAULT CHARSET='UTF8' AUTO_INCREMENT=10000 COMMENT='部门表';

-- 用户表
CREATE TABLE sys_user(
user_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID,主键自增',
user_name VARCHAR(100) NOT NULL COMMENT '用户名称',
phone VARCHAR(50) UNIQUE NOT NULL COMMENT '手机号',
password VARCHAR(255) NOT NULL COMMENT '手机号',
nick_name VARCHAR(100) COMMENT '昵称',
avatar VARCHAR(521) COMMENT '头像地址',
status INT DEFAULT 1 COMMENT '部门状态 1: 可用 0:禁用',
is_delete INT DEFAULT 0 COMMENT '是否删除 1:删除 0:未删除',
dept_id INT COMMENT '部门ID,关联部门表sys_department主键',
create_time DATETIME COMMENT '数据创建时间',
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
)DEFAULT CHARSET='UTF8' AUTO_INCREMENT=10000 COMMENT='用户表';
-- 角色表
CREATE TABLE sys_role(
role_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID,主键自增',
role_name VARCHAR(100) NOT NULL COMMENT '角色名称',
status INT DEFAULT 1 COMMENT '部门状态 1: 可用 0:禁用',
is_delete INT DEFAULT 0 COMMENT '是否删除 1:删除 0:未删除',
create_time DATETIME COMMENT '数据创建时间',
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
)DEFAULT CHARSET='UTF8' AUTO_INCREMENT=10000 COMMENT='角色表';
-- 权限表
CREATE TABLE sys_permission(
permission_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID,主键自增',
permission_name VARCHAR(100) NOT NULL COMMENT '权限名称',
status INT DEFAULT 1 COMMENT '部门状态 1: 可用 0:禁用',
is_delete INT DEFAULT 0 COMMENT '是否删除 1:删除 0:未删除',
create_time DATETIME COMMENT '数据创建时间',
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
)DEFAULT CHARSET='UTF8' AUTO_INCREMENT=10000 COMMENT='权限表';

-- 用户角色关联表 n-n
CREATE TABLE sys_user_role(
user_id INT COMMENT '用户ID,关联用户表sys_user主键',
role_id INT COMMENT '角色ID,关联角色表sys_role主键',
PRIMARY KEY (user_id,role_id)
)DEFAULT CHARSET='UTF8' COMMENT='用户角色关联表';

-- 角色权限关联表 n-n
CREATE TABLE sys_role_permission(
role_id INT COMMENT '角色ID,关联角色表sys_role主键',
permission_id INT COMMENT '权限ID,关联权限表sys_permission主键',
PRIMARY KEY (role_id,permission_id)
)DEFAULT CHARSET='UTF8' COMMENT='用户角色关联表';

第3节 基础类设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/**
* 状态管理枚举
*/
@Getter
@Schema(description = "YueAn状态管理枚举类")
public enum YueAnEnum {
SUCCESS(200,"成功"),
PARAMS_NOT_NULL(201,"请求参数不能为空"),
FILE_CONTENT_NOT_NULL(202,"文件内容不能为空"),
FILE_FORMAT_ILLEGAL(203,"文件格式不合法"),
AUTHORIZATION_FAILURE(204,"登陆失败,用户名或密码错误"),
AUTHENTICATION_FORBIDDEN(205,"权限不足"),
ACCOUNT_NULL(206,"账户不存在"),
TOKEN_NULL(207,"token为空"),
CAPTCHA_FAILURE(208,"验证码生成失败"),
CAPTCHA_ERROR(209,"验证码输入错误"),
TOKEN_TIMEOUT(211,"token过期或未登陆,请重新登陆"),
PARAMS_FORMAT_ERROR(212,"请求参数格式错误"),
// TODO ...

ERROR(-1,"系统异常,请联系管理员");

private int code;
private String msg;

YueAnEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public void setCode(int code) {
this.code = code;
}

public void setMsg(String msg) {
this.msg = msg;
}
}


/**
* 统一返回数据封装类
*/
@ToString
@Setter
@Getter
@Schema(description = "统一返回数据封装类",name = "Result<T>")
public class Result<T> {
/**
* 响应状态码
*/
@Schema(description = "响应状态码")
private int code = YueAnEnum.SUCCESS.getCode();
/**
* 响应消息
*/
@Schema(description = "响应提示消息")
private String msg = YueAnEnum.SUCCESS.getMsg();
/**
* 响应结果数据
*/
@Schema(description = "返回数据")
private T data;
/**
* 服务器响应时间
*/
@Schema(description = "服务器响应时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime timestamp = LocalDateTime.now();

public Result() {
}

public Result(T data) {
this.data = data;
}

public Result(int code, String msg) {
this.code = code;
this.msg = msg;
}

public Result(YueAnEnum yueAnEnum) {
this.code = yueAnEnum.getCode();
this.msg = yueAnEnum.getMsg();
}

public Result(YueAnEnum yueAnEnum, T data) {
this.code = yueAnEnum.getCode();
this.msg = yueAnEnum.getMsg();
this.data = data;
}

/**
* 成功 - 无返回数据
*/
public static <T> Result<T> success(){
return new Result<>();
}

/**
* 成功 - 有返回数据
*/
public static <T> Result<T> success(T t){
return new Result<>(t);
}

/**
* 失败 - 无返回数据 - 设置异常消息
*/
public static <T> Result<T> error(YueAnEnum yueAnEnum){
return new Result<>(yueAnEnum.getCode(),yueAnEnum.getMsg());
}

/**
* 失败 - 无返回数据 - 设置异常消息
*/
public static <T> Result<T> error(int code,String msg){
return new Result<>(code,msg);
}

/**
* 失败 - 有返回数据 - 设置异常消息
*/
public static <T> Result<T> error(YueAnEnum yueAnEnum,T t){
return new Result<>(yueAnEnum,t);
}
}

第4节 统一异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 统一异常处理器
*/
@ResponseBody
@ControllerAdvice
public class YueAnExceptionHandler {

private final static Logger LOGGER = LoggerFactory.getLogger(YueAnExceptionHandler.class);


/**
* 通用统一异常处理
* @param e 异常
* @return 返回异常信息
*/
@ExceptionHandler(value = {YueAnException.class, AccessDeniedException.class,Exception.class})
public Result<String> commonException(Exception e){
LOGGER.error("全局异常: YueAnExceptionHandler#commonException",e);
if(e instanceof YueAnException){
YueAnException yueAnException = (YueAnException) e;
return Result.error(yueAnException.getCode(),yueAnException.getMessage());
} else if (e instanceof AccessDeniedException) {
return Result.error(YueAnEnum.AUTHENTICATION_FORBIDDEN);
}
return Result.error(YueAnEnum.ERROR.getCode(),StringUtils.isNotEmpty(e.getMessage())?e.getMessage():YueAnEnum.ERROR.getMsg());
}

/**
* 请求参数数据格式转换异常
*/
@ExceptionHandler(value = {HttpMessageNotReadableException.class})
public Result<String> parseHttpMessageNotReadableException(Exception e){
LOGGER.error("全局异常: YueAnExceptionHandler#parseHttpMessageNotReadableException",e);
return Result.error(YueAnEnum.PARAMS_FORMAT_ERROR);
}
}

第5节 统一日志处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 统一日志处理
*/
@Component
@Aspect
@Order(value = 2)
public class LogAspect {
/**
* 日志对象声明
*/
private final static Logger LOGGER = LoggerFactory.getLogger(LogAspect.class);

/**
* 统一切入点表达式
*/
@Pointcut(value = "execution(* io.yue.controller.*.*(..))")
public void log(){}

/**
* 日志环绕通知
* @param joinPoint 切入点表达式
* @return 返回值
* @throws Throwable 异常
*/
@Around(value = "log()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//获取主机IP
LOGGER.info("IP地址 :{}",request.getRemoteAddr());
//国家省市区地址详情 中国|0|黑龙江省|哈尔滨市|教育网
try {
IPInfo ipInfo = IPInfoUtils.getIpInfo(request.getRemoteAddr());
LOGGER.info("IP地址详情 :{}-{}-{}-{}",ipInfo.getCountry(),ipInfo.getProvince(),ipInfo.getAddress(),ipInfo.getIsp());
}catch (Exception e){
LOGGER.error("IP地址解析失败:",e);
}
//获取请求地址
LOGGER.info("请求URL :{}",request.getRequestURL().toString());
//获取请求方式
LOGGER.info("HTTP 请求方式 :{}",request.getMethod());
//获取类名和方法名
LOGGER.info("类方法 :{}.{}",joinPoint.getSignature().getDeclaringTypeName(),joinPoint.getSignature().getName());
//方法入参
Object[] args = joinPoint.getArgs();
if(ArrayUtils.isNotEmpty(args)){
LOGGER.info("方法入参 :{}", Arrays.asList(args));
}else {
LOGGER.info("方法入参 :{}", "参数为空");
}
Object proceed = joinPoint.proceed();
LOGGER.info("返回结果 :{}",proceed);
LOGGER.info("-------------------------------------------------------");
return proceed;
}
}

第6节 日志链路追踪

6.1 给日志添加唯一ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 日志链路追踪配置
*/
@Component
@Aspect
@Order(value = 1)
public class MDCAspect {

private static final String TRACE_ID_KEY = "traceId";

@Pointcut(value = "execution(* io.yue.controller.*.*(..))")
private void mdc(){}

/**
* 前置通知
*/
@Before(value = "mdc()")
public void mdcPre(){
MDC.put(TRACE_ID_KEY, UUID.randomUUID().toString());
}
/**
* 返回通知
*/
@AfterReturning(value = "mdc()")
public void mdReturn(){
MDC.remove(TRACE_ID_KEY);
}
/**
* 异常通知
*/
@AfterThrowing(value = "mdc()")
public void mdcException(){
MDC.remove(TRACE_ID_KEY);
}
}

6.2 日志文件配置唯一ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!-- logback-spring.xml -->
<configuration>
<!--
日志存放路径
${user.home}: 在不同的操作系统上动态获取用户家目录
-->
<property name="log.path" value="${user.home}/yue/log" />

<!-- 控制台输出配置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36}-[%X{traceId}]- %msg%n</pattern>
</encoder>
</appender>
<!-- 文件输出配置 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 时时日志的名称 -->
<file>${log.path}/yue-an.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 最终生成日志文件的名称,以天作为基本单元 -->
<fileNamePattern>${log.path}/yue-an.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 存储60天历史记录 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36}-[%X{traceId}]- %msg%n</pattern>
</encoder>
</appender>
<!-- 日志级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>

第7节 IP地址定位

1
2
3
4
5
6
<!-- 离线地理信息定位,在第四章第5节日志中添加 -->
<dependency>
<groupId>com.jthinking.common</groupId>
<artifactId>ip-info</artifactId>
<version>2.1.7</version>
</dependency>

第8节 静态资源管理

1
2
3
4
5
6
yue:
file-dir: D:/yue/
spring:
web:
resources:
static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,file:${yue.file-dir}/ # 配置静态资源位置,通过服务器地址可以访问此目录下的资源

第9节 Knife4j在线API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Knife4j配置 在线API文档和工具(Swagger)
*/
@Configuration
public class Knife4jConfig {
/**
* Knife4j基础信息配置
*/
@Bean
public OpenAPI openAPI(){
return new OpenAPI()
.info(new Info()
.title("Yue-An 在线API文档")
.version("v1.0.0")
.contact(new Contact()
.name("Yue-An")
.email("hd1611756908@163.com")
.url("https://hs-an-yue.github.io"))
.description("Yue-An是一个可定制化的快速成站的单点框架"));
}
}

第五章 其它配置

1
2
1. 自定义日志图案打印(banner图)
2. 项目基础信息打印

第六章 Spring Security整合

第1节 Spring Security 介绍

1
保护我们的JavaWeb网站

第2节 Spring Security API介绍

1
2
3
4
5
6
7
8
9
10
11
12
1. 创建封装数据传递的类				UserDetails
2. 从数据库获取数据,送到Spring Security上下文 UserDetailsService
3. Spring Security核心配置
3.1 核心过滤器 SecurityFilterChain
3.2 密码加密 PasswordEncoder
3.3 认证管理器
3.3.1 AuthenticationManager
3.3.2 AuthenticationConfiguration
4. 登录实现 UsernamePasswordAuthenticationToken
5. 登录失败配置 AuthenticationEntryPoint
6. 注销成功配置 LogoutSuccessHandler
7. 前后端分离统一令牌校验和处理 OncePerRequestFilter

第3节 Spring Security整合

1
实现 略

第七章 基础架构总结

1
2
3
4
5
1. 当前架构虽为单点架构,但是采用前后端分离的方式,具备可扩展性,可以部署成集群方式
2. 当前架构添加了在线API文档,链路日志追踪等功能可以很好的为后期业务提供支持
2.1 在线API文档 : 快速的文档响应速度,以及在线API工具的测试
2.2 链路日志追踪 : 定位错误消息时更方便
...

--------------------------已经到底啦!--------------------------