springBoot
发表于:2025-07-28 | 分类: java学习笔记

SpringBoot

SpringBoot 是目前流行的微服务框架,其目的是用来简化新 Spring 应用的初始化搭建以及开发过程。SpringBoot 提供了很多核心功能,比如自动化配置 starter(启动器)简化 Maven 配置、内嵌 Servlet 容器、应用监控等功能,让我们可以快速构建企业级应用程序。

SpringBoot 入门

脚手架

软件工程中的脚手架是用来快速搭建一个小的可用的应用程序的骨架,将开发过程中要用到的工具,环境都配置好,同时生成必要的模板代码。Spring Initializr 是创建 Spring Boot 的脚手架,IDEA 内置了此工具,可以帮助我们快速创建项目。

Spring Initializr 脚手架的 Web 地址

阿里云脚手架

项目中的.mvnmvnw.cmdHELP.mdmvnw可以删除。

application.properties配置文件中配置server.port可以解决可能出现的 8080 端口冲突的问题。

starter 启动器

starter 是一组依赖描述,应用中包含 starter,可以获取 Spring 相关技术的一站式的依赖和版本。不必复制、粘贴代码,通过 starter 能够快速启动并运行项目。包含了依赖和版本、传递依赖和版本、配置类、配置项。

父项目

默认的 SpringBoot 项目继承了 SpringBoot 父项目,该父项目包含了 SpringBoot 相关的依赖管理和版本管理,直接继承该父项目可以直接使用 SpringBoot 相对应的依赖和版本。如果不继承父项目,想要让项目继承自己的父项目,可以通过以下配置获得相关依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Core

核心注解

SpringBoot 核心注解:@SpringBootApplication,作用在 main 方法所在的类之上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* SpringBootApplication是一个复合注解
* 包含了以下三个注解:
* @SpringBootConfiguration
* 该注解包含了Configuration注解的功能,有这个注解的类就是配置类
* @EnableAutoConfiguration
* 该注解可以开启自动配置,可以将Spring和第三方库中的对象创建好,注入到容器中
* @ComponentScan
* 自动扫描器,SpringBoot约定扫描启动类所在的根包,故我们的类必须写在根包下
*/
@SpringBootApplication
public class Lession06PackageApplication {

public static void main(String[] args) {
// run方法第一个参数是源类,也就是配置类
// 从源类开始创建各种对象并注入到容器之中
// 该方法的返回值是一个容器对象ApplicationContext,可以从容器获取对象
ApplicationContext ctx = SpringApplication.run(Lession06PackageApplication.class, args);
Date bean = ctx.getBean(Date.class)
}

}

运行 SpringBoot项目方式

  • 开发工具,例如 IEDA 执行 main 方法

  • Maven 插件,mvn spring-boot:run

  • java -jar myWeb.jar

    SpringBoot 可以将项目打包为 jar 包 或者 war 文件,因为 SpringBoot 内嵌了 web 服务器,例如 tomcat。能够以 jar 方式运行 web 应用。无需安装 tomcat 程序。

SpringBoot 的 jar 文件和普通 jar 文件的区别

项目 SpringBoot jar 普通 jar
目录 BOOT-INF:应用的 class 和依赖 jar、META-INF:清单、org.springframework.boot.loader:spring-boot-loader 模块类 META-INF:清单、class 的文件夹:jar 中的所有类
BOOT-INF class:应用的类、lib:应用的依赖 没有BOOT-INF
spring-boot-loader 执行 jar 的 SpringBoot 类 没有此部分
可执行

外部化配置

应用程序 = 代码 + 数据(数据库,文件,url)

应用程序的配置文件:SpringBoot 允许在代码之外,提供应用程序运行的数据, 以便在不同的环境中使用相同的应用程序代码,避免硬编码,提供系统的灵活性。项目中常使用 propertiesyaml 文件进行配置,其次是命令行参数。

配置文件的名称为application,如果 propertiesyml 类型都存在,优先加载 properties。不过,考虑到阅读性,我们更推荐使用 yml 格式的配置文件。

配置文件格式

配置文件格式有两种:propertiesyaml(yml)properties是 Java 中的常用一种配置文件格式,key = value。key 是唯一的,文件扩展名是properties

yaml也看做是yml,是一种做配置文件的数据格式,基本的语法是key:[空格]值。yml 文件文件扩展名是 yaml 和 yml(常用)。

yml格式特点:

YAML基本语法规则:

  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进可以使用空格,不允许使用 Tab 键
  • 缩进的空格数目不重要,相同层级的元素左侧对齐即可
  • #字符表示注释,只支持单行注释。#放在注释行的第一个字符

YAML 缩进必须使用空格,而且区分大小写,建议编写YAML文件只用小写和空格。

YAML支持三种数据结构

  • 对象:键值对的集合,又称为映射、哈希、字典
  • 数组:一组按次序排列的值,又称为序列、列表
  • 标量:单个的、不可再分的值

properties 配置

在 properties 文件中指定 key 和 value:

1
2
app.owner=zhangsan
app.password=040809

利用注解使用 properties 中的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class SomeService {

// 使用@Value注入值
@Value("${app.owner}")
private String name;

@Value("${app.password}")
private String password;

// ${key:默认值},找不到key就使用默认值给属性赋值
@Value("${app.port:8080}")
private Integer port;

public void printValue() {
System.out.println(name + " " + password + " " + port);
}
}

yml 扁平化

在 yml 文件中也可以指定配置的 key 和 value:

1
2
3
4
5
6
7
8
9
# 编写配置项,格式 -> key: 值
# 通过换行表示层级关系,这种也叫yml扁平化
app:
name: Lession06-package
owner: zhangsan
password: 040809

server:
port: 8023

注意,如果 yml 和 properties 文件同时存在的话,会优先加载 properties 文件的内容。

Environment

Environment 是外部化的抽象,是多种数据来源的集合。从中可以读取 application 配置文件、yml 文件、环境变量,系统属性。使用方式在 Bean 中注入 Environment,调用它的 getProperty(key) 方法。也就是说,如果我们要读取的配置文件散落在很多地方,那么我们只需要使用 Environment 来读取即可,相当于 Environment 帮助我们把这些散落的文件组织到一起了。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class ReadConfig {

// 注入环境对象
@Autowired
private Environment environment;

public void print() {
// 判断某个key是否存在
if(environment.containsProperty("app.owner")) {
System.out.println(environment.getProperty("app.owner"));
}
else {
System.out.println("key不存在");
}

// 读取key的值,转为期望类型,同时提供默认值
Integer password = environment.getProperty("app.password", Integer.class, 666);
System.out.println(password);
}
}

组织多文件

大型集成的第三方框架,中间件比较多。每个框架的配置细节相对复杂,如果都将配置到一个 application 文件中,那么会导致文件的内容非常庞大,不易于阅读。所以,我们要将每个框架都独立出来一个配置文件,最后将多个文件集中到一个 application 文件中。

1
2
3
4
# 导入其他的配置文件,多个文件利用逗号分割
spring:
config:
import: conf/db.yml, conf/redis.yml

多环境配置

软件开发中常提到环境这个概念,影响软件运行的因素就叫环境,例如 ip、用户名和密码、端口、配置文件的路径等。Spring Profiles 表示环境,Profiles 有助于隔离应用程序配置,并使得它们在某些环境中可用。SpringBoot 规定环境文件名称为application-{profile}.properties(yml),其中 profile 表示自定义的环境名称,通常我们用 dev 表示开发环境,test 表示测试环境,prod 表示生产环境,feature 表示特性。在加载的时候,是配置文件和环境文件一起加载的。

环境配置文件(application-dev.yml)示例:

1
2
3
4
5
6
7
8
myapp:
memo: 开发环境的配置文件

# 指定环境名称
spring:
config:
activate:
on-profile: dev

在 application 中使用对应的环境配置文件:

1
2
3
4
5
6
7
# 导入其他的配置文件,多个文件利用逗号分割
spring:
config:
import: conf/db.yml, conf/redis.yml
# 激活环境配置文件
profiles:
active: dev

绑定 Bean

当我们使用 @Value 绑定属性值的时候,一次性只能绑定单个值,比较不方便。SpringBoot 提供了另一种属性的方法,将多个配置项绑定到 Bean 的属性,提供强类型的 Bean。

基本原则:标准的 JavaBean 有无参数构造方法,包含属性的访问器,配合 @ConfigurationProperties 注解一起使用,Bean 的 static 属性不支持

SpringBoot 中大量使用绑定 Bean 与 @ConfigurationProperties,提供对框架的定制参数。项目中要使用的数据如果是可变的,推荐在 yml 或 properties 中提供,这样可以让代码具有更加大的灵活性。

@ConfigurationProperties 注解能够配置多个简单类型属性,同时支持 Map、List、数组类型,对属性还能验证基本格式。

绑定简单类型数据

假设 yml 文件中的内容是这样的:

1
2
3
4
app:
name: Lession06-package
owner: zhangsan
password: 040809

我们使用 @ConfigurationProperties 注解来绑定 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
// 创建普通的Bean,非 spring 代理
@Configuration(proxyBeanMethods = false) // 默认创建的是代理对象,但如果我们不需要,关掉反而可以提高性能
@ConfigurationProperties(prefix = "app") // 指定前缀,即相同的开始关键字
public class AppBean {

// key的名称与属性名相同,框架会调用set方法给其赋值,属性不可以用static修饰
private String name;
private String owner;
private String password;

// getter and setter
}

嵌套 Bean

有些时候我们需要在 Bean 中包含其他 Bean 作为属性,将配置文件中的配置项绑定到 Bean 以及引用类型的成员。

例如有配置文件如下:

1
2
3
4
5
6
7
8
9
app:
name: Lession06-package
owner: zhangsan
password: 040809
port: 9090
# 嵌套的Security类
security:
username: root
password: 123456

接下来我们使用嵌套 Bean 为属性赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration(proxyBeanMethods = false)
@ConfigurationProperties(prefix = "app")
public class NestAppBean {
// NestAppBean的自身原本属性
private String name;
private String owner;
private String password;
private Integer port;
// 内部嵌套了另一个bean
private Security security;

// getter and setter
}

扫描注解

