SpringBoot2 核心
- 一、配置文件
-
- 1、文件类型
-
- 1.1、properties
- 1.2、yaml
-
- 1、简介:
- 2、基本语法:
- 3、数据类型:
- 4、示例:
- 2、配置提示
- 二、Web开发
-
- 1、SpringMVC自动配置概览
- 2、简单功能分析
-
- 2.1、静态资源访问
-
- 1、静态资源目录:
- 2、静态资源访问前缀:
- 3、webjar
- 2.2、欢迎页支持
- 2.3、自定义 Favicon
- 2.4、静态资源配置原理
-
- 1、配置类只有一个有参构造器
- 2、资源处理的默认规则
- 3、欢迎页的处理规则
- 4、favicon
- 3、请求参数处理
-
- 3.1、请求映射
-
- 1、rest使用与原理
- 2、请求映射原理
- 3.2、普通参数与基本注解
-
- 1、注解:
- 2、Servlet API:
- 3、复杂参数:
- 4、自定义对象参数:
- 3.3、POJO封装过程
- 3.4、参数处理原理
-
- 1、HandlerAdapter
- 2、执行目标方法
- 3、参数解析器-HandlerMethodArgumentResolver
- 4、返回值处理器
- 5、如何确定目标方法每一个参数的值
-
- 5.1、挨个判断所有参数解析器那个支持解析这个参数
- 5.2、解析这个参数的值
- 5.3、自定义类型参数 封装POJO
- 6、目标方法执行完成
- 7、处理派发结果
- 4、数据响应与内容协商
-
- 4.1、响应JSON
-
- 1、[email protected]
-
- 返回值解析器
- 返回值解析器原理
- 2、SpringMVC到底支持哪些返回值
- 3、HTTPMessageConverter原理
-
- 1、MessageConverter规范
- 2、默认的MessageConverter
- 4.2、内容协商
-
- 1、引入xml依赖
- 2、postman分别测试返回json和xml
- 3、开启浏览器参数方式内容协商功能
- 4、内容协商原理
- 5、自定义 MessageConverter
- 5、视图解析与模板引擎
-
- 5.1、视图解析
-
- 1、视图解析原理流程
- 5.2、模板引擎-Thymeleaf
-
- 1、thymeleaf简介
- 2、基本语法
-
- 1、表达式:
- 2、字面量:
- 3、文本操作:
- 4、数学运算:
- 5、布尔运算:
- 6、比较运算:
- 7、条件运算:
- 8、特殊操作:
- 3、设置属性值-th:attr
- 4、迭代
- 5、条件运算
- 6、属性优先级
- 5.3、thymeleaf使用
-
- 1、引入Starter
- 2、自动配置好了thymeleaf
- 3、页面开发
- 5.4、构建后台管理系统
-
- 1、项目创建
- 2、静态资源处理
- 3、路径构建
- 4、模板抽取
- 5、页面跳转
- 6、数据渲染
- 6、拦截器
-
- 6.1、HandlerInterceptor 接口
- 6.2、配置拦截器
- 6.3、拦截器原理
- 7、文件上传
-
- 7.1、页面表单
- 7.2、文件上传代码
- 7.3、自动配置原理
- 8、异常处理
-
- 8.1、错误处理
-
- 1、默认规则
- 2、定制错误处理逻辑
- 3、异常处理自动配置原理
- 4、异常处理步骤流程
- 9、Web原生组件注入(Servlet、Filter、Listener)
-
- 9.1、使用Servlet API
- 9.2、使用RegistrationBean
- 10、嵌入式Servlet容器
-
- 10.1、切换嵌入式Servlet容器
- 10.2、定制Servlet容器
- 11、定制化原理
-
- 11.1、定制化的常见方式
- 11.2、原理分析套路
- 三、数据访问
-
- 1、SQL
-
- 1.1、数据源的自动配置-HikariDataSource
-
- 1、导入JDBC场景
- 2、分析自动配置
-
- 2.1、自动配置的类
- 3、修改配置项
- 4、测试
- 1.2、使用Druid数据源
-
- 1、druid官方github地址
- 2、自定义方式
-
- 2.1、创建数据源
- 2.2、StatViewServlet
- 2.3、StatFilter
- 3、使用官方starter方式
-
- 3.1、引入druid-starter
- 3.2、分析自动配置
- 3.3、配置示例
- 1.3、整合MyBatis操作
-
- 1、配置模式
- 2、注解模式
- 3、混合模式
- 1.4、整合 MyBatis-Plus 完成CRUD
-
- 1、什么是MyBatis-Plus
- 2、整合MyBatis-Plus
- 3、CRUD功能
- 2、NoSQL
-
- 2.1、Redis自动配置
- 2.2、RedisTemplate与Lettuce
- 2.3、切换至jedis
- 四、单元测试
-
- 1、JUnit5 的变化
- 2、JUnit5常用注解
- 3、断言(assertions)
-
- 3.1、简单断言
- 3.2、数组断言
- 3.3、组合断言
- 3.4、异常断言
- 3.5、超时断言
- 3.6、快速失败
- 4、前置条件(assumptions)
- 5、嵌套测试
- 6、参数化测试
- 7、迁移指南
- 五、指标监控
-
- 1、SpringBoot Actuator
-
- 1.1、简介
- 1.2、1.x与2.x的不同
- 1.3、如何使用
- 1.4、可视化
- 2、Actuator Endpoint
-
- 2.1、最常使用的端点
- 2.2、Health Endpoint
- 2.3、Metrics Endpoint
- 2.4、管理Endpoints
-
- 1、开启与禁用Endpoints
- 2、暴露Endpoints
- 3、定制 Endpoint
-
- 3.1、定制 Health 信息
- 3.2、定制info信息
-
- 1、编写配置文件
- 2、编写InfoContributor
- 3.3、定制Metrics信息
-
- 1、SpringBoot支持自动适配的Metrics
- 2、增加定制Metrics
- 3.4、定制Endpoint
- 六、原理解析
-
- 1、Profile功能
-
- 1.1、application-profile功能
- 1.2、@Profile条件装配功能
- 1.3、profile分组
- 2、外部化配置
-
- 2.1、外部配置源
- 2.2、配置文件查找位置
- 2.3、配置文件加载顺序:
- 2.3、指定环境优先,外部优先,后面的可以覆盖前面的同名配置项
- 3、自定义starter
-
- 3.1、starter启动原理
- 3.2、自定义starter
- 4、SpringBoot原理
-
- 4.1、SpringBoot启动过程
- 4.2、Application Events and Listeners
- 4.3、ApplicationRunner 与 CommandLineRunner
连接视频
文档地址
一、配置文件
1、文件类型
1.1、properties
同以前的properties用法
1.2、yaml
1、简介:
YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。
非常适合用来做以数据为中心的配置文件
2、基本语法:
- key: value;kv之间有空格
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
- '#'表示注释
- 字符串无需加引号,如果要加,’'与""表示字符串内容 会被 转义/不转义
3、数据类型:
- 字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
- 对象:键值对的集合。map、hash、set、object
行内写法: k: {k1:v1,k2:v2,k3:v3}
#或
k:
k1: v1
k2: v2
k3: v3
- 数组:一组按次序排列的值。array、list、queue
行内写法: k: [v1,v2,v3]
#或者
k:
- v1
- v2
- v3
4、示例:
对象
@Data
@ToString
@ConfigurationProperties(prefix = "person")
@Component
public class Person {
private String name;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] strs;
private List<String> list;
private Map<String, Object> map;
private Set<Double> set;
private Map<String, List<Pet>> pets;
}
@Data
public class Pet {
private String name;
private Double weight;
}
application.yml配置文件:yaml表示以上对象:
person:
name: "zhansan \n lala"
# 单引号会将 \n 作为字符串输出 双引号会将 \n 作为换行输出
# 双引号不会转义 双引号会转义
boss: true
birth: 2019/12/9
age: 30
# strs: [游戏,跑步]
strs:
- 游戏
- 跑步
- 22
list: [小猫,小狗]
# map:
# math: 80
# english: 90
map: {math: 80,english: 90}
set:
- 99.9
- 99.2
pet:
name: 小猫
weight: 8.8
pets:
sick:
- {name: 小狗,weight: 10.9}
- name: 小猫
weight: 5.5
- name: 小虫
weight: 1.5
health:
- {name: 小花,weight: 6.9}
- {name: 小池,weight: 16.9}
controller层:
@Slf4j
@RestController
public class HelloController {
@Autowired
private Person person;
@RequestMapping("/person")
public Person person(){
log.info("person={}",person);
return person;
}
}
启动访问:
localhost:8080/person
2、配置提示
查看手册:
自定义的类和配置文件绑定一般没有提示。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<proc>none</proc>
</configuration>
</plugin>
</plugins>
</build>
二、Web开发
1、SpringMVC自动配置概览
参照官网文档
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)
The auto-configuration adds the following features on top of Spring’s defaults:
- Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans…
- 内容协商视图解析器和BeanName视图解析器
- Support for serving static resources, including support for WebJars (covered later in this document )
- 静态资源(包括webjars)
- Automatic registration of Converter, GenericConverter, and Formatter beans.
- 自动注册 Converter,GenericConverter,Formatter
- Support for HttpMessageConverters (covered later in this document )
- 支持 HttpMessageConverters (后来我们配合内容协商理解原理)
- Automatic registration of MessageCodesResolver (covered later in this document )
- 自动注册 MessageCodesResolver (国际化用)
- Static index.html support.
- 静态index.html 页支持
- Custom Favicon support (covered later in this document )
- 自定义 Favicon
- Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document )
- 自动使用 ConfigurableWebBindingInitializer,(DataBinder负责将请求数据绑定到JavaBean上)
If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own
@Configuration
class of type WebMvcConfigurer but without
@EnableWebMvc
.
不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则
如果您想保留这些Spring-Boot MVC定制并进行更多MVC定制(拦截器、格式化程序、视图控制器和其他特性),您可以添加自己的WebMvcConfigurer类型的@Configuration类,但不添加@EnableWebMvc。
If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.
声明 WebMvcRegistrations 改变默认底层组件
如果要提供RequestMappingHandlerMapping、RequestMappingHandlerAdapter或ExceptionHandlerExceptionResolver的自定义实例,并且仍然保留Spring Boot MVC自定义设置,可以声明WebMvcRegistrations类型的bean,并使用它来提供这些组件的自定义实例。
If you want to take complete control of Spring MVC, you can add your own
@Configuration
annotated with
@EnableWebMvc
, or alternatively add your own
@Configuration-annotated
DelegatingWebMvcConfiguration as described in the Javadoc of
@EnableWebMvc
.
使用 @[email protected]+DelegatingWebMvcConfiguration 全面接管SpringMVC
如果您想完全控制springmvc,您可以添加自己的@Configuration,并用@EnableWebMvc注释,或者也可以添加自己的@Configuration annotated delegatingwebmvc配置,如@EnableWebMvc的Javadoc中所述。
Note:
Spring MVC uses a different ConversionService to the one used to convert values from your application.properties or application.yaml file. It means that Period, Duration and DataSize converters are not available and that @DurationUnit and @DataSizeUnit annotations will be ignored.
If you want to customize the ConversionService used by Spring MVC, you can provide a WebMvcConfigurer bean with an addFormatters method. From this method you can register any converter that you like, or you can delegate to the static methods available on ApplicationConversionService.
SpringMVC使用不同的转换服务来转换application.properties或application.yaml文件中的值。这意味着Period、Duration和DataSize转换器不可用,@DurationUnit和@DataSizeUnit注释将被忽略。
如果您想定制springmvc使用的ConversionService,可以提供一个带有addFormatters方法的webmvcconfigurerbean。通过此方法,您可以注册任何您喜欢的转换器,也可以委托给ApplicationConversionService上可用的静态方法。
2、简单功能分析
创建boot-01-web-01 springboot项目
引用
spring-boot-starter-web
2.1、静态资源访问
文档
1、静态资源目录:
只要静态资源放在类路径下: called /static (or /public or /resources or /META-INF/resources
访问 : 当前项目根路径/ + 静态资源名
测试:在resources目录下放入四张图片
启动:访问
原理: 静态映射 / 。
请求进来,先去找 Controller 看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面
测试:
@RestController
public class HelloController {
@RequestMapping("/45.jpg")
public String hello(){
return "aaa";
}
}
访问:
http://localhost:8080/45.jpg,这时候是响应 aaa,不是一张图片
访问不存在的静态资源地址:404
改变默认的静态资源路径
spring:
resources:
static-locations: [classpath:/haha/]
添加haha目录
重启测试:
2、静态资源访问前缀:
application.yml文件添加
spring:
mvc:
static-path-pattern: /res/**
重启,测试;
当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找
3、webjar
自动映射 /webjars/**
https://www.webjars.org/
引入jquery依赖
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
重启:
访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径
2.2、欢迎页支持
- 静态资源路径下 index.html
- 可以配置静态资源路径
- 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效
resources:
static-locations: [classpath:/haha/]
测试:创建html文件:
启动。访问:http://localhost:8080/
- controller能处理/index
2.3、自定义 Favicon
favicon.ico 放在静态资源目录下即可。(小图标)
重启,访问:http://localhost:8080/
2.4、静态资源配置原理
- SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
- SpringMVC功能的自动配置类
,生效WebMvcAutoConfiguration
package org.springframework.boot.autoconfigure.web.servlet;
....
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {...}
- 给容器中配了什么。
WebMvcAutoConfigurationAdapter
package org.springframework.boot.autoconfigure.web.servlet;
....
public class WebMvcAutoConfiguration {
....
@Configuration(
proxyBeanMethods = false
)
@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class, WebProperties.class})
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {...}
....
}
- 配置文件(
)的相关属性和xxx进行了绑定。@EnableConfigurationProperties
==WebMvcProperties
、spring.mvc
==ResourceProperties
和spring.resources
==WebProperties
spring.web
1、配置类只有一个有参构造器
有参构造器所有参数的值都会从容器中确定
①
ResourceProperties resourceProperties
;获取和
spring.resources
绑定的所有的值的对象
②
WebProperties webProperties
;获取和
spring.web
绑定的所有的值的对象
③
WebMvcProperties mvcProperties
; 获取和
spring.mvc
绑定的所有的值的对象
④
ListableBeanFactory beanFactory Spring
的
beanFactory
⑤
HttpMessageConverters
找到所有的
HttpMessageConverters
⑥
ResourceHandlerRegistrationCustomizer
找到 资源处理器的自定义器
⑦
DispatcherServletPath
处理路径
⑧
ServletRegistrationBean
给应用注册Servlet、Filter…
package org.springframework.boot.autoconfigure.web.servlet;
....
public class WebMvcAutoConfiguration {
....
@Configuration(
proxyBeanMethods = false
)
@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class, WebProperties.class})
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
...
// 有参构造
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties,
WebProperties webProperties,
WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory,
ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = (Resources)(resourceProperties.hasBeenCustomized() ? resourceProperties : webProperties.getResources());
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = (WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}
}
....
}
2、资源处理的默认规则
package org.springframework.boot.autoconfigure.web.servlet;
....
public class WebMvcAutoConfiguration {
....
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
...
// 有参构造
public WebMvcAutoConfigurationAdapter(...}
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
registration.addResourceLocations(new Resource[]{resource});
}
});
}
}
}
....
}
禁用所有静态资源规则
spring.resources.add-mappings: false
spring:
resources:
add-mappings: false # 禁用所有静态资源规则
重启,测试:
this.resourceProperties.getStaticLocations()
默认读取静态文件路径
package org.springframework.boot.autoconfigure.web;
...
@ConfigurationProperties("spring.web")
public class WebProperties {
....
public static class Resources {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
private String[] staticLocations;
....
}
3、欢迎页的处理规则
WelcomePageHandlerMapping welcomePageHandlerMapping()
HandlerMapping
:处理器映射。保存了每一个Handler能处理哪些请求。
package org.springframework.boot.autoconfigure.web.servlet;
....
public class WebMvcAutoConfiguration {
....
@EnableConfigurationProperties({WebProperties.class})
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext),
applicationContext, this.getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
return welcomePageHandlerMapping;
}
}
....
}
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping()
底层写死进入欢迎页(welcome page)的地址
package org.springframework.boot.autoconfigure.web.servlet;
.....
final class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping {
....
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext,
Resource welcomePage, String staticPathPattern) {
if (welcomePage != null && "/**".equals(staticPathPattern)) {
//要用欢迎页功能,必须是/**
logger.info("Adding welcome page: " + welcomePage);
this.setRootViewName("forward:index.html");
} else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
// 调用Controller /index
logger.info("Adding welcome page template: index");
this.setRootViewName("index");
}
}
....
}
4、favicon
浏览器会发送 /favicon.ico 请求获取到图标,整个session期间不再获取
3、请求参数处理
3.1、请求映射
1、rest使用与原理
-
;@xxxMapping
- Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
- 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
- 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
- 核心Filter;HiddenHttpMethodFilter
- 用法: 表单method=post,隐藏域 _method=put
- SpringBoot中手动开启
- 扩展:如何把 _method 这个名字换成我们自己喜欢的。
@RestController
public class HelloController {
@RequestMapping("/45.jpg")
public String hello(){
return "aaa";
}
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
return "GET-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String saveUser(){
return "POST-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser(){
return "PUT-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-张三";
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
测试REST风格:
<form action="/user" method="get">
<input value="REST-GET 提交" type="submit">
</form>
<form action="/user" method="post">
<input value="REST-POST 提交" type="submit">
</form>
<form action="/user" method="delete">
<input value="REST-DELETE 提交" type="submit">
</form>
<form action="/user" method="put">
<input value="REST-PUT 提交" type="submit">
</form>
</body>
</html>
启动测试:
这里delete和put请求是get请求方法
源码底层做了限制,是false,
@ConditionalOnProperty( prefix = "spring.mvc.hiddenmethod.filter", name = {"enabled"}
package org.springframework.boot.autoconfigure.web.servlet;
....
...
public class WebMvcAutoConfiguration {
...
@Bean
@ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
@ConditionalOnProperty(
prefix = "spring.mvc.hiddenmethod.filter",
name = {"enabled"}
)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
Rest原理(表单提交要使用REST的时候)
- 表单提交会带上_method=PUT
- 请求过来被
拦截HiddenHttpMethodFilter
- 请求是否正常,并且是POST
- 获取到 _method 的值。
- 兼容以下请求;PUT.DELETE.PATCH
- 原生request(post),包装模式requesWrapper 重写了 getMethod方法,返回的是传入的值。
- 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
- 请求是否正常,并且是POST
package org.springframework.web.filter;
....
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
private static final List<String> ALLOWED_METHODS;
public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = "_method";
....
static {
ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter((ServletRequest)requestToUse, response);
}
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
this.method = method;
}
public String getMethod() {
return this.method;
}
}
application.yml添加配置:开启页面表单的Rest功能
#开启页面表单的Rest功能
spring:
mvc:
hiddenmethod:
filter:
enabled: true
index.html添加,
<input name="_method" type="hidden" value="DELETE">和<input name="_method" type="hidden" value="PUT">
,修改
method="post"
提交
重启测试:
Rest使用客户端工具,
- 如PostMan 直接发送 Put、delete 等方式请求,无需Filter。
怎么改变默认的_method
添加配置类:设置成
methodFilter.setMethodParam("_m");
package com.zzp.boot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.HiddenHttpMethodFilter;
@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
}
修改html:
启动,测试:
2、请求映射原理
SpringMVC功能分析都从
org.springframework.web.servlet.DispatcherServlet-》doDispatch()
package org.springframework.web.servlet;
...
public class DispatcherServlet extends FrameworkServlet {
...
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 找到当前请求使用哪个Handler(Controller的方法)处理
mappedHandler = this.getHandler(processedRequest);
//HandlerMapping:处理器映射。/xxx->>xxxx
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
....
}
}
RequestMappingHandlerMapping
:保存了所有
@RequestMapping
和
handler
的映射规则
所有的请求映射都在
HandlerMapping
中。
- SpringBoot自动配置欢迎页的
。访问 '/ '能访问到index.html;WelcomePageHandlerMapping
- SpringBoot自动配置了默认 的
RequestMappingHandlerMapping
- 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
- 如果有就找到这个请求对应的handler
- 如果没有就是下一个 HandlerMapping
- 我们需要一些自定义的映射处理,我们也可以自己给容器中放
。自定义HandlerMapping
HandlerMapping
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
3.2、普通参数与基本注解
1、注解:
@PathVariable
、
@RequestHeader
、
@ModelAttribute
、
@RequestParam
、
@MatrixVariable
、
@CookieValue
、
@RequestBody
@PathVariable
注解获取路径变量
@RestController
public class ParameterTestController {
// car/2/owner/zhangsan
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String,String> pv){
Map<String,Object> map = new HashMap<>();
map.put("id",id);
map.put("name",name);
map.put("pv",pv);
return map;
}
}
启动,请求:http://localhost:8080/car/2/owner/zzp
@RequestHeader
注解获取请求头信息
@GetMapping("/getHeader")
public Map<String,Object> getHeader(@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> header){
Map<String,Object> map = new HashMap<>();
map.put("userAgent",userAgent);
map.put("header",header);
return map;
}
测试:
@RequestParam
注解获取请求参数信息
@GetMapping("/getParams")
public Map<String,Object> getParam(@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestHeader Map<String,String> params){
Map<String,Object> map = new HashMap<>();
map.put("age",age);
map.put("inters",inters);
map.put("params",params);
return map;
}
测试:
@CookieValue
注解获取cookie值
@GetMapping("/getCookie")
public Map<String,Object> getCookie(@CookieValue("_ga") String _ga,
@CookieValue("_ga") Cookie cookie){
Map<String,Object> map = new HashMap<>();
map.put("_ga",_ga);
map.put("cookie",cookie);
return map;
}
@RequestBody
注解获取请求体信息
@PostMapping("/getBody")
public Map getBody(@RequestBody String content){
Map<String,Object> map = new HashMap<>();
map.put("content",content);
return map;
}
测试:
@RequestAttribute
注解获取request域属性
@Controller
public class RequestController {
@GetMapping("/goto")
public String getAttribute(HttpServletRequest request){
request.setAttribute("msg","成功转发到 /success");
request.setAttribute("code",200);
// 转发到 /success 请求
return "forward:/success";
}
@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute("msg") String msg,
@RequestAttribute("code") Integer code,
HttpServletRequest request){
Object obj1 = request.getAttribute("msg");
Map<String,Object> map = new HashMap<>();
map.put("reqMethod_msg",obj1);
map.put("msg",msg);
map.put("code",code);
return map;
}
}
测试:
@MatrixVariable
注解获取矩阵变量
/car/{path}?xx=xxx&aa=ccc queryString 查询字符串。 @RequestParam
/car/sell;low=34;brand=buy,audi,yd ; 矩阵变量
页面开发,cookie被禁用了,session里面的内容怎么使用;
session.set(a, b) ---> jsessionid --> cookie ----> 每次发请求携带
url重写,/acb;jsessionid=xxx 把cookie的值使用矩阵变量的方式进行传递
//1、语法: /cars/sell;low=34;brand=byd,audi,yd
//2、SpringBoot默认是禁用了矩阵变量的功能
// 手动开启:原理。对于路径的处理。UrlPathHelper进行解析。
// removeSemicolonContent(移除分号内容)支持矩阵变量的
//3、矩阵变量必须有url路径变量才能被解析
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand,
@PathVariable("path") String path){
Map<String,Object> map = new HashMap<>();
map.put("low",low);
map.put("brand",brand);
map.put("path",path);
return map;
}
添加配置类:
@Configuration(proxyBeanMethods = false)
public class WebConfig /**implements WebMvcConfigurer */ {
//方式二
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 设置不移除; 后面的内容。矩阵变量功能就可以生效
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}
}
测试:
多个相同矩阵变量参数,指定路径变量
pathVar =xxx
// /boss/1;age=20/2;age=10
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
Map<String,Object> map = new HashMap<>();
map.put("bossAge",bossAge);
map.put("empAge",empAge);
return map;
}
测试:
2、Servlet API:
WebRequest
、
ServletRequest
、
MultipartRequest
、
HttpSession
、
javax.servlet.http.PushBuilder
、
Principal
、
InputStream
、
Reader
、
HttpMethod
、
Locale
、
TimeZone
、
ZoneId
ServletRequestMethodArgumentResolver
以上的部分参数
package org.springframework.web.servlet.mvc.method.annotation;
...
public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {
....
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
pushBuilder != null && pushBuilder.isAssignableFrom(paramType) ||
Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations() ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType;
}
}
3、复杂参数:
Map、Model(map、model里面的数据会被放在
request
的请求域
request.setAttribute
)、
Errors/BindingResult
、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、
SessionStatus
、
UriComponentsBuilder
、
ServletUriComponentsBuilder
代码演示:
@Controller
public class RequestController {
@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute(value = "msg",required = false) String msg,
@RequestAttribute(value = "code",required = false) Integer code,
HttpServletRequest request){
Object obj1 = request.getAttribute("msg");
Map<String,Object> map = new HashMap<>();
map.put("reqMethod_msg",obj1);
map.put("msg",msg);
map.put("code",code);
Object hello = request.getAttribute("hello");
Object world = request.getAttribute("world");
Object message = request.getAttribute("message");
map.put("hello",hello);
map.put("world",world);
map.put("message",message);
return map;
}
@GetMapping("/params")
public String testParam(Map<String,Object> map,
Model model,
HttpServletRequest request,
HttpServletResponse response){
map.put("hello","world6666");
model.addAttribute("world","hello666");
request.setAttribute("message","HelloWorld");
Cookie cookie = new Cookie("c1","v1");
//cookie.setDomain("localhost");
response.addCookie(cookie);
return "forward:/success";
}
}
访问页面:
Map、Model类型的参数,会返回
mavContainer.getModel();---> BindingAwareModelMap
是Model 也是Map
mavContainer.getModel(); 获取到值的
4、自定义对象参数:
可以自动类型转换与格式化,可以级联封装。
/**
* 姓名: <input name="userName"/> <br/>
* 年龄: <input name="age"/> <br/>
* 生日: <input name="birth"/> <br/>
* 宠物姓名:<input name="pet.name"/><br/>
* 宠物年龄:<input name="pet.age"/>
*/
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
public class Pet {
private String name;
private Integer age;
}
@PostMapping("/saveuser")
public Person saveuser(Person person){
return person;
}
index.html添加:
测试复杂类型:<hr>
测试封装POJO:
<form action="/saveuser" method="post">
姓名:<input name="userName" value="zhangsan"/><br>
年龄:<input name="age" value="18"/><br>
生日:<input name="birth" value="2019/10/10"/><br>
宠物姓名:<input name="pet.name" value="amao"/><br>
宠物年龄:<input name="pet.age" value="2"/><br>
<input type="submit" value="提交"/><br>
</form>
测试:
3.3、POJO封装过程
- ServletModelAttributeMethodProcessor
3.4、参数处理原理
-
中找到能处理请求的HandlerMapping
Handler(Controller.method())
- 为当前
找一个适配器Handler
;HandlerAdapter
RequestMappingHandlerAdapter
- 适配器执行目标方法并确定方法参数的每一个值
1、HandlerAdapter
0(
RequestMappingHandlerAdapter
) - 支持方法上标注
@RequestMapping
1(
HandlerFunctionAdapter
) - 支持函数式编程的
xxxxxx
2、执行目标方法
package org.springframework.web.servlet;
...
public class DispatcherServlet extends FrameworkServlet {
....
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
....
try {
try {
ModelAndView mv = null;
.....
// Actually invoke the handler.
//DispatcherServlet -- doDispatch
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
} finally {
....
}
}
}
//RequestMappingHandlerAdapter.handleInternal()方法
mav = invokeHandlerMethod(request, response, handlerMethod); //执行目标方法
//ServletInvocableHandlerMethod.invokeAndHandle()方法
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//InvocableHandlerMethod.invokeForRequest()获取方法的参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
3、参数解析器-HandlerMethodArgumentResolver
确定将要执行的目标方法的每一个参数的值是什么;
SpringMVC目标方法能写多少种参数类型。取决于参数解析器
- 当前解析器是否支持解析这种参数(
)supportsParameter
- 支持就调用
resolveArgument
4、返回值处理器
5、如何确定目标方法每一个参数的值
package org.springframework.web.method.support;
....
public class InvocableHandlerMethod extends HandlerMethod {
....
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
MethodParameter[] parameters = this.getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
} else {
Object[] args = new Object[parameters.length];
for(int i = 0; i < parameters.length; ++i) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] == null) {
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
} catch (Exception var10) {
if (logger.isDebugEnabled()) {
String exMsg = var10.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw var10;
}
}
}
return args;
}
}
}
5.1、挨个判断所有参数解析器那个支持解析这个参数
package org.springframework.web.method.support;
....
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
...
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
if (result == null) {
Iterator var3 = this.argumentResolvers.iterator();
while(var3.hasNext()) {
HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, resolver);
break;
}
}
}
return result;
}
}
5.2、解析这个参数的值
调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可
5.3、自定义类型参数 封装POJO
ServletModelAttributeMethodProcessor 这个参数处理器支持
是否为简单类型。
package org.springframework.beans;
...
public abstract class BeanUtils {
...
public static boolean isSimpleValueType(Class<?> type) {
return Void.class != type && Void.TYPE != type && (ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type);
}
}
package org.springframework.web.method.annotation;
...
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
...
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = (ModelAttribute)parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
} else {
try {
attribute = this.createAttribute(name, parameter, binderFactory, webRequest);
} catch (BindException var10) {
if (this.isBindExceptionRequired(parameter)) {
throw var10;
}
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
} else {
attribute = var10.getTarget();
}
bindingResult = var10.getBindingResult();
}
}
if (bindingResult == null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
this.bindRequestParameters(binder, webRequest);
}
this.validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
}
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中
GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean – Integer)
byte – > file
@FunctionalInterfacepublic interface Converter<S, T>
未来我们可以给WebDataBinder里面放自己的Converter;
private static final class StringToNumber<T extends Number> implements Converter<String, T>
自定义 Converter:
在配置类添加配置方法
@Configuration(proxyBeanMethods = false)
public class WebConfig{
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 设置不移除; 后面的内容。矩阵变量功能就可以生效
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) {
// 以逗号分割
if(!StringUtils.isEmpty(source)){
Pet pet = new Pet();
String[] split = source.split(",");
pet.setName(split[0]);
pet.setAge(Integer.valueOf(split[1]));
return pet;
}
return null;
}
});
}
};
}
}
index.html修改:
重启,测试:
6、目标方法执行完成
将所有的数据都放在
ModelAndViewContainer
;包含要去的页面地址View。还包含Model数据
7、处理派发结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)
;
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response)
;
package org.springframework.web.servlet.view;
...
public class InternalResourceView extends AbstractUrlBasedView {
...
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
this.exposeModelAsRequestAttributes(model, request);
this.exposeHelpers(request);
String dispatcherPath = this.prepareForRendering(request, response);
RequestDispatcher rd = this.getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + this.getUrl() + "]: Check that the corresponding file exists within your web application archive!");
} else {
if (this.useInclude(request, response)) {
response.setContentType(this.getContentType());
if (this.logger.isDebugEnabled()) {
this.logger.debug("Including [" + this.getUrl() + "]");
}
rd.include(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Forwarding to [" + this.getUrl() + "]");
}
rd.forward(request, response);
}
}
}
}
暴露模型作为请求域属性
// Expose the model object as request attributes.
this.exposeModelAsRequestAttributes(model, request);
package org.springframework.web.servlet.view;
....
public abstract class AbstractView extends WebApplicationObjectSupport implements View, BeanNameAware {
...
protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception {
//model中的所有数据遍历挨个放在请求域中
model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
} else {
request.removeAttribute(name);
}
});
}
}
4、数据响应与内容协商
4.1、响应JSON
1、[email protected]
// 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
// spring-boot-starter-web 自带 web场景自动引入了json场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.5.0</version>
<scope>compile</scope>
</dependency>
给前端自动返回json数据;
@Controller
public class ResponseTestController {
@ResponseBody
@GetMapping("/test/person")
public Person getPerson(){
Person person = new Person();
person.setAge(28);
person.setUserName("zhangsan");
return person;
}
}
测试:
返回值解析器
package org.springframework.web.servlet.mvc.method.annotation;
...
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
...
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
...
try {
this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception var6) {
...
}
throw var6;
}
}
}
package org.springframework.web.method.support;
...
public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {
...
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
} else {
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
}
}
package org.springframework.web.servlet.mvc.method.annotation;
...
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
...
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = this.createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = this.createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
// 使用消息转换器进行写出操作
this.writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
}
返回值解析器原理
1、返回值处理器判断是否支持这种类型返回值 supportsReturnType
2、返回值处理器调用 handleReturnValue 进行处理
3、RequestResponseBodyMethodProcessor 可以处理返回值标了
@ResponseBody
注解的
利用 MessageConverters 进行处理 将数据写为json
- 1、内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
- 2、服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据
-
3、SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
1.得到MappingJackson2HttpMessageConverter可以将对象写为json
2.利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
2、SpringMVC到底支持哪些返回值
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
3、HTTPMessageConverter原理
1、MessageConverter规范
HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。
例子:Person对象转为JSON。或者 JSON转为Person
2、默认的MessageConverter
0 - 只支持Byte类型的
1 - String
2 - String
3 - Resource
4 - ResourceRegion
5 - DOMSource .class \ SAXSource .class) \ StAXSource .class \StreamSource .class \Source .class
6 - MultiValueMap
7 - true
8 - true
9 - 支持注解方式xml处理的。
最终 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的jackson的objectMapper转换的)
自定义返回文件资源:
@ResponseBody // RequestResponseBodyMethodProcessor -- > messageConverter
@GetMapping("/he11")
public FileSystemResource file(){
// 文件统一返回设置
return null;
}
4.2、内容协商
根据客户端接收能力不同,返回不同媒体类型的数据。
1、引入xml依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
重启测试:
2、postman分别测试返回json和xml
只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。
3、开启浏览器参数方式内容协商功能
为了方便内容协商,开启基于请求参数的内容协商功能
application.yml文件添加配置
spring:
mvc:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
在请求地址后添加 format 参数
发请求: http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
debug源码:
确定客户端接收什么样的内容类型;
1、Parameter策略优先确定是要返回json数据(获取请求头中的format的值)
2、最终进行内容协商返回给客户端json即可。
4、内容协商原理
- 1、判断当前响应头中是否已经有确定的媒体类型。MediaType
- 2、获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)【application/xml】
- contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
- HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
- 3、遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
- 4、找到支持操作Person的converter,把converter支持的媒体类型统计出来。
- 5、客户端需要【application/xml】。服务端能力【10种、json、xml】
- 6、进行内容协商的最佳匹配媒体类型
- 7、用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。
导入了jackson处理xml的包,xml的converter就会自动进来
package org.springframework.web.servlet.config.annotation;
...
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
...
private static final boolean jackson2XmlPresent;
static {
ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader();
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
....
}
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new StringHttpMessageConverter());
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new ResourceRegionHttpMessageConverter());
Jackson2ObjectMapperBuilder builder;
if (!shouldIgnoreXml) {
if (jackson2XmlPresent) {
builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
} else if (jaxb2Present) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
}
...
}
}
5、自定义 MessageConverter
实现多协议数据兼容。json、xml、x-zzp
1、
@ResponseBody
响应数据出去 调用 RequestResponseBodyMethodProcessor 处理
2、Processor 处理方法返回值。通过 MessageConverter 处理
3、所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
4、内容协商找到最终的 messageConverter;
自定义响应头信息
步骤
1、添加一个自定义的MessageConverter进系统底层
2、系统底层就会统计所有MessageConverter能操作哪些类型
3、进行客户端协商 [zzp ---> zzp]
SpringMVC的什么功能。一个入口给容器中添加一个 WebMvcConfigurer
@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new ZzpMeaageConverter());
}
};
}
}
自定义的converter类:
/**
* 自定义的converter
*/
public class ZzpMeaageConverter implements HttpMessageConverter<Person> {
/**
*
* @param aClass
* @param mediaType
* @return
*/
@Override
public boolean canRead(Class<?> aClass, MediaType mediaType) {
return aClass.isAssignableFrom(Person.class);
}
@Override
public boolean canWrite(Class<?> aClass, MediaType mediaType) {
return aClass.isAssignableFrom(Person.class);
}
/**
* 服务器统计所有MeaageConverter都能写出哪里内容类型
*
* 自定义类:application/x-zzp
* @return
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-zzp");
}
@Override
public Person read(Class<? extends Person> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Person person, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
// 自定义协议数据的写出
String data = person.getUserName() + ";" + person.getAge() + ";" + person.getBirth();
// 写出数据
OutputStream outputMessageBody = httpOutputMessage.getBody();
outputMessageBody.write(data.getBytes());
}
}
dubug重启测试:
修改format的参数,format默认为两种:json、xml
基于请求地址参数的配置(format)
WebConfig添加配置,重写WebMvcConfigurer的configureContentNegotiation的方法:
/**
* 自定义内容协商策略
* @param configurer
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
//Map<String, MediaType> mediaTypes
Map<String, MediaType> mediaTypes = new HashMap<>();
//指定支持解析哪些参数对应的哪些媒体类型
mediaTypes.put("json",MediaType.APPLICATION_JSON);
mediaTypes.put("xml",MediaType.APPLICATION_XML);
// 自定义 媒体类型 gg -> application/x-zzp
mediaTypes.put("gg",MediaType.parseMediaType("application/x-zzp"));
ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
configurer.strategies(Arrays.asList(parameterStrategy));
}
重启,测试:
dubug截图:
基于请求头参数的配置(Accept)
WebConfig添加配置,重写WebMvcConfigurer的configureContentNegotiation的方法:
/**
* 自定义内容协商策略
* @param configurer
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
//Map<String, MediaType> mediaTypes
Map<String, MediaType> mediaTypes = new HashMap<>();
//指定支持解析哪些参数对应的哪些媒体类型
mediaTypes.put("json",MediaType.APPLICATION_JSON);
mediaTypes.put("xml",MediaType.APPLICATION_XML);
// 自定义 媒体类型 gg -> application/x-zzp
mediaTypes.put("gg",MediaType.parseMediaType("application/x-zzp"));
ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
// 基于请求头参数配置
HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();
configurer.strategies(Arrays.asList(parameterStrategy,headerStrategy));
}
重启测试:
有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】
5、视图解析与模板引擎
视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。
5.1、视图解析
1、视图解析原理流程
1、目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址
2、方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
3、任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)。
4、processDispatchResult 处理派发结果(页面改如何响应)
- 1、render(mv, request, response); 进行页面渲染逻辑
- 1、根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
- 1、所有的视图解析器尝试是否能根据当前返回值得到View对象
- 2、得到了 redirect:/main.html --> Thymeleaf new RedirectView()
- 3、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
- 4、view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作
- RedirectView 如何渲染【重定向到一个页面】
- 1、获取目标url地址
- 2、response.sendRedirect(encodedURL);
- 1、根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
视图解析:
- 返回值以 forward: 开始: new InternalResourceView(forwardUrl); --> 转发request.getRequestDispatcher(path).forward(request, response);
- 返回值以 redirect: 开始: new RedirectView() --》 render就是重定向
- 返回值是普通字符串: new ThymeleafView()—>
自定义视图解析器+自定义视图; 大厂学院。
5.2、模板引擎-Thymeleaf
1、thymeleaf简介
thymeleaf官网
Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.
现代化、服务端Java模板引擎
2、基本语法
1、表达式:
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域、session域、对象等值 |
选择变量 | *{…} | 获取上下文对象值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成链接 |
片段表达式 | ~{…} | jsp:include 作用,引入公共页面片段 |
2、字面量:
文本值: ‘one text’ , ‘Another one!’ ,…数字: 0 , 34 , 3.0 , 12.3 ,…布尔值: true , false
空值: null
变量: one,two,… 变量不能有空格
3、文本操作:
字符串拼接: +
变量替换: |The name is ${name}|
4、数学运算:
运算符: + , - , * , / , %
5、布尔运算:
运算符: and , or
一元运算: ! , not
6、比较运算:
比较: > , < , >= , <= ( gt , lt , ge , le )等式: == , != ( eq , ne )
7、条件运算:
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
8、特殊操作:
无操作:_
3、设置属性值-th:attr
设置单个值
<form action="subscribe.html" th:attr="[email protected]{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>
设置多个值
<img src="../../images/gtvglogo.png" th:attr="[email protected]{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
以上两个的代替写法 th:xxxx
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">
所有h5兼容的标签写法
使用文档
4、迭代
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
5、条件运算
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>
6、属性优先级
5.3、thymeleaf使用
1、引入Starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2、自动配置好了thymeleaf
package org.springframework.boot.autoconfigure.thymeleaf;
...
@Configuration(
proxyBeanMethods = false
)
@EnableConfigurationProperties({ThymeleafProperties.class})
@ConditionalOnClass({TemplateMode.class, SpringTemplateEngine.class})
@AutoConfigureAfter({WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class})
public class ThymeleafAutoConfiguration {}
自动配好的策略:
- 1、所有thymeleaf的配置值都在 ThymeleafProperties
- 2、配置好了 SpringTemplateEngine
- 3、配好了 ThymeleafViewResolver
- 4、我们只需要直接开发页面
package org.springframework.boot.autoconfigure.thymeleaf;
...
@ConfigurationProperties(
prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
...
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html"; //默认转跳xxx.html
}
3、页面开发
在resources目录templates文件夹下添加success.html文件
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <!-- http://www.thymeleaf.org 核心连接 -->
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${msg}">哈哈</h1>
<h2>
<a href="www.hao123.com" th:href="${link}">去百度</a> <br/>
<a href="www.hao123.com" th:href="@{link}">去百度2</a>
</h2>
</body>
</html>
控制类:
@Controller
public class ViewTestController {
@GetMapping("/zzp")
public String zzp(Model model){
// model 中的数据会被放在请求域中 request.setAttribute("a",aa)
model.addAttribute("msg","您好 zzp!");
model.addAttribute("link","https://www.baidu.com/");
return "success";
}
}
启动,测试访问:http://localhost:8080/zzp
原页面:
5.4、构建后台管理系统
1、项目创建
创建新的项目:boot-01-web-admin
基本依赖:thymeleaf、web-starter、devtools、lombok
2、静态资源处理
自动配置好,我们只需要把所有静态资源放到 static 文件夹下
3、路径构建
th:action="@{/login}"
4、模板抽取
th:insert/replace/include
5、页面跳转
@Controller
public class IndexController {
@PostMapping("/login")
public String main(User user, HttpSession session, Model model){
if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){
//把登陆成功的用户保存起来
session.setAttribute("loginUser",user);
//登录成功重定向到main.html; 重定向防止表单重复提交
return "redirect:/main.html";
}else {
model.addAttribute("msg","账号密码错误");
//回到登录页面
return "login";
}
}
}
thymeleaf行内写法 “[[${xx.xx}]]”
测试:截图
6、数据渲染
请求地址参数:
@GetMapping("/dynamic_table")
public String dynamic_table(Model model){
//表格内容的遍历
List<User> users = Arrays.asList(new User("zhangsan", "123456"),
new User("lisi", "123444"),
new User("haha", "aaaaa"),
new User("hehe ", "aaddd"));
model.addAttribute("users",users);
return "table/dynamic_table";
}
修改dynamic_table.html文件
<table class="display table table-bordered" id="hidden-table-info">
<thead>
<tr>
<th>#</th>
<th>用户名</th>
<th>密码</th>
</tr>
</thead>
<tbody>
<tr class="gradeX" th:each="user,stats:${users}">
<td th:text="${stats.count}">Trident</td>
<td th:text="${user.userName}">Internet</td>
<td >[[${user.password}]]</td>
</tr>
</tbody>
</table>
6、拦截器
6.1、HandlerInterceptor 接口
/**
* 登录检查
* 1、配置好拦截器要拦截哪些请求
* 2、把这些配置放在容器中
*/
@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 {
String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}",requestURI);
//登录检查逻辑
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
//放行
return true;
}
//拦截住。未登录。跳转到登录页
// session.setAttribute("msg","请先登录");
// response.sendRedirect("/");
request.setAttribute("msg","请先登录");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
/**
* 目标方法执行完成以后
* @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);
}
}
6.2、配置拦截器
/**
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
* 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**", "/js/**"); //放行的请求
}
}
/**
* 去main页面
* @return
*/
@GetMapping("/main.html")
public String mainPage(HttpSession session,Model model){
log.info("当前方法是:{}","mainPage");
//是否登录。 拦截器,过滤器
// Object loginUser = session.getAttribute("loginUser");
// if(loginUser != null){
// return "main";
// }else {
// //回到登录页面
// model.addAttribute("msg","请重新登录");
// return "login";
// }
return "main";
}
测试:
6.3、拦截器原理
1、根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】(
mappedHandler = this.getHandler(processedRequest);
)
2、先来顺序执行 所有拦截器的 preHandle方法
- 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
- 2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;
- 3、如果任何一个拦截器返回false。直接跳出不执行目标方法(
)if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; }
- 4、所有拦截器都返回True。执行目标方法
- 5、倒序执行所有拦截器的postHandle方法。
- 6、前面的步骤有任何异常都会直接倒序触发 afterCompletion (
)catch (Exception var22) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
- 7、页面成功渲染完成以后,也会倒序触发 afterCompletion(
)if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); }
源码:
package org.springframework.web.servlet;
...
public class HandlerExecutionChain {
...
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
//1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
if (!interceptor.preHandle(request, response, this.handler)) {
//2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}
//5、倒序执行所有拦截器的postHandle方法
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
for(int i = this.interceptorList.size() - 1; i >= 0; --i) {
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
interceptor.postHandle(request, response, this.handler, mv);
}
}
//org.springframework.web.servlet.DispatcherServlet
// 前面的步骤有任何异常都会直接倒序触发 afterCompletion
private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception {
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, ex);
}
throw ex;
}
// 7、页面成功渲染完成以后,也会倒序触发 afterCompletion
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
for(int i = this.interceptorIndex; i >= 0; --i) {
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
try {
interceptor.afterCompletion(request, response, this.handler, ex);
} catch (Throwable var7) {
logger.error("HandlerInterceptor.afterCompletion threw exception", var7);
}
}
}
//直接 倒序执行所有已经执行了的拦截器的 afterCompletion;
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
for(int i = this.interceptorIndex; i >= 0; --i) {
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
try {
interceptor.afterCompletion(request, response, this.handler, ex);
} catch (Throwable var7) {
logger.error("HandlerInterceptor.afterCompletion threw exception", var7);
}
}
}
}
7、文件上传
7.1、页面表单
templates/form/form_layouts.html文件
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="exampleInputEmail1">邮箱</label>
<input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
</div>
<div class="form-group">
<label for="exampleInputPassword1">名字</label>
<input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password">
</div>
<div class="form-group">
<label for="exampleInputFile">头像</label>
<input type="file" name="headerImg" id="exampleInputFile">
</div>
<div class="form-group">
<label for="exampleInputFile">生活照</label>
<input type="file" name="photos" multiple>
</div>
<div class="checkbox">
<label>
<input type="checkbox"> Check me out
</label>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
7.2、文件上传代码
/**
* 文件上传测试
*/
@Slf4j
@Controller
public class FormTestController {
@GetMapping("/form_layouts")
public String form_layouts(){
return "form/form_layouts";
}
/**
* MultipartFile 自动封装上传过来的文件
* @param email
* @param username
* @param headerImg
* @param photos
* @return
*/
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("上传的信息:email={},username={},headerImg.getSize={},photos.length={}",
email,username,headerImg.getSize(),photos.length);
if(!headerImg.isEmpty()){
String originalFilename = headerImg.getOriginalFilename();
int i = originalFilename.lastIndexOf("\\");
headerImg.transferTo(new File("F:\\data\\" + originalFilename.substring(i +1)));
}
if(photos.length > 0){
for(MultipartFile file : photos){
if(!file.isEmpty()){
String originalFilename = file.getOriginalFilename();
int i = originalFilename.lastIndexOf("\\");
file.transferTo(new File("F:\\data\\" + originalFilename.substring(i +1)));
}
}
}
return "main";
}
}
自定义文件大小上传配置:
spring:
servlet:
multipart:
max-file-size: 10MB # 单个上传文件大小
max-request-size: 100MB # 整体文件上传大小
测试:
7.3、自动配置原理
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
- 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
- 原理步骤
- 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
- 2、参数解析器来解析请求中的文件内容封装成MultipartFile
-
3、将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile>
FileCopyUtils。实现文件流的拷贝
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos)
8、异常处理
官方文档
8.1、错误处理
1、默认规则
- 默认情况下,Spring Boot提供 /error 处理所有错误的映射
- 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
- 测试异常情况:
- 要对其进行自定义,添加View解析为error
- 要完全替换默认行为,可以实现 ErrorController 并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。
- error/ 下的4xx,5xx页面会被自动解析;
- 测试效果:
2、定制错误处理逻辑
- 自定义错误页
- error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
- @[email protected]处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的
添加一个全局异常处理类:
/**
* 处理整个web controller的异常
*/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ArithmeticException.class,NullPointerException.class}) //处理异常
public String handleArithException(Exception e){
log.error("异常是:{}",e);
return "login"; //视图地址
}
}
测试:
-
;底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息组装成 ModelAndView返回;底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的 /error@ResponseStatus+自定义异常
异常处理类:
@ResponseStatus(value= HttpStatus.FORBIDDEN,reason = "用户数量太多")
public class UserTooManyException extends RuntimeException {
public UserTooManyException(){
}
public UserTooManyException(String message){
super(message);
}
}
测试:
- Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
- 自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则
- 自定义解析异常类:
/**
* 自定义解析异常
*/
@Order(value= Ordered.HIGHEST_PRECEDENCE) //优先级,数字越小优先级越高
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
try {
response.sendError(511,"我喜欢的错误");
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView();
}
}
- ErrorViewResolver 实现自定义处理异常;
- response.sendError 。error请求就会转给controller
- 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
- basicErrorController 要去的页面地址是 ErrorViewResolver ;
3、异常处理自动配置原理
- ErrorMvcAutoConfiguration类 自动配置异常处理规则
- 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
-
- public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
- DefaultErrorAttributes:定义错误页面中可以包含哪些数据。
- 容器中的组件:类型:BasicErrorController --> id:basicErrorController(json+白页 适配响应)
- 处理默认 /error 路径的请求;页面响应 new ModelAndView(“error”, model);(
)@RequestMapping({"${server.error.path:${error.path:/error}}"})
- 容器中有组件 View->id是error;(响应默认错误页)
- 容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。
- 处理默认 /error 路径的请求;页面响应 new ModelAndView(“error”, model);(
- 容器中的组件(错误视图解析器):类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
- 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
- error/404、5xx.html
如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)
4、异常处理步骤流程
1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束webRequest.requestCompleted();并且用 dispatchException
2、进入视图解析流程(页面渲染?)
this.processDispatchResult(processedRequest, response, mappedHandler,
mv
,
(Exception)dispatchException
);
3、mv = processHandlerException;处理handler发生的异常,处理完成返回ModelAndView;
- 1、遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】
- 2、系统默认的 异常解析器;
-
- 1、DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;
- 2、默认没有任何人能处理异常,所以异常会被抛出
- 1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
- 2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
- 3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
- 4、模板引擎最终响应这个页面 error/500.html
9、Web原生组件注入(Servlet、Filter、Listener)
9.1、使用Servlet API
@ServletComponentScan
(basePackages = “com.zzp.boot”) :指定原生Servlet组件都放在那里
@WebServlet
(urlPatterns = “/my”):效果:直接响应,没有经过Spring的拦截器
配置类:
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("66666");
}
}
主启动类添加
@ServletComponentScan
注解
@ServletComponentScan(basePackages = "com.zzp.boot")
@SpringBootApplication
public class MainAppliction {}
测试:
@WebFilter
(urlPatterns={"/css/","/images/"})
配置类:
@Slf4j
@WebFilter(urlPatterns={"/css/*","/images/*"}) //拦截静态资源
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作");
chain.doFilter(request,response);
}
@Override
public void destroy() {
log.info("MyFilter销毁");
}
}
测试:
@WebListene
配置类:
@Slf4j
@WebListener
public class MySwervletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MySwervletContextListener监听到项目初始化完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MySwervletContextListener监听到项目销毁");
}
}
启动,日记输出:
推荐可以这种方式;
扩展:DispatchServlet 如何注册进来
- 容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。
- 通过 ServletRegistrationBean 把 DispatcherServlet 配置进来。
- 默认映射的是 / 路径。
Tomcat-Servlet;
多个Servlet都能处理到同一层路径,精确优选原则
A: /my/
B: /my/1
9.2、使用RegistrationBean
ServletRegistrationBean, FilterRegistrationBean, and ServletListenerRegistrationBean
配置类:
/**
* 1、MyServlet --> /my
* 2、DispatcherServlet --> /
*/
// (proxyBeanMethods = true):保证依赖的组件始终是单实例的
@Configuration(proxyBeanMethods = true)
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my","/my02");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
// return new FilterRegistrationBean(myFilter,myServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}
10、嵌入式Servlet容器
官网文档
10.1、切换嵌入式Servlet容器
- 默认支持的webServer
- Tomcat, Jetty, or Undertow
- ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
- 切换服务器 引用undertow服务
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
启动日记:
- 原理
- SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
- web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext
- ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory(Servlet 的web服务器工厂—> Servlet 的web服务器)
- SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory
- 底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
- ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
- ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
- TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize—this.tomcat.start();
- 内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
10.2、定制Servlet容器
- 实现 WebServerFactoryCustomizer
- 把配置文件的值和ServletWebServerFactory 进行绑定
- 修改配置文件 server.xxx
- 直接自定义 ConfigurableServletWebServerFactory
xxxxxCustomizer:定制化器,可以改变xxxx的默认规则
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;
@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}
11、定制化原理
11.1、定制化的常见方式
- 修改配置文件;
- xxxxxCustomizer;
- 编写自定义的配置类
替换、增加容器中默认组件;视图解析器xxxConfiguration;+ @Bean
- Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件
/**
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
* 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
*
* @EnableWebMvc: 全面接管mvc
* 1、静态资源?视图解析器?欢迎页... 全部失效
*/
//@EnableWebMvc
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
/**
* 定义静态资源行为
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry){
/**
* 访问 /aa/** 所有请求都去 classpath:/static/ 下面进行匹配
*/
registry.addResourceHandler("/aa/**")
.addResourceLocations("classpath:/static/");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**",
"/images/**", "/js/**","/aa/**"); //放行的请求
}
// @Bean
// public WebMvcRegistrations webMvcRegistrations(){
// return new WebMvcRegistrations() {
// @Override
// public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
// return WebMvcRegistrations.super.getRequestMappingHandlerMapping();
// }
// };
// }
}
-
——@EnableWebMvc + WebMvcConfigurer
@Bean
,所有规则全部自己重新配置; 实现定制和扩展功能可以全面接管SpringMVC
- 原理
- 1、WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。静态资源、欢迎页…
- 2、一旦使用
、。会@EnableWebMvc
@Import(DelegatingWebMvcConfiguration.class)
- 3、DelegatingWebMvcConfiguration 的 作用,只保证SpringMVC最基本的使用
- 把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
- 自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
- public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
- 4、WebMvcAutoConfiguration 里面的配置要能生效 必须
没有这个组件才生效@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
- 5、
导致了 WebMvcAutoConfiguration 没有生效。@EnableWebMvc
- … …
11.2、原理分析套路
场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项
三、数据访问
1、SQL
1.1、数据源的自动配置-HikariDataSource
1、导入JDBC场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
数据库驱动?
为什么导入JDBC场景,官方不导入驱动?
官方不知道我们接下要操作什么数据库。
数据库版本和驱动版本对应
默认版本:<mysql.version>8.0.25</mysql.version>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!-- <version>5.1.49</version>-->
</dependency>
想要修改版本
1、直接依赖引入具体版本(maven的就近依赖原则)
2、重新声明版本(maven的属性的就近优先原则)
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.49</mysql.version>
</properties>
2、分析自动配置
2.1、自动配置的类
- DataSourceAutoConfiguration : 数据源的自动配置
- 修改数据源相关的配置:spring.datasource
- 数据库连接池的配置,是自己容器中没有DataSource才自动配置的
- 底层配置好的连接池是:HikariDataSource
package org.springframework.boot.autoconfigure.jdbc;
...
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(
type = {"io.r2dbc.spi.ConnectionFactory"}
)
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, InitializationSpecificCredentialsDataSourceInitializationConfiguration.class, SharedCredentialsDataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
....
@Configuration(proxyBeanMethods = false )
@Conditional({DataSourceAutoConfiguration.PooledDataSourceCondition.class})
@ConditionalOnMissingBean({DataSource.class, XADataSource.class})
@Import({Hikari.class, Tomcat.class, Dbcp2.class, OracleUcp.class, Generic.class, DataSourceJmxConfiguration.class})
protected static class PooledDataSourceConfiguration {
protected PooledDataSourceConfiguration() {
}
}
}
- DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置
- JdbcTemplateAutoConfiguration: JdbcTemplate的自动配置,可以来对数据库进行crud
- 可以修改这个配置项
来修改JdbcTemplate@ConfigurationProperties(prefix = "spring.jdbc")
-
;容器中有这个组件@[email protected] JdbcTemplate
- 可以修改这个配置项
- JndiDataSourceAutoConfiguration: jndi的自动配置(web配置)
- XADataSourceAutoConfiguration: 分布式事务相关的
3、修改配置项
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
jdbc:
template:
query-timeout: 3 # 设置连接数控库超时数据 单位:秒
4、测试
@Slf4j
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
// jdbcTemplate.queryForObject("select * from account_tbl");
// jdbcTemplate.queryForList("select * from account_tbl",)
Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class);
log.info("记录总数:{}",aLong);
}
}
控制台输出:记录总数:2
1.2、使用Druid数据源
1、druid官方github地址
https://github.com/alibaba/druid
整合第三方技术的两种方式:
- 自定义
- 找starter
2、自定义方式
2.1、创建数据源
pom文件引用依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
xml文件:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="maxActive" value="20" />
<property name="initialSize" value="1" />
<property name="maxWait" value="60000" />
<property name="minIdle" value="1" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="poolPreparedStatements" value="true" />
<property name="maxOpenPreparedStatements" value="20" />
2.2、StatViewServlet
StatViewServlet的用途包括:
- 提供监控信息展示的html页面
- 提供监控信息的JSON API
<servlet>
<servlet-name>DruidStatView</servlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DruidStatView</servlet-name>
<url-pattern>/druid/*</url-pattern>
</servlet-mapping>
2.3、StatFilter
用于统计监控信息;如SQL监控、URI监控
需要给数据源中配置如下属性;可以允许多个filter,多个用,分割;如:
<property name="filters" value="stat,slf4j" />
系统中所有filter:
别名 | Filter类名 |
---|---|
default | com.alibaba.druid.filter.stat.StatFilter |
stat | com.alibaba.druid.filter.stat.StatFilter |
mergeStat | com.alibaba.druid.filter.stat.MergeStatFilter |
encoding | com.alibaba.druid.filter.encoding.EncodingConvertFilter |
log4j | com.alibaba.druid.filter.logging.Log4jFilter |
log4j2 | com.alibaba.druid.filter.logging.Log4j2Filter |
slf4j | com.alibaba.druid.filter.logging.Slf4jLogFilter |
commonlogging | om.alibaba.druid.filter.logging.CommonsLogFilter |
慢SQL记录配置
<bean id="stat-filter" class="com.alibaba.druid.filter.stat.StatFilter">
<property name="slowSqlMillis" value="10000" />
<property name="logSlowSql" value="true" />
</bean>
使用 slowSqlMillis 定义慢SQL的时长
代码演示:
自定义数据源:
@Configuration
public class MyDataSourceConfig {
// 默认的自动配置是判断容器中没有才会配 @ConditionalOnMissingBean(DataSource.class)
@ConfigurationProperties("spring.datasource") //从配置文件获取
@Bean
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
// druidDataSource.setUrl();
// druidDataSource.setUsername();
// druidDataSource.setPassword();
return druidDataSource;
}
}
测试:
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
DataSource dataSource;
@Test
void contextLoads() {
// jdbcTemplate.queryForObject("select * from account_tbl");
// jdbcTemplate.queryForList("select * from account_tbl",)
Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class);
log.info("记录总数:{}",aLong);
log.info("数据源类型:{}",dataSource.getClass());
}
控制台输出:
数据源类型:class com.alibaba.druid.pool.DruidDataSource
引入监控功能:
pom文件添加依赖:
<!-- 引入监控功能-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置类:
@Configuration
public class MyDataSourceConfig {
// 默认的自动配置是判断容器中没有才会配 @ConditionalOnMissingBean(DataSource.class)
@ConfigurationProperties("spring.datasource") //从配置文件获取
@Bean
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
// druidDataSource.setUrl();
// druidDataSource.setUsername();
// druidDataSource.setPassword();
//加入监控功能
druidDataSource.setFilters("stat,wall");//监控,防火墙
// druidDataSource.setMaxActive(10); // 设置最大活跃线程数
return druidDataSource;
}
/**
* 配置 druid的监控页功能
* @return
*/
@Bean
public ServletRegistrationBean statViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");
//设置账号密码
registrationBean.addInitParameter("loginUsername","admin");
registrationBean.addInitParameter("loginPassword","123456");
return registrationBean;
}
/**
* WebStatFilter 用于采集web-jdbc关联监控的数据。
*/
@Bean
public FilterRegistrationBean webStatFilter(){
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
添加接口访问,方便看监控页面:
@Autowired
JdbcTemplate jdbcTemplate;
@ResponseBody
@GetMapping("/sql")
public String queryFromDb(){
Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class);
return aLong.toString();
}
登录:http://localhost:8080/druid
访问sql接口:http://localhost:8080/sql
监控一些功能的配置也可以写配置文件中,因为MyDataSourceConfig类的dataSource方法的注解
@ConfigurationProperties("spring.datasource") //从配置文件获取
spring:
datasource:
filters: stat,wall //开启监控,防火墙
max-active: 12 // 设置线程活跃最大数量
3、使用官方starter方式
3.1、引入druid-starter
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
3.2、分析自动配置
- 扩展配置项 spring.datasource.druid
package com.alibaba.druid.spring.boot.autoconfigure;
...
@Configuration
@ConditionalOnClass({DruidDataSource.class})
@AutoConfigureBefore({DataSourceAutoConfiguration.class})
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@Import({DruidSpringAopConfiguration.class,
DruidStatViewServletConfiguration.class,
DruidWebStatFilterConfiguration.class,
DruidFilterConfiguration.class})
public class DruidDataSourceAutoConfigure {}
-
DruidSpringAopConfiguration.class, 监控SpringBean的;配置项: spring.datasource.druid.aop-patterns@Import(
- DruidStatViewServletConfiguration.class, 监控页的配置:spring.datasource.druid.stat-view-servlet;默认开启
- DruidWebStatFilterConfiguration.class, web监控配置;spring.datasource.druid.web-stat-filter;默认开启
- DruidFilterConfiguration.class}) 所有Druid自己filter的配置
package com.alibaba.druid.spring.boot.autoconfigure.stat;
...
public class DruidFilterConfiguration {
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
private static final String FILTER_WALL_CONFIG_PREFIX = "spring.datasource.druid.filter.wall.config";
...
}
3.3、配置示例
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
druid:
aop-patterns: com.zzp.boot.* #springbean监控
filters: stat,wall,slf4j # 底层开启功能,stat(sql监控),wall(防火墙)
stat-view-servlet: # 配置监控页功能
enabled: true
login-username: admin # 登录用户
login-password: admin # 登录密码
resetEnable: false # 重置按钮
web-stat-filter: # web监控
enabled: true
urlPattern: /* # 匹配路径
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' # 排除
filter:
stat: # 对上面filters里面的stat的详细配置
slow-sql-millis: 1000 # 慢查询sql时间
logSlowSql: true
enabled: true
wall: #防火墙
enabled: true
config:
drop-table-allow: false # 不允许删表操作
SpringBoot配置示例
https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
配置项列表
https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8
1.3、整合MyBatis操作
https://github.com/mybatis
starter
SpringBoot官方的Starter:spring-boot-starter-*
第三方的: *-spring-boot-starter
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
1、配置模式
- 全局配置文件
- SqlSessionFactory: 自动配置好了
- SqlSession:自动配置了 SqlSessionTemplate 组合了SqlSession
-
;@Import(AutoConfiguredMapperScannerRegistrar.class)
- Mapper: 只要我们写的操作MyBatis的接口标准了
就会被自动扫描进来@Mapper
package org.mybatis.spring.boot.autoconfigure;
...
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisProperties.class}) //MyBatis配置项绑定类
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {...}
@ConfigurationProperties(
prefix = "mybatis"
)
public class MybatisProperties {}
mybatis文档
可以修改配置文件中 mybatis 开始的所有;
application.yml添加配置:
# 配置mybatis规则
mybatis:
config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置
mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件位置
结构图:
mybatis-config.xml文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 开启驼峰命名自动映射 -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
AccountMapper.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">
<mapper namespace="com.zzp.boot.mapper.AccountMapper">
<!-- Account getAccount(Long id); -->
<select id="getAccount" resultType="com.zzp.boot.bean.Account">
select * from account_tbl where id = #{id}
</select>
</mapper>
编写测试代码:
@Data
public class Account {
private Long id;
private String userId;
private Long money;
}
@Mapper
public interface AccountMapper {
Account getAccount(Long id);
}
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
public Account getAcctByid(Long id){
return accountMapper.getAccount(id);
}
}
@Autowired
private AccountService accountService;
@ResponseBody
@GetMapping("/acct")
public Account getById(@RequestParam("id") Long id){
return accountService.getAcctByid(id);
}
测试:
配置 private Configuration configuration; mybatis.configuration下面的所有,就是相当于改mybatis全局配置文件中的值
# 配置mybatis规则
mybatis:
# config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置
mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件位置
configuration: # 指定mybatis全局配置文件中的相关配置项
map-underscore-to-camel-case: true
# 可以不写全局;配置文件,所有全局配置文件的配置都放在configuration配置项中即可
- 导入mybatis官方starter
- 编写mapper接口。标准
注解@Mapper
- 编写sql映射文件并绑定mapper接口
- 在application.yaml中指定Mapper配置文件的位置,以及指定全局配置文件的信息 (建议;配置在mybatis.configuration)
2、注解模式
city表创建sql
CREATE TABLE `city` (
`id` int(19) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`name` varchar(64)NOT NULL DEFAULT '' COMMENT '名称',
`state` varchar(30)NOT NULL DEFAULT '' COMMENT '状态',
`country` varchar(30)NOT NULL DEFAULT '' COMMENT '国家',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='城市表';
@Mapper
public interface CityMapper {
@Select("select * from city where id=#{id}")
City getById(Long id);
@Insert("insert into city(`name`,`state`,`country`) values (#{name},#{state},#{country})")
@Options(useGeneratedKeys = true,keyProperty = "id")
void insert(City city);
}
3、混合模式
@Mapper
public interface CityMapper {
@Select("select * from city where id=#{id}")
public City getById(Long id);
public void insert(City city);
}
CityMapper.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">
<mapper namespace="com.zzp.boot.mapper.CityMapper">
<!-- void insert(City city); -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into city(`name`,`state`,`country`) values
(#{name},#{state},#{country});
</insert>
</mapper>
最佳实战:
- 引入mybatis-starter
- 配置application.yaml中,指定mapper-location位置即可
- 编写Mapper接口并标注
@Mapper注解
- 简单方法直接注解方式
- 复杂方法编写mapper.xml进行绑定映射
- 主启动类添加
注解 简化,其他的接口就可以不用标注@MapperScan("com.atguigu.admin.mapper")
@Mapper注解
// An highlighted block
var foo = 'bar';
1.4、整合 MyBatis-Plus 完成CRUD
1、什么是MyBatis-Plus
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
mybatis plus 官网
建议安装 MybatisX 插件
MyBatis-Plus quick-start代码实例
2、整合MyBatis-Plus
pom文件依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
自动配置:
- MybatisPlusAutoConfiguration 配置类,MybatisPlusProperties 配置项绑定。mybatis-plus:xxx 就是对mybatis-plus的定制
- SqlSessionFactory 自动配置好。底层是容器中默认的数据源
-
mapperLocations 自动配置好的。有默认值。 classpath*:/mapper*.xml;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下
- 容器中也自动配置好了 SqlSessionTemplate
-
标注的接口也会被自动扫描;建议直接 主启动类添加@Mapper
批量扫描就行@MapperScan("com.atguigu.admin.mapper")
优点:
- 只需要我们的Mapper继承 BaseMapper 就可以拥有crud能力
/**
* 继承BaseMapper 就可以拥有crud能力
*/
public interface UserMapper extends BaseMapper<User> {
}
@Data
public class User {
/**
* 所有属性都应该在数据库中
*/
@TableField(exist = false) //在表中字段不存在
private String userName;
@TableField(exist = false)
private String password;
//以下是数据库字段
private Long id;
private String name;
private Integer age;
private String email;
}
测试:
@Autowired
UserMapper userMapper;
@Test
void testUserMapper(){
User user = userMapper.selectById(1L);
log.info("用户信息:{}",user);
}
控制台输出:用户信息:User(userName=null, password=null, id=1, name=Jone, age=18, email=[email protected].com)
3、CRUD功能
/**
* Service 的CRUD也不用写了
*/
public interface UserService extends IService<User> {
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
@GetMapping("/user/delete/{id}")
public String deleteUser(@PathVariable("id") Long id,
@RequestParam(value = "pn",defaultValue = "1")Integer pn,
RedirectAttributes ra){
userService.removeById(id);
ra.addAttribute("pn",pn);
return "redirect:/dynamic_table";
}
@GetMapping("/dynamic_table")
public String dynamic_table(@RequestParam(value="pn",defaultValue = "1") Integer pn,Model model){
//表格内容的遍历
// List<User> users = Arrays.asList(new User("zhangsan", "123456"),
// new User("lisi", "123444"),
// new User("haha", "aaaaa"),
// new User("hehe ", "aaddd"));
// model.addAttribute("users",users);
// if(users.size() > 3){
// throw new UserTooManyException();
// }
//从数据库中查出user表中的用户进行展示
// List<User> list = userService.list();
// model.addAttribute("users",list);
//构造分页参数
Page<User> userPage = new Page<>(pn, 2);
//调用page进行分页
Page<User> page = userService.page(userPage, null);
// List<User> records = page.getRecords();
// long current = page.getCurrent();
// long pages = page.getPages();
// long total = page.getTotal();
model.addAttribute("page",userPage);
return "table/dynamic_table";
}
2、NoSQL
Redis 中文官网网址
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询,bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions)和不同级别的 磁盘持久化(persistence) , 并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。
2.1、Redis自动配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
自动配置:
- RedisAutoConfiguration 自动配置类。RedisProperties 属性类 --> spring.redis.xxx是对redis的配置
- 连接工厂是准备好的。LettuceConnectionConfiguration、JedisConnectionConfiguration
- 自动注入了RedisTemplate<Object, Object> : xxxTemplate;
-
自动注入了StringRedisTemplate;k:v都是String
key:value
- 底层只要我们使用 StringRedisTemplate、RedisTemplate就可以操作redis
redis环境搭建
1、阿里云按量付费redis。经典网络
2、申请redis的公网连接地址
3、修改白名单 允许0.0.0.0/0 访问
2.2、RedisTemplate与Lettuce
配置信息:
sring:
redis:
host: 127.0.0.1 # 单机模式-host
port: 6379 # 单机模式-端口
password: 123456 # 密码
@Test
void testRedis(){
ValueOperations<String, String> operations = redisTemplate.opsForValue();
operations.set("hello","world");
String hello = operations.get("hello");
System.out.println(hello);
}
输出:world
2.3、切换至jedis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 导入jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
yaml配置
spring:
redis:
host: 127.0.0.1 # 单机模式-host
port: 6379 # 单机模式-端口
password: 123456 # 密码
client-type: jedis # 指定连接的是 jedis
jedis:
pool:
max-active: 10 # 最大线程连接数
min-idle: 5
操作统计访问次数实验:
编写拦截器:
@Component
public class RedisUrlCountInterceptor implements HandlerInterceptor {
@Autowired
StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
//默认每次访问当前uri就会计数+1
redisTemplate.opsForValue().increment(uri);
return true;
}
}
添加组件中:
public class AdminWebConfig implements WebMvcConfigurer {
/**
* Filter、Interceptor 几乎拥有相同的功能?
* 1、Filter是Servlet定义的原生组件。好处,脱离Spring应用也能使用
* 2、Interceptor是Spring定义的接口。可以使用Spring的自动装配等功能
*
*/
@Autowired
RedisUrlCountInterceptor redisUrlCountInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**",
"/images/**", "/js/**","/aa/**"); //放行的请求
registry.addInterceptor(redisUrlCountInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**",
"/js/**","/aa/**");
}
}
控制层
@Autowired
StringRedisTemplate redisTemplate;
/**
* 去main页面
* @return
*/
@GetMapping("/main.html")
public String mainPage(HttpSession session,Model model){
log.info("当前方法是:{}","mainPage");
ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
String s = opsForValue.get("/main.html");
String s1 = opsForValue.get("/sql");
model.addAttribute("mainCount",s);
model.addAttribute("sqlCount",s1);
return "main";
}
main.html修改:
<div class="state-value">
<div class="value" th:text="${mainCount}">230</div>
<div class="title">/main.html</div>
</div>
<div class="state-value">
<div class="value" th:text="${sqlCount}">3490</div>
<div class="title">/sql</div>
</div>
测试效果:
四、单元测试
1、JUnit5 的变化
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
作为最新版本的JUnit框架,JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。
JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。
注意:
SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)
JUnit 5’s Vintage Engine Removed from spring-boot-starter-test,如果需要继续兼容junit4需要自行引入vintage
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
pom文件依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
现在版本:
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Test
void contextLoads() {
}
}
以前:
@SpringBootTest + @RunWith(SpringTest.class)
SpringBoot整合Junit以后:(
@SpringBootTest
注解标注的类)
- 编写测试方法:@Test标注(注意需要使用junit5版本的注解)
- Junit类具有Spring的功能,
、比如@Autowired
标注测试方法,测试完成后自动回滚@Transactional
2、JUnit5常用注解
JUnit5的注解与JUnit4的注解有所变化
https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
- @Test :表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
- @ParameterizedTest :表示方法是参数化测试,下方会有详细介绍
- @RepeatedTest :表示方法可重复执行,下方会有详细介绍
- @DisplayName :为测试类或者测试方法设置展示名称
- @BeforeEach :表示在每个单元测试之前执行
- @AfterEach :表示在每个单元测试之后执行
- @BeforeAll :表示在所有单元测试之前执行
- @AfterAll :表示在所有单元测试之后执行
- @Tag :表示单元测试类别,类似于JUnit4中的@Categories
- @Disabled :表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
- @Timeout :表示测试方法运行如果超过了指定时间将会返回错误
- @ExtendWith :为测试类或测试方法提供扩展类引用
import org.junit.jupiter.api.Test; //注意这里使用的是jupiter的Test注解!!
/**
* @BootstrapWith(SpringBootTestContextBootstrapper.class)
* @ExtendWith(SpringExtension.class)
*/
//@SpringBootTest
@DisplayName("junit5功能测试类")
public class Junit5Test {
@Autowired
JdbcTemplate jdbcTemplate;
@DisplayName("测试displayname注解")
@Test
void testDisplayName() {
System.out.println("testDisplayName().....");
System.out.println(jdbcTemplate);
}
@Disabled
@DisplayName("测试方法2")
@Test
void test2() {
System.out.println("test2...");
}
@RepeatedTest(5) //重复测试
@Test
void test3() {
System.out.println("test3...");
}
/**
* 规定方法超时时间。超出时间测试出异常
*
* @throws InterruptedException
*/
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
@Test
void testTimeout() throws InterruptedException {
Thread.sleep(600);
}
@BeforeEach
void testBeforeEach() {
System.out.println("测试就要开始了...");
}
@AfterEach
void testAfterEach() {
System.out.println("测试结束了...");
}
@BeforeAll
static void testBeforeAll() {
System.out.println("所有测试就要开始了...");
}
@AfterAll
static void testAfterAll() {
System.out.println("所有测试以及结束了...");
}
}
3、断言(assertions)
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下
几个类别:
检查业务逻辑返回的数据是否合理。
所有的测试运行结束以后,会有一个详细的测试报告;
3.1、简单断言
用来对单个值进行简单的验证。如:
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
@Test
@DisplayName("simple assertion")
public void simple() {
assertEquals(3, 1 + 2, "simple math");
assertNotEquals(3, 1 + 1);
assertNotSame(new Object(), new Object());
Object obj = new Object();
assertSame(obj, obj);
assertFalse(1 > 2);
assertTrue(1 < 2);
assertNull(null);
assertNotNull(new Object());
}
3.2、数组断言
通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等
@Test
@DisplayName("array assertion")
void array() {
assertArrayEquals(new int[]{1, 2}, new int[]{1, 2}, "数组内容不相等");
}
3.3、组合断言
assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言
@Test
@DisplayName("组合断言")
void all() {
/**
* 所有断言全部需要成功
*/
assertAll("test",
() -> assertTrue(true && true, "结果不为true"),
() -> assertEquals(1, 2, "结果不是1"));
System.out.println("=====");
}
3.4、异常断言
在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。
@DisplayName("异常断言")
@Test
void testException() {
//断定业务逻辑一定出现异常
assertThrows(ArithmeticException.class, () -> {
int i = 10 / 2;
}, "业务逻辑居然正常运行?");
}
3.5、超时断言
@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
3.6、快速失败
通过 fail 方法直接使得测试失败
@DisplayName("快速失败")
@Test
void testFail(){
//xxxxx
if(1 == 2){
fail("测试失败");
}
}
4、前置条件(assumptions)
JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
@DisplayName("前置条件")
public class AssumptionsTest {
private final String environment = "DEV";
@Test
@DisplayName("simple")
public void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}
@Test
@DisplayName("assume then do")
public void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}
/**
* 测试前置条件
*/
@DisplayName("测试前置条件")
@Test
void testassumptions(){
Assumptions.assumeTrue(false,"结果不是true");
System.out.println("111111");
}
assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
5、嵌套测试
JUnit 5 可以通过 Java 中的内部类和
@Nested
注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用
@BeforeEach
和
@AfterEach
注解,而且嵌套的层次没有限制。
@DisplayName("嵌套测试")
public class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
//嵌套测试情况下,外层的Test不能驱动内层的Before(After)Each/All之类的方法提前/之后运行
assertNull(stack);
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
/**
* 内层的Test可以驱动外层的Before(After)Each/All之类的方法提前/之后运行
*/
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
6、参数化测试
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用
@ValueSource
等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。
@ParameterizedTest
@DisplayName("参数化测试")
@ValueSource(ints = {1,2,3,4,5})
void testParameterized(int i){
System.out.println(i);
}
@ParameterizedTest
@DisplayName("参数化测试")
@MethodSource("stringProvider")
void testParameterized2(String i){
System.out.println(i);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana","ZZP");
}
7、迁移指南
在进行迁移的时候需要注意如下的变化:
- 注解在
包中,断言在org.junit.jupiter.api
类中,前置条件在org.junit.jupiter.api.Assertions
类中。org.junit.jupiter.api.Assumptions
- 把
和@Before
替换成@After
和@BeforeEach
@AfterEach
。
把
和@BeforeClass
替换成@AfterClass
和@BeforeAll
。@AfterAll
- 把
替换成@Ignore
。@Disabled
- 把
替换成@Category
。@Tag
- 把
、@RunWith
和@Rule
替换成@ClassRule
。@ExtendWith
五、指标监控
1、SpringBoot Actuator
官网文档
1.1、简介
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
<!-- 引入监控功能-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
1.2、1.x与2.x的不同
1.3、如何使用
- 引入场景
- 访问 http://localhost:8080/actuator/**
- 暴露所有监控信息为HTTP
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露
-
测试
http://localhost:8080/actuator/beans
http://localhost:8080/actuator/configprops
http://localhost:8080/actuator/metrics
http://localhost:8080/actuator/metrics/jvm.gc.pause
http://localhost:8080/actuator/endpointName/detailPath
。。。。。。
1.4、可视化
https://github.com/codecentric/spring-boot-admin
2、Actuator Endpoint
2.1、最常使用的端点
ID | 描述 |
---|---|
auditevents | 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件。 |
beans | 显示应用程序中所有Spring Bean的完整列表。 |
caches | 暴露可用的缓存。 |
conditions | 显示自动配置的所有条件信息,包括匹配或不匹配的原因。 |
configprops | 显示所有@ConfigurationProperties。 |
env | 暴露Spring的属性ConfigurableEnvironment |
flyway | 显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。 |
health | 显示应用程序运行状况信息。 |
httptrace | 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。 |
info | 显示应用程序信息。 |
integrationgraph | 显示Spring integrationgraph 。需要依赖spring-integration-core。 |
loggers | 显示和修改应用程序中日志的配置。 |
liquibase | 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。 |
metrics | 显示当前应用程序的“指标”信息。 |
mappings | 显示所有@RequestMapping路径列表。 |
scheduledtasks | 显示应用程序中的计划任务。 |
sessions | 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。 |
shutdown | 使应用程序正常关闭。默认禁用。 |
startup | 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup。 |
threaddump | 执行线程转储。 |
如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:
ID | 描述 |
---|---|
heapdump | 返回hprof堆转储文件。 |
jolokia | 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core。 |
logfile | 返回日志文件的内容(如果已设置logging.file.name或logging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容 |
prometheus | 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus。 |
最常用的Endpoint:
- Health:监控状况
- Metrics:运行时指标
- Loggers:日志记录
2.2、Health Endpoint
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:
- health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
- 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
- 可以很容易的添加自定义的健康检查机制
添加对某个端点的具体配置
# management 是所有actuator的配置
# management.endpoint.端点名.xxxx 对某个端点的具体配置
management:
endpoints:
enabled-by-default: true #默认开启所有监控端点 true
web:
exposure:
include: '*' # 以web方式暴露所有端点
endpoint: #对某个端点的具体配置
health:
show-details: always
2.3、Metrics Endpoint
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;
- 通过Metrics对接多种监控系统
- 简化核心Metrics开发
- 添加自定义Metrics或者扩展已有Metrics
2.4、管理Endpoints
1、开启与禁用Endpoints
- 默认所有的Endpoint除过shutdown都是开启的。
- 需要开启或者禁用某个Endpoint。配置模式为 management.endpoint.<endpointName>.enabled = true
management:
endpoint:
beans:
enabled: true
- 或者禁用所有的Endpoint然后手动开启指定的Endpoint
management:
endpoints:
enabled-by-default: false
endpoint:
beans:
enabled: true
health:
enabled: true
2、暴露Endpoints
支持的暴露方式
- HTTP:默认只暴露health和info Endpoint
- JMX:默认暴露所有Endpoint
- 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则
ID | JMX | Web |
---|---|---|
auditevents | Yes | No |
beans | Yes | No |
caches | Yes | No |
conditions | Yes | No |
configprops | Yes | No |
env | Yes | No |
flyway | Yes | No |
health | Yes | No |
heapdump | N/A | No |
httptrace | Yes | No |
info | Yes | No |
integrationgraph | Yes | No |
jolokia | N/A | No |
logfile | N/A | No |
loggers | Yes | No |
liquibase | Yes | No |
metrics | Yes | No |
mappings | Yes | No |
prometheus | N/A | No |
scheduledtasks | Yes | No |
sessions | Yes | No |
shutdown | Yes | No |
startup | Yes | No |
threaddump | Yes | No |
3、定制 Endpoint
3.1、定制 Health 信息
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class MyHealthIndicator implements HealthIndicator {
@Override
public Health health() {
int errorCode = check(); // perform some specific health check
if (errorCode != 0) {
return Health.down().withDetail("Error Code", errorCode).build();
}
return Health.up().build();
}
}
构建Health
Health build = Health.down()
.withDetail("msg", "error service")
.withDetail("code", "500")
.withException(new RuntimeException())
.build();
management:
health:
enabled: true
show-details: always #总是显示详细信息。可显示每个模块的状态信息
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {
/**
* 真实的检查方法
* @param builder
* @throws Exception
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
//mongodb。 获取连接进行测试
Map<String,Object> map = new HashMap<>();
// 检查完成
if(1 == 1){
// builder.up(); //健康
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
}else {
// builder.down(); // 不健康
builder.status(Status.OUT_OF_SERVICE);
map.put("err","连接超时");
map.put("ms",3000);
}
builder.withDetail("code",100)
.withDetails(map);
}
}
测试:访问 http://localhost:8080/actuator/health
3.2、定制info信息
常用两种方式
1、编写配置文件
info:
appName: boot-admin
appVersion: 1.0.0
mavenProjectName: @[email protected]
mavenProjectVersion: @[email protected]
测试:访问 http://localhost:8080/actuator/info
2、编写InfoContributor
@Component
public class AppInfoInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("msg","你好")
.withDetail("hello","zzp")
.withDetails(Collections.singletonMap("world","6666"));
}
}
http://localhost:8080/actuator/info会输出以上方式返回的所有info信息
3.3、定制Metrics信息
1、SpringBoot支持自动适配的Metrics
- JVM metrics, report utilization of:
- Various memory and buffer pools
- Statistics related to garbage collection
-
Threads utilization
Number of classes loaded/unloaded
- CPU metrics
- File descriptor metrics
- Kafka consumer and producer metrics
- Log4j2 metrics: record the number of events logged to Log4j2 at each level
- Logback metrics: record the number of events logged to Logback at each level
- Uptime metrics: report a gauge for uptime and a fixed gauge representing the application’s absolute start time
- Tomcat metrics (server.tomcat.mbeanregistry.enabled must be set to true for all Tomcat metrics to be registered)
- Spring Integration metrics
2、增加定制Metrics
@Service
public class CityServiceImpl implements CityService {
@Autowired
CityMapper cityMapper;
Counter counter;
public CityServiceImpl(MeterRegistry meterRegistry){
counter = meterRegistry.counter("cityService.saveCity.count");
}
@Override
public void saveCity(City city) {
counter.increment();
cityMapper.insert(city);
}
}
//也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}
测试:http://localhost:8080/actuator/metrics
http://localhost:8080/actuator/metrics/cityService.saveCity.count
3.4、定制Endpoint
@Component
@Endpoint(id = "myservice")
public class MyServiceEndPoint {
@ReadOperation
public Map getDockerInfo(){
//端点的读操作 http://localhost:8080/actuator/myservice
return Collections.singletonMap("dockerInfo","docker started.....");
}
@WriteOperation
public void stopDocker(){
System.out.println("docker stopped.....");
}
}
测试:http://localhost:8080/actuator
http://localhost:8080/actuator/myservice
场景:开发ReadinessEndpoint来管理程序是否就绪,或者LivenessEndpoint来管理程序是否存活;
当然,这个也可以直接使用 https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-kubernetes-probes
六、原理解析
1、Profile功能
为了方便多环境适配,springboot简化了profile功能。
1.1、application-profile功能
- 默认配置文件 application.yaml;任何时候都会加载
- 指定环境配置文件 application-{env}.yaml
- 激活指定环境
- 配置文件激活
- 命令行激活:java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
- 修改配置文件的任意值,命令行优先
- 默认配置与环境配置同时生效
- 同名配置项,profile配置优先
绑定属性配置类:
@Data
@Component
@ConfigurationProperties("person")
public interface Person {}
yaml文件配置:
person:
xxx: xxx
1.2、@Profile条件装配功能
@Configuration(proxyBeanMethods = false)
@Profile("production")
public class ProductionConfiguration {
// ...
}
1.3、profile分组
# 指定激活的环境。默认配置文件和指定环境的配置文件都会生效。
spring.profiles.active=myprod
spring.profiles.group.myprod[0]=ppd
spring.profiles.group.myprod[1]=prod
spring.profiles.group.mytest[0]=test
2、外部化配置
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config
1、Default properties (specified by setting SpringApplication.setDefaultProperties).
2、@PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as
logging.*
and
spring.main.*
which are read before refresh begins.
3、Config data (such as application.properties files)
4、A RandomValuePropertySource that has properties only in random.*.
5、OS environment variables.
6、Java System properties (System.getProperties()).
7、JNDI attributes from
java:comp/env
.
8、ServletContext init parameters.
9、ServletConfig init parameters.
10、Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
11、Command line arguments.
12、properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
13、@TestPropertySource annotations on your tests.
14、Devtools global settings properties in the
$HOME/.config/spring-boot
directory when devtools is active.
2.1、外部配置源
常用:Java属性文件、YAML文件、环境变量、命令行参数;
2.2、配置文件查找位置
(1) classpath 根路径
(2) classpath 根路径下config目录
(3) jar包当前目录
(4) jar包当前目录的config目录
(5) /config子目录的直接子目录
2.3、配置文件加载顺序:
1、当前jar包内部的application.properties和application.yml
2、当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
3、引用的外部jar包的application.properties和application.yml
4、引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml
2.3、指定环境优先,外部优先,后面的可以覆盖前面的同名配置项
3、自定义starter
3.1、starter启动原理
- autoconfigure包中配置使用 META-INF/spring.factories 中 EnableAutoConfiguration 的值,使得项目启动加载指定的自动配置类
- 编写自动配置类 xxxAutoConfiguration -> xxxxProperties
- @Configuration
- @Conditional
- @EnableConfigurationProperties
- @Bean
- …
引入starter — xxxAutoConfiguration — 容器中放入组件 ---- 绑定xxxProperties ---- 配置项
3.2、自定义starter
zzp-hello-spring-boot-starter(启动器)空项目
pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.zzp</groupId>
<artifactId>zzp-hello-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- 依赖 自动配置包 -->
<dependency>
<groupId>com.zzp</groupId>
<artifactId>zzp-hello-spring-boot-starter-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
zzp-hello-spring-boot-starter-autoconfigure(自动配置包)项目
pom文件:
<?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.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.zzp</groupId>
<artifactId>zzp-hello-spring-boot-starter-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>zzp-hello-spring-boot-starter-autoconfigure</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
服务类:
/**
* 默认不要放在容器中
*/
public class HelloService {
@Autowired
HelloProperties helloProperties;
public String sayHello(String userName){
return helloProperties.getPrefix() + ":"+userName+"》"+helloProperties.getSuffix();
}
}
配置类:
@ConfigurationProperties("zzp.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;
}
}
自动配置类:
@Configuration
@EnableConfigurationProperties(HelloProperties.class) //默认HelloProperties放在容器中
public class HelloServiceAutoConfiguration{
@ConditionalOnMissingBean(HelloService.class)
@Bean
public HelloService helloService(){
HelloService helloService = new HelloService();
return helloService;
}
}
META-INF/spring.factories 文件中 EnableAutoConfiguration 的值,使得项目启动加载指定的自动配置类
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zzp.hello.auto.HelloServiceAutoConfiguration
项目结构
最后,idea的Maven菜单 clean --> install 添加到本地仓库中
新建测试项目boot-09-hello-test服务
pom文件依赖:
<dependency>
<groupId>org.zzp</groupId>
<artifactId>zzp-hello-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
application.properties配置文件:
zzp.hello.prefix=zzp
zzp.hello.suffix=666
测试类:
@RestController
public class HelloController {
@Autowired
HelloService helloService;
@GetMapping("/hello")
public String sayHello(){
String zhangsan = helloService.sayHello("张三");
return zhangsan;
}
}
测试,访问 http://localhost:8080/hello
4、SpringBoot原理
Spring原理【Spring注解】、SpringMVC原理、自动配置原理、SpringBoot原理
4.1、SpringBoot启动过程
- 1、创建 SpringApplication
- 保存一些信息。
- 判定当前应用的类型。ClassUtils。Servlet
- bootstrappers:初始启动引导器(List<Bootstrapper>):去spring.factories文件中找 org.springframework.boot. Bootstrapper
- 找 ApplicationContextInitializer;去spring.factories找 ApplicationContextInitializer
- List<ApplicationContextInitializer<?>> initializers
- 找 ApplicationListener ;应用监听器。去spring.factories找 ApplicationListener
- List<ApplicationListener<?>> listeners
- 2、运行 SpringApplication
- StopWatch
- 记录应用的启动时间 –
stopWatch.start()
- 创建引导上下文(Context环境)
createBootstrapContext()
- 获取到所有之前的 bootstrappers 挨个执行 intitialize() 来完成对引导启动器上下文环境设置
- 让当前应用进入headless模式。java.awt.headless
- 获取所有 RunListener(运行监听器)【为了方便所有Listener进行事件感知】
- getSpringFactoriesInstances 去spring.factories找 SpringApplicationRunListener.
- 遍历 SpringApplicationRunListener 调用 starting 方法;
- 相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting。
- 保存命令行参数;ApplicationArguments
- 准备环境 prepareEnvironment();
- 返回或者创建基础环境信息对象。StandardServletEnvironment
- 配置环境信息对象。
- 读取所有的配置源的配置属性值。
- 绑定环境信息
- 监听器调用 listener.environmentPrepared();通知所有的监听器当前环境准备完成
- 创建IOC容器(
)createApplicationContext()
- 根据项目类型(Servlet)创建容器
- 当前会创建 AnnotationConfigServletWebServerApplicationContext
- 准备ApplicationContext IOC容器的基本信息 prepareContext()
- 保存环境信息
- IOC容器的后置处理流程。
postProcessApplicationContext()
- 应用初始化器;
;applyInitializers
- 遍历所有的 ApplicationContextInitializer 。调用 **initialize.。来对ioc容器进行初始化扩展功能**
- 遍历所有的 listener 调用 **contextPrepared。EventPublishRunListenr;通知所有的监听器contextPrepared**
- 所有的监听器 调用 contextLoaded。通知所有的监听器 contextLoaded
- 刷新IOC容器。
refreshContex
- 创建容器中的所有组件(Spring注解)
- 容器刷新完成后工作?
afterRefresh
- 所有监听 器 调用
通知所有的监听器 startedlisteners.**started**(context);
- 调用所有runners;
callRunners()
- 获取容器中的 ApplicationRunner
- 获取容器中的 CommandLineRunner
- 合并所有runner并且按照@Order进行排序
- 遍历所有的runner。调用 run 方法
- 如果以上有异常,
- 调用Listener 的 failed
- 调用所有监听器的 running 方法
通知所有的监听器 runninglisteners.running(context);
- running如果有问题。继续通知 failed 。调用所有 Listener 的 failed;通知所有的监听器 failed
package org.springframework.boot;
public interface Bootstrapper {
void intitialize(BootstrapRegistry registry);
}
package org.springframework.boot;
@FunctionalInterface
public interface ApplicationRunner {
/**
* Callback used to run the bean.
* @param args incoming application arguments
* @throws Exception on error
*/
void run(ApplicationArguments args) throws Exception;
}
package org.springframework.boot;
@FunctionalInterface
public interface CommandLineRunner {
/**
* Callback used to run the bean.
* @param args incoming main method arguments
* @throws Exception on error
*/
void run(String... args) throws Exception;
}
4.2、Application Events and Listeners
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-application-events-and-listeners
ApplicationContextInitializer
ApplicationListener
SpringApplicationRunListener
4.3、ApplicationRunner 与 CommandLineRunner
自定义监听:
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("MyApplicationContextInitializer ....initialize.... ");
}
}
public class MyApplicationListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("MyApplicationListener.....onApplicationEvent...");
}
}
public class MySpringApplicationRunListener implements SpringApplicationRunListener {
private SpringApplication application;
public MySpringApplicationRunListener(SpringApplication application, String[] args){
this.application = application;
}
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("MySpringApplicationRunListener....starting....");
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("MySpringApplicationRunListener....environmentPrepared....");
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener....contextPrepared....");
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener....contextLoaded....");
}
@Override
public void started(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener....started....");
}
@Override
public void running(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener....running....");
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("MySpringApplicationRunListener....failed....");
}
}
@Order(1)
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("MyApplicationRunner...run...");
}
}
/**
* 应用启动做一个一次性事情
*/
@Order(2)
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("MyCommandLineRunner....run....");
}
}
resources目录下创建META-INF文件夹添加spring.factories文件
org.springframework.context.ApplicationContextInitializer=\
com.zzp.boot.listener.MyApplicationContextInitializer
org.springframework.context.ApplicationListener=\
com.zzp.boot.listener.MyApplicationListener
org.springframework.boot.SpringApplicationRunListener=\
com.zzp.boot.listener.MySpringApplicationRunListener
测试: