day01

Nacos的原理和使用

四大功能:

  • 服务发现和服务健康监测:(使服务更容易注册,并通过DNS或HTTP接口发现其他服务,还提供服务的实时健康检查,以防止向不健康的主机或服务实例发送请求。)
  • 动态配置服务:(即Nacos即是服务发现中心,又是配置中心,当然配置写在服务本地也可,写在Nacos里可以利用公用配置简化服务配置过程)
  • 动态DNS服务:动态 DNS 服务支持权重路由,更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。
  • 服务及其元数据管理:管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。

服务发现过程:

  1. 在每个服务启动时会向服务发现中心上报自己的网络位置。在服务发现中心内部会形成一个服务注册表,服务注册表是服务发现的核心部分,是包含所有服务实例的网络地址的数据库。

  2. 服务发现客户端(即每个服务本身)会定期从服务发现中心同步服务注册表 ,并缓存在客户端。

  3. 当需要对某服务进行请求时,服务实例通过该注册表,定位目标服务网络地址。若目标服务存在多个网络地址,则使用负载均衡算法从多个服务实例中选择出一个,然后发出请求。

如何实现一个需求(以查询课程为例)

  1. 需求分析
    分析该模块业务流程(以查询课程为例):
  • 在课程进行列表查询页面输入查询条件查询课程信息
  • 当不输入查询条件时输入全部课程信息。
  • 输入查询条件查询符合条件的课程信息。
  • 约束:本教学机构查询本机构的课程信息。
  1. 分析数据模型
    可以找大模型帮忙分析一下
  • 查询条件:
    包括:课程名称、课程审核状态、课程发布状态
    课程名称:可以模糊搜索
    课程审核状态:未提交、已提交、审核通过、审核未通过
    课程发布状态:未发布、已发布、已下线
    因为是分页查询所以查询条件中还要包括当前页码、每页显示记录数。

  • 查询结果
    包括:课程id、课程名称、任务数、创建时间、是否付费、审核状态、类型,操作
    任务数:该课程所包含的课程计划数,即课程章节数。
    是否付费:课程包括免费、收费两种。
    类型:录播、直播。
    因为是分页查询所以查询结果中还要包括总记录数、当前页、每页显示记录数。

根据以上分析内容创建PO(与数据库字段对等,使用代码生成器,比如Mybatis-plus的生成器)

3.接口设计分析

  • 确定协议: 通常协议采用HTTP,查询类接口通常为get或post,查询条件较少的使用get,较多的使用post,其他的还有(Put,Delete等,采用RestFul风格)。确定content-type:参数以什么数据格式提交,结果以什么数据格式响应。(一般以JSON格式响应)。

Get和Post异同?

同:
GET/POST都是Http请求,都走TCP链接。GET和POST能做的事情是一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的(取决于服务器响应策略)。
异:

  1. 直观的区别就是GET把参数包含在URL中,POST通过request body传递参数。
  2. Get参数显示在Url中,安全性较差。Post不显示,安全性较好。
  3. GET产生一个TCP数据包;POST产生两个TCP数据包。
  • 分析请求参数:(DTO(公司脚手架里的ReqVO),前端给后端的数据) 主要根据对上述数据模型的分析,请求参数为:课程名称、课程审核状态、当前页码、每页显示记录数。根据分析的请求参数定义模型类。

  • 分析响应结果:(RespVO,后端向前端的响应数据) 根据上述对数据模型的分析,响应结果为数据列表加一些分页信息(总记录数、当前页、每页显示记录数)。数据列表中数据的属性包括:课程id、课程名称、任务数、创建时间、审核状态、类型。

关于为什么需要VO、DTO、PO:
alt text

  • 接口中调用Service方法完成业务处理。

Swagger文档的一些常用注解,仿照项目中脚手架也可以。

day02

Mybatis-plus的使用

Mybatis Plus 对 Mapper 层和 Service 层都将常见的增删改查操作都封装好了,只需简单继承即可,使用时只要注入接口就好

BaseMapper层

1
2
@Resource
private UserMapper userMapper;

在公司脚手架的下段代码中,操作数据库的逻辑都在这个接口里用default方法写。(BaseMapperX是公司封装的MyBatis-plus包,没有封装的情况继承的是BaseMapper),继承于BaseMapper的方法有:userMapper.insert(),select(),delete(),upadate等变体,其中查询语句多与QueryWrapper相关。

