案例
环境搭建
- 准备数据库(dept、emp)
CREATE TABLE emp(
id int UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT 'ID',
username VARCHAR(20) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(32) NOT NULL 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 (username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time)
VALUES
('user1', 'password1', '张三', 1, 'image1.jpg', 1, '2023-05-01', 1, NOW(), NOW()),
('user2', 'password2', '李四', 1, 'image2.jpg', 2, '2023-05-02', 2, NOW(), NOW()),
('user3', 'password3', '王五', 1, 'image3.jpg', 3, '2023-05-03', 1, NOW(), NOW()),
('user4', 'password4', '赵六', 2, 'image4.jpg', 4, '2023-05-04', 3, NOW(), NOW()),
('user5', 'password5', '小明', 1, 'image5.jpg', 5, '2023-05-05', 2, NOW(), NOW()),
('user6', 'password6', '小红', 2, 'image6.jpg', 1, '2023-05-06', 1, NOW(), NOW()),
('user7', 'password7', '小刚', 1, 'image7.jpg', 2, '2023-05-07', 2, NOW(), NOW()),
('user8', 'password8', '小美', 2, 'image8.jpg', 3, '2023-05-08', 1, NOW(), NOW()),
('user9', 'password9', '小强', 1, 'image9.jpg', 4, '2023-05-09', 3, NOW(), NOW()),
('user10', 'password10', '小花', 2, 'image10.jpg', 5, '2023-05-10', 2, NOW(), NOW()),
('user11', 'password11', '大明', 1, 'image11.jpg', 1, '2023-05-11', 1, NOW(), NOW()),
('user12', 'password12', '大红', 2, 'image12.jpg', 2, '2023-05-12', 2, NOW(), NOW()),
('user13', 'password13', '大刚', 1, 'image13.jpg', 3, '2023-05-13', 1, NOW(), NOW()),
('user14', 'password14', '大美', 2, 'image14.jpg', 4, '2023-05-14', 3, NOW(), NOW()),
('user15', 'password15', '大强', 1, 'image15.jpg', 5, '2023-05-15', 2, NOW(), NOW()),
('user16', 'password16', '大花', 2, 'image16.jpg', 1, '2023-05-16', 1, NOW(), NOW()),
('user17', 'password17', '大明', 1, 'image17.jpg', 2, '2023-05-17', 2, NOW(), NOW());
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 (name, create_time, update_time)
VALUES
('部门A', NOW(), NOW()),
('部门B', NOW(), NOW()),
('部门C', NOW(), NOW()),
('部门D', NOW(), NOW()),
('部门E', NOW(), NOW()),
('部门F', NOW(), NOW());
- 创建springboot工程,引入对应的起步依赖(web、mybatis、mybatis驱动、lombok)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 配置文件application.properties中引入mybatis的配置信息,准备对应的实体类
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/itheima
spring.datasource.username=itcast
spring.datasource.password=root
#配置mybatis的日志,指定输出到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#开启mybatis驼峰命名自动映射开关
mybatis.configuration.map-underscore-to-camel-case=true
- 准备对应的Mapper、Service、Controller基础结构
开发规范——Restful
-
REST(representational state transfer),表述性状态转换,软件架构风格
-
url定位资源,HTTP动词描述操作
-
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的用户 -
统一响应结果
package com.example.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@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);
}
}
开发流程
- 查看页面原型,明确需求
- 阅读接口文档
- 思路分析
- 接口开发:日志小技巧
- 接口测试:postman测试
- 前后端联调
部门管理
-
部门Controller层
@Slf4j // 日志注解 @RestController @CrossOrigin @RequestMapping("/depts") public class DeptController { // private static org.mybatis.logging.Logger // log=LoggerFactory.getLogger(DeptController.class); @Autowired private DeptService deptService; // 查询部门数据 // @RequestMapping(value = "/depts", method = RequestMethod.GET) // 指定请求方式为get @GetMapping public Result list() { log.info("查询全部部门数据"); // 调用service查询部门数据 List<Dept> deptList = deptService.list(); return Result.success(deptList); } // 删除部门 @DeleteMapping("/{id}") public Result delete(@PathVariable Integer id) { log.info("根据id删除部门:{}", id); // 调用service删除部门 deptService.delete(id); return Result.success(); } // 新增部门 @PostMapping public Result add(@RequestBody Dept dept) { log.info("新增部门:{}", dept); // 调用service新增部门 deptService.add(dept); return Result.success(); } // 根据ID查询部门 @GetMapping("/{id}") public Result selectById(@PathVariable Integer id) { log.info("根据ID查询部门:{}", id); // 调用service根据ID查询部门 Dept dept = deptService.selectById(id); return Result.success(dept); } // 修改部门 @PutMapping public Result update(@RequestBody Dept dept) { log.info("修改部门:{}", dept); // 根据ID修改部门 deptService.update(dept); return Result.success(); } }
-
部门Service层
public interface DeptService { public List<Dept> list(); public void delete(Integer id); public void add(Dept dept); public Dept selectById(Integer id); public void update(Dept dept); }
@Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Autowired private EmpMapper empMapper; @Override public List<Dept> list() { return deptMapper.list(); } @Override public void delete(Integer id) { deptMapper.deleteById(id);//根据部门ID删除部门数据 //根据部门ID删除员工数据 empMapper.deleteByDeptId(id); } @Override public void add(Dept dept) { dept.setCreDateTime(LocalDateTime.now()); dept.setUpDateTime(LocalDateTime.now()); deptMapper.insert(dept); } @Override public Dept selectById(Integer id){ return deptMapper.selectById(id); } @Override public void update(Dept dept){ deptMapper.update(dept); } }
-
部门Mapper层
@Mapper public interface DeptMapper { // 查询全部部门 @Select("select * from dept") List<Dept> list(); // 根据id删除部门 @Delete("delete from dept where id = #{id}") void deleteById(Integer id); // 新增部门 @Insert("insert into dept(name,create_time,update_time) values(#{name},#{creDateTime},#{upDateTime})") void insert(Dept dept); //根据ID查询部门 @Select("select * from dept where id = #{id}") Dept selectById(Integer id); //根据ID修改部门 @Update("update dept set name = #{name} where id = #{id}") void update(Dept dept); }
员工管理
-
添加分页插件
<!-- 分页插件 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.2</version> </dependency>
-
职工Controller层
@Slf4j // 日志注解 @RestController @CrossOrigin @RequestMapping("/emps") public class EmpController { @Autowired private EmpService empService; @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); // 调用service分页查询 PageBean pageBean = empService.page(page, pageSize, name, gender, begin, end); return Result.success(pageBean); } @DeleteMapping("/{ids}") public Result delete(@PathVariable List<Integer>ids){ log.info("批量删除操作,ids:{}",ids); empService.delete(ids); return Result.success(); } @PostMapping public Result save(@RequestBody Emp emp){ log.info("新增员工,emp:{}",emp); empService.save(emp); return Result.success(); } @GetMapping("/{id}") public Result getById(@PathVariable Integer id){ log.info("根据ID查询员工信息,id:{}",id); Emp emp=empService.getById(id); return Result.success(emp); } @PutMapping public Result update(@RequestBody Emp emp){ log.info("更新员工信息:{}",emp); empService.update(emp); return Result.success(); } }
-
职工Service层
public interface EmpService { public PageBean page(Integer page,Integer pageSize,String name,Short gender,LocalDate begin,LocalDate end); public void delete(List<Integer>ids); public void save(Emp emp); public Emp getById(Integer id); public void update(Emp emp); }
@Service public class EmpServiceImpl implements EmpService { @Autowired private EmpMapper empMapper; // @Override // public PageBean page(Integer page, Integer pageSize) { // // 获取总记录数 // Long total = empMapper.count(); // // 获取分页结果列表 // Integer start = (page - 1) * pageSize; // List<Emp> rows = empMapper.page(start, pageSize); // // 封装PageBean对象 // PageBean pageBean = new PageBean(); // pageBean.setTotal(total); // pageBean.setRows(rows); // return pageBean; // } @Override public PageBean page(Integer page,Integer pageSize,String name,Short gender,LocalDate begin,LocalDate end){ //设置分页数 PageHelper.startPage(page, pageSize); //执行查询 List<Emp> emplList=empMapper.list(name,gender,begin,end); Page<Emp>p=(Page<Emp>)emplList; //封装PageBean对象 PageBean pageBean=new PageBean(); pageBean.setTotal(p.getTotal()); pageBean.setRows(p.getResult()); return pageBean; } @Override public void delete(List<Integer>ids){ empMapper.delete(ids); } @Override public void save(Emp emp){ emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); empMapper.save(emp); } @Override public Emp getById(Integer id){ return empMapper.getById(id); } @Override public void update(Emp emp){ empMapper.update(emp); } }
-
职工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); // @Select("select * from emp") public List<Emp> list(String name,Short gender,LocalDate begin,LocalDate end); public void delete(List<Integer> ids); // 新增员工 @Insert("insert into emp(username,name,gender,image,job,entrydate,dept_id,create_time,update_time)" +" values(#{username},#{name},#{gender},#{image},#{job},#{entrydate},#{deptId},#{createTime},#{updateTime})") public void save(Emp emp); // 根据ID查询员工 @Select("select * from emp where id = #{id}") public Emp getById(Integer id); //更新员工信息 public void update(Emp emp); //根据部门ID删除该部门下的员工数据 @Delete("delete from emp where dept_id = #{deptId}") void deleteByDeptId(Integer deptId); }
<?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.example.mapper.EmpMapper"> <select id = "list" resultType="com.example.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> <delete id="delete"> delete from emp where id in <foreach collection="ids" item="id" separator="," open="(" close=")"> #{id} </foreach> </delete> <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 and gender!=''"> gender=#{gender}, </if> <if test="image!=null and image!=''"> image=#{image}, </if> <if test="job!=null and job!=''"> job=#{job}, </if> <if test="entrydate!=null and entrydate!=''"> entrydate=#{entrydate}, </if> <if test="deptId!=null and deptId!=''"> dept_id=#{deptId}, </if> <if test="updateTime!=null and updateTime!=''"> update_time=#{updateTime} </if> </set> where id = #{id} </update> </mapper>
文件上传
- 前端页面三要素
- 表单项type=“file”
- 表单提交方式post
- 表单的enctype属性multipart/form-data
- 服务端接收文件
- MutipartFile
本地存储
-
实现UploadController类
@Slf4j @CrossOrigin @RestController public class UploadController { @PostMapping("/upload") public Result upload(String username,Integer age,MultipartFile image)throws Exception{ log.info("文件上传:{},{},{}",username,age,image); //获取原始文件名 String originFilename=image.getOriginalFilename(); //构造唯一的文件名(不能重复)——uuid(通用唯一标识符) int index = originFilename.lastIndexOf("."); String extname=originFilename.substring(index); String newFilename=UUID.randomUUID().toString()+extname; log.info("新的文件名:{}", newFilename); //将文件存储在服务器的磁盘目录中 image.transferTo(new File("G:\\APPDATA\\code\\"+newFilename)); return Result.success(); } }
-
配置上传文件限制
#配置单个文件上传大小限制 spring.servlet.multipart.max-file-size=10MB #配置单个请求最大大小的限制(一次请求中可以上传多个文件) spring.servlet.multipart.max-request-size=100MB
云存储OSS
-
云存储
- 云对象存储OSS(object storage service)
- SDK(softwa development kit),包括辅助依赖jar包、代码示例
- Bucket:存储空间、用户存储对象的容器
-
实现AliOSSUtils类
@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.bucketNam}") private String bucketNam; // 实现上传图片到OSS public String upload(MultipartFile file)throws IOException{ // 获取上传的文件的输入流 InputStream inputStream=file.getInputStream(); // 避免文件覆盖 String originFilename=file.getOriginalFilename(); int index = originFilename.lastIndexOf("."); String extname=originFilename.substring(index); String newFilename=UUID.randomUUID().toString()+extname; // 上传文件到OSS OSS ossClient=new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); ossClient.putObject(bucketName, newFilename, inputStream); //关闭文件流,防止残留本地缓存文件tmp报错 inputStream.close(); // 文件访问路径 String url=endpoint.split("//")[0]+"//"+bucketName+"."+endpoint.split("//")[1]+"/"+newFilename; // 关闭ossClient ossClient.shutdown(); return url; } }
#自定义的阿里云OSS配置信息 aliyun.oss.endpoint=https://oss-cn-beijing.aliyuncs.com aliyun.oss.accessKeyId=LTAI5tCcVzUUxvyTq16meDt8 aliyun.oss.accessKeySecret=ndIAxRdq7MM9FUSlsuKbx8PWd8bZxt aliyun.oss.bucketName=overblowg1
-
实现UploadController类
@CrossOrigin
@RestController
public class UploadController {
@Autowired
private AliOSSUtils aliOSSUtils;
@PostMapping("/upload")
public Result upload(MultipartFile image) throws IOException {
log.info("文件上传,文件名:{}", image.getOriginalFilename());
// 调用阿里云OSS工具类进行文件上传
String url = aliOSSUtils.upload(image);
log.info("文件上传成功,文件访问url:{}", url);
return Result.success(url);
}
}
配置文件
- 参数配置化
- @Value注解用于外部配置的属性注入,具体用法为
@Value("${配置文件中的key}")
- key=value
- @Value注解用于外部配置的属性注入,具体用法为
- yml配置文件
- 常见配置文件:XML、properties、yaml/yml
- 基本语法:
- 大小写敏感
- 数值前面必须有空格,作为分隔符
- 不允许使用tab键缩进
- 缩进空格数目不重要,相同层级元素左侧对齐即可
- #表示注释,注释范围为一行
- 数据格式
- 对象/Map集合:
key: value
- 数组/List/Set集合:
key: - val1 - val2 - val3
- 对象/Map集合:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/tlias username: itcast password: root servlet: multipart: #配置单个文件上传大小限制 max-file-size: 10MB #配置单个请求最大大小的限制(一次请求中可以上传多个文件) max-request-size: 100MB mybatis: configuration: #配置mybatis的日志,指定输出到控制台 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启mybatis驼峰命名自动映射开关 map-underscore-to-camel-case: true #自定义的阿里云OSS配置信息 aliyun: oss: endpoint: https://oss-cn-beijing.aliyuncs.com accessKeyId: LTAI5tCcVzUUxvyTq16meDt8 accessKeySecret: ndIAxRdq7MM9FUSlsuKbx8PWd8bZxt bucketName: overblowg1
-
@ConfigurationProperities:批量将外部属性配置注入到bean对象属性中
@Value注解只能一个一个进行外部属性注入
- AliOSSProperties封装类
@Data @Component @ConfigurationProperties(prefix = "aliyun.oss") public class AliOSSProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }
- 修改后的AliOSSUtils Controller类
@ConfigurationProperties(prefix = "aliyun.oss") public class AliOSSUtils { @Autowired private AliOSSProperties aliOSSProperties; // 实现上传图片到OSS public String upload(MultipartFile file) throws IOException { String endpoint = aliOSSProperties.getEndpoint(); String accessKeyId = aliOSSProperties.getAccessKeyId(); String accessKeySecret = aliOSSProperties.getAccessKeySecret(); String bucketName = aliOSSProperties.getBucketName(); ... } }
登录认证
登录功能
登录校验
-
会话技术
-
会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束,在一次会话中可以包含多次请求和响应
-
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自同一个浏览器,以便在同一次会话的多次请求间共享数据
-
会话跟踪方案:
-
客户端会话跟踪技术:Cookie
- 优点:HTTP协议支持的技术,浏览器自动保存cookie
- 缺点:移动端App无法使用cookie,不安全,cookie不能跨域
-
服务端会话跟踪技术:Session
- 优点:存储在服务端,安全
- 缺点:服务器集群环境下无法直接使用Session,Cookie的缺点
-
令牌技术(主流方案
- 优点:支持PC端、移动端,解决集群环境下的认证问题,减轻服务器端存储压力
- 缺点:需要自己实现
-
@Slf4j @RestController public class SessionController { // 设置cookie @GetMapping("/c1") public Result cookie1(HttpServletResponse response) { response.addCookie(new Cookie("login_username", "itheima")); return Result.success(); } // 获取cookie @GetMapping("/c2") public Result cookie2(HttpServletRequest request) { // 获取所有的cookie Cookie[] cookies = request.getCookies(); for (Cookie cookie : cookies) { if (cookie.getName().equals("login_name")) { // 输出name为login_username的cookie System.out.println("login_username:" + cookie.getValue()); } } return Result.success(); } // 向HttpSession中存储值 @GetMapping("/s1") public Result session(HttpSession session) { log.info("HttpSession-s1:{}", session.hashCode()); // 向session中存储数据 session.setAttribute("loginUser", "tom"); return Result.success(); } // 向HttpSession中获取值 @GetMapping("/s2") public Result session2(HttpServletRequest request) { HttpSession session = request.getSession(); log.info("HttpSession-s2:{}", session.hashCode()); // 从session中获取数据 Object loginUser = session.getAttribute("loginUser"); log.info("loginUser:{}", loginUser); return Result.success(loginUser); } }
-
JWT令牌技术
- json web token(https://jwt.io/)
- 定义了一种简洁的、自包含的格式,用于通信双方以json数据格式安全地传输信息,由于数字签名的存在,信息安全可靠
- 组成:使用base64编码
- header,记录令牌类型、签名算法
- payload,携带自定义信息,默认信息
- signature,签名,防止token被篡改,将header、payload、指定密钥,通过指定签名算法计算
- 场景:登录认证
- 登录成功后,生成令牌
- 后续每个请求,都要携带JWT令牌,系统每次请求处理前,先校验令牌,通过后,再处理
- 分发令牌和校验令牌
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Jwt快速入门
class DemoApplicationTests {
// 生成JWT
@Test
public void testGenJwt() {
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("name", "tom");
String Jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "itheima")// 签名算法
.setClaims(claims)// 自定义内容(载荷
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))// 有效期
.compact();
System.out.println(Jwt);
}
// 解析JWT
@Test
public void testParseJwt() {
Claims claims = Jwts.parser()
.setSigningKey("itheima")//指定签名密钥
.parseClaimsJws(
"eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY5MzA1NzIxNn0.Vezopyb8uY9nhT_T3CMnSONQkZ4_CkBW8GUQFmqzwq4")
.getBody();//解析令牌并返回结果
System.out.println(claims);
}
}
- 令牌生成:登录成功后,生成JWT令牌,并返回给前端
- 令牌校验:请求到达服务端后,对令牌进行统一拦截、校验
登录功能实现
public class JwtUtils {
private static String signKey = "itheima";
private static Long expire = 43200000L;
// 生成JWT令牌
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)
.parseClaimsJwt(jwt)// 使用jws带有签名的jwt
.getBody();
return claims;
}
}
@Slf4j
@RestController
@CrossOrigin
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 e != null ? Result.success() : Result.error("用户名或密码错误");
}
}
过滤器Filter
- 概念:filter过滤器,javaweb的三大组件(servlet、filter、listener)
- 过滤器可以拦截对资源的请求,从而实现一些特殊的功能
- 完成通用的操作,登录校验、统一编码处理、敏感字符处理
- 快速入门
- 定义Filter:定义类,实现Filter接口,重写所有的方法
- 配置Filter:Filter加上@WebFilter注解,配置拦截资源的路径,引导类上加@ServletComponentScan开启Servlet组件支持
- filter执行流程
@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)
throws IOException, ServletException {
System.out.println("拦截到了请求");
System.out.println("放行前逻辑");
// 放行
chain.doFilter(request, response);
System.out.println("放行后逻辑");
}
@Override // 销毁方法,只调用一次
public void destroy() {
System.out.println("destroy销毁方法执行了");
}
}
@WebFilter
public class AbcFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("Abc拦截到了请求");
System.out.println("Abc放行前逻辑");
// 放行
chain.doFilter(request, response);
System.out.println("Abc放行后逻辑");
}
}
- filter拦截路径
- 具体路径,
/login
,只有访问/login
路径才会别拦截 - 目录拦截,
/emps/*
,访问/emps
下的所有资源,被拦截 - 拦截所有,
/*
,所有资源都会被拦截
- 具体路径,
- filter过滤器链
- 一个web应用中,可以配置多个过滤器,多个过滤器形成一个过滤器链
- 注解配置的Filter,优先级按照过滤器类名(字符串)自然排序
-
登录校验filter——流程
@Slf4j
@WebFilter(urlPatterns = "/*")
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();
log.info("请求的url:{}",url);
// 2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if(url.contains("login")){
log.info("登录操作,放行...");
chain.doFilter(request, response);
return;
}
// 3.获取请求头中的令牌token
String jwt=req.getHeader("token");
// 4.判断令牌是否存在,如果不存在,返回错误结果——未登录
if(!StringUtils.hasText(jwt)){
log.info("请求头token为空,返回未登录的信息");
Result error=Result.error("NOT_LOGIN");
//手动转换对象——json-------->alibaba.fastJSON
String notLogin=JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
// 5.解析token,如果解析失败,返回错误结果——未登录
try {
JwtUtils.parseJWT(jwt);// 带有签名的登录认证jws
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error=Result.error("NOT_LOGIN");
//手动转换对象——json------->alibaba.fastJSON
String notLogin=JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
// 6.放行
log.info("令牌合法,放行");
chain.doFilter(request, response);
}
}
拦截器Interceptor
-
概念:动态拦截方法调用的机制,类似于过滤器,spring框架中提供,用来动态拦截控制器方法的执行
-
作用:拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码
-
interceptor快速入门
- 定义拦截器,实现HandlerInterceptor接口,并重写所有方法
@Component public class LoginCheckInterceptor implements HandlerInterceptor { @Override // 目标资源运行前运行,返回true:放行;返回false:不放行 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle.."); return false; } @Override // 目标资源方法运行后运行 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { System.out.println("postHandle.."); } @Override // 视图渲染完毕后运行,最后运行 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { System.out.println("afterHandle.."); } }
- 注册拦截器
@Configuration // 配置类 public class WebConfig implements WebMvcConfigurer { @Autowired private LoginCheckInterceptor loginCheckInterceptor; public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login"); } }
-
拦截器——拦截路径
/*
一级路径,能匹配/depts,/emps
,不能匹配/depts/1
/**
任意级路径,能匹配/depts,depts/1,depts/1/2
/depts/*
匹配/depts
下的一级路径,能匹配/depts/1
/depts/**
匹配/depts
下的任意级路径,能匹配/depts,/depts/1,depts/1/2
addPathPatterns
需要拦截那些资源excludePathPatterns
不需要拦截那些资源 -
拦截器——执行流程
- filter和interceptor
- 接口规范不同:过滤器需要实现Filter接口,拦截器需要实现HandlerInterceptor接口
- 拦截范围不同:过滤器Filter会拦截所有的资源,Interceptor只会拦截Spring环境中的资源
- filter和interceptor
-
登录校验Inteceptor——流程
@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) throws IOException, ServletException { System.out.println("Demo拦截到了请求"); System.out.println("Demo放行前逻辑"); // 放行 chain.doFilter(request, response); System.out.println("Demo放行后逻辑"); } @Override // 销毁方法,只调用一次 public void destroy() { System.out.println("destroy销毁方法执行了"); } }
@Component @Slf4j public class LoginCheckInterceptor implements HandlerInterceptor { @Override // 目标资源运行前运行,返回true:放行;返回false:不放行 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.请求url String url = request.getRequestURL().toString(); log.info("请求的url:{}", url); // 2.判断请求url中是否包含login,如果包含,说明是登录操作,放行 if (url.contains("login")) { log.info("登录操作,放行..."); return true; } // 3.获取请求头中的令牌token String jwt = request.getHeader("token"); // 4.判断令牌是否存在,如果不存在,返回错误结果——未登录 if (!StringUtils.hasText(jwt)) { log.info("请求头token为空,返回未登录的信息"); Result error = Result.error("NOT_LOGIN"); // 手动转换对象——json-------->alibaba.fastJSON String notLogin = JSONObject.toJSONString(error); response.getWriter().write(notLogin); return false; } // 5.解析token,如果解析失败,返回错误结果——未登录 try { JwtUtils.parseJWT(jwt); } catch (Exception e) { e.printStackTrace(); log.info("解析令牌失败,返回未登录错误信息"); Result error = Result.error("NOT_LOGIN"); // 手动转换对象——json------->alibaba.fastJSON String notLogin = JSONObject.toJSONString(error); response.getWriter().write(notLogin); return false; } // 6.放行 log.info("令牌合法,放行"); return true; } ... }
异常处理
-
在Controller中进行
try...catch
处理,代码臃肿,不推荐 -
全局异常处理器
// 全局异常处理器 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) // 捕获所有异常 public Result ex(Exception ex) { ex.printStackTrace(); return Result.error("对不起,操作失败,请联系管理员"); } }
事务管理
-
事务:一组操作的集合,不可分割的工作单位,操作要么同时成功,要么同时失败
-
操作
- 开启事务(一组操作开始前,开启事务):start transaction / begin
- 提交事务(这组操作全部成功后,提交事务):commit
- 回滚事务(中间任何一个操作出现异常,回滚事务):rollback
-
spring事务管理——注解@Transactional
- 位置:业务层service的方法、类、接口上
- 作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务
@Transactional(rollbackFor = Exception.class) @Override public void delete(Integer id) { try { deptMapper.deleteById(id);// 根据部门ID删除部门数据 // 根据部门ID删除员工数据 empMapper.deleteByDeptId(id); } finally { ; } }
-
spring事务进阶——Transactional属性
-
rollbackFor
- 默认情况下,只有出现RuntimeException才回滚异常,rollbackFor属性用于控制出现何种异常类型,回滚事务
-
propagation
- 事务传播行为:当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制
属性值 含义 REQUIRED 默认,需要事务则加入,无则创建新事物 REQUIRES_NEW 总是创建新事务 SUPPORTS 支持事务,有则加入,无则无事务状态 NOT_SUPPORTED 不支持事务,如果当前存在已有事务,则挂起当前事务 MANDATORY 必须有事务,否则抛异常 NEVER 必须没事务,否则抛异常 - 实现功能:解散部门时,无论成功还是失败,都要记录操作日志
- 大部分情况下使用REQUIRED传播行为即可,对于不希望事务之间相互影响,则使用REQUIRES_NEW传播行为
-
AOP
- AOP基础
- AOP:aspect oriented programming面向切面编程、面向方面编程,面向特定方法编程
- 场景:定位执行耗时较长的业务方法,统计每一个业务方法的执行耗时
- 实现:
- 动态代理是面向切面编程最主流的实现
- SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程
-
AOP使用场景
- 记录操作日志
- 权限控制
- 事务管理:开启事务、提交回滚事务
-
springboot AOP快速入门
- 导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
- 对特定方法根据业务需要进行编程
@Slf4j @Component @Aspect // 切面类 public class TimeAspect { // 切面 @Around("execution(* com.example.service.*.*(..))") // 切入点表达式:目标对象+切入点 public Object recordTime(ProceedingJoinPoint joinPoint) // 连接点 throws Throwable { // 通知 // 1.记录开始时间 long begin = System.currentTimeMillis(); // 2.调用原始方法运行 Object result = joinPoint.proceed(); // 3.记录结束时间,计算方法执行耗时 long end = System.currentTimeMillis(); log.info(joinPoint.getSignature() + "方法执行耗时:{}ms", end - begin); return result; } }
- AOP核心概念
- 连接点JoinPoint:可以被AOP控制的方法(暗含方法执行时的相关信息)
- 通知Advice:指那些重复的逻辑,也就是共性功能(最终体现为一个方法)
- 切入点PointCut:匹配连接点的条件,通知仅会在切入点方法执行时被应用
- 切面Aspect:描述通知与切入点的对应关系(通知+切入点)
- 目标对象Target:通知所应用的对象
- AOP通知类型
- @Around:环绕通知,目标方法前、后都被执行
- 需要自己调用ProceedingJointPoint.proceed()让原始方法执行
- 必须指定返回值为Object,接收原始方法的返回值
- @Before:前置通知,目标方法前被执行
- @After:后置通知,目标方法后被执行,无论是否有异常
- @AfterReturning:返回后通知,目标方法后被执行,有异常不会执行
- @AfterThrowing:异常后通知,目标方法发生异常后执行
- @PointCut:将公共切入点表达式抽取出来,用到时引用切点表达式即可
@Slf4j @Aspect @Component public class MyAspect1 { // 抽取切入点表达式 @Pointcut("execution(* com.example.service.*.*(..))"); private void pct(){}; @Before("pct()") public void before() { log.info("before..."); } @Around("pct()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ log.info("around before..."); //调用目标对象的原始方法执行 Object result=proceedingJoinPoint.proceed(); log.info("around after..."); return result; } @After("pct()") public void after() { log.info("after..."); } @AfterReturning("pct()") public void afterReturning() { log.info("afterReturning..."); } @AfterThrowing("pct()") public void afterThrowing() { log.info("afterThrowing..."); } }
- @Around:环绕通知,目标方法前、后都被执行
-
AOP通知顺序
- 多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行
- 不同切面类中,默认按照切面类的类名字母排序
@Order(index)
注解控制切面类执行顺序
-
AOP切入点表达式
-
决定项目中哪些方法需要加入通知
-
execution(…):根据方法的签名来匹配
-
execution(访问修饰符? 返回值 包名.类名.方法名(方法参数) throws exception?)
结合通配符写成正则表达式的形式 - 访问修饰符、方法声明抛出的异常可以省略
- 通配符:
*
:单个任意符号,统配任何返回值、包名、类名、方法名、参数..
:连续多个任意符号- 使用
&&、||、!
组合复杂的切入点表达式
- 建议:
- 方法名命名规范,方便切入点表达式快速匹配
- 基于接口描述,增强拓展
- 尽量缩小切入点的匹配范围
-
-
@annotation(…):根据注解匹配
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyLog { }
public class DeptServiceImpl implements DeptService { ... @MyLog @Override public List<Dept> list() { return deptMapper.list(); } ... }
public class MyAspect1 { // @Pointcut("execution(* com.example.service.*.*(..))") @Pointcut("@annotation(com.example.aop.MyLog)") private void pct(){}; @Around("pct()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ log.info("around before..."); //调用目标对象的原始方法执行 Object result=proceedingJoinPoint.proceed(); log.info("around after..."); return result; } }
-
-
AOP连接点
- 在spring中用JointPoint抽象了连接点,用它可以获得方法执行时的相关信息,比如目标类名、方法名、方法参数等
- 对于@Around通知,获取连接点信息只能用ProceedingJointPoint
- 对于其他四种通知,获取连接点信息只能用JointPoint,它时ProceedingJointPoint的父类型
public class MyAspect1 { ... @Before("pct()") public void before(JoinPoint joinPoint) { log.info("before..."); } @Around("pct()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ log.info("around before..."); //1.获取目标对象的类名 String className=proceedingJoinPoint.getTarget().getClass().getName(); log.info("目标对象的类名:{}",className); //2.获取目标对象的方法名 String methodName=proceedingJoinPoint.getSignature().getName(); log.info("目标对象的方法名:{}",methodName); //3.获取目标方法运行时传入的参数 Object[] args=proceedingJoinPoint.getArgs(); log.info("目标方法运行时传入的参数:{}", args); //4.调用目标对象的原始方法执行 Object result=proceedingJoinPoint.proceed(); log.info("目标方法运行时的返回值:{}", result); log.info("around after..."); return result; } ... }
- 在spring中用JointPoint抽象了连接点,用它可以获得方法执行时的相关信息,比如目标类名、方法名、方法参数等
-
AOP记录操作日志
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 '操作日志表'