要想让 @ConfigurationProperties 所绑定的 Bean 起作用,我们还需要是用 @EnableConfigurationProperties@ConfigurationPropertiesScan。这些注解是专门寻找 @ConfigurationProperties 注解的,将它的对象注入到 Spring 容器。在启动类上使用扫描注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 启用 ConfigurationProperties 属性是类的名字
//@EnableConfigurationProperties({NextAppBean.class})
//扫描注解的包名 其中绑定的 Bean 会被注入 Spring 容器
@ConfigurationPropertiesScan(basePackages = {"com.example.demo.config.pk5"})
// 以上两种方式都可以使得 @ConfigurationProperties 所绑定的 Bean 起作用
@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}

绑定第三方对象

有些时候,我们嵌套 Bean 中使用的 Bean 不是我们自己定义的,无源代码,但是我们需要在配置文件中提供属性。此时 @ConfigurationProperties 结合 @Bean 一起在方法上面使用可以解决这个问题。

比如现在有一个 Security 类是第三方库中的类,现在要提供它的 username 和 password 属性值。

在本项目的 application 配置文件中配置:

1
2
3
4
# 第三方库对象,没有源代码
security:
username: common
password: 1234567

创建配置类,注入属性值:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class ApplicationConfig {

// 创建Bean对象,属性来自配置文件
@ConfigurationProperties(prefix = "security")
// 标记为Bean的创建方法
@Bean
public Security createSecurity() {
return new Security();
}

}

接下来可以使用对象:

1
2
3
4
5
6
7
8
9
10
// 属性自动注入
@Autowired
Security security;

@Test
void testApplicationConfig() {
// Security security = applicationConfig.createSecurity();
// System.out.println(security);
System.out.println(security);
}

集合的绑定

Map、List、Array 都能提供配置数据。

保存数据的 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class User {
private String name;
private int age;
private String gender;

// getter and setter
}

public class MyServer {
private String title;
private String ip;

// getter and setter
}

// 使用ConfigurationProperties进行配置文件属性赋值,主要在启动类上要进行包扫描
@ConfigurationProperties
public class CollectionConfig {
private List<MyServer> servers;
private Map<String, User> users;
private String[] names;

// getter and setter
}

application 配置文件中编写属性配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 配置集合
# 数组和List集合的配置格式是一样的,使用 - 表示一个成员
names:
- lisi
- zhangsan

servers:
- title: 华北服务器
ip: 202.12.9.7
- title: 西南服务器
ip: 106.23.32.11

# Map集合的成员配置需要指定key和value
users:
user1: # 指定key为user1
name: zhangsan
age: 22
gender:
user2:
name: lisi
age: 21
gender:

指定数据源文件

一般我们使用 application 作为配置文件,但是,我们通常希望有一个特定的文件配置各个 Bean 的属性值来作为我们的数据来源。在类上使用 @PropertySource 可以指定 properties 文件(注意不是 yaml 文件,该注解并不支持这种类型的文件解析)作为数据来源。该注解与 @Configuration 一同使用:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@PropertySource("classpath:/groupInfo.properties") // 指定类路径下的资源
@ConfigurationProperties(prefix = "group")
public class Group {
private String name;
private String leader;
private Integer members;


// setter and getter
}

创建对象的三种方式

将对象注入到 Spring 容器,可以通过如下方式:

  • 传统的 XML 配置文件。
  • Java Config 技术,通过 @Configuration 和 @Bean。
  • 创建对象的注解:@Component、@Controller、@Service、@Repository。

SpringBoot 不推荐使用 XML 配置文件的方式,自动配置已经解决了大部分 xml 中的配置工作了。如果需要 xml 提供 bean 的声明,@ImportResource 加载 xml 注册 Bean。

1
2
3
4
5
6
7
8
9
10
// 指定xml配置文件
@ImportResource(locations = "classpath:/applicationContext.xml")
@SpringBootApplication
public class Lession06PackageApplication {
public static void main(String[] args) {
ConfigurableAppicationContext run = SpringApplication.run(Lession06PackageApplication.class, args);
// 获取bean对象
Person bean = run.getBean(Person.class);
}
}

AOP

面向切面编程,可以在保持原本代码不变的情况下,给原有的业务逻辑添加二维的功能,对于扩展功能十分有利,Spring 的事务功能就是在 AOP 的基础上去实现的。

  • Aspect:表示切面,开发自己编写功能增强代码的地方,这些代码会通过动态代理加入到原有的业务方法中。@Aspect 注解表示当前类是切面类。切面类是一个普通类。
  • Joinpoint:表示连接点,连接切面和目标对象。或是一个方法名称,一个包名,类名。在这个特定的位置执行切面中的功能代码。
  • 切入点(Pointcut):其实筛选出来的连接点,一个类中的所有方法都可能是 JoinPoint,具体的哪个方法要增加功能,这个方法就是 PointCut。
  • Advice:通知,也叫增强。表示增强的功能执行时间。主要包括5个注解:@Before @After @ AFterReturning @AfterThrowing @Around。注解来自 aspectj 框架。

SpringBoot 中使用 AOP 需要先引入对应依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

AOP 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 目标对象
@Service("someService")
public class SomeServiceImpl implements SomeService {
@Override
public void query(Integer id) {
System.out.println("query");
}

@Override
public void save(String name, Integer age) {
System.out.println("save " + name + " " + age);
}
}

// 切面类
@Component
@Aspect
public class LogAspect {
// 在类中定义功能增强的方法
@Around("execution(public void cn.hnu.springboot.lession08.aop.service.*.*(..))")
public void sysLog(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println(new Date());
joinPoint.proceed();
System.out.println("功能增强完毕");
}
}

自动配置

启用 autoconfigure(自动配置),框架尝试猜测要使用的 Bean,从类路径中查找 xxx.jar,创建这个 jar 中某些需要的 Bean。例如我们使用 MyBatis 访问数据,从我们项目的类路径中寻找 mybatis.jar,进一步创建 SqlSessionFactory,还需要 DataSource 数据源对象,尝试连接数据。这些工作交给XXXAutoConfiguration 类,这些就是自动配置类。在 spring-boot-autoconfigure-3.0.2.jar 定义了很多的XxXAutoConfiguration 类。第三方框架的 starter 里面包含了自己的 XXXAutoConfiguration类。

例如,在和 Mybatis 框架进行整合的时候,就提供了MybatisAutoConfiguration自动配置类,该类提供了SqlSessionFactory用于创建 SqlSession,还提供了SqlSessionTemplate用于执行 sql 语句,还有MapperFactoryBean用于创建 Dao 接口的代理对象。

MyBatis

MyBatis 需要的依赖项有:

  • Lombok
  • MyBatis Framework
  • MySQL Driver

DataSource

DataSource 在 application 配置文件中以 spring.datasource.* 作为配置项。DataSourceProperties 是数据源的配置类,更多配置参考这个类的属性。

1
2
3
4
5
# 配置数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
spring.datasource.username=root
spring.datasource.password=MySQL:040809
1
2
3
4
5
6
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/big-event?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
username: root
password: MySQL:040809

除此之外还需要添加 mapper 配置文件扫描、自动驼峰映射、起别名和日志信息:

1
2
3
4
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.type-aliases-package=cn.hnu.springboot.lession10.mybatis.pojo
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
1
2
3
4
5
6
mybatis:
mapper-locations: classpath:mappers/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
type-aliases-package: cn.hnu.springboot.bigevent.model

启动类配置:

1
2
3
4
5
6
7
8
@SpringBootApplication
// 启动类需要利用MapperScan扫描mapper接口所在的包
@MapperScan(basePackages = "cn.hnu.springboot.lession10.mybatis.mapper")
public class Lession10MybatisApplication {
public static void main(String[] args) {
SpringApplication.run(Lession10MybatisApplication.class, args);
}
}

在使用的时候直接利用 @Autowired 对 mapper 接口进行注入即可,无需手动调用 SqlSession 去创建 mapper 的动态代理类。

SqlProvider

MyBatis 提供了 SQL 提供者的功能,将 SQL 以方法的形式定义在单独的类中。Mapper 接口通过引用 SQL 提供者中的方法名称,表示要执行的 SQL。

SQL 提供者有四类 @SelectProvider,@InsertProvider,@UpdateProvider,@DeleteProvider。

编写 SQL 提供者类:

1
2
3
4
5
6
public class SqlProvider {
// 定义静态方法
public static String selectCar() {
return "select * from t_car where id = #{id}";
}
}

使用 SQL 提供者:

1
2
3
4
5
6
public interface CarMapper {
// Sql提供者
// type填入提供者类的字节码文件,method填入提供者类的方法
@SelectProvider(type = SqlProvider.class, method = "selectCar")
Car selectById2(Integer id);
}

一(多)对一

MyBatis 支持一对一、一对多、多对多的查询。XML 文件和注解都能实现关系的操作。我们使用注解表示上述的关系:**@One 表示一对一、@Many 表示一对多**。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 课程Mapper
public interface ClazzMapper {
// 查询
@Select("select * from t_clazz where cid = #{cid}")
Clazz getClazzById(Integer cid);
}

// 学生Mapper
public interface StudentMapper {
// 查询
@Select("select * from t_stu where sid = #{sid}")
@Results({
@Result(id = true, column = "sid", property = "sid"),
@Result(column = "sname", property = "sname"),
@Result(column = "cid", property = "clazz",
// 使用One进行分步查询,同时也支持懒加载
one=@One(select = "cn.hnu.springboot.lession10.mybatis.mapper.ClazzMapper.getClazzById", fetchType = FetchType.LAZY))
})
Student getStudentById(int sid);
}

一对多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 学生Mapper
public interface StudentMapper {
// 查询
@Select("select * from t_stu where cid = #{cid}")
List<Student> getStusById(int cid);
}

// 课程Mapper
public interface ClazzMapper {
// 查询
@Select("select * from t_clazz where cid = #{cid}")
@Results({
@Result(id = true, column = "cid", property = "cid"),
@Result(column = "cname", property = "cname"),
@Result(column = "cid", property = "stus",
// 使用many进行分步查询
many = @Many(select = "cn.hnu.springboot.lession10.mybatis.mapper.StudentMapper.getStusById", fetchType = FetchType.LAZY))
})
ClazzStus getClazzStusById(Integer cid);
}

常用设置和自动配置

MyBatis 框架在 SpringBoot 中的自动配置类为:MybatisAutoConfiguration。

除了之前我们说过的可以直接在 properties 文件中进行 MyBatis 的配置之外,也可以使用 MyBatis 的默认 xml 配置,然后再指定到 properties 文件中,如下指定:

1
mybatis.config-location=classpath:/mybatis-config.xml

连接池

HikariCP 连接池,MySQL 配置提示