1
2
3
4
5
6
public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {

default AdminUserDO selectByUsername(String username) {
return selectOne(new LambdaQueryWrapper<AdminUserDO>().eq(AdminUserDO::getUsername, username));
}
}

BaseMapper相关查询方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 根据 ID 查询
T selectById(Serializable id);
// 通过 Wrapper 组装查询条件,查询一条记录
T selectOne(Wrapper<T> queryWrapper);

// 查询(根据ID 批量查询)
List<T> selectBatchIds(Collection<? extends Serializable> idList);
// 通过 Wrapper 组装查询条件,查询全部记录
List<T> selectList(Wrapper<T> queryWrapper);
// 查询(根据 columnMap 来设置条件)
List<T> selectByMap(Map<String, Object> columnMap);
// 根据 Wrapper 组装查询条件,查询全部记录,并以 map 的形式返回
List<Map<String, Object>> selectMaps(Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(Wrapper<T> queryWrapper);

// =========================== 分页相关 ===========================
// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询总记录数
Integer selectCount(Wrapper<T> queryWrapper);

参数说明:
Collection<? extends Serializable> : 主键ID列表(不能为Null)

Service层

定义UserService 接口 ,让其继承自IService

1
2
public interface UserService extends IService<User> {
}

再定义实现类UserServiceImpl,让其继承自 ServiceImpl, 同时实现 UserService接口,这样就可以让UserService拥有了基础的CRUD功能,当然,实际开发中,业务会更加复杂,就需要向IService接口自定义方法并实现:

在公司脚手架中未见这种实现方式,相关数据库操作似乎只在BaseMapper方式中实现,另外这一类的Service是否能写业务相关的逻辑呢,在xuecheng项目里这一类的Service里都只写CRUD操作。

1
2
3
@Service
public class UserServiceImpl extends ServiceImpl<**UserMapper**, User> implements UserService {
}

Service层CRUD方法:
saveBatch操作都是循环插入,但是MyBatis-plus帮你优化插入性能,内部会将每次的插入语句缓存起来,等到达到 1000 条的时候,才会统一推给数据库。
增加数据:save开头,删除remove开头,更新update开头,查询下面详细说明

1
2
3
4
5
6
7
8
9
10
11
sava(T) : boolean
// 伪批量插入,实际上是通过 for 循环一条一条的插入
savaBatch(Collection<T>) : boolean
// 伪批量插入,int 表示批量提交数,默认为 1000
savaBatch(Collection<T>, int) : boolean
// 新增或更新(单条数据)
saveOrUpdate(T) : boolean
// 批量新增或更新
saveOrUpdateBatch(Collection<T>) : boolean
// 批量新增或更新(可指定批量提交数)
saveOrUpdateBatch(Collection<T>, int) : boolean

注入UserService进行使用:

1
2
@Autowired
private UserService userService;

Service相关查询方法

  • getXXX : get 开头的方法,用于查询一条数据。
  • listXXX : list 开头的方法,用于查询多条数据;
  • pageXXX : page 开头的方法,用于分页查询;
  • count : 用于查询总记录数;

分页查询:(进行分页查询之前需要在MybatisPlusConfig配置类中,添加分页插件 PaginationInnerInterceptor),记得在MyBatisPlusConfig中添加@MapperScan("com.xxx.xxx.mapper")注解,告诉MyBatisPlus应该扫描哪里。

BaseMapper 提供的分页查询相关的方法如下:

1
2
3
4
// 分页查询,page 用于设置需要查询的页数,以及每页展示数据量,wrapper 用于组装查询条件
IPage<T> selectPage(IPage<T> page, Wrapper<T> queryWrapper);
// 同上,区别是用 map 来接受查询的数据
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, Wrapper<T> queryWrapper);

参数说明:

类型 参数名 描述
Wrapper queryWrapper 实体对象封装操作类,查询条件
IPage page 分页查询条件,page的相关属性设置条件,如size和Current

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 组装查询条件,当我想无条件查询的时候,queryWrapper为Null或者不设置条件都可以
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// where age = 30
queryWrapper.eq("age", 30);

// 查询第 2 页数据,每页 10 条
Page<User> page = new Page<>(2, 10);
//或者这样设置 page.setCurrent(2), page.setSize(10);

page = userMapper.selectPage(page, queryWrapper);
System.out.println("总记录数:" + page.getTotal());
System.out.println("总共多少页:" + page.getPages());
System.out.println("当前页码:" + page.getCurrent());
// 当前页数据
List<User> users = page.getRecords();

