SpringBoot
SpringBoot 是目前流行的微服务框架,其目的是用来简化新 Spring 应用的初始化搭建以及开发过程。SpringBoot 提供了很多核心功能,比如自动化配置 starter(启动器)简化 Maven 配置、内嵌 Servlet 容器、应用监控等功能,让我们可以快速构建企业级应用程序。
SpringBoot 入门
脚手架
软件工程中的脚手架是用来快速搭建一个小的可用的应用程序的骨架,将开发过程中要用到的工具,环境都配置好,同时生成必要的模板代码。Spring Initializr 是创建 Spring Boot 的脚手架,IDEA 内置了此工具,可以帮助我们快速创建项目。
Spring Initializr 脚手架的 Web 地址。
阿里云脚手架。
项目中的.mvn
、mvnw.cmd
、HELP.md
、mvnw
可以删除。
在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 public class Lession06PackageApplication {
public static void main(String[] args) { ApplicationContext ctx = SpringApplication.run(Lession06PackageApplication.class, args); Date bean = ctx.getBean(Date.class) } }
|
运行 SpringBoot项目方式
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 允许在代码之外,提供应用程序运行的数据, 以便在不同的环境中使用相同的应用程序代码,避免硬编码,提供系统的灵活性。项目中常使用 properties
和 yaml
文件进行配置,其次是命令行参数。
配置文件的名称为application
,如果 properties
和 yml
类型都存在,优先加载 properties。不过,考虑到阅读性,我们更推荐使用 yml 格式的配置文件。
配置文件格式
配置文件格式有两种:properties
和 yaml(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("${app.owner}") private String name;
@Value("${app.password}") private String password;
@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
|
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() { if(environment.containsProperty("app.owner")) { System.out.println(environment.getProperty("app.owner")); } else { System.out.println("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
| @Configuration(proxyBeanMethods = false) @ConfigurationProperties(prefix = "app") public class AppBean {
private String name; private String owner; private String password;
}
|
嵌套 Bean
有些时候我们需要在 Bean 中包含其他 Bean 作为属性,将配置文件中的配置项绑定到 Bean 以及引用类型的成员。
例如有配置文件如下:
1 2 3 4 5 6 7 8 9
| app: name: Lession06-package owner: zhangsan password: 040809 port: 9090 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 { private String name; private String owner; private String password; private Integer port; private Security security;
}
|
扫描注解
要想让 @ConfigurationProperties
所绑定的 Bean 起作用,我们还需要是用 @EnableConfigurationProperties
或 @ConfigurationPropertiesScan
。这些注解是专门寻找 @ConfigurationProperties
注解的,将它的对象注入到 Spring 容器。在启动类上使用扫描注解:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
@ConfigurationPropertiesScan(basePackages = {"com.example.demo.config.pk5"})
@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 {
@ConfigurationProperties(prefix = "security") @Bean public Security createSecurity() { return new Security(); }
}
|
接下来可以使用对象:
1 2 3 4 5 6 7 8 9 10
| @Autowired Security security;
@Test void testApplicationConfig() {
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; }
public class MyServer { private String title; private String ip; }
@ConfigurationProperties public class CollectionConfig { private List<MyServer> servers; private Map<String, User> users; private String[] names; }
|
application 配置文件中编写属性配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
names: - lisi - zhangsan
servers: - title: 华北服务器 ip: 202.12.9.7 - title: 西南服务器 ip: 106.23.32.11
users: 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; }
|
创建对象的三种方式
将对象注入到 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
| @ImportResource(locations = "classpath:/applicationContext.xml") @SpringBootApplication public class Lession06PackageApplication { public static void main(String[] args) { ConfigurableAppicationContext run = SpringApplication.run(Lession06PackageApplication.class, args); 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(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 { @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
| public interface ClazzMapper { @Select("select * from t_clazz where cid = #{cid}") Clazz getClazzById(Integer cid); }
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
| public interface StudentMapper { @Select("select * from t_stu where cid = #{cid}") List<Student> getStusById(int cid); }
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 minimum-idle: 10 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)。
声明式事务的方式:
@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) 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 public int myTransForm(String fromAccount, String toAccount, int money) { return transform(fromAccount, toAccount, money); }
@Override @Transactional(rollbackFor = Exception.class) 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) public int transform(String fromAccount, String toAccount, int money) { } }
|
回滚规则
Web
SpringBoot 可以创建两种类型的 Web 应用:
- 基于 Servlet 体系的 Spring Web MVC 应用。
- 使用 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 public class QuickController {
@RequestMapping("/exam/quick") 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 {
@RequestMapping("/exam/json") public void responseJson(HttpServletResponse response) throws IOException { String json = "{\"name\":\"lisi\",\"age\":18}"; response.setContentType("application/json;charset=utf-8"); response.getWriter().write(json); }
@RequestMapping("/exam/userJson") @ResponseBody public User getUserJson() { User user = new User(); user.setUsername("zhangsan"); user.setPassword("123"); return user; } }
|
favicon
favicon.ico 是网站的缩略标志,可以显示在浏览器标签、地址栏左边和收藏夹,是展示网站个性的 logo 标志。可以利用这个网站快速生成。
- 将生成的 favicon.ico 拷贝到项目的
resources/static/
目录。
- 在视图的
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")
@GetMapping("/images/*.gifs")
@GetMapping("/pic/**")
@GetMapping("/order/{*id}")
@GetMapping("/pages/{fname:\\w+}.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") public String parameterTest1(String name, Integer age, String gender) { return "接受参数: name = " + name + ", age = " + age + ", gender = " + gender; }
@RequestMapping("/exam/param/p2") public String parameterTest2(User user) { return user.toString(); }
@RequestMapping("/exam/param/p3") public String parameterTest3(HttpServletRequest request) { String name = request.getParameter("name"); String password = request.getParameter("password"); return name + " " + password; }
@RequestMapping("/exam/param/p4") 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") public String parameterTest5(@RequestHeader("Accept") String accept) { return accept; }
@RequestMapping("/exam/param/p6") public String parameterTest6(@RequestBody User user) { return user.toString(); }
@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") 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 { @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.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;
}
|
自定义验证
已有的注解不能满足所有的校验需求的时候,特殊的情况需要自定义校验(自定义校验注解)。步骤如下:
- 自定义注解 State。
- 自定义校验数据的类 StateValidation 实现 ConstrainValidator 接口。
- 在需要校验的地方使用自定义注解。
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 {};
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> {
@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,职责:
- 是一个门面,接收请求,控制请求的处理过程。所有请求都必须有 DispatcherServlet 控制。SpringMVC 对外的入口。可以看做门面设计模式。
- 访问其他的控制器,这些控制器处理业务逻辑。
- 创建合适的视图,将 2 中得到业务结果放到视图,响应给用户。
- 解耦了其他组件,所有组件只与 DispatcherServlet 交互,彼此之间没有关联。
- 实现 ApplictionContextAware,每个 DispatcherServlet 都拥自己的 WebApplicationContext,它继承了ApplicationContext(意味着 DispatcherServlet 也可以看作一个容器,可以访问到各种 Bean)。WebApplicationContext 包含了Web 相关的 Bean 对象,比如开发人员注释 @Controller 的类,视图解析器,视图对象等等。DispatcherServlet 访问容器中 Bean 对象。
- 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=/
server.servlet.encoding.charset=utf-8
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
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
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
public String date(LocalDateTime date) { return "时间: " + date; }
@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
| @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 registrationBean = new ServletRegistrationBean();
registrationBean.setServlet(new LoginServlet()); registrationBean.addUrlMappings("/login"); registrationBean.setLoadOnStartup(1);
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 { 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() { FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new LogFilter()); registrationBean.addUrlPatterns("/*");
return registrationBean; } }
|
Filter 的排序
多个 Filter 对象如果要排序,有两种途径:
- 滤器类名称,按字典顺序排列,AuthFilter -> LogFilter。
- 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() { FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new LogFilter()); registrationBean.addUrlPatterns("/*"); registrationBean.setOrder(1);
return registrationBean; } @Bean public FilterRegistrationBean addAuthFilter() { FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new AuthFilter()); registrationBean.addUrlPatterns("/*"); registrationBean.setOrder(2);
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() { } @Bean public FilterRegistrationBean addCommonLogFilter() { FilterRegistrationBean registrationBean = new FilterRegistrationBean();
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter(); filter.setIncludeQueryString(true);
registrationBean.setFilter(filter); registrationBean.addUrlPatterns("/*"); registrationBean.setOrder(1);
return registrationBean; } }
|
使用这个过滤器时还需要把日志的级别设置成 debug 级别:
Listener 的创建
Listener 平时用的比较少,这里不作详细介绍。如果想要创建 Listener,可以继承 HttpSessionListener,并使用 @WebListener 注解进行标记。另一种方式是使用 ServletListenerRegistrationBean 登记 Listener 对象。
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) { 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> {}
|
一些和硬件打交道的项目,数据格式往往不是我们平常见到的那样,可能是一串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> {
@Override public DeviceInfo parse(String text, Locale locale) throws ParseException { DeviceInfo deviceInfo = null; 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; }
@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 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 { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("AuthInterceptor拦截器执行了"); String user = request.getParameter("user"); 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(".")); 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
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
|
@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 { @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; }
}
|
完整异常处理简单示例如下:
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
|
@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()); }
@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()); }
@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()); }
@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
|
@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;
@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 .forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage()); problemDetail.setType(URI.create("/api/problem/notFound")); 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 .forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage()); problemDetail.setType(URI.create("/api/problem/notFound")); 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 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); 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 {
@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.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 的动态加载,程序运行所需要的多有依赖项均在静态分析阶段完成。