赞
踩
当前所有的数据查询都是从数据库MySQL中直接查询的,那么可能就存在如下问题: 频繁访问数据库,数据库访问压力大,系统性能下降,用户体验较差。
要解决我们上述提到的问题,就可以使用我们前面学习的一个技术:Redis,通过Redis来做缓存,从而降低数据库的访问压力,提高系统的访问性能,从而提升用户体验。加入Redis做缓存之后,我们在进行数据查询时,就需要先查询缓存,如果缓存中有数据,直接返回,如果缓存中没有数据,则需要查询数据库,再将数据库查询的结果,缓存在redis中。
在使用缓存过程中,要注意保证数据库中和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。
Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能,大大简化我们在业务中操作缓存的代码。
Spring Cache只是提供了一层抽象,底层可以切换不同的cache实现。具体就是通过CacheManager接口来统一不同的缓存技术。CacheManager是Spring提供的各种缓存技术抽象接口。
针对不同的缓存技术需要实现不同的CacheManager:
CacheManager | 描述 |
---|---|
EhCacheCacheManager | 使用EhCache作为缓存技术 |
GuavaCacheManager | 使用Google的GuavaCache作为缓存技术 |
RedisCacheManager | 使用Redis作为缓存技术 |
SpringCache注解
在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:
注解 | 说明 |
---|---|
@EnableCaching | 开启缓存注解功能 |
@Cacheable | 在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中 |
@CachePut | 将方法的返回值放到缓存中 |
@CacheEvict | 将一条或多条数据从缓存中删除 |
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。
例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
注意:返回值对象要实现序列化接口,否则无法缓存
SpringCache中使用Redis
1.pom.xml
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-cache</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
2.application.yml
- spring:
- redis:
- host: 192.168.200.200
- port: 6379
- password: root@123456
- database: 0
- cache:
- redis:
- time-to-live: 1800000 #设置缓存过期时间,可选
注意:如果报错要求设置密码将password改为requirePass
- @RestController
- @RequestMapping("/user")
- @Slf4j
- public class UserController {
- @Autowired
- private CacheManager cacheManager;
-
- @Autowired
- private UserService userService;
-
- /**
- * CachePut:将方法返回值放入缓存
- * value:缓存名称,每个缓存名称下面可以有多个key
- * key:缓存的key
- * */
- @CachePut(value = "userCache",key = "#user.id")
- @PostMapping
- public User save(User user){
- userService.save(user);
- return user;
- }
-
- @CacheEvict(value = "userCache",key = "#id")
- // @CacheEvict(value = "userCache",key = "#root.args[0]")
- // @CacheEvict(value = "userCache",key = "#p0")
- @DeleteMapping("/{id}")
- public void delete(@PathVariable Long id){
- userService.removeById(id);
- }
-
- // @CacheEvict(value = "userCache",key = "#root.args[0].id")
- // @CacheEvict(value = "userCache",key = "#p0.id")
- // @CacheEvict(value = "userCache",key = "#result.id")
- @CacheEvict(value = "userCache",key = "#user.id")
- @PutMapping
- public User update(User user){
- userService.updateById(user);
- return user;
- }
- /**
- * Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
- * value:缓存名称,每个缓存名称下面可以有多个key
- * key:缓存的key
- * condition:条件,满足条件才能缓存数据
- * unless:满足条件不缓存
- * */
- @Cacheable(value = "userCache",key = "#id",unless = "#result==null")
- @GetMapping("/{id}")
- public User getById(@PathVariable Long id){
- User user = userService.getById(id);
- return user;
- }
- @Cacheable(value = "userCache",key = "#user.id +_+ #user.name")
- @GetMapping("/list")
- public List<User> list(User user){
- LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
- queryWrapper.eq(user.getId()!= null,User::getId,user.getId());
- queryWrapper.eq(user.getName()!=null,User::getName,user.getName());
- List<User> list = userService.list(queryWrapper);
- return list;
- }
- }
MySQL服务器只有一台,那么就可能会存在如下问题:
读和写所有压力都由一台数据库承担,压力大
数据库服务器磁盘损坏则数据丢失,单点故障
为了解决上述提到的两个问题,我们可以准备两台MySQL,一台主(Master)服务器,一台从(Slave)服务器,主库的数据变更,需要同步到从库中(主从复制)。而用户在访问我们项目时,如果是写操作(insert、update、delete),则直接操作主库;如果是读(select)操作,则直接操作从库(在这种读写分离的结构中,从库是可以有多个的),这种结构我们称为 读写分离。
MySQL数据库默认是支持主从复制的,不需要借助于其他的技术,只需要在数据库中简单的配置即可。
MySQL主从复制是一个异步的复制过程,底层是基于Mysql数据库自带的 二进制日志 功能。就是一台或多台MySQL数据库(slave,即从库)从另一台MySQL数据库(master,即主库)进行日志的复制,然后再解析日志并应用到自身,最终实现 从库 的数据和 主库 的数据保持一致。MySQL主从复制是MySQL数据库自带功能,无需借助第三方工具,只需要在数据库中简单的配置即可。
MySQL的主从复制原理如下:
MySQL复制过程分成三步:
1. MySQL master 将数据变更写入二进制日志( binary log)
2.slave将master的binary log拷贝到它的中继日志(relay log)
3.slave重做中继日志中的事件,将数据变更反映它自己的数据
搭建
主库配置
1.提前准备两台服务器,并且在服务器中安装MySQL,服务器的信息如下:
数据库 | IP | 数据库版本 |
---|---|---|
Master | 192.168.138.100 | 5.7.25 |
Slave | 192.168.138.101 | 5.7.25 |
2.防火墙开放3306端口号
- firewall-cmd --zone=public --add-port=3306/tcp --permanent
-
- firewall-cmd --zone=public --list-ports
3.并将两台数据库服务器启动起来:
systemctl start mysqld
4.登录MySQL,验证是否正常启动
5.修改Mysql数据库的配置文件vim /etc/my.cnf,在最下面增加配置:
- log-bin=mysql-bin #[必须]启用二进制日志
- server-id=200 #[必须]服务器唯一ID(唯一即可)
6.重启Mysql服务
systemctl restart mysqld
7.创建数据同步的用户并授权
登录mysql,并执行如下指令,创建用户并授权:
GRANT REPLICATION SLAVE ON *.* to 'xiaoming'@'%' identified by 'Root@123456';
注意:上面SQL的作用是创建一个用户 xiaoming ,密码为 Root@123456 ,并且给xiaoming用户授予REPLICATION SLAVE权限。常用于建立复制时所需要用到的用户权限,也就是slave必须被master授权具有该权限的用户,才能通过该用户复制。
遇到问题:密码格式错误
mysql5.7默认密码校验策略等级为 MEDIUM , 该等级要求密码组成为: 数字、小写字母、大写字母 、特殊字符、长度至少8位,可以设置密码安全等级低:
- set global validate_password_length=4; 设置密码长度最低位数
- set global validate_password_policy=LOW; 设置密码安全等级低,便于密码可以修改成
8.登录Mysql数据库,查看master同步状态
执行下面SQL,记录下结果中File和Position的值
show master status;
注意:上面SQL的作用是查看Master的状态,执行完此SQL后不要再执行任何操作,因为
在执行其他操作时当前日志名File值、日志位置Position值就会发生变化,而这些值从库会使用到
从库配置
1.修改Mysql数据库的配置文件 vim /etc/my.cnf
server-id=201 #[必须]服务器唯一ID
2.重启Mysql服务
systemctl restart mysqld
3.登录Mysql数据库,设置主库地址及同步位置
- change master to master_host='192.168.200.200',master_user='xiaoming',master_password='Root@123456',master_log_file='mysql-bin.000001',master_log_pos=154;
-
- start slave;
注意:
A. master_host : 主库的IP地址
B. master_user : 访问主库进行主从复制的用户名(上面在主库创建的)
C. master_password : 访问主库进行主从复制的用户名对应的密码
D. master_log_file : 从哪个日志文件开始同步(上述查询master状态中展示的有)
E. master_log_pos : 从指定日志文件的哪个位置开始同步(上述查询master状态中展示的有)
4. 查看从数据库的状态
show slave status;
然后通过状态信息中的 Slave_IO_running 和 Slave_SQL_running 可以看出主从同步是否就绪,如果这两个参数全为Yes,表示主从同步已经配置完成。
5.测试
主从复制的环境,已经搭建好了,接下来,我们可以通过Navicat连接上两台MySQL服务器,进行测试。测试时,我们只需要在主库Master执行操作,查看从库Slave中是否将数据同步过去即可。
在master中创建数据库itcast, 刷新slave查看是否可以同步过去
在master的itcast数据下创建user表, 刷新slave查看是否可以同步过去
在master的user表中插入一条数据, 刷新slave查看是否可以同步过去
主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
主从复制的结构,我们在第一节已经完成了,那么我们在项目中,如何通过java代码来完成读写分离呢,如何在执行select的时候查询从库,而在执行insert、update、delete的时候,操作主库呢?这个时候,我们就需要介绍一个新的技术 ShardingJDBC。
Sharding-JDBC定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。使用Sharding-JDBC可以在程序中轻松的实现数据库读写分离。
Sharding-JDBC具有以下几个特点:
1). 适用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
2). 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
3). 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据库。
使用Sharding-JDBC实现读写分离步骤:
1.导入maven坐标
- <dependency>
- <groupId>org.apache.shardingsphere</groupId>
- <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
- <version>4.0.0-RC1</version>
- </dependency>
2.在配置文件中配置读写分离规则
- spring:
- shardingsphere:
- datasource:
- names:
- master,slave
- # 主数据源 名字前后要对应
- master:
- type: com.alibaba.druid.pool.DruidDataSource
- driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://192.168.138.100:3306/rw?characterEncoding=utf-8
- username: root
- password: root
- # 从数据源
- slave:
- type: com.alibaba.druid.pool.DruidDataSource
- driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://192.168.138.101:3306/rw?characterEncoding=utf-8
- username: slave
- password: slave
- masterslave:
- # 读写分离配置
- load-balance-algorithm-type: round_robin
- # 最终的数据源名称
- name: dataSource
- # 主库数据源名称
- master-data-source-name: master
- # 从库数据源名称列表,多个逗号分隔
- slave-data-source-names: slave
- props:
- sql:
- show: true #开启SQL显示,默认false
3.在配置文件中配置允许bean定义覆盖配置项
在声明 org.apache.shardingsphere.shardingjdbc.spring.boot 包下的SpringBootConfiguration中的dataSource这个bean时出错, 原因是有一个同名的 dataSource 的bean在com.alibaba.druid.spring.boot.autoconfigure包下的DruidDataSourceAutoConfigure类加载时已经声明了。所以要允许数据源覆盖:
- spring:
- main:
- allow-bean-definition-overriding: true
遇到问题:
注意:此时的数据源DataSource已经以前不一样了,已经可以进行读写分离了
Nginx是一款轻量级的Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用nginx的网站有:百度、京东、新浪、网易、腾讯、淘宝等。
Nginx下载和安装
在Nginx的官网的下载页面中(http://nginx.org/en/download.html),就展示了当前Nginx版本,并提供了下载的连接。 如下:
在本项目中,我们所学习的Nginx选择的是稳定版本的1.16这个版本,我们可以直接从官网下载,当然在我们的课程资料中也已经提供了该版本的安装包。
1.安装依赖包
由于nginx是基于c语言开发的,所以需要安装c语言的编译环境,及正则表达式库等第三方依赖库。
yum -y install gcc pcre-devel zlib-devel openssl openssl-devel
2.下载Nginx安装包
wget : wget命令用来从指定的URL下载文件。wget非常稳定,它在带宽很窄的情况下和不稳定网络中有很强的适应性,如果是由于网络的原因下载失败,wget会不断的尝试,直到整个文件下载完毕。如果是服务器打断下载过程,它会再次联到服务器上从停止的地方继续下载。
- yum install wget
- wget https://nginx.org/download/nginx-1.16.1.tar.gz
执行完wget指令后,就会在当前所在目录看到下载下来的文件。
3. 解压nginx压缩包
tar -zxvf nginx-1.16.1.tar.gz
4. 配置Nginx编译环境
创建Nginx的安装目录
mkdir -p /usr/local/nginx
--prefix 指定的目录,就是安装Nginx的目录。
- cd nginx-1.16.1
- ./configure --prefix=/usr/local/nginx
5. 编译&安装
make & make install
目录结构
安装完Nginx后,可以切换到Nginx的安装目录(/usr/local/nginx),先来熟悉一下Nginx的目录结构,
yum install tree
上述我们用到的一个指令 tree,该指令可以将指定的目录以树状结构展示出来。如果没有这个指令,可以通过以下指令进行安装。
重点目录和文件如下:
目录/文件 | 说明 | 备注 |
---|---|---|
conf | 配置文件的存放目录 | |
conf/nginx.conf | Nginx的核心配置文件 | conf下有很多nginx的配置文件,我们主要操作这个核心配置文件 |
html | 存放静态资源(html, css, ) | 部署到Nginx的静态资源都可以放在html目录中 |
logs | 存放nginx日志(访问日志、错误日志等) | |
sbin/nginx | 二进制文件,用于启动、停止Nginx服务 |
Nginx常用命令
1.查看版本
./nginx -v
2.检查配置文件
修改了nginx.conf核心配置文件之后,在启动Nginx服务之前,可以先检查一下conf/nginx.conf文件配置的是否有错误,命令如下:
./nginx -t
3. 启动
./nginx
启动之后,我们可以通过ps -ef指令来查看nginx的进程是否存在。
注意:
nginx服务启动后,默认就会有两个进程,第三个进程是执行命令所需要的一个进程可以忽略它。
nginx默认服务的端口号为80
启动之后,可以直接访问Nginx的80端口, http://192.168.138.100
遇到问题:访问不了是因为端口的防火墙没有开放
- firewall-cmd --zone=public --add-port=80/tcp --permanent
-
- firewall-cmd --reload
4. 停止
./nginx -s stop
停止之后,我们可以查看nginx的进程:
ps -ef|grep nginx
5. 重新加载
当修改了Nginx配置文件后,需要重新加载才能生效,可以使用下面命令重新加载配置文件:
./nginx -s reload
环境变量配置
在上述我们在使用nginx命令在进行服务的启动、停止、重新加载时,都需要用到一个指令nginx,而这个指令是在nginx/sbin目录下的,每一次使用这个指令都需要切换到sbin目录才可以,使用相对繁琐。那么我们能不能在任意目录下都可以执行该指令来操作nginx呢?答案是可以的,配置nginx的环境变量即可。
通过vim编辑器,打开/etc/profile文件, 在PATH环境变量中增加nginx的sbin目录,如下:
修改完配置文件之后,需要执行 source /etc/profile 使文件生效。
source /etc/profile
接下来就可以在任意目录下执行nginx的指令了
nginx的配置文件(conf/nginx.conf)整体上分为三部分: 全局块、events块、http块。这三块的分别配置什么样的信息呢,看下表:
区域 | 职责 |
---|---|
全局块 | 配置和nginx运行相关的全局配置 |
events块 | 配置和网络连接相关的配置 |
http块 | 配置代理、缓存、日志记录、虚拟主机等配置 |
具体结构图如下:
在全局块、events块以及http块中,我们经常配置的是http块。
在http块中可以包含多个server块,每个server块可以配置多个location块。
Nginx应用
部署静态资源
Nginx可以作为静态web服务器来部署静态资源。这里所说的静态资源是指在服务端真实存在,并且能够直接展示的一些文件,比如常见的html页面、css文件、js文件、图片、视频等资源。
相对于Tomcat,Nginx处理静态资源的能力更加高效,所以在生产环境下,一般都会将静态资源部署到Nginx中。
将静态资源部署到Nginx非常简单,只需要将文件复制到Nginx安装目录下的html目录中即可。
- server {
- listen 80; #监听端口
- server_name localhost; #服务器名称
- location / { #匹配客户端请求url
- root html; #指定静态资源根目录
- index index.html; #指定默认首页
- }
- }
测试
提供了一个静态的html文件,我们需要将这个文件部署到nginx中,然后通过nginx访问html静态资源。
1. 将静态资源上传到 /usr/local/nginx/html 目录
2.启动nginx
3. 访问
http://192.168.138.100/hello.html
http://192.168.138.100 , 访问该地址,访问的是nginx的默认首页
4. 配置首页
如果我们需要将hello.html作为nginx的首页,可以修改location的index指令,配置为hello.html,
配置完毕后,我们可以通过指令,来检查配置文件是否配置正确: nginx -t
配置文件修改了,我们需要重新加载一下,才可以生效:
nginx -s reload
反向代理
1. 正向代理
正向代理服务器是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。
正向代理的典型用途是为在防火墙内的局域网客户端提供访问Internet的途径。
正向代理一般是在客户端设置代理服务器,通过代理服务器转发请求,最终访问到目标服务器。
2. 反向代理
反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源,反向代理服务器负责将请求转发给目标服务器。用户不需要知道目标服务器的地址,也无须在用户端作任何设定,对于用户来说,访问反向代理服务器是完全无感知的。
使用nginx来作为反向代理服务器使用。 在nginx中,可以在nginx.conf中配置反向代理:
- server {
- listen 82;
- server_name localhost;
- location / {
- proxy_pass http://192.168.138.101:8080; #反向代理配置,将请求转发到指定服务
- }
- }
上述配置的含义为: 当访问nginx的82端口时,根据反向代理配置,会将请求转发到 http://192.168.138.101:8080 对应的服务上。
注意:需要开启82端口的防火墙
- firewall-cmd --zone=public --add-port=82/tcp --permanent
-
- firewall-cmd --reload
早期的网站流量和业务功能都比较简单,单台服务器就可以满足基本需求,但是随着互联网的发展,业务流量越来越大并且业务逻辑也越来越复杂,单台服务器的性能及单点故障问题就凸显出来了,因此需要多台服务器组成应用集群,进行性能的水平扩展以及避免单点故障出现。
应用集群:将同一应用部署到多台机器上,组成应用集群,接收负载均衡器分发的请求,进行业务处理并返回响应数据
负载均衡器:将用户请求根据对应的负载均衡算法分发到应用集群中的一台服务器进行处理
在测试时,并没有那么多服务器,可以在一台服务器中启动多个服务,运行在不同的端口号上进行测试。
将两个jar包,上传到192.168.138.101服务器上
jar | 运行端口 | 请求链接 | 响应数据 |
---|---|---|---|
8080 | /hello | 8080 | |
8081 | /hello | 8081 |
运行上传上来的两个jar包,运行端口分别是 8080 , 8081
在nginx中配置负载均衡
打开nginx的配置文件nginx.conf并增加如下配置:
- #upstream指令可以定义一组服务器
- upstream targetserver{
- server 192.168.138.101:8080;
- server 192.168.138.101:8081;
- }
-
- server {
- listen 8080;
- server_name localhost;
- location / {
- proxy_pass http://targetserver;
- }
- }
重新加载nginx配置文件
nginx -s reload
访问
在上述的测试过程中,我们看到请求均衡的转发到了8080和8081,因为默认模式的负载均衡策略是轮询。
注意: 上述所有涉及到的端口号,都需要在对应的服务器的防火墙中开放,或者彻底关闭防火墙
负载均衡策略
处理上述默认的轮询策略以外,在Nginx中还提供了其他的负载均衡策略,如下:
名称 | 说明 | 特点 |
---|---|---|
轮询 | 默认方式 | |
weight | 权重方式 | 根据权重分发请求,权重大的分配到请求的概率大 |
ip_hash | 依据ip分配方式 | 根据客户端请求的IP地址计算hash值, 根据hash值来分发请求, 同一个IP发起的请求, 会发转发到同一个服务器上 |
least_conn | 依据最少连接方式 | 哪个服务器当前处理的连接少, 请求优先转发到这台服务器 |
url_hash | 依据url分配方式 | 根据客户端请求url的hash值,来分发请求, 同一个url请求, 会发转发到同一个服务器上 |
fair | 依据响应时间方式 | 优先把请求分发给处理请求时间短的服务器 |
权重的配置:
- #upstream指令可以定义一组服务器
- upstream targetserver{
- server 192.168.200.201:8080 weight=10;
- server 192.168.200.201:8081 weight=5;
- }
上述配置的weight权重是相对的,在上述的配置中,效果就是,在大数据量的请求下,最终8080接收的请求数是8081的两倍。
当前项目中,前端代码和后端代码混合在一起,主要存在以下几点问题:
1). 开发人员同时负责前端和后端代码开发,分工不明确
2). 开发效率低
3). 前后端代码混合在一个工程中,不便于管理
4). 对开发人员要求高(既会前端,又会后端),人员招聘困难
为了解决上述提到的问题,现在比较主流的开发方式,就是前后端分离开发,前端人员开发前端的代码,后端开发人员开发服务端的业务功能,分工明确,各司其职。需要将之前的项目进行优化改造,变成前后端分离开发的项目。
介绍
前后端分离开发,就是在项目开发过程中,对于前端代码的开发由专门的前端开发人员负责,后端代码则由后端开发人员负责,这样可以做到分工明确、各司其职,提高开发效率,前后端代码并行开发,可以加快项目开发进度。
目前,前后端分离开发方式已经被越来越多的公司所采用,成为当前项目开发的主流开发方式。
前后端分离开发后,从工程结构上也会发生变化,即前后端代码不再混合在同一个maven工程中,而是分为 前端工程 和 后端工程 。
开发流程
前后端分离开发后,面临一个问题,就是前端开发人员和后端开发人员如何进行配合来共同开发一个项目?可以按照如下流程进行:
1). 定制接口: 这里所说的接口不是我们之前在service, mapper层定义的interface; 这里的接口(API接口)就是一个http的请求地址,主要就是去定义:请求路径、请求方式、请求参数、响应数据等内容。
2. 前后端并行开发: 依据定义好的接口信息,前端人员开发前端的代码,服务端人员开发服务端的接口; 在开发中前后端都需要进行测试,后端需要通过对应的工具来进行接口的测试,前端需要根据接口定义的参数进行Mock数据模拟测试。
3. 联调: 当前后端都开发完毕并且自测通过之后,就可以进行前后端的联调测试了,在这一阶段主要就是校验接口的参数格式。
4. 提测: 前后端联调测试通过之后,就可以将项目部署到测试服务器,进行自动化测试了。
介绍
YApi 是高效、易用、功能强大的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi 还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。
YApi让接口开发更简单高效,让接口的管理更具可读性、可维护性,让团队协作更合理。
源码地址: https://github.com/YMFE/yapi
官方文档: https://hellosean1025.github.io/yapi/
要使用YApi,项目组需要自己进行部署,在本项目中可以使用课程提供的平台进行测试,域名: https://mock-java.itheima.net/
使用方式
1.注册账号,登录平台
2.创建项目
2.添加分类
3.添加接口
但是目前,接口中我们并未制定请求参数,响应数据等信息,我们可以进一步点击编辑,对该接口详情进行编辑处理。
4.运行接口
5.导出接口文档
在Yapi平台中我们不仅可以在线阅读文档,还可以将Yapi中维护的文档直接导出来,可以导出md,json,html格式,在导出时自行选择即可。
而在导出的html文件或md文件中,主要描述的就是接口的基本信息, 包括: 请求路径、请求方式、接口描述、请求参数、返回数据等信息。展示形式如下:
6.导入接口文档
导入过程中出现的确认弹窗,选择"确认"。
导入成功之后,我们就可以在Yapi平台查看到已导入的接口。
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。功能主要包含以下几点:
A. 使得前后端分离开发更加方便,有利于团队协作
B. 接口文档在线自动生成,降低后端开发人员编写接口文档的负担
C. 接口功能测试
使用Swagger只需要按照它的规范去定义接口及接口相关的信息,再通过Swagger衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,以及在线接口调试页面等等。
直接使用Swagger, 需要按照Swagger的规范定义接口, 实际上就是编写Json文件,编写起来比较繁琐、并不方便, 。而在项目中使用,一般会选择一些现成的框架来简化文档的编写,而这些框架是基于Swagger的,如knife4j。knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。而使用kinfe4j,需要在pom.xml中引入如下依赖即可:
- <dependency>
- <groupId>com.github.xiaoymin</groupId>
- <artifactId>knife4j-spring-boot-starter</artifactId>
- <version>3.0.2</version>
- </dependency>
使用方式
1. 导入knife4j的maven坐标
- <dependency>
- <groupId>com.github.xiaoymin</groupId>
- <artifactId>knife4j-spring-boot-starter</artifactId>
- <version>3.0.2</version>
- </dependency>
2. 导入knife4j相关配置类
这里我们就不需要再创建一个新的配置类了,我们直接在WebMvcConfig配置类中声明即可。
A. 在该配置类中加上两个注解 @EnableSwagger2 @EnableKnife4j ,开启Swagger和Knife4j的功能。
B. 在配置类中声明一个Docket类型的bean, 通过该bean来指定生成文档的信息。
- @Slf4j
- @Configuration
- @EnableSwagger2
- @EnableKnife4j
- public class WebMvcConfig extends WebMvcConfigurationSupport {
- //设置静态资源映射
- @Override
- protected void addResourceHandlers(ResourceHandlerRegistry registry) {
- log.info("开始进行静态资源映射...");
- registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
- registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
- }
-
- /**
- * 扩展mvc框架的消息转换器
- * 项目启动的时候就会调用
- * @param converters
- */
- @Override
- protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
- log.info("扩展消息转换器...");
- //创建消息转换器对象
- MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
- //设置对象转换器,底层使用Jackson将对象转为json
- messageConverter.setObjectMapper(new JacksonObjectMapper());
- //converters包含了Springmvc默认的几个消息转换器
- //追加自己的消息转换器,将上面的消息转换器对象追加到mvc框架的转换器集合中,设置成优先使用
- converters.add(0,messageConverter);
- }
- @Bean
- public Docket createRestApi() {
- // 文档类型
- return new Docket(DocumentationType.SWAGGER_2)
- .apiInfo(apiInfo())
- .select()
- .apis(RequestHandlerSelectors.basePackage("com.itheima.reggie.controller"))
- .paths(PathSelectors.any())
- .build();
- }
-
- private ApiInfo apiInfo() {
- return new ApiInfoBuilder()
- .title("瑞吉外卖")
- .version("1.0")
- .description("瑞吉外卖接口文档")
- .build();
- }
- }
注意: Docket声明时,指定的有一个包扫描的路径,该路径指定的是Controller所在包的路径。因为Swagger在生成接口文档时,就是根据这里指定的包路径,自动的扫描该包下的@Controller, @RestController, @RequestMapping等SpringMVC的注解,依据这些注解来生成对应的接口文档。
3. 设置静态资源映射
由于Swagger生成的在线文档中,涉及到很多静态资源,这些静态资源需要添加静态资源映射,否则接口文档页面无法访问。因此需要在 WebMvcConfig类中的addResourceHandlers方法中增加如下配置。
- registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
- registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
4. 在LoginCheckFilter中设置不需要处理的请求路径
需要将Swagger及Knife4j相关的静态资源直接放行,无需登录即可访问,否则我们就需要登录之后,才可以访问接口文档的页面。
在原有的不需要处理的请求路径中,再增加如下链接:
- "/doc.html",
- "/webjars/**",
- "/swagger-resources",
- "/v2/api-docs"
常用注解
在上面我们直接访问Knife4j的接口文档页面,可以查看到所有的接口文档信息,但是我们发现,这些接口文档分类及接口描述都是Controller的类名(驼峰命名转换而来)及方法名,而且在接口文档中,所有的请求参数,响应数据,都没有中文的描述,并不知道里面参数的含义,接口文档的可读性很差。
为了解决上述的问题,Swagger提供了很多的注解,通过这些注解,可以更好更清晰的描述接口,包含接口的请求参数、响应数据、数据模型等。核心的注解,主要包含以下几个:
注解 | 位置 | 说明 |
---|---|---|
@Api | 类 | 加载Controller类上,表示对类的说明 |
@ApiModel | 类(通常是实体类) | 描述实体类的作用 |
@ApiModelProperty | 属性 | 描述实体类的属性 |
@ApiOperation | 方法 | 说明方法的用途、作用 |
@ApiImplicitParams | 方法 | 表示一组参数说明 |
@ApiImplicitParam | 方法 | 用在@ApiImplicitParams注解中,指定一个请求参数的各个方面的属性 |
测试
1.实体类
- @Data
- @ApiModel("套餐")
- public class Setmeal implements Serializable {
- private static final long serialVersionUID = 1L;
- @ApiModelProperty("主键")
- private Long id;
-
- //分类id
- @ApiModelProperty("分类id")
- private Long categoryId;
-
- //套餐名称
- @ApiModelProperty("套餐名称")
- private String name;
-
- //套餐价格
- @ApiModelProperty("套餐价格")
- private BigDecimal price;
-
- //状态 0:停用 1:启用
- @ApiModelProperty("状态")
- private Integer status;
-
- //编码
- @ApiModelProperty("套餐编号")
- private String code;
-
- //描述信息
- @ApiModelProperty("描述信息")
- private String description;
-
- //图片
- @ApiModelProperty("图片")
- private String image;
-
- @TableField(fill = FieldFill.INSERT)
- private LocalDateTime createTime;
-
- @TableField(fill = FieldFill.INSERT_UPDATE)
- private LocalDateTime updateTime;
-
- @TableField(fill = FieldFill.INSERT)
- private Long createUser;
-
- @TableField(fill = FieldFill.INSERT_UPDATE)
- private Long updateUser;
- }
2.响应实体R
- @Data
- @ApiModel("返回结果")
- public class R<T> implements Serializable{
-
- @ApiModelProperty("编码")
- private Integer code; //编码:1成功,0和其它数字为失败
-
- @ApiModelProperty("错误信息")
- private String msg; //错误信息
-
- @ApiModelProperty("数据")
- private T data; //数据
-
- @ApiModelProperty("动态数据")
- private Map map = new HashMap(); //动态数据
-
- //省略静态方法 ....
- }
3. Controller类及其中的方法
- @RestController
- @RequestMapping("/setmeal")
- @Slf4j
- @Api(tags = "套餐相关接口")
- public class SetmealController {
-
- @Autowired
- private SetmealService setmealService;
- @Autowired
- private CategoryService categoryService;
- @Autowired
- private SetmealDishService setmealDishService;
-
- /**
- * 新增套餐
- * @param setmealDto
- * @return
- */
- @PostMapping
- @CacheEvict(value = "setmealCache",allEntries = true)
- @ApiOperation(value = "新增套餐接口")
- public R<String> save(@RequestBody SetmealDto setmealDto){
- log.info("套餐信息:{}",setmealDto);
-
- setmealService.saveWithDish(setmealDto);
-
- return R.success("新增套餐成功");
- }
-
- /**
- * 套餐分页查询
- * @param page
- * @param pageSize
- * @param name
- * @return
- */
- @GetMapping("/page")
- @ApiOperation(value = "套餐分页查询接口")
- @ApiImplicitParams({
- @ApiImplicitParam(name = "page",value = "页码",required = true),
- @ApiImplicitParam(name = "pageSize",value = "每页记录数",required = true),
- @ApiImplicitParam(name = "name",value = "套餐名称",required = false)
- })
- public R<Page> page(int page,int pageSize,String name){
- //分页构造器对象
- Page<Setmeal> pageInfo = new Page<>(page,pageSize);
- Page<SetmealDto> dtoPage = new Page<>();
-
- LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
- //添加查询条件,根据name进行like模糊查询
- queryWrapper.like(name != null,Setmeal::getName,name);
- //添加排序条件,根据更新时间降序排列
- queryWrapper.orderByDesc(Setmeal::getUpdateTime);
-
- setmealService.page(pageInfo,queryWrapper);
-
- //对象拷贝
- BeanUtils.copyProperties(pageInfo,dtoPage,"records");
- List<Setmeal> records = pageInfo.getRecords();
-
- List<SetmealDto> list = records.stream().map((item) -> {
- SetmealDto setmealDto = new SetmealDto();
- //对象拷贝
- BeanUtils.copyProperties(item,setmealDto);
- //分类id
- Long categoryId = item.getCategoryId();
- //根据分类id查询分类对象
- Category category = categoryService.getById(categoryId);
- if(category != null){
- //分类名称
- String categoryName = category.getName();
- setmealDto.setCategoryName(categoryName);
- }
- return setmealDto;
- }).collect(Collectors.toList());
-
- dtoPage.setRecords(list);
- return R.success(dtoPage);
- }
-
- /**
- * 删除套餐
- * @param ids
- * @return
- */
- @DeleteMapping
- @CacheEvict(value = "setmealCache",allEntries = true)
- @ApiOperation(value = "套餐删除接口")
- public R<String> delete(@RequestParam List<Long> ids){
- log.info("ids:{}",ids);
-
- setmealService.removeWithDish(ids);
-
- return R.success("套餐数据删除成功");
- }
-
- /**
- * 根据条件查询套餐数据
- * @param setmeal
- * @return
- */
- @GetMapping("/list")
- @Cacheable(value = "setmealCache",key = "#setmeal.categoryId + '_' + #setmeal.status")
- @ApiOperation(value = "套餐条件查询接口")
- public R<List<Setmeal>> list(Setmeal setmeal){
- LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
- queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId());
- queryWrapper.eq(setmeal.getStatus() != null,Setmeal::getStatus,setmeal.getStatus());
- queryWrapper.orderByDesc(Setmeal::getUpdateTime);
-
- List<Setmeal> list = setmealService.list(queryWrapper);
-
- return R.success(list);
- }
- }
部署架构
前端部署服务器: Nginx
后端部署服务器: Tomcat(内嵌)
使用这三台服务器,具体的软件规划如下:
服务器 | 软件 | 名称 |
---|---|---|
192.168.138.100 | Nginx(部署前端项目、配置反向代理),MySQL(主从复制的主库) | 服务器A |
192.168.138.101 | JDK1.8、Git、Maven、jar(项目jar包基于内嵌Tomcat运行)、MySQL(主从复制的从库) | 服务器B |
172.17.2.94 | Redis(缓存中间件) | 服务器C |
前端部署
1.在服务器A(192.168.138.100)中安装Nginx,将课程资料中的dist目录上传到Nginx的html目录下
将整个dist目录上传至/usr/local/nginx/html目录下
2.修改Nginx配置文件nginx.conf
将nginx.conf配置文件中,将原有的监听80, 82, 8080端口号的虚拟主机注释掉,引入如下的配置信息:
- server {
- listen 80;
- server_name localhost;
-
- location / {
- root html/dist;
- index index.html;
- }
-
- location ^~ /api/ {
- rewrite ^/api/(.*)$ /$1 break;
- proxy_pass http://192.168.138.101:8080;
- }
-
- location = /50x.html {
- root html;
- }
- }
注意:当修改了Nginx配置文件后,一定要重新加载,这样才能生效。
./nginx -s reload
环境变量配置
3.通过nginx访问前端工程
4.反向代理配置
前端工程部署完成之后,我们可以正常的访问到系统的登录页面,点击登录按钮,可以看到服务端发起的请求,请求信息如下:
在之前开发的工程中,是没有/api这个前缀的,那这个时候,在不修改服务端代码的情况下,如何处理该请求呢?
实际上,通过nginx的就可以轻松解决这个问题。
在上述我们配置的nginx.conf中,除了配置了静态资源的加载目录以外,我们还配置了一段反向代理的配置,配置信息如下:
- location ^~ /api/ {
- rewrite ^/api/(.*)$ /$1 break;
- proxy_pass http://192.168.138.101:8080;
- }
这一段配置代表,如果请求当前nginx,并且请求的路径如果是 /api/ 开头,将会被该location处理。而在该location中,主要配置了两块儿信息: rewrite(url重写) 和 proxy_pass(反向代理)。 接下来我们就来解析一下这两项的配置。
1.路径重写rewrite
rewrite ^/api/(.*)$ /$1 break;
这里写的是一个正则表达式,代表如果请求路径是以 /api/
开头,后面的请求路径任意,此时将原始的url路径重写为 /$1
,这里的$1
指代的就是通配符 .* 这一块的内容。比如:
/api/employee/login ------> ^/api/(.*)$ --------> 此时 (.*) 匹配的就是 employee/login ------> 最终重写为/$1 : /employee/login
2.反向代理
proxy_pass http://192.168.138.101:8080;
路径重写后的请求,将会转发到后端的 http://192.168.138.101:8080 服务器中。 而这台服务器中,就部署的是我们的后端服务。
服务端部署
1.在服务器B(192.168.138.101)中安装jdk、git、maven、MySQL,使用git clone命令将git远程仓库的代码克隆下来
确认JDK环境
确认Git环境
确认Maven环境
将我们开发完成的代码推送至远程仓库,并在服务器B中克隆下来
- #创建java代码存放目录
- mkdir -p /usr/local/javaapp
-
- #切换目录
- cd /usr/local/javaapp
-
- #克隆代码 , 需要使用自己的远程仓库
- git clone https://gitee.com/ChuanZhiBoKe/reggie_take_out.git
2.将资料中提供的reggieStart.sh文件上传到服务器B,通过chmod命令设置执行权限
3.执行reggieStart.sh脚本文件,自动部署项目
执行完shell脚本之后,我们可以通过 ps -ef|grep java 指令,查看服务是否启动。
4. 访问系统测试
图片展示问题处理
在上述的测试中,我们发现菜品的图片无法正常展示。原因是因为,在我们的配置文件中,图片信息依然是从 D:/img 中加载的,但是在Linux服务器中,是不存在D盘的。
1.修改文件存储目录
- reggie:
- path: /usr/local/img/
修改完成之后,需要将变动的代码提交到本地仓库,并推送至远程仓库。
2.执行shell脚本,进行自动化部署
3.将本地的测试图片文件夹img(整个文件夹)上传到服务器B的/usr/local目录下
4.访问测试
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。