springboot主要特点如下:
Spring Boot 被广泛推荐给 Java 初学者学习的原因主要有以下几点:
学习 Spring Boot 3 的基础知识和步骤如下:
官方文档
Spring的官网
Spring Boot 中文文档
视频教程
黑马程序员SpringBoot3+Vue3全套视频教程
Spring Boot 3 需要 Java 17
为以下构建工具提供了明确的构建支持。
构建工具 | 版本 |
Maven | 3.6.3 及其以上 |
Gradle | 7.x (7.5 及其以上) 和 8.x |
Spring Boot 可以使用 “经典的” Java开发工具,也可以作为命令行工具安装。无论哪种方式,你都需要 Java SDK v17 或更高版本。在你开始之前,你应该使用以下命令检查你当前安装的Java。
$ java -version
为Java开发者提供的安装说明:
Spring Boot与 Apache Maven 3.6.3 或以上版本兼容。 如果你还没有安装Maven,你可以按照 maven.apache.org 上的说明先进行安装。
检查你的maven版本:
$ mvn -v
如果maven版本在3.6.3以下或显示''maven' 不是内部或外部命令,也不是可运行的程序',就去官网下载符合的版本,解压后配置环境变量
Spring Boot 与 Gradle 7.x(7.5或更高版本)或 8.x 兼容 如果你还没有安装Gradle,你可以按照 gradle.org 上的说明进行安装。官网下载界面
检查你的gradle版本:
$ gradle -v
如果gradle版本在7.5以下或显示''gradle' 不是内部或外部命令,也不是可运行的程序',就去官网下载符合的版本,解压后配置环境变量
下载并配置MySQL8
可以使用IDEA或Eclipse,推荐IDEA。
安装:谷歌、Edge、火狐等浏览器均可
作用:对于开发Web应用(如使用Thymeleaf、JSP或Angular、React等前端框架构建的单页面应用),浏览器是用户与之交互的主要工具。开发者可以通过浏览器查看页面布局、设计和功能,并进行初步的用户体验测试。
对于API驱动的应用程序(如RESTful服务),浏览器的作用就有限了。虽然浏览器可以用来发送GET请求并查看返回的JSON或XML数据,但它不支持更复杂的HTTP方法(如POST、PUT、DELETE),也不方便设置请求头、请求体或测试API的多种场景。此外,浏览器不适合执行自动化测试,也不便于管理多个环境或API版本的请求。
因此,除了浏览器之外,还需要安装API开发工具,如Postman或APIPost
API开发工具的主要作用包括:
创建工程时可以将Server URL修改为start.aliyun.com,有初始代码更适合初学者
基本的web项目只需要Spring Web依赖,其他依赖后续可以在pom.xml添加。
@RestController public class TestController { @RequestMapping(value = "/test") public String test(){ return "Hello, world!"; } }
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
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自己处理请求,后面会进一步讲解。
___________ ____ ______/ \__// \__/____\ _/ \_/ : //____\\ /| : : .. / \ | | :: :: \ / | | :| || \ \______/ | | || || |\ / | \| || || | / | \ | || || | / /_\ \ | ___ || ___ || | / / \ \_-_/ \_-_/ | ____ |/__/ \ _\_--_/ \ / /____ / / \ / \______\_________/
初始项目如下:
完整结构如下:
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); } }
pom.xml
pom.xml 是 Maven 项目中的一个核心文件,用于定义项目的构建、报告和依赖关系等信息。在 Spring Boot 3 中,pom.xml 文件同样扮演着重要的角色。
一个基本的 pom.xml 文件通常包含以下几个部分:
application.yml
application.yml 是一个用于配置 Spring Boot 应用程序的文件。它允许开发者为应用程序的不同方面(如数据库连接、安全设置、消息服务等)提供特定的配置。
日志门面是一个抽象层,它定义了日志记录的接口,但不提供具体的日志记录实现。它的目的是提供一种统一的方式来访问日志记录功能,而不关心底层的日志记录系统是什么。这样,无论底层使用的是哪种日志实现,开发者都可以使用相同的方法和API来记录日志。
日志实现是具体实现日志记录功能的库或框架。它实现了日志门面定义的接口,并提供实际的日志记录能力。例如,Logback和Log4J都是日志实现的例子。
要使用@Slf4j注解,首先要保证在pom.xml中引入了Lombok 库的依赖
org.projectlombok lombok true
示例:
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); }
实体类代码臃肿(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修饰的字段之外带有各参数的构造器方法。 |
org.projectlombok lombok
@Data @NoArgsConstructor @AllArgsConstructor
实例
@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 + '\'' + // '}'; // } }
优先级:properties>yml>yaml
在Spring Boot项目中,pom.xml文件是Maven项目对象模型(Project Object Model)的定义文件,它用于管理项目的构建、依赖和插件。
Maven是一个流行的自动化构建工具,它通过pom.xml文件来执行构建过程。本教程全部项目都使用Maven构建。
下面是一个基本的pom.xml文件的结构和各个部分的解释:
4.0.0 com.example my-spring-boot-app 1.0-SNAPSHOT My Spring Boot App Spring Boot 3 example project org.springframework.boot spring-boot-starter-parent 3.0.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-maven-plugin
注意事项:
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文件中的所有值都是字符串,这意味着在将它们赋值给配置类中的不同类型字段时,需要手动进行类型转换。
YAML是“YAML Ain’t Markup Language”(递归缩写为“YAML不是标记语言”)的缩写,它是一种直观的数据序列化格式,可以被用于配置文件。
YAML使用空白字符(空格和缩进)来表示结构层次,它比properties文件更适合表示复杂的配置数据,实际开发基本都使用yml配置文件,有的程序员甚至在创建springboot工程后第一件事就是把配置文件的后缀改为yml。
适合复杂的、具有层级结构的配置场景,尤其是当你需要配置大量的、相关的配置项时。
优点:
清晰的层次结构:YAML使用缩进来表示层级关系,这使得表示复杂的数据结构变得非常清晰。类型支持:YAML支持多种数据类型,如字符串、数字、布尔值等,并且可以自动将值转换为适当的类型。可读性强:YAML文件通常更易于阅读和理解,尤其是对于具有复杂层次结构的配置。
缺点:
轻微的复杂性:YAML的语法比properties文件稍微复杂一些,初学者可能需要一些时间来适应。
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
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("${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); }
@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")); }
先在pom.xml中添加依赖
org.springframework.boot spring-boot-configuration-processor true
创建配置类
@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()); }
介绍:profiles是不同配置选项的集合,它们对应于应用程序的不同运行环境,如开发环境、测试环境和生产环境。每个环境可能需要不同的设置,例如数据库连接、API端点、服务地址等。
使用:
${...}
指定需要使用的配置例如:
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
加载顺序为上面的排列顺序,高优先级配置的属性会生效
注意:当前项目下的/config目录下的配置文件和当前项目的根目录的配置文件因为不符合maven结构,不会被打入jar包
@Componect
:@Component是Spring框架中的一个注解,用于将一个Java类标记为可被Spring容器管理的组件。通过@Component注解,我们可以实现依赖注入和组件的自动化管理。@Autowired
:@Autowired是Spring框架中的一个注解,用于实现自动化的依赖注入。通过在类的字段、构造函数或方法上添加@Autowired注解,我们可以告诉Spring容器自动解析并注入相应的依赖对象。@Resource
:如果多个类实现了同一个接口,需要使用@Resource("名称")来指定一个类@Componect
:声明Bean对象的基础注解,不属于以下三类时,用此注解@Controller
:@Componect的衍生注解,标注在控制器上@Service
:@Componect的衍生注解,标注在业务类上@Respository
:@Componect的衍生注解,标注在数据访问类上(由于与mybatis整合,使用较少)@Controller
@Autowired
注解默认按照类型进行注入,如果存在多个相同类型的bean,就会报错@Primary
:在bean声明注解上面添加该注解,提升注入优先级,使当前bean生效。@Qualifier("value")
:在@Autowired
注解下添加该注解,指定让value的bean生效(注意bean默认为类名首字母小写)@Resource(name="")
:通过该注解,指定要生效的bean的名字,不需要配合@Autowired
Spring Boot 的自动配置原理基于 Spring Framework 的条件化配置和 Bean 的自动化装配。在 Spring Boot 应用启动时,会根据类路径中的 jar 包、Spring Beans 和各种可用的属性设置,自动配置 Spring 应用。
Spring Boot 的自动配置原理是通过一系列的条件注解和内建的自动配置类,根据应用程序中的类路径和用户配置,智能地配置 Spring 应用。这一机制极大地简化了 Spring 应用的配置过程,使得快速开发成为可能。
Spring Beans 是由 Spring 容器管理的对象。
在 Spring 中,一个 Bean 是一个被 Spring 容器实例化、组装和管理的对象。这些对象通常被注入(Inject)到应用程序中的其他对象中。Spring Beans 通常是通过配置元数据定义的,配置元数据可以是 XML 文件、注解或 Java 配置类。
Spring Beans 具有以下特点:
Application Context 是 Spring 容器的一个接口,它提供了应用程序的配置和生命周期管理。它是 BeanFactory 的一个子接口,提供了更多的高级特性,如国际化支持、事件传播、Web 应用上下文等。
Application Context 的主要作用包括:
在实际应用中,通常会通过创建一个或多个 Spring Beans,并将它们注册到 Application Context 中,然后通过 Application Context 获取和管理这些 Bean。这样,开发者可以专注于业务逻辑的实现,而无需担心对象的创建和管理。
请求协议和响应协议在前置知识有详细介绍。
@RequestMapping
注解来映射HTTP请求到相应的处理方法。使用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参数
@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(value = "/path", method = RequestMethod.GET) public responseType handlerMethod() { // ... }
其中,value
属性指定了请求的实际地址,method
属性指定了请求的方法类型,可以是 GET、POST、PUT 或者 DELETE 等。
@RequestMapping注解可以接受多种属性,例如:
Spring Boot 提供了几个 @RequestMapping
的专用变体,这些变体都带有特定的 HTTP 请求方法,使得代码更加清晰易懂。
@GetMapping
是 @RequestMapping
的 GET 请求专用版,其基本语法如下:
@GetMapping("/path") public responseType handlerMethod() { // ... }
@PostMapping
是 @RequestMapping
的 POST 请求专用版,其基本语法如下:
@PostMapping("/path") public responseType handlerMethod() { // ... }
@PutMapping
是 @RequestMapping
的 PUT 请求专用版,其基本语法如下:
@PutMapping("/path") public responseType handlerMethod() { // ... }
@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
的主要作用是将HTTP请求体中的数据自动转化为对象实例。这种转化过程通常是基于请求体中的JSON或XML格式数据进行的。
@RequestBody
通常与 @RequestMapping
或 @PostMapping
等注解一起使用,其基本语法如下:
@RequestMapping("/jsonParam") public String jsonParam(@RequestBody User user){ System.out.println(user); return "OK"; }
在这个例子中,@RequestBody User user
指定了要从请求体中获取一个 User
对象。当请求体是JSON格式时,Spring Boot会自动将其转化为 User
对象。
@RequestBody
有一个主要的属性:
在使用 @RequestBody
时,需要注意以下几点:
@RequestBody
主要用于处理请求体中的数据,因此通常与POST请求一起使用。对于GET请求,由于没有请求体,所以不能使用 @RequestBody
。@RequestBody
会将请求体中的JSON或XML格式数据转化为对象实例,这个过程需要依赖Jackson库,因此在项目中需要加入Jackson的依赖。@RequestBody
时,一定要确保参数类型与请求体中的数据类型相符,否则可能会出现数据解析错误的问题。基本示例:
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; }
// 数组参数绑定测试 @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 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"; }
@RequestBody
注解标识接收:
@RequestMapping("/jsonParam") public String jsonParam(@RequestBody User user){ log.info(user.toString()); return "OK"; }
接收单个路径参数:
@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 等。以下是一些常用的方法:
这是最常见的数据返回方式。在 Spring Boot 中,你可以使用 @RestController
或 @ResponseBody
注解来直接返回对象,这些对象会被自动转换成 JSON 格式。
@RestController public class MyController { @GetMapping("/api/users") public List getUsers() { return userService.findAll(); } }
如果你需要返回 XML 格式的数据,你可以使用 @RequestMapping
注解,并设置 produces
属性为 "application/xml"。
@RequestMapping(value = "/api/users", produces = "application/xml") public List getUsers() { return userService.findAll(); }
CSV是一种简单的文件格式,用于存储表格数据,如电子表格或数据库。CSV 文件以纯文本形式存储表格数据,其中每行表示表格中的一行,而行中的每个单元格数据由逗号分隔。
CSV 文件通常用于数据交换,因为它们可以被多种不同的应用程序和系统读取和写入。你可以将电子表格数据导出为 CSV 文件,然后在数据库管理工具中导入这些数据。
要返回 CSV 格式的数据,你可以使用 @ResponseBody
和 ResponseEntity
。
@ResponseBody @RequestMapping(value = "/api/users/csv", produces = "text/csv") public ResponseEntity 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 getUser(@RequestParam Long id) { User user = userDetailsService.findById(id); return ResponseEntity.ok(user); } }
在这个例子中,"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> 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!"); }
上面只是简单的响应类,实际开发处理响应结果的方式可能更加复杂。
可以通过return new ModelAndView返回一个新视图。
在thymeleaf详细介绍。
RESTful Web 服务是一种网络服务的架构风格,它使用 HTTP 协议作为通信手段,并利用 URI 来访问资源,它使用统一的接口和状态无关的请求来构建可扩展的Web服务。
实际开发中,springboot web程序除了处理请求,还有其他复杂的功能。例如:业务逻辑、数据访问、数据传输、异常处理、登录认证与授权、消息传递、缓存等。
我们不可能将所有功能放在一个程序中,就像我们学习java基础时,将一个个功能从main函数中封装到不同函数和不同文件中一样。我们需要将springboot web程序根据单一职责原则拆分为一个个组件,而这个组件也不过是一个个文件夹(软件包)。
单一职责原则:一个类或一个方法,就只做一件事情,只管一块功能。 这样就可以让类、接口、方法的复杂度更低,可读性更强,扩展性更好,也更利用后期的维护。
那么,我们要如何根据单一职责原则拆分springboot web程序的各个功能呢?分层架构
软件架构风格。
分层架构的好处:
分层架构通常包括以下层次:
目前我们只是开发基本的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
等注解进行数值范围的校验。@PreAuthorize
、@Secured
等注解来定义方法级别的安全约束。@RequestMapping
、@GetMapping
、@PostMapping
等注解确保请求方法(GET、POST、PUT、DELETE等)与控制器方法定义相匹配。@CrossOrigin
注解或者在配置类中设置全局的跨域处理规则,以允许或限制跨域请求。@ControllerAdvice
或@ExceptionHandler
注解定义全局的异常处理逻辑,以捕获验证失败或其他异常情况,并返回适当的错误响应。MethodArgumentNotValidException
异常。可以通过定义全局异常处理器来捕获这类异常,并返回统一的错误响应。在Spring Boot Web开发中,业务逻辑层
是应用程序的核心,它负责处理来自表示层
的请求,执行业务规则,并与数据访问层
进行交互。
业务逻辑层通常是由一系列的@Service注解的类组成,这些类包含了业务逻辑处理的公共方法和私有方法。这些服务类会注入对应的Repository接口,以进行数据访问操作。通过这样的设计,业务逻辑层为表示层提供了一个清晰的API接口,同时隐藏了数据访问的细节,使得业务逻辑更加集中和易于管理。
以下是业务逻辑层的一般处理流程:
@Transactional
注解来声明事务边界,保证业务操作的ACID特性。数据访问层比较复杂,可以使用mybatis或Spring Data JPA与数据库进行交互。这里先介绍mybatis,后面在学习spring Data时进一步了解
具体见下面的mybatis章节。
实际开发中,表示层会先接收前端的请求,然后调用业务逻辑层的对应方法;业务逻辑层进行对应处理后,会调用数据访问层,访问和处理相关数据;数据访问层处理后,将处理结果返回给业务逻辑层,业务逻辑层再放回给显示层。
这其实函数的相互的调用和返回,从而将一个冗余的项目根据职责拆分到不同的层中。
这里先做简单的了解,后面会给出相关示例。
MVC(Model-View-Controller)是一种软件设计模式,用于将应用程序的逻辑层和表现层分离。
Spring MVC 的主要组件:
在Spring Boot中,可以使用Spring MVC框架来实现MVC模式。Spring MVC提供了一组注解和类,用于定义和处理RESTful API的请求映射、请求参数绑定、数据验证、响应处理等。
分层架构与MVC架构的区别与联系:
在Spring Boot中使用Spring MVC框架来实现MVC模式需要以下步骤:
@Controller public class MyController { // 处理GET请求的示例方法 @GetMapping("/hello") public String sayHello() { return "hello"; } }
spring.mvc.view.prefix=/WEB-INF/views/ spring.mvc.view.suffix=.html
@Controller public class MyController { @GetMapping("/hello") public String sayHello() { return "hello"; } }
@Controller public class MyController { @GetMapping("/hello") public String sayHello(@RequestParam("name") String name, Model model) { model.addAttribute("name", name); return "hello"; } }
RESTful端点是指用于处理RESTful Web服务请求的特定URL路径。它们是客户端和服务器之间通信的入口点,通过HTTP方法(如GET、POST、PUT、DELETE等)和URL路径来定义对资源的操作。
在Spring Boot中,可以使用Spring MVC框架来创建RESTful端点。以下是创建RESTful端点的一般步骤:
@RestController public class MyController { // 处理GET请求的示例方法 @GetMapping("/users") public List getAllUsers() { // 从数据库或其他数据源中获取用户列表 List 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); } }
这是一个简单的示例,演示了如何创建基本的RESTful端点。还可以使用其他注解和功能来处理异常、版本控制、分页、过滤等更复杂的场景。
程序中不可避免的会遇到异常,异常可能会出现不符合api文档中的响应结果
@RestContrlorAdvice
和@ExceptionHandler
@RestContrllorAdvice
=@ContrllorAdvice
+@RespponseBody
@RespponseBody
会将结果转换为json格式响应会前端@RestControllerAdvice public class GlobalExceptionHandler { // 捕获所有异常 @ExceptionHandler(Exception.class) public Result ex(Exception ex){ ex.printStackTrace(); return Result.error("对不起,操作失败,请联系管理员"); } }
MyBatis是一个强大的持久层框架,它内部封装了对JDBC的操作,让开发者只需要关注SQL本身,而不需要处理繁琐的数据库连接、SQL构造、结果集处理等JDBC代码。
以下是使用MyBatis的数据访问层的工作流程:
application.properties
或application.yml
文件中配置MyBatis的相关设置,如mapper文件的位置、数据源信息等。@Transactional
注解来声明事务边界。参数占位符
#{...}
#{...}
替换为?
,生成预编译sql,会自动设置参数值${...}
Spring Boot整合MyBatis进行数据库操作时,可以使用注解或者XML文件来编写SQL语句。
IDEA推荐安装插件mybatisX,方便xml映射文件操作。
创建数据库、表,配置springboot工程
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, '高三二班');
... ... com.mysql mysql-connector-j runtime org.mybatis.spring.boot mybatis-spring-boot-starter 3.0.3 org.mybatis.spring.boot mybatis-spring-boot-starter-test 3.0.3 test ...
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
- 这些是连接数据库时使用的参数。
配置日志输出到控制台:
#配置mybatis日志,指定输出到控制台(记住log即可) mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
连接数据库
@Data @AllArgsConstructor @NoArgsConstructor public class Student { private int id; private String name; private int age; private char gender; private String className; }
在mapper包下创建一个StudentMapper.java文件,用于数据库操作。
使用两个注解:
在数据层访问层方法前添加 @Select
注解,在注解在传递sql语句进行查询
@Mapper @Repository public interface StudentMapper { @Select("SELECT * FROM students WHERE gender = '女';") public List findGirl() ; @Select("SELECT * FROM students WHERE id = #{id};") List findById(int id);//不加public也可,因为interface中的方法都是公用的 }
在/test包下的测试类测试
@SpringBootTest class XXXApplicationTests { @Autowired public StudentMapper studentMapper; @Test void getGirl() { List 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("插入失败"); } }
// 插入 @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
手动封装 //法一:给字段取别名 @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
public Connection getConnection() throws SQLException;
com.alibaba druid 最新版本
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数据源专有配置:
示例:
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
示例:
mybatis: configuration: map-underscore-to-camel-case: true # 配置mapper xml文件所在路径 mapper-locations: classpath:mapping/*.xml # 配置实体类所在位置 type-aliases-package: com.fl.boot.entity
@Mapper @Repository public interface StudentMapper { public List findGirl() ; public List findById(int id); public int insertStudent(Student student); public int deleteById(int id); public int updateClassName(int id, String className); }
中编写sql insert into student(id,sname,classId,birthday,email) values (#{id},#{sname},#{classId},#{birthday},#{email}); DELETE FROM students WHERE id = #{id} UPDATE students SET class_name = #{className} WHERE id = #{id};
讲解:
标签的namespace
属性用于指定Mapper接口,必须传入全限定名
、
、
、
进行增删改查id
属性用于指定Mapper接口中的方法,必须和要指定的方法一致parameterType
:指定输入参数的类型,可以是简单类型、Map、POJO等。注意,参数只能传入一个,如果要传入多个参数,需要在Mapper.java中指定参数如前面的updateClassName方法,需要修改为:
public int updateClassName(@Param("id") int id, @Param("className") String className);
特性:
和
特性(只有两个共有属性)
特性在Test类进行测试:
@SpringBootTest class XXXApplicationTests { @Autowired public StudentMapper studentMapper; @Autowired private SqlSessionFactory sqlSessionFactory; @Test void getGirl() { List 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("更新失败"); } } }
补充:复制全类名
在xml映射文件中,可以用
定义数据库表记录和Java对象之间的映射关系。
可以使用
为数据表的主键,
为其他结果列, column
为数据表的列名,property
为实体类的属性名
在MyBatis的
前端可能有上面这样的筛选列表,那么发送的请求中,可能没有参数,这时我们需要将所有数据返回给前端,而如果含有一个或多个参数,这时我们就需要返回特定筛选条件的数据。我们可以使用动态SQL处理这样参数会变化的SQL语句。
动态SQL是指根据程序运行时的条件动态生成的SQL语句。MyBatis通过XML映射文件或注解提供了一系列强大的动态SQL功能,允许你根据不同的条件构建不同的SQL语句。这使得MyBatis能够灵活地应对复杂的数据库操作需求。
动态SQL的主要特点是能够在SQL语句中包含条件判断、循环和表达式计算等逻辑,从而实现SQL的动态构建
MyBatis提供了多种动态SQL元素:
示例:查询学生,可以指定性别,名字的姓,年龄和班级
//将int类型改为Integer,char类型改为Character,否则不能为null public List findStudent(@Param("name") String name,@Param("gender") Character gender,@Param("age") Integer age,@Param("className") String className);
或
测试:
@Test void findStudent() { List students = studentMapper.findStudent(null, '男', null, "高三一班"); for (Student student : students) { System.out.println(student); } System.out.println("===============SUCCESS!================"); }
public List findStudentByIdList(@Param("ids") List idList);
@Test void findStudentByIdList() { List idList = List.of(1, 2, 3); List students = studentMapper.findStudentByIdList(idList); for (Student student : students) { System.out.println(student); } System.out.println("===============SUCCESS!================"); }
UPDATE students name = #{name}, age = #{age}, gender = #{gender}, class_name = #{className}, WHERE id = #{id}
元素用于自定义字符串的截取规则,可以用来去除或添加前缀、后缀,以及根据需要包含或忽略指定的字符串。它可以用于WHERE子句,也可以用于SET子句或其他任何需要根据条件动态生成SQL的部分。
示例:将前面的findStudent的where改为使用trim
作用:将重复的sql提取成可以重用的sql片段
:用于定义可以在其他SQL元素中重用的SQL片段。它通常用于定义那些在多个查询中重复出现的代码,比如表名、列名或者复杂的计算表达式。你甚至可以使用它定义整个SQL语句,不过实际开发中用的很少。
:通过属性refid,指定包含的sql片段
在下面的例子里,使用*
。
id,name,age,gender,class_name
在数据库阶段我们已学习过事务了,我们知道:
事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。确保了一系列数据库操作要么全部成功执行,要么全部失败回滚,从而保证了数据的一致性和完整性。
事务的操作主要有三步:
Spring Boot和MyBatis的整合简化了传统的事务管理方式,使得开发者能够更容易地实现事务控制,同时保持代码的简洁性和可维护性。
@Transactional作用:就是在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。
@Transactional注解:我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。
@Transactional注解书写位置:
常在业务方法上加上 @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); } }
在需要事务管理的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特有异常,例如插入重复索引的数据,或删除不存在的数据等
常见异常:
我们可以在Test类添加try...catch...语句,可是以后业务一旦复杂起来,需要这样处理的方法多了怎么办呢?要去一个一个try-catch吗?多麻烦啊!
所以,我们使用异常拦截器进行全局异常捕获。
@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(); } }
@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 "更新成功"; } }
http://localhost:8080/updateStudent?id=4&name=FL
注意,如果直接使用测试类进行测试,该错误不会被全局异常处理器捕获
@Test void updateStudent2(){ studentService.updateStudent(7,"蔡徐坤",null,null,"高三三班"); }
springboot3.2配置如下(其他版本可能不同)
com.baomidou mybatis-plus-boot-starter 3.5.5 org.mybatis mybatis-spring org.mybatis mybatis-spring 3.0.3
创一个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 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; }
@Mapper @Repository public interface GoodsMapper extends BaseMapper { }
在 MyBatis-Plus 中,selectList
方法是一个用于查询数据列表的便捷方法。这个方法通常在继承自 BaseMapper
的接口中提供,它会根据你提供的查询条件来返回符合条件的数据列表。
这个方法的使用非常简单,只需要提供一个查询条件构造器作为参数即可。MyBatis-Plus 提供了两种查询条件构造器:QueryWrapper
和 LambdaQueryWrapper
。注意,如果你提供null,就会查询所有数据。
selectList
方法返回的是一个 List
类型的对象,包含了符合查询条件的所有实体类对象。你可以直接操作这个列表,或者将其转换为其他类型的集合。
QueryWrapper
QueryWrapper queryWrapper = new QueryWrapper<>();
eq
、ne
、gt
等。@Test public void testSelectAll() { List goods = goodsMapper.selectList(null); goods.forEach(System.out::println); System.out.println("测试成功"); }
LambdaQueryWrapper
LambdaQueryWrapper lambdaQuery = new LambdaQueryWrapper<>();
eq
、ne
、gt
等。List list = goodsMapper.selectList(lambdaQuery.eq(Goods::getId, 1));
/** * 查找price>30000并且star=4的记录或者price<1000的记录,记录只显示id、name、price、star字段 */ @Test public void testSelectByPriceAndStar() { QueryWrapper 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 = goodsMapper.selectList(queryWrapper); goods.forEach(System.out::println); System.out.println("测试成功"); }
limit
@Test public void testSelectByPage() { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.last("limit 0,2"); List goods = goodsMapper.selectList(queryWrapper); goods.forEach(System.out::println); System.out.println("测试成功"); }
Page
类进行分页。@Test public void testSelectByPage2() { Page page = new Page<>(1, 2); //selectPage需要两个参数,一个是Page,另一个是QueryWrapper List 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 page = new Page<>(1, 2); QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("typeName", "休闲零食"); List 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 提供了一系列的方法来插入数据,以下是一些常用的插入方法及其代码示例:
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 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 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("测试成功"); }
@Test public void testDeleteById() { int result = goodsMapper.deleteById(17); System.out.println("影响行数:" + result); System.out.println("测试成功"); }
@Test public void testDeleteByPrice() { int result = goodsMapper.delete(new QueryWrapper().lt("price", 1000)); System.out.println("影响行数:" + result); System.out.println("测试成功"); }
在Spring Boot中,逻辑删除是一种数据删除的范式,它并不是真正地从数据库中移除数据,而是通过设置一个标志位(通常是一个布尔值字段)来标记数据为“已删除”状态。这样做的目的是为了能够在必要时恢复数据或者进行审计跟踪,同时也避免了实际删除操作可能带来的性能开销。
逻辑删除通常涉及以下几个步骤:
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
@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("测试成功"); }
PageHelper是一个MyBatis的分页插件,它能够非常简单地实现MyBatis的物理分页。PageHelper与MyBatis和MyBatis-Plus兼容,你可以在不改变原有代码的基础上,通过简单的配置和调用,实现分页功能。
MyBatis-Plus虽然提供了强大的ORM功能和内置的分页插件,但对于一些老项目或者特定需求,可能会更倾向于使用PageHelper。
导入依赖
com.github.pagehelper pagehelper-spring-boot-starter 1.4.6
测试
@Test public void testSelectByPage3() { // 第一个参数表示当前页数,第二个参数表示每页显示的记录数,第三个参数表示排序字段 PageHelper.startPage(1, 2,"price desc"); List 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 pageInfo = PageHelper.startPage(page, pageSize); // 查询分页数据,返回分页结果(只查询id、name、username、phone、status) QueryWrapper wrapper = new QueryWrapper<>(); wrapper.select("id", "name", "username", "phone", "status"); List list = adminMapper.selectList(null, wrapper); return new PageResult(pageInfo.getTotal(), list); }
在实现BaseMapper类的方法中,可以自己实现自定义的方法,然后通过注解或xml进行数据库操作。实现方式与mybatis一致。
实际上mybatis-plus提供的方法已经实现了大部分功能,除了有极其复杂的需求,否则不推荐这种方式。
thymeleaf 是一个 Java 模板引擎,用于将模板文件(通常是 HTML)与服务器端数据结合起来,生成动态的 HTML 页面。
实际开发中,通常采用前后端分离的开发模式,thymeleaf 使用并不多。
优势:
Thymeleaf 是一个强大的 Java 模板引擎,它提供了一系列的标签来帮助开发者生成动态的 HTML 页面。
实际开发中,页面通常由前端负责。生成动态的 HTML 页面可以在前端使用vue完成,所以这些标签了解一下即可。
以下是一些常用的 Thymeleaf 标签:
${expression}
:用于嵌入表达式,如变量、方法调用、流程控制等。${...}
:用于绑定表达式到页面元素,如文本框、下拉列表等。*{...}
:用于注释表达式,不会影响页面渲染。
:用于遍历集合或数组,生成列表项。
:用于条件渲染,如果条件为 true,则包含其中的内容。
:与
相反,当条件为 false 时,则包含其中的内容。
:用于多条件分支,类似于 Java 中的 switch 语句。
:与
一起使用,定义每个分支的条件和内容。
:用于创建一个对象,可以用来访问属性。
:用于创建一个临时变量,并设置其值。 org.springframework.boot spring-boot-starter-thymeleaf
spring: thymeleaf: prefix: classpath:/templates/ suffix: .html mode: HTML encoding: UTF-8 cache: false # 开发时建议设置为 false,生产环境设置为 true
Title 登录
默认消息
传递模型数据: 在处理方法中,可以使用 Model 或 ModelMap 对象来传递数据到视图中。可以通过 addAttribute 方法将数据添加到模型中,然后在 Thymeleaf 模板中使用这些数据。
import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @Controller public class MyController { @GetMapping("/login") public String index(Model model) { model.addAttribute("message", "欢迎使用 Thymeleaf!"); return "login"; } }
http://localhost:8088/login
注意我在application.yml中修改了端口号
server: port: 8088
效果:
页面跳转通常指的是当用户请求一个URL时,服务器返回一个页面给用户的操作。在Spring Boot中,你可以通过控制器(Controller)中的方法来处理请求并返回页面。
见前面的示例。
重定向指的是当用户请求一个URL时,服务器返回一个状态码,告诉浏览器去请求另一个URL。在Spring Boot中,你可以使用Spring MVC的RedirectView对象或"redirect:"前缀字符串进行重定向。
重定向用于将用户从一个URL重定向到另一个URL,适用于表单提交后防止页面刷新导致的重复提交。
方式一:使用 "redirect"前缀字符串,类的注解不能使用@RestController,要用@Controller
RequestMapping("/") public String index(Model model) { model.addAttribute("message", "欢迎使用 Thymeleaf!"); return "redirect:/login";//路由重定向 }
方式二:使用Spring MVC的RedirectView对象,类的注解不能使用@RestController,要用@Controller
@GetMapping("/index") public RedirectView redirectSource() { // 创建RedirectView对象 RedirectView redirectView = new RedirectView(); // 设置要重定向到的URL redirectView.setUrl("/"); // 如果需要,添加参数 redirectView.addStaticAttribute("message", "This is a redirected message!"); // 返回RedirectView对象 return redirectView; } @GetMapping("/") public String redirectTarget() { // 处理重定向后的请求并返回视图名称 return "/index"; }
方式三(不推荐):使用servlet 提供的API,类的注解可以使用@RestController,也可以使用@Controller
@RequestMapping(value="/test" , method = RequestMethod.GET) public void test( HttpServletResponse response) throws IOException { response.sendRedirect("/login"); }
请求转发是指当一个请求到达服务器后,服务器内部将请求转发给另一个处理器或资源,而客户端(浏览器)不知道这一过程(请求转发不会改变浏览器地址栏中的URL)。在Spring Boot中,你可以使用"forward" 前缀字符串或使用Spring MVC的注解和方法来实现请求转发。
请求转发是在服务器内部转发请求,客户端感知不到,适用于服务器内部资源之间的跳转。
方式一:使用 "forward" 前缀字符串,类的注解不能使用@RestController 要用@Controller
RequestMapping("/") public String index(Model model) { model.addAttribute("message", "欢迎使用 Thymeleaf!"); return "forward:/login";//路由重定向 }
方式二:使用Spring MVC的注解和方法来实现请求转发
@Controller public class MyController { @GetMapping("/forwardSource") public ModelAndView forwardSource() { // 创建ModelAndView对象 ModelAndView modelAndView = new ModelAndView(); // 设置要转发到的视图名称 modelAndView.setViewName("forward:/forwardTarget"); // 如果需要,添加模型数据 modelAndView.addObject("message", "This is a forwarded message!"); // 返回ModelAndView对象 return modelAndView; } @GetMapping("/forwardTarget") public String forwardTarget() { // 处理转发后的请求并返回视图名称 return "login"; } }
案例1:重定向至404页面
Title 发生了404错误
@Controller @RequestMapping({"${server.error.path:${error.path:/error}}"}) @Slf4j public class BasicErrorController extends AbstractErrorController { public BasicErrorController(ErrorAttributes errorAttributes) { super(errorAttributes); } @RequestMapping(produces = "text/html") public ModelAndView handleError(HttpServletRequest request, HttpServletResponse response) { Map model = this.getErrorAttributes(request, ErrorAttributeOptions.defaults()); HttpStatus status = this.getStatus(request); response.setStatus(status.value()); // 默认视图 String viewName = "error/error"; if (status == HttpStatus.NOT_FOUND) { viewName = "error/404"; } else if (status == HttpStatus.INTERNAL_SERVER_ERROR) { viewName = "error/500"; } // 日志记录错误信息 logError(status, model); return new ModelAndView(viewName, model); } private void logError(HttpStatus status, Map model) { // 实现日志记录逻辑 log.error("Error Status: {}", status); } }
这样就实现了基本的根据不同的HTTP状态码返回不同的错误视图。
会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
会话跟踪方案:
会话跟踪技术方案对比:
全称:JSON Web Token (官网)
场景:登录认证。
①登录成功后,生成令牌
②后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理
io.jsonwebtoken jjwt 0.9.1
@Test // 当运行不与整个工程有关的测试时,可以先将@SpringBootTest注解注掉,只加载测试类 public void testGenJwt(){ // 自定义内容 HashMap 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); }
@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 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("用户名或密码错误"); }
概念: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() { } }
@WebFilter(urlPatterns = "/*") //拦截所有请求 @WebFilter(urlPatterns = "/emps/*") //拦截目录请求 @WebFilter(urlPatterns = "/login") //拦截具体请求
@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); } }
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"); }
@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框架中,AOP是一种编程范式,它允许开发者定义跨多个对象的横切关注点,例如日志、事务管理和安全等。在Spring框架中,AOP是通过代理模式实现的。
需求:统计各个业务层方法执行耗时。
实现步骤:
为演示方便,可以自建新项目springboot-aop-quickstart
org.springframework.boot spring-boot-starter-aop
1. 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
2. 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
3. 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
4. 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
5. 目标对象:Target,通知所应用的对象
Spring中AOP的通知类型:
@Slf4j @Component @Aspect public class MyAspect { //前置通知 @Before("execution(* com.itheima.service.*.*(..))") public void before(JoinPoint joinPoint){ log.info("before ..."); } //环绕通知 @Around("execution(* com.itheima.service.*.*(..))") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { log.info("around before ..."); //调用目标对象的原始方法执行 Object result = proceedingJoinPoint.proceed(); //原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了 log.info("around after ..."); return result; } //后置通知 @After("execution(* com.itheima.service.*.*(..))") public void after(JoinPoint joinPoint){ log.info("after ..."); } //返回后通知(程序在正常执行的情况下,会执行的后置通知) @AfterReturning("execution(* com.itheima.service.*.*(..))") public void afterReturning(JoinPoint joinPoint){ log.info("afterReturning ..."); } //异常通知(程序在出现异常的情况下,执行的后置通知) @AfterThrowing("execution(* com.itheima.service.*.*(..))") public void afterThrowing(JoinPoint joinPoint){ log.info("afterThrowing ..."); } }
注意:@Around
环绕通知需要自己调用ProceedingJoinPoint.proceed()来让原始方法执行,且返回类型必须指定为Object
@Pointcut
方法上,其他需要使用直接引用即可@Pointcut("execution(* com.itheima.service.*.*(..))") private void pt(){} //如果pt方法是public的,则在其他类中也可以使用 //前置通知 @Before("pt()") public void before(JoinPoint joinPoint){ log.info("before ..."); } ...//其他同理
@Slf4j @Component @Aspect @Order(2) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行) public class MyAspect2 { //前置通知 @Before("execution(* com.itheima.service.*.*(..))") public void before(){ log.info("MyAspect2 -> before ..."); } //后置通知 @After("execution(* com.itheima.service.*.*(..))") public void after(){ log.info("MyAspect2 -> after ..."); } }
@Slf4j @Component @Aspect @Order(3) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行) public class MyAspect3 { //前置通知 @Before("execution(* com.itheima.service.*.*(..))") public void before(){ log.info("MyAspect3 -> before ..."); } //后置通知 @After("execution(* com.itheima.service.*.*(..))") public void after(){ log.info("MyAspect3 -> after ..."); } }
@Slf4j @Component @Aspect @Order(1) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行) public class MyAspect4 { //前置通知 @Before("execution(* com.itheima.service.*.*(..))") public void before(){ log.info("MyAspect4 -> before ..."); } //后置通知 @After("execution(* com.itheima.service.*.*(..))") public void after(){ log.info("MyAspect4 -> after ..."); } }
切入点表达式:
execution
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带?
的表示可以省略的部分
@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
可以使用通配符描述切入点
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数*
号代替(任意返回值类型)*
号代替,代表任意包(一层包使用一个*
)..
配置包名,标识此包以及此包下的所有子包*
号代替,标识任意类*
号代替,表示任意方法*
配置参数,一个任意类型的参数..
配置参数,任意个任意类型的参数注意:
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))
@annotation
已经学习了execution切入点表达式的语法。那么如果我们要匹配多个无规则的方法,比如:list()和 delete()这两个方法。这个时候我们基于execution这种切入点表达式来描述就不是很方便了。而在之前我们是将两个切入点表达式组合在了一起完成的需求,这个是比较繁琐的。
我们可以借助于另一种切入点表达式annotation来描述这一类的切入点,从而来简化切入点表达式的书写。
实现步骤:
自定义注解:MyLog
@Target(ElementType.METHOD)//指定目标类型 @Retention(RetentionPolicy.RUNTIME)//指定生效时间 public @interface MyLog { }
业务类:DeptServiceImpl
@Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Override @MyLog //自定义注解(表示:当前方法属于目标方法) public List list() { List deptList = deptMapper.list(); //模拟异常 //int num = 10/0; return deptList; } @Override @MyLog //自定义注解(表示:当前方法属于目标方法) public void delete(Integer id) { //1. 删除部门 deptMapper.delete(id); } @Override public void save(Dept dept) { dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); deptMapper.save(dept); } @Override public Dept getById(Integer id) { return deptMapper.getById(id); } @Override public void update(Dept dept) { dept.setUpdateTime(LocalDateTime.now()); deptMapper.update(dept); } }
切面类
@Slf4j @Component @Aspect public class MyAspect6 { //针对list方法、delete方法进行前置通知和后置通知 //前置通知 @Before("@annotation(com.itheima.anno.MyLog)") public void before(){ log.info("MyAspect6 -> before ..."); } //后置通知 @After("@annotation(com.itheima.anno.MyLog)") public void after(){ log.info("MyAspect6 -> after ..."); } }
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
@Slf4j @Component @Aspect public class MyAspect7 { @Pointcut("@annotation(com.itheima.anno.MyLog)") private void pt(){} //前置通知 @Before("pt()") public void before(JoinPoint joinPoint){ log.info(joinPoint.getSignature().getName() + " MyAspect7 -> before ..."); } //后置通知 @Before("pt()") public void after(JoinPoint joinPoint){ log.info(joinPoint.getSignature().getName() + " MyAspect7 -> after ..."); } //环绕通知 @Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { //获取目标类名 String name = pjp.getTarget().getClass().getName(); log.info("目标类名:{}",name); //目标方法名 String methodName = pjp.getSignature().getName(); log.info("目标方法名:{}",methodName); //获取方法执行时需要的参数 Object[] args = pjp.getArgs(); log.info("目标方法参数:{}", Arrays.toString(args)); //执行原始方法 Object returnValue = pjp.proceed(); return returnValue; } }