SpringBoot2 学习
SpringBoot2 基础入门
4.@Import({User.class}) 给容器中自动创建出这两个类型的组件
在容器中自动创建出这两个类型的组件,默认的组件名字就是全类名
1.boot2-01 基础
1.1了解自动配置原理
引入了父项目之后会自动进行版本控制,如果不想使用规定的版本,可以自定义使用自己的版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<properties>
<mysql.version>5.1.43</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>
上面就是例子,我们覆盖了默认的mysql连接的版本
1.2starter场景启动器
SpringBoot有很多这种场景启动器,引入了一个场景启动器,就相当于把这个场景所有常规所需要的依赖引入了
我们也可以自定义自己的Starter,不过自己创建的不要以Spring开头
*-spring-boot-starter 类似于这种格式
所有场景启动器依赖都会依赖一个父类依赖
如果引入的是非版本仲裁的,就需要声明版本
1.3自动配置
- 自动配置好Tomcat
- 引入Tomcat依赖管理
- Tomcat配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
- 自动配置好SpringMVC
- 引入了SpringMVC全套组件
- 自动配置好了SpringMVC常用功能
- 自动配置好Web常见功能,把这些都放在了IOC容器里面,如:字符编码问题
- SpringBoot帮我们配置好了所有web开发的常见场景
- 默认的包结构
- 主程序所在的包及其下面所有的子包都会被扫描进来
- 无需以前的包扫描配置,
- 不过如果真的想要扫描其他的,那也可以,在主启动类配置一下@SpringBootApplication(scanBasePackages = “com.atguigu”)
- 或者 @ComponentScan(“com.atguigu”)
- 主启动注解是由三个注解组成的,
- 我们可以分开写这三个,然后自定义扫描
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
- 很多配置都有默认值
- 默认配置最终都是映射到某一个类上的,这些类是被加载到容器里面的,所以最后可以被使用到
- 配置文件的值最终会绑定到每一个类,这个类在容器中会有创建对象
- 按需加载所有自动配置项
- 非常多的starter
- 引入了哪些场景,这个场景的自动配置才会开启,
- SpringBoot的所有自动配置功能都在 spring-boot-autoconfigure 这个项目里面
2、容器功能
2.1组件添加
1.@Configuration
- 基本使用
- Full模式与Lite模式
- 示例
- 最佳实战
- 配置 类组件之间无依赖关系用Lite模式会加速容器启动的过程,减少判断
- 配置类组件之间有依赖关系,方法之间会相互调用,使用Full模式
/**
* User:曹帅
* Date:2021/1/25
* Version:1.0
* 配置类,
* 1.可以使用@Bean标注在方法上面给容器注册组件,默认也是单实例的
* 2.配置类本身也是组件
* 3.proxyBeanMethods: 是不是代理bean的方法
* Full 使用代理
* Lite 不使用代理
* 组件依赖
* 如果
*/
@Configuration(proxyBeanMethods = false)
public class MyConfig {
@Bean
public User user01(){
User user = new User("zs", 18);
//user组件依赖了pet组件
user.setPet(tomcat());
return user;
}
@Bean
public Pet tomcat(){
return new Pet("tomcat");
}
}
@SpringBootApplication
public class MainApp {
public static void main(String[] args) {
//1.返回我们的IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApp.class, args);
//2.查看容器里面的组件
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
//3.从容器中获取组件
Pet pet = run.getBean(Pet.class);
Pet pet1 = run.getBean(Pet.class);
System.out.println("组件:"+(pet==pet1));
//4.获取到的本身就是代理对象
MyConfig myConfig = run.getBean(MyConfig.class);
//5.如果@Configuration(proxyBeanMethods = true),代理对象调用方法,SpringBoot总会检查这个组件是否在容器中有,如果有
//保持组件单实例
System.out.println(myConfig);
User user = myConfig.user01();
User user01 = myConfig.user01();
System.out.println("user:"+(user==user01));
User user1 = run.getBean(User.class);
Pet pet2 = run.getBean(Pet.class);
System.out.println("用户的宠物:"+(user1.getPet()==pet2));
}
}
2、@Bean、@Component、@Controller、@Service、@Repository
**
3、@ComponentScan、@Import
@Import({User.class}) 给容器中自动创建出这两个类型的组件
在容器中自动创建出这两个类型的组件,默认的组件名字就是全类名
4.@Conditional
- 条件装配,@ConditionalOnBean(name = “tomcat”),容器中有某个组件时
- @ConditionalOnMissingBean 反向判断
2.2原生配置文件引入
- @ImprotResource(“classpath:beans.xml”) 引入配置文件
2.3配置绑定
/**
* User:曹帅
* Date:2021/1/25
* Version:1.0
* 只有在容器中的组件,才会拥有SpringBoot提供的强大功能
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {
private String brand;
private Integer price;
}
- 上面可以直接获取到配置文件中的内容,
- 在配置类上面声明注解其实也可以
//1.开启Car的属性配置绑定功能
//2.把这个组件自动导入容器中
@EnableConfigurationProperties(Car.class)
3、自动配置原理
3.1、引导加载自动配置类
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication
- @SpringBootConfiguration
@Configuration
public @interface SpringBootConfiguration
@Configuration 代表当前是一个配置类,
- @ComponentScan
指定扫描哪些,
- @EnableAutoConfiguration
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration
- @AutoConfigurationPackage 自动配置包
@Import({Registrar.class})
public @interface AutoConfigurationPackage
利用Register给容器中导入一系列组件
将指定的一个包下的所有组件导入进来, 主启动类下面
- @Import({AutoConfigurationImportSelector.class})
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
利用 getAutoConfigurationEntry方法给容器中导入一些组件
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
获取所有需要导入到容器中的配置类
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
然后这个时利用工厂加载器去加载的
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader)
最后调用这个方法加载,得到所有的组件
从 META-INF/spring.factories位置来加载一个文件 默认
默认扫描当前系统所有 的factories文件
这些127个配置类都是从 spring-boot-autoconfigure 下面的factories文件下面去找的
3.2、按需开启自动配置项
虽然我们127个场景的所有自动配置启动的时候默认全部加载,
但是最终会按需配置
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({Advice.class})
static class AspectJAutoProxyingConfiguration
这个时候 @ConditionalOnClass 注解就起到了作用,
只要我们没有导入aop相关的包,这个配置类就不能生效
加载了但是却不能生效
3.3、定制化修改自动配置
@Bean
@ConditionalOnBean(MultipartResolver.class) //容器中有这个类型组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)//容器中没有此名字的组件
public MultipartResolver multipartResolver(MultipartResolver resolver) {
//给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中去找,
//SpringMVC MULTIPART_RESOLVER_BEAN_NAME 防止有些用户配置的文件上传解析器不符合规范
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
给容器中加入了文件上传解析器,
- SpringBoot默认会在底层配好所有的组件,但是如果用户配置好了就以用户的优先,
- 自己配置的步骤,在配置类里面声明一个这个类型的参数,然后@Bean,就会注入进去
总结:
- SpringBoot 先加载所有的自动配置类, xxAutoConfiguration
- 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值 xxProperties里面 =》配置文件
- 生效的配置类就会给容器中装配很多的组件,
- 只要容器中有这些组件,相当于这些功能就有了
- 只要用户有自己配置的,就以用户的优先
- 定制化配置
- 用户直接@Bean替换底层组件
- 修改配置文件即可
@EnableConfigurationProperties(ServerProperties.class)
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties
自动配置类会引用配置属性类,
配置属性类,又会去配置文件里面去找
3.4、最佳实践
- 引入场景依赖
- 查看自动配置了哪些 选做
- 自己分析,引入场景对应的自动配置
- 配置文件中debug=true 开启自动配置报告 在控制台可以看出来
- Negative matches:
- Positive matches
- 是否需要修改
- 参考文档修改配置项
- 官方文档
- 自己分析 xxxx.properties 绑定了哪些配置文件的前缀
- 自定义加入或者替换组件
- @Bean @Component
- 自定义器 xxxCustomizer
- 参考文档修改配置项
开发小技巧
Lomok
简化JavaBean的开发
- 引入pom依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
- idea安装插件 plugins
- Bean的get set toString 方法都不需要写了
@Tostring
@Data //get set
@AllArgsConstructor //全参构造器
@NoArgsConstructor //无参构造
@EqualsAndHashCode
public class Car {
private String brand;
private Integer price;
}
- 日志记录器
@Slf4j
@RestController
public class HelloController {
@RequestMapping("/car")
public Car car() {
log.info("xxx");
return null;
}
}
Spring Initailizer
快速创建SpringBoot应用,里面基本上什么都有了
resources: static 里面放置静态资源
templates 里面放置web页面
dev-tools
- 热部署,修改项目之后不需要重新启动即可
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<version>2.2.2.RELEASE</version>
</dependency>
- 随意修改代码,然后 build 重新build项目就会刷新 ctrl+f9 也可以
- 但是这个原理只是重启一次,如果真的是想要热部署,
- JRebel 可以进行热部署,不过需要付费
SpringBoot2 核心技术-核心功能
4、配置文件
4.1、文件类型
- properties 这个是非常基本的
- yaml
简介 : yaml 不是一种标记语言,
基本语法
- key: value kv之间有空格
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab, 只能使用空格
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
-
表示注释
- `` 与 “” 表示字符串内容,不会被转义
数据类型
- 字面量:单个的、不可再分的,data,boolean,string,number、null
- 对象: 键值对的集合,map hash set object
k1: {k1: v1,k2: v2,k3: v3}
k:
k1: v1
k2: v2
k3: v3
- 数组 array、list、queue
k:
-v1
-v2
-v3
- 练习
person:
username: zz
boss: true
birth: 1998/07/13
age: 22
interests:
- 杨梦
animals: [小猫,小狗]
score:
chinese: 99
English: 95
math: 100
salary:
- 10000.00
- 8000.00
- 30000.00
pet:
name: 阿猫
weight: 3
allPets:
sick:
- {name: 小猫,weight: 2}
- name: 小白
weight: 0.5
- name: 大黄
weight: 5
health: [{name: 阿花,weight: 10},{name: 大白,weight: 8}]
@ConfigurationProperties(prefix = "person")
@Component
@Data
@ToString
public class Person {
private String username;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animals;
private Map<String,Object> score;
private Set<Double> salary;
private Map<String,List<Pet>> allPets;
}
注意: 单引号里面的数据不会进行转义,双引号里面的数据会进行转义
也可以理解为双引号不会改变字符串原本的行为,单引号会改变
- 上面写的时候还有一个问题,就是我们自定义的bean,再配置文件里面写值没有提示,
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
导入了这个依赖之后就可以了
- 不过还有一点小细节,因为这个只是为了简便开发,和功能无关,所以打包的时候可以去除掉这个,虽然有了也不影响,不过没有更好一些
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configurationprocessor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
5、web开发
5.1、自动配置概览
- 自动配置,
- 静态资源
- 自动注册视图转换器
- 静态页面支持
- 自动使用绑定
- 国际化
- 自定义Favicon
5.2、简单功能分析
- 静态资源访问
- 静态资源目录: called/static public resources /META-INF/resources
- 只要将资源放在这些目录下,项目启动 /文件名字就可以访问到
- 原理:静态映射 /**
- 请求进来,先去找controller,看能不能处理,不能处理的所有请求,又都交给静态资源处理器,静态资源就去指定目录寻找,
- 静态资源找不到就 404
- 静态资源访问前缀
- 默认是无前缀的
- 可以按照下面在配置文件里面进行修改
- 修改之后静态资源的访问路径是: 项目名+/static-path-pattern/静态资源名 = 静态资源文件夹下找
spring:
mvc:
static-path-pattern: /res/**
- 声明前缀
spring:
mvc:
static-path-pattern: /res/**
resources:
static-locations: [classpath:/haha/]
- webjar 的自动映射,有时候需要用到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
后面的地址需要按照依赖的包路径
5.3、欢迎页支持
- 静态资源路径下 index.html
- 可以配置静态资源路径
- 但是不能配置静态资源访问前缀,冲突
- controller 能处理 /index
5.4、静态资源配置原理
- SpringBoot启动默认加载 xxAutoConfiguration 自动配置类
- SpringMVC功能的自动配置类 几乎都在这里面 WebMvcAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration
- 给容器中配置了什么
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class,
.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter
- 配置文件的相关属性和xxx进行了绑定,WebMvcProperties、ResourceProperties
@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties
配置类只有一个有参构造器,有参构造器所有参数的值都会去容器中找
// ResourceProperties resourceProperties 获取和 spring.resources 绑定的所有值的对象
// WebMvcProperties mvcProperties spring.mvc 绑定的所有值
// ListableBeanFactory beanFactory IOC容器
// ObjectProvider<HttpMessageConverters> messageConvertersProvider 找到所有的HttpMessageConverters
// ResourceHandlerRegistrationCustomizer 找到资源处理器的自定义器
// ObjectProvider<DispatcherServletPath> dispatcherServletPath
// ObjectProvider<ServletRegistrationBean<?>> servletRegistrations 给用户注册 servlet、filter、listener。。。
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations)
- 资源处理的默认规则
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
public class ResourceProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
5.5、欢迎页的处理规则
HandlerMapping:处理器映射,保存了每一个Handler能处理哪些请求,
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
//要使用欢迎页功能,必须是 /**
logger.info("Adding welcome page: " + welcomePage.get());
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
//调用controller,看谁能处理这个请求
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
- favicon: 浏览器会发送 /favicon.ico 请求获取到图标,整个session期间不再获取
5.6、请求参数处理
- 请求映射
- @xxMApping;
- Rest风格支持,使用HTTP请求方式动词来表示对资源的操作
- 以前: /getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
- 现在:/user get-获取用户 delete-删除用户 put-修改用户 post-保存用户
- 核心Filter: HiddenHttpMethodFilter, 表单 method=post,隐藏域_method=put
- name = _method method=post value=put
- 如果想要使用需要在SpringBoot中手动开启 spring.mvc.hiddenmethod.filter=enabled
<form action="/user" method="get">
<input type="submit" value="REST-GET 提交">
</form>
<form action="/user" method="post">
<input type="submit" value="REST-POST 提交">
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="DELETE">
<input type="submit" value="REST-DELETE 提交">
</form>
<form action="/user" method="post">
<input type="submit" value="REST-PUT 提交">
<input type="hidden" value="PUT" name="_method">
</form>
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
spring:
mvc:
hiddenmethod:
filter:
enabled: true
- Rest风格原理 表单提交要使用Rest的时候
- 表单提交会带上 _method=put
- 请求过来会被 Hidden HTTP MethodFilter拦截
- 判断是否是post请求,看是否有什么错误,
- 一切正常之后获取请求参数 _method 的值
- 转换成大写的值
- 判断允许的请求方式里面是否包含当前的请求方式
- 兼容的请求:put,delete,patch
- 然后包装成一个新的请求,请求方式为得到的这个 ,这样原来的post请求就变成其他的了,包装模式
- 过滤器链放行1的时候用wrapper
- 所以以后的方法调用getMethod是调用Wrapper里面的
- 不过如果使用postman测试,直接就是对应的请求了
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);
}
- 这个_method也是系统里面进行配置的,如果我们不想使用它也可以进行替换
@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter filter = new HiddenHttpMethodFilter();
filter.setMethodParam("_m");
return filter;
}
}
- 请求映射原理
- doGet
- doService
- doDispatch
- 找到当前请求使用的哪个Handler进行处理 ,controller的方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
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;
}
-
RequestMappingHandlerMapping:保存了所有RequestMapping和controller的映射规则
-
所有的请求映射都保存在了HandlerMapping中
-
SpringBoot自动配置欢迎页的HandleMapping,访问 / 能访问到index.html
-
SpringBoot自动配置了RequestMappingHandlerMapping ,reject methods which in controller
-
请求进来,挨个尝试所有的HandleMapping,看是否有请求信息,如果有,就找到这个请求对应的Handler
-
如果没有就是下一个HadlerMapping
-
我们需要一些自定义的映射处理,我们也可以自己给容器中放一些 HandlerMapping
-
普通参数与基本注解
- 注解:
<a href="car/3/owner/lisi?age=26&inters=basketball&inters=ym">car/3/owner/lisi</a>
<li>@PathVariable(路径变量)</li>
<li>@RequestHeader(获取请求头)</li>
<li>@RequestParam(获取请求参数)</li>
<li>@CookieValue(获取cookie)</li>
<li>@RequestBody(获取请求体)</li>
<li>@RequestAttribute(获取request域属性)</li>
<li>@MatrixVariable(矩阵变量)</li>
<form action="/save" method="post">
测试 @RequestBody 获取数据 <br>
用户名:<input type="text" name="userName" /><br>
邮箱:<input type="text" name="email" /> <br>
<input type="submit" value="提交">
</form>
<br>
<hr>
<a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable(矩阵变量)</a>
<a href="/boss/1;age=20/2;age=21">@MatrixVariable(矩阵变量) pathVar</a>
<ol>
<li>矩阵变量需要在SpringBoot中手动开启</li>
<li>应该绑定在路径变量中</li>
<li>若是有多个矩阵变量,应该使用英文符号;进行分割</li>
<li>若是一个矩阵变量有多个值,应该使用英文符号,进行分割,或直接命名多个重复的key即可</li>
<li>如: /car/sell;low=34;brand=byd,audi,yd</li>
</ol>
</body>
</html>
@GetMapping("/car/{id}/owner/{username}")
public Map<String, Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String username,
@PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> headers,
@RequestParam("age")Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String,String> params,
@CookieValue("HISTORY") Cookie _ga) {
Map<String, Object> map = new HashMap<>();
// map.put("id", id);
// map.put("name",username);
// map.put("pv",pv);
// map.put("userAgent",userAgent);
// map.put("headers",headers);
map.put("age",age);
map.put("inters",inters);
map.put("params",params);
map.put("_ga",_ga);
return map;
}
@PostMapping("/save")
public Map postMethod(@RequestBody String content){
Map<String, Object> map = new HashMap<>();
map.put("content",content);
return map;
}
// /car/sell;low=34;brand=byd,audi,yd
//默认是禁用掉矩阵变量的,需要手动开启,对于l就的处理,
//使用UrlPathHelper进行解析,
//里面有一个属性:removeSemicolonContent 支持矩阵变量移除分号后内容
@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;
}
@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;
}
@GetMapping("/goto")
public String goToPage(HttpServletRequest request) {
request.setAttribute("msg", "成功了...");
request.setAttribute("code", 2021);
//转发到当前项目下的 /success请求
return "forward:success";
}
@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute("msg") String msg,
@RequestAttribute("code") Integer code,
HttpServletRequest request) {
Object msg1 = request.getAttribute("msg");
HashMap<String, Object> map = new HashMap<>();
map.put("msg", msg);
map.put("code", code);
map.put("msg1", msg1);
return map;
}
有一点需要注意的是,SpringBoot默认是无法使用矩阵变量的传递参数,因为默认;后面的内容直接给截取了,但是矩阵变量确实经常需要;,这个时候,有两种方式可以开启,都是关于配置类的
1是自定义一个类,然后实现哪个接口,把方法修改一下,
另外一个就是我们自己书写一个方法注入到容器中,修改里面的内容
@Configuration(proxyBeanMethods = false)
public class WebConfig /*implements WebMvcConfigurer*/ {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
HiddenHttpMethodFilter filter = new HiddenHttpMethodFilter();
filter.setMethodParam("_m");
return filter;
}
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}
/*public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
//设置不移除分号后的内容
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}*/
}
- Servlet API
- 方法中可以传入一些API的对象,ServletRequest,WebRequest,MyltipartRequest,HttpSession,Principal,Reader,TimeZone
- ServletRequestMethodArgumentResolver 以上的部分参数
- 复杂参数
- map,Errors,Model ,ServletResponse,SessionStatus, RedirectAttributes,
- map Model 里面的数据会被默认放在Request请求与中, request.setAttribute()
Map<String,Object> map,
Model model,
HttpServletRequest request
这三个类型相当于都在request作用域里面,都可以在request域中放数据
map和Model类型的参数,会返回mavContainer.getModel() 这个是Model,也是Map
请求处理完成之后,将所有的数据都放在ModelAndViewContainer;包含要去页面的view,还包含Model数据
处理派发结果:
- 自定义对象参数
- 数据绑定,页面请求提交大数据,都可以和对象进行绑定 get post都可以
- ServletModelAttributeMethodProcessor
- 判断是否为简单类型
- WebDataBinder web数据绑定器,将请求参数的值绑定到指定的对象里面
- 这个binder利用它里面的converters将请求数据转换成指定的数据类型,再次封装到JavaBean中
- GenericConversionService:在设置每一个值的时候,会找它里面的所有的converter,找出可以转换的那个,默认一共有125个converter,着实有些对,所以说对象不容易阿
- 将来我们可以在WebDataBiinder里面放入自己的converter
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// Create attribute instance
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// No BindingResult parameter -> fail with BindException
throw ex;
}
// Otherwise, expose null/empty value and associated BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
- 参数处理原理
//1.从HandleMapping中找到能够处理请求的Handler
//2.为当前Handler找一个适配器
doDispatch
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
最初一共有四个适配器:
0:方法上面的适配器
1:函数式的适配器
//3.找到适配器之后判断是不是get方法
//4.然后执行目标方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
//执行目标方法
mav = invokeHandlerMethod(request, response, handlerMethod);
//在执行方法之前,设置了参数解析器,
//SpringMVC目标方法能写多少种参数类型取决于参数解析器
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
//参数解析器其实是一个接口,
//当前解析器是否支持解析这种参数,如果支持就解析,
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter var1);
@Nullable
Object resolveArgument(MethodParameter var1, @Nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4) throws Exception;
}
//还有一个返回值处理器
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
//真正执行目标方法
invokeAndHandle
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//获取方法参数值
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
//核心就是这个方法
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 (this.logger.isDebugEnabled()) {
String exMsg = var10.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
this.logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw var10;
}
}
}
return args;
}
}
//挨个判断所有参数解析器,哪个支持解析这种参数
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;
}
//解析这个参数,可以去缓存里面取出参数解析器,调用参数解析器的resolve方法进行解析
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
} else {
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
}
![image.png](https://img-blog.csdnimg.cn/img_convert/fd275554a86b6864753c947fb956b11f.png#align=left&display=inline&height=62&margin=[object Object]&name=image.png&originHeight=124&originWidth=452&size=12488&status=done&style=none&width=226)
5.7、自定义转换器
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@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.parseInt(split[1]));
return pet;
}
return null;
}
});
}
};
}
5.8、数据响应与内容协商
- 响应json,
- 引入web场景
- @ResponseBody
- invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); 返回值解析器
- HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
- 返回值处理器先判断是否支持这种类型的返回值, supportsReturnType
- 最后找到了RequestResponseBodyMethodProcessor
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
使用消息转换其来进行写出操作
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
- 利用MessageConverters来进行处理将数据写为json
- 内容协商
- 浏览器会以请求头的方式告诉服务器能够接收什么杨的类型,
- 服务器最终根据自己自身的能力决定服务器能生产出什么样的内容类型的数据
- SpringMVC会挨个遍历所有容器底层的消息转换器,看谁能处理
- 系统默认的消息转换器 also have a lot
![image.png](https://img-blog.csdnimg.cn/img_convert/1cd27170fa5fe85f36b8ef26ea08af3e.png#align=left&display=inline&height=80&margin=[object Object]&name=image.png&originHeight=159&originWidth=365&size=14835&status=done&style=none&width=182.5)
- system default message converter
![image.png](https://img-blog.csdnimg.cn/img_convert/39a68a9ded4344e7189742c74285d9ea.png#align=left&display=inline&height=142&margin=[object Object]&name=image.png&originHeight=283&originWidth=482&size=30601&status=done&style=none&width=241)
- SpringMVC到底支持哪些返回值
- View
- ModelAndView
- Model
- ResponseEntity
- StreamingResponseBody
- HttpEntity
- HttpHeaders
- Callable
- DeferredResult
- ListenableFuture
- CompletionStage
- WebAsyncTask
- @ModelAttribute
- @ResonseBody ->RequestResponseBodyMethodProcessor
- 内容协商,根据客户端接收能力的不同,返回不同媒体类型数据
- 引入 jackson-dataformat-xml依赖
- 只需要改变请求头中 Accept字段,Http协议中规定的,告诉服务器,本客户端可以接收的数据类型
- 就会得到不同的效果
- 原理
- 判断当前响应头中是否已经有确定的媒体类型,
- 客户端支持接收的内容类型,分局请求头
- contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
- HeaderContentNegitiationStrategy 确定客户端可以接收的内容类型
- 遍历循环所有当前系统的MessageConverter,看谁支持操作这个对象
- 找到支持操作Person的converter,把converter支持的媒体类型统计出来
- 客户端需要xml,服务端能处理很多,可以处理xml
- 进行内容协商的最佳匹配
- 找到最佳匹配的converter,调用它进行转换
- 浏览器的内容协商没有办法修改怎么办?
- 可以在配置文件中进行配置
spring:
mvc:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
- 然后请求接口的时候请求路径带上一个参数 format=json
![image.png](https://img-blog.csdnimg.cn/img_convert/80a013b2f206424742a2ff7edd95202e.png#align=left&display=inline&height=144&margin=[object Object]&name=image.png&originHeight=287&originWidth=549&size=32626&status=done&style=none&width=274.5)
5.9、自定义MessageConverter
- 我们有时候会想要自定义一种数据格式用来进行传输,但是SpringMVC是无法理解我们想要的格式的,因为这毕竟是我们自定义的
- 这个时候我们可以自己定义一个消息转换器,把它注册到容器里面,这样容器就知道怎么处理返回我们这个格式的数据了
- 主要需要重写的方法是canWrite和write,在write里面声明我们的逻辑
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiguMessageConverter());
}
}
}
public class GuiguMessageConverter implements HttpMessageConverter {
@Override
public boolean canRead(Class clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Class clazz, MediaType mediaType) {
return clazz.isAssignableFrom(Person.class);
}
/*
服务器需要统计所有的MessageConverter都能写出哪些类型
application/x-guigu
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-guigu");
}
@Override
public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
Person person = (Person)o;
//自定义协议数据的写出
String data = person.getUserName()+";"+person.getAge()+";"+person.getBirth();
//写出去
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
}
}
- 但是刚才有一些缺点,我们在浏览器是不能自定义返回格式的,
- 原来1可以使用format来声明我们想要的返回格式,
- 现在我们也可以自己加入这种模式
- 我们可以自定义一个内容协商策略,原来format后面只能是json或者xml
- 现在1加入我们的,这样我们不需要伪造请求也可以得到我们定义的格式返回的数据了
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
/*
自定义内容协商策略
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
HashMap<String, MediaType> map = new HashMap<>();
map.put("json",MediaType.APPLICATION_JSON);
map.put("xml",MediaType.APPLICATION_XML);
map.put("gg",MediaType.parseMediaType("application/x-guigu"));
//指定支持解析哪些参数对应的媒体类型
HeaderContentNegotiationStrategy strategy1 = new HeaderContentNegotiationStrategy();
configurer.strategies(Arrays.asList(strategy,strategy1));
}
}
}
- 也可能我们添加的自定义功能会覆盖默认的功能,导致一些默认的功能会失效
5.10、Thymeleaf的使用
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 自动配置好了thymeleaf
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration
- 自动配置好的策略的配置值都在 ThymeleafProperties
- 配置好了SPringTemplateEngine
- 配置好了 ThymeleafViewResolver
5.10、Thymeleaf的使用
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 自动配置好了thymeleaf
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration
- 自动配置好的策略的配置值都在 ThymeleafProperties
- 配置好了SPringTemplateEngine
- 配置好了 ThymeleafViewResolver
- 我们只需写页面即可
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
- 页面开发
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Success</title>
</head>
<body>
<h1 th:text="${msg}">哈哈</h1>
<h2>
<a href="www.atguigu.com" th:href="${link}">去哪</a>
<a href="www.atguigu.com" th:href="@{link}">去哪</a>
</h2>
<h2>哈哈</h2>
</body>
</html>
现在也不太经常使用,所以我也不太想说
- 视图解析流程
- 目标方法处理的过程中,所有的数据都会放在 ModelAndViewContainer中
- 如果方法的参数是一个自定义类型对象,从请求参数中确定的,把它重新放在MAV中
- 任何目标方法执行完成后都会返回ModelAndView对象
- 看这个对象如果是空的,就给他一个默认的跳转页
- processDispatchResult 处理派发结果, 决定页面如何响应
- this.render(mv, request, response); 进行页面渲染
- 根据方法的String返回值得到view对象
- 所有的视图解析器尝试是否能根据当前返回值得到view对象
- 得到 redirect:/main.html -> RedirectView
- 视图对象最终会render view.render(mv.getModelInternal(), request, response);
- 先获取目标地址
- response.sendRedirect(encodeUrl);
- 返回值若以 forward 开始, new InternalResourceView
- 返回值是普通字符串, new ThymeleafView
- 自定义视图解析器加自定义视图可以完成更复杂的操作
5.11、拦截器的使用
- 编写一个拦截器
/**
* User:曹帅
* Date:2021/1/27
* Version:1.0
* 登录检查
* 1.配置好拦截器需要拦截哪些请求
* 2.把这些配置放在容器中
*/
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 {
//登录检查逻辑
HttpSession session = request.getSession();
Object user = session.getAttribute("loginUser");
if (user != null) {
//放行
return true;
}
//拦截,未登录,需要跳转到登录页面
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 {
}
/**
* 页面渲染完成之后
*
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
- 注入到配置类中,注意尽量拦截所有的请求,但是登录的页面不要给拦截了,然后静态的资源也不能拦截,要不然连样式都无法使用了
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //所有的请求包含静态资源
.excludePathPatterns("/login","/","/css/**","/fonts/**","images/**","/js/**"); //静态资源也被拦截了
}
}
- 拦截器的拦截流程:
- preHandle
- 方法执行
- postHandle
- afterCompletion
5.12、拦截器原理
- 根据当前请求找到可以处理请求的Handler以及handler的所有拦截器 HandlerExcutionChain
- 顺序执行所有拦截器的PreHandle方法,如果都返回为true,就放行,
- 如果当前拦截器返回true,就执行下一个拦截器
- 如果返回为false,直接倒序执行所有已经执行了的拦截器的afterCompletion;
- 这有点类似于一个栈的样子
- 如果任何一个拦截器执行失败,返回false,直接跳出,不执行目标方法,
- 所有拦截器都返回true’,执行目标方法
- 倒序执行所有拦截器的postHandle方法
- 前面的步骤有任何异常,都会直接触发afterCompletion
- 页面成功渲染完成,也会倒序触发 afterCompletion
- 永远都会触发已经执行过的拦截器的afterCompletion
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
mappedHandler.applyPostHandle(processedRequest, response, mv);
![image.png](https://img-blog.csdnimg.cn/img_convert/e0e26ea705299a49378a22c05e6cb412.png#align=left&display=inline&height=275&margin=[object Object]&name=image.png&originHeight=367&originWidth=871&size=185828&status=done&style=none&width=653)
5.13、文件上传
后端代码
@Slf4j
@Controller
public class FormController {
@GetMapping("/form_layouts")
public String form_layouts() {
return "form/form_layouts";
}
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestParam("headerImg") MultipartFile headerImg,
@RequestParam("photos") MultipartFile[] photos) throws IOException {
log.info("上传的信息:email={},username={},headerImg={},photos={}",
email, username, headerImg.getSize(), photos.length);
if (!headerImg.isEmpty()) {
//保存到文件服务器,oss服务器
String filename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("E:\\cache\\" + filename));
}
if (photos.length > 0) {
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
photo.transferTo(new File("E:\\cache\\1"+photo.getOriginalFilename()));
}
}
}
return "index";
}
}
自定义设置上传大小限制
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 30MB
原理
- 请求一进来使用文件上传解析器判断并封装文件上传请求 isMultipart
- 使用resolveMultipart返回MultipartHttpServletRequest
- 参数解析器解析请求中的文件内容,封装成MultipartFile
- 将文件的信息封装为一个map <String,MultipartFile>
5.14、异常处理
- 默认情况SpringBoot提供/error处理所有的错误映射
- 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息,对于浏览器客户端,响应一个whitelabel视图,以HTML格式呈现相同的数据
- 要对其进行自定义,添加View解析为error
- 要完全替换默认行为,可以实现ErrorController 并注册入容器或添加ErrorAttribute类型的组件,以使用现有机制但替换其内容
- error/下的4xx,5xx页面会被自动解析
配置的原理
- 容器中的组件 DefaultErrorAttributes 这个来进行处理
- BasicErrorController
- 处理 /error请求,页面响应 new ModelAndView(“error”,model);
- 容器中有组件View,id为error 响应默认错误页
- 容器中放组件BeanNameViewResolver 视图解析器:按照返回的视图名作为组件的id去容器中找view对象
- 容器中还配置了 DefaultErrorViewResolver
- 如果发生错误,会以http状态码作为视图页地址,找到真正的页面
- error/viewName.html
总结:如果想要返回页面,就会找error【staticview】视图,默认是一个白页
处理步骤
- 执行目标方法,目标方法运行期间有任何异常都会被catch;并且用dispatchException来进行封装
- 而且会被标志当前请求结束
- 进入视图解析流程 页面渲染
- 遍历所有的handleExceptionResolvers,看谁能处理异常
- 默认没有任何人处理异常,就会发送error请求,被底层的errorController处理
- 解析错误视图:遍历所有的errorViewResolver,看谁能解析
- 默认的是DefaultViewResolver,会把请求中状态码拿到,
- 把响应状态码作为错误页的地址,拼接成error/5xx.html
- 模板引擎最终响应这个页面
Web原生组件注入
- 使用Servlet API
- 新建一个servlet继承HttpServlet
- @WebServlet(filter=“”) 标明拦截路径
- 实现doget方法,
- 主启动类 @ServletComponentScan(basePackages="")表明扫描哪些包
- 这个没有经过Spring的拦截器
- 同理,filter 也要实现Filter接口, @WebFilter注解 重写 doFilter方法,init,destroy方法、
- filter可以过滤一些路径,urlPattern 里面进行配置
- listener实现ServletContextListener接口, @WebListener注解,重写 contextIntialized,contextDestroyed
- 使用RegistrationBean
- ServletRegistryBean,FilterRegistrationBean,ServletListenerRegistrationBean
@Configuration
public class MyRegistryConfig {
@Bean
public ServletRegistrationBean myServlet() {
return new ServletRegistrationBean(new MyServlet(), "/my", "/my02");
}
@Bean
public FilterRegistrationBean myFilter() {
//return new FilterRegistrationBean(new MyFilter(),myServlet());
MyFilter myFilter = new MyFilter();
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>(myFilter);
registrationBean.setUrlPatterns(Arrays.asList("/my", "/css/*"));
return registrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MyServletContextListener listener = new MyServletContextListener();
return new ServletListenerRegistrationBean(listener);
}
}
DispatchServlet如何注册进来
- 在DispatcherServletConfiguration 中自动注入了一个
- 并且利用配置文件的内容进行绑定
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
return dispatcherServlet;
}
- 通过DispatcherServletRegistrationConfiguration 把DispatchServlet组件注入
- 默认映射的是/路径
5.15、嵌入式Servlet容器
切换嵌入式Servlet容器,默认支持webserver,
原理
- SpringBoot应用启动发现当前是web应用,web场景包,导入tomcat,
- web应用会创建一个web版的ioc容器
- ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory 服务器工厂
- SPringBoot底层默认有很多的WebServer工厂
- 底层直接会有一个自动配置类, 这个配置类底层给我们配置了一些常用的,默认就是tomcat,按照条件配置的,哪个有就可以用哪个
- TomcatServeltWebServerFactory创建出Tomcat服务器并启动:TomcatWebServer的构造器用于初始化方法 initialize—this.tomcat.start();
- 我们也可以使用想要的服务器,就是在导入依赖的时候把tomcat排除掉,然后再引入其他的依赖就可以了
- @EnableWebMvc :在配置类上面添加,代表全面接管SpringMVC,
- WebMVCAutoConfiguration 默认的SpringMVC的自动配置功能类,静态资源,欢迎页
- 一旦使用@ENableWebMvc ,会 @Import DelegatingWebMvcConfiguration.class
- DelegatingWebMvcConfiguration 只保证SpringMVC最基本的使用
- 把所有系统中的WebMVCConfigurer 拿过来,所有功能的定制都是这些 WebMvcConfigurer合起来
- 自动配置了一些非常底层的组件,RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中去获取
- DelegatingWebMvcConfiguration extends WebMvcConcfigurationSupport
- WebMVCAutoConfiguration 里面的配置要生效,必须 没有WebMvcConcfigurationSupport
5.16、定制化原理
- 修改配置文件
- 编写自定义的配置类, xxxConfiguration
- Web 应用,编写一个配置类实现 WebMvcConfigurer即可定制化Web功能
06、数据访问
1、数据源自动配置
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
- 自动配置的类,
- DataSourceAutoConfiguration:数据源的自动配置
- 修改数据源相关的配置
- 数据库连接池的配置,是自己容器中没有注入DataSource才会自动配置进去的,
- 底层配置好的连接池是:HikariDataSource
我们可以自己在yaml文件中配置dataSource方面的信息
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driveryam
2、使用Druid数据源
这个是阿里出的,比较常用,功能也比较强大
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.18</version>
</dependency>
这个提供了很多监控的功能可以手动开启,
还可以引入官方的启动器
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
这个将很多配置都默认定义了
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
druid:
aop-patterns: com.atguigu.admin.* #监控SpringBean
filters: stat,wall # 底层开启功能,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
logSlowSql: true
enabled: true
wall:
enabled: true
config:
drop-table-allow: false
3、整合Mybatis
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
- 全局配置文件
- SqlSessionFactory自动配置好了,
- SqlSession:自动配置了 SqlSessionTemplate 组合了SqlSession
- @Import(AutoConfiguredMapperScannerRegistrar.class)
- Mapper:只要我们写的擦欧总Mybatis的接口标注了@Mapper就会被扫描到
@EnableConfigurationProperties(MybatisProperties.class) : MyBatis配置项绑定类。
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration{}
@ConfigurationProperties(prefix = "mybatis")
public class MybatisProperties
如果使用xml配置的方式,比较复杂一些
# 配置Mybatis的规则
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
configuration: #指定全局配置文件的相关属性
map-underscore-to-camel-case: true
<?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>
<!--<mappers>
<mapper resource="mapper/RoleMapper.xml"/>
</mappers>-->
</configuration>
<?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.atguigu.admin.mapper.RoleMapper">
<select id="getRole" resultType="com.atguigu.admin.bean.Role">
select * from role where id = #{id}
</select>
</mapper>
- 但凡可以用xml文件进行配置的,都可以在yaml里面直接进行配置
- 但是这两个不能一块使用
最佳处理方式
- 配置yaml文件,指定 mapper-location位置即可,
- 编写Mapper接口并标注@Mapper注解
- 简单方法,直接注解
- 复杂方法编写 xml文件进行绑定映射
# 配置Mybatis的规则
mybatis:
#config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
configuration: #指定全局配置文件的相关属性
map-underscore-to-camel-case: true
如果觉得在每一个Mapper类上都声明一下,比较麻烦,可以直接在主启动类声明一下要扫描哪个包下的东西,也可以不用写了
4、整合Mybatis-PLus CRUD
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
自动配置:
- MybatisPlusAutoConfiguration自动配置类,MybatisPlusProperties配置项进行绑定,扫描的是配置文件mybatis-plus: 开头的内容
- SqlSessionFactory 自动配置好,底层是容器中默认的数据源
- mapperLocations 自动配置好的,有默认值
- classpath*: /mapper/**/*.xml;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件,建议以后sql映射文件,放在mapper下面
- 容器中也自动配置好了SqlSessionTemplate
- @Mapper 标注的接口也会被自动扫描,可以直接使用批量扫描, @MapperScan(“com.atguigu.admin.mapper”)
优点:只需要我们的Mapper继承BaseMapper,就可以拥有crud 能力
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
public interface UserService extends IService<User> {
}
@GetMapping("/dynamic_table")
public String dynamic_table(@RequestParam(value = "pn",defaultValue = "1")Integer pn,
Model model){
log.info("方法被访问,参数"+pn);
//表格内容的遍历
// List<User> userList = Arrays.asList(new User("zs", "123456"),
// new User("xx", "123456"),
// new User("123", "123456"),
// new User("cs", "123456"),
// new User("ym", "123456"),
// new User("haha", "123456"));
// model.addAttribute("users",userList);
//List<User> userList = userService.list();
//model.addAttribute("users",userList);
//分页查询数据
Page<User> userPage = new Page<>(pn,2);
Page<User> page = userService.page(userPage, null);
model.addAttribute("page",page);
return "table/dynamic_table";
}
使用方法就和上面一样,dao层和service层,都可以继承默认的接口,然后我们可以直接使用较为简单的逻辑方法,这个也可以进行分页,但是需要我们在配置类手动进行开启,这个分页功能感觉写的特别的烂,建议还是不要用了,信息不是很全
@Configuration
public class MybatisConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
}
5、整合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)。
依赖导入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
自动配置:
- RedisAutoConfiguration 自动配置类,这个类引入的属性配置类是 RedisProperties 这个属性引入的是 spring.redis.xxx 的文件
- 连接工厂是准备好的,默认有两个 Lettuce Jedis ,第一个建议不要使用,有bug
- 默认的Redis操作模板也有两个, RedisTemplate<Object,Object> StringRedisTemplate<String,String>
环境搭建: 这个就不必说了,以前的文章里面有过
Filter和Inteceptor两者有哪些区别
- Filter是Servlet定义的组件,好处是脱离Spring也能应用
- Inteceptor是Spring定义的接口,可以使用Spring的自动装配的功能
默认的连接时Lettuce ,如果想要使用 Jedis的话,可以配置一下
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
spring:
redis:
host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com
port: 6379
password: lfy:Lfy123456
client-type: jedis
jedis:
pool:
max-active: 10
07、单元测试
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的测试引擎
SpringBoot2.4以上版本移除了默认对Vintage的依赖,如果需要使用Junnit4需要自行引入,
- 以前我们需要测试一个web项目里面的内容时需要两个注解 @SpringBootTest 和 RunWith
- 现在我们只需要一个就可以了
4、常用注解
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 😗*为测试类或测试方法提供扩展类引用
@DisplayName("junit5功能测试类")
public class Junit5Test {
@DisplayName("测试前置条件")
@Test
void testAssumptions(){
Assumptions.assumeTrue(false,"结果不是True");
}
@DisplayName("quickly failed")
@Test
void testFail(){
if (1>0){
fail("测试失败");
}
}
@DisplayName("异常断言")
@Test
void testException() {
//断定业务逻辑一定出现异常
assertThrows(ArithmeticException.class, () -> {
int i = 10 / 0;
}, "出现数学运算异常");
}
@Test
@DisplayName("组合断言")
void all() {
assertAll("test",
() -> assertTrue(1 < 0),
() -> assertEquals(3, (1 + 1)));
}
@Test
@DisplayName("array assertion")
public void array() {
assertArrayEquals(new int[]{2, 2}, new int[]{1, 2});
}
/*
断言:前面断言失败,后面的代码都不会执行
*/
@DisplayName("测试简单断言")
@Test
void Assert() {
int cal = cal(3, 3);
assertEquals(5, cal, "计算出错");
Object obj1 = new Object();
Object obj2 = new Object();
assertSame(obj1, obj2, "我们不一样");
}
int cal(int i, int j) {
return i + j;
}
@BeforeEach
void before() {
System.out.println("测试就要开始");
}
@Test
@DisplayName("测试DisplayName注解")
void testDisplayName() {
System.out.println(1);
}
@Test
@DisplayName("测试方法2")
void test2() {
System.out.println(2);
}
@RepeatedTest(3)
@Test
void test3() {
System.out.println(3);
}
/**
* 规定方法超时时间,超过时间就会出现异常
*
* @throws InterruptedException
*/
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void testTimeout() throws InterruptedException {
Thread.sleep(500);
}
@AfterEach
void after() {
System.out.println("测试结束");
}
@BeforeAll
static void beforeAll() {
System.out.println("所有测试开始");
}
@AfterAll
static void afterAll() {
System.out.println("所有测试结束");
}
}
3、断言
断言时测试方法中的核心部分,用来测试需要满足的条件进行验证,这些断言方法都是 Assertions里面的静态方法, 断言可以分为几个类别:
检查业务逻辑返回的数据是否合理,
所有的测试运行结束后,会有一个详细的测试报告
用来对单个值进行简单的验证。如:
- 简单断言
| 方法 | 说明 |
| :— | :— |
| 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());
}
- 数组断言
@Test
@DisplayName("array assertion")
public void array() {
assertArrayEquals(new int[]{2, 2}, new int[]{1, 2});
}
- 组合断言
@Test
@DisplayName("组合断言")
void all() {
assertAll("test",
() -> assertTrue(1 < 0),
() -> assertEquals(3, (1 + 1)));
}
- 异常断言
@DisplayName("异常断言")
@Test
void testException() {
//断定业务逻辑一定出现异常
assertThrows(ArithmeticException.class, () -> {
int i = 10 / 0;
}, "出现数学运算异常");
}
- 超时断言
/**
* 规定方法超时时间,超过时间就会出现异常
*
* @throws InterruptedException
*/
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void testTimeout() throws InterruptedException {
Thread.sleep(500);
}
- 快速失败
@DisplayName("quickly failed")
@Test
void testFail(){
if (1>0){
fail("测试失败");
}
}
4、前置条件
前置条件类似于断言,不同的是,不满足的断言会是的测试方法是啊比,但是不满足的前置条件只会是的测试方法执行终止,前置条件可以看成是执行测试方法的前提,当该前提不满足时,就没有继续执行的必要
@DisplayName("测试前置条件")
@Test
void testAssumptions(){
Assumptions.assumeTrue(false,"结果不是True");
}
assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
5、嵌套测试
JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。这个我也不会
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new 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
@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、参数化测试
参数化测试是一个新的特性,它使不同的参数多次运行测试成为了可能,
@ValueSource 为参数化测试指定入参来源,支持8大基础类以及String类型,Class类型
@NullSource:表示为参数化测试提供一个Null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource: 表示读取指定方法的赶回之作为参数化测试入参,注意的是,方法返回值需要是一个流
@DisplayName("参数化测试")
@ParameterizedTest
@ValueSource(ints={1,2,3,4,5})
public void testParam(int i) {
System.out.println(i);
}
@DisplayName("参数化测试")
@ParameterizedTest
@MethodSource("stringProvider")
public void testParam2(String i) {
System.out.println(i);
}
static Stream<String> stringProvider(){
return Stream.of("apple","banana","atguigu");
}
8、指标监控
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
1、SpringBoot Actuator
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 支持SPringMVC、JAX-RS Webflux,注解驱动进行扩展、层级,名称空间Metrics、底层使用MicroMeter,强大,便捷,默认丰富的安全策略
- 在配置文件中开启,然后访问 端口下的 actuator就可以看到了
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露
- 有很多路径,里面有不同的内容,beans、metrics、configprops
- 可视化,https://github.com/codecentric/spring-boot-admin
2、Actuator Endpoint
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 Endpoint 返回的结果,应该是一系列健康检查后的报告,
- 很多健康检查默认已经自动配置好了,比如数据库 redis等
- 可以很容易添加自定义的健康检查机制
- 这个默认就一个字段,行还是不行,我们可以设置一下,让它显示的比较详细一些
4、Metrics Endpoint
提供详细的、层级的、空间指标信息,这些信息可以被pull或者push方式得到
我们可以通过Metrics对接多种健监控系统,简化Metrics开发,添加自定义Metrics或者扩展已有Metrics
- 默认所有的Endpoint都是开启的
- 如果我们嫌多的话,可以关闭所有的,然后再开启我们想要的
management:
endpoint:
beans:
enabled: true
endpoint:
beans:
enabled: true
health:
enabled: true
- 除了,health和info,剩下的Endpoint都应该进行保护访问,如果引入SpringSecurity,则会配置安全访问规则
- 我们还可以定制自己的安全信息,这些别人就可以看到
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {
/*
真实的检查方法
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
Map<String,Object> map = new HashMap<>();
//mongodb 获取连接进行测试
if (1 == 1) {
//builder.up();
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
} else {
builder.down();
map.put("err","连接超时");
map.put("ms",3000);
}
builder.withDetail("code",100)
.withDetails(map);
}
}
management:
health:
enabled: true
show-details: always #总是显示详细信息。可显示每个模块的状态信息
- 定制info信息
编写配置文件,两个@ 之间是项目的内容
info:
appName: boot-admin
version: 2.0.1
mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值
mavenProjectVersion: @project.version@
或者编写 InfoController
@Component
public class ExampleInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("example",
Collections.singletonMap("key", "value"));
}
}
- 增加定制Metrics
class MyService{
Counter counter;
public MyService(MeterRegistry meterRegistry){
counter = meterRegistry.counter("myservice.method.running.counter");
}
public void hello() {
counter.increment();
}
}
//也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}
9、原理解析
1、Profile
为了方便多环境的切换使用
- 默认的配置文件 application.yml 任何时候都会加载
- 指定hh环境配置文件 application-dev.yaml
- 激活指定环境 spring.profiles.active=prod
- 命令行激活,java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
- 默认配置与环境配置同时生效,同名配置项,profile声明的优先
- 同时, @@Profile 也有条件装配的功能,我们可以使用这个来让某一个环境下哪些类不能被装配
@Configuration
public class MyConfig {
@Bean
@Profile("prod")
public Color red(){
return new Color();
}
@Bean
@Profile("test")
public Color green(){
return new Color();
}
}
- profile话可以进行分组
spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq
使用:--spring.profiles.active=production 激活
2、外部化配置
- 常用的外部配置源有,Java属性文件,yaml文件 环境变量,命令行参数
- 配置文件查找位置
- classpath 根路径
- classpath根路径下config目录
- jar包当前目录
- jar包当前目录的config目录
- /config子目录的直接子目
- 配置文件加载顺序
- 当前jar包内部的application.xml
- 当前jar包内部的application-{profile}.yaml
- 引用的外部jar包的application.yaml
- 引用的外部jar包的application-{profile}.yaml
- 指定环境优先,外部优先,后面的可以覆盖前面的同名配置项
3、自定义 starter
- 首先我们准备两个项目,一个项目啥都没有,到最后是一个starter,一个项目是自动配置项目
- 在自动配置项目的 autoconfigure包中配置使用 META-INF/spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.atguigu.hello.auto.HelloServiceAutoConfiguration
- 我们的自动配置类使用自动配置属性类,然后可以返回一个Service,这个Service里面的一些内容需要使用到自动配置属性类里面的数据,最后将这个 Service注入到Ioc容器中,当然,这个默认是注入这个Service,我们还可以添加一些条件,如果容器中有其他的相同类型的,我们就不必注入了
@Configuration
@EnableConfigurationProperties(HelloProperties.class) //默认这个组件会放入容器中
public class HelloServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean(HelloService.class)
public HelloService helloService(){
HelloService helloService = new HelloService();
return helloService;
}
}
@ConfigurationProperties("atguigu.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;
}
}
public class HelloService {
@Autowired
HelloProperties helloProperties;
public String sayHello(String userName) {
return helloProperties.getPrefix() + ": " + userName + ">>" + helloProperties.getSuffix();
}
}
我们的空项目作为我们的starter,引入这个自动配置的项目
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>atguigu-hello-spring-starter-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- 然后我们其他的项目如果有想要使用我们自定义starter的,可以直接引入这个空项目就可以了,
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>atguigu-hello-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 我们就可以使用 starter里面Service的默认方法,如果我们没有写配置文件,就用自动配置项目中的配置文件中的内容,如果我们自己项目的配置文件中写了响应的内容,就会使用我们自己的
@RestController
public class HelloController {
@Autowired
HelloService helloService;
@GetMapping("/hello")
public String sayHello(){
return helloService.sayHello("。。");
}
}
4、SpringBoot 原理
- 创建 SpringApplication
- 保存一些信息
- 判断当前应用类型, Servlet,ClassUtils
- bootstrappers:初始化启动引导器,去spring.factories 里面去找,一共会找到7个
- 找 ApplicaitonContextInitializer:去spring.factories 找
- 找 ApplicationListener:应用监听器,还是去factories里面找
- 运行SpringApplication
- StopWatch
- 记录应用的启动时间
- 创建引导上下文 createBootstrapContext();
- 获取到之前找到的bootstrapper挨个执行,initialize来完成对引导启动器上下文环境设置
- 让当前应用进入headless模式
- 获取所有的RunListener 运行监听器, 为了方便所有Listener进行事件感知
- getSpringFactoriesInstances 去spring.factories找 SrpingApplicationRunListener
- 遍历SrpingApplicationRunListener 调用starting方法,
- 相当于通知所有感兴趣系统正在启动过程的人,项目正在starting
- 保存命令行参数 ApplicationArguments
- 准备环境 prepareEnvironment();
- 返回或者创建基础环境信息对象, StandardServletEnvironment
- 配置环境信息对象
- 读取所有的配置源的配置属性值
- 绑定环境信息
- 监听器调用 listener.environmentPrepared(); 通知所有的监听器当前环境准备完成
- 创建IOC容器,
- 根据当前项目类型创建容器
- 当前会创建 AnnotationConfigServletWebServerApplicationContnexnnntn
- 准备 ApplicationContext IOC容器的基本信息, prepareContext()
- 保存环境信息
- IOC容器的后置处理流程
- 应用初始化器 apply Initializers
- 遍历所有的ApplicationContextInitializer,调用 initialize 来对容器进行初始化扩展功能
- 遍历所有的listener调用 contextPrepared EventPublishRunListener通知所有的监听器contextPrepared
- 所有的监听器调用 contextLoaded,通知所有监听器 contextLoaded
- 刷新IOC容器, refreshContext
- 创建容器中的所有组件
- 容器刷新完成后工作? afterRefresh
- 所有监听器调用listeners.started(context) 通知所有的监听器 started
- 调用所有 runners: callRUnners()
- 获取容器中的ApplicationRunner
- 获取容器中的CommandLineRunner
- 合并所有runner并且按照 @Order进行排序
- 遍历所有runner方法,调用run方法
- 如果以上有异常
- 调用listener的failed
- 调用所有监听器的running方法 listeners.running(context) 通知所有的监听器running
- running如果有问题,继续通知failed,调用所有listner的failed,通知所有的监听器failed