Service层封装的相关分页方法(其实与BaseMapper类似,BaseMapper是selectPage(),Service是page(),二者入参类似):

1
2
3
4
5
6
7
IPage<T> page(IPage<T> page);
// 条件分页查询
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 无条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page);
// 条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 组装查询条件
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// where age = 30
queryWrapper.eq("age", 30);

// 查询第 2 页数据,每页 10 条
Page<User> page = new Page<>(2, 10);

page = userService.page(page, queryWrapper);
System.out.println("总记录数:" + page.getTotal());
System.out.println("总共多少页:" + page.getPages());
System.out.println("当前页码:" + page.getCurrent());
// 当前页数据
List<User> users = page.getRecords();

多表联查(join):

UserMapper.xml 中编写关联语句,以及需要映射的对象,内容如下(注:多表联查涉及到xml,所以只用BaseMapper层):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.quanxiaoha.mybatisplusdemo.mapper.UserMapper">

<resultMap id="orderMap" type="com.quanxiaoha.mybatisplusdemo.model.OrderVO">
<result property="userName" column="name"/>
<result property="userAge" column="age"/>
<result property="userGender" column="gender"/>
<result property="orderId" column="order_id"/>
<result property="userId" column="user_id"/>
<result property="goodsName" column="goods_name"/>
<result property="goodsPrice" column="goods_price"/>
</resultMap>

<select id="selectOrders" resultMap="orderMap"> //selectOrders就是查询方法名
select o.order_id, o.user_id, o.goods_name, o.goods_price, u.name, u.age, u.gender
from t_order as o left join t_user as u on o.user_id = u.id
</select>
</mapper>
  • 关于xml文件的创建:

使用MyBatis-Plus的Generator开发持久层,每张表对应的PO类、Mapper接口、Mapper的xml文件。PO类对应数据库的每张表,每张表需要创建一个Mapper接口和Mapper的xml映射文件 。

  • 关于xml文件的属性:

namespace标签内容=“前文BaseMapper接口的包路径名”
resultMap、type 标签定义查询语句返回结果集的类型,将实体类字段与数据库表中字段进行关联映射
select标签的id为下文使用的方法名,resultMap为上文定义的查询结果类型

  • 创建完了 UserMapper.xml,还要在applicatoin.yml 中添加如下配置,告诉 Mybatis-Plus 框架去扫描这些xml文件:
1
2
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml //当放在Resource文件下是这么写

联表查询代码示例:

1
List<OrderVO> orderVOS = userMapper.selectOrders(); //Mapperxml的selectId名

分页联表查询(分页+联表+条件查询):

实际开发场景中,很多关联查询都需要结合分页一起使用,假设上面展示的数据需要分页展示且需要支持条件查询,要怎么做呢?

  • 定义关联查询分页方法(同样基于UserMapper层):
1
2
3
4
5
public interface UserMapper extends BaseMapper<User> {
//...
IPage<OrderVO> selectOrderPage(IPage<OrderVO> page, @Param(Constants.WRAPPER) QueryWrapper<OrderVO> wrapper);
//...
}

Tips: 入参:Mybatis-plus提供的分页类IPage与QueryWrapper (用于组装 where 条件)。

  • UserMapper.xml中创建该方法对应的关联查询:
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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.quanxiaoha.mybatisplusdemo.mapper.UserMapper">

<resultMap id="orderMap" type="com.quanxiaoha.mybatisplusdemo.model.OrderVO">
<result property="userName" column="name"/>
<result property="userAge" column="age"/>
<result property="userGender" column="gender"/>
<result property="orderId" column="order_id"/>
<result property="userId" column="user_id"/>
<result property="goodsName" column="goods_name"/>
<result property="goodsPrice" column="goods_price"/>
</resultMap>

//...

<select id="selectOrderPage" resultMap="orderMap">
select u.name, u.age, u.gender, o.order_id, o.goods_name, o.goods_price
from t_user as u left join t_order as o on u.id = o.user_id
${ew.customSqlSegment} //占位符,表示能扩展QueryWrapper的条件
</select>

//...

</mapper>

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Autowired
private UserMapper userMapper;

