JavaWeb学习
文章目录
- 学习路线
- Maven
- Maven 坐标
- 依赖管理
- 生命周期
- 1. Web入门
- HTTP协议
- Tomcat
- 2. 请求响应
- - 请求
- 1. 简单参数
- 2. 实体参数
- 3. 数组集合参数
- 4. 日期参数
- 5.Json参数
- 6.路径参数
- - 响应
- 响应数据
- 3.分层解耦
- 三层架构
- 分层解耦
- IOC & DI入门
- IOC详解
- DI详解
- 4. MySQL
- 数据库设计
- 1. MySQL概述
- 2. 数据库设计-DDL
- 数据库操作
- 1. 数据库操作-DML
- 2. 数据库查询操作-DQL
- 3. 多表设计
- 4. 多表查询
- 5. 事务
- 数据库优化
- 1. 索引
- 5. Mybatis
- 5.1 Mybatis入门
- 1. 快速入门
- 2. JDBC介绍
- 3. 数据库连接池
- 4. lombok
- 5.2 Mybatis基础增删改查
- 根据主键删除
- 新增
- 更新
- 查询
- 条件查询
- 5.3 XML映射文件
- 5.4 Mybatis动态SQL
- if
- foreach
- sql&include
- 6. 案例
- 6.1 环境搭建
- 6.2 员工管理
- 分页查询
- 6.3 文件上传
- 本地存储
- 云存储
- 6.4 配置文件
- 参数配置化
- yml配置文件(推荐)
- @ConfigurationProperties
- 7. 登录校验
- 7.1 登录标记
- 会话技术
- 7.2 统一拦截
- 过滤器Filter
- 拦截器Intercepter
- 7.3 异常处理
- 全局异常处理器
- 8. 事务管理&AOP
- 事务管理
- Spring事务管理
- 事务进阶
- AOP基础
- AOP概述
- 实现
- AOP核心概念
- AOP进阶
- 1.通知类型
- 2.通知顺序
- 3.切入点表达式
- 4.连接点
- 9. Springboot原理
- 9.1 配置优先级
- 9.2 Bean管理
- 获取bean
- bean作用域
- 第三方bean
- 9.3 SpringBoot原理
- 起步依赖原理
- 自动配置原理
- Web后端开发-总结
- 10. Maven高级
- 10.1 分模块设计与开发
- 10.2 继承与聚合
- 继承
- 聚合
- 10.3 私服
- 介绍
- 资源上传与下载
Written with StackEdit中文版.
学习路线
html、css、js --> Ajax、Axios -->Vue、Element–>前端工程化,Vue脚手架
Maven–>Springboot基础(基于Springboot进行讲解Spring的IOC、DI等)–>Springboot SpringMVC基础–>MySQL(基于产品原型和需求分析,设计数据库表)–>JDBC Mabatis–>Web案例(基于Springboot整合SSM,根据页面原型、需求、接口文档编写接口)–>会话跟踪技术(Cookie、Session,令牌技术JWT)–>Filter Intercepter(令牌的统一拦截校验)–>ADP–>Springboot原理
Maven
jdk 11
构建和管理java项目的工具
项目对象模型POM
Maven 坐标
groupid:maven项目隶属组织名称(通常是域名反写)
artifactid
version
依赖管理
- 依赖传递
直接依赖-间接依赖
A依赖B,B依赖C,那么A会依赖C - 排除依赖
主动断开依赖的资源,被排除的资源无需指定版本<exclusions><exclusion>
- 依赖范围
依赖的jar包,默认情况下可以在任何地方使用,可以通过<scope>...<scope>
设置其作用范围- 主程序范围有效(main文件夹范围内)
- 测试程序范围有效(test文件夹范围内)
- 是否参与打包运行(package指令范围内)
生命周期
maven的生命周期是为了对所有的maven项目构建过程进行抽象和统一
maven有三套独立的生命周期
- clean:清理工作
- default:核心工作,如:编译、测试、打包、安装、部署等
- site:生成报告、发布站点等
在同一套生命周期中,阶段是有顺序的,后面的阶段依赖于前面的阶段。运行后面的阶段时,前面的阶段都会运行
1. Web入门
HTTP协议
超文本传输协议,规定了浏览器和服务器之间数据传输的规则
- 请求协议
请求行:请求方式、资源路径、协议
请求头
请求体:POST请求,存放请求参数
GET请求,请求参数在请求行中,没有请求体。GET请求大小有限制
POST请求,请求参数在请求体中,POST请求大小没有限制
- 响应协议
响应行 响应头 响应体
响应行 200 404 500
响应头
状态码大全 https://cloud.tencent.com/developer/section/1190137
- 协议解析
Tomcat web服务器对http协议进行封装
Tomcat
轻量级Web服务器,支持Servlet/JSP少量JavaEE规范。也被称为web容器、servlet容器
- 起步依赖
包含了对应业务开发所需要的常见依赖 - 内嵌Tomcat服务器
2. 请求响应
前端发出的请求,都会先传给DispatcherServlet(前端控制器),DispatcherServlet再转给Controller程序。
TomCat会对前端的请求进行解析,并将解析后的所有信息封装到HttpServletRequest对象中。根据HttpServletResponse对象设置要响应的数据。
- 请求(HttpServletRequest):获取请求数据
- 响应(HttpServletResponse):设置相应数据
- BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端
- CS架构:Client/Server,客户端/服务器架构模式
- 请求
1. 简单参数
- 原始方式
在原始的web程序中,获取请求参数,需要通过HttpServletRequest
对象手动获取
繁琐、手动类型转换 - Springboot方式
简单参数:参数名与形参变量名相同,定义形参即可接收参数
在接受过程中会自动进行类型转换 - RequestParam注解
- 如果方法形参名称与请求参数名称不匹配,可以使用
@RequestParam
完成映射 @RequestParam
中的required属性默认为true,代表该请求参数必须传递。如果该参数是可选的,可以将required设置为false。
public String simpleParam(@RequestParam(name = "name",required = false) String username, Integer age){ // 获取请求参数 System.out.println(username+":"+age); return "OK"; }
- 如果方法形参名称与请求参数名称不匹配,可以使用
2. 实体参数
- 简单实体对象
请求参数名与形参对象属性名相同,定义POJO接收即可 - 复杂实体对象
请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套POJO属性参数@RequestMapping("/complexPojo") public String complexPojo(User user){ System.out.println(user); return "OK"; }
3. 数组集合参数
- 数组参数
请求参数名与形参数组名称相同,且请求参数为多个,定义数组类型形参即可接收参数@RequestMapping("/arrayParam") public String arrayParam(String[] hobby){ System.out.println(Arrays.toString(hobby)); return "OK"; }
- 集合参数
请求参数名与形参集合名称相同,且请求参数为多个,@RequestParam
绑定参数关系
默认封装到数组集合,如果要用list,就要加上注解@RequestMapping("/listParam") public String listParam(@RequestParam List<String> hobby){ System.out.println(hobby); return "OK"; }
4. 日期参数
- 使用
@DataTimeFormat
注解完成日期参数格式转换,存到LocalDateTime
对象中@RequestMapping("/dataParam") public String dataParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime){ System.out.println(updateTime); return "OK"; }
5.Json参数
- JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数,需要使用
@RequestBody
标识@RequestMapping("/jsonParam") public String jsonParam(@RequestBody User user){ System.out.println(user); return "OK"; }
6.路径参数
- 通过请求URL直接传递参数,使用{…}来标识该路径参数,需要使用
@PathVariable
获取路径参数@RequestMapping("/path/{id}") public String pathParam(@PathVariable Integer id){ System.out.println(id); return "OK"; }
@RequestMapping("/path/{id}/{name}") public String pathParam2(@PathVariable Integer id,@PathVariable String name){ System.out.println(id+":"+name); return "OK"; }
- 响应
响应数据
- @ResponseBody
类型:方法注解、类注解
位置:Controller方法上/类上
作用:将方法返回值直接响应,如果返回值类型是 实体对象/集合,将会转换成JSON格式响应
说明:@RestController=@Controller+@ResponseBody - 统一响应结果
Result对象
Result(code,msg,data)
3.分层解耦
三层架构
- Controller:控制层,接受前端发送到请求,对请求进行处理,并响应数据
- Service:业务逻辑层,处理具体的业务逻辑
- Dao:数据访问层(Data Access Object)(持久层),负责数据访问操作,包括数据的增、删、改、查
分层解耦
-
- 内聚:软件中各个功能模块内部的功能联系
- 耦合:软件中各个层/模块之间的依赖、关联程度
- 软件设计原则:高内聚低耦合
在容器里放耦合代码,谁需要调用就来容器里面调用
- 控制反转:Inversion of Control,IOC
对象的创建控制权由程序自身转移到外部(容器) - 依赖注入:Dependency Injection,DI
容器为应用程序提供运行时,所依赖的资源,称之为依赖注入 - Bean对象:IOC容器内创建、管理的对象,称之为bean
IOC & DI入门
- Service层及Dao层的实现类,交给IOC容器管理 添加
@Component
注解
@Component 将当前类交给IOC容器管理,成为容器中的bean - 为Controller及Service注入运行时依赖的对象,添加
@Autowired
注解
IOC详解
要把某个对象交给IOC容器管理,需要在对应类上加上如下注解之一:
@Component
,@Controller
,@Service
,@Repository
- 声明bean的时候,可以通过value属性指定bean的名字,没有指定时默认类名是类名首字母小写
- 声明控制器bean只能用@Controller
- 组件扫描
- Bean组件扫描
- 声明bean的四大注解,需要被组件扫描注解
@ComponentScan
扫描 - @ComponentScan注解虽然没有显示配置,但实际上已经包含在了启动类生命注解@SpringBootApplication中,默认扫描的范围是启动类所在包及其子包
- 声明bean的四大注解,需要被组件扫描注解
DI详解
- @Autowired注解,默认按照类型进行,如果存在多个相同类型的bean,就会报错
- 解决方案
@Primary
想让哪个生效,就在哪个类前加该注解@Qualifier
在@Autowired前添加@Qualifier(“类名首字母小写”)@Resource
按照名称进行@Resource(name=“类名首字母小写”)
- @Resource 与 @Autowired 区别
- @Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
- @Autowired 默认是按照类型注入,而@Resource默认按照名称注入
4. MySQL
- DBMS 数据库管理系统
数据库设计
1. MySQL概述
-
企业开发使用方式
net start mysql80
mysql -u用户名 -p密码 [-h数据库服务器IP地址 -P端口号]
-
数据模型
- 关系型数据库(RDBMS):建立在关系模型基础上,由多张相互连接的二维表组成的数据库
- 创建数据库
create database db01;
创建表结构
-
SQL简介
分类 用途 DDL 数据定义语言,操作表结构 DML 数据操作语言,增删改 DQL 数据查询语言 DCL 数据控制语言,创建用户、控制访问权限
2. 数据库设计-DDL
-
数据库
- 查询
查询所有数据库:show databases;
查询当前数据库:select database();
- 使用
use 数据库名;
- 创建
create database [if not exists] 数据库名;
- 删除
drop database [if exists] 数据库名;
- 上述语法中的database,也可以替换成schema
[]代表可写可不写
- 查询
-
数据类型
数值类型、字符串类型、日期时间类型
-
DDL 表(创建、查询、修改、删除)
- 创建
create table 表名(字段1 字段类型 [约束] [comment 字段1注释],......字段n 字段类型 [约束] [comment 字段n注释],)[comment 表注释];
约束是作用于表中字段上的规则,用于限制存储在表中的数据。保证数据库中数据的正确性、有效性和完整性
create table tb_user( id int primary key comment 'ID 唯一标识', username varchar(20) not null unique comment '用户名', name varchar(10) not null comment '姓名', age int comment '年龄', gender char(1) default '女' comment '性别' )comment '用户表';
- 查询
查询当前数据库所有表show tables;
查询表结构desc 表名;
查询建表语句show create table 表名;
- 修改
添加字段:alter table 表名 add 字段名 类型(长度) [comment 注释] [约束];
修改字段类型alter table 表名 modify 字段名 新数据类型(长度);
修改字段名和字段类型alter table 表名 change 旧字段名 类型(长度) [comment 注释] [约束];
删除字段alter table 表名 drop column 字段名;
修改表名rename table 表名 to 新表名;
- 删除
drop table [if exits] 表名;
-
datalog 图形化界面
intelliJ IDEA 嵌入
数据库操作
1. 数据库操作-DML
Data Munipulation Language
- 添加数据(INSERT)
指定字段添加数据:insert into 表名 (字段名1,字名2) values(值1,值2);
全部字段添加数据:insert into 表名 values (值1,值2,….);
批量添加数据(指定字段):insert into 表名(字段名1,字段名2) values (值1,值2),(值1,值2);
批量添加数据(全部字段):insert into 表名 values(值1,值2,...),(值1,值2,...);
- 修改数据(UPDATE)
修改数据:update 表名 set 字段名1=值1,字段名2=值2,…[where 条件];
- 删除数据(DELETE)
DELETE语句不能删除某一个字段的值(如果要操作,可以使用UPDATE,将该字段的值置为NULLdelete form 表名 [where 条件];
2. 数据库查询操作-DQL
关键字 SELECT
-
基本查询
- 查询多个字段:
select 字段1,字段2,字段3 from 表名;
- 查询所有字段(通配符):
select * from 表名;
*号尽量不使用 - 设置别名:
select 字段1 as 别名1,字段2 as 别名2 from 表名;
as可省略 - 去除重复记录:
select distinct 字段列表 from 表名;
- 查询多个字段:
-
条件查询
select 字段列表 from 表名 where 条件列表;
-
聚合函数
将一列数据作为一个整体,进行纵向计算
不能对null值进行计算select 聚合函数(字段列表) from 表名;
函数 功能 count 统计数量 max 最大值 min 最小值 sum 求和 avg 平均值 - count
count()(推荐)、 count(常量)、 count(列名)
COUNT(常量) 和 COUNT()表示的是直接查询符合条件的数据库表的行数
- count
-
分组查询
select 分组字段[聚合函数] from 表名 [where 条件] group by 分组字段名 [having 分组后过滤条件];
/*先查询入职时间在'2015-01-01'(包含)以前的员工,并对结果根据职位分组,获取员工数量大于等于2的职位*/ select job,count(*) from tb_emp where entrydate<='2015-01-01' group by job having count(*)>=2;
- where与having区别
执行时机不同:where是分组之前进行过滤,不满足where条件,不参与分组;而having是分组之后对结果进行过滤。
判断条件不同:where不能对聚合函数进行判断,而having可以。 - 注意事项
分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义
执行顺序:where>聚合函数>having
- where与having区别
-
排序查询
select 分组字段[聚合函数] from 表名 [where 条件] group by 分组字段名 order by 字段1 排序方式1,字段2 排序方式2 ...;
- 排序方式:ASC:升序(默认值),DESC:降序
- 如果是多字段排序,当第一个字段值相同时,才会根据第二个字段进行排序。
-
分页查询
select 字段列表 from 表名 limit 起始索引,查询记录数;
- 起始索引从0开始,起始索引=(查询页码-1)*每页显示记录数。
- 分页查询是数据库的方言,不同的数据库有不同的实现,MySOL中是LIMIT。
- 如果查询的是第一页数据,起始索引可以省略,直接简写为limit 10。
select if(gender=1,'女性员工','男性员工')性别,count(*) from tb_emp group by gender;
3. 多表设计
-
一对多
一对多关系实现:在数据库表中多的一方,添加字段,来关联一的一方的主键 -
外键约束
–创建表时指定create table 表名(字段名 数据类型...[constraint][外键名称]foreign key(外键字段名)references 主表(字段名) );
--建完表后,添加外键 alter table 表名 add constraint 外键名称 foreign key(外键字段名)references 主表(字段名)
- 缺点
影响增、删、改的效率(需要检查外键关系)仅用于单节点数据库,不适用与分布式、集群场景。
容易引发数据库的死锁问题,消耗性能。
- 缺点
-
一对一
- 案例:用户 与 身份证信息 的关系
- 关系:一对一关系,多用于单表拆分,将一张表的基础字段放在一张表中,其他字段放在另一张表中,以提升操作效率
- 实现:在任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE)
-
多对多
- 实现:建立第三张中间表,中间表至少包含两个外键,分别关联两方主键
4. 多表查询
笛卡尔积
select * from tb_emp,tb_dept;
消除无效的笛卡尔积
select * from tb_emp,tb_dept where tb_emp.dept_id=tb_dept.id;
-
- 连接查询
- 内连接:查询A、B交集数据
隐式内连接:select字段列表 from 表1,表2where 条件...
显式内连接:查询员工的姓名,及所属的部门名称 select tb_emp.name,tb_dept.name from tb_emp,tb_dept where tb_emp.dept_id=tb_dept.id; 起别名 select e.name,d.name from tb_emp e,tb_dept d where e.dept_id=d.id;
select字段列表 from 表1[inner]join 表2 on 连接条件...
select tb_emp.name,tb_dept.name from tb_emp inner join tb_dept on tb_emp.dept_id = tb_dept.id;
- 外连接
- 左外连接:以左表为基准,完全包括左表的数据和交集数据
select 字段列表 from 表1 left [outer]join 表2 on 连接条件 …;
- 右外连接:以右表为基准,完全包括右表的数据和交集数据
select 字段列表 from 表1 right [outer]join 表2 on 连接条件...;
- 左外连接:以左表为基准,完全包括左表的数据和交集数据
-
- 子查询
介绍:SQL语句中嵌套select语句,称为嵌套查询,又称子查询。
形式:select *from tl where column1=(select columnl from t2 ... );
子查询外部的语句可以是insert/update/delete/select 的任何一个,最常见的是 select。
- 标量子查询:子查询返回的结果为单个值
常用操作符:= <> > >= < <=
select * from tb_emp where dept_id = (select id from tb_dept where name='教研部') ;
- 列子查询:子查询返回的结果为一列
常用操作符:in、not in
select * from tb_emp where dept_id in (select id from tb_dept where name = '教研部' or name='咨询部');
- 行子查询:子查询返回的结果为一行
常用操作符:= <> in not in
select * from tb_emp where (entrydate,job) = (select entrydate,job from tb_emp where name='韦一笑');
- 表子查询:子查询返回的结果为多行多列
常见操作符:in
select *,tb_dept.name from (select * from tb_emp where entrydate > '2006-01-01') e ,tb_dept where tb_dept.id = e.dept_id;
- 子查询
5. 事务
事务 是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作 要么同时成功,要么同时失败
。
- 事务控制
- 开启事务:
start transaction;/begin;
- 提交事务:
commit;
- 回滚事务:
rollback;
- 开启事务:
- 四大特性(ACID)
数据库优化
1. 索引
索引(index)是帮助数据库 高效获取数据 的 数据结构
-
介绍
- 优缺点
优点:降低查询排序成本
缺点:降低增删改效率
- 优缺点
-
结构
MySQL数据库支持的索引结构有很多,如:Hash索引、B+Tree索引、Ful-Text索引等。我们平常所说的索引,如果没有特别指明,都是指默认的 B+Tree 结构组织的索引。 -
B+Tree(多路平衡搜索树)
每一个节点,可以存储多个key(有n个key,就有n个指针)
所有的数据都存储在叶子节点,非叶子节点仅用于索引数据。
叶子节点形成了一颗双向链表,便于数据的排序及区间范围查询。
-
语法
- 创建索引:
create [unique]index 索引名 on 表名(字段名,.);
- 查看索引:
show index from 表名;
- 删除索引:
drop index 索引名 on 表名;
- 注意
主键字段,在建表时,会自动创建主键索引。
添加唯一约束时,数据库实际上会添加唯一索引。
- 创建索引:
5. Mybatis
5.1 Mybatis入门
1. 快速入门
-
准备工作(创建springboot工程、数据库表user、实体类User)
-
引入Mybatis的相关依赖,配置Mybatis(数据库连接信息)
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/mybatis spring.datasource.username=root spring.datasource.password=1234
-
编写SQL语句(注解/XML)
定义持久层接口,加上Mapper注解定义接口@Mapper public interface UserMapper {@Select("select * from user")public List<User> list(); }
2. JDBC介绍
JDBC:(Java DataBase Connectivity),就是使用Java语言操作关系型数据库的一套API。
各个数据库厂商去实现这套接口,提供数据库驱动jar包,
我们可以使用这套接口(JDBC)编,真正执行的代码是驱动jar包中的实现类
3. 数据库连接池
数据库连接池是个容器,负责分配、管理数据库连接(Connection)
它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个
释放 空闲时间超过最大空闲时间 的连接,来避免因为没有释放连接而引起的数据库连接遗漏
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-3-starter</artifactId> <version>1.2.21</version>
</dependency>
4. lombok
5.2 Mybatis基础增删改查
根据主键删除
```
@Delete("delete from emp where id=#{id}") //占位符
public void delete(Integer id);
```
- 日志输出
#指定mybatis输出日志的位置,输出控制台 mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutmpl
- 预编译SQL
优势:性能更高、更安全(防止SQL注入)
- SQL注入
SQL注入是通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法 - 参数占位符
新增
// 新增员工
@Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time)" + "values (#{username},#{name},#{gender},#{image},#{job},#{entrydate},#{deptId},#{createTime},#{updateTime})")
public void insert(Emp emp);
- 主键返回
@Options(useGeneratedKeys = true,keyProperty = "id")//获取返回属性,并将属性封装到id属性
更新
查询
实体类属性名 和 数据库表查询返回的字段名一致,mybatis会自动封装
如果实体类属性名 和 数据库表查询返回的字段名不一致,不能自动封装
-
解决方案
- 方案一:给字段起别名,让别名与实体类属性一致
@Select("select id, username, password, name, gender, image, job, entrydate, " + "dept_id deptId, create_time createTime, update_time updateTime from emp where id=#{id}") public Emp getById(Integer id);
- 方案二:通过@Results,@Result注解手动映射封装
@Results({ @Result(column = "dept_id",property = "deptId"), @Result(column = "create_time",property = "createTime"), @Result(column = "update_time",property = "updateTime"), }) @Select("select * from emp where id=#{id}") public Emp getById(Integer id);
- 方案三:开启mybatis驼峰命名自动映射开关 a_column------>aColumn,需要严格遵守命名规范
在application.properties
中添加
#开启驼峰命名 mybatis.configuration.map-underscore-to-camel-case=true
条件查询
// concat 字符串拼接函数
@Select("select * from emp where name like concat('%',#{name},'%') and gender=#{gender} and entrydate between #{beginDate} and #{endDate} order by update_time desc")
public List<Emp> list(String name, Short gender, LocalDate beginDate, LocalDate endDate);
5.3 XML映射文件
- XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)
- XML映射文件的namespace属性为Mapper接口全限定名一致。
- XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致。
<?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.ljytest.mapper.EmpMapper"> <!--resultType:--> <select id="list" resultType="com.ljytest.pojo.Emp"> select * from emp where name like concat('%',#{name},'%') and gender=#{gender} and entrydate between #{beginDate} and #{endDate} order by update_time desc </select>
</mapper>
使用Mybatis注解,主要完成一些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句
5.4 Mybatis动态SQL
随着用户的输入或外部条件的变化而变化的SQL语句,我们称为动态SQL
if
用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则拼接SQL
<where>
:where 元素只会在子元素有内容的情况下才插入where子句。而且会自动去除子句的开头的AND 或OR。
<set>
:动态地在行首插入 SET关键字,并会删掉额外的逗号。(用在update语句中)
<!--动态更新员工信息-->
<update id="update2"> update emp <set> <if test="username!=null"> username=#{username},</if> <if test="name!=null">name=#{name},</if> <if test="gender!=null">gender=#{gender},</if> <if test="image!=null">image=#{image},</if> <if test="job!=null">job=#{job},</if> <if test="entrydate!=null">entrydate=#{entrydate},</if> <if test="deptId!=null">dept_id=#{deptId},</if> <if test="updateTime!=null">update_time=#{updateTime}</if> </set> where id = #{id}
</update>
<!--resultType:-->
<select id="list" resultType="com.ljytest.pojo.Emp"> select * from emp <where> <if test="name!=null"> name like concat('%', #{name}, '%') </if> <if test="gender!=null"> and gender = #{gender} </if> <if test="beginDate != null and endDate != null"> and entrydate between #{beginDate} and #{endDate} </if> </where> order by update_time desc
</select>
foreach
- collection:遍历的集合
- item:遍历出来的元素
- separator:分隔符
- open:遍历开始前拼接的SQL片段
- close:遍历结束后拼接的SQL片段
<!--批量删除-->
<delete id="deleteByIds"> delete from emp where id in <foreach collection="ids" item="id" separator="," open="(" close=")"> #{id} </foreach>
</delete>
sql&include
<sql>
:定义可重用的SQL片段
<include>
:通过属性refid,制定包含的sql片段
<sql id="commonSelect"> select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp
</sql>
<include refid="commonSelect"></include>
6. 案例
6.1 环境搭建
准备数据库表(dept、emp)
创建springboot工程,引入对应的起步依赖(web、mybatis、mysql驱动、lombok)配置文件application.properties中引入mybatis的配置信息,准备对应的实体类准备对应的Mapper、Service(接口、实现类)、Controller基础结构
-
开发规范-Restful
REST(REpresentational State Transfer),表述性状态转换,它是一种软件架构风格
-
开发规范-统一响应结果
前后端交互统一响应结果 Result
6.2 员工管理
分页查询
- 注解
@RequestParam(defaultValue="1")
//设置请求参数默认值
@Slf4j
//定义日志记录对象 - 分页插件PageHelper
- 引入依赖
<!--PageHelper分页插件--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.6</version> </dependency>
- 使用
EmpServiceImpl
EmpMapperpublic PageBean page(Integer page, Integer pageSize) { // 1.设置分页参数 PageHelper.startPage(page,pageSize); // 2.执行查询 List<Emp> empList = empMapper.list(); Page<Emp> empPage = (Page<Emp>) empList; // 3.封装PageBean对象 PageBean pageBean = new PageBean(empPage.getTotal(),empPage.getResult()); return pageBean; }
@Select("select * from emp") List<Emp> list();
6.3 文件上传
- 服务端接收文件
MultipartFile
本地存储
uuid 唯一标识
@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image) throws IOException { log.info(" 文件上传:{},{},{}",username,age,image); // 获取原始文件名 String originalFilname = image.getOriginalFilename(); // 构造唯一文件名(不能重复)-uuid(通用唯一识别码) int index = originalFilname.lastIndexOf("."); String extname = originalFilname.substring(index); String newFileName = UUID.randomUUID().toString() + extname; log.info("新的文件名:{}",newFileName); //将文件存储在服务器的磁盘目录下 image.transferTo(new File("F:\\LJY\\PostGraduate\\CodeLearing\\Java\\JavaWebTest\\images\\"+newFileName)); return Result.success();
}
#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB
#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB
常用方法
String getOriginalFilename(); //获取原始文件名
void transferTo(File dest);//将接收的文件转存到磁盘文件中
long getsize(); //获取文件的大小,单位:字节
byte[] getBytes(); //获取文件内容的字节数组
Inputstream getinputstream();//获取收到的文件内容的输入流
无法直接访问、磁盘空间限制、磁盘损坏
云存储
https://oss.console.aliyun.com/bucket/oss-cn-beijing/ljy-web-framework01/object
阿里云对象存储OSS(Object Storage Service)
第三方服务-通用思路
Bucket:存储空间是用户用于存储对象(0bject,就是文件)的容器,所有的对象都必须隶属于某个存储空间,
SDK:Software Development Kit 的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。
<!--阿里云OSS-->
<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.15.1</version>
</dependency> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version>
</dependency>
<dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.3</version>
</dependency>
@Component
public class AliOSSUtils { private String endpoint = "https://oss-cn-beijing.aliyuncs.com"; // 从环境变量中获取访问凭证 private String accessKeyId = "LTAI5tJd5jSaFs5rEaprVPfL"; private String accessKeySecret = "6Xm6GYkmopkwCkyvk22X6qzOwZWJrv"; // 填写Bucket名称 private String bucketName = "ljy-web-framework01"; /** * 实现上传图片到OSS */ public String upload(MultipartFile file) throws IOException { // 获取上传的文件的输入流 InputStream inputStream = file.getInputStream(); // 避免文件覆盖 String originalFilename = file.getOriginalFilename(); String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf(".")); //上传文件到 OSS OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); ossClient.putObject(bucketName, fileName, inputStream); //文件访问路径 String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName; // 关闭ossClient ossClient.shutdown(); return url;// 把上传到oss的路径返回 } }
6.4 配置文件
参数配置化
@Value 注解通常用于外部配置的属性注入,具体用法为: @Value("${配置文件中的key}")
- application.properties
#自定义的阿里云OSS配置信息 aliyun.oss.endpoint=https://oss-cn-beijing.aliyuncs.com #从环境变量中获取访问凭证 aliyun.oss.accessKeyId=LTAI5tJd5jSaFs5rEaprVPfL aliyun.oss.accessKeySecret=6Xm6GYkmopkwCkyvk22X6qzOwZWJrv #填写Bucket名称 aliyun.oss.bucketName=ljy-web-framework01
- AliOSSUtils
@Value("${aliyun.oss.endpoint}") private String endpoint; // 从环境变量中获取访问凭证 @Value("${aliyun.oss.accessKeyId}") private String accessKeyId; @Value("${aliyun.oss.accessKeySecret}") private String accessKeySecret; // 填写Bucket名称 @Value("${aliyun.oss.bucketName}") private String bucketName;
yml配置文件(推荐)
yml yaml
@ConfigurationProperties
- 添加配置文件后,yml文件会有相对应的配置项的提示
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId> </dependency>
- AliOSSProperties.java
@Data @Component @ConfigurationProperties(prefix = "aliyun.oss") public class AliOSSProperties { private String endpoint; // 从环境变量中获取访问凭证 private String accessKeyId; private String accessKeySecret; // 填写Bucket名称 private String bucketName; }
7. 登录校验
7.1 登录标记
用户登录成功之后,每一次请求中,都可以获取到该标记——会话技术
会话技术
-
会话:用户打开浏览器,访问web服务器资源,会话建立,知道有一方断开连接,会话结束。一次会话中可以包含多次请求和响应
-
会话跟踪:识别请求是否来自于同一个浏览器
-
会话跟踪方案:
- 客户端会话跟踪技术:Cookie
- 服务端会话跟踪技术:Session
- 令牌技术
-
客户端会话跟踪技术:Cookie
跨域区分三个维度:协议、IP/域名、端口
<dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> </dependency>
//设置Cookie @GetMapping("/c1") public Result cookie1(HttpServletResponse response){ response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie return Result.success(); } //获取Cookie @GetMapping("/c2") public Result cookie2(HttpServletRequest request){ Cookie[] cookies = request.getCookies(); for (Cookie cookie : cookies) { if(cookie.getName().equals("login_username")){ System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie } } return Result.success(); }
-
服务端会话跟踪技术:Session
@GetMapping("/s1") public Result session1(HttpSession session){ log.info("HttpSession-s1: {}", session.hashCode()); session.setAttribute("loginUser", "tom"); //往session中存储数据 return Result.success(); } @GetMapping("/s2") public Result session2(HttpServletRequest request){ HttpSession session = request.getSession(); log.info("HttpSession-s2: {}", session.hashCode()); Object loginUser = session.getAttribute("loginUser"); //从session中获取数据 log.info("loginUser: {}", loginUser); return Result.success(loginUser); }
-
令牌技术
JWT:JSON Web Token
①登录成功后,生成令牌
②后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理
```
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version>
</dependency>
```
```
/** * 测试jwt令牌生成 */
@Test
public void testJwt(){ Map<String, Object> claims = new HashMap<>(); claims.put("id",1); claims.put("name","tom"); String jwt = Jwts.builder() .signWith(SignatureAlgorithm.HS256, "ljytest")//签名算法 .setClaims(claims)//设置自定义内容(载荷) .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))//设置有效期为1h .compact(); System.out.println(jwt);
} /** * 解析令牌 */
@Test
public void testParseJwt(){ Claims claims = Jwts.parser() .setSigningKey("ljytest")//指定签名令牌 .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTcyMTUzOTU0NH0.tjJtAFRl_-BQSM0a2siMpYwUjxjRsZCSOIwhKA-hzYk")//解析令牌 .getBody(); System.out.println(claims);
}
```
7.2 统一拦截
过滤器Filter
-
快速入门
定义:实现Filter接口
配置:@WebFilter(urlPatterns="/*")
@ServletComponentScan
-
详解
-
Filter拦截路径
-
过滤器链
@Slf4j @WebFilter(urlPatterns = "/*") public class LoginCheckFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse resp = (HttpServletResponse) servletResponse; // 1.获取请求url String url = req.getRequestURI().toString(); log.info("请求的url",url); // 2.判断请求url中是否包含login,如果包含,说明是登录操作,放行 if(url.contains("login")){ log.info("登录操作,放行..."); filterChain.doFilter(servletRequest,servletResponse); return; } // 3.获取请求头中的令牌(token) String jwt = req.getHeader("token"); // 4.判断令牌是否存在,如果不存在,返回错误结果(未登录) if(!StringUtils.hasLength(jwt)){ log.info("请求头token为空,返回未登录的信息"); Result error = Result.error("NOT_LOGIN"); // 手动转换 对象——json ————————>阿里巴巴fastJson String notLogin = JSON.toJSONString(error); // 将字符串响应给浏览器 resp.getWriter().write(notLogin); return; } // 5.解析token,如果解析失败,返回错误结果(未登录) try { JwtUtils.parseJWT(jwt); }catch (Exception e){ e.printStackTrace(); log.info("解析令牌失败,返回未登录的信息"); Result error = Result.error("NOT_LOGIN"); // 手动转换 对象——json ————————>阿里巴巴fastJson String notLogin = JSON.toJSONString(error); // 将字符串响应给浏览器 resp.getWriter().write(notLogin); return; } // 6.放行 log.info("令牌合法,放行"); filterChain.doFilter(servletRequest,servletResponse); } }
拦截器Intercepter
- 快速入门
- 定义拦截器,实现HandlerInterceptor接口,并重写所有方法(Ctrl+O)
- 注册拦截器
-
详解
-
登录校验
LoginCheckInterceptor@Slf4j @Component public class LoginCheckInterceptor implements HandlerInterceptor { @Override//目标资源方法(即controller)运行前运行,返回true,放行;返回false,不放行 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String jwt = request.getHeader("token"); if(!StringUtils.hasLength(jwt)){ log.info("请求头token为空,返回未登录的信息"); Result error = Result.error("NOT_LOGIN"); String notLogin = JSON.toJSONString(error); response.getWriter().write(notLogin); return false; } try { JwtUtils.parseJWT(jwt); }catch (Exception e){ log.info("jwt解析失败,返回未登录的信息"); Result error = Result.error("NOT_LOGIN"); String notLogin = JSON.toJSONString(error); response.getWriter().write(notLogin); return false; } log.info("令牌合法,放行"); return true; } @Override//目标资源方法运行后运行 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle..."); } @Override//视图渲染完毕后运行,最后运行 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion..."); } }
WebConfig
@Configuration //配置类 public class WebConfig implements WebMvcConfigurer { @Autowired private LoginCheckInterceptor loginCheckInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login"); } }
7.3 异常处理
全局异常处理器
@RestControllerAdvice
@ExceptionHandler
@RestControllerAdvice
public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) //捕获所有的异常 public Result ex(Exception e){ e.printStackTrace(); return Result.error("对不起,操作失败,请联系管理员"); }
}
8. 事务管理&AOP
事务管理
Spring事务管理
#开启事务管理日志
logging: level: org.springframework.jdbc.support.jdbcTransactionManager: debug
事务进阶
-
rollbackFor
默认情况下,只有出现 RuntimeException 才回滚异常。rollbackFor属性用于控制出现何种异常类型,回滚事务@Transactional(rollbackFor = Exception.class)//设置所有异常都会进行事务回滚
-
propagation
事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
@Transactional(propagation = Propagation.REQUIRES_NEW) @Override public void insert(DeptLog deptLog) { deptLogMapper.insert(deptLog); }
AOP基础
AOP概述
Aspect oriented ProaramnId(面向切面编程、面向方面编程),其实就是面向特定方法编程
实现
动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
- 导入依赖:在pom.xml中导入AOP的依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
- 编写AOP程序:针对于特定方法根据业务需要进行编程
@Slf4j @Component @Aspect public class TimeAspect { @Around("execution(* com.ljytest.projectest.service.impl.*.*(..))") // 第一个*是方法的返回,第二个*是类名,第三个*是方法名 public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // 记录开始时间 long begin = System.currentTimeMillis(); // 调用原始方法 Object result = proceedingJoinPoint.proceed(); // 记录结束时间,计算方法执行耗时 long end = System.currentTimeMillis(); log.info(proceedingJoinPoint.getSignature()+"方法执行耗时:{}ms",end-begin); return result; } }
AOP核心概念
AOP进阶
1.通知类型
- PointCut
将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可@Pointcut("execution(* com.ljytest.projectest.service.impl.DeptServiceImpl.*(..))") private void pt(){}
2.通知顺序
3.切入点表达式
-
execution(…):根据方法的签名来匹配,主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
- 其中带 ? 的表示可以省略的部分
访问修饰符:可省略(比如: public、protected)
包名.类名: 可省略
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常) - 使用通配符描述切入点
- 其中带 ? 的表示可以省略的部分
-
@annotation(…):根据注解匹配
com.ljytest.projectest.aop.MyLog@Retention(RetentionPolicy.RUNTIME) //指定什么时候生效 @Target(ElementType.METHOD)//作用在哪些地方 public @interface MyLog { }
DeptServiceImpl
@MyLog //自定义注解 public List<Dept> findAll(){ return deptMapper.findAll(); }
@Pointcut("@annotation(MyLog)") private void pt1(){} @Before("pt1()") public void before1(){ log.info("before1.."); }
4.连接点
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
- 对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
- 对于其他四种通知,获取连接点信息只能使用 JoinPoint ,它是 ProceedingJoinPoint 的父类型
9. Springboot原理
9.1 配置优先级
-
application.properties>yml>yaml
-
SpringBoot 除了支持配置文件属性配置,还支持Java系统属性和命令行参数的方式进行属性配置
-
优先级(从低到高)
9.2 Bean管理
获取bean
默认情况下,Spring项目启动时,会把bean都创建好放在IOC容器中,如果想要主动获取这些bean,可以通过如下方式:
// 根据bean的名称获取
DeptController bean1 = (DeptController) applicationContext.getBean("deptController");
System.out.println(bean1);
// 根据bean的类型获取
DeptController bean2 = applicationContext.getBean(DeptController.class);
System.out.println(bean2);
// 根据bean的名称和类型获取
DeptController bean3 = applicationContext.getBean("deptController", DeptController.class);
System.out.println(bean3);
bean作用域
Spring支持五种作用域,后三种在web环境才生效:
@Scope("prototype")
第三方bean
如果要管理的bean对象来自于第三方(不是自定义的),是无法用 @Component 及衍生注解声明bean的,就需要用到 @Bean注解
@Configuration //配置类
public class CommonConfig { //声明第三方bean @Bean //将当前方法的返回值对象交给IOC容器管理, 成为IOC容器bean //通过@Bean注解的name/value属性指定bean名称, 如果未指定, 默认是方法名 public SAXReader reader(DeptService deptService){ System.out.println(deptService); return new SAXReader(); }
}
9.3 SpringBoot原理
起步依赖 自动配置
起步依赖原理
依赖传递
自动配置原理
SpringBoot的自动配置就是当spring容器启动后,一些配置类、bean对象就自动存入到了IOC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作。
-
第三方依赖自动配置
ImportSelector接口类
public class MyImportSelector implements ImportSelector { public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[]{"com.example.HeaderConfig"}; } }
-
源码跟踪
-
@SpringBootApplication
-
@Conditional
-
-
自定义starter
在实际开发中,经常会定义一些公共组件,提供给各个项目团队使用。而在SpringBoot的项目中,一般会将这些公共组件封装为SpringBoot 的 starter- 规范
SpringBoot官方:spring-boot-starter-XXX
其他技术提供:XXX-spring-boot-starter
- 规范
Web后端开发-总结
10. Maven高级
10.1 分模块设计与开发
将项目按照功能拆分成若干个子模块,方便项目的管理维护、扩展,也方便模块间的相互调用,资源共享
10.2 继承与聚合
继承
继承描述的是两个工程间的关系,与java中的继承相似,子工程可以继承父工程中的配置信息
<parent> … </parent>
- 继承关系实现
projectest-parent
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.1</version> <relativePath/> <!-- lookup parent from repository -->
</parent><groupId>org.ljytest</groupId>
<artifactId>projectest-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
其他
<parent> <groupId>org.ljytest</groupId> <artifactId>projectest-parent</artifactId> <version>1.0-SNAPSHOT</version> <relativePath>../projectest-parent/pom.xml</relativePath>
</parent>
- 版本锁定
在maven中,可以在父工程的pom文件中通过<dependencyManagement>
来统一管理依赖版本。
子工程需要引入依赖,无需指定<version>
版本号,父工程统一管理。变更依赖版本,只需在父工程中统一变更。 - 自定义属性/引用属性
<properties> <lombok.version>RELEASE</lombok.version> <jjwt.version>0.9.1</jjwt.version> </properties>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version>
聚合
- 聚合:将多个模块组织成一个整体,同时进行项目的构建
- 聚合工程:一个不具有业务功能的“空”工程(有且仅有一个pom文件)
- 作用:快速构建项目(无需根据依赖关系手动构建,直接在聚合工程上构建即可)
- maven中可以通过
<modules>
设置当前聚合工程所包含的子模块名称<!-- 聚合其他模块--> <modules> <module>../projectest-pojo</module> <module>../projectest-utils</module> <module>../projectest-web-management</module> </modules>
10.3 私服
介绍
私服是一种特殊的远程仓库,它是架设在局域网内的仓库服务,用来代理位于外部的中央仓库,用于解决团队内部的资源共享与资源同步问题。
依赖查找顺序:本地仓库–>私服–>中央仓库
资源上传与下载
- 1.设置私服的访问用户名/密码(settings.xml中的servers中配置)
<server> <id>maven-releases</id> <username>admin</username> <password>admin</password> </server> <server> <id>maven-snapshots</id> <username>admin</username> <password>admin</password> </server>
- 2.IDEA的maven工程的pom文件中配置上传(发布)地址
<distributionManagement> <repository> <id>maven-releases</id> <url>http://192.168.150.101:8081/repository/maven-releases/</url> </repository> <snapshotRepository> <id>maven-snapshots</id> <url>http://192.168.150.101:8081/repository/maven-snapshots/</url> </snapshotRepository> </distributionManagement>
- 3.设置私服依赖下载的仓库组地址(settings.xml中的mirrors、profiles中配置)
<mirror> <id>maven-public</id> <mirrorOf>*</mirrorOf> <url>http://192.168.150.101:8081/repository/maven-public/</url> </mirror>
<profile> <id>allow-snapshots</id> <activation> <activeByDefault>true</activeByDefault> </activation> <repositories> <repository> <id>maven-public</id> <url>http://192.168.150.101:8081/repository/maven-public/</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> </profile>
mybatis-plus 瑞吉外卖 springCloud微服务技术栈 学成在线