Java - SpringBootWeb 案例
![](https://img.haomeiwen.com/i21107801/bc64b5e8a7cffadd.png)
👉 在线笔记:https://du1in9.github.io/springboot_demo.github.io/
1 准备工作
1.1 需求
![](https://img.haomeiwen.com/i21107801/cd616103ba8d3a17.png)
1.2 环境准备
1. 准备数据库表 (dept、emp)
-- 部门管理
create table dept(
id int unsigned primary key auto_increment comment '主键ID',
name varchar(10) not null unique comment '部门名称',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '部门表';
insert into dept (id, name, create_time, update_time) values(1,'学工部',now(),now()),(2,'教研部',now(),now()),(3,'咨询部',now(),now()), (4,'就业部',now(),now()),(5,'人事部',now(),now());
-- 员工管理(带约束)
create table emp (
id int unsigned primary key auto_increment comment 'ID',
username varchar(20) not null unique comment '用户名',
password varchar(32) default '123456' comment '密码',
name varchar(10) not null comment '姓名',
gender tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',
image varchar(300) comment '图像',
job tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师',
entrydate date comment '入职时间',
dept_id int unsigned comment '部门ID',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '员工表';
INSERT INTO emp
(id, username, password, name, gender, image, job, entrydate,dept_id, create_time, update_time) VALUES
(1,'jinyong','123456','金庸',1,'1.jpg',4,'2000-01-01',2,now(),now()),
(2,'zhangwuji','123456','张无忌',1,'2.jpg',2,'2015-01-01',2,now(),now()),
(3,'yangxiao','123456','杨逍',1,'3.jpg',2,'2008-05-01',2,now(),now()),
(4,'weiyixiao','123456','韦一笑',1,'4.jpg',2,'2007-01-01',2,now(),now()),
(5,'changyuchun','123456','常遇春',1,'5.jpg',2,'2012-12-05',2,now(),now()),
(6,'xiaozhao','123456','小昭',2,'6.jpg',3,'2013-09-05',1,now(),now()),
(7,'jixiaofu','123456','纪晓芙',2,'7.jpg',1,'2005-08-01',1,now(),now()),
(8,'zhouzhiruo','123456','周芷若',2,'8.jpg',1,'2014-11-09',1,now(),now()),
(9,'dingminjun','123456','丁敏君',2,'9.jpg',1,'2011-03-11',1,now(),now()),
(10,'zhaomin','123456','赵敏',2,'10.jpg',1,'2013-09-05',1,now(),now()),
(11,'luzhangke','123456','鹿杖客',1,'11.jpg',5,'2007-02-01',3,now(),now()),
(12,'hebiweng','123456','鹤笔翁',1,'12.jpg',5,'2008-08-18',3,now(),now()),
(13,'fangdongbai','123456','方东白',1,'13.jpg',5,'2012-11-01',3,now(),now()),
(14,'zhangsanfeng','123456','张三丰',1,'14.jpg',2,'2002-08-01',2,now(),now()),
(15,'yulianzhou','123456','俞莲舟',1,'15.jpg',2,'2011-05-01',2,now(),now()),
(16,'songyuanqiao','123456','宋远桥',1,'16.jpg',2,'2007-01-01',2,now(),now()),
(17,'chenyouliang','123456','陈友谅',1,'17.jpg',NULL,'2015-03-21',NULL,now(),now());
2. 创建 springboot 工程,引入对应的起步依赖(web、mybatis、mysql 驱动、lombok)
![](https://img.haomeiwen.com/i21107801/bbfad4ee1d2a36c9.png)
3. 配置文件 application.properties 中引入 mybatis 的配置信息,准备对应的实体类
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/tlias
spring.datasource.username=root
spring.datasource.password=123456
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis.configuration.map-underscore-to-camel-case=true
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Dept {
private Integer id; // ID
private String name; // 部门名称
private LocalDateTime createTime; // 创建时间
private LocalDateTime updateTime; // 修改时间
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
private Integer id; // ID
private String username; // 用户名
private String password; // 密码
private String name; // 姓名
private Short gender; // 性别, 1 男, 2 女
private String image; // 图像 url
private Short job; // 职位, 1 班主任, 2 讲师, 3 学工主管, 4 教研主管, 5 咨询师
private LocalDate entrydate; // 入职日期
private Integer deptId; // 部门 ID
private LocalDateTime createTime; // 创建时间
private LocalDateTime updateTime; // 修改时间
}
4. 准备对应的 Mapper、Service (接口、实现类)、Controller 基础结构
@RestController
public class DeptController {}
@RestController
public class EmpController {}
@Mapper
public interface DeptMapper {}
@Mapper
public interface EmpMapper {}
@Service
public interface DeptService {}
@Service
public interface EmpService {}
public class DeptServiceImpl {}
public class EmpServiceImpl {}
1.3 开发规范
1. Restful:REST(REpresentational State Transfer),表述性状态转换,它是一种软件架构风格
![](https://img.haomeiwen.com/i21107801/b124a3bcbf39b3a5.png)
2. 统一响应结果
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;// 响应码,1 代表成功; 0 代表失败
private String msg; // 响应信息 描述字符串
private Object data; // 返回的数据
// 增删改 成功响应
public static Result success(){
return new Result(1,"success",null);
}
// 查询 成功响应
public static Result success(Object data){
return new Result(1,"success",data);
}
// 失败响应
public static Result error(String msg){
return new Result(0,msg,null);
}
}
2 部门管理
2.1 查询部门
![](https://img.haomeiwen.com/i21107801/28995194a6bbc31d.png)
请求路径:/depts
请求方式:GET
接口描述:该接口用于部门列表数据查询
@Slf4j
@RestController
public class DeptController {
@Autowired
private DeptService deptService;
@GetMapping("/depts")
public Result list(){
log.info("查询全部部门数据");
List<Dept> deptList = deptService.list();
return Result.success(deptList);
}
}
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
public List<Dept> list() {
return deptMapper.list();
}
}
public interface DeptService {
List<Dept> list();
}
@Mapper
public interface DeptMapper {
@Select("select * from dept")
List<Dept> list();
}
请求参数:无
响应数据:application/json
// postman: get: http://localhost:8080/depts`
{
"code": 1,
"msg": "success",
"data": [
{
"id": 1,
"name": "学工部",
"createTime": "2024-05-17T14:05:21",
"updateTime": "2024-05-17T14:05:21"
},
...
{
"id": 5,
"name": "人事部",
"createTime": "2024-05-17T14:05:21",
"updateTime": "2024-05-17T14:05:21"
}
]
}
前后端联调:在资料中提供的 “前端工程” 文件夹中启动 nginx,访问 http://localhost:90 测试功能
2.2 删除部门
![](https://img.haomeiwen.com/i21107801/b73c3b3319388fd6.png)
请求路径:/depts/{id}
请求方式:DELETE
接口描述:该接口用于根据 ID 删除部门数据
@Slf4j
@RestController
public class DeptController {
@Autowired
private DeptService deptService;
@DeleteMapping("/depts/{id}")
public Result delete(@PathVariable Integer id){
log.info("根据id删除部门:{}", id);
deptService.delete(id);
return Result.success();
}
}
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
public void delete(Integer id) {
deptMapper.deleteById(id);
}
}
public interface DeptService {
void delete(Integer id);
}
@Mapper
public interface DeptMapper {
@Delete("delete from dept where id = #{id}")
void deleteById(Integer id);
}
请求参数:路径参数
响应数据:application/json
// postman: delete: http://localhost:8080/depts/5
{
"code": 1,
"msg": "success",
"data": null
}
前后端联调:在资料中提供的 “前端工程” 文件夹中启动 nginx,访问 http://localhost:90 测试功能
2.3 新增部门
![](https://img.haomeiwen.com/i21107801/3092f3a16c29a5ee.png)
请求路径:/depts
请求方式:POST
接口描述:该接口用于添加部门数据
@Slf4j
@RestController
public class DeptController {
@Autowired
private DeptService deptService;
@PostMapping("/depts")
public Result add(@RequestBody Dept dept){
log.info("新增部门: {}" , dept);
deptService.add(dept);
return Result.success();
}
}
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
public void add(Dept dept) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.insert(dept);
}
}
public interface DeptService {
void add(Dept dept);
}
@Mapper
public interface DeptMapper {
@Insert("insert into dept(name, create_time, update_time) values(#{name},#{createTime},#{updateTime})")
void insert(Dept dept);
}
请求参数:application/json
响应数据:application/json
// postman: post: { "name": "消防部" }
{
"code": 1,
"msg": "success",
"data": null
}
前后端联调:在资料中提供的 “前端工程” 文件夹中启动 nginx,访问 http://localhost:90 测试功能
2.4 修改部门
2.4.1 根据 id 查询
请求路径:/depts/{id}
请求方式:GET
接口描述:该接口用于根据 ID 查询部门数据
@GetMapping("/depts/{id}")
public Result find(@PathVariable Integer id){
log.info("根据id查询部门:{}", id);
Dept dept = deptService.find(id);
return Result.success(dept);
}
@Override
public Dept find(Integer id) {
return deptMapper.findById(id);
}
Dept find(Integer id);
@Select("select * from dept where id = #{id}")
Dept findById(Integer id);
请求参数:路径参数
响应数据:application/json
// postman: get: http://localhost:8080/depts/3
{
"code": 1,
"msg": "success",
"data": {
"id": 3,
"name": "咨询部",
"createTime": "2024-05-17T19:39:41",
"updateTime": "2024-05-17T19:39:41"
}
}
2.4.2 修改部门
请求路径:/depts
请求方式:PUT
接口描述:该接口用于修改部门数据
@PutMapping ("/depts")
public Result put(@RequestBody Dept dept){
log.info("修改部门");
Dept dept2 = deptService.find(dept.getId());
deptService.put(dept2, dept.getId(), dept.getName());
return Result.success();
}
@Override
public void put(Dept dept, Integer id, String name) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.put(id, name);
}
public interface DeptService {
void put(Dept dept, Integer id, String name);
}
@Update("update dept set name = #{name} where id = #{id}")
void put(Integer id, String name);
请求参数:application/json
响应数据:application/json
// postman: put: { "id": 1, "name": "公安部" }
{
"code": 1,
"msg": "success",
"data": null
}
前后端联调:在资料中提供的 “前端工程” 文件夹中启动 nginx,访问 http://localhost:90 测试功能
3 员工管理
3.1 分页查询
3.1.1 分页查询
![](https://img.haomeiwen.com/i21107801/ccd57295d738659f.png)
请求路径:/emps
请求方式:GET
接口描述:该接口用于员工列表数据的条件分页查询
@GetMapping("/emps")
public Result page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize){
log.info("分页查询:{},{}",page, pageSize);
PageBean pageBean = empService.page(page, pageSize);
return Result.success(pageBean);
}
public PageBean page(Integer page, Integer pageSize) {
Long count = empMapper.count();
Integer start = (page - 1) * pageSize;
List<Emp> empList = empMapper.page(start, pageSize);
PageBean pageBean = new PageBean(count, empList);
return pageBean;
}
PageBean page(Integer page, Integer pageSize);
@Select("select count(*) from emp")
public Long count();
@Select("select * from emp limit #{start}, #{pageSize}")
public List<Emp> page(Integer start, Integer pageSize);
请求参数:queryString
响应数据:application/json
// postman: get: http://localhost:8080/emps?page=2&pageSize=5
{
"code": 1,
"msg": "success",
"data": {
"total": 17,
"rows": [
{
"id": 6,
"username": "xiaozhao",
"password": "123456",
"name": "小昭",
"gender": 2,
"image": "6.jpg",
"job": 3,
"entrydate": "2013-09-05",
"deptId": 1,
"createTime": "2024-05-17T14:05:21",
"updateTime": "2024-05-17T14:05:21"
},
...
{
"id": 10,
"username": "zhaomin",
"password": "123456",
"name": "赵敏",
"gender": 2,
"image": "10.jpg",
"job": 1,
"entrydate": "2013-09-05",
"deptId": 1,
"createTime": "2024-05-17T14:05:21",
"updateTime": "2024-05-17T14:05:21"
}
]
}
}
分页插件 PageHelper:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
@Override
public PageBean page(Integer page, Integer pageSize) {
PageHelper.startPage(page,pageSize);
List<Emp> empList = empMapper.list();
Page<Emp> p = (Page<Emp>) empList;
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}
@Select("select * from emp")
public List<Emp> list();
3.1.2 分页查询(带条件)
![](https://img.haomeiwen.com/i21107801/6bfc066603e67fd2.png)
@GetMapping
public Result page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
String name, Short gender,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
log.info("分页查询, 参数: {},{},{},{},{},{}",page,pageSize,name,gender,begin,end);
PageBean pageBean = empService.page(page,pageSize,name,gender,begin,end);
return Result.success(pageBean);
}
@Override
public PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end) {
PageHelper.startPage(page,pageSize);
List<Emp> empList = empMapper.list(name, gender, begin, end);
Page<Emp> p = (Page<Emp>) empList;
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}
PageBean page(Integer page, Integer pageSize,String name, Short gender,LocalDate begin,LocalDate end);
<mapper namespace="com.itheima.mapper.EmpMapper">
<select id="list" resultType="com.itheima.pojo.Emp">
select * from emp
<where>
<if test="name != null and name != ''">
name like concat('%',#{name},'%')
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="begin != null and end != null">
and entrydate between #{begin} and #{end}
</if>
</where>
order by update_time desc
</select>
</mapper>
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
// postman:get:http://localhost:8080/emps?page=1&pageSize=5&name=张&gender=1&begin=2000-01-01&end=2010-01-01
{
"code": 1,
"msg": "success",
"data": {
"total": 1,
"rows": [
{
"id": 14,
"username": "zhangsanfeng",
"password": "123456",
"name": "张三丰",
"gender": 1,
"image": "14.jpg",
"job": 2,
"entrydate": "2002-08-01",
"deptId": 2,
"createTime": "2024-05-17T14:05:21",
"updateTime": "2024-05-17T14:05:21"
}
]
}
}
3.2 删除员工
![](https://img.haomeiwen.com/i21107801/bd44e37635e308ff.png)
请求路径:/emps/{ids}
请求方式:DELETE
接口描述:该接口用于批量删除员工的数据信息
@DeleteMapping("emps/{ids}")
public Result delete(@PathVariable List<Integer> ids){
log.info("批量删除操作, ids:{}",ids);
empService.delete(ids);
return Result.success();
}
@Override
public void delete(List<Integer> ids) {
empMapper.delete(ids);
}
void delete(List<Integer> ids);
<mapper namespace="com.itheima.mapper.EmpMapper">
<delete id="delete">
delete from emp
where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
</mapper>
void delete(List<Integer> ids);
请求参数:路径参数
响应数据:application/json
// postman: delete: http://localhost:8080/emps/15,16,17
{
"code": 1,
"msg": "success",
"data": null
}
3.3 新增员工
![](https://img.haomeiwen.com/i21107801/d2832034da4be50b.png)
请求路径:/emps
请求方式:POST
接口描述:该接口用于添加员工的信息
@PostMapping("/emps")
public Result save(@RequestBody Emp emp){
log.info("新增员工, emp: {}",emp);
empService.save(emp);
return Result.success();
}
@Override
public void save(Emp emp) {
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
empMapper.insert(emp);
}
void save(Emp emp);
@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})")
void insert(Emp emp);
请求参数:application/json
响应数据:application/json
/* postman: post:
{
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-03-07-37-38222.jpg",
"username": "linpingzhi",
"name": "林平之",
"gender": 1,
"job": 1,
"entrydate": "2022-09-18",
"deptId": 1
} */
{
"code": 1,
"msg": "success",
"data": null
}
3.4 修改员工
3.4.1 查询回显
请求路径:/emps/{id}
请求方式:GET
接口描述:该接口用于根据主键ID查询员工的信息
@GetMapping("/emps/{id}")
public Result getById(@PathVariable Integer id){
log.info("根据ID查询员工信息, id: {}",id);
Emp emp = empService.getById(id);
return Result.success(emp);
}
@Override
public Emp getById(Integer id) {
return empMapper.getById(id);
}
Emp getById(Integer id);
@Select("select * from emp where id = #{id}")
Emp getById(Integer id);
请求参数:路径参数
响应数据:application/json
/* postman: get: http://localhost:8080/emps/1
{
"code": 1,
"msg": "success",
"data": {
"id": 1,
"username": "jinyong",
"password": "123456",
"name": "金庸",
"gender": 1,
"image": "1.jpg",
"job": 4,
"entrydate": "2000-01-01",
"deptId": 2,
"createTime": "2024-05-18T19:10:18",
"updateTime": "2024-05-18T19:10:18"
}
}
3.4.2 修改员工
请求路径:/emps
请求方式:PUT
接口描述:该接口用于修改员工的数据信息
@PutMapping("/emps")
public Result update(@RequestBody Emp emp){
log.info("更新员工信息 : {}", emp);
empService.update(emp);
return Result.success();
}
@Override
public void update(Emp emp) {
emp.setUpdateTime(LocalDateTime.now());
empMapper.update(emp);
}
void update(Emp emp);
<mapper namespace="com.itheima.mapper.EmpMapper">
<update id="update">
update emp
<set>
<if test="username != null and username != ''">
username = #{username},
</if>
...
<if test="updateTime != null">
update_time = #{updateTime}
</if>
</set>
where id = #{id}
</update>
</mapper>
void update(Emp emp);
请求参数:application/json
响应数据:application/json
/* postman: put: body: raw:
{
"id": 1,
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-03-07-37-38222.jpg",
"username": "linpingzhi",
"name": "林平之",
"gender": 1,
"job": 1,
"entrydate": "2022-09-18",
"deptId": 1
} */
{
"code": 1,
"msg": "success",
"data": null
}
4 文件上传
4.1 简介
文件上传,是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程
1. 前端页面三要素
<!-- 1. 表单提交方式 post-->
<!-- 2. 表单的 enctype 属性 multipart/form-data -->
<form action="/upload" method="post" enctype="multipart/form-data">
姓名: <input type="text" name="username"><br>
年龄: <input type="text" name="age"><br>
头像: <input type="file" name="image"><br>
<!-- 3. 表单项 type=“file” -->
<input type="submit" value="提交">
</form>
![](https://img.haomeiwen.com/i21107801/fa9e7b5a61176991.png)
2. 服务端接收文件
@Slf4j
@RestController
public class UploadController {
@PostMapping("/upload")
public Result upload(String username, Integar age, MultipartFile image){
log.info("文件上传: {}, {}, {}", username, age, image);
return Result.success();
}
}
4.2 本地存储
在服务端,接收到上传上来的文件之后,将文件存储在本地服务器磁盘中
@PostMapping("/upload")
public Result upload(String username , Integer age , MultipartFile image) throws Exception {
// 1. 获取原始文件名
String originalFilename = image.getOriginalFilename();
// 2. 构造唯一的文件名 (不能重复) - uuid (通用唯一识别码)
int index = originalFilename.lastIndexOf(".");
String extname = originalFilename.substring(index);
String newFileName = UUID.randomUUID().toString() + extname;
// 3. 将文件存储在服务器的磁盘目录中 C:\Develop\JavaStudy\images
image.transferTo(new File("C:\\Develop\\JavaStudy\\images"+newFileName));
return Result.success();
}
/*
String getOriginalFilename(); // 获取原始文件名
void transferTo(File dest); // 将接收的文件转存到磁盘文件中
long getSize(); // 获取文件的大小, 单位: 字节
byte[] getBytes(); // 获取文件内容的字节数组
InputStream getInputStream(); // 获取接收到的文件内容的输入流
*/
在 SpringBoot 中,文件上传,默认单个文件允许最大大小为 1M。如果需要上传大文件,可以进行如下配置:
#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB
#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB
4.3 阿里云 OSS
4.3.1 准备工作
阿里云是阿里巴巴集团旗下全球领先的云计算公司,也是国内最大的云服务提供商
![](https://img.haomeiwen.com/i21107801/be4cc4920d04e4ba.png)
阿里云对象存储 OSS(Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务
使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件
![](https://img.haomeiwen.com/i21107801/ca0b817afb4e6ba3.png)
AccessKey ID: LTAI5tSH18FLKGFhWgsuJsGS
AccessKey Secret: *******************
- Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间
- SDK:Software Development Kit 的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做 SDK
4.3.2 快速入门程序
<!--阿里云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>
// 官方示例代码
public class Demo {
public static void main(String[] args) throws Exception {
String endpoint = "https://oss-cn-beijing.aliyuncs.com";
String accessKeyId = "LTAI5tSH18FLKGFhWgsuJsGS";
String accessKeySecret = "*******************";
String bucketName = "web-framework2024-5-8-01";
String objectName = "test.jpg";
String filePath= "C:\\Develop\\JavaStudy\\images\\test.jpg";
...
}
}
4.3.3 案例集成 OSS
请求路径:/upload
请求方式:POST
接口描述:上传图片接口
@RestController
public class UploadController {
@Autowired
private AliOSSUtils aliOSSUtils;
@PostMapping("/upload")
public Result upload(MultipartFile image) throws IOException {
String url = aliOSSUtils.upload(image);
return Result.success(url);
}
}
// 官方示例代码
@Component
public class AliOSSUtils {
private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
private String accessKeyId = "LTAI5tSH18FLKGFhWgsuJsGS";
private String accessKeySecret = "*******************";
private String bucketName = "web-framework2024-5-8-01";
...
}
请求参数:multipart/form-data
响应数据:application/json
// postman: post: body: form-data: image(file) => test.jpg
{
"code": 1,
"msg": "success",
"data": "https://web-framework2024-5-8-01.oss-cn-beijing.aliyuncs.com/dbb28c6d-9dfe-4e87-a6e0-4bc392ebd02f.jpg"
}
5 配置文件
1. 参数配置化
@Component
public class AliOSSUtils {
@Value("${aliyun.oss.endpoint}")
private String endpoint ;
@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId ;
@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret ;
@Value("${aliyun.oss.bucketName}")
private String bucketName ;
...
}
aliyun.oos.endpoint: https://oss-cn-beijing.aliyuncs.com
aliyun.oos.accessKeyId: LTAI5tSH18FLKGFhWgsuJsGS
aliyun.oos.accessKeySecret: *******************
aliyun.oos.bucketName: web-framework2024-5-8-01
2. yml 配置文件
![](https://img.haomeiwen.com/i21107801/9819bebc29170d11.png)
spring:
#数据库连接信息
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/tlias
username: root
password: 123456
#文件上传的配置
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
#Mybatis配置
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
#阿里云OSS
aliyun:
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
accessKeyId: LTAI5tSH18FLKGFhWgsuJsGS
accessKeySecret: 5wPyUojv5seKI5XqsTbrexcHHIoCwD
bucketName: web-framework2024-5-8-01
3. @ConfigurationProperties
@Value 注解只能一个一个的进行外部属性的注入
@ConfigurationProperties 可以批量的将外部的属性配置注入到 bean 对象的属性中
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
@Component
public class AliOSSUtils {...}
6 登录校验
6.1 登录认证
请求路径:/login
请求方式:POST
接口描述:该接口用于员工登录 Tlias 智能学习辅助系统,登录完毕后,系统下发 JWT 令牌
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("员工登录: {}", emp);
Emp e = empService.login(emp);
return e != null ? Result.success() : Result.error("用户名或密码错误");
}
}
@Override
public Emp login(Emp emp) {
return empMapper.getByUsernameAndPassword(emp);
}
Emp login(Emp emp);
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
联调测试,发现 bug:在未登录情况下,我们也可以直接访问部门管理、员工管理等功能
6.2 登录校验
6.2.1 会话技术
会话:用户打开浏览器访问 web 服务器的资源,会话建立,有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应
会话跟踪:一种维护浏览器状态的方法,服务器识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据
![](https://img.haomeiwen.com/i21107801/149a96716cbca693.png)
1. 客户端会话跟踪技术:Cookie
![](https://img.haomeiwen.com/i21107801/fed2d4096e08579c.png)
2. 服务端会话跟踪技术:Session
![](https://img.haomeiwen.com/i21107801/641d026ad5844704.png)
3. 令牌技术
![](https://img.haomeiwen.com/i21107801/7182c964f4b93ec1.png)
6.2.2 令牌 JWT
JSON Web Token 定义了一种简洁的、自包含的格式,用于在通信双方以 json 数据格式安全的传输信息
- 第一部分:Header (头), 记录令牌类型、签名算法等。 例如:
{"alg":"HS256","type":"JWT"}
- 第二部分:Payload (有效载荷),携带一些自定义信息、默认信息等。 例如:
{"id":"1","username":"Tom"}
- 第三部分:Signature (签名),防止 Token 被篡改、确保安全性。将 header、payload 并加入指定秘钥,指定签名算法而来
![](https://img.haomeiwen.com/i21107801/e6cae42c73a93075.png)
Base64:是一种基于64个可打印字符(A-Z a-z 0-9 + /)来表示二进制数据的编码方式
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
public class JwtUtils {
private static String signKey = "itheima";
private static Long expire = 43200000L;
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
Emp e = empService.login(emp);
if (e != null){
Map<String, Object> claims = new HashMap<>();
claims.put("id", e.getId());
claims.put("name", e.getName());
claims.put("username", e.getUsername());
String jwt = JwtUtils.generateJwt(claims);
return Result.success(jwt);
}
return Result.error("用户名或密码错误");
}
请求参数:application/json
响应数据:application/json
/* postman: post: body: raw:
{
"username": "zhangwuji",
"password": "123456"
} */
{
"code": 1,
"msg": "success",
"data": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi5byg5peg5b-MIiwiaWQiOjIsInVzZXJuYW1lIjoiemhhbmd3dWppIiwiZXhwIjoxNzE2MTM5NjUyfQ.OeJnOCNg1f78FejZSfRn6_srJp8ZLre43jY9_9eD0D4"
}
6.2.3 过滤器 Filter
1. 快速入门
-
Filter 过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一
-
过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
-
过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
@Override // 初始化方法, 只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法执行了");
}
@Override // 拦截到请求之后调用, 调用多次
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain){
System.out.println("DemoFilter 拦截方法执行了");
chain.doFilter(request,response); // 放行
}
@Override // 销毁方法, 只调用一次
public void destroy() {
System.out.println("destroy 销毁方法执行了");
}
}
@ServletComponentScan // 开启了对 servlet 组件的支持
@SpringBootApplication
public class TliasWebManagementApplication {...}
2. Filter 详解
- 放行后访问对应资源,资源访问完成后,还会回到 Filter 中执行放行后的逻辑
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain){
System.out.println("DemoFilter 拦截到了请求...放行前逻辑");
chain.doFilter(request,response);
System.out.println("DemoFilter 拦截到了请求...放行后逻辑");
}
}
- Filter 可以根据需求,配置不同的拦截资源路径:
拦截路径 | urlPatterns 值 | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问 /login 路径时,才会被拦截 |
目录拦截 | /emps/* | 访问 /emps 下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
-
过滤器链:一个 web 应用中,可以配置多个过滤器
顺序:注解配置的 Filter,优先级是按照过滤器类名(字符串)的自然排序
@WebFilter(urlPatterns = "/*")
public class AbcFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain){
System.out.println("AbcFilter 拦截到了请求...放行前逻辑");
chain.doFilter(request,response);
System.out.println("AbcFilter 拦截到了请求...放行后逻辑");
}
}
AbcFilter 拦截到了请求...放行前逻辑
DemoFilter 拦截到了请求...放行前逻辑
DemoFilter 拦截到了请求...放行后逻辑
AbcFilter 拦截到了请求...放行后逻辑
3. Filter 实现
@Component
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 1.获取请求 url
String url = req.getRequestURL().toString();
// 2.判断请求 url 中是否包含 login, 如果包含, 说明是登录操作, 放行
if(url.contains("login")){
chain.doFilter(request,response);
return;
}
// 3.获取请求头中的令牌 (token)
String jwt = req.getHeader("token");
// 4.判断令牌是否存在, 如果不存在, 返回错误结果 (未登录)
if(!StringUtils.hasLength(jwt)){
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error); //手动转换对象 -- json
resp.getWriter().write(notLogin);
return;
}
// 5.解析 token, 如果解析失败, 返回错误结果 (未登录)
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error); // 手动转换对象 -- json
resp.getWriter().write(notLogin);
return;
}
// 6.放行
chain.doFilter(request, response);
}
}
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
用户登录成功后,系统会自动下发 JWT 令牌,然后在后续的每次请求中,都需要在请求头 header 中携带到服务端,请求头的名称为 token ,值为 登录时下发的 JWT 令牌。
如果检测到用户未登录,则会返回固定错误信息。
// postman: get: http://localhost:8080/depts
// Headers: Key = token, value = eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi5byg5peg5b-MIiwiaWQiOjIsInVzZXJuYW1lIjoiemhhbmd3dWppIiwiZXhwIjoxNzE2MTM5NjUyfQ.OeJnOCNg1f78FejZSfRn6_srJp8ZLre43jY9_9eD0D4
{
"code": 1,
"msg": "success",
"data": [...]
}
// postman: get: http://localhost:8080/depts
// Headers: Key = token, value = ABChbGciOiJIUzI1NiJ9.eyJuYW1lIjoi5byg5peg5b-MIiwiaWQiOjIsInVzZXJuYW1lIjoiemhhbmd3dWppIiwiZXhwIjoxNzE2MTM5NjUyfQ.OeJnOCNg1f78FejZSfRn6_srJp8ZLre43jY9_9eD0D4
{
"code": 0,
"msg": "NOT_LOGIN",
"data": null
}
6.2.4 拦截器 Interceptor
1. 快速入门
-
概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring 框架中提供的,用来动态拦截控制器方法的执行
-
作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码
@Component // 1. 定义拦截器
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override // 目标资源方法运行前运行, 返回 true 放行, 放回 false 不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
return true;
}
@Override // 目标资源方法运行后运行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
System.out.println("postHandle ...");
}
@Override // 视图渲染完毕后运行, 最后运行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
System.out.println("afterCompletion...");
}
}
@Configuration // 2. 注册拦截器
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");
}
}
2. Interceptor 详解
- 拦截器可以根据需求,配置不同的拦截路径:
拦截路径 | 含义 | 举例 |
---|---|---|
/* | 一级路径 | 能匹配 /depts,/emps,/login,不能匹配 /depts/1 |
/** | 任意级路径 | 能匹配 /depts,/depts/1,/depts/1/2 |
/depts/* | /depts 下的一级路径 | 能匹配 /depts/1,不能匹配 /depts/1/2,/depts |
/depts/** | /depts 下的任意级路径 | 能匹配 /depts,/depts/1,/depts/1/2,不能匹配 /emps/1 |
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
-
Filter 与 Interceptor
接口规范不同:过滤器需要实现 Filter 接口,而拦截器需要实现 HandlerInterceptor 接口
拦截范围不同:过滤器 Filter 会拦截所有的资源,而 Interceptor 只会拦截 Spring 环境中的资源
![](https://img.haomeiwen.com/i21107801/c7c96e912afc6f10.png)
3. Interceptor 实现
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
... // 同 Filter 接口, 放行返回 true, 不放行放回 false
}
// postman: get: http://localhost:8080/depts
{
"code": 0,
"msg": "NOT_LOGIN",
"data": null
}
// postman: get: http://localhost:8080/depts
// Headers: Key = token, value = xyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi5byg5peg5b-MIiwiaWQiOjIsInVzZXJuYW1lIjoiemhhbmd3dWppIiwiZXhwIjoxNzE2MTM5NjUyfQ.OeJnOCNg1f78FejZSfRn6_srJp8ZLre43jY9_9eD0D4
{
"code": 1,
"msg": "success",
"data": [...]
}
7 异常处理
程序开发过程中不可避免的会遇到异常现象,出现异常时,默认返回的结果不符合规范
- 在 Controller 的方法中进行 try…catch 处理(代码臃肿,不推荐)
- 全局异常处理器(简单、优雅,推荐)
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) // 捕获所有异常
public Result ex(Exception ex){
ex.printStackTrace();
return Result.error("对不起,操作失败,请联系管理员");
}
}
参考链接:https://www.bilibili.com/video/BV1m84y1w7Tb?p=135&vd_source=ed621eaa6bcf9bf6acb7d0527c30489a