1
2
3
4
5
6
7
8
9
10
11
12
13
jdbcUrl=jdbc:mysql://localhost:3306/simpsons
username=test
password=test
dataSource.cachePrepStmts=true
dataSource.prepStmtCacheSize=250
dataSource.prepStmtCacheSqlLimit=2048
dataSource.useServerPrepStmts=true
dataSource.useLocalSessionState=true
dataSource.rewriteBatchedStatements=true
dataSource.cacheResultSetMetadata=true
dataSource.cacheServerConfiguration=true
dataSource.elideSetAutoCommits=true
dataSource.maintainTimeStats=false

在 SpringBoot 的 application 配置文件中,使用下述代码配置连接池:

1
2
# 默认连接池
spring.datasource.type=com.zaxxer.hikari.HikariDataSource

完整实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql//localhost:3306/news_system
username: root
password: MySQL:040809
type: com.zaxxer.hikari.HikariDataSource
hikari:
auto-commit: true
maximum-pool-size: 10
# connections = ((cpu 核心数 * 2) + 磁盘数量) 近似值。 默认 10
minimum-idle: 10
#最小连接数,默认 10,不建议设置。默认与 maximum-pool-size 一样大小。推荐使用
#固定大小的连接池
# 获取连接时,检测语句
connection-test-query: select 1
connection-timeout: 20000
# 其他属性
data-source-properties:
cachePreStmts: true
dataSource.cachePreStmtst: true
dataSource.preStmtCacheSize: 250
dataSource.preStmtCacheSqlLimit: 2048
dataSource.useServerPrepStmts: true

事务

事务分为全局事务与本地事务,本地事务是特定于资源的,例如与 JDBC 连接关联的事务。本地事务可能更

容易使用,但有一个显著的缺点:它们不能跨多个事务资源工作。比如在方法中处理连接多个数据库的事务,本

地事务是无效的。

Spring 解决了全局和本地事务的缺点。它允许应用程序开发人员在任何环境中使用一致的编程模型。只需编

写一次代码,就可以从不同环境中的不同事务管理策略中获益。Spring 框架同时提供声明式和编程式事务管理。

推荐声明式事务管理。

Spring 事务抽象的关键是事务策略的概念,org.springframework.transaction.PlatformTransactionManager 接口

定义了事务的策略。

事务控制的属性:

  • Propagation : 传播行为。代码可以继续在现有事务中运行(常见情况),也可以暂停现有事务并创建新事务

  • Isolation: 隔离级别。此事务与其他事务的工作隔离的程度。例如,这个事务能看到其他事务未提交的写吗?

  • Timeout 超时时间:该事务在超时和被底层事务基础结构自动回滚之前运行的时间。

  • Read-only 只读状态:当代码读取但不修改数据时,可以使用只读事务。在某些情况下,例如使用 Hibernate

时,只读事务可能是一种有用的优化。

AOP:

Spring Framework 的声明式事务管理是通过 Spring 面向方面编程(AOP)实现的。事务方面的代码以样板的方

式使用,及时不了解AOP 概念,仍然可以有效地使用这些代码。事务使用AOP的环绕通知(TransactionInterceptor)。

声明式事务的方式:

  • XML 配置文件:全局配置

  • @Transactional 注解驱动 :和代码一起提供,比较直观。==和代码的耦合比较高==。【Spring 团队建议您只使用

@Transactional 注释具体类(以及具体类的方法),而不是注释接口。当然,可以将@Transactional 注解放在接

口(或接口方法)上,但这只有在使用基于接口的代理时才能正常工作】

方法的可见性:

公共(public)方法应用@Transactional 主机。如果使用@Transactional 注释了受保护的、私有的或包可见的方法,

则不会引发错误,但注释的方法不会显示配置的事务设置,事务不生效。如果需要受保护的、私有的方法具有事

务考虑使用 AspectJ。而不是基于代理的机制。

声明式事务

Spring 框架的事务管理是通过 Spring 面向切面编程实现的,事务使用的是环绕通知(TransactionInterceptor)。Spring 团队建议将 @Transactional 注解注释到具体类(以及具体类的方法),而不是注释接口,这样可以降低代码的耦合度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Service("accountService")
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountMapper accountMapper;

@Override
@Transactional(rollbackFor = Exception.class) // 设置事务注解,碰到Exception时回滚
public int transform(String fromAccount, String toAccount, int money) {
Account fromAct = accountMapper.getByName(fromAccount);
if (fromAct.getMoney() < money) {
throw new RuntimeException("账户余额不足");
}
Account toAct = accountMapper.getByName(toAccount);

// 转账
fromAct.setMoney(fromAct.getMoney() - money);
int count = accountMapper.modifyById(fromAct);

toAct.setMoney(toAct.getMoney() + money);
count += accountMapper.modifyById(toAct);

return count;
}
}
1
2
3
4
5
6
7
8
9
10
@EnableTransactionManagement    // 可选,不加也可以开启事务管理器
@SpringBootApplication
@MapperScan(basePackages = "cn.hnu.springboot.lession11transaction.mapper")
public class Lession11TransactionApplication {

public static void main(String[] args) {
SpringApplication.run(Lession11TransactionApplication.class, args);
}

}

无效事务

跨方法调用事务

Spring 事务处理是 AOP 的环绕通知,只有通过代理对象调用具有事务的方法才能生效。类中有 A方法,调用带有事务的 B 方法。调用 A方法事务无效。当然 protected, private 方法默认是没有事务功能的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service("accountService")
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountMapper accountMapper;

@Override
// @Transactional(propagation = Propagation.REQUIRED)
// 如果不加入这个事务传播行为,那么这个myTransForm方法没办法开启事务
public int myTransForm(String fromAccount, String toAccount, int money) {
return transform(fromAccount, toAccount, money);
}

@Override
@Transactional(rollbackFor = Exception.class) // 设置事务注解,碰到Exception时回滚
public int transform(String fromAccount, String toAccount, int money) {
//...
}

新线程调用事务

方法在线程中运行,在同一线程中方法具有事务功能,新的线程中的代码事务无效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Service("accountService")
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountMapper accountMapper;

@Override
public AtomicInteger theadTransForm(String fromAccount, String toAccount, int money) {
System.out.println("父线程:" + Thread.currentThread().getName());
AtomicInteger count = new AtomicInteger();

// 开启了一个新的线程,该线程是新线程,调用的方法不会开启事务
Thread thread = new Thread(()->{
count.addAndGet(transform(fromAccount, toAccount, money));
});

thread.start();

return count;
}

@Override
@Transactional(rollbackFor = Exception.class) // 设置事务注解,碰到Exception时回滚
public int transform(String fromAccount, String toAccount, int money) {
//...
}
}

回滚规则

  • RuntimeException 的实例或子类时回滚事务

  • Error 会导致回滚

  • 已检查异常不会回滚。默认提交事务

  • @Transactional 注解的属性控制回滚

    ​ rollbackFor

    ​ noRollbackFor

    ​ rollbackForClassName

    ​ noRollbackForClassName

Web

SpringBoot 可以创建两种类型的 Web 应用:

  1. 基于 Servlet 体系的 Spring Web MVC 应用。
  2. 使用 spring-boot-starter-webflux 模块来构建响应式,非阻塞的 Web 应用程序。

Web 应用需要的依赖项有:

  • Lombok
  • Spring Web
  • Thymeleaf

基础使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller	// 声明为Controller
public class QuickController {

// 指定访问url
@RequestMapping("/exam/quick")
// 导入的是springframework.ui.Model,用于存储数据,把数据放在request作用域
public String quick(Model model) {
// 处理参数数据
model.addAttribute("title", "Web开发");
model.addAttribute("time", new Date());

// 指定视图,显示数据
return "quick";
}

}

thymeleaf 拿取 request 作用域的参数语法:

1
<div th:text="${attribute_name}"></div>

视图

上面的例子以 Html 文件作为视图,可以编写复杂的交互的页面,CSS 美化数据。除了带有页面的数据,还有一种只需要数据的视图。比如手机应用 app,app 的数据来自服务器应用处理结果。app 内的数据显示和服务器无关,只需要数据就可以了。主流方式是服务器返回 json 格式数据给手机 app 应用。我们可以通过原始的HttpServletResponse 应该数据给请求方。借助 Spring MVC 能够无感知的处理 json。这种视图我们称为 json 视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Controller
public class JsonViewController {

// 响应json串
@RequestMapping("/exam/json")
public void responseJson(HttpServletResponse response) throws IOException {
String json = "{\"name\":\"lisi\",\"age\":18}";
// 应答,通过HttpServletResponse输出
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(json);
}

// SpringMVC支持控制器返回对象,由框架将要使用的对象转为json后输出
@RequestMapping("/exam/userJson")
@ResponseBody // 使用@ResponseBody将数据以json格式写出
public User getUserJson() {
User user = new User();
user.setUsername("zhangsan");
user.setPassword("123");
return user;
}
}

favicon

favicon.ico 是网站的缩略标志,可以显示在浏览器标签、地址栏左边和收藏夹,是展示网站个性的 logo 标志。可以利用这个网站快速生成。

  1. 将生成的 favicon.ico 拷贝到项目的 resources/static/ 目录。
  2. 在视图的 Header 部分加入

路径

SpringMVC 支持多种策略,匹配请求路径到控制器方法。分别为:AntPathMatcher、PathPatternParser。从 SpringBoot3 开始,推荐使用 PathPatternParser,比之前 AntPathMathcer 提升 6-8 倍的吞吐量。

配置如下:

1
spring.mvc.pathmatch.matching-strategy=path_pattern_parser

让我们看一下 PathPatternParser 中有关 uri 的定义:

  • ?:一个字符。
  • *:0 或多个字符。在一个路径段中匹配字符。
  • **:匹配 0 个或多个路径段,相当于是所有。
  • 正则表达式:支持正则表达式。