@Test
void testSelectOrdersPage() {
// 查询第一页,每页显示 10 条
Page<OrderVO> page = new Page<>(1, 10);
// 注意:一定要手动关闭 SQL 优化,不然查询总数(Count(*))的时候只会查询主表
page.setOptimizeCountSql(false);
// 组装查询条件 where age = 20
QueryWrapper<OrderVO> queryWrapper = new QueryWrapper<>();
queryWrapper.ge("age", 20);

IPage<OrderVO> page1 = userMapper.selectOrderPage(page, queryWrapper);

System.out.println("总记录数:" + page1.getTotal());
System.out.println("总共多少页:" + page1.getPages());
System.out.println("当前页码:" + page1.getCurrent());
System.out.println("查询数据:" + page1.getRecords());
}

批量插入(SQL注入器实现):

MySQL 支持一条 SQL 语句可以批量插入多条记录,格式如下:

1
INSERT INTO `t_user` (`name`, `age`, `gender`) VALUES ('ldp01', 0, 1), ('ldp02', 0, 1), ('ldp03', 0, 1);

Mybatis的伪批量插入方法saveBatch(),源码实例如下:

1
2
3
4
5
6
7
8
public boolean saveBatch(Collection<T> entityList, int batchSize) {
// 获取预编译的插入 SQL
String sqlStatement = this.getSqlStatement(SqlMethod.INSERT_ONE);
// for 循环执行 insert
return this.executeBatch(entityList, batchSize, (sqlSession, entity) -> {
sqlSession.insert(sqlStatement, entity);
});
}

