一、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的一款功能强大、方便易用的分页插件,支持任何形式的单标、多表的分页查询。
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: 8080address: 127.0.0.1
# application.yaml server:port: 8080address: 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
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 //指定可以捕获哪种类型的异常进行处理