RESTFul 的支持路径变量:

  • {变量名}:路径占位符。
  • {myname:[a-z]+}:正则匹配 a-z 的多个字面,路径变量名称为 myname。(@PathVariable("myname")
  • {*myname}:匹配多个路径一直到 uri 的结尾。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@GetMapping("/file/t?st.html")
// http://localhost:8080/file/test.html
// http://localhost:8080/file/teest.html 该地址匹配不成功,因为?只能匹配单个字符

@GetMapping("/images/*.gifs")
/*
以下几个url都满足要求
http://localhost:8080/images/user.gifs
http://localhost:8080/images/cat.gifs
http://localhost:8080/images/.gifs

http://localhost:8080/images/gif/header.gif 该地址匹配不成功,因为*不能包括段落
*/

@GetMapping("/pic/**")
/*
以下几个url都满足要求,**适合多段落匹配
http://localhost:8080/pic/p1.gif
http://localhost:8080/pic/2024/p1.gif
http://localhost:8080/pic/user
http://localhost:8080/pic/

*/

@GetMapping("/order/{*id}")
// 匹配/order开始的所有请求,id表示order后面直到路径结束的所有内容
// 可以结合@PathVariable将id的内容拿出来

// http://localhost:8080/order/1001 id=/1001
// http://localhost:8080/order/1001/2024-05-01 id=/1001/2024-05-01
// 注意"/order/{*id}/{*date}"是无效的,{*..}后面不能再有匹配规则了

@GetMapping("/pages/{fname:\\w+}.log")
// :\\w+正则匹配,xxx.log
// http://localhost:8080/pages/req.log

接收请求参数

接收参数方式:

  • 请求参数与形参一一对应,适用于简单类型。
  • 对象类型,控制器方法形参是对象,请求的多个参数名与属性名相对应。
  • @RequestParam 注解,进行请求参数和方法参数的映射。
  • @RequestBody,接受前端传递的 json 格式参数。
  • HttpServletRequest 使用 request 对象接受参数。
  • @RequestHeader,从请求 header 中获取某项值。

解析参数需要的值,SpringMVC 中专门有个接口来干这个事情,这个接口就是:HandlerMethodArgumentResolver,中文称呼:处理器方法参数解析器,说白了就是解析请求得到 Controller 方法的参数的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@RestController
public class ParameterController {

// 简单参数直接接收
@RequestMapping("/exam/param/p1")
// http://localhost:8023/exam/param/p1?name=zhangsan&age=18&gender=男
public String parameterTest1(String name, Integer age, String gender) {
return "接受参数: name = " + name + ", age = " + age + ", gender = " + gender;
}

// 利用pojo类接收参数
@RequestMapping("/exam/param/p2")
// http://localhost:8023/exam/param/p2?username=zhangsan&password=123
public String parameterTest2(User user) {
return user.toString();
}

// 利用原生servlet接收参数
@RequestMapping("/exam/param/p3")
// http://localhost:8023/exam/param/p3?name=zhangsan&password=123
public String parameterTest3(HttpServletRequest request) {
String name = request.getParameter("name");
String password = request.getParameter("password");
return name + " " + password;
}

// 进行方法参数和请求参数的映射
@RequestMapping("/exam/param/p4")
// http://localhost:8023/exam/param/p4?user_password=123456
// required = false意味着这个参数可选,如果没有,默认值是defaultValue的值
public String parameterTest4(@RequestParam(value = "user_name", required = false, defaultValue = "zhangsan") String name,
@RequestParam("user_password") String password) {
return name + " " + password;
}

// 接收请求头中的参数值
@RequestMapping("/exam/param/p5")
// http://localhost:8023/exam/param/p5
public String parameterTest5(@RequestHeader("Accept") String accept) {
return accept;
}

// 接收前端传递过来的json串(请求体数据)
@RequestMapping("/exam/param/p6")
// http://localhost:8023/exam/param/p6
// Content-Type: application/json
//
// {"username":"张三", "password":123}
public String parameterTest6(@RequestBody User user) {
return user.toString();
}

// 使用Reader也可以读取请求体中的数据
@RequestMapping("/exam/param/p7")
public String parameterTest7(Reader reader) {
StringBuffer buffer = new StringBuffer();
try (BufferedReader br = new BufferedReader(reader)) {
String line;
while ((line = br.readLine()) != null) {
buffer.append(line);
}
} catch (IOException e) {
e.printStackTrace();
}
return buffer.toString();
}

// 数组作为形参,接收多个参数值
@RequestMapping("/exam/param/p8")
// http://localhost:8023/exam/param/p8?ids=1&ids=2&ids=3
public String parameterTest8(Integer[] ids) {
return Arrays.toString(ids);
}
}

BeanValidation

服务器端程序,Controller 在方法接受了参数,这些参数是由用户提供的,使用之前必须校验参数是我们需要的吗,值是否在允许的范围内,是否符合业务的要求。

BeanValidation 是提供数据验证 JSR-303 的一个子规范,为 JavaBean 验证定义了相应的元数据模型和 API,其中,hibernate-validator 是一个比较有名的实现。

BeanValidation 内置的 Constraint:

Constraint 详细信息
@Null 必须为null
@NotNull 必须非null
@NotBlank 字符串必须非null和非空字符串
@AssertTrue 必须为true
@AssertFalse 必须为false
@Min(value) 必须是一个数字,值必须大于等于指定最小值
@Max(value) 必须是一个数字,值必须小于等于指定最大值
@DecimalMin(value) 必须是一个数字,值必须大于等于指定最小值
@DecimalMax(value) 必须是一个数字,值必须小于等于指定最大值
@Size(min, max) 值必须在指定范围内,一般用于注释集合等
@Digits(integer, fraction) 值必须在指定范围内
@Past 必须是一个过去的日期
@Futuret 必须是一个将来的日期
@Pattern(value) 必须符合指定的正则表达式

hibernate-validator 附加的 constraint:

Constraint 详细信息
@Email 必须是电子邮箱地址
@Length 字符串的大小必须在指定范围内
@NotEmpty 被注释的字符串必须非空
@Range(min, max) 被注释的元素必须在合适的范围内
@URL 被注释的字符串为URL

普通验证

需要先加依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

在类的变量名上直接使用注解进行规则校验:

1
2
3
4
5
6
7
8
9
public class User {
@NotNull(message = "用户名不能为空")
@Pattern(regexp = "[a-zA-Z0-9_-]{4,16}")
String username;

@NotNull(message = "密码不能为空")
@Pattern(regexp = "\\S*(?=\\S{6,})(?=\\S*\\d)(?=\\S*[A-Z])(?=\\S*[a-z])(?=\\S*[!@#$%^&*? ])\\S*")
String password;
}

接着在 Controller 上使用 @Validated 注解进行规则验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
public class UserController {
// 加上@Validated验证Bean,利用BindingResult获取Bean的验证结果
@RequestMapping("/exam/user/add")
public Map<String, Object> addUser(@Validated @RequestBody User user,
BindingResult bindingResult) {
Map<String, Object> map = new HashMap<>();
// 获取没有通过验证的结果
if (bindingResult.hasErrors()) {
// 进行对应处理
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (int i = 0; i < fieldErrors.size(); i++) {
FieldError field = fieldErrors.get(i);
// 将出错的属性和出错原因放入map集合中
map.put(field.getField() + "-" + i, field.getDefaultMessage());
}
}
return map;
}
}

或者,直接在方法的参数上使用校验用的注解,然后把 @Validated 注解标注在控制器上也是可以的。

分组验证

现在碰到一个问题:假设 User 当中有一个 id 属性,这个属性在进行用户插入的时候应该为空,而进行用户修改的时候应该非空,如果直接注解作用在 id 上就会产生矛盾了,这个时候就需要进行分组验证(所谓的组实际上就是一个空接口)。

如果某个校验项没有指定分组,默认属于 Default 分组。且分组之间可以继承,如果A extends B,那么 A 中就拥有 B 的校验项。

使用的时候在 @Validated 注解后面标注是哪个组就好了,示例:@Validated({User.addGroup.class})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import jakarta.validation.groups.Default;

public class User {

// 新增组
public static interface addGroup extends Default {};

// 修改组
public static interface updateGroup extends Default {};

@NotNull(message = "主键在编辑时必须有值", groups = {updateGroup.class})
@Min(value = 1, message = "id要大于0", groups = {updateGroup.class})
@Null(message = "主键在插入时需为空", groups = {addGroup.class})
Integer id;

@NotBlank // 默认分组
String name;

}

自定义验证

已有的注解不能满足所有的校验需求的时候,特殊的情况需要自定义校验(自定义校验注解)。步骤如下:

  1. 自定义注解 State。
  2. 自定义校验数据的类 StateValidation 实现 ConstrainValidator 接口。
  3. 在需要校验的地方使用自定义注解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 元注解,用来标识本注解可以抽取到文档中
@Documented
// 元注解,用来标识本注解可以作用在哪些地方
@Target({ElementType.FIELD})
// 元注解,用来标识本注解在哪个阶段会被保留,我们这里保留到运行阶段
@Retention(RetentionPolicy.RUNTIME)
// 用来指定谁给注解提供校验规则
@Constraint(validatedBy = {StateValidation.class})
public @interface State {
// 用来提供校验失败后的信息
String message() default "state参数的值只能是 已发布 或者 草稿";

// 指定分组
Class<?>[] groups() default {};

// 负载,用于获取到state注解的附加信息
Class<? extends Payload>[] payload() default {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 泛型的第一个参数指将来给哪个注解提供校验规则,第二个参数指校验的数据类型
public class StateValidation implements ConstraintValidator<State, String> {

// 提供校验规则,value就是将来要校验的数据,如果返回false则校验不通过
@Override
public boolean isValid(String value,
ConstraintValidatorContext context) {

if (value == null) return false;

return value.equals("已发布") || value.equals("草稿");
}
}

ValidationAutoConfiguration

ValidationAutoConfiguration 自动配置类,创建了 LocalValidatorFactoryBean 对象,当有 class 路径中有hibernate.validator。能够创建 hiberate 的约束验证实现对象。@ConditionalOnResource(resources = "classpath:META-INF/services/jakarta.validation.spi.ValidationProvider")

自定义状态码

ResponseEntity 包含 HttpStatus Code 和 应答数据的结合体,因为有 Http Code 能表达标准的语义,200成功,404没有发现等。使用 ResponseEntity 自定义状态码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class UserController {

@RequestMapping("/exam/user/json")
public ResponseEntity<User> returnEntity() {

User user = new User();
user.setId(1);
user.setUsername("admin");
user.setPassword("123456");

// 可以自定义状态码
ResponseEntity<User> response = new ResponseEntity<>(user, HttpStatus.OK);
return response;
}

}

SpringMVC 请求流程

Spring MVC 框架是基于 Servlet 技术的。以请求为驱动,围绕 Servlet 设计的。Spring MVC 处理用户请求与访问一个 Servlet 是类似的,请求发送给 Servlet,执行 doService 方法,最后响应结果给浏览器完成一次请求处理。

DispatcherServlet 是核心对象,称为中央调度器(前端控制器 Front Controller)。负责接收所有对 Controller 的请求,调用开发者的 Controller 处理业务逻辑,将 Controller 方法的返回值经过视图处理响应给浏览器。

DispatcherServlet 作为 SpringMVC 中的 C,职责:

  1. 是一个门面,接收请求,控制请求的处理过程。所有请求都必须有 DispatcherServlet 控制。SpringMVC 对外的入口。可以看做门面设计模式。
  2. 访问其他的控制器,这些控制器处理业务逻辑。
  3. 创建合适的视图,将 2 中得到业务结果放到视图,响应给用户。
  4. 解耦了其他组件,所有组件只与 DispatcherServlet 交互,彼此之间没有关联。
  5. 实现 ApplictionContextAware,每个 DispatcherServlet 都拥自己的 WebApplicationContext,它继承了ApplicationContext(意味着 DispatcherServlet 也可以看作一个容器,可以访问到各种 Bean)。WebApplicationContext 包含了Web 相关的 Bean 对象,比如开发人员注释 @Controller 的类,视图解析器,视图对象等等。DispatcherServlet 访问容器中 Bean 对象。
  6. Servlet + Spring IoC 组合。

Web 配置

服务器配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 服务器端口号
server.port=8080
# 上下文访问路径
server.servlet.context-path=/
# request,response字符编码
server.servlet.encoding.charset=utf-8
# 强制request,response设置charset字符编码
server.servlet.encoding.force=true

# 日志路径
server.tomcat.accesslog.directory=D:/logs
# 启用访问日志
server.tomcat.accesslog.enabled=true
# 日志文件名前缀
server.tomcat.accesslog.prefix=access_log
# 日志文件日期时间
server.tomcat.accesslog.file-date-format=.yyyy-MM-dd
# 日志文件名称后缀
server.tomcat.accesslog.suffix=.log
# post请求内容最大值,默认2M
server.tomcat.max-http-form-post-size=2000000
# 服务器最大连接数
server.tomcat.max-connections=8192

DispatcherServlet 配置

1
2
3
4
5
6
# 配置访问路径
spring.mvc.servlet.path=/course
# servlet加载顺序,越小创建时间越早
spring.mvc.servlet.load-on-startup=0
# 时间格式,可以在接收请求参数时使用
spring.mvc.format.date-time=yyyy-MM-dd HH:mm:ss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 设置时间格式后接收时间
@RequestMapping("/exam/date")
@ResponseBody
// http://localhost:8023/exam/date?date=2024-05-02 19:18:22
public String date(LocalDateTime date) {
return "时间: " + date;
}

// 如果不去使用spring.mvc.format.date-time,也可以使用@DateTimeFormat来指定日期格式
@RequestMapping("exam/date")
@ResponseBody
public String date(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime date) {
return "时间: " + date;
}

HttpServlet 的创建

注解方式创建

和 JavaWeb 中的操作方式一样,使用注解 @WebServlet 指定映射路径。

1
2
3
4
5
6
7
8
9
10
@WebServlet(urlPatterns = "/helloServlet", name = "HelloServlet")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().println("<h1>Hello World</h1>");
}
}

不过还需要在启动类上增加 @ServletComponentScan 注解进行包扫描,注册 Servlet。

1
2
3
4
5
6
7
8
9
10
// servlet扫描器,可以扫描servlet,filter,listener
@ServletComponentScan("cn.hnu.springboot.lession13.web")
@SpringBootApplication
public class Lession13ServletFilterApplication {

public static void main(String[] args) {
SpringApplication.run(Lession13ServletFilterApplication.class, args);
}

}

编码方式创建

首先,Serlvet 的创建和 JavaWeb 中的操作一致,不过不再需要注解了:

1
2
3
4
5
6
7
8
9
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().println("<h1>Login Servlet</h1>");
}
}