[SQL注入器方法](https://www.quanxiaoha.com/mybatis-plus/mybatisplus-batch-insert.html)

QueryWrapper构造:
QueryWrapper

常用注解:

  • @TableName:作用:表名注解,标识实体类(PO)对应的表。
  • @TableId: 作用:主键注解,@TableId(type = IdType.AUTO)
  • @TableField:作用:指定数据库字段注解(非主键)。
  • @TableLogic:作用:逻辑删除注解。
  • @Version:作用:乐观锁注解。

day03

Git工作流

在工作中推荐使用GitHub flow协作方式,即不在主仓库的分支上开发,而是fork到自己workspace下。每次开发,需要先checkout一个新的分支,commit之后推送到自己的仓库,再向主仓库提Merge Request。所以开发过程中对于大多数项目来说,都会经过如下步骤:

  1. Fork
  2. Create a branch
  3. Add Commits
  4. Open a Merge Request
  5. Code review
  6. Merge
  7. Deploy

流程详解:

  1. Fork主仓

项目组长建立代码的主仓库,剩余小组开发人员首先要对其仓库进行Fork。Fork之后,从自己的仓库进行clone,一般来说一个仓库会有几个分支,比如:

  • master分支为主分支(保护分支),禁止直接在master上进行修改代码和提交,此分支的代码可以随时被发布到线上。
  • develop/dev分支为测试分支或者叫做合并分支,所有开发完成需要提交测试的功能合并到该分支,该分支包含最新的更改。
  • feature 分支为开发分支,大家根据不同需求创建独立的功能分支,开发完成后合并到develop分支;
  • fix 分支为bug修复分支,需要根据实际情况对已发布的版本进行漏洞修复;
    一般而言,有master分支和dev分支便可。
  1. 添加远程仓库

首先我们需要把远程主仓库给添加进来,方便以后push和pull,例如将项目组长所建立的主仓库添加进来:(upstream是远程仓库的代称,可随意命名)

1
2
3
4
5
6
$ git remote add upstream ssh://主仓库地址
$ git remote -v
origin ssh://git@gitlab.xxx.cn:8022/aaa/xxx.git (fetch) #自己的
origin ssh://git@gitlab.xxx.cn:8022/aaa/xxx.git (push) #自己的
upstream ssh://git@gitlab.xxx.cn:8022/aaa/xxx.git (fetch) #项目组长的
upstream ssh://git@gitlab.xxx.cn:8022/aaa/xxx.git (push) #项目组长的

之后可以通过以下方式push和pull代码,切记不要push主仓库。

1
2
git push origin HEAD:分支
git pull upstream develop // 每天开发之前pull一下
  1. 查看分支和状态
1
2
3
git branch #查看本地分支
git branch -a #查看远程分支
git status #查看git状态
  1. 开始工作新建分支
    不要一股脑的在develop分支上直接开发功能,先打开git bash,先pull一下主仓库保证代码最新,之后先新建一个分支。
1
2
git checkout -b 分支名 //新建分支名并进入该分支
git checkout 分支名 //切换到现有分支
  1. 提交代码
    主仓库的更新速度比你写代码快,也就是说很多情况主仓库的代码都是要比你的新,所以你要先pull主仓库的代码进行更新,但是直接Pull的话也许会产生冲突,需要你将代码放入暂存区Stash进行保管
1
2
git stash save "msg" //保存你的代码
git pull upstream master //更新你的代码

更新完代码之后,你需要将之前存储在暂存区的代码进行恢复。

1
2
3
4
5
git stash list   #查看stash了哪些存储
git stash show #显示做了哪些改动
git stash apply #将某个暂存取出,git stash apply stash@{$num}
git stash drop stash@{$num} #丢弃stash@{$num}存储,从列表中删除这个存储
git stash clear #删除所有缓存的stash
  1. commit并且push
1
2
3
4
git diff //用于查看修改的文件
git add . //add全部修改内容到暂存区
git commit -m "msg" //提交更改
git push origin HEAD:分支名 //记住,这一步是push到自己的远程仓库
  1. 提PR(Pull Request)
    到自己的远程仓库里,向主仓库提交分支合并请求,一般是自己这次工作创建的分支合并到主仓库的develop分支,然后选择代码审查人员进行CodeReview。

day04

Docker基本命令

docker命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
docker images #查看镜像
docker pull <镜像名:tag> #下载镜像并指定版本
docker run --name webserver -p 80:80 -d nginx #运行镜像并指定端口-p,后台运行-d
docker ps #查看当前运行的容器
docker save -o nginx_latest.tar nginx:latest #将镜像保存为tar文件
docker load --input nginx_latest.tar #将保存的tar文件加载为镜像
docke search nginx #在Docker Hub上搜索镜像
docker rmi <镜像名/镜像ID> #删除指定的镜像
docker rmi -f <镜像名/镜像ID> #强制删除指定的镜像

docker stop <容器ID/name> #停止运行指定ID的容器
docker start <容器ID/name> #开始运行指定ID的容器
docker restart <容器ID/name> #重启指定的容器
docker rm <容器ID/name> #删除指定的容器,不能是正在运行的
docker logs -f --tail=20 <容器名> #查看末尾20行日志

docker exec -it <容器ID/name> bash #进入正在运行的容器进行交互,交互逻辑和Linux类似,但是没有编辑器,这就是容器与数据耦合带来的结果
docker exit #在容器内部退出

ctrl+p/ctrl+q #退出容器,保持运行状态

为了将容器内数据与容器解耦合,需要数据卷:

数据卷(volume)是一个虚拟目录,指向宿主机文件系统中的某个目录

  • 可供容器使用的特殊目录,可以在容器之间共享和重用
  • 对数据卷的修改会立即生效,对数据卷的更新 不会影响镜像
  • 卷会一直存在,直到没有容器使用

在创建容器时,可以通过 -v 参数来挂载一个数据卷到某个容器内目录,命令格式如下:

1
docker run -d -v /data/webserver:/usr/share/nginx/html nginx #将主机的/data/webserver挂载到容器的/usr/share/nginx/html目录

docker volume [COMMAND]命令用于管理Docker数据卷:

  • create volumeName 创建一个volume,一般关联宿主机/var/lib/docker/volumes/目录下
  • inspect volumeName 显示一个或多个volume的信息
  • ls 列出所有volume
  • prune 删除未使用的volume
  • rm volumeName 移除一个volume

示例,创建并运行一个MySQL容器,将宿主机目录直接挂载到容器:

  1. 通过docker pull mysql:5.7.25 拉取mysql镜像
  2. 创建目录/tmp/mysql/data
  3. 创建目录/tmp/mysql/conf,并在该目录下新建文件hmy.cnf,写入如下内容,或者(docker volume create html,比如挂载nginx)
  4. -v 宿主机目录:容器内目录
  5. -v 宿主机文件:容器内文件
  6. -v volume名称:容器内目录
1
2
3
4
5
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000
  1. 将上述创建的目录挂载到mysql容器内部
1
2
3
4
5
6
docker run --name mysql 
-e MYSQL_ROOT_PASSWORD=123
-p 3307:3306
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf
-v /tmp/mysql/data:/var/lib/mysql
-d mysql:5.7.25

ApiFox的使用

看ApiFox的官方文档即可。

day05

tips:@ApiModelProperty不生效时,将POJO中属性字段名改为小写的就好。