Web入门
SpringBootWeb入门
需求:使用SpringBoot开发一个web应用,浏览器发起请求/hello后,给浏览器返回字符串"Hello World~"。
开发步骤:
· 创建SpringBoot工程,并勾选web开发相关依赖。
· 定义HelloController类,添加方法hello,并添加注解。
· 运行测试
// 请求处理类
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
System.out.println("Hello World");;
return "Hello World";
}
}
// Spring启动类
@SpringBootApplication
public class SpringbootWeb1Application {
public static void main(String[] args) {
SpringApplication.run(SpringbootWeb1Application.class, args);
}
}
// 控制台输出
// . ____ _ __ _ _
// /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
//( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
// \\/ ___)| |_)| | | | | || (_| | ) ) ) )
// ' |____| .__|_| |_|_| |_\__, | / / / /
// =========|_|==============|___/=/_/_/_/
//
// :: Spring Boot :: (v3.4.3)
//
//2025-03-13T09:59:14.390+08:00 INFO 25572 --- [Springboot-Web-1] [ main] r.i.s.SpringbootWeb1Application : Starting SpringbootWeb1Application using Java 22.0.2 with PID 25572 (C:\JavaProject\Springboot-Web-1\target\classes started by 11035 in C:\JavaProject\Springboot-Web-1)
//2025-03-13T09:59:14.392+08:00 INFO 25572 --- [Springboot-Web-1] [ main] r.i.s.SpringbootWeb1Application : No active profile set, falling back to 1 default profile: "default"
//2025-03-13T09:59:15.069+08:00 INFO 25572 --- [Springboot-Web-1] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
//2025-03-13T09:59:15.080+08:00 INFO 25572 --- [Springboot-Web-1] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
//2025-03-13T09:59:15.080+08:00 INFO 25572 --- [Springboot-Web-1] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.36]
//2025-03-13T09:59:15.117+08:00 INFO 25572 --- [Springboot-Web-1] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
//2025-03-13T09:59:15.117+08:00 INFO 25572 --- [Springboot-Web-1] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 688 ms
//2025-03-13T09:59:15.412+08:00 INFO 25572 --- [Springboot-Web-1] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
//2025-03-13T09:59:15.416+08:00 INFO 25572 --- [Springboot-Web-1] [ main] r.i.s.SpringbootWeb1Application : Started SpringbootWeb1Application in 1.368 seconds (process running for 1.933)
//2025-03-13T09:59:50.352+08:00 INFO 25572 --- [Springboot-Web-1] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
//2025-03-13T09:59:50.353+08:00 INFO 25572 --- [Springboot-Web-1] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
//2025-03-13T09:59:50.353+08:00 INFO 25572 --- [Springboot-Web-1] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
//Hello World
HTTP协议
概述
HTTP:Hyper Text Transfer Protocol,超文本传输协议,规定了服务器和服务器之间数据传输的规则。
特点:
· 基于TCP协议:面向连接,安全
· 基于请求-响应模型:一次请求对应一次响应
· HTTP协议是无状态的协议:对于处理事务没有记忆能力。每次请求-响应都是独立的。
· 优点:速度快
· 缺点:多次请求间不能共享数据
HTTP请求协议
HTTP响应协议
Tomcat
Web服务器是一个软件程序,对HTTP协议的操作进行封装,使得程序员不必直接对协议进行操作,让Web开发更加便捷。主要功能是“提供网上信息浏览服务”。
请求响应
请求
postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件
作用:常用于进行接口测试
简单参数
原始方式:
@RestController
public class RequestController {
@RequestMapping("/simpleParam")
public String simpleParam(HttpServletRequest request){
// 获取请求参数
String name = request.getParameter("name");
String ageStr = request.getParameter("age");
int age = Integer.parseInt(ageStr);
System.out.println(name + ":" + age);
return "OK";
}
}
Springboot方式:
// @RequestMapping("/simpleParam")
// public String simpleParam(String name, Integer age){
// System.out.println(name + ":" + age);
// return "OK";
// }
@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam(name = "name") String username, Integer age){
System.out.println(username + ":" + age);
// 如果参数列表为{String username, ...},则结果null:10 参数对应不上
// 此时需要添加注释@RequestParam
return "OK";
}
}
@RequestParam
中的requied
属性默认为true
,代表该请求参数必须传递,如果不传递将报错。如果该参数是可选的,可以将required
属性设置为false
。
实体参数
如果参数过多,可以将所有参数封装在一个实体类中,此时请求的参数名需要与实体类的属性名保持一致。
@RequestMapping("/simplePojo")
public String simplePojo(User user){
System.out.println(user); // User{name='Alaska', age=18}
return "OK";
}
对于复杂的实体对象,按照对象层次结构关系即可接受嵌套POJO属性参数。
@RequestMapping("/simplePojo")
public String simplePojo(User user){
System.out.println(user); // User{name='Alaska', age=18, address=Address{province='Anhui', city='Hefei'}}
return "OK";
}
数组集合参数
数组参数:请求参数名与行参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数
@RequestMapping("/arrayParam")
public String arrayParam(String[] hobby){
System.out.println(Arrays.toString(hobby)); // [game, java]
return "OK";
}
集合参数:请求参数名与行参数组名称相同且请求参数为多个,通过@RequestParam
绑定参数关系
@RequestMapping("/listParam")
public String listParam(@RequestParam List<String>hobby){
System.out.println(hobby); // [game, java]
return "OK";
}
日期参数
日期参数:使用@DateTimeFormat
完成日期参数格式转换
@RequestMapping("/dateParam")
public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime){
System.out.println(updateTime); // 2025-03-13T14:51:08
return "OK";
}
Json参数
Json参数:Json数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数,需要使用RequestBody
标识。
@RequestMapping("/jsonParam")
public String dateParam(@RequestBody User user){
System.out.println(user); // User{name='Alaska', age=18, address=Address{province='Hefei', city='beijing'}}
return "OK";
}
路径参数
路径参数:通过请求URL直接传递参数,使用{...}来表示该路径参数,需要使用PathVariable
获取路径参数。
@RequestMapping("path/{id}")
public String pathParam(@PathVariable Integer id){
System.out.println(id);
return "OK";
}
如果需要传输多个路径参数,只需要使用斜杠/
分割即可。
@RequestMapping("path/{id}/{name}")
public String pathParam(@PathVariable Integer id,@PathVariable String name){
System.out.println(id+ ":" + name);
return "OK";
}
响应
@RestController
public class ResponseController {
@RequestMapping("/hello")
public String hello(){
System.out.println("Hello World");
return "Hello World";
}
@RequestMapping("/getAddr")
public Address getAddr(){
Address addr = new Address();
addr.setCity("合肥");
addr.setProvince("安徽");
return addr;
}
@RequestMapping("/listAddr")
public List<Address> listAddr(){
List<Address> list = new ArrayList<>();
Address addr = new Address();
addr.setCity("合肥");
addr.setProvince("安徽");
Address addr2 = new Address();
addr2.setCity("运城");
addr2.setProvince("山西");
list.add(addr);
list.add(addr2);
return list;
}
}
在实际的开发过程中,往往会统一响应结果,以满足更多的业务场景。
/**
* 统一响应结果封装类
*/
public class Result {
private Integer code ;//1 成功 , 0 失败
private String msg; //提示信息
private Object data; //数据 data
public Result() {
}
public Result(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static Result success(Object data){
return new Result(1, "success", data);
}
public static Result success(){
return new Result(1, "success", null);
}
public static Result error(String msg){
return new Result(0, msg, null);
}
@Override
public String toString() {
return "Result{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}
@RequestMapping("/hello")
public Result hello(){
System.out.println("Hello World");
//return new Result(1,"success","Hello World");
return Result.success("Hello World");
}
@RequestMapping("/getAddr")
public Result getAddr(){
Address addr = new Address();
addr.setCity("合肥");
addr.setProvince("安徽");
return Result.success(addr);
}
@RequestMapping("/listAddr")
public Result listAddr(){
List<Address> list = new ArrayList<>();
Address addr = new Address();
addr.setCity("合肥");
addr.setProvince("安徽");
Address addr2 = new Address();
addr2.setCity("运城");
addr2.setProvince("山西");
list.add(addr);
list.add(addr2);
return Result.success(list);
}
/*
{
"code": 1,
"msg": "success",
"data": "Hello World"
}
{
"code": 1,
"msg": "success",
"data": {
"province": "安徽",
"city": "合肥"
}
}
{
"code": 1,
"msg": "success",
"data": [
{
"province": "安徽",
"city": "合肥"
},
{
"province": "山西",
"city": "运城"
}
]
}
*/
分层解耦
三层架构
· controller: 控制层,接受前端发送的请求,对请求进行处理,并响应数据。
· service: 业务逻辑层,处理具体的业务逻辑。
· dao: 数据访问层(Data Access Object)(持久层),负责数据访问操作,包括数据的增、删、改、查。
三层架构通过单一职责原则可以使得代码复用性强、便于维护、利于扩展。
// dao层代码,通过接口实现不同的数据访问方式
public class EmpDaoA implements EmpDao {
@Override
public List<Emp> listEmp() {
//1. 加载并解析emp.xml
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
System.out.println(file);
List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
return empList;
}
}
// service层代码
public class EmpServiceA implements EmpService {
private EmpDao empDao = new EmpDaoA();
@Override
public List<Emp> listEmp() {
//1. 调用dao, 获取数据
List<Emp> empList = empDao.listEmp();
//2. 对数据进行转换处理 - gender, job
empList.stream().forEach(emp -> {
//处理 gender 1: 男, 2: 女
String gender = emp.getGender();
if("1".equals(gender)){
emp.setGender("男");
}else if("2".equals(gender)){
emp.setGender("女");
}
//处理job - 1: 讲师, 2: 班主任 , 3: 就业指导
String job = emp.getJob();
if("1".equals(job)){
emp.setJob("讲师");
}else if("2".equals(job)){
emp.setJob("班主任");
}else if("3".equals(job)){
emp.setJob("就业指导");
}
});
return empList;
}
}
// controller层
@RestController
public class EmpController {
private EmpService empService = new EmpServiceA();
@RequestMapping("/listEmp")
public Result list(){
//1. 调用service, 获取数据
List<Emp> empList = empService.listEmp();
//3. 响应数据
return Result.success(empList);
}
分层解耦
内聚:软件中各个功能模块内部的功能联系。
耦合:衡量软件中各个层/模块之间的依赖、关联的程度。
软件设计原则:高内聚低耦合。
在之前编写的代码中,如果改变了service层的方法名(由EmpServiceA改为EmpServiceB),那么此时controller层中的代码也要跟着改动,这说明controller层和service层是耦合的。
如果要是实现解耦,则不能new对象:
可以提供一个容器,用来存储对象。
让controller程序运行时去从容器中取这个empService对象。
这里有两个问题,同时引出Spring框架重要的两个概念:
对象怎么样交给容器来管理?——控制反转
容器如何为程序提供所依赖的资源?——依赖注入
控制反转:Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。
依赖注入:Dependenvy Injection,简称DI。容器为应用程序提供运行时所依赖的资源,称之为依赖注入。
Bean对象:IOC容器中创建、管理的对象,称之为bean。
IOC&DI入门
通过IOC&DI完成controller层、service层、Dao层的解耦:
· Service层与Dao层的实现类,交给IOC容器管理。@Component
· 为Controller及Service注入运行时依赖的对象。@Autowired
@Component // 将当前类交给IOC容器管理,成为IOC管理中的bean
public class EmpServiceA implements EmpService {
// private EmpDao empDao = new EmpDaoA();
@Autowired // 运行时,IOC容器会提供该类型的bean对象,并赋值给该变量 - 依赖注入
private EmpDao empDao;
@Override
public List<Emp> listEmp() {
//1. 调用dao, 获取数据
List<Emp> empList = empDao.listEmp();
// ...
@Component // 将当前类交给IOC容器管理,成为IOC管理中的bean
public class EmpDaoA implements EmpDao {
@Override
public List<Emp> listEmp() {
//1. 加载并解析emp.xml
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
System.out.println(file);
List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
return empList;
}
}
@RestController
public class EmpController {
@Autowired // 运行时,IOC容器会提供该类型的bean对象,并赋值给该变量 - 依赖注入
// private EmpService empService = new EmpServiceA();
private EmpService empService;
@RequestMapping("/listEmp")
public Result list(){
//1. 调用service, 获取数据
List<Emp> empList = empService.listEmp();
//3. 响应数据
return Result.success(empList);
}
IOC
要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:
@Service // 将当前类交给IOC容器管理,成为IOC管理中的bean
public class EmpServiceA implements EmpService {
// private EmpDao empDao = new EmpDaoA();
@Autowired // 运行时,IOC容器会提供该类型的bean对象,并赋值给该变量 - 依赖注入
private EmpDao empDao;
@Override
public List<Emp> listEmp() {
//1. 调用dao, 获取数据
List<Emp> empList = empDao.listEmp();
// ...
@Repository // 将当前类交给IOC容器管理,成为IOC管理中的bean
public class EmpDaoA implements EmpDao {
@Override
public List<Emp> listEmp() {
//1. 加载并解析emp.xml
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
System.out.println(file);
List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
return empList;
}
}
@RestController // RestController中已经包括了Controller
public class EmpController {
@Autowired // 运行时,IOC容器会提供该类型的bean对象,并赋值给该变量 - 依赖注入
// private EmpService empService = new EmpServiceA();
private EmpService empService;
@RequestMapping("/listEmp")
public Result list(){
//1. 调用service, 获取数据
List<Emp> empList = empService.listEmp();
//3. 响应数据
return Result.success(empList);
}
bean的名字默认为首字母小写的类名,如果需要指定bean的名字,可以使用@Component(value = "beanName")
或者@Component("beanName")
Bean组件扫描:
bean注解想要生效,还需要被组件扫描注解@ComponentScan
扫描,@ComponentScan
注解虽然没有显式配置,但实际上被包含在了启动类声明注解@SpringBootApplication
中,默认扫描的范围是启动类所在包及其子包。(默认程序规范),扫描范围可以通过配置@ComponentScan("package1","package2)
。
DI
Bean注入通过@Autowired
注解,默认是按照类型进行,如果存在多个相同类型的bean,则会报错。
1.@Primary
注解设置优先级,来让对应bean对象生效。
@Primary
@Service
public class EmpServiceA implements EmpService{
}
2.@Qualifier
注解按照类型来指定注入bean对象。
@RestController
public class EmpController{
@Qualifier("empServiceA")
@Autowired
private EmpService empService;
}
3.@Resource
注解按照名称来指定注入bean对象。
@RestController
public class EmpController{
@Resource(name = "empServiceB")
private EmpService empService;
}
MySQL
· 数据库:DataBase(DB),是存储和管理数据的仓库。
· 数据库管理系统:DataBase Management System(DBMS),操纵和管理数据库的大型软件。
· SQL:Structured Query Language,操作关系型数据库的编程语言,定义了一套操作关系型数据库统一标准。
MySQL概述
关系型数据库:建立在关系模型基础上,由多张相互连接的二维表组成的数据库。
特点:
· 使用表存储数据,格式统一,便于维护。
· 使用SQL语言操作,标准统一,使用方便,可用于复杂查询
SQL语句可以单行或多行书写,以分号结尾。
SQL语句可以使用空格/缩进来增强语句的可读性。
MySQL数据库的SQL语句不区分大小写。
注释:
单行注释:-- 注释内容
或# 注释内容
(MySQL持有)
多行注释:/* 注释内容*/
SQL语句通常被分为四类:
DDL(Data Definition Language),数据定义语言,用来定义数据库对象(数据库,表,字段)
DML(Data Manipulation Language),数据操作语言,用来对数据库表中的数据进行增删改
DQL(Data Query Language),数据查询语言,用来查询数据库中表的记录
DCL(Data Control Language),数据控制语言,用来创建数据库用户、控制数据库的访问权限
MySQL数据类型
数值类型
默认为有符号类型,unsigned为无符号类型
字符串类型
char(10)
表示最多只能存10个字符,不足10个字符也会占用10个字符的空间。(空间换时间:性能高,浪费空间)
varchar(10)
表示最多只能存10个字符,不足10个字符,按照实际长度存储。(时间换空间:性能低,节省空间)
日期时间类型
DDL
DDL(Data Definition Language),数据定义语言,用来定义数据库对象(数据库,表,字段)
数据库操作
表结构操作
创建表
create table 表名(
字段1 字段类型 [约束] [comment 字段1注释],
...
字段n 字段类型 [约束] [comment 字段n注释],
)[comment 表注释];
约束:作用于表上字段上的规则,用于限制存储在表中的数据,保证数据库中数据的正确性、有效性和完整性。
create table tb_user(
id int primary key auto_increment comment 'ID,唯一标识', -- 主键约束
username varchar(20) not null unique comment '用户名', -- 非空且唯一
name varchar(10) not null comment '姓名', -- 非空
age int comment '年龄',
gender char(1) default '男' comment '性别' -- 默认性别为男
)comment '用户表';
-- 修改:为表tb_emp添加字段qq,类型为varchar(11),注释为qq
alter table tb_emp add qq varchar(11) comment `qq` ;
-- 修改:修改tb_emp字段qq,类型由varchar(11)改为varchar(13)
alter table tb_emp modify qq varchar(13) comment `qq`;
-- 修改:修改tb_emp字段名,字段名由qq改为qq_num
alter table tb_emp change qq qq_num varchar(13) comment `qq`;
-- 修改:删除tb_emp的qq_num字段
alter table tb_emp drop column qq_num;
-- 修改:将tb_emp表名修改为emp
rename table tb_emp to emp;
-- 删除:删除tb_emp表
drop table if exists tb_emp;
再删除表时,表中的数据也会被删除。
DML
DML(Data Manipulation Language),数据操作语言,用来对数据库表中的数据进行增删改
INSERT
-- 为tb_emp表中的username, name, gender字段插入值
insert into tb_emp(username,name,gender,creat_time,update_time) values ('Alaska','Gulf',1,now(),now());
-- 为tb_emp表中的所有字段插入值
insert into tb_emp values (null,'abc','123',18,2,now(),now());
-- 批量为tb_emp表中的username,name,gender字段插入数据
insert into tb_emp(username,name,gender,creat_time,update_time) values
('username1','name1',1,now(),now()),('username2','name2',2,now(),now());
注意事项:
· 插入数据时,指定的字段顺序需要与值的顺序是一一对应的。
· 字符串和日期型市局应该包含在引号中。
· 插入的数据大小,应该在字段的规定范围内。
UPDATE
-- 将tb_emp表中的ID为1的员工的姓名name字段更新为->'张三'
update tb_emp set name = '张三' ,update_time = now() where id = '1';
-- 将tb_emp表中的所有员工的姓名name字段更新为->'iloveJava'
update tb_emp set name = 'iloveJava' ,update_time = now();
DELETE
-- 删除tb_emp表中ID为2的员工
delete from tb_emp where id = 2;
-- 删除tb_emp表中的所有数据
delete from tb_emp;
注意事项:
· DELETE
语句的条件可以有,也可以没有,如果没有条件,则会删除整张表中的所有数据。
· DELETE
语句不能删除某一个字段的值(如果要操作,可以使用UDPATE
,将该字段的值置为NULL
)。
DQL
DQL(Data Query Language),数据查询语言,用来查询数据库中表的记录
基本查询
-- 查询返回所有字段
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp; -- 推荐方式
select * from tb_emp; -- 不推荐,性能低
-- 查询所有员工的 name, entrydate, 并起别名(姓名、入职日期)
-- select name as 姓名,entrydate as 入职日期 from tb_emp; as可以省略
-- 如果别名有特殊符号或者空格,需要给别名加上引号
select name '姓 名',entrydate 入职日期 from tb_emp;
-- 查询指定字段 name, entrydate并返回
select name,entrydate from tb_emp;
-- 查询已有的员工关联了哪几种职位(不要重复)
select distinct job from tb_emp;
条件查询
-- 查询 姓名 为 杨逍 的员工
select * from tb_emp where name = '杨逍';
-- 查询 id ≤ 5 的员工信息
select * from tb_emp where id <= 5;
-- 查询 没有分配职位 的员工信息
select * from tb_emp where job is null;
-- 查询 有职位 的员工信息
select * from tb_emp where job is not null;
-- 查询 密码不等于 '123456' 的员工信息
select * from tb_emp where password != '123456';
-- 查询 入职日期 在 '2000-01-01'(包含) 到'2010-01-01'(包含)之间的员工信息
select * from tb_emp where entrydate >= '2000-01-01' and entrydate <= '2010-01-01';
select * from tb_emp where entrydate between '2000-01-01' and '2010-01-01';
-- 查询 入职日期 在 '2000-01-01'(包含) 到'2010-01-01'(包含)之间 且 性别为女 的员工信息
select * from tb_emp where entrydate between '2000-01-01' and '2010-01-01' and gender =2;
-- 查询 职位是 2(讲师),3(学工主管),4(教材主管)的员工信息
select * from tb_emp where job = 2 or job = 3 or job = 4;
select * from tb_emp where job in (2,3,4);
-- 查询 姓名 为两个字的员工信息
select * from tb_emp where name like '__';
-- 查询 姓 '张' 的员工信息
select * from tb_emp where name like '张%'
分组查询
-- 统计该企业员工数量
-- count(字段)
select count(id) from tb_emp; -- 29
select count(job) from tb_emp; -- 28 聚合函数不对null值进行运算
-- count(常量)
select count(0) from tb_emp; -- 29
select count('A') from tb_emp; -- 29
-- count(*) -- 推荐
select count(*) from tb_emp; -- 29
-- 统计该企业最早入职的员工
select min(entrydate) from tb_emp; -- 2000-01-01
-- 统计该企业最迟入职的员工
select max(entrydate) from tb_emp; -- 2020-01-01
-- 统计该企业员工 ID 的平均值
select avg(id) from tb_emp; -- 15.0000
-- 统计该企业员工的 ID 之和
select sum(id) from tb_emp; -- 435
-- 根据性别分组,统计男性和女性员工的数量
select gender,count(*) from tb_emp group by gender;
-- 先查询入职时间在2015-01-01(包含)之前的员工,并对结果根据职位分组,获取员工数量大于等于2的职位
select job,count(*) from tb_emp where entrydate <= '2015-01-01' group by job having count(*) >= 2;
排序查询
-- 根据入职时间,对员工进行升序排序
select * from tb_emp order by entrydate;
-- 根据入职时间,对员工进行降序排序
select * from tb_emp order by entrydate desc;
-- 根据入职时间对公司的员工进行升序排序,入职时间相同,再按照更新时间进行降序排序
select * from tb_emp order by entrydate,update_time desc ;
分页查询
-- 从起始索引0开始查询员工数据,每页展示5条记录
select * from tb_emp limit 0,5;
-- 查询第1页的员工数据,每页展示5条记录
select * from tb_emp limit 0,5;
-- 查询第2页的员工数据,每页展示5条记录
select * from tb_emp limit 5,5;
-- 查询第3页的员工数据,每页查询5条记录
select * from tb_emp limit 10,5
-- 起始索引 = (页码 - 1) * 每页展示记录数
流程控制函数:
if(条件表达式, true取值, false取值)
case 表达式 when 值1 then 结果1 when 值2 then 结果2 ... else ... end
多表设计
项目开发中,在进行数据库表结构设计时,会根据业务需求及业务模块之间的关系,分析并设计表结构,由于业务之间相互关联,所以各个表结构之间也存在着各种联系,基本上分为三种:
· 一对多(多对一)
· 多对多
· 一对一
一对多
一对一
create table tb_user(
id int unsigned primary key auto_increment comment 'ID',
name varchar(10) not null comment '姓名',
gender tinyint unsigned not null comment '性别, 1 男 2 女',
phone char(11) comment '手机号',
degree varchar(10) comment '学历'
) comment '用户信息表';
create table tb_user_card(
id int unsigned primary key auto_increment comment 'ID',
nationality varchar(10) not null comment '民族',
birthday date not null comment '生日',
idcard char(18) not null comment '身份证号',
issued varchar(20) not null comment '签发机关',
expire_begin date not null comment '有效期限-开始',
expire_end date comment '有效期限-结束',
user_id int unsigned not null unique comment '用户ID',
constraint fk_user_id foreign key (user_id) references tb_user(id) -- 外键
) comment '用户信息表';
多对多
create table tb_student(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '姓名',
no varchar(10) comment '学号'
) comment '学生表';
create table tb_course(
id int auto_increment primary key comment '主键ID',
name varchar(10) comment '课程名称'
) comment '课程表';
create table tb_student_course(
id int auto_increment comment '主键' primary key,
student_id int not null comment '学生ID',
course_id int not null comment '课程ID',
constraint fk_courseid foreign key (course_id) references tb_course (id),
constraint fk_studentid foreign key (student_id) references tb_student (id)
)comment '学生课程中间表';
多表查询
-- 多表查询
select * from tb_emp,tb_dept; -- 17 x 5 笛卡尔积
select * from tb_emp,tb_dept where tb_emp.dept_id = tb_dept.id; -- 16 有一个没有分配部门
内连接
-- 查询员工的姓名,及所属的部门名称(隐式内连接实现)
select tb_emp.name,tb_dept.name from tb_emp,tb_dept where tb_emp.dept_id = tb_dept.id;
-- 起别名,起了别名之后必须使用别名,不能再使用原来的字段名
select e.name,d.name from tb_emp e, tb_dept d where e.dept_id = d.id;
-- 查询员工的姓名,及所属的部门名称(显式内连接实现)
select tb_emp.name,tb_dept.name from tb_emp inner join tb_dept on tb_emp.dept_id = tb_dept.id;
只有16条数据(有一个员工没有部门数据)
外连接
-- 查询员工表所有员工的姓名,和对应的部门名称(左外连接)
select e.name,d.name from tb_emp e left join tb_dept d on e.dept_id = d.id;
17条数据,由于是左外连接,所以会显示所有的员工的部门信息(即使没有所属部门)
-- 查询部门表所有部门的名称,和对应的员工名称(右外连接)
select e.name,d.name from tb_emp e right join tb_dept d on e.dept_id = d.id;
-- 与下面的左外连接效果相同
select e.name,d.name from tb_dept d left join tb_emp e on e.dept_id = d.id;
由于是右外连接,所以会显示所有部门下的员工信息(即使这个部门下没有员工)
子查询
-- 标量子查询 子查询返回的结果是单个值
-- 查询教研部的所有员工信息
-- 1.查询教研部的部门ID
select id from tb_dept where name = '教研部';
-- 2.再查询该部门ID下的员工信息
select * from tb_emp where dept_id = 2;
-- 3.子查询
select * from tb_emp where dept_id = (select id from tb_dept where name = '教研部');
-- 查询在方东白入职之后的员工信息
-- 1.查询方东白的入职日期
select tb_emp.entrydate from tb_emp where name = '方东白';
-- 2.在查询该入职日期之后的员工信息
select * from tb_emp where entrydate > '2012-11-01';
-- 3.子查询
select * from tb_emp where entrydate > (select tb_emp.entrydate from tb_emp where name = '方东白');
-- 列子查询 子查询返回的结果是一列
-- 查询教研部和咨询部的所有员工信息
-- 1.查询教研部和咨询部的部门ID
select id from tb_dept where name in('教研部','咨询部');
-- 2.根据部门ID,查询该部门下的员工信息
select * from tb_emp where dept_id in (2,3);
-- 3.子查询
select * from tb_emp where dept_id in(select id from tb_dept where name in('教研部','咨询部'));
-- 行子查询 子查询返回的结果是一行
-- 查询与韦一笑的入职日期及职位都相同的员工信息
-- 1.查询韦一笑的入职日期和职位
select entrydate,job from tb_emp where name = '韦一笑';
-- 2.根据查询到的入职日期和职位,查询该条件下的员工信息
select * from tb_emp where entrydate = '2007-01-01' and job = 2;
select * from tb_emp where (entrydate,job) = ('2007-01-01', 2);
-- 3.子查询
select * from tb_emp where(entrydate = (select entrydate from tb_emp where name = '韦一笑')and job = (select job from tb_emp where name = '韦一笑'));
select * from tb_emp where (entrydate,job) = (select entrydate,job from tb_emp where name = '韦一笑');
-- 表子查询 子查询返回的结果是一张表,当做临时表来使用
-- 查询入职日期是2006-01-01之后的员工信息,及其部门名称
-- 1.查询入职日期是2006-01-01之后的员工信息
select * from tb_emp where entrydate > '2006-01-01';
-- 2.查询这部分员工信息及其部门名称
select e.*,d.name from (select * from tb_emp where entrydate > '2006-01-01') e, tb_dept d where e.dept_id = d.id;
事务
事务控制
事务是一组操作的集合,是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
默认是MySQL的事务是自动提交的。当执行一条DML语句,MySQL会立即隐式的提交事务。
四大特性
索引
索引是帮助数据库高效获取数据的数据结构。
优点:
提高数据查询的效率,降低数据库的IO成本。
通过索引列对数据进行排序,降低数据排序的成本,降低GPU消耗。
缺点:
索引会占用存储空间
索引大大提高了查询效率,但是降低了insert、update、delete
的效率
select count(*) from tb_sku; -- 6000000
-- 无索引:
select * from tb_sku where sn = '100000003145003'; -- 耗时13s
-- 创建索引:
create index idx_sku_su on tb_sku(sn); -- 耗时45s(构建了600万条索引,只需构建一次)
-- 有索引:
select * from tb_sku where sn = '100000003145003'; -- 耗时4ms
-- 查看索引
show index from tb_sku;
-- 删除索引
drop index idx_sku_su on tb_sku;
MySQL数据库支持的索引结构有很多,如:Hash索引、B+Tree索引、Full-Text索引等。我们平常所说的索引,默认指的是B+Tree结构组织的索引
Mybatis
入门
Mybatis是一款持久层(Dao)框架,用于简化JDBC的开发。
创建实体类:
package ren.imuzi0221.springbootmybatis.pojo;
public class User {
private Integer id;
private String name;
private short age;
private short gender;
public User() {
}
public User(Integer id, String name, short age, short gender, String phone) {
this.id = id;
this.name = name;
this.age = age;
this.gender = gender;
this.phone = phone;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
", phone='" + phone + '\'' +
'}';
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public short getAge() {
return age;
}
public void setAge(short age) {
this.age = age;
}
public short getGender() {
return gender;
}
public void setGender(short gender) {
this.gender = gender;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
private String phone;
}
编写SpringBoot配置文件
spring.application.name=springboot-mybatis
# 配置数据库的连接信息 - 四要素
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=1234
编写Java(SQL)语句
@Mapper // 在运行时,会自动生成该接口的实现类对象(代理对象),并且将对象交给IOC容器管理
public interface UserMapper {
// 查询全部用户信息
@Select("select * from user")
public List<User> list();
}
@SpringBootTest // SpringBoot整合单元测试的注解
class DemoApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
public void testListUser(){
List<User> userList = userMapper.list();
userList.stream().forEach(user -> {
System.out.println(user);
});
}
}
/**
User{id=1, name='白眉鹰王', age=55, gender=1, phone='18800000000'}
User{id=2, name='金毛狮王', age=45, gender=1, phone='18800000001'}
User{id=3, name='青翼蝠王', age=38, gender=1, phone='18800000002'}
User{id=4, name='紫衫龙王', age=42, gender=2, phone='18800000003'}
User{id=5, name='光明左使', age=37, gender=1, phone='18800000004'}
User{id=6, name='光明右使', age=48, gender=1, phone='18800000005'}
**/
JDBC
// 了解即可
@Test
public void testJdbc() throws Exception {
//1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 获取连接对象
String url = "jdbc:mysql://localhost:3306/mybatis";
String username = "root";
String password = "1234";
Connection connection = DriverManager.getConnection(url, username, password);
//3. 获取执行SQL的对象Statement,执行SQL,返回结果
String sql = "select * from user";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
//4. 封装结果数据
List<User> userList = new ArrayList<>();
while (resultSet.next()){
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
short age = resultSet.getShort("age");
short gender = resultSet.getShort("gender");
String phone = resultSet.getString("phone");
User user = new User(id,name,age,gender,phone);
userList.add(user);
}
//5. 释放资源
statement.close();
connection.close();
}
数据库连接池
数据库连接池是个容器,负责分配、管理数据库连接(Connection)
它允许应用程序重复使用一个现有的数据库连接,而不是在重新建立一个新连接
释放空间时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接泄露
数据库连接池可以实现:
· 资源重用
· 提升系统响应速度
· 避免数据库连接遗漏。
Lombok
Lombok是一个使用的Java类库,能通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法,并可以自动化生成日志变量,简化Java开发,提高效率。
@Data // get、set、equals、toString
@NoArgsConstructor // 无参构造
@AllArgsConstructor // 有参构造
public class User {
private Integer id;
private String name;
private short age;
private short gender;
private String phone;
}
基础操作
删除
@Mapper
public interface EmpMapper {
// 根据ID删除数据
// SQL语句+接口方法
@Delete("delete from emp where id = #{id}")
// public void delete(Integer id);
public int delete(Integer id); // 返回的是受影响的数据的个数
}
@SpringBootTest // SpringBoot整合单元测试的注解
class DemoApplicationTests {
@Autowired
private EmpMapper empMapper;
@Test
public void testDelete(){
System.out.println(empMapper.delete(17)); // 1
}
}
ID为17的员工信息被删除。
预编译SQL语句采用?
占位符,可以有效利用缓存,提高性能。
SQL注入是通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。
校验用户名和密码本质上是SQL的查询操作
select count(*) from emp where username = 'zhangwuji' and password = '123456';
username
字段和password
字段来自于网页端的输入
count(*)
为1,说明数据库中存在对应的账户名和密码,判定为登录成功。
count(*)
为0,说明数据库中不存在对应的账户名和密码,判定为登录失败。
此时通过SQL注入达到成功登录的目的:
select count(*) from emp where username = 'wueiuwieuiwueiw' and password = '' or '1' = '1';
此时密码的输入变成了'' or '1' = '1'(加粗的为输入的内容),这会让系统出现错误判定:'1' = '1'
恒为真,导致整个SQL语句的where条件永远成立
此时需要使用预编译SQL来防止SQL注入
新增
// 新增员工
@Options(keyProperty = "id",useGeneratedKeys = true) // 会自动将生成的主键值,复制给emp的id属性
@Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time)" +
"value (#{username},#{name},#{gender},#{image},#{job},#{entryDate},#{deptId},#{createTime},#{updateTime})") // 一定要是对象的属性名
public void insert(Emp emp);
@Test
public void testInsert(){
// 构造员工对象
Emp emp = new Emp();
emp.setUsername("Tom");
emp.setName("汤姆");
emp.setImage("1.jpg");
emp.setGender((short)1);
emp.setJob((short)1);
emp.setEntryDate(LocalDate.of(2000,1,1));
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
emp.setDeptId(1);
// 执行新增员工信息操作
empMapper.insert(emp);
System.out.println(emp.getId()); // 18
/*
==> Preparing: insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time)value (?,?,?,?,?,?,?,?,?)
==> Parameters: Tom(String), 汤姆(String), 1(Short), 1.jpg(String), 1(Short), 2000-01-01(LocalDate), 1(Integer), 2025-04-06T15:33:22.167075300(LocalDateTime), 2025-04-06T15:33:22.167075300(LocalDateTime)
<== Updates: 1
*/
更新
// 更新员工
@Update("update emp set username = #{username}, name = #{name}, gender = #{gender}, image = #{image}, job = #{job}, entrydate = #{entryDate}, dept_id = #{deptId}, update_time = #{updateTime} where id = #{id};")
public void update(Emp emp);
@Test
public void testUpdate(){
// 构造员工对象
Emp emp = new Emp();
emp.setId(18);
emp.setUsername("Alaska");
emp.setName("阿拉斯加");
emp.setImage("1.jpg");
emp.setGender((short)1);
emp.setJob((short)1);
emp.setEntryDate(LocalDate.of(2000,1,1));
emp.setUpdateTime(LocalDateTime.now());
emp.setDeptId(1);
// 执行新增员工信息操作
empMapper.update(emp);
/*
* ==> Preparing: update emp set username = ?, name = ?, gender = ?, image = ?, job = ?, entrydate = ?, dept_id = ?, update_time = ? where id = ?;
* ==> Parameters: Alaska(String), 阿拉斯加(String), 1(Short), 1.jpg(String), 1(Short), 2000-01-01(LocalDate), 1(Integer), 2025-04-06T15:51:20.532233200(LocalDateTime), 18(Integer)
* <== Updates: 1
*/
查询
根据ID查询
// 根据ID查询员工
@Select("select * from emp where id = #{id}")
public Emp getById(Integer id);
@Test
public void testGetById(){
Emp emp = empMapper.getById(18);
System.out.println(emp); // Emp(id=18, username=Alaska, password=123456, name=阿拉斯加, gender=1, image=1.jpg, job=1, entryDate=2000-01-01, deptId=null, createTime=null, updateTime=null)
}
注意到deptId=null, createTime=null, updateTime=null
,这几项数据没有被封装进来
// 方案一:给字段起别名,让别名与实体类属性一致
@Select("select id, username, password, name, gender, image, job, entrydate, dept_id deptId, create_time createTime, update_time updateTime from emp where id = #{id}")
public Emp getById(Integer id);
// 方案二:通过@Results,@Result注解手动映射封装
@Results({
@Result(column = "dept_id", property = "deptId"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime")
})
@Select("select * from emp where id = #{id}")
public Emp getById(Integer id);
// Emp(id=18, username=Alaska, password=123456, name=阿拉斯加, gender=1, image=1.jpg, job=1, entryDate=2000-01-01, deptId=1, createTime=2025-04-06T15:33:22, updateTime=2025-04-06T15:51:21)
// 方案三:开启Mybatis的驼峰命名自动映射开关 a_column ---> aColumn
// 在application.properties配置文件中,开启mybatis的驼峰命名自动映射开关
// mybatis.configuration.map-underscore-to-camel-case=true
条件查询
// 条件查询员工信息
// @Select("select * from emp where name like '%${name}%' and gender = #{gender} //and entrydate " +
// "between #{begin} and #{end} order by update_time desc ;")
@Select("select * from emp where name like concat('%',#{name},'%') and gender = #{gender} and entrydate " +
"between #{begin} and #{end} order by update_time desc ;")
public List<Emp> list(String name, short gender, LocalDate begin, LocalDate end); // 此处由于单引号内不能使用#{},而直接使用${},会产生SQL注入问题,可以使用concat字符串拼接函数concat('%',#{name},'%')
@Test
public void testList(){
List<Emp> empList = empMapper.list("张", (short)1,LocalDate.of(2010,1,1),LocalDate.of(2020,1,1));
System.out.println(empList); // [Emp(id=2, username=zhangwuji, password=123456, name=张无忌, gender=1, image=2.jpg, job=2, entryDate=2015-01-01, deptId=2, createTime=2025-04-05T19:51:56, updateTime=2025-04-05T19:51:56)]
}
XML映射文件
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java注解不仅力不从心,还会让本就复杂的SQL语句更加混乱,所以如果需要做一些很复杂的操作,最好是用XML来映射语句。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.-//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="ren.imuzi0221.mybatis.mapper.EmpMapper">
<!-- resultType: 单条记录所封装的类型全类名-->
<select id="list" resultType="ren.imuzi0221.mybatis.pojo.Emp">
select * from emp where name like concat('%',#{name},'%') and gender = #{gender} and entrydate
between #{begin} and #{end} order by update_time desc
</select>
</mapper>
动态SQL
如果只选择一个或两个条件,那么此时没有被输入条件的字段会被传入NULL,会导致查询不到数据,因此需要动态SQL。
< if >
<if>:用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则执行SQL语句。
<mapper namespace="ren.imuzi0221.mybatis.mapper.EmpMapper">
<!-- resultType: 单条记录所封装的类型全类名-->
<select id="list" resultType="ren.imuzi0221.mybatis.pojo.Emp">
select *
from emp
<where> <!-- 动态生成where关键字,自动去除掉and和or,<set>标签自动去除掉逗号',' -->
<if test="name != null">
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>
< foreach >
<foreach>:常用于批量操作
// 批量删除员工
public void deleteByIds(List<Integer> ids);
<!-- 批量删除员工 (1,2,3) -->
<!--
collection: 遍历的集合
item: 遍历出来的元素
separator: 分隔符
open: 遍历开始前拼接的SQL片段
close: 遍历结束后拼接的SQL片段
-->
<delete id="deleteByIds">
delete from emp where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
< sql >< include >
<sql>
片段可以增加代码的复用型
案例:tlias智能学习辅助系统
待填坑
登录认证
会话技术
会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
回话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
回话跟踪方案:
客户端会话跟踪技术:Cookie
服务端会话跟踪技术:Session
令牌技术
Cookie
Cookie:HTTP请求报头包含存储先前通过与所述服务器发送的HTTP cookies
Set-Cookie:HTTP响应报头被用于从服务器向用户代理发送cookie
优点:
HTTP协议中本身支持的技术
缺点:
移动端APP无法使用Cookie
不安全,用户可以自己禁用Cookie
Cookie不能跨域(请求的协议、IP/域名、端口不同)
Session
优点:
存储在服务端,安全
缺点:
服务器集群环境下无法直接使用Session
移动端APP无法使用Session
不安全,用户可以自己禁用Session
Session也不能跨域
令牌
优点:
支持PC端、移动端
解决集群环境下的认证问题
减轻服务器端存储压力
缺点:
需要自己实现
JWT令牌
JWT(JSON Web Token):定义了一种简洁的、自包含的格式,用于在通信双方json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
组成
第一部分:Header(头),记录令牌类型、签名算法等。例如:{"alg":"HS256","type":"JWT"}
第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{"id":"1","username":"Tom"}
第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定密钥,通过指定签名算法计算而来。
// 生成JWT
public void testGetjwt(){
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)) // 设置有效期为1h
.compact();
System.out.println(jwt);
//eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTc0NDUzNzY2MH0.HMp9TW1nn3333WgiVIuB9zpTOgQyF3Vsk8pKfn5rv-s
}
// 解析JWT
public void testParseJwt(){
Claims claims = Jwts.parser()
.setSigningKey("itheima") .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTc0NDUzNzY2MH0.HMp9TW1nn3333WgiVIuB9zpTOgQyF3Vsk8pKfn5rv-s")
.getBody();
System.out.println(claims);
// {name=tom, id=1, exp=1744537660}
}
过滤器
过滤器(Filter):是JavaWet三大组件(Servlet、Filter、Listener)之一,过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能,一般用于完成通用的操作,例如:登录校验、统一编码处理、敏感字符处理等。
放行后访问对应资源,资源访问完成后,还会回到Filter中,并且继续执行放行后的逻辑。
过滤器链:一个Web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。
注解配置的Fliter,优先级是按照过滤器类名(字符串)的自然排序。
拦截器
拦截器(Interceptor):是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的用来动态拦截控制器方法的执行。
作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。
过滤器与拦截器:
接口规范不同:过滤器需要实现Filter
接口,而拦截器需要实现HandlerInterceptor
接口。
拦截范围不同:过滤器Filter会拦截所有的资源,而拦截器Interceptor只会拦截Spring环境中的资源。
评论区