其次,在编码方式创建中,我们需要创建一个配置类来注册添加 Serlvet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class WebAppConfig {
@Bean
public ServletRegistrationBean addSerlvet() {
// 创建ServletRegistrationBean,登录一个或多个Servlet
ServletRegistrationBean registrationBean = new ServletRegistrationBean();

// 添加Serlvet
registrationBean.setServlet(new LoginServlet());
// 指定映射路径
registrationBean.addUrlMappings("/login");
// 指定创建时间
registrationBean.setLoadOnStartup(1);

// 返回ServletRegistrationBean
return registrationBean;
}
}

Filter 的创建

Fiter 对象使用频率比较高,比如记录日志,权限验证,敏感字符过滤等等。Web 框架中包含内置的 Filter,SpringMVC 中也包含较多的内置 Filter,比如 CommonsRequestLoggingFilter,CorsFilter,FormContentFilter…

注解方式创建

注解方式创建过滤器,和 JavaWeb 中的操作一致,注意还需要在启动类上加上 @ServletComponentScan 注解进行扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 利用注解指定要过滤的路径
@WebFilter(urlPatterns = "/*")
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain)
throws IOException, ServletException {

// 转换一下servlet的类型
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

// 执行操作
String uri = request.getRequestURI();
System.out.println("过滤器执行了,uri: " + uri);

// 放行
filterChain.doFilter(request, response);

}
}

编码方式创建

原来的 @WebFilter 注解可以去掉,然后在 WebAppConfig 类中添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class WebAppConfig {
@Bean
public ServletRegistrationBean addSerlvet() {
//...
}

@Bean
public FilterRegistrationBean addFilter() {
// 登录Filter对象
FilterRegistrationBean registrationBean = new FilterRegistrationBean();

// 添加Filter
registrationBean.setFilter(new LogFilter());
// 指定映射路径
registrationBean.addUrlPatterns("/*");

// 返回FilterRegistrationBean
return registrationBean;
}
}

Filter 的排序

多个 Filter 对象如果要排序,有两种途径:

  1. 滤器类名称,按字典顺序排列,AuthFilter -> LogFilter。
  2. FilterRegistrationBean 登记 Filter,设置 order 顺序,数值越小,先执行。

利用第二种方法进行排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Configuration
public class WebAppConfig {
@Bean
public ServletRegistrationBean addSerlvet() {
//...
}

@Bean
public FilterRegistrationBean addLogFilter() {
// 登录Filter对象
FilterRegistrationBean registrationBean = new FilterRegistrationBean();

// 添加Filter
registrationBean.setFilter(new LogFilter());
// 指定映射路径
registrationBean.addUrlPatterns("/*");
// 设置顺序
registrationBean.setOrder(1);

// 返回FilterRegistrationBean
return registrationBean;
}

@Bean
public FilterRegistrationBean addAuthFilter() {
// 登录Filter对象
FilterRegistrationBean registrationBean = new FilterRegistrationBean();

// 添加Filter
registrationBean.setFilter(new AuthFilter());
// 指定映射路径
registrationBean.addUrlPatterns("/*");
// 设置顺序
registrationBean.setOrder(2);

// 返回FilterRegistrationBean
return registrationBean;
}
}

使用框架内置的 Filter

SpringBoot 中有许多已经定义好的 Filter,这些 Filter 实现了一些功能,如果我们需要使用他们。可以像自己的Filter一样,通过 FilterRegistrationBean 注册Filter对象。

假设现在我们想记录每个请求的日志,那么 CommonsRequestLoggingFilter 就能完成简单的请求记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Configuration
public class WebAppConfig {
@Bean
public ServletRegistrationBean addSerlvet() {
//...
}

@Bean
public FilterRegistrationBean addFilter() {
//...
}

// 登记框架内置的Filter
@Bean
public FilterRegistrationBean addCommonLogFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();

// 创建框架内置的Filter对象
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
// 记录请求的url地址
filter.setIncludeQueryString(true);

// 登记Filter
registrationBean.setFilter(filter);
// 添加映射路径
registrationBean.addUrlPatterns("/*");
// 排序
registrationBean.setOrder(1);

// 返回registrationBean
return registrationBean;
}
}

使用这个过滤器时还需要把日志的级别设置成 debug 级别:

1
logging.level.web=debug

Listener 的创建

Listener 平时用的比较少,这里不作详细介绍。如果想要创建 Listener,可以继承 HttpSessionListener,并使用 @WebListener 注解进行标记。另一种方式是使用 ServletListenerRegistrationBean 登记 Listener 对象。

WebMvcConfigurer 配置

WebMvcConfigurer 作为配置类是,采用 JavaBean 的形式来代替传统的 xml 配置文件形式进行针对框架个性化定制,就是 SpringMVC XML 配置文件的 JavaConfig(编码)实现方式。自定义 Interceptor,ViewResolver,MessageConverter。WebMvcConfigurer 就是JavaConfig 形式的 Spring MVC 的配置文件。

WebMvcConfigurer 是一个接口,需要自定义某个对象,实现接口并覆盖某个方法。SpringBoot 的自动配置已经设置了很多默认行为,而在一些情况下,我们可能想要对默认配置进行扩展或修改,这个时候,就可以用上 WebMvcConfigurer。

页面跳转控制器

SpringBoot 中使用页面视图,比如 Thymeleaf。要跳转显示某个页面,必须通过 Controller 对象。也就是我们需要创建一个 Controller,转发到一个视图才行。如果我们现在仅仅只需要显示页面,可以无需这个 Controller。addViewControllers()完成从请求到视图跳转。

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class MvcSettings implements WebMvcConfigurer {

// 页面跳转控制器,从请求直达视图页面
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// addViewController指定请求路径,setViewName指定视图名称
registry.addViewController("/").setViewName("index");
}

}

数据格式化

Formatter是数据转换接口,一种数据类型转换为另一种数据类型。与Formatter功能类型的还有Converter。本节我们研究应用更加广泛的Formatter

Formatter只能将 String 类型转换为其他数据类型,但是在 Web 应用上更广,因为 Web 请求的所有参数都是字符串类型。我们需要把参数转换为其他数据类型来进行处理。

Spring 中内置了Formatter

  • DateFormatter:String 和 Date 之间的解析和格式化。
  • InetAddressFormatter:String 和 InetAddress 之间的解析和格式化。
  • PercentStyleFormatter:String 和 Number 之间的解析和格式化,带货币符合。
  • NumberFormat:String 和 Number 之间的解析与格式化。

当上述内置的功能无法实现我们的要求时,我们可以通过Formatter这个扩展点来帮助我们实现我们自己想要的格式转换:

1
2
3
4
public interface Formatter<T> extends Printer<T>, Parser<T> {}
// Formatter<T>是一个组合接口,没有自己的方法,需要继承Printer<T>和Parser<T>
// Printer<T>用于将T类型转为String,格式化输出
// Parser<T>用于将String类型转换为T对象

一些和硬件打交道的项目,数据格式往往不是我们平常见到的那样,可能是一串1111;2222;333,NF;4;561,接下来我们模拟一下如何接收这种数据格式:

首先,我们需要创建对应的 pojo 类:

1
2
3
4
5
6
7
public class DeviceInfo {
private String item1;
private String item2;
private String item3;
private String item4;
private String item5;
}

其次,实现Formatter接口,进行方法的重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class DeviceFormatter implements Formatter<DeviceInfo> {

// parse是将String转为T对象
@Override
public DeviceInfo parse(String text, Locale locale) throws ParseException {
DeviceInfo deviceInfo = null;
// 利用spring框架提供的工具类判断是否有值
if (StringUtils.hasLength(text)) {
String[] split = text.split(";");
deviceInfo = new DeviceInfo();
deviceInfo.setItem1(split[0]);
deviceInfo.setItem2(split[1]);
deviceInfo.setItem3(split[2]);
deviceInfo.setItem4(split[3]);
deviceInfo.setItem5(split[4]);
}
return deviceInfo;
}

// print是将T对象转为String
@Override
public String print(DeviceInfo object, Locale locale) {
StringJoiner joiner = new StringJoiner("#");
joiner.add(object.getItem1());
joiner.add(object.getItem2());
joiner.add(object.getItem3());
joiner.add(object.getItem4());
joiner.add(object.getItem5());
return joiner.toString();
}
}

然后,告诉 Spring 框架有这么一个转换器,也就是进行转换器的注册:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class MvcSettings implements WebMvcConfigurer {

// 数据格式转换器
@Override
public void addFormatters(FormatterRegistry registry) {
// 添加转换器
registry.addFormatter(new DeviceFormatter());
}

}

然后直接接收参数就好:

1
2
3
4
5
6
7
8
9
10
@Controller
public class DeviceController {
@RequestMapping("/exam/formatter")
@ResponseBody
// 直接利用pojo类接收即可
// http://localhost:8023/exam/formatter?deviceInfo=1111;2222;333,NF;4;561
public String addDeviceInfo(DeviceInfo deviceInfo) {
return deviceInfo.toString();
}
}

拦截器

Handlerlnterceptor 接口和它的实现类称为拦截器,是 SpringMVC 的一种对象。拦截器是 SpringMVC 框架的对象,与Servlet无关。拦截器能够预先处理发给 Controller 的请求。可以决定请求是否被 Controller 处理。用户请求是先由 DispatcherServlet 接收后,在 Controller 之前执行的拦截器对象。根据拦截器的特点,类似权限验证,记录日志,过滤字符,登录 token 处理都可以使用拦截器。

拦截器可以深入到方法级别的控制,提供对 Spring 上下文中 bean 的访问能力,允许更精细的控制请求处理流程。

现在我们使用拦截器对某个用户进行行为限制:只能查看,不能修改和删除。

准备 Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class ArticleController {

@RequestMapping("/article/add")
public String addArticle() {
return "发布新文章";
}

@RequestMapping("/article/edit")
public String editArticle() {
return "修改文章";
}

@RequestMapping("/article/query")
public String queryArticle() {
return "查询文章";
}
}

设计拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class AuthInterceptor implements HandlerInterceptor {
// preHandle,在控制器方法执行前执行
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
System.out.println("AuthInterceptor拦截器执行了");
// 获取登录用户
String user = request.getParameter("user");
// 获取请求uri地址
String uri = request.getRequestURI();
// 判断用户和操作
if ("zhangsan".equals(user) && (
uri.startsWith("/article/add") ||
uri.startsWith("/article/edit"))) {
return false;
}
return true;
}
}

注册拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class MvcSettings implements WebMvcConfigurer {

@Autowired
private AuthInterceptor interceptor;

// 拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor)
.addPathPatterns("/article/**") // 拦截哪些地址
.excludePathPatterns("/article/query"); // 不拦截哪些地址
}
}

上述是单个拦截器的情况,接下来假设我们还需要一个拦截器来进行身份拦截,这种情况下就是多个拦截器,就涉及到拦截器的排序问题。

再来一个拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LoginInterceptor implements HandlerInterceptor {

private List<String> permitUser = new ArrayList<String>();

public LoginInterceptor() {
Collections.addAll(permitUser, "zhangsan", "lisi", "admin");
}

@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
System.out.println("LoginInterceptor执行了");
// 获取登录用户并进行判断
String user = request.getParameter("user");
return StringUtils.hasLength(user) && permitUser.contains(user);
}
}

