SpringBoot案例 准备工作 1.准备数据库表(dept、emp)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 -- 部门管理 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)。
3.配置文件application.properties中引入mybatis的配置信息,准备对应的实体类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 =mybatis.configuration.map-underscore-to-camel-case =true
1 2 3 4 5 6 7 8 9 10 11 12 package com.itheima.pojo;@Data @NoArgsConstructor @AllArgsConstructor public class Dept { private Integer id; private String name; private LocalDateTime createTime; private LocalDateTime updateTime; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.itheima.pojo;@Data @NoArgsConstructor @AllArgsConstructor public class Emp { private Integer id; private String username; private String password; private String name; private Short gender; private String image; private Short job; private LocalDate entrydate; private Integer deptId; private LocalDateTime createTime; private LocalDateTime updateTime; }
4.准备对应的Mapper、Service(接口、实现类)、Controller基础结构。
开发规范 Restful 传统风格:
1 2 3 4 http://localhost:8080/user/getById?id =1 GET:查询id 为1的用户 http://localhost:8080/user/saveUser POST:新增用户 http://localhost:8080/user/updateUser POST:修改用户 http://localhost:8080/user/deleteUser?id =1 GET:删除id 为1的用户
REST(REpresentational State Transfer),表述性状态转换,它是一种软件架构风格。
REST风格:URL定位资源,HTTP动词描述操作。
1 2 3 4 http://localhost:8080/users/1 GET:查询id 为1的用户 http://localhost:8080/users POST:新增用户 http://localhost:8080/users PUT:修改用户 http://localhost:8080/users/1 DELETE:删除id 为1的用户
注意:
REST是风格,是约定方式,约定不是规定,可以打破。
描述模块的功能通常使用复数,也就是加s的格式来描述,表示此类资源,而非单个资源。如:users、emps、book。
统一响应结果 前后端交互统一响应结果Result。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.itheima.pojo;@Data @NoArgsConstructor @AllArgsConstructor public class Result { private Integer code; 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 ); } }
部门管理 查询部门
请求路径:/depts
请求方式:GET
接口描述:该接口用于部门列表数据查询
接口测试:http://localhost:8080/depts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.itheima.controller;@Slf4j @RestController public class DeptController { @Autowired private DeptService deptService; @GetMapping("/depts") public Result depts () { log.info("查询全部部门数据" ); List<Dept> deptList = deptService.list(); return Result.success(deptList); } }
1 2 3 4 5 6 package com.itheima.service;public interface DeptService { List<Dept> list () ; }
1 2 3 4 5 6 7 8 9 10 11 12 package com.itheima.service.impl;@Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Override public List<Dept> list () { return deptMapper.list(); } }
1 2 3 4 5 6 7 8 package com.itheima.mapper;@Mapper public interface DeptMapper { @Select("select * from dept") List<Dept> list () ; }
前后端联调 将前端工程文件夹中的压缩包,拷贝到一个没有中文不带空格的目录下,解压。启动nginx,访问测试:http://localhost:90 。
前端请求路径:http://localhost:90/api/depts,`localhost:90/api/`代表的是请求`nginx`路径,`nginx`服务器接收到请求之后,把该请求转给后端的`8080`端口`Tomcat`,最终由`Tomcat`处理这次请求。
删除部门
请求路径:/depts/{id}
请求方式:DELETE
接口描述:该接口用于根据ID删除部门数据
接口测试:http://localhost:8080/depts/1
1 2 3 4 5 6 7 8 @DeleteMapping("depts/{id}") public Result delete (@PathVariable Integer id) { log.info("根据id删除部门:{}" , id); deptService.delete(id); return Result.success(); }
1 2 3 void delete (Integer id) ;
1 2 3 4 5 6 @Override public void delete (Integer id) { deptMapper.deleteById(id); }
1 2 3 4 @Delete("delete from dept where id = #{id}") void deleteById (Integer id) ;
新增部门
请求路径:/depts
请求方式:POST
接口描述:该接口用于添加部门数据
接口测试:http://localhost:8080/depts
请求参数样例:
注意:一个完整的请求路径,是类上的@RequestMapping的value属性 + 方法上的@RequestMapping的value属性。
1 2 3 4 5 6 7 8 @PostMapping("depts") public Result add (@RequestBody Dept dept) { log.info("新增部门:{}" , dept); deptService.add(dept); return Result.success(); }
1 2 3 4 5 6 7 8 @Override public void add (Dept dept) { dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); deptMapper.insert(dept); }
1 2 3 4 @Insert("insert into dept(name, create_time, update_time) values (#{name}, #{createTime}, #{updateTime})") void insert (Dept dept) ;
根据ID查询
请求路径:/depts/{id}
请求方式:GET
接口描述:该接口用于根据ID查询部门数据
接口测试:http://localhost:8080/depts/1
1 2 3 4 5 6 7 8 @GetMapping("depts/{id}") public Result get (@PathVariable Integer id) { log.info("根据ID查询:{}" , id); Dept dept = deptService.get(id); return Result.success(dept); }
1 2 3 4 5 6 7 @Override public Dept get (Integer id) { Dept dept = deptMapper.getById(id); return dept; }
1 2 3 4 @Select("select * from dept where id = #{id}") Dept getById (Integer id) ;
修改部门
请求路径:/depts
请求方式:PUT
接口描述:该接口用于修改部门数据
接口测试:http://localhost:8080/depts
请求参数样例:
1 2 3 4 { "id" : 1 , "name" : "其他部" }
1 2 3 4 5 6 7 8 @PutMapping("depts") public Result update (@RequestBody Dept dept) { log.info("修改部门:{}" , dept); deptService.update(dept); return Result.success(); }
1 2 3 void update (Dept dept) ;
1 2 3 4 5 6 7 8 9 @Override public void update (Dept dept) { Dept dept_new = get(dept.getId()); dept_new.setName(dept.getName()); dept_new.setUpdateTime(LocalDateTime.now()); deptMapper.update(dept_new); }
1 2 3 4 @Update("update dept set name = #{name}, update_time = #{updateTime} where id = #{id}") void update (Dept dept1) ;
员工管理 分页查询
请求路径:/emps
请求方式:GET
接口描述:该接口用于员工列表数据的(条件)分页查询
请求参数样例:(参数格式:queryString)
1 /emps?page=1&pageSize=10
【注意】使用@RequestParam给page和pageSize设置默认值后,请求参数可以不填写这两个值。
请求参数:页码、每页展示记录数。
响应结果:总记录数、结果列表 (PageBean)。
注意:@RequestParam的属性defaultValue可以来设置参数的默认值。
1 2 3 4 5 6 7 8 9 10 package com.itheima.pojo;@Data @NoArgsConstructor @AllArgsConstructor public class PageBean { private long total; private List rows; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.itheima.controller;@Slf4j @RestController public class EmpController { @Autowired private EmpService empService; @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); } }
1 2 3 4 5 6 package com.itheima.service;public interface EmpService { PageBean page (Integer page, Integer pageSize) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.itheima.service.impl;@Service public class EmpServiceImpl implements EmpService { @Autowired private EmpMapper empMapper; @Override 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; } }
1 2 3 4 5 6 7 8 9 10 11 12 package com.itheima.mapper;@Mapper public interface EmpMapper { @Select("select count(*) from emp") public Long count () ; @Select("select * from emp limit #{start}, #{pageSize}") public List<Emp> page (Integer start, Integer pageSize) ; }
分页插件PageHelper
在pom.xml文件中引入分页插件:
1 2 3 4 5 6 <dependency > <groupId > com.github.pagehelper</groupId > <artifactId > pagehelper-spring-boot-starter</artifactId > <version > 1.4.7</version > </dependency >
只需要修改EmpServiceImpl.java和EmpMapper.java两个文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @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; }
1 2 3 4 @Select("select * from emp") public List<Emp> list () ;
测试链接:http://localhost:8080/emps?page=2&pageSize=10
输出日志:虽然只调用了list方法的select * from emp语句,但是还是分别执行了SELECT count(0) FROM emp和select * from emp LIMIT ?, ?这两个操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 JDBC Connection [HikariProxyConnection@935528035 wrapping com.mysql.cj.jdbc.ConnectionImpl@7 b18ae7] will not be managed by Spring ==> Preparing: SELECT count(0 ) FROM emp ==> Parameters: <== Columns: count(0 ) <== Row: 17 <== Total: 1 ==> Preparing: select * from emp LIMIT ?, ? ==> Parameters: 10 (Long), 10 (Integer) <== Columns: id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time <== Row: 11 , luzhangke, 123456 , 鹿杖客, 1 , 11 .jpg, 5 , 2007 -02 -01 , 3 , 2024 -10 -20 21 :55 :12 , 2024 -10 -20 21 :55 :12 <== Row: 12 , hebiweng, 123456 , 鹤笔翁, 1 , 12 .jpg, 5 , 2008 -08 -18 , 3 , 2024 -10 -20 21 :55 :12 , 2024 -10 -20 21 :55 :12 <== Row: 13 , fangdongbai, 123456 , 方东白, 1 , 13 .jpg, 5 , 2012 -11 -01 , 3 , 2024 -10 -20 21 :55 :12 , 2024 -10 -20 21 :55 :12 <== Row: 14 , zhangsanfeng, 123456 , 张三丰, 1 , 14 .jpg, 2 , 2002 -08 -01 , 2 , 2024 -10 -20 21 :55 :12 , 2024 -10 -20 21 :55 :12 <== Row: 15 , yulianzhou, 123456 , 俞莲舟, 1 , 15 .jpg, 2 , 2011 -05 -01 , 2 , 2024 -10 -20 21 :55 :12 , 2024 -10 -20 21 :55 :12 <== Row: 16 , songyuanqiao, 123456 , 宋远桥, 1 , 16 .jpg, 2 , 2007 -01 -01 , 2 , 2024 -10 -20 21 :55 :12 , 2024 -10 -20 21 :55 :12 <== Row: 17 , chenyouliang, 123456 , 陈友谅, 1 , 17 .jpg, null, 2015 -03 -21 , null, 2024 -10 -20 21 :55 :12 , 2024 -10 -20 21 :55 :12 <== Total: 7 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@32425622 ]
条件分页查询
和分页查询一致。
请求路径:/emps
请求方式:GET
接口描述:该接口用于员工列表数据的(条件)分页查询
请求参数样例:(参数格式:queryString)
1 /emps?name=张&gender=1&begin=2007-09-01&end=2022-09-01&page=1&pageSize=10
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("emps") 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); }
1 2 3 PageBean page (Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @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; }
1 2 3 4 public List<Emp> list (String name, Short gender, LocalDate begin, LocalDate end) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?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.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 >
删除员工
请求路径:/emps/{ids}
请求方式:DELETE
接口描述:该接口用于批量删除员工的数据信息
请求参数样例:http://localhost:8080/emps/21,22
1 2 3 4 5 6 7 8 @DeleteMapping("emps/{ids}") public Result delete (@PathVariable() List<Integer> ids) { log.info("批量删除操作,ids = {}" , ids); empService.delete(ids); return Result.success(); }
1 2 3 void delete (List<Integer> ids) ;
1 2 3 4 5 6 @Override public void delete (List<Integer> ids) { empMapper.delete(ids); }
1 2 3 void delete (List<Integer> ids) ;
1 2 3 4 5 6 7 8 <delete id ="delete" > delete from emp where id in <foreach collection ="ids" item ="id" separator ="," open ="(" close =")" > #{id} </foreach > </delete >
新增员工
请求路径:/emps
请求方式:POST
接口描述:该接口用于添加员工的信息
请求参数样例:
1 2 3 4 5 6 7 8 9 { "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 }
1 2 3 4 5 6 7 8 @PostMapping("emps") public Result save (@RequestBody Emp emp) { log.info("新增员工,emp:{}" , emp); empService.save(emp); return Result.success(); }
1 2 3 4 5 6 7 8 @Override public void save (Emp emp) { emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); empMapper.insert(emp); }
1 2 3 4 5 @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) ;
根据ID查询员工(查询回显)
请求路径:/emps/{id}
请求方式:GET
接口描述:该接口用于根据主键ID查询员工的信息
请求参数样例:http://localhost:8080/emps/23
1 2 3 4 5 6 7 8 @GetMapping("emps/{id}") public Result getById (@PathVariable() Integer id) { log.info("根据id查询员工信息,id:{}" , id); Emp emp = empService.getById(id); return Result.success(emp); }
1 2 3 Emp getById (Integer id) ;
1 2 3 4 5 6 7 @Override public Emp getById (Integer id) { Emp emp = empMapper.getById(id); return emp; }
1 2 3 4 @Select("select * from emp where id = #{id}") Emp getById (Integer id) ;
修改员工
请求路径:/emps
请求方式:PUT
接口描述:该接口用于修改员工的数据信息
请求参数样例:
1 2 3 4 5 6 7 8 9 10 11 { "id" : 25 , "password" : 888888 , "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 }
1 2 3 4 5 6 7 8 @PutMapping("emps") public Result update (@RequestBody Emp emp) { log.info("更新员工信息:{}" , emp); empService.update(emp); return Result.success(); }
1 2 3 4 5 6 7 @Override public void update (Emp emp) { emp.setUpdateTime(LocalDateTime.now()); empMapper.update(emp); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <update id ="update" > update emp <set > <if test ="username != null and username != '' " > username = #{username}, </if > <if test ="password != null and password != '' " > password = #{password}, </if > <if test ="name != null and name != '' " > name = #{name}, </if > <if test ="gender != null" > gender = #{gender}, </if > <if test ="image != null and image != '' " > 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 >
文件上传 简介 文件上传:将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。
前端页面三要素 :method="post"、enctype="multipart/form-data"、type="file"。
1 2 3 4 5 6 <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 > <input type ="submit" value ="提交" > </form >
enctype的默认值是application/ x-www-form-urlencoded,若使用该方式上传文件,则最后上传的请求为:username=&age=&image=%E4%B8%AD%E5%9B%BD%E6%A2%A6.txt。即:
1 2 3 4 5 { "username" : "" , "age" : "" , "image" : "中国梦.txt" }
指定enctype的值为multipart/form-data,使用该方式上传文件,在火狐浏览器 (其他浏览器已经封装好文件,无法查看文件具体内容)中打开可以看到上传的是整个文件。其中boundary是分割符 。
将前端页面upload.html放在src/main/resources/static文件夹下,在src/main/java/com/itheima/controller下编写UploadController.java文件,用来设置上传文件。
1 2 3 4 5 6 7 8 9 10 11 package com.itheima.controller;@Slf4j @RestController public class UploadController { @PostMapping("/upload") public Result upload (String username, Integer age, MultipartFile image) { log.info("文件上次:{},{},{}" , username, age, image); return Result.success(); } }
【注意】
1.服务端使用MultipartFile来接收文件。
2.在log.info处设置断点,浏览器使用http://localhost:8080/upload.html访问前端页面并提交文件,在IDEA中可以收到MultipartFile的文件信息。
其中,上述C:\Users\srr18\AppData\Local\Temp\tomcat.8080.10055437552755735705\work\Tomcat\localhost\ROOT存储的就是上传的临时文件,0.tmp是username的值surourou,1.tmp是age的值18,2.tmp是上传的文件。
当该函数运行结束,则该临时文件就会自动删除。
本地存储 在服务端,接收到上传上来的文件之后,将文件存储在本地服务器磁盘中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.itheima.controller;@Slf4j @RestController public class UploadController { @PostMapping("/upload") public Result upload (String username, Integer age, MultipartFile image) throws Exception { log.info("文件上次:{},{},{}" , username, age, image); String originalFilename = image.getOriginalFilename(); int index = originalFilename.lastIndexOf("." ); String extname = originalFilename.substring(index); String newFileName = UUID.randomUUID().toString() + extname; log.info("新的文件名:{}" , newFileName); image.transferTo(new File ("E:/2_学习/JavaWeb/code/itheima_web_project/springboot-web-tlias/src/main/resources/static/images/" + newFileName)); return Result.success(); } }
在SpringBoot中,文件上传默认单个文件允许最大大小为1M。如果上传的文件大于1M,则会报错:
使用Postman上传大于1M的文件,会报413 Request Entity Too Large Request is too large for the server to process.,显示无法上传文件。
如果需要上传大文件,可以进行如下配置:
1 2 3 4 spring.servlet.multipart.max-file-size =10MB spring.servlet.multipart.max-request-size =100MB
MultipartFile类的方法:
1 2 3 4 5 String getOriginalFilename () ; void transferTo (File dest) ; long getSize () ; byte [] getBytes(); InputStream getInputStream () ;
阿里云OSS 阿里云对象存储OSS(Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。
阿里云使用步骤
Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。
SDK:Software Development Kit的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。
1.手机注册登录阿里云账号。
2.在右上角点击控制台。
3.进入控制台页面,点击左上角阿里云logo,搜索oss,点击对象存储服务。
4.第一次进入可能显示需要充值,可以认证之后点击免费试用,申请免费试用可以试用三个月。
5.创建Bucket列表。
6.创建Bucket成功后在概览可以查看Endpoint。
7.在文件列表可以查看上传的文件。
8.在个人账户头像处选择AccessKey管理,选择创建AccessKey,并获取保存AccessKey ID和AccessKey Secret(只能查看一次,需要保存下来)。
9.列表左下角找到SDK,点击Java文档可以查看帮助文档,文档中心打开查看。
10.SDK参考->Java->安装下找到安装SDK,在pom.xml文件中引入如下依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <dependency > <groupId > com.aliyun.oss</groupId > <artifactId > aliyun-sdk-oss</artifactId > <version > 3.17.4</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 > <dependency > <groupId > org.glassfish.jaxb</groupId > <artifactId > jaxb-runtime</artifactId > <version > 2.3.3</version > </dependency >
11.SDK参考->Java->对象/文件->上传文件->简单上传,找到上传文件流,复制这段代码到test下进行测试(这段代码导入还需要导入一些包,按照IDEA提示导入即可),即springboot-web-tlias/src/test/java/com/itheima目录下的Demo.java文件,然后根据自己的信息修改即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package com.itheima;import com.aliyun.oss.*;import com.aliyun.oss.common.auth.*;import com.aliyun.oss.common.comm.SignVersion;import com.aliyun.oss.model.PutObjectRequest;import com.aliyun.oss.model.PutObjectResult;import java.io.FileInputStream;import java.io.InputStream;public class Demo { public static void main (String[] args) throws Exception { String endpoint = "https://oss-cn-hangzhou.aliyuncs.com" ; EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); String bucketName = "srr-web-tlias" ; String objectName = "1.jpg" ; String filePath= "E:/2_学习/JavaWeb/code/itheima_web_project/springboot-web-tlias/src/main/resources/static/images/Space02-Default.jpg" ; String region = "cn-hangzhou" ; ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration (); clientBuilderConfiguration.setSignatureVersion(SignVersion.V4); OSS ossClient = OSSClientBuilder.create() .endpoint(endpoint) .credentialsProvider(credentialsProvider) .clientConfiguration(clientBuilderConfiguration) .region(region) .build(); try { InputStream inputStream = new FileInputStream (filePath); PutObjectRequest putObjectRequest = new PutObjectRequest (bucketName, objectName, inputStream); PutObjectResult result = ossClient.putObject(putObjectRequest); } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason." ); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network." ); System.out.println("Error Message:" + ce.getMessage()); } finally { if (ossClient != null ) { ossClient.shutdown(); } } } }
测试运行该段程序,即可将本地图片E:/2_学习/JavaWeb/code/itheima_web_project/springboot-web-tlias/src/main/resources/static/images/Space02-Default.jpg上传到阿里云的Bucket中,在文件列表就可以查看上传的文件。
【注意】 上述代码需要在电脑本地配置环境变量,设置AccessKey ID和AccessKey Secret的值。
【补充】Windows使用CMD命令行设置环境变量
参考链接:Windows使用CMD命令行设置环境变量_windows cmd设置环境变量-CSDN博客
1.set设置临时环境变量 。它的作用范围只限于当前窗口 ,在关闭该命令窗口后,所有通过set命令设置的变量都会丢失。
设置临时环境变量:set name=value
查看当前系统全部环境变量:set
2.setx设置永久性环境变量 。这些变量不仅在当前命令行窗口中有效,而且在其他所有命令行窗口和应用程序中也都有效。
设置永久环境变量:setx name value或setx /M name value
【报错】Exception in thread "main" com.aliyun.oss.common.auth.InvalidCredentialsException: Access key id should not be null or empty.
参考链接:03_阿里云_配置OSS环境变量_access key id should not be null or empty.-CSDN博客
1.执行以下命令配置RAM用户的访问密钥。
1 2 set OSS_ACCESS_KEY_ID=AccessKey IDset OSS_ACCESS_KEY_SECRET=AccessKey Secret
2.执行以下命令以使更改生效。
1 2 setx OSS_ACCESS_KEY_ID "%OSS_ACCESS_KEY_ID% " setx OSS_ACCESS_KEY_SECRET "%OSS_ACCESS_KEY_SECRET% "
3.执行以下命令验证环境变量配置。
1 2 echo %OSS_ACCESS_KEY_ID% echo %OSS_ACCESS_KEY_SECRET%
配置环境变量后需要重启 IDEA,这样IDEA就可以读取环境变量里的OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 C:\Windows \System32 >set OSS_ACCESS_KEY_ID =LTAI5tK5ZM5yGVVFWvKxeNqJ C :\Windows \System32 >set OSS_ACCESS_KEY_SECRET =C :\Windows \System32 >setx OSS_ACCESS_KEY_ID "%OSS_ACCESS_KEY_ID %"成功: 指定的值已得到保存。 C :\Windows \System32 >setx OSS_ACCESS_KEY_SECRET "%OSS_ACCESS_KEY_SECRET %"成功: 指定的值已得到保存。 C :\Windows \System32 >echo %OSS_ACCESS_KEY_ID %LTAI5tK5ZM5yGVVFWvKxeNqJ C :\Windows \System32 >echo %OSS_ACCESS_KEY_SECRET %
阿里云集成
1.引入阿里云OSS上传文件工具类 AliOSSUtils(由官方的示例代码改造而来),放在com.itheima.utils包下。
【注意】需要修改endpoint,accessKeyId,accessKeySecret,bucketName四个变量的值为自己bucket对应的值。其他的不用修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.itheima.utils;import com.aliyun.oss.OSS;import com.aliyun.oss.OSSClientBuilder;import org.springframework.stereotype.Component;import org.springframework.web.multipart.MultipartFile;import java.io.*;import java.util.UUID;@Component public class AliOSSUtils { private String endpoint = "https://oss-cn-hangzhou.aliyuncs.com" ; private String accessKeyId = "LTAI5tK5ZM5yGVVFWvKxeNqJ" ; private String accessKeySecret = "" ; private String bucketName = "srr-web-tlias" ; 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 ossClient = new OSSClientBuilder ().build(endpoint, accessKeyId, accessKeySecret); ossClient.putObject(bucketName, fileName, inputStream); String url = endpoint.split("//" )[0 ] + "//" + bucketName + "." + endpoint.split("//" )[1 ] + "/" + fileName; ossClient.shutdown(); return url; } }
2.上传图片接口开发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.itheima.controller;@Slf4j @RestController public class UploadController { @Autowired private AliOSSUtils aliOSSUtils; @PostMapping("upload") public Result upload (MultipartFile image) throws IOException { log.info("文件上传,文件名:{}" , image.getOriginalFilename()); String url = aliOSSUtils.upload(image); log.info("文件上传完成,文件访问的url:{}" , url); return Result.success(url); } }
3.接口测试:在Postman中使用http://localhost:8080/upload接口,POST方法上传文件。
请求路径:/upload
请求方式:POST
接口描述:上传图片接口
配置文件 参数配置化 @Value注解通常用于外部配置的属性注入 ,具体用法为:@Value("${配置文件中的key}")
1 2 3 4 5 aliyun.oss.endpoint =https://oss-cn-hangzhou.aliyuncs.com aliyun.oss.accessKeyId =LTAI5tK5ZM5yGVVFWvKxeNqJ aliyun.oss.accessKeySecret =aliyun.oss.bucketName =srr-web-tlias
1 2 3 4 5 6 7 8 9 10 11 @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; }
yml配置文件 配置格式 SpringBoot提供了多种属性配置方式。
application.properties
1 2 server.port =8080 server.address =127.0.0.1
application.yml
1 2 3 server: port: 8080 address: 127.0 .0 .1
application.yaml
1 2 3 server: port: 8080 address: 127.0 .0 .1
yml基本语法
大小写敏感。
数值前边必须有空格,作为分隔符。
使用缩进表示层级关系 ,缩进时,不允许使用Tab键,只能用空格(IDEA中会自动将Tab转换为空格)。
缩进的空格数目不重要,只要相同层级的元素左侧对齐 即可。
#表示注释,从这个字符一直到行尾,都会被解析器忽略。
yml数据格式 对象/Map集合:
1 2 3 4 user: name: zhangsan age: 18 password: 123456
数组/List/Set集合:
1 2 3 4 hobby: - java - game - sport
修改application.properties文件 在application.yml中的配置案例相关的配置项:(将application.properties修改为application.yml)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 server: port: 8080 spring: application: name: springboot-web-tlias datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/tlias username: root password: servlet: multipart: max-file-size: 10MB max-request-size: 100MB mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true aliyun: oss: endpoint: https://oss-cn-hangzhou.aliyuncs.com accessKeyId: LTAI5tK5ZM5yGVVFWvKxeNqJ accessKeySecret: bucketName: srr-web-tlias
@ConfigurationProperties@ConfigurationProperties与@Value都是用来注入外部配置的属性的。
不同:
@Value注解只能一个一个的进行外部属性的注入。
@ConfigurationProperties可以批量的将外部的属性配置注入到bean对象的属性中。
1 2 3 4 5 6 7 8 9 10 11 package com.itheima.utils;@Data @Component @ConfigurationProperties(prefix = "aliyun.oss") public class AliOSSProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.itheima.utils;@Component public class AliOSSUtils { @Autowired private AliOSSProperties aliOSSProperties; public String upload (MultipartFile file) throws IOException { String endpoint = aliOSSProperties.getEndpoint(); String accessKeyId = aliOSSProperties.getAccessKeyId(); String accessKeySecret = aliOSSProperties.getAccessKeySecret(); String bucketName = aliOSSProperties.getBucketName(); InputStream inputStream = file.getInputStream(); String originalFilename = file.getOriginalFilename(); String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("." )); OSS ossClient = new OSSClientBuilder ().build(endpoint, accessKeyId, accessKeySecret); ossClient.putObject(bucketName, fileName, inputStream); String url = endpoint.split("//" )[0 ] + "//" + bucketName + "." + endpoint.split("//" )[1 ] + "/" + fileName; ossClient.shutdown(); return url; } }
在pom.xml文件引入依赖(可选,引入该依赖,在配置yml时会有提示):
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > </dependency >
登录认证 员工登录
请求路径:/login
请求方式:POST
接口描述:该接口用于员工登录Tlias智能学习辅助系统,登录完毕后,系统下发JWT令牌。
请求参数样例:
1 2 3 4 { "username" : "jinyong" , "password" : "123456" }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.itheima.controller;@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("用户名或密码错误" ); } }
1 2 3 4 5 6 @Override public Emp login (Emp emp) { return empMapper.getByUsernameAndPassword(emp); }
1 2 3 @Select("select * from emp where username = #{username} and password = #{password}") Emp getByUsernameAndPassword (Emp emp) ;
登录校验 会话技术 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应 。
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
会话跟踪方案:
客户端会话跟踪技术:Cookie
服务端会话跟踪技术:Session
令牌技术
Cookie 优点:HTTP协议中支持的技术。
缺点:移动端APP无法使用Cookie,不安全,用户可以自己禁用Cookie,Cookie不能跨域。(跨域区分三个维度:协议、IP/域名、端口,任何一个不同则是跨域。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.itheima.controller;import com.itheima.pojo.Result;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import jakarta.servlet.http.Cookie;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import jakarta.servlet.http.HttpSession;@Slf4j @RestController public class SessionController { @GetMapping("/c1") public Result cookie1 (HttpServletResponse response) { response.addCookie(new Cookie ("login_username" ,"itheima" )); return Result.success(); } @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()); } } return Result.success(); } }
【报错】
参考链接:请求500失败-No primary or single unique constructor found-记录SpringBoot3.0 做导出功能HttpServletResponse的导包问题_no primary or single unique constructor found for -CSDN博客
在pom.xml文件下导入会话技术依赖(错误,不需要引入)。但是使用javax.servlet-api会报错java.lang.IllegalStateException: No primary or single unique constructor found for interface javax.servlet.http.HttpServletResponse。
1 2 3 4 5 6 <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 3.1.0</version > </dependency >
解决:这是因为springboot3.0的导出的依赖要从import javax.servlet.http.HttpServletResponse; 变为import jakarta.servlet.http.HttpServletResponse;。
原来使用会报错:
1 2 3 4 import javax.servlet.http.Cookie;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;
修改为即可正常运行:
1 2 3 4 import jakarta.servlet.http.Cookie;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import jakarta.servlet.http.HttpSession;
访问http://localhost:8080/c1,服务端会设置Cookie,在响应标头会显示set-cookie。
浏览器识别到set-cookie会将Cookie的值存储到浏览器本地。在Application应用程序中的Cookie下可以查看Cookie的值。
以后的每次请求浏览器都会将Cookie携带到服务端,访问http://localhost:8080/c2,浏览器将在请求标头的Cookie字段中将Cookie的值携带到服务端。
Session 优点:存储在服务端,安全。
缺点:服务器集群环境下无法直接使用Session,Cookie的缺点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.itheima.controller;@Slf4j @RestController public class SessionController { @GetMapping("/s1") public Result session1 (HttpSession session) { log.info("HttpSession-s1: {}" , session.hashCode()); session.setAttribute("loginUser" , "tom" ); 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" ); log.info("loginUser: {}" , loginUser); return Result.success(loginUser); } }
访问http://localhost:8080/s1,在响应标头set-cookie下的JSESSIONID代表服务器端session会话对象的ID。
浏览器会将Cookie的值存储到浏览器本地。在Application应用程序中的Cookie下可以查看JSESSIONID的值。
访问http://localhost:8080/s2,浏览器将在请求标头的Cookie字段中将Cookie的值,上述设置的login_username和JSESSIONID这两个Cookie都携带到服务端。服务器端会根据JSESSIONID找到对应的会话对象。
令牌技术 优点:支持PC端、移动端,解决集群环境下的认证问题,减轻服务器端存储压力。
缺点:需要自己实现。
【补充:cookie、session、JWT的区别】
参考链接:
还分不清 Cookie、Session、Token、JWT?看这一篇就够了-阿里云开发者社区 (aliyun.com)
一文彻底搞清session、cookie、token的区别 - 知乎 (zhihu.com)
面试必问:session,cookie和token的区别-腾讯云开发者社区-腾讯云 (tencent.com)
跨域:从头到尾讲讲 cookie?同源策略?跨越?解决跨域问题?_同源策略cookie-CSDN博客
JWT令牌 JWT JWT全称:JSON Web Token( https://jwt.io/ )。定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
组成:
第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}。
第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}。
第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
第一部分和第二部分使用Base64编码,可以通过Base64解码得到。
场景:登录认证
登录成功后,生成令牌。
后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理。
JWT生成和解析 1.引入依赖。老师用的是0.9.1版本的,我用的是最新版本的0.12.3。
1 2 3 4 5 <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.12.3</version > </dependency >
2.生成JWT和解析JWT。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.itheima;class SpringbootWebTliasApplicationTests { @Test public void testGenJwt () { Map<String, Object> claims = new HashMap <>(); claims.put("id" , 1 ); claims.put("name" , "surourou" ); String jwt = Jwts.builder() .signWith(SignatureAlgorithm.HS256, "itheimaitheimaitheimaitheimaitheimaitheimaitheimaitheima" ) .setClaims(claims) .setExpiration(new Date (System.currentTimeMillis() + 3600 * 1000 )) .compact(); System.out.println(jwt); } @Test public void testParseJwt () { Claims claims = Jwts.parser() .setSigningKey("itheimaitheimaitheimaitheimaitheimaitheimaitheimaitheima" ) .build() .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoic3Vyb3Vyb3UiLCJpZCI6MSwiZXhwIjoxNzI5NzM2Njc0fQ.kOLJv1uPsmn-bEUagHO53L1esUl3c7f_4JlbMUHKso0" ) .getBody(); System.out.println(claims); } }
注意:
JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。
如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效了,令牌非法。
报错:
io:签名秘钥需要大于或等于256bit。
io.jsonwebtoken.MalformedJwtException: Malformed protected header JSON: Unable to deserialize: Illegal character ((CTRL-CHAR, code 19)): only regular white space (\r, \n, \t) is allowed between tokens at [Source: REDACTED (StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION disabled); line: 1, column: 2]:Jwt令牌错误。
io.jsonwebtoken.ExpiredJwtException: JWT expired 23961 milliseconds ago at 2024-10-24T01:32:32.000Z. Current time: 2024-10-24T01:32:55.961Z. Allowed clock skew: 0 milliseconds.:Jwt令牌过期异常。
登录生成令牌 1.引入JWT令牌操作工具类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.itheima.utils;public class JwtUtils { private static String signKey = "itheimaitheimaitheimaitheimaitheimaitheimaitheimaitheima" ; 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) .build() .parseClaimsJws(jwt) .getBody(); return claims; } }
2.登录完成后,调用工具类生成JWT令牌,并返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.itheima.controller;@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); 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("用户名或密码错误" ); } }
进入登录页面,输入账号名和密码点击登录后,控制台Network网络会返回响应数据,响应数据包含生成的JWT令牌。
前端会将JWT令牌存储在本地,即存储在Application应用程序下的Local Storage本地存储中。本地存储在移动端也支持,而Cookie只支持浏览器,在移动端不支持。(这是通过前端来存储的)
前端将该JWT令牌的值存储起来,在后续的每一次请求当中都会将JWT令牌获取出来,然后携带到服务端。如:请求dept查询部门数据,前端在请求头token中携带了JWT令牌的值。
使用Postman测试:
请求路径:/login
请求方式:POST
接口描述:该接口用于员工登录Tlias智能学习辅助系统,登录完毕后,系统下发JWT令牌。
请求参数样例:
1 2 3 4 { "username" : "jinyong" , "password" : "123456" }
过滤器Filter 概念:Filter过滤器,是JavaWeb三大组件(Servlet、Filter、Listener)之一。
过滤器可以把对资源的请求拦截下来 ,从而实现一些特殊的功能。
过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
快速入门 1.定义Filter:定义一个类,实现Filter接口,并重写其所有方法。
注意:必须使用jakarta下的包,不能使用javax,javax包虽然不报错,但是无法实现过滤功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.itheima.filter;import jakarta.servlet.*;import jakarta.servlet.annotation.WebFilter;import java.io.IOException;@WebFilter(urlPatterns = "/*") public class DemoFilter implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { System.out.println("init 初始化方法执行了" ); } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("拦截到了请求" ); filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy () { System.out.println("destroy 销毁方法执行了" ); } }
2.配置Filter:Filter类上加@WebFilter注解,配置拦截资源的路径。引导类上加@ServletComponentScan开启Servlet组件支持。
Filter是JavaWeb三大组件之一,并不是SpringBoot当中提供的功能,在SpringBoot要想使用JavaWeb三大组件,必须要使用@ServletComponentScan组件,表示开启了对servlet组件的支持。
1 2 3 4 5 6 7 8 9 package com.itheima;@ServletComponentScan @SpringBootApplication public class SpringbootWebTliasApplication { public static void main (String[] args) { SpringApplication.run(SpringbootWebTliasApplication.class, args); } }
详解(执行流程、拦截路径、过滤器链)
1.执行流程:请求 –> 放行前逻辑 –> 放行 –> 资源 –> 放行后逻辑。
放行后访问对应资源,资源访问完成后,还会回到Filter中。回到Filter中是执行放行后的逻辑。
2.拦截路径
Filter可以根据需求,配置不同的拦截资源路径:Filter类上加@WebFilter注解,配置拦截资源的路径。
拦截路径
urlPatterns值
含义
拦截具体路径
/login
只有访问 /login 路径时,才会被拦截
目录拦截
/emps/*
访问/emps下的所有资源,都会被拦截
拦截所有
/*
访问所有资源,都会被拦截
3.过滤器链:一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。
顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序 。
doFilter中的参数FilterChain filterChain就是一个过滤器链,执行filterChain.doFilter就是放行到下一个过滤器,如果当前是最后一个过滤器,就会放行到Web资源当中来访问Web资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.itheima.filter;@WebFilter(urlPatterns = "/*") public class AbcFilter implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { System.out.println("Abc init 初始化方法执行了" ); } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("Abc 拦截到了请求,放行前逻辑" ); filterChain.doFilter(servletRequest, servletResponse); System.out.println("Abc 拦截到了请求,放行后逻辑" ); } @Override public void destroy () { System.out.println("Abc destroy 销毁方法执行了" ); } }
执行顺序:(注意:初始化和销毁的顺序不确定)
1 2 3 4 Abc 拦截到了请求,放行前逻辑 Demo 拦截到了请求,放行前逻辑 Demo 拦截到了请求,放行后逻辑 Abc 拦截到了请求,放行后逻辑
如果类名修改为AbcFilter,则执行顺序为:
1 2 3 4 Demo 拦截到了请求,放行前逻辑 Xbc 拦截到了请求,放行前逻辑 Xbc 拦截到了请求,放行后逻辑 Demo 拦截到了请求,放行后逻辑
登录校验Filter 流程:
获取请求url。
判断请求url中是否包含login,如果包含,说明是登录操作,放行。
获取请求头中的令牌(token)。
判断令牌是否存在,如果不存在,返回错误结果(未登录)。
解析token,如果解析失败,返回错误结果(未登录)。
放行。
在pom.xml文件中引入依赖:
1 2 3 4 5 6 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 2.0.52</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 package com.itheima.filter;import com.alibaba.fastjson.JSONObject;import com.itheima.pojo.Result;import com.itheima.utils.JwtUtils;import jakarta.servlet.*;import jakarta.servlet.annotation.WebFilter;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import lombok.extern.slf4j.Slf4j;import org.springframework.util.StringUtils;import java.io.IOException;@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; String url = req.getRequestURL().toString(); log.info("请求的url:{}" , url); if (url.contains("login" )){ log.info("登陆操作,放行" ); filterChain.doFilter(servletRequest, servletResponse); return ; } String jwt = req.getHeader("token" ); if (!StringUtils.hasLength(jwt)){ log.info("请求头token为空,返回未登录的信息" ); Result error = Result.error("NOT_LOGIN" ); String notLogin = JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return ; } try { JwtUtils.parseJWT(jwt); }catch (Exception e){ e.printStackTrace(); log.info("解析令牌失败,返回未登录错误信息" ); Result error = Result.error("NOT_LOGIN" ); String notLogin = JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return ; } log.info("令牌合法,放行" ); filterChain.doFilter(servletRequest, servletResponse); } }
使用前端页面测试时,需要在localStorage本地存储删除token值,然后随便点击一个页面,因为没有识别到token令牌,前端页面会跳转到登陆页面。登录成功后则会执行放行操作。
使用Postman测试时,随便一个申请(如:http://localhost:8080/depts)都会返回:`{"code":0,"msg":"NOT_LOGIN"}`。使用接口(http://localhost:8080/login)获取到token值:
1 2 3 4 5 { "code" : 1 , "msg" : "success" , "data" : "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTcyOTgwMzMwNH0.ePvji_UHQY8Yl88hoDntPt2FNZZ7Oc0duJzFpSNYAw0" }
在访问其他接口时,在Headers处填写token值即可获取到访问信息。如果token值为空或者报错,都会返回{"code":0,"msg":"NOT_LOGIN"}。
拦截器Interceptor 概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring`框架中提供的,用来动态拦截控制器方法的执行 。
作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。
快速入门 1.定义拦截器,实现HandlerInterceptor接口,并重写其所有方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.itheima.interceptor;@Component public class LoginCheckInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle" ); 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" ); } }
2.注册拦截器。
1 2 3 4 5 6 7 8 9 10 11 12 package com.itheima.config;@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginCheckInterceptor loginCheckInterceptor; @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**" ).excludePathPatterns("/login" ); } }
详解(拦截路径、执行流程) 1.拦截路径:拦截器可以根据需求,配置不同的拦截路径:
拦截路径
含义
举例
/*
一级路径
能匹配/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
2.拦截器执行流程:
Filter与Interceptor的区别:
接口规范不同 :过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
拦截范围不同 :过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。
登录校验Interceptor 和登录校验Filter的流程、代码一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 package com.itheima.interceptor;import com.alibaba.fastjson.JSONObject;import com.itheima.pojo.Result;import com.itheima.utils.JwtUtils;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import org.springframework.util.StringUtils;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;@Slf4j @Component public class LoginCheckInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { String url = req.getRequestURL().toString(); log.info("请求的url:{}" , url); if (url.contains("login" )){ log.info("登陆操作,放行" ); return true ; } String jwt = req.getHeader("token" ); if (!StringUtils.hasLength(jwt)){ log.info("请求头token为空,返回未登录的信息" ); Result error = Result.error("NOT_LOGIN" ); String notLogin = JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return false ; } try { JwtUtils.parseJWT(jwt); }catch (Exception e){ e.printStackTrace(); log.info("解析令牌失败,返回未登录错误信息" ); Result error = Result.error("NOT_LOGIN" ); String notLogin = JSONObject.toJSONString(error); resp.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" ); } }
异常处理 出现异常时(比如:添加存在的部门时,会报错:java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '就业部' for key 'dept.name'),默认返回的结果不符合规范:
1 2 3 4 5 6 { "timestamp" : "2024-10-24T09:20:35.147+00:00" , "status" : 500 , "error" : "Internal Server Error" , "path" : "/depts" }
出现异常时,异常一层一层往上抛,最后抛给框架,框架返回上述JSON格式数据,里面装着错误信息。但是该错误信息不符合开发规范,需要返回Result格式的JSON数据。
出现异常时处理方法:
方案一:在Controller的方法中进行try…catch处理。(代码臃肿,不推荐)
方案二:全局异常处理器。(简单、优雅,推荐)
1 2 3 4 5 6 7 8 9 10 11 package com.itheima.exception;@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public Result exception (Exception e) { e.printStackTrace(); return Result.error("对不起,操作失败,请联系管理员" ); } }
事务管理 事务是一组操作的集合,它是一个不可分割的工作单位,这些操作要么同时成功,要么同时失败。
开启事务(一组操作开始前,开启事务):start transaction / begin ;
提交事务(这组操作全部成功后,提交事务):commit ;
回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;
Spring事务管理 案例:解散部门:删除部门,同时删除该部门下的员工。
注解:@Transactional
位置:业务(service)层的方法上、类上、接口上。
作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务。
1 2 3 4 @Delete("delete from emp where dept_id = #{deptId}") void deleteByDeptId (Integer deptId) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.itheima.service.impl;@Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Autowired private EmpMapper empMapper; @Transactional @Override public void delete (Integer id) { deptMapper.deleteById(id); empMapper.deleteByDeptId(id); } }
开启事务管理日志(可以显示事务开启、提交、回滚等信息):
1 2 3 4 logging: level: org.springframework.jdbc.support.JdbcTransactionManager: debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #事务管理日志 #创建日志2024 -10 -25 T14:22 :20 .343 +08 :00 DEBUG 11996 --- [springboot-web-tlias] [nio-8080 -exec-3 ] o.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [com.itheima.service.impl.DeptServiceImpl.delete]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT2024 -10 -25 T14:22 :20 .346 +08 :00 DEBUG 11996 --- [springboot-web-tlias] [nio-8080 -exec-3 ] o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [HikariProxyConnection@1443413090 wrapping com.mysql.cj.jdbc.ConnectionImpl@6 b466344] for JDBC transaction2024 -10 -25 T14:22 :20 .351 +08 :00 DEBUG 11996 --- [springboot-web-tlias] [nio-8080 -exec-3 ] o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [HikariProxyConnection@1443413090 wrapping com.mysql.cj.jdbc.ConnectionImpl@6 b466344] to manual commit #日志回滚2024 -10 -25 T14:22 :20 .368 +08 :00 DEBUG 11996 --- [springboot-web-tlias] [nio-8080 -exec-3 ] o.s.jdbc.support.JdbcTransactionManager : Initiating transaction rollback2024 -10 -25 T14:22 :20 .369 +08 :00 DEBUG 11996 --- [springboot-web-tlias] [nio-8080 -exec-3 ] o.s.jdbc.support.JdbcTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@1443413090 wrapping com.mysql.cj.jdbc.ConnectionImpl@6 b466344]2024 -10 -25 T14:22 :20 .392 +08 :00 DEBUG 11996 --- [springboot-web-tlias] [nio-8080 -exec-3 ] o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [HikariProxyConnection@1443413090 wrapping com.mysql.cj.jdbc.ConnectionImpl@6 b466344] after transaction #日志提交2024 -10 -25 T14:26 :06 .811 +08 :00 DEBUG 39052 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Initiating transaction commit2024 -10 -25 T14:26 :06 .812 +08 :00 DEBUG 39052 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@2140932661 wrapping com.mysql.cj.jdbc.ConnectionImpl@3 cd75022]2024 -10 -25 T14:26 :06 .826 +08 :00 DEBUG 39052 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [HikariProxyConnection@2140932661 wrapping com.mysql.cj.jdbc.ConnectionImpl@3 cd75022] after transaction
事务属性 回滚 默认情况下,只有出现RuntimeException才回滚异常。rollbackFor属性用于控制出现何种异常类型,回滚事务。
1 2 3 4 5 6 7 8 9 10 @Transactional(rollbackFor = Exception.class) @Override public void delete (Integer id) throws Exception { deptMapper.deleteById(id); if (true ){ throw new Exception ("出错啦" ); } empMapper.deleteByDeptId(id); }
传播行为 事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
属性值
含义
REQUIRED
【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW
需要新事务,无论有无,总是创建新事务
SUPPORTS
支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED
不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY
必须有事务,否则抛异常
NEVER
必须没事务,否则抛异常
案例 需求:解散部门时,无论是成功还是失败,都要记录操作日志。
步骤:
解散部门:删除部门、删除部门下的员工。
记录日志到数据库表中。
REQUIRED:大部分情况下都是用该传播行为即可。
REQUIRES_NEW:不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。
1.新建部门操作日志表。
1 2 3 4 5 create table dept_log( id int auto_increment comment '主键ID' primary key, create_time datetime null comment '操作时间', description varchar(300) null comment '操作描述' )comment '部门操作日志表';
2.记录日志到数据库表中。
1 2 3 4 5 6 7 8 9 10 package com.itheima.pojo;@Data @NoArgsConstructor @AllArgsConstructor public class DeptLog { private Integer id; private LocalDateTime createTime; private String description; }
1 2 3 4 5 package com.itheima.service;public interface DeptLogService { void insert (DeptLog deptLog) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.itheima.service.impl;@Service public class DeptLogServiceImpl implements DeptLogService { @Autowired private DeptLogMapper deptLogMapper; @Transactional @Override public void insert (DeptLog deptLog) { deptLogMapper.insert(deptLog); } }
1 2 3 4 5 6 7 package com.itheima.mapper;@Mapper public interface DeptLogMapper { @Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})") void insert (DeptLog log) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.itheima.service.impl;@Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Autowired private EmpMapper empMapper; @Autowired private DeptLogService deptLogService; @Transactional(rollbackFor = Exception.class) @Override public void delete (Integer id) throws Exception { try { deptMapper.deleteById(id); int i = 1 /0 ; empMapper.deleteByDeptId(id); } finally { DeptLog deptLog = new DeptLog (); deptLog.setCreateTime(LocalDateTime.now()); deptLog.setDescription("执行了解散部门的操作,此次操作的是:" + id + "号部门" ); deptLogService.insert(deptLog); } } }
当删除某一个部门时,运行结果:
1 2 3 4 5 6 7 8 9 # 操作日志2024 -10 -27 T10:51 :48 .305 +08 :00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [com.itheima.service.impl.DeptServiceImpl.delete]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception2024 -10 -27 T10:51 :48 .567 +08 :00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [HikariProxyConnection@504771362 wrapping com.mysql.cj.jdbc.ConnectionImpl@7 d17447b] for JDBC transaction2024 -10 -27 T10:51 :48 .569 +08 :00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [HikariProxyConnection@504771362 wrapping com.mysql.cj.jdbc.ConnectionImpl@7 d17447b] to manual commit2024 -10 -27 T10:51 :48 .618 +08 :00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Participating in existing transaction2024 -10 -27 T10:51 :48 .627 +08 :00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Initiating transaction rollback2024 -10 -27 T10:51 :48 .627 +08 :00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@504771362 wrapping com.mysql.cj.jdbc.ConnectionImpl@7 d17447b]2024 -10 -27 T10:51 :48 .629 +08 :00 DEBUG 28976 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [HikariProxyConnection@504771362 wrapping com.mysql.cj.jdbc.ConnectionImpl@7 d17447b] after transactionjava.lang.ArithmeticException: / by zero
执行过程:默认的事务传播行为是REQUIRED,如果public void insert(DeptLog deptLog)方法使用默认的传播行为,调用deptLogService.insert(deptLog)方法时,调用已存在的事务empMapper.deleteByDeptId(id),即:执行empMapper.deleteByDeptId(id)和deptLogService.insert(deptLog)两个方法共用一个事务。执行deptLogService.insert(deptLog)成功插入一条语句后,由于在该方法执行过程中抛出了java.lang.ArithmeticException: / by zero的算术运算异常,所以整个事务都需要回滚Initiating transaction rollback,由于两个方法共用一个事务,所以两个方法都会回滚,所以无法插入操作日志。
解决方法:设置public void insert(DeptLog deptLog)的传播行为为REQUIRES_NEW。无论当前运行方法中是否有事务,在调用public void insert(DeptLog deptLog)方法时,都会开启一个新的事务。
1 2 3 4 5 6 @Transactional(propagation = Propagation.REQUIRES_NEW) @Override public void insert (DeptLog deptLog) { deptLogMapper.insert(deptLog); }
当删除某一个部门时,运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #操作日志2024 -10 -27 T10:59 :46 .505 +08 :00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Creating new transaction with name [com.itheima.service.impl.DeptServiceImpl.delete]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception2024 -10 -27 T10:59 :46 .755 +08 :00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [HikariProxyConnection@1598400954 wrapping com.mysql.cj.jdbc.ConnectionImpl@73 cb1ed4] for JDBC transaction2024 -10 -27 T10:59 :46 .757 +08 :00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [HikariProxyConnection@1598400954 wrapping com.mysql.cj.jdbc.ConnectionImpl@73 cb1ed4] to manual commit2024 -10 -27 T10:59 :46 .813 +08 :00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Suspending current transaction, creating new transaction with name [com.itheima.service.impl.DeptLogServiceImpl.insert]2024 -10 -27 T10:59 :46 .833 +08 :00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Acquired Connection [HikariProxyConnection@1499621420 wrapping com.mysql.cj.jdbc.ConnectionImpl@79341019 ] for JDBC transaction2024 -10 -27 T10:59 :46 .833 +08 :00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Switching JDBC Connection [HikariProxyConnection@1499621420 wrapping com.mysql.cj.jdbc.ConnectionImpl@79341019 ] to manual commit2024 -10 -27 T10:59 :46 .844 +08 :00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Initiating transaction commit2024 -10 -27 T10:59 :46 .844 +08 :00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@1499621420 wrapping com.mysql.cj.jdbc.ConnectionImpl@79341019 ]2024 -10 -27 T10:59 :46 .847 +08 :00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [HikariProxyConnection@1499621420 wrapping com.mysql.cj.jdbc.ConnectionImpl@79341019 ] after transaction2024 -10 -27 T10:59 :46 .849 +08 :00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Resuming suspended transaction after completion of inner transaction2024 -10 -27 T10:59 :46 .849 +08 :00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Initiating transaction rollback2024 -10 -27 T10:59 :46 .850 +08 :00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@1598400954 wrapping com.mysql.cj.jdbc.ConnectionImpl@73 cb1ed4]2024 -10 -27 T10:59 :46 .853 +08 :00 DEBUG 13824 --- [springboot-web-tlias] [nio-8080 -exec-1 ] o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [HikariProxyConnection@1598400954 wrapping com.mysql.cj.jdbc.ConnectionImpl@73 cb1ed4] after transactionjava.lang.ArithmeticException: / by zero
执行过程:运行empMapper.deleteByDeptId(id)方法时会创建新事务,调用deptLogService.insert(deptLog)方法时,挂起当前事务,然后又创建一个新事务。当deptLogService.insert(deptLog)方法运行完毕后,提交该事务。当内部的事务运行结束之后,继续完成刚才挂起的事务,即empMapper.deleteByDeptId(id)方法所开启的事务。由于该事务运行过程中报错了,所以外部事务会进行正常的回滚操作。
AOP AOP概述 AOP:Aspect Oriented Programming(面向切面编程 、面向方面编程),其实就是面向特定方法编程。
场景:案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时。
实现:动态代理 是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
AOP快速入门 【SpringAOP快速入门:统计各个业务层方法执行耗时】
1.导入依赖:在pom.xml中导入AOP的依赖。
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency >
2.编写AOP程序:针对于特定方法根据业务需要进行编程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.itheima.aop;@Slf4j @Component @Aspect public class TimeAspect { @Around("execution(* com.itheima.service.*.*(..))") public Object recordTime (ProceedingJoinPoint joinPoint) throws Throwable { long begin = System.currentTimeMillis(); Object result = joinPoint.proceed(); long end = System.currentTimeMillis(); log.info(joinPoint.getSignature() + "方法执行耗时:{}ms" , end - begin); return result; } }
AOP场景:记录操作日志、权限控制、事务管理。
优势:代码无侵入、减少重复代码、提高开发效率、维护方便。
AOP核心概念 连接点 :JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)。
通知 :Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)。
切入点 :PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用。
切面 :Aspect,描述通知与切入点的对应关系(通知+切入点)。
目标对象 :Target,通知所应用的对象。
AOP执行流程 (以下图为例):SpringAOP底层是基于动态代理技术实现的。在程序运行的时候,会基于动态代理技术为目标对象DeptServiceImpl生成一个代理对象DeptServiceProxy。在代理对象中会对目标对象的原始方法进行功能增强,增强的逻辑就是AOP里面定义的通知 (Advice):在方法运行开始之前先记录方法运行的开始时间,接下来调用原始的方法(list方法)执行,之后记录方法运行的结束时间,最后统计方法执行的耗时。最终在DeptController程序运行时,注入的的DeptService不再是目标对象DeptServiceImpl,而是注入代理对象DeptServiceProxy。最终调用list方法时,调用的是代理对象DeptServiceProxy中的list方法,这个list方法已经进行了功能的增强。
运行中断点显示:使用了Spring底层动态代理技术:CG类网动态代理。
通知类型
@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行。
@Before:前置通知,此注解标注的通知方法在目标方法前被执行。
@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行。
@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行。
@AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行。
注意:
@Around环绕通知需要自己调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行。
@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值。
@AfterReturning和@AfterThrowing 是互斥的,只有一个会发生。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.itheima.aop;@Slf4j @Component @Aspect public class MyAspect { @Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") public void before () { log.info("before ..." ); } @Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") public Object around (ProceedingJoinPoint proceedingJoinPoint) throws Throwable { log.info("around before ..." ); Object result = proceedingJoinPoint.proceed(); log.info("around after ..." ); return result; } @After("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") public void after () { log.info("after ..." ); } @AfterReturning("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") public void afterReturning () { log.info("afterReturning ..." ); } @AfterThrowing("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") public void afterThrowing () { log.info("afterThrowing ..." ); } }
1 2 3 4 5 6 #没有异常时的输出日志:没有afterThrowing2024 -10 -27 T15:45 :38 .714 +08 :00 INFO 14000 --- [springboot-web-tlias] [nio-8080 -exec-1 ] com.itheima.aop.MyAspect : around before ...2024 -10 -27 T15:45 :38 .715 +08 :00 INFO 14000 --- [springboot-web-tlias] [nio-8080 -exec-1 ] com.itheima.aop.MyAspect : before ...2024 -10 -27 T15:45 :39 .174 +08 :00 INFO 14000 --- [springboot-web-tlias] [nio-8080 -exec-1 ] com.itheima.aop.MyAspect : afterReturning ...2024 -10 -27 T15:45 :39 .174 +08 :00 INFO 14000 --- [springboot-web-tlias] [nio-8080 -exec-1 ] com.itheima.aop.MyAspect : after ...2024 -10 -27 T15:45 :39 .174 +08 :00 INFO 14000 --- [springboot-web-tlias] [nio-8080 -exec-1 ] com.itheima.aop.MyAspect : around after ...
1 2 3 4 5 6 #在DeptServiceImpl类下的list方法加入int i = 1 /0 ; 页面访问部门管理时会出现异常 #异常时的输出日志:没有afterReturning和around after2024 -10 -27 T15:53 :17 .835 +08 :00 INFO 2472 --- [springboot-web-tlias] [nio-8080 -exec-1 ] com.itheima.aop.MyAspect : around before ...2024 -10 -27 T15:53 :17 .836 +08 :00 INFO 2472 --- [springboot-web-tlias] [nio-8080 -exec-1 ] com.itheima.aop.MyAspect : before ...2024 -10 -27 T15:53 :17 .836 +08 :00 INFO 2472 --- [springboot-web-tlias] [nio-8080 -exec-1 ] com.itheima.aop.MyAspect : afterThrowing ...2024 -10 -27 T15:53 :17 .836 +08 :00 INFO 2472 --- [springboot-web-tlias] [nio-8080 -exec-1 ] com.itheima.aop.MyAspect : after ...
@PointCut@PointCut:将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。
private:仅能在当前切面类中引用该表达式。
public:在其他外部的切面类中也可以引用该表达式。在当前切面类使用公共的切点表达式:@Around("pt()"),在其他外部切面类使用公共的切点表达式:@Around("com.itheima.aop.MyAspect1.pt()")。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.itheima.aop;@Slf4j @Component @Aspect public class MyAspect1 { @Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") public void pt () {} @Before("pt()") public void before () { log.info("before ..." ); } @Around("pt()") public Object around (ProceedingJoinPoint proceedingJoinPoint) throws Throwable { log.info("around before ..." ); Object result = proceedingJoinPoint.proceed(); log.info("around after ..." ); return result; } @After("pt()") public void after () { log.info("after ..." ); } @AfterReturning("pt()") public void afterReturning () { log.info("afterReturning ..." ); } @AfterThrowing("pt()") public void afterThrowing () { log.info("afterThrowing ..." ); } }
通知顺序 当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。
1.不同切面类中,默认按照切面类的类名字母排序 :
目标方法前的通知方法:字母排名靠前的先执行。
目标方法后的通知方法:字母排名靠前的后执行。
2.用@Order(数字)加在切面类上来控制顺序 。
目标方法前的通知方法:数字小的先执行。
目标方法后的通知方法:数字小的后执行。
切入点表达式 切入点表达式:描述切入点方法的一种表达式。
作用:主要用来决定项目中的哪些方法需要加入通知 。
常见形式:
execution(……):根据方法的签名来匹配。
@annotation(……):根据注解匹配。
executionexecution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
1 execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带?的表示可以省略的部分。
访问修饰符:可省略(比如: public、protected)
包名.类名: 可省略
throws异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
可以使用通配符描述切入点。
注意:根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
1 2 3 @Pointcut("execution(* com.itheima.service.DeptService.list()) || " + "execution(* com.itheima.service.DeptService.delete(java.lang.Integer))") private void pt () {}
书写建议:
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是find开头,更新类方法都是update开头。
描述切入点方法通常基于接口描述 ,而不是直接描述实现类,增强拓展性。
在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用..,使用*匹配单个包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.itheima.aop;@Slf4j @Aspect @Component public class MyAspect6 { @Pointcut("execution(* com.itheima.service.DeptService.list()) || " + "execution(* com.itheima.service.DeptService.delete(java.lang.Integer))") private void pt () {} @Before("pt()") public void before () { log.info("MyAspect6 ... before ..." ); } }
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 package com.itheima;@SpringBootTest class SpringbootAopQuickstart1ApplicationTests { @Autowired private DeptService deptService; @Test public void testAopDelete () throws Exception { deptService.delete(10 ); } @Test public void testAopList () { List<Dept> list = deptService.list(); System.out.println(list); } @Test public void testAopGetById () { Dept dept = deptService.get(1 ); System.out.println(dept); } }
@annotation@annotation切入点表达式,用于匹配标识有特定注解的方法。
1 2 3 4 5 6 package com.itheima.aop;@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyLog { }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.itheima.aop;@Slf4j @Aspect @Component public class MyAspect7 { @Pointcut("@annotation(com.itheima.aop.MyLog)") private void pt () {} @Before("pt()") public void before () { log.info("MyAspect7 ... before ..." ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.itheima.service.impl;@Service public class DeptServiceImpl implements DeptService { @MyLog @Override public List<Dept> list () { return deptMapper.list(); } @MyLog @Transactional(rollbackFor = Exception.class) @Override public void delete (Integer id) throws Exception { try { deptMapper.deleteById(id); empMapper.deleteByDeptId(id); } finally { DeptLog deptLog = new DeptLog (); deptLog.setCreateTime(LocalDateTime.now()); deptLog.setDescription("执行了解散部门的操作,此次操作的是:" + id + "号部门" ); deptLogService.insert(deptLog); } } }
连接点 在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint。
对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.itheima.aop;@Slf4j @Aspect @Component public class MyAspect8 { @Pointcut("execution(* com.itheima.service.DeptService.*(..))") private void pt () {} @Before("pt()") public void before (JoinPoint joinPoint) { log.info("MyAspect8 ... before ..." ); } @Around("pt()") public Object around (ProceedingJoinPoint joinPoint) throws Throwable { log.info("MyAspect8 around before ..." ); String className = joinPoint.getTarget().getClass().getName(); log.info("目标对象的类名:{}" , className); String methodName = joinPoint.getSignature().getName(); log.info("目标方法的方法名: {}" ,methodName); Object[] args = joinPoint.getArgs(); log.info("目标方法运行时传入的参数: {}" , Arrays.toString(args)); Object result = joinPoint.proceed(); log.info("目标方法运行的返回值: {}" ,result); log.info("MyAspect8 around after ..." ); return result; } }
AOP案例 【案例】将增、删、改相关接口的操作日志记录到数据库表中。日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长。
1.导入资料中准备好的数据库表结构,并引入对应的实体类。
1 2 3 4 5 6 7 8 9 10 11 -- 操作日志表 create table operate_log( id int unsigned primary key auto_increment comment 'ID', operate_user int unsigned comment '操作人ID', operate_time datetime comment '操作时间', class_name varchar(100) comment '操作的类名', method_name varchar(100) comment '操作的方法名', method_params varchar(1000) comment '方法参数', return_value varchar(2000) comment '返回值', cost_time bigint comment '方法执行耗时, 单位:ms' ) comment '操作日志表';
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.itheima.pojo;@Data @NoArgsConstructor @AllArgsConstructor public class OperateLog { private Integer id; private Integer operateUser; private LocalDateTime operateTime; private String className; private String methodName; private String methodParams; private String returnValue; private Long costTime; }
1 2 3 4 5 6 7 8 9 package com.itheima.mapper;@Mapper public interface OperateLogMapper { @Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " + "values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});") public void insert (OperateLog log) ; }
2.自定义注解@Log。
1 2 3 4 5 6 package com.itheima.anno;@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Log { }
3.定义切面类,完成记录操作日志的逻辑。
获取当前登录用户:获取request对象,从请求头中获取到jwt令牌,解析令牌获取出当前用户的id。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.itheima.aop;import jakarta.servlet.http.HttpServletRequest;@Slf4j @Component @Aspect public class LogAspect { @Autowired private HttpServletRequest request; @Autowired private OperateLogMapper operateLogMapper; @Around("@annotation(com.itheima.anno.Log)") public Object recordLog (ProceedingJoinPoint joinPoint) throws Throwable { String jwt = request.getHeader("token" ); Claims claims = JwtUtils.parseJWT(jwt); Integer operateUser = (Integer) claims.get("id" ); LocalDateTime operateTime = LocalDateTime.now(); String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); String methodParams = Arrays.toString(args); long begin = System.currentTimeMillis(); Object result = joinPoint.proceed(); long end = System.currentTimeMillis(); String returnValue = JSONObject.toJSONString(result); long costTime = end - begin; OperateLog operateLog = new OperateLog (null , operateUser, operateTime, className, methodName, methodParams, returnValue, costTime); operateLogMapper.insert(operateLog); log.info("AOP记录操作日志:{}" , operateLog); return result; } }
4.在DeptController和EmpController的增、删、改方法中加入@Log注解。