一、SpringBootWeb
1、需求和环境搭建
文件命名规范:
Controller:控制层,存放控制器Controller
mapper:持久层,数据访问层,存放mybatis的Mapper接口
Service:业务层,处理逻辑性问题的业务代码
pojo/domain:业务层、存放业务代码
步骤:
1. 创建一个新的数据库(tlias)准备数据库表(dept、emp)
创建需求的员工表、关系表等。
2. 创建springboot工程,引入对应的起步依赖(web、mybatis、mysql驱动、lombok)
2、生成pom.xml项目配置文件
<?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.5</version>
<relativePath/>
</parent>
<groupId>com.itheima</groupId>
<artifactId>tlias-web-management</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>tlias-web-management</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3、配置myBatis核心文件:
3. 配置文件application.properties中引入mybatis的配置信息,准备对应的实体类
#数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/tlias
spring.datasource.username=root
spring.datasource.password=1234
#开启mybatis的日志输出
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#开启数据库表字段 到 实体类属性的驼峰映射
mybatis.configuration.map-underscore-to-camel-case=true
4、准备Mapper、Service(接口、实现类)、Controller基础结构
实体类
/*部门类的命名需要与数据库表中的数据一一对应*/
@Data//Lomback插件在实体类生成字节码之前生成对应的get/set的方法toString等方法
@NoArgsConstructor
@AllArgsConstructor
public class Dept {
private Integer id;
private String name;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
Mapper层
数据访问层
DeptMapper
package com.itheima.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DeptMapper {
}
EmpMapper
package com.itheima.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EmpMapper {
}
Service层
业务层
DeptService//业务层的实现接口
package com.itheima.service;
//部门业务规则
public interface DeptService {
}
DeptServiceImpl//业务层接口的实体类
package com.itheima.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
//部门业务实现类
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
}
Controller层
控制层
package com.itheima.controller;
import org.springframework.web.bind.annotation.RestController;
//部门管理控制器
//Controller
@RestController
public class DeptController {
}
5、RESTFUL风格
- REST(Representational State Transfer),表述性状态转换,它是一种软件架构风格。
请求接口的三要素:请求路径、请求参数、请求响应
**传统URL风格如下:**
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风格URL如下:
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的用户
通过URL定位要操作的资源,通过HTTP动词(请求方式)来描述具体的操作。
在REST风格的URL中,通过四种请求方式,来操作数据的增删改查。
- GET : 查询
- POST :新增
- PUT :修改
- DELETE :删除
- REST是风格,是约定方式,约定不是规定,可以打破
- 描述模块的功能通常使用复数,也就是加s的格式来描述,表示此类资源,而非单个资源。如:users、emps、books…
6、Result响应规范
开发规范-统一响应结果**
前后端工程在进行交互时,使用统一响应结果 Result。
引入Result实体类对数据进行统一包装。
形如:
package com.itheima.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);
}
}
7、开发流程
1. 查看页面原型明确需求
- 根据页面原型和需求,进行表结构设计、编写接口文档
2. 阅读接口文档
3. 思路分析
4. 功能接口开发
- 就是开发后台的业务功能,一个业务功能,我们称为一个接口
5. 功能接口测试
- 功能开发完毕后,先通过Postman进行功能接口测试,测试通过后,再和前端进行联调测试
6. 前后端联调测试
- 和前端开发人员开发好的前端工程一起测试
二、SpringBootWeb细节
1、Controller层
在controller中接收请求路径中的路径参数
@PathVariable
如何限定请求方式是POST?
@PostMapping
在controller中接收json格式的请求参数
@RequestBody //把前端传递的json数据填充到实体类中
在Spring当中为了简化请求路径的定义,可以把公共的请求路径,直接抽取到类上,在类上加一个注解@RequestMapping,并指定请求路径。
注意事项:一个完整的请求路径,应该是类上@RequestMapping的value属性 + 方法上的 @RequestMapping的value属性
@RequestParam(defaultValue="默认值") //用于从请求参数中获取值并赋给方法参.
常用属性包括:
value:表示要绑定的请求参数名字。默认值为方法参数名,与请求参数名字一致。
required:表示该参数是否是必需的。默认为true,如果请求中没有传递该参数,则会抛出异常。如果设置为false,即可使该参数变为可选。
defaultValue:表示当请求中没有传递该参数时,使用的默认值。数。/****************************************************************************************/
@Slf4j //自动添加Logger对象,对象名为log
@RestController //@RestController注解是Spring4之后引入的,它的功能是通过@ResponseBody注解自动应用于所有的请求处理方法。
public class DeptController {
@Autowired //自动装配依赖关系。如果存在多个符合类型的对象,Spring会抛出异常。为了避免这种情况,可以结合使用@Autowired注解和@Qualifier注解,通过指定bean的名称来明确指定要注入哪个bean。
private DeptService deptService;
//@RequestMapping(value = "/depts" , method = RequestMethod.GET)
//@RequestMapping是一个通用的注解,它可以用于处理任何类型的HTTP请求(GET、POST、PUT、DELETE等)。同时,它也可以用于类级别的注解,用来定义类中所有处理请求的方法的基本请求路径。
@GetMapping("/depts")//@GetMapping是@RequestMapping的特定变体,它只处理HTTP GET请求。它可以用于方法级别的注解,用来处理特定路径的GET请求。
public Result list(){
log.info("查询所有部门数据");
List<Dept> deptList = deptService.list();
return Result.success(deptList);
}
}
2、Service业务层
//定义接口的目的为了实现解耦,在业务逻辑发生变化或者需求变更时,只需要修改实现类而不需要修改调用方的代码。
public interface DeptService {
/**
* 查询所有的部门数据
* @return 存储Dept对象的集合
*/
List<Dept> list();
}
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
return deptList;
}
}
3、Mapper层
@Mapper//@Mapper注解是MyBatis框架中的注解。框架会根据接口的定义自动生成Mapper接口的实现类,并执行接口中定义的SQL语句。
public interface DeptMapper {
//查询所有部门数据
@Select("select id, name, create_time, update_time from dept")
List<Dept> list();
}
4、PageHelper分页插件
PageHelper是Mybatis的一款功能强大、方便易用的分页插件,支持任何形式的单标、多表的分页查询。
官网使用地址
在pom.xml引入依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
@Mapper
public interface EmpMapper {
//获取当前页的结果列表
@Select("select * from emp")
public List<Emp> page();
}
//******************************************
@Override
public PageBean page(Integer page, Integer pageSize) {
// 设置分页参数
PageHelper.startPage(page, pageSize);
// 执行分页查询
List<Emp> empList = empMapper.page();
// 获取分页结果
Page<Emp> p = (Page<Emp>) empList;
//封装PageBean
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}
注意:只有紧跟着PageHelper.startPage()的sql语句才被pagehelper起作用
5、文件上传
文件上传,是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。
想要完成文件上传这个功能需要涉及到两个部分:
1. 前端程序
2. 服务端程序
前端实现代码:
<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>
表单必须有file域,用于选择要上传的文件
<input type="file" name="image"/>
表单提交方式必须为POST
> 通常上传的文件会比较大,所以需要使用 POST 提交方式
>表单的编码类型enctype必须要设置为:multipart/form-data,普通默认的编码格式是不适合传输大型的二进制数据的,所以在文件上传时,表单的编码格式必须设置为multipart/form-data。
后端程序实现:
- 首先在服务端定义这么一个controller,用来进行文件上传,然后在controller当中定义一个方法来处理`/upload` 请求
- 在定义的方法中接收提交过来的数据 (方法中的形参名和请求参数的名字保持一致)
如果表单项的名字和方法中形参名不一致,该怎么办?
答:使用@RequestParam注解进行参数绑定。
- 用户名:String name
- 年龄: Integer age
- 文件: MultipartFile image
public Result upload(String username,
Integer age,
@RequestParam("image") MultipartFile file)
> Spring中提供了一个API:MultipartFile,使用这个API就可以来接收到上传的文件
6、本地存储
文件上传功能前端和后端的基础代码实现,文件上传时在服务端会产生一个临时文件,请求响应完成之后,这个临时文件被自动删除,并没有进行保存。
1. 在服务器本地磁盘上创建images目录,用来存储上传的文件(例:E盘创建images目录)
2. 使用MultipartFile类提供的API方法,把临时文件转存到本地磁盘目录下
> MultipartFile 常见方法:
> - String getOriginalFilename(); //获取原始文件名
> - void transferTo(File dest); //将接收的文件转存到磁盘文件中
> - long getSize(); //获取文件的大小,单位:字节
> - byte[] getBytes(); //获取文件内容的字节数组
> - InputStream getInputStream(); //获取接收到的文件内容的输入流
文件上传是没有问题的。但是由于我们是使用原始文件名作为所上传文件的存储名字,当我们再次上传一个名为1.jpg文件时,发现会把之前已经上传成功的文件覆盖掉。
解决方案:保证每次上传文件时文件名都唯一的(使用UUID获取随机文件名)
@Slf4j
@RestController
public class UploadController {
@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image) throws IOException {
log.info("文件上传:{},{},{}",username,age,image);
//获取原始文件名
String originalFilename = image.getOriginalFilename();
//构建新的文件名
String extname = originalFilename.substring(originalFilename.lastIndexOf("."));//文件扩展名
String newFileName = UUID.randomUUID().toString()+extname;//随机名+文件扩展名
//将文件存储在服务器的磁盘目录
image.transferTo(new File("E:/images/"+newFileName));
return Result.success();
}
}
需要上传大文件,可以在application.properties进行如下配置:
//配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB
//配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB
直接存储在服务器的磁盘目录中,存在以下缺点:
- 不安全:磁盘如果损坏,所有的文件就会丢失
- 容量有限:如果存储大量的图片,磁盘空间有限(磁盘不可能无限制扩容)
- 无法直接访问
通常有两种解决方案:
- 自己搭建存储服务器,如:fastDFS 、MinIO
- 使用现成的云服务,如:阿里云,腾讯云,华为云
7、OSS存储
云服务指的就是通过互联网对外提供的各种各样的服务。
SDK:Software Development Kit 的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。
简单说,sdk中包含了我们使用第三方云服务时所需要的依赖,以及一些示例代码。我们可以参照sdk所提供的示例代码就可以完成入门程序。
Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。
下面我们根据之前介绍的使用步骤,完成准备工作:
1.通过控制台找到对象存储OSS服务
2.开通OSS服务之后,就可以进入到阿里云对象存储的控制台
3.点击 "Bucket列表",创建一个Bucket
4.参照官方提供的SDK,改造一下,即可实现文件上传功能
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.FileInputStream;
import java.io.InputStream;
public class AliOssTest {
public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "oss-cn-shanghai.aliyuncs.com";
// AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "LTAI5t9MZK8iq5T2Av5GLDxX";
String accessKeySecret = "C0IrHzKZGKqU8S7YQcevcotD3Zd5Tc";
// 填写Bucket名称,例如examplebucket。
String bucketName = "web-framework01";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "1.jpg";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "C:\\Users\\Administrator\\Pictures\\1.jpg";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
// 设置该属性可以返回response。如果不设置,则返回的response为空。
putObjectRequest.setProcess("true");
// 创建PutObject请求。
PutObjectResult result = ossClient.putObject(putObjectRequest);
// 如果上传成功,则返回200。
System.out.println(result.getResponse().getStatusCode());
} 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();
}
}
}
}
在以上代码中,需要替换的内容为:
- accessKeyId: AccessKey
- accessKeySecret: AccessKey对应的秘钥
- bucketName:Bucket名称
- objectName:对象名称,在Bucket中存储的对象的名称
- filePath:文件路径
三、参数配置文件
1、参数配置化
AliOSSUtils工具类,将文件上传到OSS对象存储服务当中。而在调用工具类进行文件上传时,需要一些参数:
- endpoint //OSS域名
- accessKeyID //用户身份ID
- accessKeySecret //用户密钥
- bucketName //存储空间的名字
AliOSSUtils工具类,将文件上传到OSS对象存储服务当中。而在调用工具类进行文件上传时,需要一些参数:
- endpoint //OSS域名
- accessKeyID //用户身份ID
- accessKeySecret //用户密钥
- bucketName //存储空间的名字
将参数配置在配置pom.xml文件中。如下:
#自定义OSS配置信息
aliyun.oss.endpoint=https://oss-cn-hangzhou.aliyuncs.com
aliyun.oss.accessKeyId=LTAI4GCH1vX6DKqJWxd6nEuW
aliyun.oss.accessKeySecret=yBshYweHOpqDuhCArrVHwIiBKpyqSL
aliyun.oss.bucketName=web-tlias
在将OSS配置参数交给properties配置文件来管理之后,我们的AliOSSUtils工具类就变为以下形式:
@Component
public class AliOSSUtils {
/*以下4个参数没有指定值(默认值:null)*/
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
//省略其他代码...
}
application.properties是springboot项目默认的配置文件,所以springboot程序在启动时会默认读取application.properties配置文件,而我们可以使用一个现成的注解:@Value,获取配置文件中的数据。
@Value 注解通常用于外部配置的属性注入,具体用法为: @Value("${配置文件中的key}")
@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;
//省略其他代码...
}
2、yml配置文件
# application.properties
server.port=8080
server.address=127.0.0.1
# application.yml
server:
port: 8080
address: 127.0.0.1
# application.yaml
server:
port: 8080
address: 127.0.0.1
yml 格式的配置文件,后缀名有两种:
- yml (推荐)
- yaml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/tlias
username: root
password: 1234
yml格式的数据有以下特点:
- 容易阅读
- 容易与脚本语言交互
- 以数据为核心,重数据轻格式
yml配置文件的基本语法:
- 大小写敏感
- 数值前边必须有空格,作为分隔符
- 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格)
- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
- #表示注释,从这个字符一直到行尾,都会被解析器忽略
yml文件中常见的数据格式
1. 定义对象或Map集合
2. 定义数组、list或set集合
对象/Map集合
user:
name: zhangsan
age: 18
password: 123456
数组/List/Set集合
hobby:
- java
- game
- sport
3、@ConfigurationProperties
Spring中给我们提供了一种简化方式@ConfigurationProperties 可以简化这些配置参数的注入
1. 需要创建一个实现类,且实体类中的属性名和配置文件当中key的名字必须要一致
> 比如:配置文件当中叫endpoints,实体类当中的属性也得叫endpoints,另外实体类当中的属性还需要提供 getter / setter方法
2. 需要将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象
3. 在实体类上添加`@ConfigurationProperties`注解,并通过perfect属性来指定配置参数项的前缀
需要引入一个起始依赖,这项依赖它的作用就是会自动的识别被@Configuration Properties注解标识的bean对象。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
实体类:AliOSSProperties
/*OSS相关配置*/
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
//区域
private String endpoint;
//身份ID
private String accessKeyId ;
//身份密钥
private String accessKeySecret ;
//存储空间
private String bucketName;
}
@ConfigurationProperties注解我们已经介绍完了,接下来我们就来区分一下@ConfigurationProperties注解以及我们前面所介绍的另外一个@Value注解:
相同点:都是用来注入外部配置的属性的。
不同点:
- @Value注解只能一个一个的进行外部属性的注入。
- @ConfigurationProperties可以批量的将外部的属性配置注入到bean对象的属性中。
四、会话技术统一拦截技术
HTTP协议是无状态协议。所谓无状态,指的是每一次请求都是独立的,下一次请求并不会携带上一次请求的数据。
会话指的就是浏览器与服务器之间的一次连接,我们就称为一次会话。这个会话就建立了,直到有任何一方断开连接,此时会话就结束了。在一次会话当中,是可以包含多次请求和响应的。这个会话就建立了,直到有任何一方断开连接,此时会话就结束了。在一次会话当中,是可以包含多次请求和响应的。
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
会话跟踪技术有三种:
1. Cookie(客户端会话跟踪技术)
- 数据存储在客户端浏览器当中
2. Session(服务端会话跟踪技术)
- 数据存储在储在服务端
3. 令牌技术
统一拦截技术现实方案有两种:
1. Servlet规范中的Filter过滤器
2. Spring提供的interceptor拦截器
1、会话跟踪方案 Cookie
cookie 是客户端会话跟踪技术,它是存储在客户端浏览器中的。在浏览器第一次发起请求来请求服务器的时候,我们在服务器端来设置一个cookie。在 cookie 当中我们就可以来存储用户相关的一些数据信息。
服务器端在给客户端在响应数据的时候,会**自动**的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会**自动**的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie **自动**地携带到服务端。
在服务端我们就可以获取到 cookie 的值。我们可以去判断一下这个 cookie 的值是否存在,如果不存在这个cookie,就说明客户端之前是没有访问登录接口的;如果存在 cookie 的值,就说明客户端之前已经登录完成了。这样我们就可以基于 cookie 在同一次会话的不同请求之间来共享数据。
//*************************************************************
3 个自动:
- 服务器会 自动 的将 cookie 响应给浏览器。
- 浏览器接收到响应回来的数据之后,会 自动 的将 cookie 存储在浏览器本地。
- 在后续的请求当中,浏览器会 自动 的将 cookie 携带到服务器端。
在 HTTP 协议官方给我们提供了一个响应头和请求头:
- 响应头 Set-Cookie :设置Cookie数据的
- 请求头 Cookie:携带Cookie数据的
//*****************************************************************
//代码测试
@Slf4j
@RestController
public class CookieController {
//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
return Result.success();
}
//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
}
}
return Result.success();
}
}
**优缺点**
- 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)
- 缺点:
- 移动端APP(Android、IOS)中无法使用Cookie
- 不安全,用户可以自己禁用Cookie
- Cookie不能跨域
区分跨域的维度:
- 协议
- IP/协议
- 端口
只要上述的三个维度有任何一个维度不同,那就是跨域操作
2、会话跟踪方案 Session
Session 的底层其实就是基于我们刚才所介绍的 Cookie 来实现的。
基于 Session 来进行会话跟踪,浏览器在第一次请求服务器的时候,我们就可以直接在服务器当中来获取到会话对象Session。如果是第一次请求Session ,会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session 。而每一个会话对象Session ,它都有一个ID(示意图中Session后面括号中的1,就表示ID),我们称之为 Session 的ID。
服务器端在给浏览器响应数据的时候,它会将 Session 的 ID 通过 Cookie 响应给浏览器。其实在响应头当中增加了一个 Set-Cookie 响应头。这个 Set-Cookie 响应头对应的值是不是cookie? cookie 的名字是固定的 JSESSIONID 代表的服务器端会话对象 Session 的 ID。浏览器会自动识别这个响应头,然后自动将Cookie存储在浏览器本地。
在后续的每一次请求当中,都会将 Cookie 的数据获取出来,并且携带到服务端。接下来服务器拿到JSESSIONID这个 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就会从众多的 Session 当中来找到当前请求对应的会话对象Session。
//*********代码测试*******************:
@Slf4j
@RestController
public class SessionController {
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());
session.setAttribute("loginUser", "tom"); //往session中存储数据
return Result.success();
}
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession-s2: {}", session.hashCode());
Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
}
**优缺点**
- 优点:Session是存储在服务端的,安全
- 缺点:
- 服务器集群环境下无法直接使用Session
- 移动端APP(Android、IOS)中无法使用Cookie
- 用户可以自己禁用Cookie
- Cookie不能跨域
> PS:Session 底层是基于Cookie实现的会话跟踪,如果Cookie不可用,则该方案,也就失效了。
服务器集群环境为何无法使用Session?
首先第一点,我们现在所开发的项目,一般都不会只部署在一台服务器上,因为一台服务器会存在一个很大的问题,就是单点故障。所谓单点故障,指的就是一旦这台服务器挂了,整个应用都没法访问了。
3、会话跟踪令牌
令牌就是用户身份的标识,其本质就是一个字符串。令牌的形式有很多,我们使用的是功能强大的 JWT令牌。
原理:
在浏览器发起请求。在请求登录接口的时候,如果登录成功,我就可以生成一个令牌,令牌就是用户的合法身份凭证。接下来我在响应数据的时候,我就可以直接将令牌响应给前端。
在前端程序当中接收到令牌之后,就需要将这个令牌存储起来。这个存储可以存储在 cookie 当中,也可以存储在其他的存储空间(比如:localStorage)当中。
接下来,在后续的每一次请求当中,都需要将令牌携带到服务端。携带到服务端之后,接下来我们就需要来校验令牌的有效性。如果令牌是有效的,就说明用户已经执行了登录操作,如果令牌是无效的,就说明用户之前并未执行登录操作。
如果是在同一次会话的多次请求之间,我们想共享数据,我们就可以将共享的数据存储在令牌当中就可以了。
**优缺点**
- 优点:
- 支持PC端、移动端
- 解决集群环境下的认证问题
- 减轻服务器的存储压力(无需在服务器端存储)
- 缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)
针对于这三种方案,现在企业开发当中使用的最多的就是第三种令牌技术进行会话跟踪。而前面的这两种传统的方案,现在企业项目开发当中已经很少使用了。所以在我们的课程当中,我们也将会采用令牌技术来解决案例项目当中的会话跟踪问题。
4、JWT令牌
JWT令牌最典型的应用场景就是登录认证:
JWT全称:JSON Web Token
官方网址
定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。
自包含:指的是jwt令牌,看似是一个随机的字符串,但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。
jwt将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了。
★★★★ JWT令牌的组成:
- 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}
- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}
- 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
签名的目的就是为了防jwt令牌被篡改,而正是因为jwt令牌最后一个部分数字签名的存在,所以整个jwt 令牌是非常安全可靠的。
JWT通过Base64对数据信息进行编码(是一种基于64个可打印的字符来表示二进制数据的编码方式。)用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号。
Base64是编码方式,而不是加密方式。
5、生成和校验
要想使用JWT令牌,需要先引入JWT的依赖:
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
在引入完JWT来赖后,就可以调用工具包中提供的API来完成JWT令牌的生成和校验
工具类:Jwts
生成JWT代码实现:
@Test
public void genJwt(){
Map<String,Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("username","Tom");
String jwt = Jwts.builder()
.setClaims(claims) //自定义内容(载荷)
.signWith(SignatureAlgorithm.HS256, "itheima".getBytes(StandardCharsets.UTF_8)) //签名算法
.setExpiration(new Date(System.currentTimeMillis() + 24*3600*1000)) //有效期
.compact();
System.out.println(jwt);
}
运行测试结果:
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk
该字符串被英文标点分为三部分:
> 第一部分解析出来,看到JSON格式的原始数据,所使用的签名算法为HS256。
>
> 第二个部分是我们自定义的数据,之前我们自定义的数据就是id,还有一个exp代表的是我们所设置的过期时间。
>
> 由于前两个部分是base64编码,所以是可以直接解码出来。但最后一个部分并不是base64编码,是经过签名算法计算出来的,所以最后一个部分是不会解析的。
修改生成令牌的时指定的过期时间,修改为1分钟
@Test
public void genJwt(){
Map<String,Object> claims = new HashMap<>();
claims.put(“id”,1);
claims.put(“username”,“Tom”);
String jwt = Jwts.builder()
.setClaims(claims) //自定义内容(载荷)
.signWith(SignatureAlgorithm.HS256, "itheima".getBytes(StandardCharsets.UTF_8)) //签名算法
.setExpiration(new Date(System.currentTimeMillis() + 60*1000)) //有效期60秒
.compact();
System.out.println(jwt);
//输出结果:eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjczMDA5NzU0fQ.RcVIR65AkGiax-ID6FjW60eLFH3tPTKdoK7UtE4A1ro
}
@Test
public void parseJwt(){
Claims claims = Jwts.parser()
.setSigningKey("itheima".getBytes(StandardCharsets.UTF_8))//指定签名密钥
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjczMDA5NzU0fQ.RcVIR65AkGiax-ID6FjW60eLFH3tPTKdoK7UtE4A1ro")
.getBody();
System.out.println(claims);
}
★★★ 登录下发令牌
1. 生成令牌
- 在登录成功之后来生成一个JWT令牌,并且把这个令牌直接返回给前端
2. 校验令牌
- 拦截前端请求,从请求中获取到令牌,对令牌进行解析校验
五、过滤器和拦截器
统一拦截到所有的请求校验令牌:
1. Filter过滤器 (Filter过滤器是Servlet API的一部分,用于对HTTP请求或响应进行预处理或后处理操作。)
2. Interceptor拦截器
Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一。
- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
- 使用了过滤器之后,要想访问web服务器上的资源,必须先经过滤器,过滤器处理完毕之后,才可以访问对应的资源。
- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
Filter快速入门程序掌握过滤器的基本使用操作:
- 第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。
- 第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。
★★定义过滤器:
//定义一个类,实现一个标准的Filter过滤器的接口
@WebFilter(urlPattern="/*")//配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
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 拦截到了请求...放行前逻辑");
//放行
chain.doFilter(request,response);
}
@Override //销毁方法, 只调用一次
public void destroy() {
System.out.println("destroy 销毁方法执行了");
}
}
★★★ //三个方法的含义:
> - init方法:过滤器的初始化方法。在web服务器启动的时候会自动的创建Filter过滤器对象,在创建过滤器对象的时候会自动调用init初始化方法,这个方法只会被调用一次。
>
> - doFilter方法:这个方法是在每一次拦截到请求之后都会被调用,所以这个方法是会被调用多次的,每拦截到一次请求就会调用一次doFilter()方法。
>
> - destroy方法: 是销毁的方法。当我们关闭服务器的时候,它会自动的调用销毁方法destroy,而这个销毁方法也只会被调用一次。
★★★ 注意: 在Filter类上面加了@WebFilter注解之后,接下来我们还需要在启动类上面加上一个注解@ServletComponentScan,通过这个@ServletComponentScan注解来开启SpringBoot项目对于Servlet组件的支持。
@ServletComponentScan
@SpringBootApplication
public class TliasWebManagementApplication {
public static void main(String[] args) {
SpringApplication.run(TliasWebManagementApplication.class, args);
}
}
过滤器拦截到了请求之后,如果希望继续访问后面的web资源,就要执行放行操作,放行就是调用 FilterChain对象当中的doFilter()方法,在调用doFilter()这个方法之前所编写的代码属于放行之前的逻辑。
放行后访问完 web 资源之后还会回到过滤器当中,回到过滤器之后如有需求还可以执行放行之后的逻辑,放行之后的逻辑我们写在doFilter()这行代码之后。
拦截路径
拦截路径 | urlPatterns值 | 含义 |
拦截具体路径 |
/login |
只有访问 /login 路径时,才会被拦截 |
目录拦截 |
/emps/* |
访问/emps下的所有资源,都会被拦截 |
拦截所有 |
/* |
访问所有资源,都会被拦截 |
@WebFilter(urlPatterns = "/login") //拦截/login具体路径
2、过滤器链
在我们web服务器当中,定义了两个过滤器,这两个过滤器就形成了一个过滤器链。
执行顺序:
这个链上的过滤器在执行的时候会一个一个的执行,会先执行第一个Filter,放行之后再来执行第二个Filter,如果执行到了最后一个过滤器放行之后,才会访问对应的web资源。
访问完web资源之后,按照我们刚才所介绍的过滤器的执行流程,还会回到过滤器当中来执行过滤器放行后的逻辑,而在执行放行后的逻辑的时候,顺序是反着的。
先要执行过滤器2放行之后的逻辑,再来执行过滤器1放行之后的逻辑,最后在给浏览器响应数据。
以注解方式配置的Filter过滤器,它的执行优先级是按时过滤器类名的自动排序确定的,类名排名越靠前,优先级越高。
3、拦截器Interceptor
- 它是一种动态拦截方法调用的机制,类似于过滤器。
- 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。
拦截器的作用:
- 拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码
在拦截器当中,我们通常也是做一些通用性的操作,校验令牌合法性。
**自定义拦截器:**实现HandlerInterceptor接口,并重写其所有方法
//自定义拦截器
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
//目标资源方法执行前执行。 返回true:放行 返回false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
return true; //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 .... ");
}
}
注意:
• preHandle方法:目标资源方法执行前执行。返回true:放行,返回false:不放行
• postHandle方法:目标资源方法执行后执行
• afterCompletion方法:视图渲染完毕后执行,最后执行
**注册配置拦截器**:实现WebMvcConfigurer接口,并重写addInterceptors方法
@Configuration
public class WebConfig implements WebMvcConfigurer {
//自定义的拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
}
}
4、拦截路径
在注册配置拦截器的时候,我们要指定拦截器的拦截路径,通过`addPathPatterns("要拦截路径")`方法,就可以指定要拦截哪些资源。
在入门程序中我们配置的是`/**`,表示拦截所有资源,而在配置拦截器时,不仅可以指定要拦截哪些资源,还可以指定不拦截哪些资源,只需要调用`excludePathPatterns("不拦截路径")`方法,指定哪些资源不需要拦截。 /***************************************************/
@Configuration
public class WebConfig implements WebMvcConfigurer {
//拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
.excludePathPatterns("/login");//设置不拦截的请求路径
}
}
在拦截器中除了可以设置/**
拦截所有资源外,还有一些常见拦截路径设置:
拦截路径 | 含义 | 举例 |
/* |
一级路径 |
能匹配/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 |
Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在Spring的Web环境中提供了一个非常核心的Servlet:DispatcherServlet(前端控制器),所有请求都会先进行到DispatcherServlet,再将请求转给Controller。
当我们定义了拦截器后,会在执行Controller的方法之前,请求被拦截器拦截住。执行`preHandle()`方法,这个方法执行完成后需要返回一个布尔类型的值,如果返回true,就表示放行本次操作,才会继续访问controller中的方法;如果返回false,则不会放行(controller中的方法也不会执行)。
在controller当中的方法执行完毕之后,再回过来执行`postHandle()`这个方法以及`afterCompletion()` 方法,然后再返回给DispatcherServlet,最终再来执行过滤器当中放行后的这一部分逻辑的逻辑。执行完毕之后,最终给浏览器响应数据。
/***********************************************************************************************/
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
return true; //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 .... ");
}
}
过滤器和拦截器之间的区别主要是两点:
- 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
- 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。
六、异常处理
三层架构处理异常的方案:
- Mapper接口在操作数据库的时候出错了,此时异常会往上抛(谁调用Mapper就抛给谁),会抛给service。
- service 中也存在异常了,会抛给controller。
- 而在controller当中,我们也没有做任何的异常处理,所以最终异常会再往上抛。最终抛给框架之后,框架就会返回一个JSON格式的数据,里面封装的就是错误的信息,但是框架返回的JSON格式的数据并不符合我们的开发规范。
1、全局异常处理器
- 在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。
- 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。
/***************************************************************************/
@RestControllerAdvice
public class GlobalExceptionHandler {
//处理异常
@ExceptionHandler(Exception.class) //指定能够处理的异常类型
public Result ex(Exception e){
e.printStackTrace();//打印堆栈中的异常信息
//捕获到异常之后,响应一个标准的Result
return Result.error("对不起,操作失败,请联系管理员");
}
}
@RestControllerAdvice = @ControllerAdvice + @ResponseBody 理异常的方法返回值会转换为json后再响应给前端
主要涉及到两个注解:
- @RestControllerAdvice //表示当前类为全局异常处理器
- @ExceptionHandler //指定可以捕获哪种类型的异常进行处理