SpringBoot基础
第一个Spring Web项目
Hello World
- 使用IDEA创建工程。工程名,jdk版本
创建工程时可以将Server URL修改为start.aliyun.com,有初始代码更适合初学者
- 选择springboot版本(后面不要有后缀的),导入依赖(依赖后续可以在pom.xml中修改)
基本的web项目只需要Spring Web依赖,其他依赖后续可以在pom.xml添加。
- 定义请求处理类:创建controller目录,在其下面创建一个测试Controller(如果工程连Controller都跑不起来就白写了)
@RestController
public class TestController {
@RequestMapping(value = "/test")
public String test(){
return "Hello, world!";
}
}
- 修改启动类(XXXApplication.java)的注解,使springboot工程不需要连接数据库也可以运行
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
- 启动工程,并用浏览器打开http://localhost:8080/test(一般来说springboot默认端口都是8080,可以在控制台查看)
localhost:8080/test,意思是向本地计算机中的8080端口程序,获取资源位置 是/test的数据
打开浏览器显示"Hello world"表示第一个应用开发成功。
如果你使用浏览器访问了其他路由,例如http://localhost:8080或其他随便一个如http://localhost:8080/fl,会出现以下页面:
控制台显示Failed to load resource: the server responded with a status of 404 ()。这是因为你没有在声明处理该请求的controller,你可以仿照前面的方式自己添加controller自己处理请求,后面会进一步讲解。
- 彩蛋-设置springboot启动时图标
- 在resource文件下创建banner.txt
- 在ASCII艺术字(图)集网址中生成后复制到banner.txt
___________ ____
______/ \__// \__/____\
_/ \_/ : //____\\
/| : : .. / \
| | :: :: \ /
| | :| || \ \______/
| | || || |\ / |
\| || || | / | \
| || || | / /_\ \
| ___ || ___ || | / / \
\_-_/ \_-_/ | ____ |/__/ \
_\_--_/ \ /
/____ /
/ \ /
\______\_________/
- 启动工程,就会发现控制台中springboot的图标变化了
Hello World探究
项目基本结构
初始项目如下:
完整结构如下:
boot3-01-helloworld/
|-- src/
| |-- main/
| |-- java/ # 项目的源代码
| |-- com/
| |-- fl/
| |-- boot/ # 包名,例如com.fl.boot
| |-- MyApplication.java # 应用的入口类,包含 main 方法,用于启动 Spring Boot 应用
| |-- controller/ # 包含所有的控制器类(Controller),它们处理用户的输入并返回响应
| |-- MyController.java
| |-- service/ # 包含服务类(Service),它们包含业务逻辑。
| |-- MyService.java
| |-- repository/ # 用于Spring Data项目,适用于JPA、MongoDB、Neo4j等多种数据源
| |-- MyRepository.java
| |-- mapper/ # 用于MyBatis项目,用于关系型数据库,也可以通过扩展支持其他数据源
| |-- MyMapper.java
| |-- entity/ # 包含实体类(Entity),它们映射到数据库表。
| |-- MyEntity.java
| |-- config/ # 包含配置类,用于配置应用的行为
| |-- MyConfig.java
| |-- exception
| |-- GlobalExceptionHandler.java # 捕获全局异常并处理
| |-- resources/ # 包含了应用的所有资源文件
| |-- application.properties # 配置文件
| |-- application.yml # 配置文件,实际开发比上面的更常用
| |-- static/ # 用于存放静态资源,如CSS、JavaScript和图片文件
| |-- templates/ # 用于存放Web应用的模板文件,这通常是在使用模板引擎(如Thymeleaf)时需要的
| |-- schema.sql # 用于存放创建数据库结构的SQL脚本
| |-- data.sql # 用于初始化数据库中的数据的SQL脚本
| |-- test/ # 可以按照与src/main/java相似的包结构组织测试类
|-- pom.xml # Maven构建文件,用于定义项目的依赖、插件和其他构建配置
|-- build.gradle # Gradle构建文件,用于定义项目的依赖、插件和其他构建配置
|-- .gitignore # 定义 Git 版本控制系统应该忽略的文件和目录
|-- README.md # 这是项目的 README 文件,通常包含项目的基本信息和如何运行应用的指南
重要文件
下面对Springboot3的项目中的几个重要文件做基本的介绍:
入口点MyApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
- Spring Boot 应用的入口点是包含 @SpringBootApplication注解的类,以及在该类中定义的 main 方法。这个入口类负责启动 Spring Boot 应用。
- @SpringBootApplication:这是一个组合注解,它包含了三个主要的注解:
- @SpringBootConfiguration:标记这个类作为应用的配置类。
- @EnableAutoConfiguration:让 Spring Boot 根据类路径中的 jar 包、类,为当前项目进行自动配置。
- @ComponentScan:告诉 Spring 扫描这个类所在的包及其子包中的注解组件(如 @Component, @Service, @Repository, @Controller 等)。
- main 方法:这是 Java 应用的标准入口点。当运行 Spring Boot 应用时,JVM 调用这个 main 方法启动应用。
- SpringApplication.run(MyApplication.class, args);:这行代码负责启动 Spring 应用上下文。SpringApplication 提供了一个方便的方式来启动 Spring 应用。run 方法的参数 MyApplication.class 是入口类的类对象,它用于告诉 SpringApplication 起始的配置类是哪一个。args 是从命令行传入的参数。
- 当运行这个 main 方法时,会发生以下步骤:
- 创建一个合适的 ApplicationContext 实例(取决于类路径和其它因素)。
- 注册一个 CommandLinePropertySource 以将命令行参数添加到 Spring 的 Environment 中。
- 刷新 ApplicationContext,加载所有单例 beans。
- 执行任何 CommandLineRunner beans。
pom.xml
pom.xml 是 Maven 项目中的一个核心文件,用于定义项目的构建、报告和依赖关系等信息。在 Spring Boot 3 中,pom.xml 文件同样扮演着重要的角色。
一个基本的 pom.xml 文件通常包含以下几个部分:
- 项目的基本信息:包括 <groupId>、<artifactId>、<version> 等,它们分别代表项目的组ID、项目ID 和版本号。
- 父项目:如果你的项目依赖于其他项目,那么你需要声明你的项目是哪个项目的子项目,这可以通过 <parent> 元素来实现。
- 依赖关系:在你的项目中,可能需要用到其他的库或者框架,这时就需要在 <dependencies> 元素下声明对这些库或框架的依赖。
- 插件:<plugins> 元素用于声明项目需要的插件,例如编译插件、打包插件等。
- 构建配置:<build> 元素用于设置项目的构建参数,例如源代码目录、目标目录、测试目录等。
application.yml
application.yml 是一个用于配置 Spring Boot 应用程序的文件。它允许开发者为应用程序的不同方面(如数据库连接、安全设置、消息服务等)提供特定的配置。
日志★
介绍
- 日志是指应用程序运行时产生的信息,这些信息可以帮助开发者了解应用程序的运行状态、调试问题以及监控应用程序的行为。日志可以包括信息性的消息、警告、错误以及调试信息等。
- 开发规范:不要使用System.out.println(),使用专业日志框架记录信息。这是因为使用专业的日志框架可以提高应用程序的可维护性、性能和可扩展性。它允许您以更灵活和高效的方式处理日志记录,而System.out.println()则是一种简陋且不灵活的日志解决方案。
- Spring Boot默认使用Apache Commons Logging作为内部的日志框架,但允许您通过配置使用其他日志框架,如Logback、Log4J2等。Spring Boot为Logback和Log4J2提供了默认的配置,使得您可以快速开始记录日志。
日志门面是一个抽象层,它定义了日志记录的接口,但不提供具体的日志记录实现。它的目的是提供一种统一的方式来访问日志记录功能,而不关心底层的日志记录系统是什么。这样,无论底层使用的是哪种日志实现,开发者都可以使用相同的方法和API来记录日志。
日志实现是具体实现日志记录功能的库或框架。它实现了日志门面定义的接口,并提供实际的日志记录能力。例如,Logback和Log4J都是日志实现的例子。
基本使用
- 要自己生成日志我们可以使用Lombok 库提供的一个注解,@Slf4j。
- @Slf4j 注解可以为当前的类自动生成一个 SLF4J的日志对象,通常这个对象被命名为 log。
- 在 SLF4J 中,Logger 接口定义了几个常用的日志记录方法,包括 info(), debug(), warn(), 和 error()。这些方法用于记录不同级别的日志信息,以便于开发者根据日志级别来过滤和查看应用程序的运行情况。
- info(): 用于记录一般信息,通常是应用程序运行中的正常事件或者流程信息。这些信息通常对监控应用程序的日常运行是有帮助的。
- debug(): 用于记录调试信息,这些信息通常只在开发阶段需要,用于帮助开发者诊断问题。在生产环境中,通常不会启用 debug 级别的日志。
- warn(): 用于记录警告信息,表明应用程序可能遇到一些不正常或不期望的情况,但这些情况并不影响应用程序的继续运行。
- error(): 用于记录错误信息,通常是指应用程序遇到了严重的错误,这些错误可能会导致应用程序的部分功能失败或者整个应用程序崩溃。
要使用@Slf4j注解,首先要保证在pom.xml中引入了Lombok 库的依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
示例:
import lombok.extern.slf4j.Slf4j;
@RestController
@Slf4j
public class TestController {
// 无参测试
@RequestMapping(value = "/test")
public String test() {
log.info("Hello, world!");
log.warn("Hello, world!");
log.error("Hello, world!");
return "Hello, world!";
}
}
使用浏览器访问http://localhost:8080/test,控制台出现下面日志:
在实际使用中,你可以在日志消息中包含变量或者使用占位符来提高日志的可读性。
String userName = "张三";
log.info("用户 {} 尝试登录系统。", userName);
error() 方法通常用于记录异常信息,你可以将异常对象作为参数传递给 error() 方法
try {
// ... 可能会抛出异常的代码 ...
} catch (Exception e) {
log.error("发生了一个异常:", e);
}
小技巧
lombok★
介绍
实体类代码臃肿(getter、setter、toString...),太繁琐
Lombok是一个实用的java类库,能通过注解的形式自动生成构造器、getter/setter、equals、hashcode、tostring等方法,并可以自动化生成日志变量,简化java开发、提高效率。
注解 | 作用 |
@Getter/@Setter | 为所有的属性提供get/set方法 |
@ToString | 会给类自动生成易阅读的toString方法 |
@EqualsAndHashCode | 根据类所拥有的非静态字段自动重写equals方法和hashCode方法 |
@Data | 提供了更综合的生成代码功能(@Getter+@Setter+@ToString+@EqualsAndHashCode) |
@NoArgsConstructor | 为实体类生成无参的构造器方法 |
@AllArgsConstructor | 为实体类生成除了static修饰的字段之外带有各参数的构造器方法。 |
使用
- 在pom.xml引入依赖(如果在创建时引入了可以跳过)
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!--不需要指定版本,在父工程已指定-->
</dependency>
- 在实体类中使用Lombok注解
@Data
@NoArgsConstructor
@AllArgsConstructor
- lombok在编译时,会自动生成对应java代码。使用代码时,还需要安装一个lombok插件(idea自带,除非较老版本)
实例
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String name;
private Short age;
private Short gender;
private String phone;
//使用lombok省略了以下代码
// 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;
// }
// public Integer getId() {
// return id;
// }
// ...
// @Override
// public String toString() {
// return "User{" +
// "id=" + id +
// ", name='" + name + '\'' +
// ", age=" + age +
// ", gender=" + gender +
// ", phone='" + phone + '\'' +
// '}';
// }
}
配置
在项目根目录下:
config.stopBubbling=true
lombok.toString.callSuper=CALL
lombok.equalsAndHashCode.callSuper=CALL
clear lombok.val.flagUsage
lombok.val.flagUsage=ERROR
clear lombok.accessors.flagUsage
lombok.accessors.flagUsage=ERROR
springboot配置
配置文件分类
- springboot提供了多种属性配置方式
-
- application.xml(只有老的spring项目使用)
- application.properties
- application.yml(或yaml)
优先级:properties>yml>yaml
配置文件
pom.xml★
在Spring Boot项目中,pom.xml文件是Maven项目对象模型(Project Object Model)的定义文件,它用于管理项目的构建、依赖和插件。
Maven是一个流行的自动化构建工具,它通过pom.xml文件来执行构建过程。本教程全部项目都使用Maven构建。
下面是一个基本的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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 项目的基本信息 -->
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId> <!-- 组织或项目的唯一标识符 -->
<artifactId>my-spring-boot-app</artifactId> <!-- 项目的基本名称 -->
<version>1.0-SNAPSHOT</version> <!-- 项目的版本号 -->
<name>My Spring Boot App</name> <!-- 项目的显示名称 -->
<description>Spring Boot 3 example project</description> <!-- 项目的描述 -->
<!-- Spring Boot父项目,提供依赖管理和插件管理 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0</version>
</parent>
<!-- 项目依赖 -->
<dependencies>
<!-- Spring Boot的Web Starter,包含Spring MVC和Tomcat等 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 其他依赖... -->
</dependencies>
<!-- 项目构建配置 -->
<build>
<plugins>
<!-- Spring Boot的Maven插件,用于打包可执行的jar或war文件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- 其他插件... -->
</plugins>
</build>
</project>
注意事项:
- 继承Spring Boot父项目:通过指定spring-boot-starter-parent作为父POM,您的项目将继承Spring Boot的默认配置,包括依赖管理、插件管理等。
- 依赖管理:在<dependencies>部分,您可以添加项目所需的所有依赖。Spring Boot提供了许多“Starter”依赖,它们是一组方便的依赖描述符,可以简化构建配置。
- 插件配置:在<build>部分的<plugins>中,您可以配置项目构建时使用的插件。例如,spring-boot-maven-plugin用于创建可执行的jar或war文件。
- 属性和配置:您可以在pom.xml中定义和使用属性,以便于维护和管理配置。例如,您可以定义一个属性来表示Spring Boot的版本号,并在多个地方引用它。
- 多环境配置:Maven支持多环境配置,您可以通过 profiles 来定义不同环境下的配置,并在构建时激活相应的 profile。
properties配置文件*
介绍
properties配置文件是一种用于配置应用程序属性的文件格式。它是一个标准的Java属性文件,通常包含键值对,其中键和值之间用等号=分隔。properties文件可以直接放在src/main/resources目录下,或者放在任何类路径(classpath)可以访问的地方。
只有你需要与旧的Java应用程序或框架保持兼容时才使用。
示例
server.port=8080
server.servlet.context-path=/myapp
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=myuser
spring.datasource.password=mypassword
优缺点
优点:
向后兼容性:properties文件格式在Java历史中非常悠久,几乎所有版本的Java都支持这种格式。简单性:properties文件的语法非常简单,对于简单的配置来说,它是非常直观的。
缺点
树状结构的复杂性:对于复杂的配置,尤其是层级结构的数据,properties文件可能会变得难以阅读和维护。类型不明确:properties文件中的所有值都是字符串,这意味着在将它们赋值给配置类中的不同类型字段时,需要手动进行类型转换。
yml配置文件★
介绍
YAML是“YAML Ain’t Markup Language”(递归缩写为“YAML不是标记语言”)的缩写,它是一种直观的数据序列化格式,可以被用于配置文件。
YAML使用空白字符(空格和缩进)来表示结构层次,它比properties文件更适合表示复杂的配置数据,实际开发基本都使用yml配置文件,有的程序员甚至在创建springboot工程后第一件事就是把配置文件的后缀改为yml。
适合复杂的、具有层级结构的配置场景,尤其是当你需要配置大量的、相关的配置项时。
优缺点
优点:
清晰的层次结构:YAML使用缩进来表示层级关系,这使得表示复杂的数据结构变得非常清晰。类型支持:YAML支持多种数据类型,如字符串、数字、布尔值等,并且可以自动将值转换为适当的类型。可读性强:YAML文件通常更易于阅读和理解,尤其是对于具有复杂层次结构的配置。
缺点:
轻微的复杂性:YAML的语法比properties文件稍微复杂一些,初学者可能需要一些时间来适应。
yml基本语法
- 大小写敏感
- 数值前必须有空格,作为分隔符
- 使用缩进表示层级关系,缩进时,不允许使用tab键,只能使用空格
- 缩进的空格数不重要,只要相同层级的元素左侧对齐即可
- #表示注解
server:
# 修改springboot工程运行端口
port: 8081
#驱动类名称
spring:
datasource:
# 设置数据库驱动
driver-class-name: com.mysql.cj.jdbc.Driver
# 设置数据库地址
url: jdbc:mysql://localhost:3306/tlias
username: root
password: 123456
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 100MB
#配置mybatis的日志, 指定输出到控制台
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
用户自定义内容
- 对象(map): 键值对的集合
person:
name: zhangsan
# 行内写法
person: {name: zhangsan}
- 数组: 一组按次序排序的值
address:
- beijing
- shanghai
# 行内写法
address: [beijing,shanghai]
- 纯量: 不可分割的值
s1: '123 \n 456' # 单引号不会被转义
s2: "123 \n 456" # 双引号会被转义
- 参数引用
name: zhangsan
person:
name: ${name}
获取自定义值
- @Value
@Value("${person1.name}")
private String name;
@Value("${address1[0]}")
private String a1;
@Value("${s1}")
private String s1;
@Value("${s2}")
private String s2;
@Test
void test() {
System.out.println(name);
System.out.println(a1);
System.out.println(s1);
System.out.println(s2);
}
- Envirment
@Autowired
private Environment env;
@Test
void test() {
System.out.println(env.getProperty("person1.name"));
System.out.println(env.getProperty("address1[0]"));
System.out.println(env.getProperty("s1"));
System.out.println(env.getProperty("s2"));
}
- @ConfigurationProperties
先在pom.xml中添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
创建配置类
@Data //lombok
@Component //添加到组件
@ConfigurationProperties(prefix = "person1")//绑定配置文件中配置类
public class Person {
private String name;
private int age;
private String[] address;
}
在测试类中使用
@Autowired
private Person person;
@Test
void test() {
System.out.println(person.getName());
}
Profile
介绍:profiles是不同配置选项的集合,它们对应于应用程序的不同运行环境,如开发环境、测试环境和生产环境。每个环境可能需要不同的设置,例如数据库连接、API端点、服务地址等。
使用:
- 在application.yml中,添加spring.profiles.active属性可以指定默认使用的配置环境
- 在application.yml中可以使用
${...}
指定需要使用的配置
例如:
server:
port: 8080
spring:
profiles:
# 指定 默认使用的配置环境
active: dev
main:
allow-circular-references: true
datasource:
druid:
driver-class-name: ${sky.datasource.driver-class-name}
url: jdbc:mysql://${sky.datasource.host}:${sky.datasource.port}/${sky.datasource.database}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: ${sky.datasource.username}
password: ${sky.datasource.password}
sky:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
host: localhost
port: 3306
database: sky_take_out
username: root
password: 123456
配置加载顺序
内部配置
- file:./config/:当前项目下的/config目录下
- file:./当前项目的根目录
- classpath:/config/classpath的/config目录
- classpath:.classpath的根目录
加载顺序为上面的排列顺序,高优先级配置的属性会生效
外部配置
注意:当前项目下的/config目录下的配置文件和当前项目的根目录的配置文件因为不符合maven结构,不会被打入jar包
- java -jar xxxx.jar --springboot.port=端口号
- java -jar xxxx.jar --springboot.config.location=配置文件路径
- 将配置文件写在target根目录或target目录的config目录(比前面优先级高)下,可以使配置文件直接被读取
Springboot web
Web请求响应★
请求协议和响应协议在前置知识有详细介绍。
原理概述
- BS架构
-
- BS架构,即浏览器/服务器架构(Browser/Server Architecture),是一种网络架构模式,其中用户通过浏览器向服务器发送请求,服务器处理请求并将结果返回给浏览器,浏览器再负责展示这些结果给用户。
- 这种模式的主要特点是用户界面是通过Web浏览器实现的,而数据和应用程序逻辑则存储在服务器上。
在BS架构中,客户端通常不需要安装任何专门的软件,只需一个可以访问Web的浏览器,这大大简化了客户端的维护工作,所有的升级、修改和维护操作几乎都在服务器端完成。
- Spring Boot Web程序与浏览器通信的过程大致如下:
-
- 启动Spring Boot应用程序:当Spring Boot应用程序启动时,它会自动配置嵌入式的Tomcat(或Jetty、Undertow等其他服务器)。
- 创建控制器(Controller):在Spring Boot应用程序中,开发者定义控制器来处理HTTP请求。控制器中的方法通常用
@RequestMapping
注解来映射HTTP请求到相应的处理方法。 - 接收HTTP请求:当用户在浏览器中输入URL并提交请求时,这个请求会发送到Spring Boot应用程序的嵌入式服务器。
- 处理请求:服务器接收到请求后,根据URL路由到相应的控制器方法。控制器方法执行业务逻辑,并可能访问数据库或其他服务来获取数据。
- 返回响应:控制器方法处理完请求后,会返回一个模型和视图(ModelAndView),或者直接返回一个对象,Spring Boot会自动将这个对象转换为JSON或XML等格式,然后服务器将这个响应发送回浏览器。
- 浏览器渲染页面:浏览器接收到响应后,根据返回的数据(可能是HTML、CSS、JavaScript,或者是一个JSON数据),渲染页面并展示给用户。
发送请求
使用API开发工具,向“接口地址”发送各种请求。
“接口地址”通常指的是您要测试或开发的API的URL(统一资源定位符)。这个URL是您向API发送请求的地方,它指定了网络上的资源位置,使得客户端(如Postman或Apipost)能够与服务器上的API进行通信。
例如,在前面的HelloWorld程序中,我们api的url是这样的http://localhost:8080/test
,而实际开发中,一个API的URL可能看起来像这样https://api.example.com/resources/endpoint
。
请求包括请求方法、请求头和请求体。常用请求方法包括GET(获取资源)、POST(提交数据)、PUT(更新资源)、DELETE(删除资源),可以在请求头或请求体中传递参数或数据。
切换请求方法
打开Postman或Apipost,可以切换各种请求的方法。
携带参数
在前面的HelloWorld项目中,我们的url是http://localhost:8080/test
,没有携带参数。实际开发中,我们需要携带各种参数或数据,向服务器发送更复杂的请求。
下面是各种参数的介绍与携带方式:
简单参数
简介:在向服务器发起请求时,向服务器传递的是一些普通的请求数据
http://localhost:8080/simpleParam?name=Tom&age=10
实体参数
简介:如果请求参数比较多且有一定关联,可以考虑将请求参数封装到一个实体类对象中。
方式:如果是简单的实体参数, 直接传递参数即可;如果实体比较复杂(多个实体类嵌套),就需要将所有实体类的属性传递
http://localhost:8080/simpleEntity?name=Tom&age=10
http://localhost:8080/simpleEntity?name=Tom&age=18&address.province=广东&address.city=广州
数组或集合参数
使用场景:在HTML的表单中,有一个表单项是支持多选的(复选框),可以提交选择的多 个值。
发送下面请求,java可以使用数组或集合接收
http://localhost:8080/arrayParam?hobby=game&hobby=java
//或
http://localhost:8080/arrayParam?hobby=game,java
json参数
处理请求
简介
- 在Spring Boot中,
@Controller
和@RestController
注解用于标识一个类作为Web层的控制器。这些控制器负责处理来自客户端的HTTP请求,并返回相应的响应。
@Controller
@Controller
是一个用于定义控制器的注解,主要用于处理HTTP请求并生成响应。当我们在一个类上使用@Controller
注解时,表明这个类是控制器类,它的方法可以被Spring MVC框架
调用以处理HTTP请求。@Controller
主要用于处理传统的HTML请求,并生成视图,@Controller
可以返回任何类型的数据,包括字符串、模型对象、视图名称等。
@RestController
@RestController
是一个特殊的控制器,它是@Controller
和@ResponseBody
的结合。这意味着,当你在一个方法上使用@RestController
时,这个方法就不能返回视图名称,只能返回数据。@RestController
主要用于处理RESTful请求,只能返回特定类型的数据,如JSON、XML或自定义的媒体类型。
@RequestMapping
注解(及其专用变体)用于将HTTP请求映射到具体的处理器方法,可以定义在控制器类上,也可以定义在类里面的方法上,来指定一个方法或类来处理一个或多个HTTP方法和路径。
@RequestMapping 基本用法
@RequestMapping
注解的基本语法如下:
@RequestMapping(value = "/path", method = RequestMethod.GET)
public responseType handlerMethod() {
// ...
}
其中,value
属性指定了请求的实际地址,method
属性指定了请求的方法类型,可以是 GET、POST、PUT 或者 DELETE 等。
@RequestMapping注解可以接受多种属性,例如:
- value:指定请求的实际地址,如"/path"。
- method:指定请求的HTTP方法类型,如GET、POST、PUT、DELETE等。默认情况下,不指定method属性时,@RequestMapping会处理所有HTTP方法。
- consumes:指定处理请求的提交内容类型(Content-Type),例如application/json、application/xml等。
- produces:指定返回的内容类型,仅当请求头中的Accept类型中包含该指定类型才返回。
- params:指定请求中必须包含某些参数值。
- headers:指定请求中必须包含某些指定的header值。
@RequestMapping 专用变体
Spring Boot 提供了几个 @RequestMapping
的专用变体,这些变体都带有特定的 HTTP 请求方法,使得代码更加清晰易懂。
@GetMapping
@GetMapping
是 @RequestMapping
的 GET 请求专用版,其基本语法如下:
@GetMapping("/path")
public responseType handlerMethod() {
// ...
}
@PostMapping
@PostMapping
是 @RequestMapping
的 POST 请求专用版,其基本语法如下:
@PostMapping("/path")
public responseType handlerMethod() {
// ...
}
@PutMapping
@PutMapping
是 @RequestMapping
的 PUT 请求专用版,其基本语法如下:
@PutMapping("/path")
public responseType handlerMethod() {
// ...
}
@DeleteMapping
@DeleteMapping
是 @RequestMapping
的 DELETE 请求专用版,其基本语法如下:
@DeleteMapping("/path")
public responseType handlerMethod() {
// ...
}
@RequestParam
主要作用是从HTTP请求中获取参数值,并将这些值绑定到控制器方法的参数上。这些参数值可以来自查询字符串(例如,?name=John&age=30
),也可以来自请求体(例如,POST请求的JSON或表单数据)
基本语法如下:
@RequestMapping("/hello")
public String helloWorld(@RequestParam(value = "name", required = false, defaultValue = "World") String name) {
// ...
}
@RequestParam(value = "name")
指定了要从请求中获取名为 "name" 的参数值,required = false
表示这个参数不是必需的,如果没有找到这个参数,那么 name
的值将是 defaultValue
指定的默认值 "World"。
@RequestBody
@RequestBody 的作用
@RequestBody
的主要作用是将HTTP请求体中的数据自动转化为对象实例。这种转化过程通常是基于请求体中的JSON或XML格式数据进行的。
@RequestBody 的使用方法
@RequestBody
通常与 @RequestMapping
或 @PostMapping
等注解一起使用,其基本语法如下:
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody User user){
//@RequestBody User user 指定了要从请求体中获取一个 User 对象。当请求体是JSON格式时,Spring Boot会自动将其转化为 User 对象。
System.out.println(user);
return "OK";
}
@RequestBody 的属性
@RequestBody
有一个主要的属性:
- value:请求参数中的名称。
注意事项
在使用 @RequestBody
时,需要注意以下几点:
@RequestBody
主要用于处理请求体中的数据,因此通常与POST请求一起使用。对于GET请求,由于没有请求体,所以不能使用@RequestBody
。@RequestBody
会将请求体中的JSON或XML格式数据转化为对象实例,这个过程需要依赖Jackson库,因此在项目中需要加入Jackson的依赖。- 在使用
@RequestBody
时,一定要确保参数类型与请求体中的数据类型相符,否则可能会出现数据解析错误的问题。 - @Valid:当你希望对请求体进行验证时,可以在@RequestBody参数前添加@Valid注解。这会触发Spring MVC对请求体中的数据进行校验,校验规则通常由JSR-303/JSR-349/JSR-380 Bean Validation规范定义。
基本示例:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController //标识一个类作为Web层的控制器
public class TestController {
// 无参测试
@RequestMapping(value = "/test")//将HTTP请求映射到test()方法
public String test(){
return "Hello, world!";
}
}
各种参数
简单参数
- 请求参数名与方法形参变量名相同
- 会自动进行类型转换
@RequestMapping("/simpleParam")
public String simpleParam(String name, Integer age) {
log.info("name={}, age={}", name, age);
return "OK";
}
//必须携带name参数
@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam(name="name",required=true) String name, Integer age){
System.out.println(name+ ":" + age);
return "OK";
}
实体参数
- 当简单参数数量较多时,数据不利于维护
- 可以将数据封装到一个实体类中,但是需要保证请求参数名与实体参数名保持一致
简单的实体参数
// 简单的实体类参数绑定测试
@RequestMapping("/simpleEntity")
public String simpleEntity(User user) {
log.info(user.toString());
return "OK";
}
//创建一个entity软件包,用来存放实体类
//src/entity/User.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private Integer age;
}
较复杂的实体参数
@RequestMapping("/complexEntity")
public String complexEntity(User user) {
log.info(user.toString());
return "OK";
}
//src/entity/Address.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address {
private String province;
private String city;
}
//src/entity/User.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private Integer age;
private Address address;
}
数组集合参数
- 数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型参数即可接收参数
- 集合参数:请求参数名与形参数组名称相同且请求参数为多个,需要用注解@RequestParam绑定参数关系
// 数组参数绑定测试
@RequestMapping("/arrayParam")
public String arrayParam(@RequestParam(value = "hobby") String[] hobbies){
log.info(Arrays.toString(hobbies));
return "OK";
}
@RequestMapping("/listParam")
public String listParam(@RequestParam(value = "hobby") List<String> hobbies){
log.info(hobbies.toString());
return "OK";
}
日期参数
- 需要用注解
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
指定日期格式
请求:
http://localhost:8080/dateParam?updateTime=2002-11-11 00:00:00
接收:
@RequestMapping("/dateParam")
public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime) {
System.out.println(updateTime);
return "OK";
}
json参数
- JSON参数:JSON数据键名与形参对象属性名相同,定义POJO类型形参接收参数,需要用
@RequestBody
注解标识
接收:
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody User user){
log.info(user.toString());
return "OK";
}
路径参数
- 路径参数:通过请求URL直接传递参数,使用(...)来标识该路径参数,需要使用
- 路径参数是URL中的一部分,用于标识资源的特定部分或属性。
接收单个路径参数:
@RequestMapping("/path/{id}")
public String pathParam(@PathVariable Integer id) {
System.out.println(id);
return "OK";
}
接收多个路径参数:
@RequestMapping("/path/{id}/{name}")
public String pathParam2(@PathVariable Integer id,@PathVariable String name) {
System.out.println(id+":"+name);
return "OK";
}
响应数据
在前面的例子中,我们都是返回了"Ok"字符串作为响应。
除了字符串,Spring Boot 还可以通过多种方式响应不同的数据格式,例如 JSON、XML、CSV 等。以下是一些常用的方法:
返回 JSON 数据
这是最常见的数据返回方式。在 Spring Boot 中,你可以使用 @RestController
或 @ResponseBody
注解来直接返回对象,这些对象会被自动转换成 JSON 格式。
@RestController
public class MyController {
@GetMapping("/api/users")
public List<User> getUsers() {
return userService.findAll();
}
}
返回 XML 数据
如果你需要返回 XML 格式的数据,你可以使用 @RequestMapping
注解,并设置 produces
属性为 "application/xml"。
@RequestMapping(value = "/api/users", produces = "application/xml")
public List<User> getUsers() {
return userService.findAll();
}
返回 CSV 数据
CSV是一种简单的文件格式,用于存储表格数据,如电子表格或数据库。CSV 文件以纯文本形式存储表格数据,其中每行表示表格中的一行,而行中的每个单元格数据由逗号分隔。
CSV 文件通常用于数据交换,因为它们可以被多种不同的应用程序和系统读取和写入。你可以将电子表格数据导出为 CSV 文件,然后在数据库管理工具中导入这些数据。
要返回 CSV 格式的数据,你可以使用 @ResponseBody
和 ResponseEntity
。
@ResponseBody
@RequestMapping(value = "/api/users/csv", produces = "text/csv")
public ResponseEntity<String> downloadCsv() {
String csvData = convertListToCsv(userService.findAll());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=users.csv")
.body(csvData);
}
其他格式
Spring Boot 还支持其他格式,如 HTML、PDF 等。你可以使用类似的方法,设置相应的 produces
属性,并返回正确的数据格式。
内容协商
Spring Boot 还支持内容协商,即根据请求的 Accept
头部自动选择合适的响应格式。
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserDetailsService userDetailsService;
@GetMapping(produces = { "application/json", "application/xml" })
public ResponseEntity<User> getUser(@RequestParam Long id) {
User user = userDetailsService.findById(id);
return ResponseEntity.ok(user);
}
}
- **使用视图解析器**:如果你的应用需要生成HTML页面,可以使用视图解析器。
在这个例子中,"home"
是一个视图名称,Spring Boot会查找名为"home"
的视图解析器,并将模型中的"greeting"
属性填充到视图中。
- 自定义响应:如果你需要完全控制响应的内容,可以使用
ResponseEntity
类。
在这个例子中,ResponseEntity.ok()
方法设置了响应的状态码为200,.body()
方法设置了响应的内容。
@GetMapping("/")
public String home(Model model) {
model.addAttribute("greeting", "Hello, World!");
return "home";
}
@GetMapping("/users")
public ResponseEntity<List<User>> getUsers() {
return ResponseEntity.ok()
.body(userService.findAll());
}
统一响应结果
在实际开发中,为了确保前端和后端之间的交互更加便捷和统一,需要定义一套标准的响应格式。这样,前端开发者可以预期到后端返回的数据结构,从而简化前端逻辑处理,增强系统的稳定性和可维护性。
下面是一个简单的Result实体类,用来向前端统一响应结果:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {//统一响应结果封装类
private Integer code ;
private String msg;
private Object data;
public static Result success(){
return new Result(1, "success", null);
}
public static Result success(Object data){
return new Result(1, "success", data);
}
public static Result error(String msg){
return new Result(0, msg, null);
}
}
在控制器(Controller)的方法中,根据操作的结果返回统一格式的响应:
@RequestMapping("/hello")
public Result hello() {
return Result.success("Hello, world!");
}
上面只是简单的响应类,实际开发处理响应结果的方式可能更加复杂。
RESTful Web 服务
RESTful Web 服务是一种网络服务的架构风格,它使用 HTTP 协议作为通信手段,并利用 URI 来访问资源,它使用统一的接口和状态无关的请求来构建可扩展的Web服务。
RESTful 的核心思想就是,客户端发出的数据操作指令都是"动词 + 宾语"的结构。比如,GET /articles这个命令,GET是动词,/articles是宾语。
- GET:读取(Read)
- POST:新建(Create)
- PUT:更新(Update)
- PATCH:更新(Update),通常是部分更新
- DELETE:删除(Delete)
RESTful端点
RESTful端点是指用于处理RESTful Web服务请求的特定URL路径。它们是客户端和服务器之间通信的入口点,通过HTTP方法(如GET、POST、PUT、DELETE等)和URL路径来定义对资源的操作。
R
在Spring Boot中,可以使用Spring MVC框架来创建RESTful端点。以下是创建RESTful端点的一般步骤:
- 创建控制器类:创建一个控制器类,使用@Controller或@RestController注解进行标记。通常,使用@RestController注解更方便,因为它将控制器类中的所有方法默认都标记为@ResponseBody,将返回的对象自动序列化为JSON格式。
@RestController
public class MyController {
// 处理GET请求的示例方法
@GetMapping("/users")
public List<User> getAllUsers() {
// 从数据库或其他数据源中获取用户列表
List<User> userList = userService.getAllUsers();
return userList;
}
// 处理POST请求的示例方法
@PostMapping("/users")
public User createUser(@RequestBody User user) {
// 创建新用户的逻辑
User createdUser = userService.createUser(user);
return createdUser;
}
// 处理PUT请求的示例方法
@PutMapping("/users/{id}")
public User updateUser(@PathVariable("id") Long id, @RequestBody User user) {
// 更新用户的逻辑
User updatedUser = userService.updateUser(id, user);
return updatedUser;
}
// 处理DELETE请求的示例方法
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable("id") Long id) {
// 删除用户的逻辑
userService.deleteUser(id);
}
}
- 配置请求映射:使用@GetMapping、@PostMapping、@PutMapping、@DeleteMapping等注解来定义请求映射。这些注解指定了RESTful端点的URL路径和HTTP方法。
- 处理请求参数:根据需要,您可以使用@RequestParam、@PathVariable等注解来获取请求参数的值,并在方法中进行处理。
- 处理响应:根据您的业务需求,可以返回不同类型的响应。如果使用@RestController注解,方法的返回值将自动序列化为JSON格式并作为HTTP响应返回给客户端。
这是一个简单的示例,演示了如何创建基本的RESTful端点。还可以使用其他注解和功能来处理异常、版本控制、分页、过滤等更复杂的场景。
分层架构★
介绍
实际开发中,springboot web程序除了处理请求,还有其他复杂的功能。例如:业务逻辑、数据访问、数据传输、异常处理、登录认证与授权、消息传递、缓存等。
我们不可能将所有功能放在一个程序中,就像我们学习java基础时,将一个个功能从main函数中封装到不同函数和不同文件中一样。我们需要将springboot web程序根据单一职责原则拆分为一个个组件,而这个组件也不过是一个个文件夹(软件包)。
单一职责原则:一个类或一个方法,就只做一件事情,只管一块功能。 这样就可以让类、接口、方法的复杂度更低,可读性更强,扩展性更好,也更利用后期的维护。
那么,我们要如何根据单一职责原则拆分springboot web程序的各个功能呢?分层架构
软件架构风格。
分层架构的好处:
- 每层都有明确的职责,使得应用程序结构清晰,便于开发和维护。
- 此外,分层架构也有利于代码的重用和测试,因为每层的独立性较高,可以单独开发和测试。
分层架构通常包括以下层次:
- 表示层:也称为 UI 层,负责与用户交互,展示数据和接收用户的输入。在 Web 应用程序中,这一层通常由前端技术和控制器(Controllers)组成,它接收前端发送的请求,对请求进行处理,并响应数据。
- 业务逻辑层:也称为服务层,包含应用程序的核心业务逻辑。它处理来自表示层的请求,执行业务规则,并与数据访问层交互。
- 数据访问层:负责与数据库或其他数据源交互,执行数据的持久化操作。这一层通常包含数据访问对象(Repositories)和实体(Entities)。
- 基础设施层:提供支持应用程序运行的基础服务,如数据库连接、消息队列、邮件服务、缓存等。
目前我们只是开发基本的springboot web应用,只需要考虑前三层架构。
我们通常将表示层放在src/contoller
软件包下;业务逻辑层放在src/service
软件包下;数据访问层的数据库操作放在src/mapper
软件包下(关系型数据库),实体类放在src/entity
包下
分层架构的一些核心原则:
- 层次清晰:每个层次都有明确的职责和功能,层次的划分通常基于业务逻辑、数据存储、用户界面等不同的关注点。
- 单一职责:每个层次应该只负责一个特定的功能领域,避免层次的职责重叠或混淆。
- 层次间独立:每个层次应该尽可能地独立于其他层次,这意味着一个层次的变更不应该影响到其他层次。
- 下层对上层透明:下层的实现细节应该对上层隐藏,上层只能通过定义良好的接口与下层交互。
- 层次间通信:层次之间的通信应该是单向的,通常是从上层到下层,这样可以减少层次间的耦合。
- 标准化接口:层次之间的交互应该通过标准化的接口进行,这样可以使得各个层次可以独立开发和测试。
- 数据一致性:在层次之间传递数据时,应该保持数据的一致性,避免出现数据冗余或不一致的情况。
- 可扩展性:分层架构应该支持层次的扩展,允许在不影响其他层次的情况下,增加新的层次或修改现有层次。
- 可维护性:由于层次间的独立性,分层架构通常具有良好的可维护性,每个层次可以独立地进行修改和优化。
- 性能优化:分层架构允许对特定层次进行性能优化,而不影响其他层次,例如,可以在数据访问层优化数据库查询。
- 安全性:可以在不同层次实施不同的安全策略,例如,在表示层实现用户认证和授权,在数据访问层实现数据加密和访问控制。
- 事务管理:事务管理通常在业务逻辑层实现,确保业务操作的原子性、一致性、隔离性和持久性。
- 依赖注入:为了降低层次间的耦合,可以采用依赖注入的方式,使得上层依赖于接口而不是具体的实现。
表示层
表示层(通常是控制器Controller)接收用户的请求,并进行初步的验证,如请求格式、权限验证等。然后,控制器会将请求转发给业务逻辑层相应的服务(Service)进行处理。最后,控制器将处理后的的数据根据需要响应给前端。
前面已经介绍了如何接收与响应请求,下面介绍如何进行初步的验证
在Spring Boot的表示层,即控制器(Controller)中,初步验证通常包括以下几个方面的内容:
- 请求格式验证:
-
- 使用
@Valid
或@Validated
注解结合JSR 380(Bean Validation 2.0)提供的注解(如@NotNull
、@Size
、@Pattern
等)对传入的请求实体(DTO)进行验证。 - 使用
@RequestBody
注解确保接收到的数据是JSON格式,并使用@RequestParam
、@PathVariable
等注解对请求参数进行注解驱动验证。 - 对于查询参数,可以使用
@Min
、@Max
、@DecimalMin
、@DecimalMax
等注解进行数值范围的校验。
- 使用
- 权限验证:
-
- 使用Spring Security等安全框架进行用户身份验证和授权。通过配置安全规则,可以确保只有具有相应权限的用户才能访问特定的API。
- 在控制器方法上使用
@PreAuthorize
、@Secured
等注解来定义方法级别的安全约束。
- 请求方法验证:
-
- 使用
@RequestMapping
、@GetMapping
、@PostMapping
等注解确保请求方法(GET、POST、PUT、DELETE等)与控制器方法定义相匹配。
- 使用
- 跨域请求验证:
-
- 对于跨域请求,可以使用
@CrossOrigin
注解或者在配置类中设置全局的跨域处理规则,以允许或限制跨域请求。
- 对于跨域请求,可以使用
- 自定义验证:
-
- 可以在控制器方法中添加自定义验证逻辑,例如检查请求中的某些属性是否符合特定的业务规则。
- 使用
@ControllerAdvice
或@ExceptionHandler
注解定义全局的异常处理逻辑,以捕获验证失败或其他异常情况,并返回适当的错误响应。
- 异常处理:
-
- 对于验证失败的情况,Spring会抛出
MethodArgumentNotValidException
异常。可以通过定义全局异常处理器来捕获这类异常,并返回统一的错误响应。
- 对于验证失败的情况,Spring会抛出
业务逻辑层
在Spring Boot Web开发中,业务逻辑层
是应用程序的核心,它负责处理来自表示层
的请求,执行业务规则,并与数据访问层
进行交互。
业务逻辑层通常是由一系列的@Service注解的类组成,这些类包含了业务逻辑处理的公共方法和私有方法。这些服务类会注入对应的Repository接口,以进行数据访问操作。通过这样的设计,业务逻辑层为表示层提供了一个清晰的API接口,同时隐藏了数据访问的细节,使得业务逻辑更加集中和易于管理。
以下是业务逻辑层的一般处理流程:
- 接收请求:表示层控制器会将请求转发给业务逻辑层相应的服务(Service)进行处理。
- 参数验证:业务逻辑层接收到请求后,会对传入的参数进行进一步的验证,确保数据的完整性和合法性。这通常涉及到业务规则的校验,如数据格式、范围、依赖关系等。
- 业务处理:验证通过后,业务逻辑层会执行具体的业务操作。这可能包括复杂的数据计算、调用其他服务、执行特定的业务流程等。业务逻辑层的核心目的是实现应用程序的核心业务功能。
- 数据访问:在执行业务操作的过程中,业务逻辑层通常需要与数据访问层进行交互,以读取或写入数据。它会调用数据访问层的接口,而不直接与数据库打交道。这样的设计可以保证业务逻辑与数据访问的解耦,提高系统的可维护性。
- 事务管理:业务逻辑层还需要处理事务管理,确保一组操作的原子性。在Spring Boot中,通常会使用
@Transactional
注解来声明事务边界,保证业务操作的ACID特性。 - 返回结果:业务操作完成后,业务逻辑层会构造相应的响应数据,并将其返回给表示层。如果业务操作失败,业务逻辑层还需要负责处理异常,并将错误信息传递回表示层。
数据访问层
数据访问层比较复杂,可以使用mybatis或Spring Data JPA与数据库进行交互。这里先介绍mybatis,后面在学习spring Data时进一步了解
具体见下面的mybatis章节。
实际开发中,表示层会先接收前端的请求,然后调用业务逻辑层的对应方法;业务逻辑层进行对应处理后,会调用数据访问层,访问和处理相关数据;数据访问层处理后,将处理结果返回给业务逻辑层,业务逻辑层再放回给显示层。
这其实函数的相互的调用和返回,从而将一个冗余的项目根据职责拆分到不同的层中。
这里先做简单的了解,后面会给出相关示例。
MVC 架构★
介绍
MVC(Model-View-Controller)是一种软件设计模式,用于将应用程序的逻辑层和表现层分离。
Spring MVC 的主要组件:
- 模型(Model):模型封装了应用程序的数据和业务逻辑。在 Spring MVC 中,模型通常是一个简单的 POJO(Plain Old Java Object)。
- 视图(View):视图负责展示模型数据。在 Spring MVC 中,视图可以是 JSP、HTML、XML 或其他格式。
- 控制器(Controller):控制器处理用户的请求,并调用模型来处理业务逻辑。然后,它将模型数据传递给视图进行展示。
在Spring Boot中,可以使用Spring MVC框架来实现MVC模式。Spring MVC提供了一组注解和类,用于定义和处理RESTful API的请求映射、请求参数绑定、数据验证、响应处理等。
分层架构与MVC架构的区别与联系:
- 分层架构是一种宏观的架构风格,它涵盖了整个应用程序的结构;而 MVC 是一种微观的设计模式,它主要关注于用户界面的设计。
- 在分层架构中,表示层可以采用 MVC 模式来实现,而业务逻辑层、持久层和数据库层则遵循分层架构的其他原则。
- RESTful Web 服务是实现网络通信的一种方式,分层架构提供了一种组织代码的宏观视角,而 MVC 架构则是在表示层实现代码的一种具体模式。
实现
在Spring Boot中使用Spring MVC框架来实现MVC模式需要以下步骤:
- 添加Spring MVC依赖:在您的Spring Boot项目的构建文件(例如pom.xml)中添加Spring MVC的依赖项。可以使用Maven或Gradle构建工具来管理依赖关系。
- 创建控制器类:创建一个控制器类,使用@Controller注解进行标记。控制器类负责处理来自客户端的请求,并根据逻辑进行处理。
@Controller
public class MyController {
// 处理GET请求的示例方法
@GetMapping("/hello")
public String sayHello() {
return "hello";
}
}
- 创建视图模板:为了展示用户界面,您可以创建一个视图模板。在Spring Boot中,默认使用Thymeleaf作为模板引擎,您可以使用Thymeleaf的语法来创建视图模板。
- 配置视图解析器:在应用程序的配置文件中,配置视图解析器,以将逻辑视图名称解析为实际的视图模板文件。
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.html
- 处理请求映射:在控制器类中,使用@RequestMapping或其他相关注解来定义请求映射。这些注解指定了URL路径和HTTP方法与处理方法的关联。
@Controller
public class MyController {
@GetMapping("/hello")
public String sayHello() {
return "hello";
}
}
- 处理请求参数:在处理方法中,您可以使用@RequestParam注解或其他相关注解来获取请求参数的值,并在方法中进行处理。
@Controller
public class MyController {
@GetMapping("/hello")
public String sayHello(@RequestParam("name") String name, Model model) {
model.addAttribute("name", name);
return "hello";
}
}
- 处理响应:处理方法可以返回视图名称、视图模型、JSON数据或其他类型的响应。根据您的业务需求,选择适当的返回类型。
MyBatis
介绍
- jdbc 缺点:
-
- 硬编码,代码量大
- 操作繁琐
- 资源浪费,性能低
MyBatis是一个强大的持久层框架,它内部封装了对JDBC的操作,让开发者只需要关注SQL本身,而不需要处理繁琐的数据库连接、SQL构造、结果集处理等JDBC代码。
- 持久层:指的是就是数据访问层(dao),是用来操作数据库的。
以下是使用MyBatis的数据访问层的工作流程:
- 配置MyBatis:在Spring Boot项目中,首先需要在
application.properties
或application.yml
文件中配置MyBatis的相关设置,如mapper文件的位置、数据源信息等。 - 定义实体类:与JPA类似,需要定义实体类(Entity Classes),这些类映射到数据库中的表。MyBatis可以使用注解或XML配置文件来指定映射关系。
- 创建MyBatis的XML映射文件或使用注解:定义Mapper接口,这些接口的方法对应于数据库操作。每个方法通常对应一个SQL语句。然后,创建XML配置文件或使用注解来编写SQL语句和映射结果。MyBatis还支持动态SQL,可以根据条件灵活构建SQL语句。
- 事务管理:MyBatis的事务管理可以通过Spring的声明式事务管理来实现。在Spring Boot中,可以使用
@Transactional
注解来声明事务边界。 - 异常处理:MyBatis在执行SQL操作时可能会遇到异常,如SQL执行错误、参数错误等。这些异常可以在Service层或Controller层捕获并转换为业务逻辑层可以理解的异常。
参数占位符
#{...}
-
- 执行sql时,会将
#{...}
替换为?
,生成预编译sql,会自动设置参数值 - 优势:性能更高,更安全
- 参数传递都使用
- 执行sql时,会将
${...}
-
- 拼接sql,直接将参数拼接在sql中,存在sql注入问题
- 对表名、列表进行动态设置时使用
Spring Boot整合MyBatis进行数据库操作时,可以使用注解或者XML文件来编写SQL语句。
- 对于简单的SQL操作,推荐使用注解,因为它更加简洁、直观。
- 对于复杂的SQL操作,尤其是需要使用动态SQL(可变sql)的场景,推荐使用XML文件,因为它更加灵活、易于维护。
IDEA推荐安装插件mybatisX,方便xml映射文件操作。
基本操作
- 目标
-
- 使用Mybatis对mysql数据库进行增删改查
准备
创建数据库、表,配置springboot工程
- 在Navicat中创建数据库mybatis
- 创建student表
CREATE TABLE students (
id INT PRIMARY KEY AUTO_INCREMENT, -- 学生ID,主键,自动递增
name VARCHAR(50) NOT NULL, -- 学生姓名,不为空
gender ENUM('男', '女') NOT NULL, -- 学生性别,枚举类型,不为空
age INT, -- 学生年龄
class_name VARCHAR(50) -- 学生所在班级名称
);
INSERT INTO students (name, gender, age, class_name) VALUES ('张三', '男', 18, '高三一班');
INSERT INTO students (name, gender, age, class_name) VALUES ('李四', '男', 17, '高三二班');
INSERT INTO students (name, gender, age, class_name) VALUES ('王五', '女', 18, '高三一班');
INSERT INTO students (name, gender, age, class_name) VALUES ('赵六', '女', 17, '高三二班');
- /src/main/java/com.example.demo(你的包名)下创建service,controller,mapper,entity包,用于三层架构
/src/main/resouces下创建mapping文件夹,用于存放xml映射文件
- 在pom.xml中引入依赖
...
<dependencies>
...
<!-- mysql依赖 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
</dependencies>
...
- 配置MyBatis(数据库连接信息)(在application.yml中)
spring:
datasource:
# 驱动
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据库地址
# url: jdbc:mysql://localhost:3306/mybatis # 可以简写为这个,其中mybatis是数据库名称
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
# 用户名
username: root
# 密码
password: 123456
mybatis:
configuration:
# 驼峰命名
map-underscore-to-camel-case: true
?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
- 这些是连接数据库时使用的参数。
- useUnicode=true 指定了使用Unicode字符集。
- characterEncoding=utf-8 指定了使用UTF-8编码。
- useSSL=false 指定了不使用SSL加密。
- serverTimezone=Asia/Shanghai 指定了服务器时区为Asia/Shanghai(中国),这通常用于解决时区相关问题。
- 其他配置
配置日志输出到控制台:
#配置mybatis日志,指定输出到控制台(记住log即可)
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
连接数据库
- 在entity包下创建实体类Student.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private int id;
private String name;
private int age;
private char gender;
private String className;
}
注意,实体类的字段要和数据库的字段对上号,即名称和类型要一致,否则会报错。
其中,实体类名称和数据库名称一般会配置驼峰名称转换,例如上面的数据库中的class_name会对应实体类className。而如果不配置驼峰命名转换就会报错。
注解操作
在mapper包下创建一个StudentMapper.java文件,用于数据库操作。
使用两个注解:
- @Mapper注解用于标记MyBatis的映射器接口。当Spring Boot项目启动时,它会自动扫描带有@Mapper注解的接口,并创建它们的全局代理实例。
- @Repository注解是Spring框架提供的一个注解,用于标记数据访问层的组件。当Spring容器扫描到带有@Repository注解的接口时,它会自动创建其代理实例,并实现基于接口的依赖注入。
查询
在数据层访问层方法前添加 @Select
注解,在注解在传递sql语句进行查询
@Mapper
@Repository
public interface StudentMapper {
@Select("SELECT * FROM students WHERE gender = '女';")
public List<Student> findGirl() ;
@Select("SELECT * FROM students WHERE id = #{id};")
List<Student> findById(int id);//不加public也可,因为interface中的方法都是公用的
}
在/test包下的测试类测试
@SpringBootTest
class XXXApplicationTests {
@Autowired
public StudentMapper studentMapper;
@Test
void getGirl() {
List<Student> students = studentMapper.findGirl();
for (Student student : students) {
System.out.println(student);
}
}
}
插入
添加 @INSERT
注解,在注解在传递sql语句进行插入
@Insert("INSERT INTO students(name,gender,age,class_name) values (#{name},#{gender},#{age},#{className})" )
public void insertStudent(Student student);
进行测试
@Test
void insertStudent() {
Student student = new Student();
student.setName("FL");
student.setGender('男');
student.setAge(20);
student.setClassName("计算机");
if (studentMapper.insertStudent(student) > 0) {
System.out.println("插入成功");
}else {
System.out.println("插入失败");
}
}
- 主键返回
在Mapper.java添加注解Option就可以返回id了
// 插入
@Options(useGeneratedKeys = true,keyProperty = "id")
删除
加 @Delete
注解,在注解在传递sql语句进行删除
@Delete("DELETE FROM students WHERE id = #{id};")
public int deleteById(int id);
进行测试
@Test
void deleteStudent() {
if (studentMapper.deleteById(5) > 0) {
System.out.println("删除成功");
}else {
System.out.println("删除失败");
}
}
修改
加 @Update
注解,在注解在传递sql语句进行修改
@Update("UPDATE students SET class_name = #{className} WHERE id = #{id};")
public int updateClassName(int id, String className);
测试
@Test
void updateStudent(){
if (studentMapper.updateClassName(3,"高三二班") > 0) {
System.out.println("更新成功");
}else {
System.out.println("更新失败");
}
}
数据封装
- 如果实体类属性名和数据库表查询返回的字段名一致,就会自动封装,否则不会自动封装
- 解决方法:
-
- 给字段取别名
- 使用注解
@Results
和@Result
手动封装 - 开启mybatis驼峰命名自动映射
//法一:给字段取别名
@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 getById1(Integer id);
//法二: 使用注解手动封装
@Results({
@Result(column= "dept_id", property = "deptId"),
@Result(column= "create_time", property = "createTime"),
@Result(column= "update_time", property = "updateTime")
})
@Select("select id, username, password, name, gender, image, job, entrydate, " +
"dept_id, create_time, update_time from emp where id = #{id}")
public Emp getById2(Integer id);
法三:
在application.yml中设置(推荐)
#开启mybatis驼峰命名自动映射 (记住camel)
mybatis:
configuration:
# 驼峰命名
map-underscore-to-camel-case: true
数据库连接池
介绍
- 数据库连接池是个容器,负责分配、管理数据库连接(Connection)
- 程序在启动时,会在数据库连接池(容器)中,创建一定数量的Connection对象 ,允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个
- 客户端在执行SQL时,先从连接池中获取一个Connection对象,然后在执行SQL语句,SQL语句执行完之后,释放Connection时就会把Connection对象归还给连接池(Connection对象可以 复用)
- 释放空闲时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏
- 数据库连接池的好处:
-
- 资源重用
- 提升系统响应速度
- 避免数据库连接遗漏
- 标准接口:
-
- 官方(sun)提供了数据库连接池标准(javax.sql.DataSource接口)
- 功能:获取连接
public Connection getConnection() throws SQLException;
- 常见产品
-
-
- Hikari (springboot默认)
- Druid (智能、准确、误报率低)
-
- 可以在控制台查看使用的连接池
切换数据库连接池
- 在pom.xml中引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>最新版本</version>
</dependency>
- 在application.yml中配置数据库信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource # type可以指定数据源类型
mybatis:
configuration:
map-underscore-to-camel-case: true
Druid数据源专有配置
常用的Druid数据源专有配置:
- initialSize:初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时。
- minIdle:最小连接池数量。
- maxActive:最大连接池数量。
- maxWait:获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock为true使用非公平锁。
- timeBetweenEvictionRunsMillis:间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒。
- minEvictableIdleTimeMillis:一个连接在池中最小生存的时间,单位是毫秒。
- validationQuery:用来检测连接是否有效的SQL,要求是一个查询语句,常用SELECT 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
- testWhileIdle:建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
- testOnBorrow:申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
- testOnReturn:归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
- poolPreparedStatements:是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说Oracle。在MySQL下建议关闭。
- maxPoolPreparedStatementPerConnectionSize:poolPreparedStatements为true时生效,在maxOpenPreparedStatements和maxPoolPreparedStatementPerConnectionSize中,优先生效maxPoolPreparedStatementPerConnectionSize。
- filters:属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:stat(统计)、log4j(日志)、wall(防火墙)。
- connectionProperties:属性类型是字符串,通过别名的方式配置连接属性,比如配置druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000表示合并SQL统计,并且记录慢SQL。
示例:
spring:
datasource:
# 前面配置省略
type: com.alibaba.druid.pool.DruidDataSource
initialSize: 5
minIdle: 5
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
XML映射文件
基本操作
示例:
- 修改application.yml,配置mybatis映射
mybatis:
configuration:
map-underscore-to-camel-case: true
# 配置mapper xml文件所在路径
mapper-locations: classpath:mapping/*.xml
# 配置实体类所在位置
type-aliases-package: com.fl.boot.entity
- 在前面注解操作的基础上,去除StudentMapper.java方法上的所有注解
@Mapper
@Repository
public interface StudentMapper {
public List<Student> findGirl() ;
public List<Student> findById(int id);
public int insertStudent(Student student);
public int deleteById(int id);
public int updateClassName(int id, String className);
}
- 在mapping包下创建StudentMapper.xml(与StudentMapper.java接口一致)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 上面的依赖可以从mybatis中文网复制 -->
<!-- namespace属性为Mapper接口全限定名一致 -->
<mapper namespace="com.fl.boot.mapper.StudentMapper">
</mapper>
- 在StudentMapper.xml的
<mapper>
中编写sql
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 上面的依赖可以从mybatis中文网复制 -->
<!-- namespace属性为Mapper接口全限定名一致 -->
<mapper namespace="com.fl.boot.mapper.StudentMapper">
<!-- id为Mapper接口中对应的方法名,resultType为返回的实体全类名 -->
<select id="findGirl" resultType="com.fl.boot.entity.Student">
SELECT * FROM students WHERE gender = '女'
</select>
<select id="findById" resultType="com.fl.boot.entity.Student">
SELECT * FROM students WHERE id = #{id}
</select>
<insert id="insertStudent" parameterType="com.fl.boot.entity.Student">
insert into student(id,sname,classId,birthday,email)
values (#{id},#{sname},#{classId},#{birthday},#{email});
</insert>
<delete id="deleteById" parameterType="int">
DELETE FROM students WHERE id = #{id}
</delete>
<update id="updateClassName" >
UPDATE students SET class_name = #{className} WHERE id = #{id};
</update>
</mapper>
讲解:
<mapper>
标签的namespace
属性用于指定Mapper接口,必须传入全限定名- 使用
<insert>
、<delete>
、<update>
、<select>
进行增删改查 - (公有)
id
属性用于指定Mapper接口中的方法,必须和要指定的方法一致 - (公有)
parameterType
:指定输入参数的类型,可以是简单类型、Map、POJO等。注意,参数只能传入一个,如果要传入多个参数,需要在Mapper.java中指定参数
如前面的updateClassName方法,需要修改为:
public int updateClassName(@Param("id") int id, @Param("className") String className);
<insert>
特性:
-
- useGeneratedKeys:设置为true时,MyBatis会使用JDBC的getGeneratedKeys方法来获取由数据库自动生成的主键值(如自增字段)。
- keyProperty:指定主键属性,当useGeneratedKeys设置为true时,MyBatis会将获取到的主键值赋给这个属性。
- keyColumn:指定主键列名,与keyProperty配合使用。
<delete>
和<update>
特性(只有两个共有属性)<select>
特性
-
- resultType:指定查询结果的类型,可以是简单类型、Map、POJO等。
- resultMap:用于引用外部定义的<resultMap>元素,用于复杂的结果映射。
- fetchSize:指定数据库驱动程序每次批量返回的结果行数。
- timeout:设置超时时间,等待数据库返回结果的秒数。
在Test类进行测试:
@SpringBootTest
class XXXApplicationTests {
@Autowired
public StudentMapper studentMapper;
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Test
void getGirl() {
List<Student> students = studentMapper.findGirl();
for (Student student : students) {
System.out.println(student);
}
}
@Test
void insertStudent() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try{
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
Student student = new Student();
student.setName("FL");
student.setGender('男');
student.setAge(20);
student.setClassName("计算机");
if (studentMapper.insertStudent(student) > 0) {
System.out.println("插入成功");
}else {
System.out.println("插入失败");
}
sqlSession.commit();
}
finally {
sqlSession.close();
}
}
@Test
void deleteStudent() {//输出删除失败
//直接获取操作数目
if (studentMapper.deleteById(5) > 0) {
System.out.println("删除成功");
}else {
System.out.println("删除失败");
}
}
@Test
void updateStudent(){
if (studentMapper.updateClassName(4,"高三一班") > 0) {
System.out.println("更新成功");
}else {
System.out.println("更新失败");
}
}
}
补充:复制全类名
- 右键需要复制全类名的实体类文件(如Student.java)
- 点击复制类名/引用
- 点击复制引用
- 快捷键:ctrl+alt+shift+c
关系映射
在xml映射文件中,可以用 <resultMap>
定义数据库表记录和Java对象之间的映射关系。
可以使用<association>处理一对一的关系,即一个Java对象的属性对应另一个Java对象。对于一对多的关系,应该使用<collection>元素。
<resultMap id="commodityResultMap" type="com.fl.boot.entity.Commodity">
<id column="Pid" property="pid" />
<result column="Tcode" property="tcode" />
<result column="Scode" property="scode" />
<result column="Pname" property="pname" />
<result column="PPrice" property="pPrice" />
<result column="Stocks" property="stocks" />
<result column="TextAdv" property="textAdv" />
<result column="LivePriority" property="livePriority" />
<result column="Evaluate" property="evaluate" />
<result column="UpdateTime" property="updateTime" />
</resultMap>
<id>
为数据表的主键, <result>
为其他结果列, column
为数据表的列名,property
为实体类的属性名
在MyBatis的<select>或其他查询语句中,你可以使用resultMap属性来引用这个<resultMap>,而不是直接在SQL语句中指定列名和属性名的对应关系。
动态sql
介绍
前端可能有上面这样的筛选列表,那么发送的请求中,可能没有参数,这时我们需要将所有数据返回给前端,而如果含有一个或多个参数,这时我们就需要返回特定筛选条件的数据。我们可以使用动态SQL处理这样参数会变化的SQL语句。
动态SQL是指根据程序运行时的条件动态生成的SQL语句。MyBatis通过XML映射文件或注解提供了一系列强大的动态SQL功能,允许你根据不同的条件构建不同的SQL语句。这使得MyBatis能够灵活地应对复杂的数据库操作需求。
动态SQL的主要特点是能够在SQL语句中包含条件判断、循环和表达式计算等逻辑,从而实现SQL的动态构建
动态SQL元素
MyBatis提供了多种动态SQL元素:
<where>:用于生成WHERE子句,如果所有条件都不满足,则不会生成WHERE子句,同时还会去除多余的and或or 。
<if>:根据条件判断包含SQL片段,使用test属性进行条件判断/如果条件为true,则拼接SQL 。可以单独使用,适用于简单的条件判断。
<choose>、<when>、<otherwise>:类似于Java中的if-else语句,用于选择性地包含SQL片段。不能单独使用,适用于需要多个条件选择的情况。<when>相当于if语句,<otherwise>相当于else语句。
示例:查询学生,可以指定性别,名字的姓,年龄和班级
- 在StudentMapper.java中定义方法
//将int类型改为Integer,char类型改为Character,否则不能为null
public List<Student> findStudent(@Param("name") String name,@Param("gender") Character gender,@Param("age") Integer age,@Param("className") String className);
- 修改StudentMapper.xml,添加xml映射
<select id="findStudent" resultType="com.fl.boot.entity.Student">
SELECT * FROM students
<where>
<if test="name != null">
name LIKE CONCAT('%',#{name},'%')
</if>
<if test="gender != null">
AND gender = #{gender}
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="className != null ">
AND class_name = #{className}
</if>
</where>
</select>
或
<select id="findStudent" resultType="com.fl.boot.entity.Student">
SELECT * FROM students
<where>
<choose>
<when test="name != null">
name LIKE CONCAT('%',#{name},'%')
</when>
<when test="gender != null">
AND gender = #{gender}
</when>
<when test="age != null">
AND age = #{age}
</when>
<when test="className != null ">
AND class_name = #{className}
</when>
<otherwise>
AND 1=1
</otherwise>
</choose>
</where>
</select>
测试:
@Test
void findStudent() {
List<Student> students = studentMapper.findStudent(null, '男', null, "高三一班");
for (Student student : students) {
System.out.println(student);
}
System.out.println("===============SUCCESS!================");
}
<foreach>:用于遍历集合,生成批量SQL语句,如IN查询。
- foreach 属性:
-
- collection:遍历的集合
item:遍历出来的元素
separator:分隔符
open:遍历开始前拼接的sql片段
close:遍历结束后的sql片段
- collection:遍历的集合
- 定义方法
public List<Student> findStudentByIdList(@Param("ids") List<Integer> idList);
- 添加xml映射
<select id="findStudentByIdList" resultType="com.fl.boot.entity.Student">
SELECT * FROM students
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
- 测试
@Test
void findStudentByIdList() {
List<Integer> idList = List.of(1, 2, 3);
List<Student> students = studentMapper.findStudentByIdList(idList);
for (Student student : students) {
System.out.println(student);
}
System.out.println("===============SUCCESS!================");
}
<set>:用于动态生成UPDATE语句的SET子句,只更新改变的字段。
<update id="updateStudent" parameterType="com.fl.boot.entity.Student">
UPDATE students
<set>
<if test="name != null">
name = #{name},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="gender">
gender = #{gender},
</if>
<if test="className">
class_name = #{className},
</if>
</set>
WHERE id = #{id}
</update>
<trim>
元素用于自定义字符串的截取规则,可以用来去除或添加前缀、后缀,以及根据需要包含或忽略指定的字符串。它可以用于WHERE子句,也可以用于SET子句或其他任何需要根据条件动态生成SQL的部分。
<trim>提供了更多的控制选项,但因此也更为复杂。<where>则是一种专门为WHERE子句设计的简化元素。
<trim>的控制选项:
- prefix:指定添加到生成的SQL片段之前的前缀。
- 示例:<trim prefix="(" suffix=")" suffixOverrides=",">
- suffix:指定添加到生成的SQL片段之后的后缀。
- 示例:<trim prefix="(" suffix=")" suffixOverrides=",">
- prefixOverrides:指定需要被前缀替换掉的字符串。
- 示例,将所有的逗号都替换为左括号:<trim prefix="(" prefixOverrides="," suffix=")" suffixOverrides=",">
- suffixOverrides:指定需要被后缀替换掉的字符串。
- 示例,将所有的逗号都替换为右括号:<trim prefix="(" suffix=")" suffixOverrides=",">
示例:将前面的findStudent的where改为使用trim
<select id="findStudent" resultType="com.fl.boot.entity.Student">
SELECT * FROM students
<trim prefix="WHERE" prefixOverrides="AND |OR">
<if test="name != null">
name LIKE CONCAT('%',#{name},'%')
</if>
<if test="gender != null">
AND gender = #{gender}
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="className != null ">
AND class_name = #{className}
</if>
</trim>
</select>
<sql><include>
作用:将重复的sql提取成可以重用的sql片段<sql>
:用于定义可以在其他SQL元素中重用的SQL片段。它通常用于定义那些在多个查询中重复出现的代码,比如表名、列名或者复杂的计算表达式。你甚至可以使用它定义整个SQL语句,不过实际开发中用的很少。<include>
:通过属性refid,指定包含的sql片段
在下面的例子里,使用<sql>片段替换了搜索列 *
。
<sql id="selectColumns">
id,name,age,gender,class_name
</sql>
<select id="findGirl" resultType="com.fl.boot.entity.Student">
SELECT <include refid="selectColumns"/>
FROM students
WHERE gender = '女'
</select>
事务管理
介绍
在数据库阶段我们已学习过事务了,我们知道:
事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。确保了一系列数据库操作要么全部成功执行,要么全部失败回滚,从而保证了数据的一致性和完整性。
事务的操作主要有三步:
- 开启事务(一组操作开始前,开启事务):start transaction / begin ;
- 提交事务(这组操作全部成功后,提交事务):commit ;
- 回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;
Spring Boot和MyBatis的整合简化了传统的事务管理方式,使得开发者能够更容易地实现事务控制,同时保持代码的简洁性和可维护性。
@Transactional注解
@Transactional作用:就是在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。
@Transactional注解:我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。
@Transactional注解书写位置:
- 方法
-
- 当前方法交给spring进行事务管理
- 类
-
- 当前类中所有的方法都交由spring进行事务管理
- 接口
-
- 接口下所有的实现类当中所有的方法都交给spring 进行事务管理
常在业务方法上加上 @Transactional 来控制事务
在yml配置文件中开启事务管理日志
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
事务管理的步骤
下面是Spring Boot使用MyBatis进行事务管理的步骤:
- 开启事务管理:
在Spring Boot应用的主类或者配置类上添加@EnableTransactionManagement注解,开启事务管理功能。
@SpringBootApplication
@EnableTransactionManagement
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- 使用@Transactional注解并执行数据库操作:
在需要事务管理的Service层方法上添加@Transactional注解,Spring会自动管理事务的提交和回滚。
在Service层方法中,通过注入的Mapper接口调用数据库操作方法。
@Service
public class StudentService {
@Autowired
public StudentMapper studentMapper;
//默认情况下,只有出现RunTimeException才回滚事务,rollbackFor属性用于控制出现指定异常类型,都回滚事务
@Transactional(rollbackFor = Exception.class)
public void updateStudent(int id, String name, Integer age, Character gender, String className) {
studentMapper.updateStudent(id, name, age, gender, className);
//模拟异常
int i = 1 / 0;
}
}
- 处理异常和事务回滚:
如果方法执行过程中发生异常,Spring会检测到并回滚事务,确保数据的一致性。
测试:
@Test
void updateStudent2(){
studentService.updateStudent(7,"蔡徐坤",null,null,"高三三班");
}
注意,如果去掉步骤2的 @Transactional注解,那么即使报错该sql语句也会执行。而添加后开启了事务管理,报错后会回滚。
异常处理
介绍
前面的StudentService.java中,我们使用1 / 0模拟了异常,实际使用mybatis时,会有其他sql特有异常,例如插入重复索引的数据,或删除不存在的数据等
常见异常:
- SQLException:这是最基本的异常,代表数据库级别的错误。当执行数据库操作时出现任何问题时,MyBatis 可能会抛出这个异常。
- SQLDataException:当尝试将不兼容的数据类型插入表中,或者尝试对字符串使用非法数值操作时抛出。
- SQLIntegrityConstraintViolationException:当违反了完整性约束(如主键约束、唯一约束或外键约束)时抛出。
- SQLInternalError:由数据库抛出的内部错误,通常是数据库服务器的错误。
- SQLNonTransientConnectionException:当无法打开数据库连接时抛出,并且错误是持久的,不是临时的。
- SQLSyntaxErrorException:当 SQL 语法错误发生时抛出。
- SQLTimeoutException:当数据库操作超时时抛出。
- SQLTransactionRollbackException:当发生事务回滚时抛出。
- SQLTransientConnectionException:当无法打开数据库连接时抛出,并且错误是临时的。
我们可以在Test类添加try...catch...语句,可是以后业务一旦复杂起来,需要这样处理的方法多了怎么办呢?要去一个一个try-catch吗?多麻烦啊!
所以,我们使用异常拦截器进行全局异常捕获。
步骤
- 在src/main/java/com.fl.boot包下创建一个exception包(与三层架构同级),创建一个GlobalExceptionHandler.java文件
- 添加注解
-
- 全局异常处理:通过在 @ControllerAdvice 标注的类中定义方法,并使用 @ExceptionHandler 注解指定要处理的异常类型,使用value属性指定要捕获的异常类型。
@RestContrllorAdvice
是 @ControllerAdvice 的特殊化,它还包括了 @ResponseBody 注解(@RestContrllorAdvice
=@ContrllorAdvice
+@RespponseBody
),使得返回值直接作为响应体,无需视图解析。
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value =Exception.class)
@ResponseBody
public String exceptionHandler(Exception e){
System.out.println("全局异常捕获>>>:"+e);
return "全局异常捕获,错误原因>>>"+e.getMessage();
}
}
- 在contoller包下编写一个StudentContoller.java,用于接收请求
@RestController
public class StudentContoller {
@Autowired
public StudentService studentService;
@GetMapping("/updateStudent")
public String updateStudent(int id, String name, Integer age, Character gender, String className) {
//调用service层updateStudent方法,因为该方法存在算数异常,会被全局异常处理器捕获并处理
studentService.updateStudent(id, name, age, gender, className);
return "更新成功";
}
}
- 使用postman发送请求
http://localhost:8080/updateStudent?id=4&name=FL
- 观察控制台:
注意,如果直接使用测试类进行测试,该错误不会被全局异常处理器捕获
@Test
void updateStudent2(){
studentService.updateStudent(7,"蔡徐坤",null,null,"高三三班");
}
Mybatis-plus
配置
springboot3.2配置如下(其他版本可能不同)
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
创一个application.yml,进行配置:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
# mybatis-plus配置
mybatis-plus:
configuration:
# 下划线转驼峰,默认情况下mybatis-plus是开启的,而mybatis是关闭的
map-underscore-to-camel-case: true
# 日志配置
logging:
level:
包名.mapper: debug
常用注解
MyBatis-Plus 是在 MyBatis 的基础上进行了增强的 ORM 框架,它简化了 CRUD 操作,并提供了一些额外的特性来增强开发体验。
以下是一些常用的 MyBatis-Plus 注解:
在实体类中:
@TableName
:用于指定实体类映射的数据库表名。
@TableName("t_user")
public class User {
// ...
}
@TableId
:用于指定实体类的主键字段,支持多种主键类型,如自增、雪花算法等。
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField
:用于指定实体类字段与数据库表列的映射关系。
@TableField(value = "username", fill = FieldFill.INSERT)
private String username;
@TableLogic
:用于指定实体类的逻辑删除字段,支持自动填充逻辑删除值。
@TableLogic
private Integer deleted;
@EqualsAndHashCode
:简化 equals 和 hashCode 方法的实现,因为 MyBatis-Plus 会自动根据实体类的字段生成这两个方法的实现。这样,你就不需要手动编写这些方法的实现,也不需要处理继承关系。
@EqualsAndHashCode(callSuper = false)
public class MyEntity extends AnotherEntity {
// ...
}
在数据层中:
@Select
:用于自定义 SQL 查询语句,通常用于自定义查询、更新、删除等操作。
@Select("SELECT * FROM t_user WHERE id = #{id}")
User selectById(Long id);
@Update
:用于自定义 SQL 更新语句。
@Update("UPDATE t_user SET username = #{username} WHERE id = #{id}")
boolean updateUserName(User user);
@Delete
:用于自定义 SQL 删除语句。
@Delete("DELETE FROM t_user WHERE id = #{id}")
boolean deleteById(Long id);
@Insert
:用于自定义 SQL 插入语句。
@Insert("INSERT INTO t_user(username, password) VALUES(#{username}, #{password})")
boolean insertUser(User user);
@SelectProvider
:用于自定义 SQL 查询语句,通过提供者方法动态生成 SQL 语句。
@SelectProvider(type = UserProvider.class, method = "selectUser")
List<User> selectUsers();
@TableField(exist = false)
:用于指定实体类字段在数据库表中是否存在,如果设置为false
,则该字段不会映射到数据库表中。@TableField(fill = FieldFill.UPDATE)
:用于指定实体类字段在数据库表中的自动填充策略,如果设置为UPDATE
,则该字段在更新操作时会自动填充。
基本使用
准备数据库:
CREATE DATABASE shoppingdb;
USE shoppingdb;
CREATE TABLE `t_goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`price` bigint(20) NULL DEFAULT NULL,
`pubdate` date NULL DEFAULT NULL,
`typeName` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`intro` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`picture` varchar(150) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`flag` int(11) NULL DEFAULT NULL COMMENT '1上架 2下架',
`star` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `t_goods` VALUES (1, '可可可乐', 6600, '2018-11-25', '酒水饮料', '巴厘岛原装进口 百事可乐(Pepsi) blue 蓝色可乐 网红可乐汽水饮料 450ml*4瓶装', '201811/7b001eee-38df-4c66-9a0f-350879007402_js1.jpg', 1, 5);
INSERT INTO `t_goods` VALUES (2, '易拉罐可可可乐', 8800, '2018-11-25', '酒水饮料', '日本原装进口 可口可乐(Coca-Cola)碳酸饮料 500ml*6罐,味道谁喝谁知道!', '201811/f65d85f4-a622-4f6b-bbdf-2e354d7b0737_js2.jpg', 1, 5);
INSERT INTO `t_goods` VALUES (3, '干红', 99900, '2018-11-25', '酒水饮料', '自营张裕(CHANGYU)红酒 张裕干红葡萄酒750ml*6瓶(彩龙版),', '201811/fa61ef77-4adc-4895-962b-fcb084e3e809_js3.jpg', 1, 4);
INSERT INTO `t_goods` VALUES (4, '进口红酒', 99900, '2018-11-25', '酒水饮料', '法国进口红酒 拉菲(LAFITE)传奇波尔多干红葡萄酒 双支礼盒装带酒具 750ml*2瓶', '201811/cb233c79-2f18-4f97-afad-0d2079098345_js4.jpg', 1, 3);
INSERT INTO `t_goods` VALUES (5, '草莓饼干', 8800, '2018-11-25', '饼干糕点', '土斯(Totaste) 葡萄味夹层饼干(含葡萄果粒) 休闲零食蛋糕甜点心 实惠分享装360g', '201811/afdd4cd4-9782-46a5-96ed-0ba3c4036379_bg1.jpg', 1, 5);
INSERT INTO `t_goods` VALUES (6, '蔬菜棒', 10100, '2018-11-25', '饼干糕点', '土斯(Totaste) 混合蔬菜味棒形饼干 酥脆可口 独立包装 休闲零食蛋糕甜点心小吃 128g', '201811/8bdbdb3f-4cb6-4af8-9c6f-183411537726_bg2.jpg', 1, 4);
INSERT INTO `t_goods` VALUES (7, '曲奇', 24400, '2018-11-25', '饼干糕点', '丹麦进口 皇冠(danisa)丹麦曲奇精选礼盒装908g(新老包装随机发货)', '201811/db2a101d-600a-44c5-8d0f-0a3b173f81aa_bg3.jpg', 1, 5);
INSERT INTO `t_goods` VALUES (8, '夹心饼干', 6600, '2018-11-25', '饼干糕点', '马来西亚原装进口 茱蒂丝Julie\'s雷蒙德巧克力榛果夹心饼干180g×2', '201811/3b047c04-6b23-491d-bf9c-5405cf36c308_bg4.jpg', 1, 5);
INSERT INTO `t_goods` VALUES (9, '玉米棒', 1800, '2018-11-25', '休闲零食', '印尼进口 Nabati 丽芝士(Richeese)雅嘉 休闲零食 奶酪味 玉米棒 400g/盒 早餐下午茶', '201811/287f1938-f039-4d24-9942-3c8a456d757b_ls1.jpg', 1, 5);
INSERT INTO `t_goods` VALUES (10, '千层酥', 880, '2018-11-25', '休闲零食', '葡韵手信 澳门特产 休闲零食 传统糕点小吃 千层酥150g 新旧包装随机发货', '201811/7570a0dc-eacb-4b61-9085-966ac322172f_ls2.jpg', 1, 5);
INSERT INTO `t_goods` VALUES (11, '海苔', 990, '2018-11-25', '休闲零食', '泰国原装进口休闲零食 老板仔 超大片烤海苔脆紫菜 经典原味 54g/袋(新老包装随机发货)', '201811/62c5370b-2f32-450f-ba4c-401a004d5270_ls3.jpg', 1, 5);
INSERT INTO `t_goods` VALUES (12, '提子干', 4400, '2018-11-25', '休闲零食', '三只松鼠无核白葡萄干蜜饯果干休闲食品新疆特产提子干120g/袋', '201811/4e807511-5515-4ec8-9e20-41e8f49ece66_ls4.jpg', 1, 5);
INSERT INTO `t_goods` VALUES (13, '青岛啤酒', 11800, '2018-11-25', '酒水饮料', '青岛啤酒(TsingTao) 青岛啤酒经典10度 500ml*24听,好喝又实惠!', '201811/555da004-a18c-4fa9-9f23-094551928831_js5.jpg', 1, 5);
INSERT INTO `t_goods` VALUES (14, '手撕面包', 1880, '2018-11-25', '饼干糕点', '三只松鼠 手撕面包1000g整箱装零食大礼包口袋面包孕妇零食早餐食品生日蛋糕小糕点点心礼盒装', '201811/12a3f75c-8ddd-41d8-862f-93f342e5e41e_bg5.png', 1, 5);
INSERT INTO `t_goods` VALUES (15, '开心果', 3200, '2018-11-25', '休闲零食', '满199减120三只松鼠 开心果100g坚果炒货零食每日坚果 1袋装', '201811/d468a868-a7b8-4ad6-bde1-124e23c66437_ls5.jpg', 1, 5);
配置:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shoppingdb?useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
# mybatis-plus配置
mybatis-plus:
configuration:
# 下划线转驼峰,默认情况下mybatis-plus是开启的,而mybatis是关闭的
map-underscore-to-camel-case: true
# 日志配置
logging:
level:
包名.mapper: debug
- 创建实体类
import java.util.Date; //注意导入的日期类
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = false)
@TableName("t_goods")
public class Goods {
private Long id;
@TableField(value = "name", condition = SqlCondition.LIKE)
private String name;
private BigInteger price;
private Date pubdate;
@TableField("typeName")
private String typeName;
private String intro;
private String picture;
private Integer flag;
private Integer star;
}
- 数据层实现BaseMapper
@Mapper
@Repository
public interface GoodsMapper extends BaseMapper<Goods> {
}
查询
在 MyBatis-Plus 中,selectList
方法是一个用于查询数据列表的便捷方法。这个方法通常在继承自 BaseMapper
的接口中提供,它会根据你提供的查询条件来返回符合条件的数据列表。
这个方法的使用非常简单,只需要提供一个查询条件构造器作为参数即可。MyBatis-Plus 提供了两种查询条件构造器:QueryWrapper
和 LambdaQueryWrapper
。注意,如果你提供null,就会查询所有数据。
selectList
方法返回的是一个 List
类型的对象,包含了符合查询条件的所有实体类对象。你可以直接操作这个列表,或者将其转换为其他类型的集合。
查询条件构造器
QueryWrapper
- 用途:构建查询条件,用于生成 WHERE 子句。
- 创建:
QueryWrapper<MyEntity> queryWrapper = new QueryWrapper<>();
- 使用:通过链式方法添加查询条件,如
eq
、ne
、gt
等。 - 示例:
@Test
public void testSelectAll() {
List<Goods> goods = goodsMapper.selectList(null);
goods.forEach(System.out::println);
System.out.println("测试成功");
}
LambdaQueryWrapper
- 用途:与 QueryWrapper 类似,但使用 Lambda 表达式,提高代码可读性。
- 创建:
LambdaQueryWrapper<MyEntity> lambdaQuery = new LambdaQueryWrapper<>();
- 使用:通过链式方法添加查询条件,如
eq
、ne
、gt
等。 - 示例:
List<MyEntity> list = goodsMapper.selectList(lambdaQuery.eq(Goods::getId, 1));
QueryWrapper API
- eq (equals):等于查询条件。
- ne (not equals):不等于查询条件。
- gt (greater than):大于查询条件。
- ge (greater than or equals to):大于或等于查询条件。
- lt (less than):小于查询条件。
- le (less than or equals to):小于或等于查询条件。
- like:模糊查询条件,可以包含通配符 %。
- likeLeft:左模糊查询条件,通配符 % 在字符串的左侧。
- likeRight:右模糊查询条件,通配符 % 在字符串的右侧。
- in:在给定列表中的查询条件。
- notIn:不在给定列表中的查询条件。
- orderByAsc:升序排序。
- orderByDesc:降序排序。
- groupBy:分组查询。
- select:指定查询的字段。
- distinct:去重查询。
- nested:嵌套查询条件。
/**
* 查找price>30000并且star=4的记录或者price<1000的记录,记录只显示id、name、price、star字段
*/
@Test
public void testSelectByPriceAndStar() {
QueryWrapper<Goods> queryWrapper = new QueryWrapper<>();
queryWrapper.select("id", "name", "price", "star").and(i -> i.gt("price", 30000).eq("star", 4)).or(i -> i.lt("price", 1000));
List<Goods> goods = goodsMapper.selectList(queryWrapper);
goods.forEach(System.out::println);
System.out.println("测试成功");
}
分页操作
- 使用 QueryWrapper API的
limit
@Test
public void testSelectByPage() {
QueryWrapper<Goods> queryWrapper = new QueryWrapper<>();
queryWrapper.last("limit 0,2");
List<Goods> goods = goodsMapper.selectList(queryWrapper);
goods.forEach(System.out::println);
System.out.println("测试成功");
}
- 使用
Page
类进行分页。
@Test
public void testSelectByPage2() {
Page<Goods> page = new Page<>(1, 2);
//selectPage需要两个参数,一个是Page,另一个是QueryWrapper
List<Goods> goods = goodsMapper.selectPage(page, null).getRecords();
goods.forEach(System.out::println);
System.out.println("测试成功");
}
如果你需要进行分页查询并获取分页信息,Page 是一个更推荐的选择,因为它提供了分页的功能和便捷的方法来处理分页查询。如果你只需要执行普通的查询,并且不需要获取分页信息,那么使用 QueryWrapper 会更直接和方便。
在实际开发中,通常会结合使用 Page 进行分页和 QueryWrapper构建查询条件。但是不要同时使用limit
和Page
。
@Test
public void testSelectByPage2() {
Page<Goods> page = new Page<>(1, 2);
QueryWrapper<Goods> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("typeName", "休闲零食");
List<Goods> goods = goodsMapper.selectPage(page, queryWrapper).getRecords();
goods.forEach(System.out::println);
System.out.println("测试成功");
}
注意,使用Page分页必须进行配置,否则分页无效:
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig{
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setOptimizeJoin(true);
paginationInnerInterceptor.setDbType(DbType.MYSQL);
paginationInnerInterceptor.setOverflow(true);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor = new OptimisticLockerInnerInterceptor();
interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor);
return interceptor;
}
}
插入
MyBatis-Plus 提供了一系列的方法来插入数据,以下是一些常用的插入方法及其代码示例:
- insert():
-
- 用途:插入一个对象,不会自动填充字段,需要手动填充。
- 返回值:insert() 方法通常返回插入的行数,而不是布尔值。
- insert() 方法提供了更多的灵活性,因为它不会自动填充任何字段,这允许你在插入数据时完全控制每个字段的值。
MyEntity entity = new MyEntity();
entity.setName("李四");
entity.setAge(25);
entity.setCreateTime(new Date()); // 假设有一个createTime字段
boolean result = myEntityMapper.insert(entity);
修改
updateById():
- 用途:根据主键更新一个对象。
@Test
public void testUpdateById() {
Goods goods = Goods.builder()
.id(16L)
.name("鸽鸽的蛋")
.price(BigInteger.valueOf(1000L))
.pubdate(new Date(System.currentTimeMillis()))
.typeName("休闲零食")
.intro("只因你太美")
.picture("kun.jpg")
.flag(1)
.star(2)
.build();
int result = goodsMapper.updateById(goods);
System.out.println("影响行数:" + result);
System.out.println("测试成功");
}
update():
- 用途:根据条件更新对象。
UpdateWrapper 在 QueryWrapper 的基础上增加了用于更新的方法,如 set 方法。UpdateWrapper 主要用于构建更新操作的 WHERE 子句和更新字段的值,而 QueryWrapper 主要用于构建查询操作的 WHERE 子句。
@Test
public void testUpdateFlag() {
UpdateWrapper<Goods> wrapper = new UpdateWrapper<>();
wrapper.set("flag", 0);
// 设置条件,flag字段为1
// wrapper.eq("flag", 1);
int result = goodsMapper.update(null, wrapper);
System.out.println("影响行数:" + result);
System.out.println("测试成功");
}
@Test
public void testUpdateFlag2() {
// 设置条件,flag字段为1
Goods goods = Goods.builder()
.flag(1)
.build();
// 当更新的字段为null时,表示更新所有字段
int result = goodsMapper.update(goods, null);
System.out.println("影响行数:" + result);
System.out.println("测试成功");
}
@Test
public void testUpdateTypeName() {
UpdateWrapper<Goods> wrapper = new UpdateWrapper<>();
wrapper.set("flag", 1).set("star", 5).eq("typeName", "饼干糕点");
int result = goodsMapper.update(null, wrapper);
System.out.println("影响行数:" + result);
System.out.println("测试成功");
}
删除
- deleteById():根据id删除
@Test
public void testDeleteById() {
int result = goodsMapper.deleteById(17);
System.out.println("影响行数:" + result);
System.out.println("测试成功");
}
- delete():根据条件删除,可以传入一个QueryWrapper设置查询条件
@Test
public void testDeleteByPrice() {
int result = goodsMapper.delete(new QueryWrapper<Goods>().lt("price", 1000));
System.out.println("影响行数:" + result);
System.out.println("测试成功");
}
逻辑删除
在Spring Boot中,逻辑删除是一种数据删除的范式,它并不是真正地从数据库中移除数据,而是通过设置一个标志位(通常是一个布尔值字段)来标记数据为“已删除”状态。这样做的目的是为了能够在必要时恢复数据或者进行审计跟踪,同时也避免了实际删除操作可能带来的性能开销。
逻辑删除通常涉及以下几个步骤:
- 数据库设计:在数据库表中添加一个标志字段,如is_deleted,通常是一个布尔类型字段。在mysql中没有布尔类型,布尔值通常用tinyint(1)来实现,这个类型可以存储0或1。MySQL会自动将0视为false,将非0值(通常是1)视为true。
- 实体类映射:在实体类中添加一个对应于标志字段的属性,并使用JPA注解来映射该字段。
- 数据访问逻辑:在数据访问层中,编写查询方法时考虑逻辑删除标志,确保查询不会返回已标记为删除的数据。
- 业务逻辑层:在业务逻辑层中,实现删除操作时,不是执行物理删除,而是更新逻辑删除标志字段的值。
- 前端或服务调用:前端或服务在执行删除操作时,应该调用更新逻辑删除标志的接口,而不是执行物理删除。
方式一
- 在数据库为t_goods表添加一个isDelete列,类型为tinyint
- 为实体类添加isDelete字段
private boolean isDelete;
- 添加配置
mybatis-plus:
configuration:
# 下划线转驼峰,默认情况下mybatis-plus是开启的,而mybatis是关闭的
map-underscore-to-camel-case: false
global-config:
db-config:
logic-delete-field: isDelete
logic-delete-value: 1
logic-not-delete-value: 0
- 测试根据id删除
@Test
public void testDeleteById() {
int result = goodsMapper.deleteById(15);
System.out.println("影响行数:" + result);
System.out.println("测试成功");
}
- 查看表记录,发现逻辑删除成功:
方式二
在方式一的基础上,修改配置
mybatis-plus:
configuration:
# 下划线转驼峰,默认情况下mybatis-plus是开启的,而mybatis是关闭的
map-underscore-to-camel-case: false
# global-config:
# db-config:
# logic-delete-field: isDelete
# logic-delete-value: 1
# logic-not-delete-value: 0
为实体类逻辑删除的字段添加注解
@TableField(value = "isDelete")
@TableLogic(value = "0", delval = "1")
private boolean isDelete;
测试:
@Test
public void testDeleteById() {
int result = goodsMapper.deleteById(14);
System.out.println("影响行数:" + result);
System.out.println("测试成功");
}
字段填充
当我们使用 MybatisPlus 操作数据时,会遇到有些表的字段值是可以自动填充的场景,比如我们的 create_time 和 update_time 字段,这些字段值记录的是时间值,如果每次操作都要手动填写的话不仅麻烦而且代码也很臃肿,所以 MybatisPlus 为我们提供了 @TableField 注解来实现自定义自动填充的功能。
在表对应的实体类中需要自动填充的字段上面添加 @TableField
注解:
@Data
public class BaseEntity {
/**
* 创建时间
*/
@TableField(value = "create_time",fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 更新时间
*/
@TableField(value = "update_time",fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**
* 创建人
*/
@TableField(value = "create_user",fill = FieldFill.INSERT)
private Long createUser;
/**
* 更新人
*/
@TableField(value = "update_user",fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
/**
* 删除标志
*/
@TableField(value = "delete_flag")
private Integer deleteFlag;
}
其中我们使用了注解中的 fill
属性(字段自动填充策略):
FieldFill.INSERT
:表示插入时填充字段;FieldFill.INSERT_UPDATE
:表示插入和更新时填充字段;
具体更多属性值我们可以查看 FieldFill
的源码:
public enum FieldFill {
/**
* 默认不处理
*/
DEFAULT,
/**
* 插入时填充字段
*/
INSERT,
/**
* 更新时填充字段
*/
UPDATE,
/**
* 插入和更新时填充字段
*/
INSERT_UPDATE
}
配置完字段之后,MybatisPlus 在自动填充时是不知道应该填充什么值的,需要我们定义填充规则才能完成自动填充功能,下面是自定义填充规则的实例代码:
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class MyBatisMetaObjectHandler implements MetaObjectHandler {
/**
* 自定义插入时填充规则
*/
@Override
public void insertFill(MetaObject metaObject) {
// 注意是类属性字段名称,不是表字段名称
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}
/**
* 自定义更新时填充规则
*/
@Override
public void updateFill(MetaObject metaObject) {
// 注意是类属性字段名称,不是表字段名称
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}
在这里,我们通过实现 MetaObjectHandler 接口类的 insertFill 方法和 updateFill 方法来实现自动填充功能:
insertFill 方法指定了在插入值时如何自动填充字段值,而 updateFill 方法则指定了在更新值时如何自动填充字段值;
通过调用 MetaObjectHandler 的 setFieldValByName 方法实现字段值的填充,第一个参数为类属性的字段名称,第二个参数为字段要填充的值。
配置好后,当 MybatisPlus 插入和更新数据库时就会字段为我们填充指定的字段值。
PageHelpper
介绍
PageHelper是一个MyBatis的分页插件,它能够非常简单地实现MyBatis的物理分页。PageHelper与MyBatis和MyBatis-Plus兼容,你可以在不改变原有代码的基础上,通过简单的配置和调用,实现分页功能。
MyBatis-Plus虽然提供了强大的ORM功能和内置的分页插件,但对于一些老项目或者特定需求,可能会更倾向于使用PageHelper。
基本使用
导入依赖
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
配置:
#MyBatis使用pageHelper分页
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
测试
@Test
public void testSelectByPage3() {
// 第一个参数表示当前页数,第二个参数表示每页显示的记录数,第三个参数表示排序字段
PageHelper.<Goods>startPage(1, 2,"price desc");
List<Goods> goods = goodsMapper.selectList(null);
goods.forEach(System.out::println);
System.out.println("测试成功");
}
@Override
public PageResult pageQuery(AdminPageQueryDTO adminPageQueryDTO) {
int page = adminPageQueryDTO.getPage();
int pageSize = adminPageQueryDTO.getPageSize();
Page<Admin> pageInfo = PageHelper.<Admin>startPage(page, pageSize);
// 查询分页数据,返回分页结果(只查询id、name、username、phone、status)
QueryWrapper<Admin> wrapper = new QueryWrapper<>();
wrapper.select("id", "name", "username", "phone", "status");
List<Admin> list = adminMapper.selectList(null, wrapper);
return new PageResult(pageInfo.getTotal(), list);
}
封装Service层
如果只需要简单的增删改查操作,可以使用Mybatis Plus封装Service层
基本用法
假设我们有一个用户表user
,对应的实体类如下:
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("user")
public class User {
@TableId
private Long id;
private String name;
private Integer age;
private String email;
}
定义Service接口
首先,我们定义一个Service接口,继承自IService<User>
:
import com.baomidou.mybatisplus.extension.service.IService;
public interface UserService extends IService<User> {
// 可以在这里定义自定义的业务方法
}
实现Service接口
然后,我们实现这个Service接口,继承自ServiceImpl<UserMapper, User>
:
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
// 可以在这里实现自定义的业务方法
}
定义Mapper接口
接下来,我们定义一个Mapper接口,继承自BaseMapper<User>
:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 可以在这里定义自定义的SQL方法
}
常用方法
Service接口提供了许多常用的数据库操作方法,例如:
- save:保存一个实体。
- saveBatch:批量保存实体。
- removeById:根据ID删除实体。
- updateById:根据ID更新实体。
- getById:根据ID查询实体。
- list:查询所有实体。
- page:分页查询实体。
示例代码:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public boolean saveUser(@RequestBody User user) {
return userService.save(user);
}
@DeleteMapping("/{id}")
public boolean deleteUser(@PathVariable Long id) {
return userService.removeById(id);
}
@PutMapping
public boolean updateUser(@RequestBody User user) {
return userService.updateById(user);
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getById(id);
}
@GetMapping
public List<User> getAllUsers() {
return userService.list();
}
}
自定义业务方法
在实际开发中,我们可能需要定义一些自定义的业务方法。可以在Service接口和实现类中定义这些方法。
示例代码
在Service接口中定义自定义方法:
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
public interface UserService extends IService<User> {
List<User> findByName(String name);
}
在Service实现类中实现自定义方法:
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public List<User> findByName(String name) {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getName, name);
return list(lambdaQueryWrapper);
}
}
在Controller中调用自定义方法:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/byName")
public List<User> getUsersByName(@RequestParam String name) {
return userService.findByName(name);
}
}
安全性
- 登录标记(会话技术)
-
- 用户登录成功后,每一次请求中,都可以获取该标记
- 统一拦截
-
- 过滤器
- 拦截器
异常处理
介绍
前面的StudentService.java中,我们使用1 / 0模拟了异常,实际使用mybatis时,会有其他sql特有异常,例如插入重复索引的数据,或删除不存在的数据等
常见异常:
- SQLException:这是最基本的异常,代表数据库级别的错误。当执行数据库操作时出现任何问题时,MyBatis 可能会抛出这个异常。
- SQLDataException:当尝试将不兼容的数据类型插入表中,或者尝试对字符串使用非法数值操作时抛出。
- SQLIntegrityConstraintViolationException:当违反了完整性约束(如主键约束、唯一约束或外键约束)时抛出。
- SQLInternalError:由数据库抛出的内部错误,通常是数据库服务器的错误。
- SQLNonTransientConnectionException:当无法打开数据库连接时抛出,并且错误是持久的,不是临时的。
- SQLSyntaxErrorException:当 SQL 语法错误发生时抛出。
- SQLTimeoutException:当数据库操作超时时抛出。
- SQLTransactionRollbackException:当发生事务回滚时抛出。
- SQLTransientConnectionException:当无法打开数据库连接时抛出,并且错误是临时的。
我们可以在Test类添加try...catch...语句,可是以后业务一旦复杂起来,需要这样处理的方法多了怎么办呢?要去一个一个try-catch吗?多麻烦啊!
所以,我们使用异常拦截器进行全局异常捕获。
步骤
- 在src/main/java/com.fl.boot包下创建一个exception包(与三层架构同级),创建一个GlobalExceptionHandler.java文件
- 添加注解
-
- 全局异常处理:通过在 @ControllerAdvice 标注的类中定义方法,并使用 @ExceptionHandler 注解指定要处理的异常类型,使用value属性指定要捕获的异常类型。
@RestContrllorAdvice
是 @ControllerAdvice 的特殊化,它还包括了 @ResponseBody 注解(@RestContrllorAdvice
=@ContrllorAdvice
+@RespponseBody
),使得返回值直接作为响应体,无需视图解析。
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value =Exception.class)
@ResponseBody
public String exceptionHandler(Exception e){
System.out.println("全局异常捕获>>>:"+e);
return "全局异常捕获,错误原因>>>"+e.getMessage();
}
}
- 在contoller包下编写一个StudentContoller.java,用于接收请求
@RestController
public class StudentContoller {
@Autowired
public StudentService studentService;
@GetMapping("/updateStudent")
public String updateStudent(int id, String name, Integer age, Character gender, String className) {
//调用service层updateStudent方法,因为该方法存在算数异常,会被全局异常处理器捕获并处理
studentService.updateStudent(id, name, age, gender, className);
return "更新成功";
}
}
- 使用postman发送请求
http://localhost:8080/updateStudent?id=4&name=FL
- 观察控制台:
注意,如果直接使用测试类进行测试,该错误不会被全局异常处理器捕获
@Test
void updateStudent2(){
studentService.updateStudent(7,"蔡徐坤",null,null,"高三三班");
}
会话技术
会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
会话跟踪方案:
- 客户端会话跟踪技术:Cookie
- 服务端会话跟踪技术:Session
- 令牌技术
会话跟踪技术方案对比:
- Cookie:
-
- 优点:HTTP协议支持
- 缺点:移动端app无法使用;不安全,用户可以自己禁用;不能跨域
- Session:
-
- 优点:存储在服务端,安全
- 缺点:服务器集群环境下无法直接使用Session
- JWT
-
- 优点:支持PC端、移动端;解决集群环境下的认证问题;减轻服务器端存储压力
- 缺点:需要自己实现
JWT令牌
介绍
JWT , 全写JSON Web Token, 是开放的行业标准RFC7591,用来实现端到端安全验证.
简单来说, 就是通过一些算法对JSON对象进行加密、解密。
JWT对JSON对象进行加密,将加密后的字符串保存在客户端,不需要在服务端保存会话信息。JWT可以应用在前后端分离的用户验证上,后端对前端输入的用户信息进行加密产生一个令牌字符串(也叫token),前端再次请求时附加此令牌字符串,后端再使用算法解密,验证该令牌字符串是否为合法用户。
场景:登录认证。
①登录成功后,生成令牌
②后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理
- 使用
- 在pom.xml中引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
- 生成JWT令牌(设置签名算法,自定义内容,和有效期)
@Test // 当运行不与整个工程有关的测试时,可以先将@SpringBootTest注解注掉,只加载测试类
public void testGenJwt(){
// 自定义内容
HashMap<String, Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("name","tom");
// 定义令牌
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "fl123456") //签名算法。注意生成token的密钥secret字符串不能过短,否则会引起异常。
.addClaims(claims) //设置自定义内容
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) //设置令牌有效时间
.compact(); //将令牌转为字符串
System.out.println(jwt);
}
- 基于java代码解析jwt令牌
@Test
public void testParseJwt(){
Claims claims = Jwts.parser()
.setSigningKey("fl123456") //指定秘钥(要与生成的一致)
.parseClaimsJws("令牌")
.getBody(); //获取自定义内容
System.out.println(claims);
}
案例:修改登录接口,如果登录成功就生成令牌,否则返回错误信息
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("登录账号:{}密码:{}",emp.getUsername(),emp.getPassword());
Emp e = empService.login(emp);
// 登录成功,生成令牌,下发令牌
if (e != null){
HashMap<String, Object> claims = new HashMap<>();
claims.put("id",e.getId());
claims.put("name",e.getName());
claims.put("username",e.getUsername());
String jwt = JwtUtils.generateJwt(claims);
return Result.success(jwt);
}
// 登录失败,放回错误信息
return Result.error("用户名或密码错误");
}
基本使用
java-jwt是Java语言中推荐的JWT实现库
(1)使用Maven导入:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.3</version>
</dependency>
(2)产生加密Token
@Test
public void createToken() {
String token = JWT.create()
.withExpiresAt(Date.from(ZonedDateTime.now().plusMinutes(10).toInstant())) //设置过期时间,过期时间为10分钟后
.withAudience("user1") //设置接受方信息,一般是登录用户
.withClaim("userName", "tom")
.sign(Algorithm.HMAC256("111111")); //使用HMAC算法,111111作为密钥加密
System.out.println(token);
}
(3)解密Token获取负载信息
@Test
public void decodeToken() {
String token = "";
String userId = JWT.decode(token).getAudience().get(0);
Assertions.assertEquals("user1", userId);
System.out.println(userId);
System.out.println(JWT.decode(token).getClaim("userName").asString());
}
(4)验证Token是否有效
@Test
public void verifyToken() {
String token = "";
try {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("111111"))
.build();//验证时一定要跟生成时的算法和密钥一致
jwtVerifier.verify(token);
System.out.println("验证成功");
//以上代码就已经完成验证了,如果没有出异常,就验证成功!出现了就是验证失败
} catch (Exception e) {
System.out.println("验证失败");
}
}
示例
过滤器Filter
介绍
概念:Filter 过滤器
,是 早期JavaWeb 三大组件(Servlet、Filter、Listener)之一(现在其他两个组件不常用)。
作用:过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。比如:登录校验、统一编码处理、敏感字符处理等。
快速入门
1.定义Filter:定义一个类,实现 Filter 接口(注意导入javax.servlet.*
),并重写其所有方法。
2.配置Filter:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。
@WebFilter(urlPatterns = "/*") //拦截所有请求
public class DemoFilter implements Filter {
@Override//初始化方法,只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法执行了");
}
@Override//拦截请求后调用,调用多次
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 拦截请求,并操作
System.out.println("拦截到了请求...");
//放行前逻辑
// 放行请求
filterChain.doFilter(servletRequest,servletResponse);
//放行后逻辑
}
@Override//销毁方法,只调用一次
public void destroy() {
}
}
- init方法和destroy方法不常用,用默认实现方法,可以不实现
细节
- 拦截流程:放行后访问对应资源,资源完成访问后,还会回到Filter,执行放行后的逻辑
- 拦截路径
@WebFilter(urlPatterns = "/*") //拦截所有请求
@WebFilter(urlPatterns = "/emps/*") //拦截目录请求
@WebFilter(urlPatterns = "/login") //拦截具体请求
- 过滤器链:
-
- 一个web应用可以配置多个过滤器,这多个过滤器构成了一个过滤器链
- 注解配置的Filter,优先级按照过滤器类名(字符串)的自然排序
过滤器案例
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest)servletRequest;
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
//获取请求的url
String url = httpRequest.getRequestURL().toString();
// 判断请求url中是否含有login
if (url.contains("login")){
log.info("登录操作,直接放行...");
filterChain.doFilter(servletRequest,servletResponse);
return; }
// 获取请求头中的令牌
String jwt = httpRequest.getHeader("token");
// 判断令牌是否为空
if (!StringUtils.hasLength(jwt)){//调用spring的工具类,判断令牌是否为空
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
// 手动转换对象(用阿里巴巴fastJSON)
String notLogin = JSONObject.toJSONString(error);
httpResponse.getWriter().write(notLogin);
return; }
// 判断令牌是否合法
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
// 手动转换对象(用阿里巴巴fastJSON)
String notLogin = JSONObject.toJSONString(error);
httpResponse.getWriter().write(notLogin);
return; }
// 放行
filterChain.doFilter(servletRequest,servletResponse);
}
}
拦截器Interceptor
介绍
- 概念:spring框架提供的,用来动态拦截控制器方法的执行
- 作用:拦截请求,在指定方法调用前后,根据业务需要执行预先设置的代码
快速入门
- 定义拦截器,实现
HandlerInterceptor
接口
@Component //交给ioc容器
public class LoginCheckInterceptor implements HandlerInterceptor {//将光标放在要实现的接口上,然后ctrl+O可以快捷实现
@Override //目标资源方法运行前运行,如果返回true就放行,否则不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle...");
return true; }
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle...");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
}
- 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");
}
}
细节
- 拦截路径
拦截器可以根据需求,配置不同拦截路径
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
- 执行流程
- 过滤器和拦截器使用其中一种即可。
- 拦截器与过滤器的区别
-
- 接口规范不同:过滤器实现Filter接口,拦截器实现HandlerInterceptor接口
- 拦截范围不同:过滤器会拦截所有资源,而拦截器只会拦截Spring环境中的资源
拦截器实现登录校验
@Override //目标资源方法运行前运行,如果返回true就放行,否则不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取请求的url
String url = request.getRequestURL().toString();
// 判断请求url中是否含有login
if (url.contains("login")){
log.info("登录操作,直接放行...");
return true; }
// 获取请求头中的令牌
String jwt = request.getHeader("token");
// 判断令牌是否为空
if (!StringUtils.hasLength(jwt)){//调用spring的工具类,判断令牌是否为空
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
// 手动转换对象(用阿里巴巴fastJSON)
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false; }
// 判断令牌是否合法
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
// 手动转换对象(用阿里巴巴fastJSON)
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false; }
// 放行
return true;
}
Spring Data Redis
介绍
前面我们讲解了Redis的常用命令,这些命令是我们操作Redis的基础,那么我们在java程序中应该如何操作Redis呢?这就需要使用Redis的Java客户端,就如同我们使用JDBC操作MySQL数据库一样。
Spring 对 Redis 客户端进行了整合,提供了 Spring Data Redis,在Spring Boot项目中还提供了对应的Starter,即 spring-boot-starter-data-redis。
Spring Data Redis 是 Spring 的一部分,提供了在 Spring 应用中通过简单的配置就可以访问 Redis 服务,对 Redis 底层开发包进行了高度封装。在 Spring 项目中,可以使用Spring Data Redis来简化 Redis 操作。在 Spring Data Redis 中,RedisTemplate
提供了操作 Redis 各种数据结构的方法。
基于SpringBoot3引入Redis并封装常用的操作RedisUtils_springboot redisutil-CSDN博客
环境搭建
在pom.xml中添加下面依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在application-dev.yml中添加配置
#redis配置信息
redis:
host: localhost
port: 6379
password: 123456
database: 1
解释说明:
database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15。
可以通过修改Redis配置文件redis.windows.conf来指定数据库的数量。
配置
Spring Data Redis中提供了一个高度封装的类:RedisTemplate,对相关api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下:
- ValueOperations:string数据操作
- SetOperations:set类型数据操作
- ZSetOperations:zset类型数据操作
- HashOperations:hash类型的数据操作
- ListOperations:list类型的数据操作
编写配置类,配置序列化
public class JacksonObjectMapper extends ObjectMapper {
@Serial
private static final long serialVersionUID = 2220067913562288238L;
public static final String DATE_TIME_FORMATTER= "yyyy-MM-dd HH:mm:ss";
public static final String TIME_FORMATTER= "HH:mm:ss";
public static final String DATE_FORMATTER= "yyyy-MM-dd";
public JacksonObjectMapper() {
this.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
this.setSerializationInclusion(JsonInclude.Include.ALWAYS);
this.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT,true);
this.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);
this.configure(SerializationFeature.FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS,false);
this.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES,false);
this.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
this.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,false);
SimpleModule simpleModule = new SimpleModule()
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMATTER)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMATTER)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_FORMATTER)))
//防止Long类型在前端精度缺失
.addSerializer(Long.class, new ToStringSerializer())
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMATTER)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMATTER)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(TIME_FORMATTER)));
this.registerModule(simpleModule);
}
}
扩展转换器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(new JacksonObjectMapper());
converters.add(0,converter);
}
}
编写配置类,创建RedisTemplate对象
@Configuration
public class RedisConfig {
@Bean
public GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer() {
String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
ObjectMapper objectMapper = new ObjectMapper();
// 处理Date类型
objectMapper.setDateFormat(new SimpleDateFormat(DATE_TIME_FORMAT));
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
objectMapper.registerModule(new JavaTimeModule()
// 处理LocalDateTime类型
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)))
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))));
// 序列化java对象时,将类的信息写入redis
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
redisTemplate.setDefaultSerializer(genericJackson2JsonRedisSerializer());
return redisTemplate;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)) // 配置缓存时长
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer()));
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
解释说明:
当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别,故设置为StringRedisSerializer序列化器。
通过RedisTemplate对象操作Redis
import org.junit.Test;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SpringDataRedisTest {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
public void testRedisTemplate(){
System.out.println(redisTemplate);
//string数据操作
ValueOperations valueOperations = redisTemplate.opsForValue();
//hash类型的数据操作
HashOperations hashOperations = redisTemplate.opsForHash();
//list类型的数据操作
ListOperations listOperations = redisTemplate.opsForList();
//set类型数据操作
SetOperations setOperations = redisTemplate.opsForSet();
//zset类型数据操作
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
}
}
- 这里RedisTemplate 是针对 <String, Object> 类型,如果其他地方使用该模板,参数需要与该参数匹配。
操作常见类型数据
字符串类型
- 增加(存储)字符串值:
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void setValue(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
- 获取字符串值:
public String getValue(String key) {
return redisTemplate.opsForValue().get(key);
}
- 更新字符串值:
更新操作实际上是设置操作的另一种形式,因为 Redis 中的字符串是原子操作的,所以你可以直接使用set
方法来覆盖旧值。
public void updateValue(String key, String newValue) {
redisTemplate.opsForValue().set(key, newValue);
}
- 删除字符串值:
public void deleteValue(String key) {
redisTemplate.delete(key);
}
在使用这些方法之前,请确保你的 RedisTemplate
是针对 <String, Object>
类型的。
如果你想要操作其他类型的数据,比如 Integer
、Long
或者自定义对象,你需要相应地更改 RedisTemplate
的泛型参数,并且可能需要使用不同的序列化器。
示例:
@Test
public void testString(){
// 将字符串"小明"存储到Redis的"name"键中
redisTemplate.opsForValue().set("name","小明");
//从Redis中获取键为"name"的值
String name = (String) redisTemplate.opsForValue().get("name");
System.out.println(name);//小明
//修改键为"name"的值为"小红"
redisTemplate.opsForValue().set("name","小红");
// 删除字符串值
redisTemplate.delete("name");
name = (String) redisTemplate.opsForValue().get("name");
System.out.println(name);//null
}
哈希类型数据
在 Spring Data Redis 中,RedisTemplate
提供了操作 Redis 哈希(Hash)数据结构的方法。以下是如何使用 RedisTemplate
来进行哈希类型的增删改查操作:
- 增加(设置)哈希值:
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void putHashValue(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
- 获取哈希值:
public Object getHashValue(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
- 获取整个哈希映射:
public Map<Object, Object> getEntireHash(String key) {
return redisTemplate.opsForHash().entries(key);
}
- 更新哈希值:
更新哈希值与设置哈希值相同,因为如果哈希字段已经存在,put
方法会覆盖旧的值。
public void updateHashValue(String key, String hashKey, Object newValue) {
redisTemplate.opsForHash().put(key, hashKey, newValue);
}
- 删除哈希字段:
public void deleteHashField(String key, String hashKey) {
redisTemplate.opsForHash().delete(key, hashKey);
}
- 其他操作:
- 检查哈希字段是否存在:
public boolean hashFieldExists(String key, String hashKey) {
return redisTemplate.opsForHash().hasKey(key, hashKey);
}
示例:
@Test
public void testHashOperations() {
String KEY = "user";
String HASH_KEY = "username";
String VALUE = "john_doe";
HashOperations<String, Object, Object> hashOperations = redisTemplate.opsForHash();
// 测试增加(设置)哈希值
hashOperations.put(KEY, HASH_KEY, VALUE);
Object o = hashOperations.get(KEY, HASH_KEY);
System.out.println(o);// john_doe
// 测试获取整个哈希映射
Map<Object, Object> entries = hashOperations.entries(KEY);
System.out.println(entries.get(HASH_KEY));// john_doe
// 测试更新哈希值
String newValue = "ikun";
hashOperations.put(KEY, HASH_KEY, newValue);
o = hashOperations.get(KEY, HASH_KEY);
System.out.println(o);// ikun
// 测试删除哈希字段
hashOperations.delete(KEY, HASH_KEY);
o = hashOperations.get(KEY, HASH_KEY);
System.out.println(o);// null
}
列表类型数据
在 Redis 的列表(List)数据结构中,元素可以被推入(push)或弹出(pop)列表的左侧或右侧。列表在 Redis 中是一种双向链表,允许从两端插入或删除元素。
RedisTemplate
提供了丰富的操作 Redis 列表(List)数据结构的方法。以下是一些常用的方法:
- leftPush(K key, V value): 将一个值插入到列表的左侧(头部)。
- rightPush(K key, V value): 将一个值插入到列表的右侧(尾部)。
- leftPop(K key): 从列表的左侧移除并返回第一个元素。
- rightPop(K key): 从列表的右侧移除并返回最后一个元素。
- range(K key, long start, long end): 获取列表指定区间内的元素。
- size(K key): 获取列表的长度。
- trim(K key, long start, long end): 裁剪列表,只保留指定区间内的元素。
- set(K key, long index, V value): 设置列表指定位置的元素值。
- index(K key, long index): 获取列表指定位置的元素。
- remove(K key, long count, Object value): 从列表中移除指定数量的值为
value
的元素。 - leftPushAll(K key, V... values): 将多个值插入到列表的左侧。
- rightPushAll(K key, V... values): 将多个值插入到列表的右侧。
- leftPushIfPresent(K key, V value): 只有当列表存在时,才将值插入到列表的左侧。
- rightPushIfPresent(K key, V value): 只有当列表存在时,才将值插入到列表的右侧。
- leftPush(K key, V value, V... values): 将一个值和一个值数组插入到列表的左侧。
- rightPush(K key, V value, V... values): 将一个值和一个值数组插入到列表的右侧。
示例:
@Test
public void testListOperations() {
String KEY = "user";
String VALUE = "john_doe";
ListOperations<String, Object> listOperations = redisTemplate.opsForList();
// 测试左侧推入列表值
listOperations.leftPush(KEY, VALUE);
String item = (String) listOperations.leftPop(KEY);
System.out.println(item); // item1
// 再次推入值以便后续测试
listOperations.leftPush(KEY, VALUE);
// 测试获取列表的一段范围
List<Object> range = listOperations.range(KEY, 0, -1);
System.out.println(range); // [item1]
// 测试获取列表长度
long size = listOperations.size(KEY);
System.out.println(size); // 1
// 测试删除列表值
listOperations.remove(KEY, 1, VALUE);
size = listOperations.size(KEY);
System.out.println(size); // 0
}
有序列表
RedisTemplate
提供了一系列操作 Redis 有序列表(Sorted Set)的常用方法。以下是一些基本操作:
- 添加元素:
-
zAdd(K key, V value, double score)
: 添加一个元素及其分数到有序列表。zAdd(K key, Set<TypedTuple<V>> tuples)
: 添加多个元素及其分数到有序列表。
- 移除元素:
-
zRemove(K key, Object... values)
: 移除有序列表中的一个或多个元素。zRemove(K key, Set<V> values)
: 移除有序列表中的多个元素。zRemove(K key, long start, long end)
: 移除有序列表中指定排名范围内的所有元素。
- 获取元素:
-
zRange(K key, long start, long end)
: 获取有序列表中指定排名范围内的所有元素。zRangeWithScores(K key, long start, long end)
: 获取有序列表中指定排名范围内的所有元素及其分数。zRevRange(K key, long start, long end)
: 获取有序列表中指定倒数排名范围内的所有元素。zRevRangeWithScores(K key, long start, long end)
: 获取有序列表中指定倒数排名范围内的所有元素及其分数。
- 获取元素分数:
-
zScore(K key, Object value)
: 获取元素的分数。
- 增加元素分数:
-
zIncrementScore(K key, V value, double delta)
: 增加元素的分数。
- 获取列表长度:
-
zCard(K key)
: 获取有序列表的长度。zCount(K key, double min, double max)
: 获取分数在指定范围内的元素数量。
- 排名:
-
zRank(K key, Object value)
: 获取元素的排名(从低到高)。zRevRank(K key, Object value)
: 获取元素的倒数排名(从高到低)。
- 交集、并集和差集:
-
zIntersectAndStore(K key, K otherKey, K destKey)
: 计算两个有序列表的交集,并将结果存储在新的有序列表中。zUnionAndStore(K key, K otherKey, K destKey)
: 计算两个有序列表的并集,并将结果存储在新的有序列表中。zDifferenceAndStore(K key, K otherKey, K destKey)
: 计算两个有序列表的差集,并将结果存储在新的有序列表中。
示例:
@Test
public void testSortedSetOperations() {
String KEY = "mySortedSet";
String MEMBER = "member1";
double SCORE = 1.0;
ZSetOperations<String, Object> zSetOperations = redisTemplate.opsForZSet();
// 测试添加元素
zSetOperations.add(KEY, MEMBER, SCORE);
Long size = zSetOperations.zCard(KEY);
System.out.println(size); // 1
// 测试获取元素及其分数
Set<Object> range = zSetOperations.range(KEY, 0, -1);
System.out.println(range); // [member1]
Double score = zSetOperations.score(KEY, MEMBER);
System.out.println(score); // 1.0
// 测试增加元素分数
zSetOperations.incrementScore(KEY, MEMBER, 1.5);
score = zSetOperations.score(KEY, MEMBER);
System.out.println(score); // 2.5
// 测试移除元素
zSetOperations.remove(KEY, MEMBER);
size = zSetOperations.zCard(KEY);
System.out.println(size); // 0
}
通用命令
- 设置键的过期时间:
-
- expire(Object key, long timeout, TimeUnit unit): 设置键的过期时间。
- 检查键是否存在:
-
- hasKey(Object key): 检查键是否存在。
- 获取键的剩余过期时间:
-
- getExpire(Object key): 获取键的剩余过期时间(以秒为单位)。
- 执行命令:
-
- execute(RedisCallback<T> action): 执行一个 Redis 回调操作。
- execute(SessionCallback<T> session): 执行一个 Redis 会话回调操作。
- 转换和序列化:
-
- convertAndSend(String channel, Object message): 发布一个消息到指定的频道。
- convertIfNecessary(Object value): 如果需要,将对象转换为 Redis 支持的类型。
- 绑定和解绑:
-
- bind(RedisMessageListenerContainer container): 将消息监听容器绑定到 RedisTemplate。
- unbind(RedisMessageListenerContainer container): 将消息监听容器从 RedisTemplate 解绑。
- 操作键:
-
- delete(Object key): 删除一个键。
- delete(Collection<K> keys): 删除多个键。
- expire(Object key, long timeout, TimeUnit unit): 设置键的过期时间。
- hasKey(Object key): 检查键是否存在。
- 获取 Redis 连接和通道:
-
- getConnectionFactory(): 获取 Redis 连接工厂。
- getConnection(): 获取 Redis 连接。
- getChannel(): 获取 Redis 发布/订阅通道。
- 发布/订阅:
-
- convertAndSend(String channel, Object message): 发布消息到指定频道。
- listen(RedisPubSubListener<K, V> listener): 监听指定频道的消息。
- 事务和流水线:
-
- multi(): 开始一个事务。
- exec(): 执行所有队列中的命令并返回结果。
- discard(): 取消当前事务。
- watch(Object key): 监视一个键,用于实现乐观锁。
- unwatch(): 取消对所有键的监视。
- 脚本:
-
- eval(String script, List<K> keys, Object... args): 执行 Lua 脚本。
- 排序:
-
- sort(SortQuery<K> query): 对列表、集合或有序集合进行排序。
- 其他操作:
-
- geoAdd(K key, Point point, V value): 添加地理位置信息。
- geoRadius(K key, Circle circle): 查询地理位置信息。
工具类
一般情况下,我们都是单独封装一个工具类,来把常用的一些方法进行抽象。操作的时候,直接通过工具类来操作。
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
/**
* 给一个指定的 key 值附加过期时间
*
* @param key
* @param time
* @return
*/
public boolean expire(String key, long time) {
return redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
/**
* 根据key 获取过期时间
*
* @param key
* @return
*/
public long getTime(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 根据key 获取过期时间
*
* @param key
* @return
*/
public boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 移除指定key 的过期时间
*
* @param key
* @return
*/
public boolean persist(String key) {
return redisTemplate.boundValueOps(key).persist();
}
//- - - - - - - - - - - - - - - - - - - - - String类型 - - - - - - - - - - - - - - - - - - - -
/**
* 根据key获取值
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 将值放入缓存
*
* @param key 键
* @param value 值
* @return true成功 false 失败
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 将值放入缓存并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) -1为无期限
* @return true成功 false 失败
*/
public void set(String key, String value, long time) {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
redisTemplate.opsForValue().set(key, value);
}
}
/**
* 批量添加 key (重复的键会覆盖)
*
* @param keyAndValue
*/
public void batchSet(Map<String, String> keyAndValue) {
redisTemplate.opsForValue().multiSet(keyAndValue);
}
/**
* 批量添加 key-value 只有在键不存在时,才添加
* map 中只要有一个key存在,则全部不添加
*
* @param keyAndValue
*/
public void batchSetIfAbsent(Map<String, String> keyAndValue) {
redisTemplate.opsForValue().multiSetIfAbsent(keyAndValue);
}
/**
* 对一个 key-value 的值进行加减操作,
* 如果该 key 不存在 将创建一个key 并赋值该 number
* 如果 key 存在,但 value 不是长整型 ,将报错
*
* @param key
* @param number
*/
public Long increment(String key, long number) {
return redisTemplate.opsForValue().increment(key, number);
}
/**
* 对一个 key-value 的值进行加减操作,
* 如果该 key 不存在 将创建一个key 并赋值该 number
* 如果 key 存在,但 value 不是 纯数字 ,将报错
*
* @param key
* @param number
*/
public Double increment(String key, double number) {
return redisTemplate.opsForValue().increment(key, number);
}
//- - - - - - - - - - - - - - - - - - - - - set类型 - - - - - - - - - - - - - - - - - - - -
/**
* 将数据放入set缓存
*
* @param key 键
* @return
*/
public void sSet(String key, String value) {
redisTemplate.opsForSet().add(key, value);
}
/**
* 获取变量中的值
*
* @param key 键
* @return
*/
public Set<Object> members(String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 随机获取变量中指定个数的元素
*
* @param key 键
* @param count 值
* @return
*/
public void randomMembers(String key, long count) {
redisTemplate.opsForSet().randomMembers(key, count);
}
/**
* 随机获取变量中的元素
*
* @param key 键
* @return
*/
public Object randomMember(String key) {
return redisTemplate.opsForSet().randomMember(key);
}
/**
* 弹出变量中的元素
*
* @param key 键
* @return
*/
public Object pop(String key) {
return redisTemplate.opsForSet().pop("setValue");
}
/**
* 获取变量中值的长度
*
* @param key 键
* @return
*/
public long size(String key) {
return redisTemplate.opsForSet().size(key);
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
/**
* 检查给定的元素是否在变量中。
*
* @param key 键
* @param obj 元素对象
* @return
*/
public boolean isMember(String key, Object obj) {
return redisTemplate.opsForSet().isMember(key, obj);
}
/**
* 转移变量的元素值到目的变量。
*
* @param key 键
* @param value 元素对象
* @param destKey 元素对象
* @return
*/
public boolean move(String key, String value, String destKey) {
return redisTemplate.opsForSet().move(key, value, destKey);
}
/**
* 批量移除set缓存中元素
*
* @param key 键
* @param values 值
* @return
*/
public void remove(String key, Object... values) {
redisTemplate.opsForSet().remove(key, values);
}
/**
* 通过给定的key求2个set变量的差值
*
* @param key 键
* @param destKey 键
* @return
*/
public Set<Set> difference(String key, String destKey) {
return redisTemplate.opsForSet().difference(key, destKey);
}
//- - - - - - - - - - - - - - - - - - - - - hash类型 - - - - - - - - - - - - - - - - - - - -
/**
* 加入缓存
*
* @param key 键
* @param map 键
* @return
*/
public void add(String key, Map<String, String> map) {
redisTemplate.opsForHash().putAll(key, map);
}
/**
* 获取 key 下的 所有 hashkey 和 value
*
* @param key 键
* @return
*/
public Map<Object, Object> getHashEntries(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 验证指定 key 下 有没有指定的 hashkey
*
* @param key
* @param hashKey
* @return
*/
public boolean hashKey(String key, String hashKey) {
return redisTemplate.opsForHash().hasKey(key, hashKey);
}
/**
* 获取指定key的值string
*
* @param key 键
* @param key2 键
* @return
*/
public String getMapString(String key, String key2) {
return redisTemplate.opsForHash().get("map1", "key1").toString();
}
/**
* 获取指定的值Int
*
* @param key 键
* @param key2 键
* @return
*/
public Integer getMapInt(String key, String key2) {
return (Integer) redisTemplate.opsForHash().get("map1", "key1");
}
/**
* 弹出元素并删除
*
* @param key 键
* @return
*/
public String popValue(String key) {
return redisTemplate.opsForSet().pop(key).toString();
}
/**
* 删除指定 hash 的 HashKey
*
* @param key
* @param hashKeys
* @return 删除成功的 数量
*/
public Long delete(String key, String... hashKeys) {
return redisTemplate.opsForHash().delete(key, hashKeys);
}
/**
* 给指定 hash 的 hashkey 做增减操作
*
* @param key
* @param hashKey
* @param number
* @return
*/
public Long increment(String key, String hashKey, long number) {
return redisTemplate.opsForHash().increment(key, hashKey, number);
}
/**
* 给指定 hash 的 hashkey 做增减操作
*
* @param key
* @param hashKey
* @param number
* @return
*/
public Double increment(String key, String hashKey, Double number) {
return redisTemplate.opsForHash().increment(key, hashKey, number);
}
/**
* 获取 key 下的 所有 hashkey 字段
*
* @param key
* @return
*/
public Set<Object> hashKeys(String key) {
return redisTemplate.opsForHash().keys(key);
}
/**
* 获取指定 hash 下面的 键值对 数量
*
* @param key
* @return
*/
public Long hashSize(String key) {
return redisTemplate.opsForHash().size(key);
}
//- - - - - - - - - - - - - - - - - - - - - list类型 - - - - - - - - - - - - - - - - - - - -
/**
* 在变量左边添加元素值
*
* @param key
* @param value
* @return
*/
public void leftPush(String key, Object value) {
redisTemplate.opsForList().leftPush(key, value);
}
/**
* 获取集合指定位置的值。
*
* @param key
* @param index
* @return
*/
public Object index(String key, long index) {
return redisTemplate.opsForList().index("list", 1);
}
/**
* 获取指定区间的值。
*
* @param key
* @param start
* @param end
* @return
*/
public List<Object> range(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 把最后一个参数值放到指定集合的第一个出现中间参数的前面,
* 如果中间参数值存在的话。
*
* @param key
* @param pivot
* @param value
* @return
*/
public void leftPush(String key, String pivot, String value) {
redisTemplate.opsForList().leftPush(key, pivot, value);
}
/**
* 向左边批量添加参数元素。
*
* @param key
* @param values
* @return
*/
public void leftPushAll(String key, String... values) {
// redisTemplate.opsForList().leftPushAll(key,"w","x","y");
redisTemplate.opsForList().leftPushAll(key, values);
}
/**
* 向集合最右边添加元素。
*
* @param key
* @param value
* @return
*/
public void leftPushAll(String key, String value) {
redisTemplate.opsForList().rightPush(key, value);
}
/**
* 向左边批量添加参数元素。
*
* @param key
* @param values
* @return
*/
public void rightPushAll(String key, String... values) {
//redisTemplate.opsForList().leftPushAll(key,"w","x","y");
redisTemplate.opsForList().rightPushAll(key, values);
}
/**
* 向已存在的集合中添加元素。
*
* @param key
* @param value
* @return
*/
public void rightPushIfPresent(String key, Object value) {
redisTemplate.opsForList().rightPushIfPresent(key, value);
}
/**
* 向已存在的集合中添加元素。
*
* @param key
* @return
*/
public long listLength(String key) {
return redisTemplate.opsForList().size(key);
}
/**
* 移除集合中的左边第一个元素。
*
* @param key
* @return
*/
public void leftPop(String key) {
redisTemplate.opsForList().leftPop(key);
}
/**
* 移除集合中左边的元素在等待的时间里,如果超过等待的时间仍没有元素则退出。
*
* @param key
* @return
*/
public void leftPop(String key, long timeout, TimeUnit unit) {
redisTemplate.opsForList().leftPop(key, timeout, unit);
}
/**
* 移除集合中右边的元素。
*
* @param key
* @return
*/
public void rightPop(String key) {
redisTemplate.opsForList().rightPop(key);
}
/**
* 移除集合中右边的元素在等待的时间里,如果超过等待的时间仍没有元素则退出。
*
* @param key
* @return
*/
public void rightPop(String key, long timeout, TimeUnit unit) {
redisTemplate.opsForList().rightPop(key, timeout, unit);
}
}
Spring Cache
介绍
Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:
- EHCache
- Caffeine
- Redis(常用)
常用注解
在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:
注解 | 说明 |
开启缓存注解功能,通常加在启动类上 | |
在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中。一般用在查询方法上。 | |
将方法的返回值放到缓存中。通常用在新增方法上。 | |
将一条或多条数据从缓存中删除。一般用在更新或者删除的方法上。 |
- value/cacheNames:指明缓存的类型。我们可以在一个方法上定义两个cacheNames,虽然也可以用
value
,它是cacheNames的别名,但如果有多个配置的时候,更推荐用cacheNames,因为这样具有更好的可读性。 - key:用于标记某次缓存,一般通过id标记,查询条件标记。删除缓存时,通过标记去删除。
- conditon:是否缓存的判断条件,可以用来指明查询结果为null时不缓存。
- unless:与condition相反。
实现
- 导入Spring Cache和Redis相关maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
- 在启动类上加入@EnableCaching注解,开启缓存注解功能
@Slf4j
@SpringBootApplication
@EnableCaching//开启缓存注解功能
public class CacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CacheDemoApplication.class,args);
log.info("项目启动成功...");
}
}
- 序列化配置类
用于处理返回前端的数据,序列化和反序列化的格式
public class JacksonObjectMapper extends ObjectMapper {
@Serial
private static final long serialVersionUID = 2220067913562288238L;
public static final String DATE_TIME_FORMATTER= "yyyy-MM-dd HH:mm:ss";
public static final String TIME_FORMATTER= "HH:mm:ss";
public static final String DATE_FORMATTER= "yyyy-MM-dd";
public JacksonObjectMapper() {
this.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
this.setSerializationInclusion(JsonInclude.Include.ALWAYS);
this.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT,true);
this.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);
this.configure(SerializationFeature.FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS,false);
this.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES,false);
this.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
this.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,false);
SimpleModule simpleModule = new SimpleModule()
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMATTER)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMATTER)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_FORMATTER)))
//防止Long类型在前端精度缺失
.addSerializer(Long.class, new ToStringSerializer())
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMATTER)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMATTER)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(TIME_FORMATTER)));
this.registerModule(simpleModule);
}
}
- 配置扩展转换器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(new JacksonObjectMapper());
converters.add(0,converter);
}
}
- redis配置类
@EnableCaching
@Configuration
public class RedisConfig {
@Bean
public GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer() {
String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
ObjectMapper objectMapper = new ObjectMapper();
// 处理Date类型
objectMapper.setDateFormat(new SimpleDateFormat(DATE_TIME_FORMAT));
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
objectMapper.registerModule(new JavaTimeModule()
// 处理LocalDateTime类型
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)))
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))));
// 序列化java对象时,将类的信息写入redis
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
redisTemplate.setDefaultSerializer(genericJackson2JsonRedisSerializer());
return redisTemplate;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)) // 配置缓存时长
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer()));
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
- 使用:在实现三层架构(Controller/DTO/Service)时,最佳实践是将
@Cacheable
注解添加在 Service 层。
在save方法上加注解@CachePut
/**
* CachePut:将方法返回值放入缓存
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@CachePut(cacheNames = "userCache", key = "#user.id")//key的生成:userCache::1
public User save(@RequestBody User user){
userMapper.insert(user);
return user;
}
在getById上加注解@Cacheable
/**
* Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据, *调用方法并将方法返回值放到缓存中
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@Cacheable(cacheNames = "userCache",key="#id")
public User getById(Long id){
User user = userMapper.getById(id);
return user;
}
在 delete 方法上加注解@CacheEvict
@CacheEvict(cacheNames = "userCache",key = "#id")//删除某个key对应的缓存数据
public void deleteById(Long id){
userMapper.deleteById(id);
}
@CacheEvict(cacheNames = "userCache",allEntries = true)//删除userCache下所有的缓存数据
public void deleteAll(){
userMapper.deleteAll();
}
spring cache不够灵活,不能设置缓存的过期时间等。