spring boot快速入门
1.什么是SpringBoot
-
一个javaweb的开发框架,和SpringMVC类似,对比其他javaweb框架的好处,官方说是简化开发,约定大于配置, you can "just run",能迅速的开发web应用,几行代码开发一个http接口。
-
所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景 衍生 一种规范框架,人们只需要进行各种配置而不需要自己去实现它,这时候强大的配置功能成了优点;发展到一定程度之后,人们根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架;之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡“约定大于配置”,进而衍生出一些一站式的解决方案。
-
是的这就是Java企业级应用->J2EE->spring->springboot的过程。
-
随着 Spring 不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,慢慢变得不那么易用简单,违背了最初的理念,甚至人称配置地狱。Spring Boot 正是在这样的一个背景下被抽象出来的开发框架,目的为了让大家更容易的使用 Spring 、更容易的集成各种常用的中间件、开源软件;
-
Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。Spring Boot 以约定大于配置的核心思想,默认帮我们进行了很多设置,多数 Spring Boot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),Spring Boot 应用中这些第三方库几乎可以零配置的开箱即用。
-
简单来说就是SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。
-
Spring Boot 出生名门,从一开始就站在一个比较高的起点,又经过这几年的发展,生态足够完善,Spring Boot 已经当之无愧成为 Java 领域最热门的技术。
Spring Boot的主要优点:
● 能够快速创建基于Spring的应用程序
● 能够直接使用java main方法启动内嵌的Tomcat服务器运行SpringBoot程序,不需要部署war包文件
● 提供约定的starter POM来简化Maven配置,让Maven的配置变得简单
● 自动化配置,根据项目的Maven依赖配置,Springboot自动配置Spring、Spring mvc等
● 提供了程序的健康检查等功能
● 基本可以完全不使用XML配置文件,采用注解配置
SpringBoot四大核心
● 自动配置
针对很多Spring应用程序和常见的应用功能,SpringBoot能自动提供相关配置
● 起步依赖
告诉SpringBoot需要什么功能,它就能引入需要的依赖库
● Actuator
让你能够深入运行中的SpringBoot应用程序,一探SpringBoot程序的内部信息
● 命令行界面
这是SpringBoot的可选特性,主要针对Groovy语言使用;
Groovy是一种基于JVM(Java虚拟机) 的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与Java代码很好地结合,也能用于扩展现有代码,由于其运行在JVM上的特性,Groovy可以使用其他Java语言编写的库。
2.第一个spring boot程序
2.1.使用 IDEA 直接创建项目
1、创建一个新项目
2、选择spring initalizr , 可以看到默认就是去官网的快速构建工具那里实现
3、填写项目信息
4、选择初始化的组件(初学勾选 Web 即可)
5、填写项目路径
6、等待项目构建成功
2.2.pom.xml 分析
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 父依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springbootOne</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springbootOne</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- web场景启动器 -->
<!--web依赖:tomcat,dispatcherServlet,xml...-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring-boot-starter:所有的springboot依赖都是使用这个开头的-->
<!-- springboot单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<!-- 剔除依赖 -->
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
-
项目元数据:创建时候输入的Project Metadata部分,也就是Maven项目的基本元素,包括:groupId、artifactId、version、name、description等
-
parent:继承
spring-boot-starter-parent
的依赖管理,控制版本与打包等内容 -
dependencies:项目具体依赖,这里包含了
spring-boot-starter-web
用于实现HTTP接口(该依赖中包含了Spring MVC),官网对它的描述是:使用Spring MVC构建Web(包括RESTful)应用程序的入门者,使用Tomcat作为默认嵌入式容器。spring-boot-starter-test
用于编写单元测试的依赖包。更多功能模块的使用将在后面逐步展开。 -
build:构建配置部分。默认使用了
spring-boot-maven-plugin
,配合spring-boot-starter-parent
就可以把Spring Boot应用打包成JAR来直接运行。
2.3.编写一个http接口
1、在主程序的同级目录下,新建一个controller包,一定要在同级目录下,否则识别不到
 {
SpringApplication.run(SpringbootOneApplication.class, args);
}
}
3.3.2注解(@SpringBootApplication)
-
作用:标注在某个类上说明这个类是SpringBoot的主配置
-
SpringBoot就应该运行这个类的main方法来启动SpringBoot应用;
-
进入这个注解:可以看到上面还有很多其他注解!
@ComponentScan
-
这个注解在Spring中很重要 ,它对应XML配置中的元素。
-
作用:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中
@SpringBootConfiguration
-
作用:SpringBoot的配置类 ,标注在某个类上 , 表示这是一个SpringBoot的配置类;
-
我们继续进去这个注解查看
-
这里的 @Configuration,说明这是一个spring的配置类 ,配置类就是对应Spring的xml 配置文件;
-
@Component 这就说明,启动类本身也是Spring中的一个组件而已,负责启动应用!
-
我们回到 SpringBootApplication 注解中继续看。
@EnableAutoConfiguration
-
开启自动配置功能
- 以前我们需要自己配置的东西,而现在SpringBoot可以自动帮我们配置 ;
- @EnableAutoConfiguration告诉SpringBoot开启自动配置功能,这样自动配置才能生效;
点进注解接续查看:
-
@AutoConfigurationPackage :自动配置包
-
-
@import :Spring底层注解@import , 给容器中导入一个组件
-
Registrar.class 作用:自动配置包注册,将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器 ;
-
这个分析完了,退到上一步,继续看
-
-
@Import({AutoConfigurationImportSelector.class}) :给容器导入组件 ;
- AutoConfigurationImportSelector :自动配置导入选择器,那么它会导入哪些组件的选择器呢?我们点击去这个类看源码:
// 获取所有的配置
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
-
获得候选的配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { // 和下面的方法对应 //这里的getSpringFactoriesLoaderFactoryClass()方法 //返回的就是我们最开始看的启动自动导入配置文件的注解类;EnableAutoConfiguration List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct."); return configurations; } //和上面的类的方法loadFactoryNames里面的第一个参数对应 //这里的getSpringFactoriesLoaderFactoryClass()方法 //返回的就是我们最开始看的启动自动导入配置文件的注解类;EnableAutoConfiguration protected Class<?> getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class; }
-
这个方法
getCandidateConfigurations()
又调用了SpringFactoriesLoader
类的静态方法!我们进入SpringFactoriesLoader
类loadFactoryNames() 方法,获取所有的加载配置public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); //这里它又调用了 loadSpringFactories 方法 return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList()); }
-
我们继续点击查看 loadSpringFactories 方法
- 项目资源:
META-INF/spring.factories
- 系统资源:
META-INF/spring.factories
- 从这些资源中配置了所有的nextElement(自动配置),分装成properties
//将所有的资源加载到配置类中(将下面的抽离出来分析,第15行) Properties properties = PropertiesLoaderUtils.loadProperties(resource);
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { //获得classLoader , 我们返回可以看到这里得到的就是EnableAutoConfiguration标注的类本身 MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader); if (result != null) { return result; } else { try { //去获取一个资源 "META-INF/spring.factories" Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories"); LinkedMultiValueMap result = new LinkedMultiValueMap(); //判断有没有更多的元素,将读取到的资源循环遍历,封装成为一个Properties while(urls.hasMoreElements()) { URL url = (URL)urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); Iterator var6 = properties.entrySet().iterator(); while(var6.hasNext()) { Entry<?, ?> entry = (Entry)var6.next(); String factoryClassName = ((String)entry.getKey()).trim(); String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue()); int var10 = var9.length; for(int var11 = 0; var11 < var10; ++var11) { String factoryName = var9[var11]; result.add(factoryClassName, factoryName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException var13) { throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13); } } }
- 项目资源:
-
发现一个多次出现的文件:spring.factories
3.3.3.spring.factories
我们根据源头打开spring.factories , 看到了很多自动配置的文件;这就是自动配置根源所在!
3.3.4.WebMvcAutoConfiguration
在上面的自动配置类随便找一个打开看看,比如 :WebMvcAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
public static final String DEFAULT_PREFIX = "";
public static final String DEFAULT_SUFFIX = "";
private static final String[] SERVLET_LOCATIONS = { "/" };
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
可以看到这些一个个的都是JavaConfig配置类,而且都注入了一些Bean,可以找一些自己认识的类,看着熟悉一下!
所以,自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中
3.3.5.结论:
-
SpringBoot在启动的时候从类路径下的
META-INF/spring.factories
中获取EnableAutoConfiguration
指定的值 -
将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
-
以前我们需要自动配置的东西,现在springboot帮我们做了
-
整合JavaEE,整体解决方案和自动配置的东西都在
springboot-autoconfigure
的jar包中; -
它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器中
-
它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并自动配置,@Configuration(javaConfig) ;
-
有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;
3.4启动
@SpringBootApplication
public class Springboot01HellowordApplication {
public static void main(String[] args) {
//该方法返回一个ConfigurableApplicationContext对象
//参数一:应用入口的类; 参数二:命令行参数
SpringApplication.run(Springboot01HellowordApplication.class, args);
}
}
SpringApplication.run分析
- 分析该方法主要分两部分
- 一是SpringApplication的实例化,
- 二是run方法的执行;
3.4.1.SpringApplication
这个类主要做了以下四件事情:
-
推断应用的类型是普通的项目还是Web项目
-
查找并加载所有可用初始化器 , 设置到initializers属性中
-
找出所有的应用程序监听器,设置到listeners属性中
-
推断并设置main方法的定义类,找到运行的主类
查看构造器:
3.4.2.run方法流程分析
4.yaml语法学习
4.1.配置文件
SpringBoot使用一个全局的配置文件 , 配置文件名称是固定的
-
application.properties
-
- 语法结构 :key=value
-
application.yaml
-
- 语法结构 :key:空格 value
配置文件的作用 :修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们自动配置好了;
比如我们可以在配置文件中修改Tomcat 默认启动的端口号!测试一下!
server:
port: 8081
4.2.YAML
yaml概述
-
YAML是 "YAML Ain't a Markup Language" (YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)
-
这种语言以数据作为中心,而不是以标记语言为重点!
-
以前的配置文件,大多数都是使用xml来配置;比如一个简单的端口配置,我们来对比下yaml和xml
-
传统xml配置:
<server> <port>8081<port> </server>
-
yaml配置:
server: prot: 8080
-
yaml基础语法
说明:语法要求严格!
-
空格不能省略
-
以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
-
属性和值的大小写都是十分敏感的。
字面量:普通的值 [ 数字,布尔值,字符串 ]
-
字面量直接写在后面就可以 , 字符串默认不用加上双引号或者单引号;
k: v
注意:
-
“ ” 双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思;
比如 :name: "wang\n jingmo" 输出 :wang换行 jingmo
-
'' 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出
比如 :name: ‘wang\n jingmo’ 输出 :wang\n jingmo
-
对象、Map(键值对)
#对象、Map格式
k:
v1:
v2:
在下一行来写对象的属性和值得关系,注意缩进;比如:
student:
name: wangyanling
age: 3
行内写法
student: {name: wangyanling,age: 3}
数组( List、set )
用 - 值表示数组中的一个元素,比如:
pets:
- cat
- dog
- pig
行内写法
pets: [cat,dog,pig]
修改SpringBoot的默认端口号
配置文件中添加,端口号的参数,就可以切换端口;
server:
port: 8082
4.3.注入配置文件
yaml文件强大的地方在于可以给实体类直接注入匹配值
4.3.1. yaml注入配置文件
① 在springboot项目中的resources目录下新建一个文件 application.yml
② 编写一个实体类 Dog
package com.wyl.springboot.pojo;
@Component //注册bean到容器中
public class Dog {
private String name;
private Integer age;
//有参无参构造、get、set方法、toString()方法
}
③ 试着用@Value给bean注入属性值
@Component //注册bean
public class Dog {
@Value("旺财")
private String name;
@Value("1")
private Integer age;
}
④ 在SpringBoot的测试类下注入并输出
@SpringBootTest
class DemoApplicationTests {
@Autowired //将狗狗自动注入进来
Dog dog;
@Test
public void contextLoads() {
System.out.println(dog); //打印看下狗狗对象
}
}
结果成功输出,@Value注入成功
Dog{name='旺财', age=1}
⑤ 再编写一个复杂点的实体类
@Component //注册bean到容器中
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
//有参无参构造、get、set方法、toString()方法
}
⑥ 使用yaml配置的方式进行注入
写的时候注意区别和优势,首先编写一个yaml配置
person:
name: wjm
age: 3
happy: false
birth: 2000/01/01
maps: {k1: v1,k2: v2}
lists:
- code
- girl
- music
dog:
name: 旺财
age: 1
⑦ 把对象的所有值都写好后,注入到类中
/*
@ConfigurationProperties作用:
将配置文件中配置的每一个属性的值,映射到这个组件中;
告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
参数 prefix = “person” : 将配置文件中的person下面的所有属性一一对应
*/
@Component //注册bean
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
}
⑧ IDEA 提示,springboot配置注解处理器没有找到
Not Found
The requested URL /spring-boot/docs/2.3.3.RELEASE/reference/html/configuration-metadata.html was not found on this server.
查看文档(在网址中更改版本获得,如回到2.1.9),找到一个依赖
<!-- 导入配置文件处理器,配置文件进行绑定就会有提示,需要重启 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
⑨ 确认以上配置都完成后,去测试类中测试
@SpringBootTest
class DemoApplicationTests {
@Autowired
Person person; //将person自动注入进来
@Test
public void contextLoads() {
System.out.println(person); //打印person信息
}
}
结果:所有值全部注入成功
4.3.2. 加载指定配置文件
@PropertySource :加载指定的配置文件;
@configurationProperties:默认从全局配置文件中获取值
- 在resources目录下新建一个person.properties文件
name=hello
- 在代码中指定加载person.properties文件
@PropertySource(value = "classpath:person.properties")
@Component //注册bean
public class Person {
@Value("${name}")
private String name;
......
}
- 再次输出测试,指定配置文件绑定成功
4.3.3.配置文件占位符
配置文件还可以编写占位符生成随机数
person:
name: wangjingmo${random.uuid} # 随机uuid
age: ${random.int} # 随机int
happy: false
birth: 2000/01/01
maps: {k1: v1,k2: v2}
lists:
- code
- girl
- music
dog:
name: ${person.hello:other}_旺财
age: 1
4.3.4.回顾properties配置
上面采用的yaml方法都是最简单的方式,也是开发中最常用的、pringboot所推荐的
接下来看看其他的实现方式,原理都是相同的,写还是那样写
配置文件除了yml还有之前常用的properties
【注意】properties配置文件在写中文的时候会有乱码 , 需要去IDEA中设置编码格式为UTF-8:settings-->FileEncodings 中配置
测试步骤
- 新建一个实体类User
@Component //注册bean
public class User {
private String name;
private int age;
private String sex;
}
- 编辑配置文件 user.properties
user1.name=wyl
user1.age=18
user1.sex=男
- 在User类上使用@Value来进行注入
@Component //注册bean
@PropertySource(value = "classpath:user.properties")
public class User {
//直接使用@value
@Value("${user.name}") //从配置文件中取值
private String name;
@Value("#{9*2}") // #{SPEL} Spring表达式
private int age;
@Value("男") // 字面量
private String sex;
}
- Springboot测试
SpringBootTest
class DemoApplicationTests {
@Autowired
User user;
@Test
public void contextLoads() {
System.out.println(user);
}
}
结果正常输出
4.3.4.对比小结
@Value使用起来并不友好!我们需要为每个属性单独注解赋值比较麻烦
- @ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加
- 松散绑定:这个什么意思呢? 比如yml中写的last-name,这个和lastName是一样的,
-
后面跟着的字母默认是大写的。这就是松散绑定 - JSR303数据校验 ,可以在字段是增加一层过滤器验证 , 保证数据的合法性
- 复杂类型封装,yml中可以封装对象 , 使用value就不支持
结论:
- 配置yml和配置properties都可以获取到值 , 强烈推荐 yml;
- 如果在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;
- 如果专门编写了一个JavaBean来和配置文件进行一一映射,就直接使用@configurationProperties
5.JSR303数据校验
Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。这里来写个注解让name只能支持Email格式
@Component //注册bean
@ConfigurationProperties(prefix = "person")
@Validated //数据校验
public class Person {
@Email(message="邮箱格式错误") //name必须是邮箱格式
private String name;
}
运行结果:default message [不是一个合法的电子邮件地址]
使用数据校验,可以保证数据的正确性; 下面列出一些常见的使用
@NotNull(message="名字不能为空")
private String userName;
@Max(value=120,message="年龄最大不能查过120")
private int age;
@Email(message="邮箱格式错误")
private String email;
空检查
@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空
格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.
Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
.......等等
除此以外,我们还可以自定义一些数据校验规则
5.1.多环境切换
profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境
5.1.1.多配置文件
在主配置文件编写的时候,文件名可以是application-{profile}.properties/yml
, 用来指定多个环境版本。例如:application-test.properties
代表测试环境配置 application-dev.properties
代表开发环境配置
但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置文件。但可以通过配置来选择需要激活的环境
#比如在配置文件中指定使用dev环境,我们可以通过设置不同的端口号进行测试;
#我们启动SpringBoot,就可以看到已经切换到dev下的配置了;
spring.profiles.active=dev
5.1.2.yml的多文档块
和properties配置文件中一样,但使用yml去实现不需要创建多个配置文件,更加方便
server:
port: 8081
#选择要激活那个环境块
spring:
profiles:
active: prod
---
server:
port: 8083
spring:
profiles: dev #配置环境的名称
---
server:
port: 8084
spring:
profiles: prod #配置环境的名称
注意:如果yml和properties同时都配置了端口,并且没有激活其他环境 , 默认会使用properties配置文件的
5.1.3.配置文件加载位置
外部加载配置文件的方式很多,一般选择最常用的即可,在开发的资源文件中进行配置
springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件
优先级1:项目路径下的config文件夹配置文件
优先级2:项目路径下配置文件
优先级3:资源路径下的config文件夹配置文件
优先级4:资源路径下配置文件
优先级由高到底,高优先级的配置会覆盖低优先级的配置;
SpringBoot会从这四个位置全部加载主配置文件;互补配置
4.4.4.运维小技巧
指定位置加载配置文件
我们还可以通过spring.config.location
来改变默认的配置文件位置
项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;
这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高
java -jar spring-boot-config.jar --spring.config.location=F:/application.properties
6.自动配置原理
----联系---- spring.factories
SpringBoot官方文档中有大量的配置,我们无法全部记住,官网:https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/html/appendix-application-properties.html#core-properties
6.1.分析自动配置原理
-
SpringBoot启动的时候加载主配置类,开启了自动配置功能 @EnableAutoConfiguration
-
@EnableAutoConfiguration 作用
-
利用EnableAutoConfigurationImportSelector给容器中导入一些组件
-
可以查看selectImports()方法的内容,他返回了一个autoConfigurationEnty,来自
this.getAutoConfigurationEntry(autoConfigurationMetadata,annotationMetadata);
这个方法我们继续来跟踪: -
这个方法有一个值:
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
叫做获取候选的配置 ,我们点击继续跟踪SpringFactoriesLoader.loadFactoryNames()
- 扫描所有jar包类路径下
META-INF/spring.factories
- 把扫描到的这些文件的内容包装成properties对象
- 从properties中获取到EnableAutoConfiguration.class类(类名)对应的值,然后把他们添加在容器中
-
在类路径下,
META-INF/spring.factories
里面配置的所有EnableAutoConfiguration的值加入到容器中:
-
-
- 每一个这样的 xxxAutoConfiguration类都是容器中的一个组件,都加入到容器中;用他们来做自动配置;
-
每一个自动配置类进行自动配置功能;
-
我们以HttpEncodingAutoConfiguration(Http编码自动配置)为例解释自动配置原理;
//表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件; @Configuration //启动指定类的ConfigurationProperties功能; //进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来; //并把HttpProperties加入到ioc容器中 @EnableConfigurationProperties({HttpProperties.class}) //Spring底层@Conditional注解 //根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效; //这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效 @ConditionalOnWebApplication( type = Type.SERVLET ) //判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器; @ConditionalOnClass({CharacterEncodingFilter.class}) //判断配置文件中是否存在某个配置:spring.http.encoding.enabled; //如果不存在,判断也是成立的 //即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的; @ConditionalOnProperty( prefix = "spring.http.encoding", value = {"enabled"}, matchIfMissing = true ) public class HttpEncodingAutoConfiguration { //他已经和SpringBoot的配置文件映射了 private final Encoding properties; //只有一个有参构造器的情况下,参数的值就会从容器中拿 public HttpEncodingAutoConfiguration(HttpProperties properties) { this.properties = properties.getEncoding(); } //给容器中添加一个组件,这个组件的某些值需要从properties中获取 @Bean @ConditionalOnMissingBean //判断容器没有这个组件? public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter(); filter.setEncoding(this.properties.getCharset().name()); filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST)); filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE)); return filter; } //。。。。。。。 }
一句话总结 :根据当前不同的条件判断,决定这个配置类是否生效!
-
一但这个配置类生效;这个配置类就会给容器中添加各种组件;
-
这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
-
所有在配置文件中能配置的属性都是在xxxxProperties类中封装着;
-
配置文件能配置什么就可以参照某个功能对应的这个属性类
//从配置文件中获取指定的值和bean的属性进行绑定 @ConfigurationProperties(prefix = "spring.http") public class HttpProperties { // ..... }
我们去配置文件里面试试前缀,看提示!
这就是自动装配的原理!
6.2.总结
-
SpringBoot启动会加载大量的自动配置类
-
我们看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;
-
我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)
-
给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;
xxxxAutoConfigurartion:自动配置类;给容器中添加组件
xxxxProperties:封装配置文件中相关属性;
6.3.@Conditional
了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;
@Conditional派生注解(Spring注解版原生的@Conditional作用)
作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;
@Conditional扩展注解 | 作用(判断是否满足当前指定条件) |
---|---|
@ConditionalOnJava | 系统的java版本是否符合要求 |
@ConditionalOnJava | 容器中存在指定Bean ; |
@ConditionalOnMissingBean | 容器中不存在指定Bean ; |
@ConditionalOnExpression | 满足SpEL表达式指定 |
@ConditionalOnClass | 系统中有指定的类 |
@ConditionalOnMissingClass | 系统中没有指定的类 |
@ConditionalOnSingleCandidate | 容器中只有一个指定的Bean ,或者这个Bean是首选Bean |
@ConditionalOnProperty | 系统中指定的属性是否有指定的值 |
@ConditionalOnResource | 类路径下是否存在指定资源文件 |
@ConditionalOnWebApplication | 当前是web环境 |
@ConditionalOnNotWebApplication | 当前不是web环境 |
@ConditionalOnJndi | JNDI存在指定项 |
那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是所有的都生效了。
6.4.自动配置类是否生效
我们可以在application.properties通过启用 debug=true
属性;
在控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效;
#开启springboot的调试类
debug=true
-
Positive matches:(自动配置类启用的:正匹配)
-
Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)
-
Unconditional classes: (没有条件的类)
6.5.自定义Starter
我们分析完毕了源码以及自动装配的过程,我们可以尝试自定义一个启动器来玩玩!
6.5.1.说明
启动器模块是一个 空 jar 文件,仅提供辅助性依赖管理,这些依赖可能用于自动装配或者其他类库;
命名归约:
官方命名:
- 前缀:spring-boot-starter-xxx
- 比如:spring-boot-starter-web....
自定义命名:
- xxx-spring-boot-starter
- 比如:mybatis-spring-boot-starter
6.5.2.编写启动器
-
在IDEA中新建一个空项目 spring-boot-starter-diy
-
新建一个普通Maven模块:kuang-spring-boot-starter
-
新建一个Springboot模块:kuang-spring-boot-starter-autoconfigure
-
点击apply即可,基本结构
-
在我们的 starter 中 导入 autoconfigure 的依赖!
<!-- 启动器 --> <dependencies> <!-- 引入自动配置模块 --> <dependency> <groupId>com.kuang</groupId> <artifactId>kuang-spring-boot-starter-autoconfigure</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies>
-
将 autoconfigure 项目下多余的文件都删掉,Pom中只留下一个 starter,这是所有的启动器基本配置!
-
我们编写一个自己的服务
package wyl.ss; public class HelloService { HelloProperties helloProperties; public HelloProperties getHelloProperties() { return helloProperties; } public void setHelloProperties(HelloProperties helloProperties) { this.helloProperties = helloProperties; } public String sayHello(String name){ return helloProperties.getPrefix() + name + helloProperties.getSuffix(); } }
-
编写
HelloProperties
配置类package wyl.ss; import org.springframework.boot.context.properties.ConfigurationProperties; // 前缀 kuang.hello @ConfigurationProperties(prefix = "kuang.hello") public class HelloProperties { private String prefix; private String suffix; public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public String getSuffix() { return suffix; } public void setSuffix(String suffix) { this.suffix = suffix; } }
-
编写我们的自动配置类并注入bean,测试!
package wyl.ss; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConditionalOnWebApplication //web应用生效 @EnableConfigurationProperties(HelloProperties.class) public class HelloServiceAutoConfiguration { @Autowired HelloProperties helloProperties; @Bean public HelloService helloService(){ HelloService service = new HelloService(); service.setHelloProperties(helloProperties); return service; } }
-
在resources编写一个自己的
META-INF\spring.factories
# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ wyl.ss.HelloServiceAutoConfiguration
-
编写完成后,可以安装到maven仓库中!
6.5.3.新建项目测试我们自己写的启动器
-
新建一个SpringBoot 项目
-
导入我们自己写的启动器
<dependency> <groupId>wyl.ss</groupId> <artifactId>ss-spring-boot-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
-
编写一个
HelloController
进行测试我们自己的写的接口!package wyl.ss.controller; @RestController public class HelloController { @Autowired HelloService helloService; @RequestMapping("/hello") public String hello(){ return helloService.sayHello("zxc"); } }
-
编写配置文件
application.properties
ss.hello.prefix="ppp" ss.hello.suffix="sss"
-
启动项目进行测试,结果成功 !
7.整合JDBC
7.1.SpringData简介
对于数据访问层,无论是 SQL(关系型数据库) 还是 NOSQL(非关系型数据库),Spring Boot 底层都是采用 Spring Data 的方式进行统一处理。
Spring Boot 底层都是采用 Spring Data 的方式进行统一处理各种数据库,Spring Data 也是 Spring 中与 Spring Boot、Spring Cloud 等齐名的知名项目。
Sping Data 官网
数据库相关的启动器 :可以参考官方文档
7.2.创建项目
7.2.1.新建项目测试
引入相应的模块:Spring Web、SQL中的JDBC API、MySql Driver
项目建好之后,Spring Boot自动导入了启动器
7.2.2.编写yaml配置文件,连接数据库
新建一个 application.yml
spring:
datasource:
username: root
password: 123456
#?serverTimezone=UTC解决时区的报错
url: jdbc:mysql://localhost:3306/数据库名称?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
7.2.3.测试
配置完这一些东西后就可以直接去使用了,SpringBoot已经默认进行了自动配置
@SpringBootTest
class SpringbootDataJdbcApplicationTests {
//DI注入数据源
@Autowired
DataSource dataSource;
@Test
public void contextLoads() throws SQLException {
//看一下默认数据源
System.out.println(dataSource.getClass());
//获得连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
//关闭连接
connection.close();
}
}
可以看到默认配置的数据源为 class com.zaxxer.hikari.HikariDataSource
, 我们并没有手动配置
HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀
可以使用 spring.datasource.type 指定自定义的数据源类型,值为使用的连接池实现的完全限定名
有了数据库连接,就可以 CRUD 操作数据库了。但需要先了解对象 JdbcTemplate
7.2.4.源码
打开 DataSourceProperties 的源码,能配置的所有东西都在这
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
private ClassLoader classLoader;
private String name;
private boolean generateUniqueName = true;
private Class<? extends DataSource> type;
private String driverClassName;
private String url;
private String username;
private String password;
............
打开 DataSourceAutoConfiguration 源码,数据源的所有自动配置都在这里
7.2.5.JDBCTemplate
- 有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库
- 即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。
- 数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。
- Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用
- JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类
JdbcTemplate主要提供以下几类方法:
- execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
- update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
- query方法及queryForXXX方法:用于执行查询相关语句;
- call方法:用于执行存储过程、函数相关语句。
测试
新建一个 controller 目录,在里面编写一个 JdbcController
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/jdbc")
public class JdbcController {
/**
* Spring Boot 默认提供了数据源,默认提供了 org.springframework.jdbc.core.JdbcTemplate
* JdbcTemplate 中会自己注入数据源,用于简化 JDBC操作
* 还能避免一些常见的错误,使用起来也不用再自己来关闭数据库连接
*/
@Autowired
JdbcTemplate jdbcTemplate;
//查询employee表中所有数据
//List 中的1个 Map 对应数据库的 1行数据
//Map 中的 key 对应数据库的字段名,value 对应数据库的字段值
@GetMapping("/userList")
public List<Map<String,Object>> userList(){
String sql = "select * from user";
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
return maps;
}
//新增一个用户
@GetMapping("/add")
public String addUser(){
//插入语句,注意时间问题
String sql = "insert into mybatis.user(id, name,pwd)" +
" values (9,'Java编程思想','qqwweer987')";
jdbcTemplate.update(sql);
//查询
return "addOk";
}
//修改用户信息
@GetMapping("/update/{id}")
public String updateUser(@PathVariable("id") int id){
//插入语句
String sql = "update mybatis.user set name=?,pwd=? where id="+id;
//数据封装
Object[] objects = new Object[2];
objects[0] = "大威天龙";
objects[1] = "qwert123";
jdbcTemplate.update(sql,objects);
//查询
return "update-Ok";
}
//删除用户
@GetMapping("/delete/{id}")
public String delUser(@PathVariable("id") int id){
//插入语句
String sql = "delete from mybatis.user where id=?";
jdbcTemplate.update(sql,id);
//查询
return "delete-Ok";
}
}
8.集成 druid
8.1.druid简介
-
Java程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。
-
Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。
-
Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。
-
Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。
-
Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控。
-
Github地址:https://github.com/alibaba/druid/
8.2.配置
加入druid相关配置(.yml配置文件)
spring:
#数据库配置
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
username: root
password: root
druid:
# 初始连接数
initial-size: 10
# 最大连接池数量
max-active: 100
# 最小连接池数量
min-idle: 10
# 配置获取连接等待超时的时间
max-wait: 60000
# 打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: false
wall:
config:
multi-statement-allow: true
com.alibaba.druid.pool.DruidDataSource 基本配置参数如下:
配置 | 缺省值 | 说明 |
---|---|---|
name | 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。 如果没有配置,将会生成一个名字,格式是:“DataSource-” + System.identityHashCode(this) | |
jdbcUrl | 连接数据库的url,不同数据库不一样。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto | |
username | 连接数据库的用户名 | |
password | 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里:https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter | |
driverClassName | 根据url自动识别 | 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName(建议配置下) |
initialSize | 0 | 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 |
maxActive | 8 | 最大连接池数量 |
maxIdle | 8 | 已经不再使用,配置了也没效果 |
minIdle | 最小连接池数量 | |
maxWait | 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 | |
poolPreparedStatements | false | 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 |
maxOpenPreparedStatements | -1 | 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 |
validationQuery | 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。 | |
validationQueryTimeout | 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法 |
|
testOnBorrow | true | 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 |
testOnReturn | false | 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 |
testWhileIdle | false | 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效 |
timeBetweenEvictionRunsMillis | 1分钟 ( 1.0.14 ) |
有两个含义: 1) Destroy线程会检测连接的间隔时间 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 |
numTestsPerEvictionRun | 不再使用,一个DruidDataSource只支持一个EvictionRun | |
minEvictableIdleTimeMillis | 30分钟 ( 1.0.14 ) |
连接保持空闲而不被驱逐的最长时间 |
connectionInitSqls | 物理连接初始化的时候执行的sql | |
exceptionSorter | 根据dbType自动识别 | 当数据库抛出一些不可恢复的异常时,抛弃连接 |
filters | 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall | |
proxyFilters | 类型是List<com.alibaba.druid.filter.Filter>,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系 |
引入druid依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
项目启动后可通过show processlist查看mysql的全部线程是否和配置的initial-size一致。
8.3.druid监控测试
访问ip:port/druid验证即可,url中的/druid要和配置文件中的url-pattern一致
stat-view-servlet:
enabled: true
url-pattern: /druid/*
效果如下:
9.整合mybatis
9.1.导入 MyBatis 所需要的依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
9.2.配置数据库连接信息
spring:
datasource:
username: root
password: admin
#?serverTimezone=UTC解决时区的报错
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
#Spring Boot 默认是不注入这些属性值的,需要自己绑定
#druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
9.3.创建实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
9.4.创建mapper
UserMapper.java
// 这个注解表示了这是一个 mybatis 的 mapper 类
@Mapper
@Repository
public interface UserMapper {
List<User> queryUserList();
User queryUserById(int id);
int addUser(User user);
int updateUser(User user);
int deleteUser(int id);
}
-
对应的Mapper映射文件
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!--namespace=绑定一个对应的Dao/Mapper接口--> <mapper namespace="com.wyl.mapper.UserMapper"> <select id="queryUserList" resultType="User"> select * from mybatis.user; </select> <select id="queryUserById" resultType="User"> select * from mybatis.user where id = #{id}; </select> <insert id="addUser" parameterType="User"> insert into mybatis.user (id, name, pwd) values (#{id},#{name},#{pwd}); </insert> <update id="updateUser" parameterType="User"> update mybatis.user set name=#{name},pwd = #{pwd} where id = #{id}; </update> <delete id="deleteUser" parameterType="int"> delete from mybatis.user where id = #{id} </delete> </mapper>
-
maven配置资源过滤问题
<resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> </resources>
-
编写部门的 UserController 进行测试!
@RestController public class UserController { @Autowired private UserMapper userMapper; @GetMapping("/queryUserList") public List<User> queryUserList() { List<User> userList = userMapper.queryUserList(); for (User user : userList) { System.out.println(user); } return userList; } //添加一个用户 @GetMapping("/addUser") public String addUser() { userMapper.addUser(new User(7,"wyl","123456")); return "ok"; } //修改一个用户 @GetMapping("/updateUser") public String updateUser() { userMapper.updateUser(new User(7,"wjm","123456")); return "ok"; } @GetMapping("/deleteUser") public String deleteUser() { userMapper.deleteUser(7); return "ok"; } }
启动项目访问进行测试!
9.5.分页
添加相关依赖
首先,我们需要在 pom.xml 文件中添加分页插件依赖包。
pom.xml
<!-- pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
添加相关配置
然后在 application.yml 配置文件中添加分页插件有关的配置。
application.yml
# pagehelper
pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql
编写分页代码
首先,在 DAO 层添加一个分页查找方法。这个查询方法跟查询全部数据的方法除了名称几乎一样。
SysUserMapper.java
import java.util.List;
import com.louis.springboot.demo.model.SysUser;
public interface SysUserMapper {
int deleteByPrimaryKey(Long id);
int insert(SysUser record);
int insertSelective(SysUser record);
SysUser selectByPrimaryKey(Long id);
int updateByPrimaryKeySelective(SysUser record);
int updateByPrimaryKey(SysUser record);
/**
* 查询全部用户
* @return
*/
List<SysUser> selectAll();
/**
* 分页查询用户
* @return
*/
List<SysUser> selectPage();
}
然后在 SysUserMapper.xml 中加入selectPage的实现,当然你也可以直接用@Select注解将查询语句直接写在DAO代码,但我们这里选择写在XML映射文件,这是一个普通的查找全部记录的查询语句,并不需要写分页SQL,分页插件会拦截查询请求,并读取前台传来的分页查询参数重新生成分页查询语句。
SysUserMapper.xml
<select id="selectPage" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from sys_user
</select>
服务层通过调用DAO层代码完成分页查询,这里统一封装分页查询的请求和结果类,从而避免因为替换ORM框架而导致服务层、控制层的分页接口也需要变动的情况,替换ORM框架也不会影响服务层以上的分页接口,起到了解耦的作用。
SysUserService.java
import java.util.List;
import com.louis.springboot.demo.model.SysUser;
import com.louis.springboot.demo.util.PageRequest;
import com.louis.springboot.demo.util.PageResult;
public interface SysUserService {
/**
* 根据用户ID查找用户
* @param userId
* @return
*/
SysUser findByUserId(Long userId);
/**
* 查找所有用户
* @return
*/
List<SysUser> findAll();
/**
* 分页查询接口
* 这里统一封装了分页请求和结果,避免直接引入具体框架的分页对象, 如MyBatis或JPA的分页对象
* 从而避免因为替换ORM框架而导致服务层、控制层的分页接口也需要变动的情况,替换ORM框架也不会
* 影响服务层以上的分页接口,起到了解耦的作用
* @param pageRequest 自定义,统一分页查询请求
* @return PageResult 自定义,统一分页查询结果
*/
PageResult findPage(PageRequest pageRequest);
}
服务实现类通过调用分页插件完成最终的分页查询,关键代码是 PageHelper.startPage(pageNum, pageSize),将前台分页查询参数传入并拦截MyBtis执行实现分页效果。
SysUserServiceImpl.java
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.louis.springboot.demo.dao.SysUserMapper;
import com.louis.springboot.demo.model.SysUser;
import com.louis.springboot.demo.service.SysUserService;
import com.louis.springboot.demo.util.PageRequest;
import com.louis.springboot.demo.util.PageResult;
import com.louis.springboot.demo.util.PageUtils;
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired
private SysUserMapper sysUserMapper;
@Override
public SysUser findByUserId(Long userId) {
return sysUserMapper.selectByPrimaryKey(userId);
}
@Override
public List<SysUser> findAll() {
return sysUserMapper.selectAll();
}
@Override
public PageResult findPage(PageRequest pageRequest) {
return PageUtils.getPageResult(pageRequest, getPageInfo(pageRequest));
}
/**
* 调用分页插件完成分页
* @param pageQuery
* @return
*/
private PageInfo<SysUser> getPageInfo(PageRequest pageRequest) {
int pageNum = pageRequest.getPageNum();
int pageSize = pageRequest.getPageSize();
PageHelper.startPage(pageNum, pageSize);
List<SysUser> sysMenus = sysUserMapper.selectPage();
return new PageInfo<SysUser>(sysMenus);
}
}
在控制器SysUserController中添加分页查询方法,并调用服务层的分页查询方法。
SysUserController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.louis.springboot.demo.service.SysUserService;
import com.louis.springboot.demo.util.PageRequest;
@RestController
@RequestMapping("user")
public class SysUserController {
@Autowired
private SysUserService sysUserService;
@GetMapping(value="/findByUserId")
public Object findByUserId(@RequestParam Long userId) {
return sysUserService.findByUserId(userId);
}
@GetMapping(value="/findAll")
public Object findAll() {
return sysUserService.findAll();
}
@PostMapping(value="/findPage")
public Object findPage(@RequestBody PageRequest pageQuery) {
return sysUserService.findPage(pageQuery);
}
}
分页查询请求封装类。
PageRequest.java
/**
* 分页请求
*/
public class PageRequest {
/**
* 当前页码
*/
private int pageNum;
/**
* 每页数量
*/
private int pageSize;
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
}
分页查询结果封装类。
PageResult.java
import java.util.List;
/**
* 分页返回结果
*/
public class PageResult {
/**
* 当前页码
*/
private int pageNum;
/**
* 每页数量
*/
private int pageSize;
/**
* 记录总数
*/
private long totalSize;
/**
* 页码总数
*/
private int totalPages;
/**
* 数据模型
*/
private List<?> content;
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public long getTotalSize() {
return totalSize;
}
public void setTotalSize(long totalSize) {
this.totalSize = totalSize;
}
public int getTotalPages() {
return totalPages;
}
public void setTotalPages(int totalPages) {
this.totalPages = totalPages;
}
public List<?> getContent() {
return content;
}
public void setContent(List<?> content) {
this.content = content;
}
}
分页查询相关工具类。
PageUtils.java
import com.github.pagehelper.PageInfo;
public class PageUtils {
/**
* 将分页信息封装到统一的接口
* @param pageRequest
* @param page
* @return
*/
public static PageResult getPageResult(PageRequest pageRequest, PageInfo<?> pageInfo) {
PageResult pageResult = new PageResult();
pageResult.setPageNum(pageInfo.getPageNum());
pageResult.setPageSize(pageInfo.getPageSize());
pageResult.setTotalSize(pageInfo.getTotal());
pageResult.setTotalPages(pageInfo.getPages());
pageResult.setContent(pageInfo.getList());
return pageResult;
}
}
编译测试运行
启动应用,访问:localhost:8088/swagger-ui.html,找到对应接口,模拟测试,结果如下。
参数:pageNum: 1, pageSize: 5
10.事务管理
SpringBoot 使用事务非常简单,底层依然采用的是Spring本身提供的事务管理
• 在入口类中使用注解 @EnableTransactionManagement 开启事务支持
• 在访问数据库的Service方法上添加注解 @Transactional 即可
案例思路
通过SpringBoot +MyBatis实现对数据库学生表的更新操作,在service层的方法中构建异常,查看事务是否生效;
项目名称:springboot--transacation
10.1.实现步骤
10.1.1.StudentController
@Controller
public class SpringBootController {
@Autowired
private StudentService studentService;
@RequestMapping(value = "/springBoot/update")
public @ResponseBody Object update() {
Student student = new Student();
student.setId(1);
student.setName("Mark");
student.setAge(100);
int updateCount = studentService.update(student);
return updateCount;
}
}
10.1.2.StudentService接口
public interface StudentService {
/**
* 根据学生标识更新学生信息
* @param student
* @return
*/
int update(Student student);
}
10.1.3.StudentServiceImpl
接口实现类中对更新学生方法进行实现,并构建一个异常,同时在该方法上加@Transactional注解
@Override
@Transactional //添加此注解说明该方法添加的事务管理
public int update(Student student) {
int updateCount = studentMapper.updateByPrimaryKeySelective(student);
System.out.println("更新结果:" + updateCount);
//在此构造一个除数为0的异常,测试事务是否起作用
int a = 10/0;
return updateCount;
}
10.1.4.Application
在类上加@EnableTransactionManagement开启事务支持
@EnableTransactionManagement可选,但是@Service必须添加事务才生效
@SpringBootApplication
@EnableTransactionManagement //SpringBoot开启事务的支持
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
10.1.5.启动Application
1.查看数据库,并没有修改数据,通过以上结果,说明事务起作用了。
2.注释掉StudentServiceImpl上的@Transactional测试----数据库发上来改变。
11.SpringMVC注解
SpringBoot下的SpringMVC和之前的SpringMVC使用是完全一样的,主要有以下注解:
1.@Controller
Spring MVC的注解,处理http请求
2.@RestController
Spring4后新增注解,是@Controller注解功能的增强,是@Controller与@ResponseBody的组合注解;
如果一个Controller类添加了@RestController,那么该Controller类下的所有方法都相当于添加了@ResponseBody注解;
用于返回字符串或json数据。
案例:
• 创建MyUserController类,演示@RestController替代@Controller + @ResponseBody
@RestController
public class MyUserController {
@Autowired
private UserService userService;
@RequestMapping("/user/getUser")
public Object getUser(){
return userService.getUser(1);
}
}
3.@RequestMapping(常用)
支持Get请求,也支持Post请求
4.@GetMapping
RequestMapping和Get请求方法的组合只支持Get请求;Get请求主要用于查询操作。
5.@PostMapping
RequestMapping和Post请求方法的组合只支持Post请求;Post请求主要用户新增数据。
6.@PutMapping
RequestMapping和Put请求方法的组合只支持Put请求;Put通常用于修改数据。
7.@DeleteMapping
RequestMapping 和 Delete请求方法的组合只支持Delete请求;通常用于删除数据。
12.RESTful实现
Spring boot开发RESTFul 主要是几个注解实现:
@PathVariable:获取url中的数据,该注解是实现RESTFul最主要的一个注解。
@PostMapping:接收和处理Post方式的请求
@DeleteMapping:接收delete方式的请求,可以使用GetMapping代替
@PutMapping:接收put方式的请求,可以用PostMapping代替
@GetMapping:接收get方式的请求
RESTful的优点
• 轻量,直接基于http,不再需要任何别的诸如消息协议,get/post/put/delete为CRUD操作
• 面向资源,一目了然,具有自解释性。
• 数据描述简单,一般以xml,json做数据交换。
• 无状态,在调用一个接口(访问、操作资源)的时候,可以不用考虑上下文,不用考虑当前状态,极大的降低了复杂度。
• 简单、低耦合
12.1.案例
使用RESTful风格模拟实现对学生的增删改查操作
该项目集成了MyBatis、spring、SpringMVC,通过模拟实现对学生的增删改查操作。
1.创建RESTfulController,并编写代码
@RestController
public class RESTfulController {
/**
* 添加学生
* 请求地址:http://localhost:8080/springboot-restful/springBoot/student/wyl/23
* 请求方式:POST
* @param name
* @param age
* @return
*/
@PostMapping(value = "/springBoot/student/{name}/{age}")
public Object addStudent(@PathVariable("name") String name,
@PathVariable("age") Integer age) {
Map retMap = new HashMap();
retMap.put("name",name);
retMap.put("age",age);
return retMap;
}
/**
* 删除学生
* 请求地址:http://localhost:8080/springboot-restful/springBoot/student/1
* 请求方式:Delete
* @param id
* @return
*/
@DeleteMapping(value = "/springBoot/student/{id}")
public Object removeStudent(@PathVariable("id") Integer id) {
return "删除的学生id为:" + id;
}
/**
* 修改学生信息
* 请求地址:http://localhost:8080/springboot-restful/springBoot/student/2
* 请求方式:Put
* @param id
* @return
*/
@PutMapping(value = "/springBoot/student/{id}")
public Object modifyStudent(@PathVariable("id") Integer id) {
return "修改学生的id为" + id;
}
@GetMapping(value = "/springBoot/student/{id}")
public Object queryStudent(@PathVariable("id") Integer id) {
return "查询学生的id为" + id;
}
}
12.2.RESTful原则
• 增post请求、删delete请求、改put请求、查get请求
• 请求路径不要出现动词
例如:查询订单接口
/boot/order/1021/1(推荐)
/boot/queryOrder/1021/1(不推荐)
• 分页、排序等操作,不需要使用斜杠传参数
例如:订单列表接口 /boot/orders?page=1&sort=desc
一般传的参数不是数据库表的字段,可以不采用斜杠
13.静态资源处理
13.1.对哪些目录映射?
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/
/:当前项目的根路径
就我们在上面五个目录下放静态资源(比如:a.png等),可以直接访问(http://localhost:8080/a.png),类似于以前web项目的webapp下;放到其他目录下无法被访问。
优先级:resources > static(默认) > public
13.2.源码分析
SpringBoot自动配置的WebMvcAutoConfiguration.java
类
-
SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置类里面;
-
我们可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;
-
有一个方法:addResourceHandlers 添加资源处理
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { // 已禁用默认资源处理 logger.debug("Default resource handling disabled"); return; } // 缓存控制 Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl(); // webjars 配置 if (!registry.hasMappingForPattern("/webjars/**")) { customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/") .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } // 静态资源配置 String staticPathPattern = this.mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern) .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations())) .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } }
读一下源代码:比如所有的
/webjars/**
, 都需要去classpath:/META-INF/resources/webjars/
找对应的资源;
13.2.1.什么是webjars 呢?
Webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。
13.2.2.第一种静态资源映射规则
使用SpringBoot需要使用Webjars,我们可以去搜索一下:
要使用jQuery,我们只要要引入jQuery对应版本的pom依赖即可!
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.1</version>
</dependency>
导入完毕,查看webjars目录结构,并访问Jquery.js文件!
访问:只要是静态资源,SpringBoot就会去对应的路径寻找资源,我们这里访问:http://localhost:8080/webjars/jquery/3.4.1/jquery.js
13.2.3.第二种静态资源映射规则
1、那我们项目中要是使用自己的静态资源该怎么导入呢?我们看下一行代码;
2、我们去找staticPathPattern
发现第二种映射规则 :/** , 访问当前的项目任意资源,它会去找 resourceProperties
这个类,我们可以点进去看一下分析:
// 进入方法
public String[] getStaticLocations() {
return this.staticLocations;
}
// 找到对应的值
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
// 找到路径
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
};
3、ResourceProperties 可以设置和我们静态资源有关的参数;这里面指向了它会去寻找资源的文件夹,即上面数组的内容。
4、所以得出结论,以下四个目录存放的静态资源可以被我们识别:
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
5、我们可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件;
6、比如我们访问 http://localhost:8080/1.js , 他就会去这些文件夹中寻找对应的静态资源文件;
13.2.4.引用
不用写static 路径
<link rel="stylesheet" href="/layui/css/layui.css">
<link rel="stylesheet" href="/easyui/default/easyui.css">
<script src="/layui/jquery-1.10.2.min.js" type="text/javascript"></script>
<script src="/easyui/jquery.easyui.min.js" type="text/javascript"></script>
13.3.自定义静态资源路径
首先,自定义会覆盖默认!所以没十足把握的情况下,不建议覆盖,但可以添加。
两种方式
13.3.1.配置类代码实现
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("file:F:/AppFiles/");
}
}
13.3.2.配置文件
spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,\
classpath:/static/,classpath:/public/,file:c:/appfiles/
13.3.3.相对路径配置
以上的情况均是绝对路径,受限于环境,开发生产LinuxWin等。这种变化情况多,建议理清思路,再决定是否适用。
解决方案
String gitPath = path.getParentFile().getParentFile().getParent()
+ File.separator + "logistics" + File.separator + "uploads"
+ File.separator;
工具类
spring框架自带的ResourceUtils
,或者结合第三方工具
13.3.4. 欢迎页与图标
14.国际化
国际化(Internationalization 简称 I18n,其中“I”和“n”分别为首末字符,18 则为中间的字符数)是指软件开发时应该具备支持多种语言和地区的功能。换句话说就是,开发的软件需要能同时应对不同国家和地区的用户访问,并根据用户地区和语言习惯,提供相应的、符合用具阅读习惯的页面和数据,例如,为中国用户提供汉语界面显示,为美国用户提供提供英语界面显示。
在 Spring 项目中实现国际化,通常需要以下 3 步:
- 编写国际化资源(配置)文件;
- 使用 ResourceBundleMessageSource 管理国际化资源文件;
- 在页面获取国际化内容。
14.1. 编写国际化资源文件
在 Spring Boot 的类路径下创建国际化资源文件,文件名格式为:基本名_语言代码_国家或地区代码,例如 login_en_US.properties、login_zh_CN.properties。
以 spring-boot-springmvc-demo1为例,在 src/main/resources 下创建一个 i18n 的目录,并在该目录中按照国际化资源文件命名格式分别创建以下三个文件,
- login.properties:无语言设置时生效
- login_en_US.properties :英语时生效
- login_zh_CN.properties:中文时生效
以上国际化资源文件创建完成后,IDEA 会自动识别它们,并转换成如下的模式:
打开任意一个国际化资源文件,并切换为 Resource Bundle 模式,然后点击“+”号,创建所需的国际化属性,如下图。
14.2.配置文件生效探究
Spring Boot 已经对 ResourceBundleMessageSource 提供了默认的自动配置。
Spring Boot 通过 MessageSourceAutoConfiguration 对 ResourceBundleMessageSource 提供了默认配置,其部分源码如下。
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration.ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {
private static final Resource[] NO_RESOURCES = {};
// 将 MessageSourceProperties 以组件的形式添加到容器中
// MessageSourceProperties 下的每个属性都与以 spring.messages 开头的属性对应
@Bean
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
//Spring Boot 会从容器中获取 MessageSourceProperties
// 读取国际化资源文件的 basename(基本名)、encoding(编码)等信息
// 并封装到 ResourceBundleMessageSource 中
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
//读取国际化资源文件的 basename (基本名),并封装到 ResourceBundleMessageSource 中
if (StringUtils.hasText(properties.getBasename())) {
messageSource.setBasenames(StringUtils
.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
}
//读取国际化资源文件的 encoding (编码),并封装到 ResourceBundleMessageSource 中
if (properties.getEncoding() != null) {
messageSource.setDefaultEncoding(properties.getEncoding().name());
}
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
return messageSource;
}
...
}
从以上源码可知:
Spring Boot 将 MessageSourceProperties 以组件的形式添加到容器中;
MessageSourceProperties 的属性与配置文件中以“spring.messages”开头的配置进行了绑定;
Spring Boot 从容器中获取 MessageSourceProperties 组件,并从中读取国际化资源文件的 basename(文件基本名)、encoding(编码)等信息,将它们封装到 ResourceBundleMessageSource 中;
Spring Boot 将 ResourceBundleMessageSource 以组件的形式添加到容器中,进而实现对国际化资源文件的管理。
查看 MessageSourceProperties 类,其代码如下。
public class MessageSourceProperties {
private String basename = "messages";
private Charset encoding;
@DurationUnit(ChronoUnit.SECONDS)
private Duration cacheDuration;
private boolean fallbackToSystemLocale;
private boolean alwaysUseMessageFormat;
private boolean useCodeAsDefaultMessage;
public MessageSourceProperties() {
this.encoding = StandardCharsets.UTF_8;
this.fallbackToSystemLocale = true;
this.alwaysUseMessageFormat = false;
this.useCodeAsDefaultMessage = false;
}
...
}
通过以上代码,我们可以得到以下 3 点信息:
- MessageSourceProperties 为 basename、encoding 等属性提供了默认值;
- basename 表示国际化资源文件的基本名,其默认取值为“message”,即 Spring Boot 默认会获取类路径下的 message.properties 以及 message_XXX.properties 作为国际化资源文件;
- 在 application.porperties/yml 等配置文件中,使用配置参数“spring.messages.basename”即可重新指定国际化资源文件的基本名。
通过以上源码分析可知,Spring Boot 已经对国际化资源文件的管理提供了默认自动配置,我们这里只需要在 Spring Boot 全局配置文件中,使用配置参数“spring.messages.basename”指定我们自定义的国际资源文件的基本名即可,代码如下(当指定多个资源文件时,用逗号分隔)。
spring.messages.basename=i18n.login
14.3. 获取国际化内容
由于页面使用的是 Tymeleaf 模板引擎,因此我们可以通过表达式 #{...} 获取国际化内容。
以 spring-boot-adminex 为例,在 login.html 中获取国际化内容,代码如下。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="description" content="">
<meta name="author" content="ThemeBucket">
<link rel="shortcut icon" href="#" type="image/png">
<title>Login</title>
<!--将js css 等静态资源的引用修改为 绝对路径-->
<link href="css/style.css" th:href="@{/css/style.css}" rel="stylesheet">
<link href="css/style-responsive.css" th:href="@{/css/style-responsive.css}" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="js/html5shiv.js" th:src="@{/js/html5shiv.js}"></script>
<script src="js/respond.min.js" th:src="@{/js/respond.min.js}"></script>
<![endif]-->
</head>
<body class="login-body">
<div class="container">
<form class="form-signin" th:action="@{/user/login}" method="post">
<div class="form-signin-heading text-center">
<h1 class="sign-title" th:text="#{login.btn}">Sign In</h1>
<img src="/images/login-logo.png" th:src="@{/images/login-logo.png}" alt=""/>
</div>
<div class="login-wrap">
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
<input type="text" class="form-control" name="username" placeholder="User ID" autofocus
th:placeholder="#{login.username}"/>
<input type="password" class="form-control" name="password" placeholder="Password"
th:placeholder="#{login.password}"/>
<label class="checkbox">
<input type="checkbox" value="remember-me" th:text="#{login.remember}">
<span class="pull-right">
<a data-toggle="modal" href="#myModal" th:text="#{login.forgot}"> </a>
</span>
</label>
<button class="btn btn-lg btn-login btn-block" type="submit">
<i class="fa fa-check"></i>
</button>
<div class="registration">
<!--Thymeleaf 行内写法-->
[[#{login.not-a-member}]]
<a class="" href="/registration.html" th:href="@{/registration.html}">
[[#{login.signup}]]
</a>
<!--thymeleaf 模板引擎的参数用()代替 ?-->
<br/>
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>|
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
</div>
</div>
<!-- Modal -->
<div aria-hidden="true" aria-labelledby="myModalLabel" role="dialog" tabindex="-1" id="myModal"
class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title">Forgot Password ?</h4>
</div>
<div class="modal-body">
<p>Enter your e-mail address below to reset your password.</p>
<input type="text" name="email" placeholder="Email" autocomplete="off"
class="form-control placeholder-no-fix">
</div>
<div class="modal-footer">
<button data-dismiss="modal" class="btn btn-default" type="button">Cancel</button>
<button class="btn btn-primary" type="button">Submit</button>
</div>
</div>
</div>
</div>
<!-- modal -->
</form>
</div>
<!-- Placed js at the end of the document so the pages load faster -->
<!-- Placed js at the end of the document so the pages load faster -->
<script src="js/jquery-1.10.2.min.js" th:src="@{/js/jquery-1.10.2.min.js}"></script>
<script src="js/bootstrap.min.js" th:src="@{/js/bootstrap.min.js}"></script>
<script src="js/modernizr.min.js" th:src="@{/js/modernizr.min.js}"></script>
</body>
</html>
14.4.区域信息解析器自动配置
我们知道,Spring MVC 进行国际化时有 2 个十分重要的对象:
- Locale:区域信息对象
- LocaleResolver:区域信息解析器,容器中的组件,负责获取区域信息对象
我们可以通过以上两个对象对区域信息的切换,以达到切换语言的目的。
Spring Boot 在 WebMvcAutoConfiguration 中为区域信息解析器(LocaleResolver)进行了自动配置,源码如下。
@Bean
@ConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)
@SuppressWarnings("deprecation")
public LocaleResolver localeResolver() {
if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.webProperties.getLocale());
}
if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
Locale locale = (this.webProperties.getLocale() != null) ? this.webProperties.getLocale()
: this.mvcProperties.getLocale();
localeResolver.setDefaultLocale(locale);
return localeResolver;
}
从以上源码可知:
- 该方法默认向容器中添加了一个区域信息解析器(LocaleResolver)组件,它会根据请求头中携带的“Accept-Language”参数,获取相应区域信息(Locale)对象。
- 该方法上使用了 @ConditionalOnMissingBean 注解,其参数 name 的取值为 localeResolver(与该方法注入到容器中的组件名称一致),该注解的含义为:当容器中不存在名称为 localResolver 组件时,该方法才会生效。换句话说,当我们手动向容器中添加一个名为“localeResolver”的组件时,Spring Boot 自动配置的区域信息解析器会失效,而我们定义的区域信息解析器则会生效。
手动切换语言
- 修改 login.html 切换语言链接,在请求中携带国际化区域信息,代码如下。
<!--thymeleaf 模板引擎的参数用()代替 ?-->
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>|
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
- 创建一个 component 包,并在该包中创建一个区域信息解析器 MyLocalResolver,代码如下。
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;
//自定义区域信息解析器
public class MyLocalResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
//获取请求中参数
String l = request.getParameter("l");
//获取默认的区域信息解析器
Locale locale = Locale.getDefault();
//根据请求中的参数重新构造区域信息对象
if (StringUtils.hasText(l)) {
String[] s = l.split("_");
locale = new Locale(s[0], s[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
}
为了让我们的区域化信息能够生效,我们需要再配置一下这个组件!在我们自己的MvcConofig
下添加bean
;
@Bean
public LocaleResolver localeResolver(){
return new MyLocalResolver();
}
15.swagger
15.1.添加依赖
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
15.2.添加配置类
添加一个swagger 配置类,在工程下新建 config 包并添加一个 SwaggerConfig 配置类。
SwaggerConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any()).build();
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("Kitty API Doc")
.description("This is a restful api document of Kitty.")
.version("1.0")
.build();
}
}
15.3.添加控制器
添加一个控制器,在工程下新建 controller包并添加一个 HelloController控制器。
HelloController.java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
/* 类注解 */
@Api(value = "desc of class")
@RestController
public class HelloController {
/* 方法注解 */
@ApiOperation(value = "desc of method", notes = "")
@GetMapping(value="/hello")
public Object hello( /* 参数注解 */ @ApiParam(value = "desc of param" , required=true ) @RequestParam String name) {
return "Hello " + name + "!";
}
}
运行即可
15.4.常用注解说明
swagger 通过注解接口生成文档,包括接口名,请求方法,参数,返回信息等。
@Api: 修饰整个类,用于controller类上
@ApiOperation: 描述一个接口,用户controller方法上
@ApiParam: 单个参数描述
@ApiModel: 用来对象接收参数,即返回对象
@ApiModelProperty: 对象接收参数时,描述对象的字段
@ApiResponse: Http响应其中的描述,在ApiResonse中
@ApiResponses: Http响应所有的描述,用在
@ApiIgnore: 忽略这个API
@ApiError: 发生错误的返回信息
@ApiImplicitParam: 一个请求参数
@ApiImplicitParam: 多个请求参数
更多使用说明,参考 Swagger 使用手册。
15.5.添加请求参数
在很多时候,我们需要在调用我们每一个接口的时候都携带上一些通用参数,比如采取token验证逻辑的往往在接口请求时需要把token也一起传入后台,接下来,我们就来讲解一下如何给Swagger添加固定的请求参数。
修改SwaggerConfig配置类,替换成如下内容,利用ParameterBuilder构成请求参数。
SwaggerConfig.java
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi(){
// 添加请求参数,我们这里把token作为请求头部参数传入后端
ParameterBuilder parameterBuilder = new ParameterBuilder();
List<Parameter> parameters = new ArrayList<Parameter>();
parameterBuilder.name("token").description("令牌")
.modelRef(new ModelRef("string")).parameterType("header").required(false).build();
parameters.add(parameterBuilder.build());
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
.apis(RequestHandlerSelectors.any()).paths(PathSelectors.any())
.build().globalOperationParameters(parameters);
// return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
// .select()
// .apis(RequestHandlerSelectors.any())
// .paths(PathSelectors.any()).build();
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("Swagger API Doc")
.description("This is a restful api document of Swagger.")
.version("1.0")
.build();
}
}
完成之后重新启动应用,再次查看hello接口,可以看到已经支持发送token请求参数了。
15.6.配置API分组
如果没有配置分组,默认是default。通过groupName()方法即可配置分组
@Bean
public Docket docket(Environment environment) {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.groupName("group1") // 配置分组
....
}
如何配置多个分组?配置多个分组只需要配置多个docket即可:
@Bean
public Docket docket1(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
}
@Bean
public Docket docket2(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
}
@Bean
public Docket docket3(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group3");
}
16.拦截器
在 Spring Boot 项目中,使用拦截器功能通常需要以下 3 步:
-
定义拦截器;
-
注册拦截器;
-
指定拦截规则(如果是拦截所有,静态资源也会被拦截)。
16.1.定义拦截器
在 Spring Boot 中定义拦截器十分的简单,只需要创建一个拦截器类,并实现 HandlerInterceptor 接口即可。
HandlerInterceptor 接口中定义以下 3 个方法,如下表。
返回值类型 | 方法声明 | 描述 |
---|---|---|
boolean | preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) | 该方法在控制器处理请求方法前执行,其返回值表示是否中断后续操作,返回 true 表示继续向下执行,返回 false 表示中断后续操作。 |
void | postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) | 该方法在控制器处理请求方法调用之后、解析视图之前执行,可以通过此方法对请求域中的模型和视图做进一步修改。 |
void | afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) | 该方法在视图渲染结束后执行,可以通过此方法实现资源清理、记录日志信息等工作。 |
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行前
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object loginUser = request.getSession().getAttribute("loginUser");
if (loginUser == null) {
//未登录,返回登陆页
request.setAttribute("msg", "您没有权限进行此操作,请先登陆!");
request.getRequestDispatcher("/index.html").forward(request, response);
return false;
} else {
//放行
return true;
}
}
/**
* 目标方法执行后
*
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}", modelAndView);
}
/**
* 页面渲染后
*
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}", ex);
}
}
16.2.注册拦截器
创建一个实现了 WebMvcConfigurer 接口的配置类(使用了 @Configuration 注解的类),重写 addInterceptors() 方法,并在该方法中调用 registry.addInterceptor() 方法将自定义的拦截器注册到容器中。
在配置类 MyMvcConfig 中,添加以下方法注册拦截器,代码如下。
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
......
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor());
}
}
16.3.指定拦截规则
在使用 registry.addInterceptor() 方法将拦截器注册到容器中后,我们便可以继续指定拦截器的拦截规则了,代码如下
@Slf4j
@Configuration
public class MyConfig implements WebMvcConfigurer {
......
@Override
public void addInterceptors(InterceptorRegistry registry) {
log.info("注册拦截器");
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") //拦截所有请求,包括静态资源文件
.excludePathPatterns("/", "/login", "/index.html", "/user/login", "/css/**", "/images/**", "/js/**", "/fonts/**"); //放行登录页,登陆操作,静态资源
}
}
在指定拦截器拦截规则时,调用了两个方法,这两个方法的说明如下:
- addPathPatterns:该方法用于指定拦截路径,例如拦截路径为“/**”,表示拦截所有请求,包括对静态资源的请求。
- excludePathPatterns:该方法用于排除拦截路径,即指定不需要被拦截器拦截的请求。
至此,拦截器的基本功能已经完成,接下来,我们先实现 spring-boot-adminex 的登陆功能,为验证登陆拦截做准备。
17.异常处理
Spring Boot 提供了一套默认的异常处理机制,一旦程序中出现了异常,Spring Boot 会自动识别客户端的类型(浏览器客户端或机器客户端),并根据客户端的不同,以不同的形式展示异常信息。
- 对于浏览器客户端而言,Spring Boot 会响应一个“ whitelabel”错误视图,以 HTML 格式呈现错误信息
- 对于机器客户端而言,Spring Boot 将生成 JSON 响应,来展示异常消息。
{
"timestamp": "2021-07-12T07:05:29.885+00:00",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/m1ain.html"
}
Spring Boot 异常处理自动配置原理
Spring Boot 通过配置类 ErrorMvcAutoConfiguration 对异常处理提供了自动配置,该配置类向容器中注入了以下 4 个组件。
-
ErrorPageCustomizer:该组件会在在系统发生异常后,默认将请求转发到“/error”上。
-
BasicErrorController:处理默认的“/error”请求。
-
DefaultErrorViewResolver:默认的错误视图解析器,将异常信息解析到相应的错误视图上。
-
DefaultErrorAttributes:用于页面上共享异常信息。
下面,我们依次对这四个组件进行详细的介绍。
ErrorPageCustomizer
ErrorMvcAutoConfiguration 向容器中注入了一个名为 ErrorPageCustomizer 的组件,它主要用于定制错误页面的响应规则。
@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
ErrorPageCustomizer 通过 registerErrorPages() 方法来注册错误页面的响应规则。当系统中发生异常后,ErrorPageCustomizer 组件会自动生效,并将请求转发到 “/error”上,交给 BasicErrorController 进行处理,其部分代码如下。
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
//将请求转发到 /errror(this.properties.getError().getPath())上
ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
// 注册错误页面
errorPageRegistry.addErrorPages(errorPage);
}
BasicErrorController
ErrorMvcAutoConfiguration 还向容器中注入了一个错误控制器组件 BasicErrorController,代码如下。
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
BasicErrorController 的定义如下。
//BasicErrorController 用于处理 “/error” 请求
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
......
/**
* 该方法用于处理浏览器客户端的请求发生的异常
* 生成 html 页面来展示异常信息
* @param request
* @param response
* @return
*/
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
//获取错误状态码
HttpStatus status = getStatus(request);
//getErrorAttributes 根据错误信息来封装一些 model 数据,用于页面显示
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
//为响应对象设置错误状态码
response.setStatus(status.value());
//调用 resolveErrorView() 方法,使用错误视图解析器生成 ModelAndView 对象(包含错误页面地址和页面内容)
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
/**
* 该方法用于处理机器客户端的请求发生的错误
* 产生 JSON 格式的数据展示错误信息
* @param request
* @return
*/
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
......
}
Spring Boot 通过 BasicErrorController 进行统一的错误处理(例如默认的“/error”请求)。Spring Boot 会自动识别发出请求的客户端的类型(浏览器客户端或机器客户端),并根据客户端类型,将请求分别交给 errorHtml() 和 error() 方法进行处理。
返回值类型 | 方法声明 | 客户端类型 | 错误信息返类型 |
---|---|---|---|
ModelAndView | errorHtml(HttpServletRequest request, HttpServletResponse response) | 浏览器客户端 | text/html(错误页面) |
ResponseEntity<Map<String, Object>> | error(HttpServletRequest request) | 机器客户端(例如安卓、IOS、Postman 等等) | JSON |
换句话说,当使用浏览器访问出现异常时,会进入 BasicErrorController 控制器中的 errorHtml() 方法进行处理,当使用安卓、IOS、Postman 等机器客户端访问出现异常时,就进入error() 方法处理。
在 errorHtml() 方法中会调用父类(AbstractErrorController)的 resolveErrorView() 方法,代码如下。
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
//获取容器中的所有的错误视图解析器来处理该异常信息
for (ErrorViewResolver resolver : this.errorViewResolvers) {
//调用错误视图解析器的 resolveErrorView 解析到错误视图页面
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
从上述源码可以看出,在响应页面的时候,会在父类的 resolveErrorView 方法中获取容器中所有的 ErrorViewResolver 对象(错误视图解析器,包括 DefaultErrorViewResolver 在内),一起来解析异常信息。
DefaultErrorViewResolver
ErrorMvcAutoConfiguration 还向容器中注入了一个默认的错误视图解析器组件 DefaultErrorViewResolver,代码如下。
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}
当发出请求的客户端为浏览器时,Spring Boot 会获取容器中所有的 ErrorViewResolver 对象(错误视图解析器),并分别调用它们的 resolveErrorView() 方法对异常信息进行解析,其中自然也包括 DefaultErrorViewResolver(默认错误信息解析器)。
DefaultErrorViewResolver 的部分代码如下。
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<HttpStatus.Series, String> SERIES_VIEWS;
static {
Map<HttpStatus.Series, String> views = new EnumMap<>(HttpStatus.Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
......
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
//尝试以错误状态码作为错误页面名进行解析
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
//尝试以 4xx 或 5xx 作为错误页面页面进行解析
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//错误模板页面,例如 error/404、error/4xx、error/500、error/5xx
String errorViewName = "error/" + viewName;
//当模板引擎可以解析这些模板页面时,就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
//在模板能够解析到模板页面的情况下,返回 errorViewName 指定的视图
return new ModelAndView(errorViewName, model);
}
//若模板引擎不能解析,则去静态资源文件夹下查找 errorViewName 对应的页面
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//遍历所有静态资源文件夹
for (String location : this.resources.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
//静态资源文件夹下的错误页面,例如error/404.html、error/4xx.html、error/500.html、error/5xx.html
resource = resource.createRelative(viewName + ".html");
//若静态资源文件夹下存在以上错误页面,则直接返回
if (resource.exists()) {
return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
}
} catch (Exception ex) {
}
}
return null;
}
......
}
DefaultErrorViewResolver 解析异常信息的步骤如下:
- 根据错误状态码(例如 404、500、400 等),生成一个错误视图 error/status,例如 error/404、error/500、error/400。
- 尝试使用模板引擎解析 error/status 视图,即尝试从 classpath 类路径下的 templates 目录下,查找 error/status.html,例如 error/404.html、error/500.html、error/400.html。
- 若模板引擎能够解析到 error/status 视图,则将视图和数据封装成 ModelAndView 返回并结束整个解析流程,否则跳转到第 4 步。
- 依次从各个静态资源文件夹中查找 error/status.html,若在静态文件夹中找到了该错误页面,则返回并结束整个解析流程,否则跳转到第 5 步。
- 将错误状态码(例如 404、500、400 等)转换为 4xx 或 5xx,然后重复前 4 个步骤,若解析成功则返回并结束整个解析流程,否则跳转第 6 步。
- 处理默认的 “/error ”请求,使用 Spring Boot 默认的错误页面(Whitelabel Error Page)。
DefaultErrorAttributes
ErrorMvcAutoConfiguration 还向容器中注入了一个组件默认错误属性处理工具 DefaultErrorAttributes,代码如下。
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
DefaultErrorAttributes 是 Spring Boot 的默认错误属性处理工具,它可以从请求中获取异常或错误信息,并将其封装为一个 Map 对象返回,其部分代码如下。
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
......
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
if (!options.isIncluded(Include.EXCEPTION)) {
errorAttributes.remove("exception");
}
if (!options.isIncluded(Include.STACK_TRACE)) {
errorAttributes.remove("trace");
}
if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
errorAttributes.remove("message");
}
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
return errorAttributes;
}
private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}
......
}
在 Spring Boot 默认的 Error 控制器(BasicErrorController)处理错误时,会调用 DefaultErrorAttributes 的 getErrorAttributes() 方法获取错误或异常信息,并封装成 model 数据(Map 对象),返回到页面或 JSON 数据中。该 model 数据主要包含以下属性:
- timestamp:时间戳;
- status:错误状态码
- error:错误的提示
- exception:导致请求处理失败的异常对象
- message:错误/异常消息
- trace: 错误/异常栈信息
- path:错误/异常抛出时所请求的URL路径
18.全局异常处理
我们知道 Spring Boot 已经提供了一套默认的异常处理机制,但是 Spring Boot 提供的默认异常处理机制却并不一定适合我们实际的业务场景,因此,我们通常会根据自身的需要对 Spring Boot 全局异常进行统一定制,例如定制错误页面,定制错误数据等。
定制错误页面
我们可以通过以下 3 种方式定制 Spring Boot 错误页面:
- 自定义 error.html
- 自定义动态错误页面
- 自定义静态错误页面
自定义 error.html
我们可以直接在模板引擎文件夹(/resources/templates)下创建 error.html ,覆盖 Spring Boot 默认的错误视图页面(Whitelabel Error Page)。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>自定义 error.html</title>
</head>
<body>
<h1>自定义 error.html</h1>
<p>status:<span th:text="${status}"></span></p>
<p>error:<span th:text="${error}"></span></p>
<p>timestamp:<span th:text="${timestamp}"></span></p>
<p>message:<span th:text="${message}"></span></p>
<p>path:<span th:text="${path}"></span></p>
</body>
</html>
如果 Sprng Boot 项目使用了模板引擎,当程序发生异常时,Spring Boot 的默认错误视图解析器(DefaultErrorViewResolver)就会解析模板引擎文件夹(resources/templates/)下 error 目录中的错误视图页面。
精确匹配
我们可以根据错误状态码(例如 404、500、400 等等)的不同,分别创建不同的动态错误页面(例如 404.html、500.html、400.html 等等),并将它们存放在模板引擎文件夹下的 error 目录中。当发生异常时,Spring Boot 会根据其错误状态码精确匹配到对应的错误页面上。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>自定义动态错误页面 404.html</h1>
<p>status:<span th:text="${status}"></span></p>
<p>error:<span th:text="${error}"></span></p>
<p>timestamp:<span th:text="${timestamp}"></span></p>
<p>message:<span th:text="${message}"></span></p>
<p>path:<span th:text="${path}"></span></p>
</body>
</html>
匹配
我们还可以使用 4xx.html 和 5xx.html 作为动态错误页面的文件名,并将它们存放在模板引擎文件夹下的 error 目录中,来模糊匹配对应类型的所有错误,例如 404、400 等错误状态码以“4”开头的所有异常,都会解析到动态错误页面 4xx.html 上。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>自定义动态错误页面 4xx.html</h1>
<p>status:<span th:text="${status}"></span></p>
<p>error:<span th:text="${error}"></span></p>
<p>timestamp:<span th:text="${timestamp}"></span></p>
<p>message:<span th:text="${message}"></span></p>
<p>path:<span th:text="${path}"></span></p>
</body>
</html>
自定义静态错误页面
若 Sprng Boot 项目没有使用模板引擎,当程序发生异常时,Spring Boot 的默认错误视图解析器(DefaultErrorViewResolver)则会解析静态资源文件夹下 error 目录中的静态错误页面。
精确匹配
我们可以根据错误状态码(例如 404、500、400 等等)的不同,分别创建不同的静态错误页面(例如 404.html、500.html、400.html 等等),并将它们存放在静态资源文件夹下的 error 目录中。当发生异常时,Spring Boot 会根据错误状态码精确匹配到对应的错误页面上。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>自定义静态错误页面 404.html</h1>
<p>status:<span th:text="${status}"></span></p>
<p>error:<span th:text="${error}"></span></p>
<p>timestamp:<span th:text="${timestamp}"></span></p>
<p>message:<span th:text="${message}"></span></p>
<p>path:<span th:text="${path}"></span></p>
</body>
</html>
模糊匹配
我们还可以使用 4xx.html 和 5xx.html 作为静态错误页面的文件名,并将它们存放在静态资源文件夹下的 error 目录中,来模糊匹配对应类型的所有错误,例如 404、400 等错误状态码以“4”开头的所有错误,都会解析到静态错误页面 4xx.html 上。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>自定义静态错误页面 4xx.html</h1>
<p>status:<span th:text="${status}"></span></p>
<p>error:<span th:text="${error}"></span></p>
<p>timestamp:<span th:text="${timestamp}"></span></p>
<p>message:<span th:text="${message}"></span></p>
<p>path:<span th:text="${path}"></span></p>
</body>
</html>
错误页面优先级
以上 5 种方式均可以定制 Spring Boot 错误页面,且它们的优先级顺序为:自定义动态错误页面(精确匹配)>自定义静态错误页面(精确匹配)>自定义动态错误页面(模糊匹配)>自定义静态错误页面(模糊匹配)>自定义 error.html。
当遇到错误时,Spring Boot 会按照优先级由高到低,依次查找解析错误页,一旦找到可用的错误页面,则直接返回客户端展示。
定制错误数据
我们知道,Spring Boot 提供了一套默认的异常处理机制,其主要流程如下:
- 发生异常时,将请求转发到“/error”,交由 BasicErrorController(Spring Boot 默认的 Error 控制器) 进行处理;
- BasicErrorController 根据客户端的不同,自动适配返回的响应形式,浏览器客户端返回错误页面,机器客户端返回 JSON 数据。
- BasicErrorController 处理异常时,会调用 DefaultErrorAttributes(默认的错误属性处理工具) 的 getErrorAttributes() 方法获取错误数据。
我们还可以定制 Spring Boot 的错误数据,具体步骤如下。
- 自定义异常处理类,将请求转发到 “/error”,交由 Spring Boot 底层(BasicErrorController)进行处理,自动适配浏览器客户端和机器客户端。
- 通过继承 DefaultErrorAttributes 来定义一个错误属性处理工具,并在原来的基础上添加自定义的错误数据。
1. 自定义异常处理类
被 @ControllerAdvice 注解的类可以用来实现全局异常处理,这是 Spring MVC 中提供的功能,在 Spring Boot 中可以直接使用。
创建一个名为 UserNotExistException 的异常类,代码如下
/**
* 自定义异常
*/
public class UserNotExistException extends RuntimeException {
public UserNotExistException() {
super("用户不存在!");
}
}
在 IndexController 添加以下方法,触发 UserNotExistException 异常,代码如下
@Controller
public class IndexController {
......
@GetMapping(value = {"/testException"})
public String testException(String user) {
if ("user".equals(user)) {
throw new UserNotExistException();
}
//跳转到登录页 login.html
return "login";
}
}
创建一个名为 MyExceptionHandler 异常处理类,代码如下
import com.wyl.Exception.UserNotExistException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
//向 request 对象传入错误状态码
request.setAttribute("javax.servlet.error.status_code",500);
//根据当前处理的异常,自定义的错误数据
map.put("code", "user.notexist");
map.put("message", e.getMessage());
//将自定的错误数据传入 request 域中
request.setAttribute("ext",map);
return "forward:/error";
}
}
2. 自定义错误属性处理工具
1)在 net.biancheng.www.componet 包内,创建一个错误属性处理工具类 MyErrorAttributes(继承 DefaultErrorAttributes ),通过该类我们便可以添加自定义的错误数据,代码如下。
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.util.Map;
//向容器中添加自定义的储物属性处理工具
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
//添加自定义的错误数据
errorAttributes.put("company", "www.biancheng.net");
//获取 MyExceptionHandler 传入 request 域中的错误数据
Map ext = (Map) webRequest.getAttribute("ext", 0);
errorAttributes.put("ext", ext);
return errorAttributes;
}
}
在 templates/error 目录下,创建动态错误页面 5xx.html,代码如下。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>自定义 error.html</title>
</head>
<body>
<p>status:<span th:text="${status}"></span></p>
<p>error:<span th:text="${error}"></span></p>
<p>timestamp:<span th:text="${timestamp}"></span></p>
<p>message:<span th:text="${message}"></span></p>
<p>path:<span th:text="${path}"></span></p>
<!--取出定制的错误信息-->
<h3>以下为定制错误数据:</h3>
<p>company:<span th:text="${company}"></span></p>
<p>code:<span th:text="${ext.code}"></span></p>
<p>path:<span th:text="${ext.message}"></span></p>
</body>
</html>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
2016-11-09 access基本操作(c#操作,远程连接,执行sql,加密,备份)