拦截器排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class MvcSettings implements WebMvcConfigurer {

// 拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //拦截所有请求
.excludePathPatterns("/article/query")
.order(1); // 排序,登录拦截器在第一位

// 权限拦截器
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/article/**") // 拦截哪些地址
.excludePathPatterns("/article/query") // 不拦截哪些地址
.order(2); // 拦截器顺序
}
}

文件上传解析器

上传文件首先想到的就是 Apache Commons FileUpload,这个库使用非常广泛。但是在 SpringBoot3 版本中已经不能使用了。代替他的是 SpringBoot 中自己的文件上传实现。

SpringBoot上传文件现在变得非常简单。提供了封装好的处理上传文件的接口 MultipartResolver,用于解析上传文件的请求,他的内部实现类 StandardServletMultipartResolver。(底层使用的是 Servlet 的 Part 接口实现)之前常用的 CommonsMultipartResolver 不可用了。CommonsMultipartResolver 是使用Apache Commons FileUpload 库时的处理类。

StandardServletMultipartResolver 内部封装了读取 POST 请求体的请求数据,也就是文件内容。我们现在只需要在 Controller 的方法加入形参 @RequestParam MultipartFile。MultipartFile 表示上传的文件,提供了方便的方法保存文件到磁盘。

MultipartFile API 如下:

方法 作用
getName() 参数名称(updfile)
getOriginalFilename() 上传文件原始名称
isEmpty() 上传文件是否为空
getSize() 上传文件的字节大小
getInputStream() 文件的 InputStream,可用于读取部件内容
transferTo(File dest) 保存上传文件到目标 dest

前端页面:

1
2
3
4
5
6
7
<div style="margin-left: 200px">
<h3>上传文件</h3>
<form method="post" action="/upload" enctype="multipart/form-data">
选择文件:<input type="file" name="upfile" value="上传"/><br>
<input type="submit" name="submit" value="确定"/>
</form>
</div>

后端控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Controller
public class UploadFileController {

// 上传文件
@PostMapping("/upload")
public String upload(@RequestParam("upfile") MultipartFile multipartFile,
HttpSession session) throws Exception {
System.out.println("开始处理上传文件");

String finalFileName = null;

if (!multipartFile.isEmpty()) {
// 获取文件名
String fileName = multipartFile.getOriginalFilename();
// 截取后缀名,注意从后方开始截取,防止文件名本身出现.
String suffix = fileName.substring(fileName.lastIndexOf("."));
// 获取随机uuid
String uuid = UUID.randomUUID().toString();
// 拼凑文件名
finalFileName = uuid + suffix;
}

if (finalFileName == null) {
throw new RuntimeException("文件不存在");
}

// 获取路径
ServletContext servletContext = session.getServletContext();
String path = servletContext.getRealPath("multipartFile");

// 创建上传路径文件
File file = new File(path);
if (!file.exists()) {
file.mkdir();
}

finalFileName = path + File.separator + finalFileName;

// 上传
multipartFile.transferTo(new File(finalFileName));
// 重定向视图,防止重复上传
return "redirect:/success";
}
}

SpringBoot 中默认单个文件最大支持 1M,一次请求最大 10M。如果要改变默认值,需要修改配置项:

1
2
3
4
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB
# 文件超过file-size-threshold时,直接写入磁盘,不在内存处理
spring.servlet.multipart.file-size-threshold=0KB

如果要实现多个文件上传,只需要在前端多做几个 input 按钮(name 属性要保持相同),后端把 MultipartFile 改成一个数组即可。

异常处理

在 Controller 处理请求过程中发生了异常,DispatcherServlet 将异常处理委托给异常处理器(处理异常的类)。实现 HandlerExceptionResolver 接口的都是异常处理类。

异常处理器

项目的异常一般集中处理,定义全局异常处理器。在结合框架提供的注解,诸如:@ExceptionHandler,@ControllerAdvice(控制器增强,给 Controller 增加异常处理功能),@RestControllerAdvice 一起完成异常的处理。@ControllerAdvice 与 @RestControllerAdvice 区别在于:@RestControllerAdvice 加了 @RepsonseBody。

前端页面和控制器如下:

1
2
3
4
5
6
7
<body>
<form method="post" action="/divide">
输入第一个数:<input type="text" name="number1"><br>
输入第二个数:<input type="text" name="number2"><br>
<input type="submit" value="确定">
</form>
</body>
1
2
3
4
5
6
7
@RestController
public class NumberController {
@PostMapping("/divide")
public String divide(Integer number1, Integer number2) {
return String.valueOf(number1 / number2);
}
}

接下来我们处理除以 0 的情况,建议在 @ExceptionHandler 注解后添加具体的异常类,而不是异常的父类,提高匹配准确度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// @RestControllerAdvice是包括了@ResponseBody的
// 但是@ControllerAdvice灵活性相对更高点
@ControllerAdvice
public class GlobalExceptionHandler {

// 定义方法处理数学异常
@ExceptionHandler({ArithmeticException.class}) // 指定算数异常类
public String handlerArithmeticException(ArithmeticException e,
Model model) {
String error = e.getMessage();
model.addAttribute("ArithmeticException", error);
return "error"; // 返回视图
}

// 这个异常处理器做兜底,不至于其他异常没办法处理,匹配的话会优先匹配上面的处理器
@ExceptionHandler({Exception.class})
public String handlerDefaultException(Exception e,
Model model) {
String error = e.getMessage();
model.addAttribute("Exception", error);
return "error";
}

}

BeanValidation 异常处理

使用 JSR-303 验证参数时,我们是在 Controller 方法,声明 BindingResul 对象获取校验结果。Controller 的方法很多,每个方法都加入 BindingResult 处理检验参数比较繁琐。校验参数失败抛出异常给框架,异常处理器能够捕获到 MethodArgumentNotValidException,它是 BindException 的子类。接下来我们演示一下如何利用异常处理器处理 BeanValidation 的异常

准备 Order 类:

1
2
3
4
5
6
7
8
9
10
11
12
public class Order {
@NotBlank(message = "订单不能为空")
private String name;

@NotNull(message = "数量不能为空")
@Range(min = 1, max = 99, message = "订单商品数量在{min}到{max}之间")
private Integer amount;

@NotNull(message = "用户不能为空")
@Min(value = 1, message = "用户id从{value}开始")
private Integer userId;
}

接下来的 Controller 只需要对要检查的参数加上 @Validated 注解即可:

1
2
3
4
5
6
7
@RestController
public class OrderController {
@PostMapping("/order/new")
public String createOrder(@Validated @RequestBody Order order) {
return order.toString();
}
}

最后在异常处理器上针对 MethodArgumentNotValidException 进行异常处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@ControllerAdvice
public class GlobalExceptionHandler {

// 处理JSR303验证参数的异常
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseBody
public Map<String, Object> handlerMethodArgumentNotValidException(MethodArgumentNotValidException e,
Model model) {
Map<String, Object> map = new HashMap<>();
// 获取异常结果
BindingResult bindingResult = e.getBindingResult();
if (bindingResult.hasErrors()) {
List<FieldError> errors = bindingResult.getFieldErrors();
for (int i = 0; i < errors.size(); i++) {
FieldError fieldError = errors.get(i);
map.put(fieldError.getField() + "-" + i, fieldError.getDefaultMessage());
}
}
return map;
}

// 上述异常处理只是一个演示,是不规范的,规范处理需要使用ProblemDetail
}

完整异常处理简单示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* 处理BeanValidation异常,用于@RequestBody校验失败的情况
* @param e MethodArgumentNotValidException
* @return 响应字段异常信息
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
public Result<String> handlerMethodArgumentNotValidException(MethodArgumentNotValidException e) {
StringBuilder builder = new StringBuilder();
BindingResult bindingResult = e.getBindingResult();
if (bindingResult.hasErrors()) {
bindingResult.getFieldErrors().forEach(fieldError ->
builder.append(fieldError.getField()).append(": ")
.append(fieldError.getDefaultMessage()).append(";\n")
);
}
return Result.error(builder.toString());
}

/**
* 处理BeanValidation异常,用于@RequestParam@PathVariable校验失败的情况
* @param ex ConstraintViolationException
* @return 响应字段异常信息
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result<String> handleConstraintViolationException(ConstraintViolationException ex) {
StringBuilder builder = new StringBuilder();
ex.getConstraintViolations().forEach(violation -> {
String fieldName = violation.getPropertyPath().toString();
String errorMessage = violation.getMessage();
builder.append(fieldName).append(": ")
.append(errorMessage).append(";\n");
});
return Result.error(builder.toString());
}

/**
* 处理BeanValidation异常,用于处理在表单或@RequestParam参数的绑定过程中,格式校验失败的场景。
* @param e BindException
* @return 响应字段异常信息
*/
@ExceptionHandler(BindException.class)
public Result<String> handleBindException(BindException e) {
StringBuilder builder = new StringBuilder();
e.getBindingResult().getFieldErrors().forEach(fieldError ->
builder.append(fieldError.getField()).append(": ")
.append(fieldError.getDefaultMessage()).append(";\n")
);
return Result.error(builder.toString());
}

/**
* 处理BeanValidation异常,用于处理当请求中的参数类型与方法参数的期望类型不匹配的情况
* @param e MethodArgumentTypeMismatchException
* @return 响应异常字段信息
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public Result<String> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
StringBuilder builder = new StringBuilder();
builder.append("参数: ").append(e.getName()).append(" 类型不匹配, 期待类型: ")
.append(e.getRequiredType().getName()).append(", 实际输入: ")
.append(e.getValue()).append(";\n");
return Result.error(builder.toString());
}

ProblemDetail

如果不作特定的异常处理,SpringBoot 也有默认的异常反馈,但是默认的异常反馈内容比较单一,包含 Http Status Code,时间,异常信息。但具体异常原因没有体现。这次 SpringBoot3 对错误信息增强了,使用的类是 ProblemDetail。

标准字段 描述 必须
type 标识错误类型的uri 可认为是
title 问题类型的简短描述
detail 错误信息的详细描述
instance 特定故障实例的uri
status 状态码

除了上述字段,还可以由用户自己自定义字段,丰富对应答结果的说明。

以下几个类,都直接或者间接地包含了 ProblemDetail,我们在进行异常处理的时候,可以返回这些类

  • ProblemDetail 类:封装标准字段和扩展字段的简单对象。
  • ErrorResponse:错误应答类,完整的 RFC 7807 错误响应的表示,包括 status、headers 和 RFC 7807 格式的ProblemDetail 正文。
  • ErrorResponseException:ErrorResponse 接口一个实现,可以作为一个方便的基类。扩展自定义的错误处理类。
  • ResponseEntityExceptionHandler:它处理所有 SpringMVC 异常,与 @ControllerAdvice 一起使用。

ProblemDetail 基础使用如下,先准备 Book 类:

1
public record BookRecord(String isbn, String name, String author) {}
1
2
3
4
5
6
// BookContainer用来包含所有的书本类数据
// 记得要在启动类上进行包扫描,不然没办法读取配置文件中的数据集
@ConfigurationProperties(prefix = "product")
public class BookContainer {
private List<BookRecord> books;
}
1
2
3
4
5
6
7
8
9
10
11
product:
books:
- isbn: B001
name: java
author: lisi
- isbn: B002
name: tomcat
author: zhangsan
- isbn: B003
name: jvm
author: wangwu

准备自定义异常:

1
2
3
4
5
6
7
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException() {}

public BookNotFoundException(String message) {
super(message);
}
}

controller 用来接收请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
public class BookController {
@Autowired
private BookContainer bookContainer;

// 根据isbn查询图书,如果没有查到,抛出异常
@GetMapping("/book")
public BookRecord getBook(String isbn) {
Optional<BookRecord> bookOption = bookContainer.getBooks().stream().filter(book ->
book.isbn().equals(isbn)
).findFirst();

if (bookOption.isEmpty()) {
throw new BookNotFoundException(isbn + "没有此图书");
}

return bookOption.get();
}

}

异常处理器处理异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BookNotFoundException.class)
public ProblemDetail bookNotFound(BookNotFoundException e) {
// 使用ProblemDetail处理异常
ProblemDetail problemDetail = ProblemDetail
.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
// type:异常类型,应该是一个uri,通过uri找到解决问题的途径
problemDetail.setType(URI.create("/api/problem/notFound"));
// title:异常信息描述
problemDetail.setTitle("图书异常");
return problemDetail;
}
}

ProblemDetail 自定义字段

修改异常处理方法,增加 ProblemDetail 自定义字段,自定义字段以Map存储,调用setProperty(name,value)将自定义字段添加到 ProblemDetail 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BookNotFoundException.class)
public ProblemDetail bookNotFound(BookNotFoundException e) {
// 使用ProblemDetail处理异常
ProblemDetail problemDetail = ProblemDetail
.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
// type:异常类型,应该是一个uri,通过uri找到解决问题的途径
problemDetail.setType(URI.create("/api/problem/notFound"));
// title:异常信息描述
problemDetail.setTitle("图书异常");

// 添加自定义字段
problemDetail.setProperty("时间", Instant.now());
problemDetail.setProperty("客服", "100886");

return problemDetail;
}
}

ErrorResponse

SpringBoot 识别 ErrorResponse 类型作为异常的应答结果。可以直接使用 ErrorResponse 作为异常处理方法的返回值,ErrorResponseException 是 ErrorResponse 的基本实现类。

1
2
3
4
5
6
7
8
9
10
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(BookNotFoundException.class)
public ErrorResponse bookNotFound(BookNotFoundException e) {
// 使用ErrorResponse处理异常
ErrorResponse error = new ErrorResponseException(HttpStatus.NOT_FOUND, e);
return error;
}
}

扩展 ErrorResponseException

自定义异常可以扩展 ErrorResponseException,SpringMVC 将处理异常并以符合 RFC 7807 的格式返回错误响应。ResponseEntityExceptionHandler 能够处理大部分 SpringMVC 的异常。

由此可以创建自定义异常类,继承 ErrorResponseException,剩下的交给 SpringMVC 内部自己处理就好。省去了自己的异常处理器,@ExceptionHandler。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 自定义异常类,让框架内置的异常处理器使用
public class IsbnNotFoundException extends ErrorResponseException {

private static ProblemDetail createProblemDetail(HttpStatus httpStatus,
String detail) {
// 封装字段
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(httpStatus, detail);
// 指定解决方案uri
problemDetail.setType(URI.create("api/problem/notfound"));
problemDetail.setDetail(detail);
problemDetail.setTitle("图书异常");
// 自定义字段
problemDetail.setProperty("严重程度", "低");
problemDetail.setProperty("客服", "100886");
return problemDetail;
}

// 异常类的构造方法
public IsbnNotFoundException(HttpStatus httpStatus, String detail) {
// 调用父类的构造方法
super(httpStatus, createProblemDetail(httpStatus, detail), null);
}
}

此外,还需要在配置文件中添加对 RFC 7087 的支持,并且需要保证没有别的自定义异常处理器存在:

1
spring.mvc.problemdetails.enabled=true

HttpExchange

远程访问是开发的常用技术,一个应用能够访问其他应用的功能。SpringBoot 提供了多种远程访问的技术。基于 HTTP 协议的远程访问是支付最广泛的。SpringBoot3 提供了新的 HTTP 的访问能力,通过接口简化 HTTP 远程访问,类似 Feign 功能。Spring 包装了底层 HTTP 客户的访问细节。

SpringBoot 中定义接口提供 HTTP 服务。生成的代理对象实现此接口,代理对象实现 HTTP 的远程访问,需要使用 @HttpExchange 和 WebClient 来完成。

WebClient 特性:

我们想要调用其他系统提供的 HTTP 服务,通常可以使用 Spring 提供的 RestTemplate 来访问,RestTemplate 是 Spring 3 中引入的同步阻塞式 HTTP 客户端,因此存在一定性能瓶颈。Spring 官方在 Spring5 中引入了 WebClient 作为非阻塞式 HTTP 客户端。

一个免费的,提供 24h 在线的 Rest Http 服务:点我进去。安装 GsonFormat 插件可以帮助我们快速进行 json 和 bean 的转换。并且,使用 WebClient 时别忘了加载 Spring Reactive Web 依赖。

先准备 java 类:

1
2
3
4
5
6
public class ToDo {
private int userId;
private int id;
private String title;
private boolean completed;
}

再准备 service 接口:

1
2
3
4
5
// 一个方法就是一个远程调用
public interface ToDoService {
@GetExchange("/todos/{id}") // 用来访问第三方接口的
ToDo getTodoById(@PathVariable Integer id);
}

准备配置类用于创建代理对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration(proxyBeanMethods = false)
public class HttpConfiguration {

// 创建服务接口的代理对象,基于WebClient
@Bean
public ToDoService requestService() {
WebClient webClient = WebClient.builder()
.baseUrl("https://jsonplaceholder.typicode.com") // 指定基地址
.build();

// 创建代理
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory
.builderFor(WebClientAdapter.create(webClient)).build();


// 通过工厂创建代理的服务
return proxyFactory.createClient(ToDoService.class);
}
}

使用接口方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
class Lession18HttpServiceApplicationTests {

// 注入远程服务的代理对象
@Autowired
private ToDoService toDoService;

// 测试访问
@Test
void testQuery() {
ToDo todo1 = toDoService.getTodoById(1);
System.out.println(todo1);
}
}

组合注解

我们还可以搭配多个注解进行组合注解开发:

1
2
3
4
5
6
7
8
// 定义基地址
@HttpExchange("https://jsonplaceholder.typicode.com/")
public interface AlbumsService {

@HttpExchange(method = "GET", url = "/albums/{id}")
Albums getById(@PathVariable Integer id);

}

创建代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration(proxyBeanMethods = false)
public class HttpConfiguration {

// 创建代理
@Bean
public AlbumsService albumsService() {
// 创建WebClient
WebClient webClient = WebClient.create();

// 创建代理工厂
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory
.builderFor(WebClientAdapter.create(webClient)).build();

// 创建代理服务
return proxyFactory.createClient(AlbumsService.class);
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class Lession18HttpServiceApplicationTests {

@Autowired
private AlbumsService albumsService;

@Test
void testAlbums() {
Albums albums = albumsService.getById(1);
System.out.println(albums);
}
}

定制 HTTP 请求服务

设置 HTTP 远程的超时时间,异常处理。在创建接口代理对象前,先设置 WebClient 的有关配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration(proxyBeanMethods = false)
public class HttpConfiguration {

// 定制服务
public AlbumsService albumsService() {
// 设置超时时间
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000) // 连接时间
.doOnConnected(conn -> { // 指定连接对象
conn.addHandlerLast(new ReadTimeoutHandler(10)); // 读超时
conn.addHandlerLast(new WriteTimeoutHandler(10)); // 写超时
});

// 设置异常处理
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient)) // 构建连接器
.defaultStatusHandler(HttpStatusCode::isError, clientResponse -> {// 定制默认错误
System.out.println("WebClient请求异常");
return Mono.error(new RuntimeException("请求异常" + clientResponse.statusCode().value()));
})
.build();

// 创建代理
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory
.builderFor(WebClientAdapter.create(webClient)).build();

return proxyFactory.createClient(AlbumsService.class);
}
}

Thymeleaf

Thymeleaf 是一个表现层的模板引擎,一般被使用在 Web 环境中,它可以处理 HTML、XML、JS 等文档,简单来说,它可以将 JSP 作为 Java Web 应用的表现层,有能力展示与处理数据。Thymeleaf 可以让表现层的界面节点与程序逻辑被共享,这样的设计,可以让界面设计人员、业务人员与技术人员都参与到项目开发中。

这样,同一个模板文件,既可以使用浏览器直接打开,也可以放到服务器中用来显示数据,并且样式之间基本上不会存在差异,因此界面设计人员与程序设计人员可以使用同一个模板文件,来查看静态与动态数据的效果。

Thymeleaf 作为视图展示模型数据,用于和用户交互操作。JSP 的代替技术。比较适合做管理系统,是一种易于学习,掌握的。我们通过几个示例掌握 Thymeleaf 基础应用。

变量表达式和链接表达式

表达式 作用 例子
${...} 变量表达式,可用于获取后台传过来的值 中国
@{...} 链接网址表达式 th:href="@{/css/home.css}"

利用控制器往 request 作用域放入数据

1
2
3
4
5
6
7
8
9
10
11
@Controller
public class ThymeleafController {

@GetMapping("/exp")
public String exp(Model model) {
model.addAttribute("name", "zhangsan");
model.addAttribute("address", "hnu");
return "exp";
}

}

Thymeleaf 使用变量表达式展示数据:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div th:text="${name}"></div>
<div th:text="${address}"></div>
</body>
</html>

使用链接网址表达式传递参数,格式为(key1=value1,key2=value2...)

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a th:href="@{/link(id=111,name=lisi)}">link链接,有参数</a>
</body>
</html>

if-for

表达式 作用 例子
th:if="boolean表达式" 当条件满足时,显示代码片段,反之不显示 显示内容
处理循环 见下方代码块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Controller
public class ThymeleafController {

@GetMapping("/if-for")
public String ifFor(Model model) {
// 增加单个简单值
model.addAttribute("login", true);

// 增加单个对象
User user = new User();
user.setId(1001);
user.setAge(20);
user.setName("李四");

model.addAttribute("user", user);

// 增加多个对象
List<User> list = new ArrayList<>();
Collections.addAll(list, new User(1002, "zhangsan", 20),
new User(1003, "wangwu", 21));
model.addAttribute("users", list);

return "base";
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>if-for</h3>
<div th:if="10>2">10大于2</div>
<div th:if="${login}">用户已经登录</div>
<div th:if="${user.getAge()}>18">用户已经成年</div>
<br>
<h3>循环</h3>
<table border="1px">
<tr>
<th>id</th>
<th>name</th>
<th>age</th>
</tr>
<tr th:each="u:${users}">
<td th:text="${u.getId()}"></td>
<td th:text="${u.getName()}"></td>
<td th:text="${u.getAge()}"></td>
</tr>
</table>
</body>
</html>

默认配置

1
2
3
4
# 视图前缀
spring.thymeleaf.prefix=classpath:/templates/
# 视图后缀
spring.thymeleaf.suffix=.html

AOT 和 GraalVM

提升性能的技术

JIT (just in time)是现在 JVM 提高执行速度的技术,JVM 执行 Java 字节码,并将经常执行的代码编译为本机代码。这称为实时(JIT)编译。

当 JVM 发现某个方法或优码块运行特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。然后 JIT 会把“热点代码”编译成本地机器相关的机器码,并进行优化,然后再把编译后的机器码缓存起来,以备下次使用。

JVM 根据执行期间收集的分析信息决定 JIT 编译哪些代码。JIT 编译器速度很快,但是 Java 程序非常大,以至于JIT 需要很长时间才能完全预热。不经常使用的 Java 方法可能根本不会被编译。

特点:在程序执行时,边运行代码边编译。JIT编译需要时间开销,空间开销,只有对执行频繁的代码才值得编译。

AOT(Ahead-of-Time Compilation),预编译(提前编译)它在 JEP-295 中描述,并在 Java9 中作为实验性功能添加。

AOT 是提升 Java 程序性能的一种方法,特别是提供 JVM 的启动时间。在启动虚拟机之前,将 Java 类编译为本机代码。改进小型和大型 Java 应用程序的启动时间。

总的来讲,AOT 是静态的,提升了应用启动时间,让 JVM 加载编译后的本机代码。而 JIT 是动态的,提升的是应用程序执行的性能。(现在主要还是使用 JIT,但是 Spring 框架提供了对 AOT 的支持)

Native Image

Native Image:原生镜像(本机镜像)。本机映像是一种预先将 Java 代码编译为独立可执行文件的技术,称为本机映像(原生镜像)。镜像是用于执行的文件。

原生镜像文件内容包括应用程序类、来自其依赖项的类、运行时库类和来自 JDK 的静态链接本机代码(二进制文件可以直接运行,不需要额外安装JDK),本机映像运行在 GraalVM 上,具有更快的启动时间和更低的运行时内存开销。(通常原生镜像文件的大小是原文件的几十倍甚至几百倍)

在 AOT 模式下,编译器在构建项目期间执行所有编译工作,这里的主要想法是将所有的”繁重工作”——昂贵的计算——转移到构建时间。也就是把项目都要执行的所有东西都准备好,具体执行的类,文件等。最后执行这个准备好的文件,此时应用能够快速启动。减少内存,cpu 开销(无需运行时的 JIT 的编译)。因为所有东西都是预先计算和预先编译好的。

Native Image Builder

Native Image Builder(镜像构建器):是一个实用程序,用于处理应用程序的所有类及其依赖项,包括来自 JDK 的类。它静态地分析这些数据以确定在应用程序执行期间可以访问哪些类和方法。然后,它预先将可到达的代码和数据编译为特定操作系统和体系结构的本机可执行文件。

1
2
3
4
5
6
7
flowchart TD
id1(AOT)
id2(Native Image)
id3(Native Image Builder)

id1 --使用镜像文件--> id2
id3 --生成Native Image文件--> id2

GraalVM

GraalVM 是一个高性能 JDK 发行版,旨在加速用 Java 和其他 JVM 语言编写的应用程序,同时支持 JavaScript、Ruby、Python 和许多其他流行语言。GraalVM 的多语言功能可以在单个应用程序中混合多种编程语言,同时消除外语调用成本。GraalVM 是支持多语言的虚拟机。(也就是说,使用 go 语言编写高并发模块,使用 java 语言编写健壮性更强的模块等,这些模块都可以直接在 GraalVM 上跑)

GraalVM 是 OpenJDK 的替代方案,包含一个名为 native image 的工具,支持预先(ahead-of-time,AOT)编译。GraalVM 执行 native image 文件启动速度更快,使用的 CPU 和内存更少,并且磁盘大小更小。这使得 Java 在云中更具竞争力。

目前,AOT 的重点是允许使用 GraalVM 将 Spring 应用程序部署为本机映像。SpringBoot3 中使用 GraalVM 方案提供 Native Image 支持。

GraalVM 的 native image 工具将 Java 字节码作为输入,输出一个本地可执行文件。为了做到这一点,该工具对字节码进行静态分析。在分析过程中,该工具寻找你的应用程序实际使用的所有代码,并消除一切不必要的东西。native image 是封闭式的静态分析和编译,不支持 class 的动态加载,程序运行所需要的多有依赖项均在静态分析阶段完成。

上一篇:
Vue
下一篇:
Maven