SpringBoot2(尚硅谷同步笔记) | 您所在的位置:网站首页 › 尚硅谷前端贴吧 › SpringBoot2(尚硅谷同步笔记) |
一、基础入门
1.1 为什么要用SpringBoot2?
SpringBoot能快速创建出生产级别的Spring应用 1.1.1 SpringBoot优点:创建独立Spring应用 内嵌web服务器 自动starter依赖,简化构建配置 自动配置Spring以及第三方功能 提供生产级别的监控、健康检查以及外部化配置 无代码生成、无需编写XML 1.1.2 SpringBoot缺点人称版本帝,迭代快,需要时刻关注变化 封装太深,内部原理复杂,不容易精通 1.1.3 时代背景微服务 微服务是一种架构风格 一个应用拆分为一组小型服务 每个服务运行在自己的进程内,也就是可独立部署和升级 服务之间使用轻量HTTP交互 服务围绕业务功能拆分 可以由全自动部署机制独立部署 去中心化、服务自治。服务可以使用不同的语言、不同的存储技术 分布式、云原生…… 1.2 HelloWorld案例①如果想使用springboot进行开发,需要将以下父工程导入到pom文件中 org.springframework.boot spring-boot-starter-parent 2.7.6②添加web的场景启动器 org.springframework.boot spring-boot-starter-web③编写主程序类 @SpringBootApplication public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class,args); } }④编写响应逻辑 @RestController public class HelloController { @RequestMapping("/hello") public String handle01(){ return "Hello SpringBoot2"; } }⑤运行主类,输入地址 SpringBoot快速构建部署: org.springframework.boot spring-boot-maven-plugin把项目打成jar包,直接在目标服务器执行 1.3 了解自动配置原理 1.3.1 依赖管理引入依赖 无需关注版本号,自动版本仲裁 可以修改版本号 ①查看spring-boot-dependencies里面规定当前依赖的版本用的key,比如mysql.version ②在当前项目里面重写配置 依赖管理: org.springframework.boot spring-boot-starter-parent 2.7.6它的父项目:几乎声明了所有开发中常用的依赖版本号 org.springframework.boot spring-boot-dependencies 2.7.6 1.3.2 starter场景启动器开发导入starter场景启动器 spring-boot-starter-*:*就代表某种场景 只要引入starter,这个场景的所有常规需要的依赖我们都会自动引入 一般来说*-spring-boot-stater为第三方为我们提供的简化开发的场景启动器 所有场景启动器最底层的依赖如下: org.springframework.boot spring-boot-starter 2.7.6 compile 1.3.3 自动配置自动配好Tomcat 引入Tomcat依赖 配置Tmocat org.springframework.boot spring-boot-starter-tomcat 2.7.6 compile自动配好SpringMVC 引入SpringMVC开发的全套组件 自动配好了SpringMVC常用组件(功能) 自动配好Web常见功能:如字符编码问题 SpringBoot帮我们配置好了所有web开发的常见场景 默认的包结构 主程序所在包以及其下面所有子包里面的组件都会被默认扫描进来 无需包扫描配置 如果想要改变扫描路径,可以通过scanBasePackages配置 @SpringBootApplication(scanBasePackages="com.atguigu")也可以通过注解@ComponentScan配置,但是会和@SpringBootApplication注解发生冲突,后续会讲解 解决方案 所有配置都拥有默认值 默认配置最终都会映射到MultipartProperties上 配置文件的值最终会绑定到某个类上,这个类会在容器中创建对象 按需加载所有自动配置项 非常多的starter 引入了哪些场景,这个场景的自动配置才会开启 SpringBoot所有的自动配置功能都在spring-boot-autoconfigure包里 org.springframework.boot spring-boot-autoconfigure 2.7.6 compile 二、底层注解 2.1 @Configuration@Configuration告诉SpringBoot这是一个配置类 配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的 配置类本身也是组件 proxyBeanMethods:代理bean方法 设置为true,@Bean 外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例 Full、Lite配置: 如果@Configuration(proxyBeanMethods=true):代理对象调用方法。 SpringBoot总会检查这个组件是否在容器中,并保持每个@Bean方法被调用多少次返回的组件都是单实例(组件之间有依赖关系) 如果@Configuration(proxyBeanMethods=false):解决组件依赖问题,不会检查这个组件是否为单实例,每个@Bean方法被调用多少次返回的组件都是新创建的 加速容器启动过程,减少判断 组件依赖必须使用Full模式,其他的一般采用Lite模式 package com.atguigu.boot.config; import com.atguigu.boot.bean.Pet; import com.atguigu.boot.bean.User; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration //告诉SpringBoot这是一个配置类 == 配置文件 public class MyConfig { @Bean //给容器中添加组件,以方法名作为组件的id。 //返回类型就是组件类型,返回的值就是组件在容器中的实例 public User user01(){ return new User("zhangsan",18); } @Bean("tom") public Pet tomcatPet(){ return new Pet("tomcat"); } }检验注册组件成功: @SpringBootApplication(scanBasePackages="com.atguigu") public class MainApplication { public static void main(String[] args) { ConfigurableApplicationContext ioc = SpringApplication.run(MainApplication.class, args); String[] names = ioc.getBeanDefinitionNames(); for(String name:names){ System.out.println(name); } }@Bean @Component @Controller @Service @Repository @ComponentScan 以前讲过,略过不讲 2.2 @Import给容器中自动创建出这两个类型的组件,默认组件名字就是组件的全类名 @Import({User.class,DBHelper.class}) 2.3 @Conditional条件装配:满足Conditional指定的条件,则进行组件注入 @Bean("tom") public Pet tomcatPet(){ return new Pet("tomcat"); } @ConditionalOnBean(name="tom") @Bean public User user01(){ return new User("zhangsan",18); }实现@Conditional的接口: 2.4 @ImportResource导入Spring的配置文件 @ImportResource("classpath:beans.xml") 2.5 @ConfigurationProperties 配置绑定只有在容器中的组件才会拥有SpringBoot提供的强大功能 因此使用@ConfigurationProperties注解之前要先使用@Component @Data @AllArgsConstructor @NoArgsConstructor @Component @ConfigurationProperties(prefix = "mycar") public class Car { private String brand; private Integer price; }application.properties mycar.brand=byd mycar.price=100000a 2.6 EnableConfigurationProperties1.开启Car属性配置绑定功能 2.把这个组件(Car)自动注册到容器中(一般针对第三方组件) @EnableConfigurationProperties(Car.class) 三、自动配置【源码分析】 3.1 自动包规则原理@SpringBootApplication相当于: @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan@SpringBootConfiguration 其实就是一个@Configuration代表当前是一个配置类(核心配置类) @ComponentScan @ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class} ), @Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class} )} )有两个自定义的扫描器,指定扫描哪些包 @EnableAutoConfiguration @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration {}@EnableAutoConfiguration是@AutoConfigurationPackage和@Import的合成 @AutoConfigurationPackage:底层通过@Import引入Register类,给容器导入一个组件 @Import({Registrar.class}) public @interface AutoConfigurationPackage {}底层Register类:利用Reriigster给容器导入一系列组件 static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { Registrar() { } public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0])); } public Set determineImports(AnnotationMetadata metadata) { return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata)); } }对于方法的形参metadata:AnnotationMetadata即注解的元信息,代表注解标注在哪里、每一个属性值都是什么 可以验证introspectedClass为MainApplication,即注解 @AutoConfigurationPackage是标在MainApplication上的 这一行负责得到根据注解元信息得到包名的数组,传递给register方法,统一注册(统一注册包) AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));总的来说,@AutoConfigurationPackage利用Register给容器中导入一系列组件,然后将指定的一个包下的所有组件导入进来 自动包规则原理小结: 3.2 初始加载自动配置类对于@EnableAutoConfiguration组合注解: @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class})分析引入的AutoConfigurationImportSelector: ①利用getAutoConfigurationEntry(annotationMetadata)给容器中批量导入组件 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方法查看: ②对应的方法getCandidateConfigurations()可以获取所有的configurations,默认导入到容器中 List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);③利用工厂加载得到所有的组件 Map loadSpringFactories(ClassLoader classLoader)④从META-INF/spring.factories位置来加载一个文件 默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件 Enumeration urls = classLoader.getResources("META-INF/spring.factories");在依赖spring-boot-autoconfigure-2.7.6.jar 包中,有META-INF/spring.factories,里面写死了spring-boot一启动就要给容器加载的所有配置类 条件装配,按需加载: 虽然144个场景的所有自动配置启动的时候默认全部加载,但是最终会按需配置 底层有@Conditional对应的限制,比如: @ConditionalOnClass({Advice.class})总结: 3.3 自动配置流程①以AopAutoConfiguration为例 matchIfMissing即 即使没有配置value,也认为配置好了 由于EnableAutoConfiguration注解已经记录了所有需要加载的场景,所以prefix和name都是有的,故AopAutoConfiguration生效 @AutoConfiguration @ConditionalOnProperty( prefix = "spring.aop", name = {"auto"}, havingValue = "true", matchIfMissing = true ) public class AopAutoConfiguration {}AopAutoConfiguration类中有两个嵌套子类: Advice并没有导入,所以这个类配置不生效 @Configuration( proxyBeanMethods = false ) @ConditionalOnClass({Advice.class}) static class AspectJAutoProxyingConfiguration { AspectJAutoProxyingConfiguration() { } @Configuration( proxyBeanMethods = false ) @EnableAspectJAutoProxy( proxyTargetClass = true ) @ConditionalOnProperty( prefix = "spring.aop", name = {"proxy-target-class"}, havingValue = "true", matchIfMissing = true ) static class CglibAutoProxyConfiguration { CglibAutoProxyConfiguration() { } } @Configuration( proxyBeanMethods = false ) @EnableAspectJAutoProxy( proxyTargetClass = false ) @ConditionalOnProperty( prefix = "spring.aop", name = {"proxy-target-class"}, havingValue = "false" ) static class JdkDynamicAutoProxyConfiguration { JdkDynamicAutoProxyConfiguration() { } } }这个类的配置条件是@ConditionalOnMissingClass({"org.aspectj.weaver.Advice"}),我们正好没有这个类,所以AspectJAutoProxyingConfiguration不能生效 与之对应的ClassProxyingConfiguration生效 @Configuration( proxyBeanMethods = false ) @ConditionalOnMissingClass({"org.aspectj.weaver.Advice"}) @ConditionalOnProperty( prefix = "spring.aop", name = {"proxy-target-class"}, havingValue = "true", matchIfMissing = true ) static class ClassProxyingConfiguration { ClassProxyingConfiguration() { } @Bean static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() { return (beanFactory) -> { if (beanFactory instanceof BeanDefinitionRegistry) { BeanDefinitionRegistry registry = (BeanDefinitionRegistry)beanFactory; AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry); AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); } }; } }②以CacheAutoConfiguration为例 @AutoConfiguration( after = {CouchbaseDataAutoConfiguration.class, HazelcastAutoConfiguration.class, HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class} ) @ConditionalOnClass({CacheManager.class}) @ConditionalOnBean({CacheAspectSupport.class}) @ConditionalOnMissingBean( value = {CacheManager.class}, name = {"cacheResolver"} ) @EnableConfigurationProperties({CacheProperties.class}) @Import({CacheAutoConfiguration.CacheConfigurationImportSelector.class, CacheAutoConfiguration.CacheManagerEntityManagerFactoryDependsOnPostProcessor.class}) public class CacheAutoConfiguration {}通过运行主程序,检查现在有没有CacheAspectSupport这个组件:没有,所以CacheAutoConfiguration配置不生效 @SpringBootApplication(scanBasePackages="com.atguigu") public class MainApplication { public static void main(String[] args) { ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); String[] beanNamesForType = run.getBeanNamesForType(CacheAspectSupport.class); System.out.println("@@@ "+beanNamesForType.length); } }③以DispatcherServletAutoConfiguration为例 1.@ConditionalOnWebApplication判断当前是否为一个web应用,而且必须是原生Servlet的web应用。 因为SpringBoot2支持两种web模式开发,一种是响应式编程,一种是原生Servlet技术栈 @AutoConfiguration( after = {ServletWebServerFactoryAutoConfiguration.class} )要求在ServletWebServerFactoryAutoConfiguration类配置完毕后再配置DispatcherServletAutoConfiguration类 @AutoConfigureOrder(-2147483648) @AutoConfiguration( after = {ServletWebServerFactoryAutoConfiguration.class} ) @ConditionalOnWebApplication( type = Type.SERVLET ) @ConditionalOnClass({DispatcherServlet.class}) public class DispatcherServletAutoConfiguration {}2.这个DispatcherServletAutoConfiguration配置生效后,再看下面的配置类: @Configuration( proxyBeanMethods = false ) @Conditional({DispatcherServletAutoConfiguration.DispatcherServletRegistrationCondition.class}) @ConditionalOnClass({ServletRegistration.class}) @EnableConfigurationProperties({WebMvcProperties.class}) @Import({DispatcherServletAutoConfiguration.DispatcherServletConfiguration.class}) protected static class DispatcherServletRegistrationConfiguration {} @EnableConfigurationProperties({WebMvcProperties.class})EnableConfigurationProperties注解功能: 1.开启对应类属性配置绑定功能 2.把这个组件自动注册到容器中(一般针对第三方组件) 进入WebMvcProperties类,会把前缀为“spring.mvc”的属性与WebMvcProperties的属性一一对应绑定 @ConfigurationProperties( prefix = "spring.mvc" ) public class WebMvcProperties { private org.springframework.validation.DefaultMessageCodesResolver.Format messageCodesResolverFormat; private final WebMvcProperties.Format format = new WebMvcProperties.Format(); private boolean dispatchTraceRequest = false; private boolean dispatchOptionsRequest = true; private boolean ignoreDefaultModelOnRedirect = true; private boolean publishRequestHandledEvents = true; private boolean throwExceptionIfNoHandlerFound = false; private boolean logRequestDetails; private boolean logResolvedException = false; private String staticPathPattern = "/**"; private final WebMvcProperties.Async async = new WebMvcProperties.Async(); private final WebMvcProperties.Servlet servlet = new WebMvcProperties.Servlet(); private final WebMvcProperties.View view = new WebMvcProperties.View(); private final WebMvcProperties.Contentnegotiation contentnegotiation = new WebMvcProperties.Contentnegotiation(); private final WebMvcProperties.Pathmatch pathmatch = new WebMvcProperties.Pathmatch(); ……………… }3.底层已经把dispatcherServlet配置好了,所以我们不需要手动写文件配置 @Configuration( proxyBeanMethods = false ) @Conditional({DispatcherServletAutoConfiguration.DefaultDispatcherServletCondition.class}) @ConditionalOnClass({ServletRegistration.class}) @EnableConfigurationProperties({WebMvcProperties.class}) protected static class DispatcherServletConfiguration { protected DispatcherServletConfiguration() { } @Bean( name = {"dispatcherServlet"} ) 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; }4.相当于给容器中加入了文件上传解析器 给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找 规范化用户配置的文件上传解析器(SpringBoot从容器中找一个MultipartResolver这个类型的对象给你的resolver赋值,并返回回去) 如果你的文件上传解析器不叫multipartResolver,因为@Bean不标注名字默认以方法名作为组件名,所以SpringBoot会找到你配置的文件上传解析器,并规范命名作为组件 @Bean @ConditionalOnBean({MultipartResolver.class}) @ConditionalOnMissingBean( name = {"multipartResolver"} ) public MultipartResolver multipartResolver(MultipartResolver resolver) { return resolver; } }④以HttpEncodingAutoConfiguration为例 @AutoConfiguration @EnableConfigurationProperties({ServerProperties.class}) @ConditionalOnWebApplication( type = Type.SERVLET ) @ConditionalOnClass({CharacterEncodingFilter.class}) @ConditionalOnProperty( prefix = "server.servlet.encoding", value = {"enabled"}, matchIfMissing = true ) public class HttpEncodingAutoConfiguration {}处理乱码问题:SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了,则以用户的配置优先 处理方案: @Bean @ConditionalOnMissingBean @Bean @ConditionalOnMissingBean public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter(); filter.setEncoding(this.properties.getCharset().name()); filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST)); filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE)); return filter; }总结: SpringBoot先加载所有的自动配置类 xxxxAutoConfiguration 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值 数据从xxxxProperties里拿,xxxxPropeties和配置文件进行了绑定 生效的配置类就会给容器中装配很多组件 只要容器中有这些组件,相当于就有了对应的功能 只要用户有自己配置的组件,就以用户的优先 定制化配置: ①用户直接@Bean替换配置文件 ②用户直接去看这个组件时获取的配置文件的什么值 xxxxAutoConfiguration--->组件--->xxxProperties里面拿值--->application.properties 四、最佳实践 4.1 SpringBoot应用如何编写1.引入场景依赖 2.查看自动配置了哪些内容(选做) 自己分析,引入场景对应的自动配置一般都生效了 配置文件中debug=true开启自动配置报告。Negative(不生效)\Positive(生效) 3.可能需要修改: 参照文档修改配置项 自定义加入或者替换组件 自定义器 xxxxCustomizer 4.2 Lombok简化开发1.引入依赖 org.projectlombok lombok2.直接使用 @Data 生成已有属性的getter、setter方法 @ToString 生成toString方法 @NoArgsConstructor 无参构造器 @AllArgsConstructor 有参构造器 @EqualsAndHashCode @Slf4j 自动注入一个log属性 @Slf4j @RestController public class HelloController { @RequestMapping("/hello") public String handle01(){ log.info("请求进来了"); return "Hello SpringBoot2"; } } 4.3 dev-toolsdev-tools 热更新。每次修改代码以后,不用重启项目,只需要ctrl+f9就能实时生效 引入依赖: org.springframework.boot spring-boot-devtools true如果想要真正的热更新功能,可以使用付费插件jrebel 4.4 Spring Initializer(项目初始化向导)勾选需要的模块: 五、配置文件 5.1 yaml 5.1.1 简介YAML ,非常适合用来做以数据为中心的配置文件 YAML是 “YAML Ain't Markup Language(YAML不是一种标记语言)”的递归缩写。在开发这种语言的时候,YAML的意思其实是“Yet Another Markup Language”(仍是一种标记语言)。因此,YAML也被戏称为薛定谔的标记语言 5.1.2 基本语法key: value;kv之间有空格 大小写敏感 使用缩进表示层级关系 缩进不允许使用tab,只允许使用空格 缩进的空格数不重要,只要相同层级的元素左对齐即可 #表示注释 字符串无需加引号,如果要加,''与""表示字符串的内容,会被 转义/不转义 5.1.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 5.1.4 用法示例 @Data @ToString @Component @ConfigurationProperties(prefix="person") public class Person { private String userName; private Boolean boss; private Date birth; private Integer age; private Pet pet; private String[] interests; private List animal; private Map score; private Set salaries; private Map allPets; } @Data @AllArgsConstructor @NoArgsConstructor public class Pet { private String name; private Double weight; }yaml写法: person: userName: zhangsan boss: true birth: 2019/12/9 age: 18 # interests: [篮球,足球] interests: - 篮球 - 足球 - rap animal: [阿猫,阿狗] #score: # english: 80 # math: 90 score: {english:80,math:90} salaries: - 9999.44 - 9999.11 pet: name: 汪汪 weight: 65.78 allPets: sick: - {name:阿狗,weight:34.6} - name: 阿猫 weight: 88.77 - name: 阿虫 weight: 77.66 healthy: - {name:阿花,weight:56.65} - {name:阿草,weight:44.55}运行截图: 备注:单引号会将\n作为字符串输出,双引号会将\n作为换行输出 双引号不会转义,单引号会转义 5.2 自定义绑定的配置提示引入依赖: org.springframework.boot spring-boot-configuration-processor true按照命名规则,会将小驼峰改为短横线 比如userName改为user-name 这个插件只在开发阶段使用,所以SpringBoot打包的时候应该将它排除在外 org.springframework.boot spring-boot-maven-plugin 3.0.0 org.springframework.boot spring-boot-configuration-processor 六、Web场景 6.1 静态资源规则与定制化 6.1.1 静态资源目录静态资源目录: /static /public /resources /META-INF/resources 只要静态资源放在类路径的上述四个目录下,访问的路径就是当前项目的根路径/+静态资源名 如果动态请求路径和静态资源名字相同,先处理动态请求 原理:静态映射/** 请求进来,先去找Controller看能不能处理,不能处理的所有请求又都交给静态资源处理器。 静态资源就回去上述四个目录下寻找,能找到则返回;找不到则404 也可以自定义静态资源所在路径: spring: web: resources: static-locations: classpath:/haha/ 6.1.2 静态资源访问前缀静态资源默认没有前缀,但是为了方便今后的拦截器配置(拦截所有/**请求,并放行指定前缀请求),可以给静态资源加上前缀 spring: mvc: static-path-pattern: /resources/** 6.2 welcome和favicon功能 6.2.1 欢迎页支持静态资源路径下index.html 可以配置静态资源路径,但是不可以配置静态资源的访问前缀,否则会导致index.html不能被默认访问 controller处理/index 6.2.2 favicon功能支持只需要将favicon.ico 静态资源放在静态资源目录下,SpringBoot就可以自动地加载这个小图标 6.3 静态资源原理SpringBoot启动默认加载 xxxxAutoConfiguration类(自动配置类) SpringMVC功能的自动配置类: WebMvcAutoConfiguration(大多集中于此) 分析WebMvcAutoConfiguration类: ①兼容RESTful风格 @Bean @ConditionalOnMissingBean({HiddenHttpMethodFilter.class}) @ConditionalOnProperty( prefix = "spring.mvc.hiddenmethod.filter", name = {"enabled"} ) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); }②配置文件的相关数据和xxx进行了绑定 @Configuration( proxyBeanMethods = false ) @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class}) @EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class}) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {}WebMvcProperties.class跟配置文件spring.mvc进行了绑定 @ConfigurationProperties( prefix = "spring.mvc" ) public class WebMvcProperties {}WebProperties.class跟配置文件spring.web进行了绑定 @ConfigurationProperties("spring.web") public class WebProperties {}扩展知识:配置类只有一个有参构造器。而有参构造器所有参数的值都会从容器中确定。 参数分析: WebProperties webProperties :获取spring.web绑定的所有值的对象 WebMvcProperties webMvcProperties:获取spring.mvc绑定的所有值的对象 ListableBeanFactory beanFactory:构建ioc容器(Spring的beanFactory) ObjectProvider messageConvertersProvider:找到所有的HttpMesageConverters HttpMessageConverter可以把不同类型的body转为Java对象,也可以吧Java对象转为满足要求的body,在序列化与反序列化中有非常重要的作用。 ObjectProvider resourceHandlerRegistrationCustomizerProvider:找到资源处理器的自定义器 ObjectProvider dispatcherServletPath:相当于dispatcherServlet中处理的路径 ObjectProvider 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; } ②将解析器缓存起来 this.argumentResolverCache.put(parameter, resolver);③调用resolveArgument ===>ServletRequestMethodArgumentResolver负责解析Servlet API 7.9 Model、Map原理Map、Model(map、model里面的数据会被放在request的请求域中 request.setAttribute) RedirectAttributes(重定向携带数据) ServletResponse(原生的response) 用于测试的接口: @GetMapping("/params") public String testParam(Map map, Model model, HttpServletRequest request, HttpServletResponse response){ map.put("hello1","woyaosile"); model.addAttribute("hello2","wuwuwuwuwuwu"); request.setAttribute("message","byebyeworld"); Cookie cookie = new Cookie("c1","v1"); response.addCookie(cookie); return "forward:/success"; }直接运行:===>Model、Map、request都是可以给request域中放数据的 原理分析: ①对于Map类型,适配器是MapMethodProcessor 适配器判断方法如下: public boolean supportsParameter(MethodParameter parameter) { return Map.class.isAssignableFrom(parameter.getParameterType()) && parameter.getParameterAnnotations().length == 0; }进行参数解析: args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);如果参数是map,会在mavContainer中调用getModel方法 return mavContainer.getModel();getModel方法会返回一个ModelMap public ModelMap getModel() {而这个ModelMap就是defaultMap private final ModelMap defaultModel = new BindingAwareModelMap();===>如果是map类型的参数,会返回mavContainer.getModel()===>getModel方法返回一个new BindingAwareModelMap===>BindingAwareModelMap继承自ExtendedModelMap public class ExtendedModelMap extends ModelMap implements Model {ModelMap就是Map public class ModelMap extends LinkedHashMap {综上,BindingAwareModelMap既是Model也是Map ②Model类型 底层也是通过mavContainer.getModel()获取到值的 适配器:ModelMethodProcessor 解析参数: args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);resolveArgument底层实现: @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { Assert.state(mavContainer != null, "ModelAndViewContainer is required for model exposure"); return mavContainer.getModel(); }map和model都会把数据放到请求域中 ③Map和Model为什么可以将数据放到请求域中? 目标方法执行完成: 将所有的数据都放在ModelAndViewContainer:包含要去的页面地址View,还包含Model数据 处理返回结果(传入mavContainer,关注mavContainer有没有做什么操作): this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);处理返回值的具体实现:(处理视图) 返回的地址称为视图,数据称为模型。 public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { if (returnValue instanceof CharSequence) { String viewName = returnValue.toString(); mavContainer.setViewName(viewName); if (this.isRedirectViewName(viewName)) { mavContainer.setRedirectModelScenario(true); } } else if (returnValue != null) { String var10002 = returnType.getParameterType().getName(); throw new UnsupportedOperationException("Unexpected return type: " + var10002 + " in method: " + returnType.getMethod()); } }执行方法mav=invokeHandlerMethod(request,response,handlerMethod),并返回mav 执行方法 mappedHandler.applyPostHandle(processedRequest,response,mv) 处理派发结果:(处理最终结果) this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);render方法渲染页面: this.render(mv, request, response);解析视图名: view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);关键方法: renderMergeOutputModel()下的方法:把暴露模型作为请求域的属性 7.10自定义对象参数自动类型转换与格式化,可以级联封装 表单: 尚硅谷欢迎您~ 姓名: 年龄: 生日: 宠物姓名: 宠物年龄:接口不做处理,直接返回数据: @PostMapping("/saveuser") public Person saveuser(Person person){ return person; }页面可以接收到数据: 数据绑定:页面提交的请求数据(GET/POST)都可以和对象属性进行绑定 pojo封装过程: 处理器resolver:ServletModelAttributeMethodProcessor 第一步:适配器处理请求 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());返回调用handleInternal方法的结果: return this.handleInternal(request, response, (HandlerMethod)handler);第二步:调用invokeAndHandle方法 进入handleInternal方法,调用invokeHandlerMethod方法 mav = this.invokeHandlerMethod(request, response, handlerMethod);进入invokeHandlerMethod方法,通过invocableMethod调用invokeAndHandle方法 invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);第三步:确定请求参数 Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);通过getMethodArgumentValues得到所有请求参数 Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);内部处理每一个参数(resolveArgument方法) args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);内部逻辑:依次确定每个参数的处理器 while(var3.hasNext()) { HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next(); if (resolver.supportsParameter(parameter)) { result = resolver; this.argumentResolverCache.put(parameter, resolver); break; } }对于pojo,处理器是ServletModelAttributeMethodProcessor 第四步:找到处理器以后,开始解析参数 args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);真正拿到解析器: HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);开始处理参数: return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);===>resolveArgument内部逻辑: @Nullable 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); } }第五步:创建实例,并填入请求参数 创建一个空的Person实例 attribute = this.createAttribute(name, parameter, binderFactory, webRequest);如果bindingResult==null,调用方法createBinder(关键),创建一个Web数据绑定器(WebDataBinder) WebDataBinder:web数据绑定器,将请求参数的值绑定到指定的JavaBean里面 参数 attribute被封装到binder的target里面 WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);并且binder里面封装了124个converters 原理:要把请求的数据绑定到JavaBean里,又请求传过来的数据是HTTP,即超文本传输协议,默认认为万物皆文本,要把文本转换为String、Integer等类型就需要数据绑定器里的转换服务(conversionService),而绑定服务里就有绑定器converters,负责数据类型转换 小结:WebDataBinder利用它里面的Converters将请求数据转成指定的数据类型,再次封装到JavaBean中 绑定参数: this.bindRequestParameters(binder, webRequest);经过这一步,attribute和target里的属性都有了属性值 底层GenericConversionService:在设置每一个值的时候,找它里面所有的converters 具体哪个可以将这个数据类型(request带来的参数字符串)转换到指定的类型(JavaBean---Integer/String等) 未来我们可以给WebDataBinder里面放自己的Converter private static final class StringToNumber implements Converter 7.11 自定义Converter原理需求:将格式"阿猫,3"转换为宠物类型 宠物:配置类:addFormatters可以重写Converter和Formatter @Configuration(proxyBeanMethods = false) public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new Converter(){ @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; } }); } }运行结果: 八、响应处理 8.1 ReturnValueHandler原理引入web场景: org.springframework.boot spring-boot-starter-web(通过引入web场景)自动引入json场景: org.springframework.boot spring-boot-starter-json 3.0.4 compile而json底层依赖jackson做数据转换与处理,使得json处理特别简单 com.fasterxml.jackson.datatype jackson-datatype-jdk8 2.14.2 compile com.fasterxml.jackson.datatype jackson-datatype-jsr310 2.14.2 compile com.fasterxml.jackson.module jackson-module-parameter-names 2.14.2 compile只要给方法上标注@ResponseBody,就可以自动给前端返回json数据 @GetMapping("/test/person") @ResponseBody public Person getPerson(){ Person person=new Person(); person.setAge(28); person.setBirth(new Date()); person.setUserName("zhangsan"); return person; }原理剖析: 返回值解析器ReturnValueHandler 处理返回值: this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);关键方法selectHandler:寻找返回值处理器来处理返回值 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); } }判断处理器是否是异步的: 判断不是异步以后,来到真正的逻辑处理: 返回处理器判断是否支持这种类型返回值 supportsReturnType HandlerMethodReturnValueHandler handler; do { do { if (!var4.hasNext()) { return null; } handler = (HandlerMethodReturnValueHandler)var4.next(); } while(isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)); } while(!handler.supportsReturnType(returnType));最后,返回值处理器调用handleReturnValue进行处理 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); } }SpringMVC到底支持哪些返回值? ModelAndView Model View ResponseEntity ResponseBodyEmitter StreamingResponseBody HttpEntity HttpHeaders Callable DeferredResult ListenableFuture CompletionStage WebAsyncTask 有@ModelAttribute注解 返回值标注了@ResponseBody===>用RequestResponseBodyMethodProcessor public boolean supportsReturnType(MethodParameter returnType) { return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class); }总结 返回值解析器原理: 1.返回值处理器判断是否支持这种类型返回值 supportsReturnType 2.返回值处理器调用 handleReturnValue进行处理 3.RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody注解的 8.2 HTTPMessageConverter原理真正处理返回值的方法:(方法含义:使用消息转换器来进行写出操作) this.writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);1.利用 MessageConverters 进行处理并将数据写为json格式 i.内容协商(浏览器默认会以请求头的方式告诉服务器它能接收什么样的内容数据) q表示权重,q=0.9>q=0.8,所以浏览器优先接收页面 ii.服务器最终根据自己自身的能力,决定服务器能生产什么样内容类型的数据 iii.SpringMVC会挨个遍历所有容器底层的消息转换器MessageConverter,看谁能处理数据 得到MappingJackson2HttpMessageConverter可以将对象写为json ①拿到浏览器可接收的类型: ②拿到服务器能响应的类型: 消息转换器可以处理的情况(规范): 消息转换器的作用是看是否支持将此Class类型的对象转为MediaType类型的数据 例子:Person对象转为JSON或JSON转为Person 默认的MessageConverter: 0--只支持Byte类型 1--String 2--String 3--Resource 4--ResourceRegion 5--DOMSource.class\SAXSource.class\StAXSource.class\StreamSource.class\Source.class 6/7 --true只返回true,能处理所有类型 protected boolean supports(Class clazz) { return true; }当消息转换器为MappingJackson2HttpMessageConverter的时候: canWrite方法:浏览器适配、服务器支持 public boolean canWrite(@Nullable Type type, Class clazz, @Nullable MediaType mediaType) { return this.canWrite(clazz, mediaType); } protected boolean canWrite(@Nullable MediaType mediaType) { if (mediaType != null && !MediaType.ALL.equalsTypeAndSubtype(mediaType)) { Iterator var2 = this.getSupportedMediaTypes().iterator(); MediaType supportedMediaType; do { if (!var2.hasNext()) { return false; } supportedMediaType = (MediaType)var2.next(); } while(!supportedMediaType.isCompatibleWith(mediaType)); return true; } else { return true; } }判断成功后拿到body,即需要响应的内容: 可以看到body就是一个Person对象,想要以json的方式写出去 写之前会添加一些头部信息: this.addContentDispositionHeader(inputMessage, outputMessage);然后使用消息转换器的write方法将body写出: genericConverter.write(body, (Type)targetType, selectedMediaType, outputMessage); this.writeInternal(t, type, outputMessage);最后通过writeValue写出去,刷新流 objectWriter = this.customizeWriter(objectWriter, javaType, contentType); objectWriter.writeValue(generator, value); this.writeSuffix(generator, object); generator.flush();writeValue的参数value仍然是一个Person对象 在响应体中,让数据流以字符串形式展现: 最终MappingJackson2HttpMessageConverter把对象转为json数据(利用底层的jackson包中的objectMapper转换的) 8.3 内容协商原理根据客户端接受能力的不同,返回不同媒体类型的数据 以xml和json为例,先导入xml的依赖: com.fasterxml.jackson.dataformat jackson-dataformat-xml利用postman对接收数据格式进行限制和测试: 内容协商原理: 1.判断当前响应头中是否已经有确定的媒体类型(MediaType) 如果有,则用已经确定的媒体类型 MediaType contentType = outputMessage.getHeaders().getContentType();2.获取客户端(postman、浏览器)支持的内容类型 获取客户端Accept请求头字段 acceptableTypes = this.getAcceptableMediaTypes(request);3.获取服务器能支持(产生)的数据类型 遍历循环所有当前系统的MessageConverter,看谁支持操作这个对象 List producibleTypes = this.getProducibleMediaTypes(request, valueType, (Type)targetType);先获取到所有的转换器: 4.找到支持Person的converter,把converter支持的媒体类型统计出来 while(var6.hasNext()) { HttpMessageConverter converter = (HttpMessageConverter)var6.next(); if (converter instanceof GenericHttpMessageConverter && targetType != null) { if (((GenericHttpMessageConverter)converter).canWrite(targetType, valueClass, (MediaType)null)) { result.addAll(converter.getSupportedMediaTypes(valueClass)); } } else if (converter.canWrite(valueClass, (MediaType)null)) { result.addAll(converter.getSupportedMediaTypes(valueClass)); } }5.客户端需要application/xml。服务器端能处理的类型如下 6.双重循环,进行内容协商的最佳匹配(拿到浏览器想要的类型和我支持的类型) this.determineCompatibleMediaTypes(acceptableTypes, producibleTypes, compatibleMediaTypes); private void determineCompatibleMediaTypes(List acceptableTypes, List producibleTypes, List mediaTypesToUse) { Iterator var4 = acceptableTypes.iterator(); while(var4.hasNext()) { MediaType requestedType = (MediaType)var4.next(); Iterator var6 = producibleTypes.iterator(); while(var6.hasNext()) { MediaType producibleType = (MediaType)var6.next(); if (requestedType.isCompatibleWith(producibleType)) { mediaTypesToUse.add(this.getMostSpecificMediaType(requestedType, producibleType)); } } } }最佳匹配结果: 虽然匹配出来的结果很多,但selectMediaType一旦确定,就不会继续匹配了 while(var15.hasNext()) { MediaType mediaType = (MediaType)var15.next(); if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } //后略 }7.用支持将对象转为最佳匹配媒体类型的converter,调用它进行转换 8.4 基于请求参数的内容协商原理获取客户端(postman、浏览器)支持接收的内容类型(获取客户端Accept请求头字段) 关键方法: private List getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request)); }contentNegotiationManager 内容协商管理器,默认使用基于请求头的策略 HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型 获取请求头的Accept内容: String[] headerValueArray = request.getHeaderValues("Accept");尽管postman测试可以很方便的改变请求头中的Accept的内容,但是浏览器本身发请求,请求头很难改变 SpringBoot支持开启浏览器参数方式内容协议功能: spring: mvc: contentnegotiation: favor-parameter: true只要路由中加上format字段加以限定即可: http://localhost:8080/test/person?format=json 或者http://localhost:8080/test/person?format=xml 打开favor-parameter,内容协商管理器会新增一个基于参数的策略,而这个参数就是format 确定客户端接收什么样的内容类型: Parameter策略优先确定返回xml数据(获取请求头中的format值) 8.5 自定义MessageConverter实现多协议数据兼容:json/xml/x-guigu 0.@ResponseBody 响应数据出去,调用RequestResponseBodyMethodProcessor处理 1.Processor处理方法返回值。通过MessageConverter处理 2.所有MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写) 3.内容协商找到最终的messageConverter 需求: 1.浏览器发送请求直接返回xml 2.如果是ajax请求,返回json 3.如果是硅谷app发送请求,返回自定义协议数据 步骤: 1.添加自定义的MessageConverter进系统底层 2.系统底层就会统计出所有MessageConverter能操作哪些类型 3.客户端内容协商 导入了jackson处理xml的包,xml的converter就会自动进来 WebMvcAutoConfiguration类: configureMessageConverters方法:将所有获取到的converter全部加载进来 public void configureMessageConverters(List> messageConverters) { messageConverters.add(new ByteArrayHttpMessageConverter()); messageConverters.add(new StringHttpMessageConverter()); messageConverters.add(new ResourceHttpMessageConverter()); messageConverters.add(new ResourceRegionHttpMessageConverter()); messageConverters.add(new AllEncompassingFormHttpMessageConverter()); //后略 }按需加载: 导入jackson处理xml的包: if (jackson2XmlPresent) { builder = Jackson2ObjectMapperBuilder.xml(); if (this.applicationContext != null) { builder.applicationContext(this.applicationContext); } messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build())); }导入web场景: if (jackson2Present) { builder = Jackson2ObjectMapperBuilder.json(); if (this.applicationContext != null) { builder.applicationContext(this.applicationContext); } messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build())); }按需加载原理:判断对应的类有没有导入(以jackson处理xml的包为例) static { ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader(); romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader); jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader); jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader); jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader); jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader); gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader); kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader); kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader); kotlinSerializationProtobufPresent = ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader); }自定义MessageConverters实现: //自定义的Converter 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); } @Override public List getSupportedMediaTypes() { return MediaType.parseMediaTypes("application/x-guigu"); } @Override public Person read(Class> converters) { converters.add(new GuiguMessageConverter()); } }debug模式: 拿到内容协商协议 服务器中所有的Converter: 所有的converter合起来能生产的数据类型: 进行内容协商处理…… 运行截图: 8.6 浏览器与postman内容协商完全适配需求: 通过url:http://localhost:8080/test/person?format=gg 得到自定义格式的数据 代码实现: @Configuration(proxyBeanMethods = false) public class WebConfig implements WebMvcConfigurer { @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { Map mediaTypes=new HashMap(); //指定支持解析哪些参数对应的哪些媒体类型 mediaTypes.put("json",MediaType.APPLICATION_JSON); mediaTypes.put("xml",MediaType.APPLICATION_XML); mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu")); ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes); configurer.strategies(Arrays.asList(parameterStrategy)); }debug模式: 适配器策略===>客户端能发起的数据类型: 服务器端能产生的数据类型: 运行截图: 在用postman测试的时候,虽然通过参数方式携带数据可以返回对应的格式,但使用accept请求头的方式,返回值都是json数据格式 解决方法:添加请求头协商策略 HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy(); configurer.strategies(Arrays.asList(parameterStrategy,headerStrategy));完美解决: 小结: 有可能我们添加的自定义功能会覆盖默认的很多功能,导致一些默认的功能失效,需要注意 修改请求参数的名字的方式: ① parameterStrategy.setParameterName("ff"); ② mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu")); 九、视图解析 9.1 Thymeleaf初体验视图解析:SpringBoot默认不支持JSP,需要引入第三方模板引擎技术实现页面渲染 第一步:引入Thymeleaf启动场景 org.springframework.boot spring-boot-starter-thymeleaf此时SpringBoot已经自动配置好了thymeleaf @AutoConfiguration( after = {WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class} ) @EnableConfigurationProperties({ThymeleafProperties.class}) @ConditionalOnClass({TemplateMode.class, SpringTemplateEngine.class}) @Import({ReactiveTemplateEngineConfiguration.class, DefaultTemplateEngineConfiguration.class}) public class ThymeleafAutoConfiguration {官方自动配好的策略: ① 所有thymeleaf的配置值都在ThymeleafProperties ②配置好了SpringTemplateEngine ③配置好了ThymeleafViewResolver ===>我们只需要直接开发页面 ThymeleafProperties默认配置了前缀和后缀: public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_SUFFIX = ".html";第二步:引入Thymeleaf名称空间 第三步:编写代码测试 Title 哈哈 去百度 去百度ViewTestController @Controller public class ViewTestController { @GetMapping("/atguigu") public String atguigu(Model model){ //model中的数据会被放在请求域中 相当于 request.setAttribute() model.addAttribute("msg","你好,尚硅谷"); model.addAttribute("link","http://www.baidu.com"); return "success"; } }查看网页源代码可以发现@{}解析的路径会把写入的内容直接当路径 9.2 web实验 9.2.1 解决表单重复提交因为访问主页面的路径还是/login,一刷新就会导致表单的重复提交。为了解决这个问题,可以使用重定向 @Controller public class indexController { @GetMapping(value={"/","/login"}) public String loginPage(){ return "login"; } @PostMapping("/login") public String main(String username,String password){ //登录成功重定向到main.html return "redirect:/main.html"; } @GetMapping("/main.html") public String mainPage(){ return "main"; } } 9.2.2 完善登录的逻辑判断 @PostMapping("/login") public String main(User user, HttpSession session, Model model){ if(!StringUtils.isEmpty(user.getUsername()) && "123456".equals(user.getPassword())){ session.setAttribute("loginUser",user); //登录成功重定向到main.html return "redirect:/main.html"; }else { model.addAttribute("msg","账号密码错误"); return "login"; } } @GetMapping("/main.html") public String mainPage(HttpSession session,Model model){ Object loginUser = session.getAttribute("loginUser"); if(loginUser!=null){ return "main"; }else{ model.addAttribute("msg","未登录,请重新登录"); return "login"; } }thymeleaf的行内样式写法: [[${session.user.name}]] 9.2.3 抽取公共页面前置步骤: 编写接口: @Controller public class TableController { @GetMapping("/basic_table") public String basic_table(){ return "table/basic_table"; } @GetMapping("/dynamic_table") public String dynamic_table(){ return "table/dynamic_table"; } @GetMapping("/responsive_table") public String responsive_table(){ return "table/responsive_table"; } @GetMapping("/editable_table") public String editable_table(){ return "table/editable_table"; } }如果想要thymeleaf解析页面,需要为每个页面加上xmlns:th="http://www.thymeleaf.org"名称空间 并且修改超链接跳转: Data Tables Basic Table Advanced Table Responsive Table Edit Table头部抽取: //css样式等引入,略尾部js引入: 左侧菜单引入: 头部菜单引入: 9.2.4 遍历数据接口: @GetMapping("/dynamic_table") public String dynamic_table(Model model){ //表格内容的遍历 List users = Arrays.asList(new User("zhangsan", "234321"), new User("lisi", "453241"), new User("wangwu", "652524")); model.addAttribute("users",users); return "table/dynamic_table"; }页面动态数据加载: # 用户名 密码 [[${user.password}]]效果图: 9.3 视图解析器与视图解析源码分析第一步:寻找当前这个请求由哪个handler处理===>indexController mappedHandler = this.getHandler(processedRequest);第二步:寻找处理器的适配器===>RequestMappingHandlerAdapter HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());第三步:适配器调用方法 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());第四步:执行目标方法 mav = this.invokeHandlerMethod(request, response, handlerMethod);第五步:确定参数和返回值的处理器 if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); }第六步:实现重定向后,对返回值进行解析:找到返回值处理器 this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);第七步:选择合适的返回值处理器 HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);ViewNameMethodReturnValueHandler可以处理返回值为空或为字符序列方法 ===>返回值处理器为ViewNameMethodReturnValueHandler public boolean supportsReturnType(MethodParameter returnType) { Class paramType = returnType.getParameterType(); return Void.TYPE == paramType || CharSequence.class.isAssignableFrom(paramType); }第八步:处理器开始处理返回值 handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);①如果返回值是字符序列,就将它转换为字符串 ②将得到的字符串放入mavContainer中 ③判断返回值是否需要重定向 protected boolean isRedirectViewName(String viewName) { return PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:"); }④如果需要重定向,则打开重定向传感器 public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { if (returnValue instanceof CharSequence) { String viewName = returnValue.toString(); mavContainer.setViewName(viewName); if (this.isRedirectViewName(viewName)) { mavContainer.setRedirectModelScenario(true); } } else if (returnValue != null) { String var10002 = returnType.getParameterType().getName(); throw new UnsupportedOperationException("Unexpected return type: " + var10002 + " in method: " + returnType.getMethod()); } }扩展:如果方法没有返回名字,会调用applyDefaultViewName this.applyDefaultViewName(processedRequest, mv);如果视图为空,会返回默认的跳转页 private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception { if (mv != null && !mv.hasView()) { String defaultViewName = this.getDefaultViewName(request); if (defaultViewName != null) { mv.setViewName(defaultViewName); } } }第九步:处理派发结果 this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);===>视图解析原理流程: 1.目标方法处理的过程中,所有的数据都会被放在ModelAndViewContainer中。包括数据和视图地址 2.方法的参数是一个自定义类型对象(从请求参数中确定的),把它放入ModelAndViewContainer中 3.任何目标方法执行完成以后都会返回ModelAndView对象(有数据和视图地址) 4.processDispatchResult 处理派发结果(页面该如何响应) 1. this.render(mv, request, response); 进行页面渲染 i.根据方法的String返回值得到View对象 view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);View实际上是一个接口,定义了render函数(即页面渲染的逻辑) void render(@Nullable Map model, HttpServletRequest request, HttpServletResponse response) throws Exception;调用resolveViewName方法得到视图解析器===>所有的视图解析器尝试是否能根据当前返回值得到View对象 2.通过ContentNegotiatingViewResolver得到了最佳匹配视图:RedirectView redirect:/main.html ==>Thymeleaf视图解析器===> new了一个RedirectView并返回 3.ContentNegotiatingViewResolver里面包含了下面所有的视图解析器,内部还是利用了下面所有视图解析器得到试图对象 4.视图对象调用render方法进行渲染 view.render(mv.getModelInternal(), request, response);①合并Model对象设置的内容 Map mergedModel = this.createMergedOutputModel(model, request, response);②重定向页面 this.renderMergedOutputModel(mergedModel, this.getRequestToExpose(request), response);RedirectView 如何渲染: a.获取目标url地址 b.response.sendRedirect(encodedURL);重定向到指定的地址 protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) throws IOException { String targetUrl = this.createTargetUrl(model, request); targetUrl = this.updateTargetUrl(targetUrl, model, request, response); RequestContextUtils.saveOutputFlashMap(targetUrl, request, response); this.sendRedirect(request, response, targetUrl, this.http10Compatible); }视图解析: 返回值以forward:开始==>new InternalResourceView(forwardUrl);==>转发 request.getRequestDispatcher(path).forward(request,response); 返回值以redirect:开始==>new RedirectView()==>render就是重定向 十、拦截器 10.1 登录检查与静态资源放行拦截器接口:HandlerInterceptor preHandle:目标方法执行之前 postHandle:目标方法执行完成以后 afterCompletion:页面渲染完成之后 public interface HandlerInterceptor { default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { } default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { } }拦截器使用步骤: ①编写一个拦截器,实现HandlerInterceptor接口 ②拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors) ③指定拦截规则(如果是拦截所有,静态资源也会被拦截) 第一步: public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //登录检查逻辑 HttpSession session = request.getSession(); Object loginUser = session.getAttribute("loginUser"); if(loginUser!=null){ //放行 return true; } //拦截住==>未登录,跳转至登录页 session.setAttribute("msg","请先登录"); request.getRequestDispatcher("/").forward(request,response); return false; } }第二步: @Configuration public class AdminWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") //拦截所有资源,包括静态资源 .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行登录界面 } } 10.2 拦截器的执行时机与原理第一步:找到合适的处理器 mappedHandler = this.getHandler(processedRequest);第二步:在执行拦截器方法前先执行preHandle方法 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; }第三步:对于所有的拦截器方法,先顺序执行preHandle方法 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); if (!interceptor.preHandle(request, response, this.handler)) { this.triggerAfterCompletion(request, response, (Exception)null); return false; } } return true; }如果preHandle方法执行失败,则倒序执行每个拦截器的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); } } }第四步:执行目标方法 第五步:目标方法执行完毕后,执行postHandle方法 mappedHandler.applyPostHandle(processedRequest, response, mv);倒序执行所有拦截器的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); } }第六步:开始页面渲染 this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);拦截器原理: 1. 根据当前请求,找到可以处理请求的handler以及handler的所有拦截器(即HandlerExecutionChain) 2. 先来顺序执行所有拦截器的preHandle方法 ①如果当前拦截器preHandle返回为true,则执行下一个拦截器的preHandle ②如果当前拦截器返回为false,直接倒序执行所有已经执行了的afterCompletion 3.如果任何一个拦截器执行失败(返回false),则直接跳出不执行目标方法(ha.handle()) 4.所有拦截器都返回为true,则执行目标方法 5.倒序执行所有拦截器的postHandle方法 6.前面的步骤有任何异常都会直接触发afterCompletion方法 7.页面成功渲染完成以后,也会倒序触发afterCompletion方法 十一、文件上传 11.1 单文件与多文件上传的使用文件上传时,表单必须做到: 添加enctype="multipart/form-data" 提交方式method="post" 文件上传输入框type="file" 多文件上传需要使用关键字mutipart 邮箱 名字 头像 生活照 Check me out Submit接口: 使用@RequestPart来接收传过来的文件 使用MultipartFile作为单文件类型,使用MultipartFile[]作为多文件类型 先验证是否接收到上传的文件: @Controller @Slf4j public class FormTestController { @GetMapping("/form_layouts") public String form_layouts(){ return "form_layouts"; } @PostMapping("/upload") public String upload(@RequestParam("email")String email, @RequestParam("username")String username, @RequestPart("headerImg")MultipartFile headerImg, @RequestPart("photos")MultipartFile[] photos){ log.info("上传的信息:email={},username={},headerImg={},photos={}",email,username,headerImg.getSize(),photos.length); return "main"; } }将上传的文件保存到文件服务器: @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={},photos={}",email,username,headerImg.getSize(),photos.length); if(!headerImg.isEmpty()){ String originalFilename = headerImg.getOriginalFilename(); headerImg.transferTo(new File("D:\\itcast\\"+originalFilename)); } if(photos.length>0){ for(MultipartFile photo:photos){ String originalFilename = photo.getOriginalFilename(); photo.transferTo(new File("D:\\itcast\\"+originalFilename)); } } return "main"; }自定义修改可上传的文件大小: spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=100MB 11.2 文件上传参数解析器文件上传自动配置类 MultipartAutoConfiguration 自动配置好了 StandardServletMultipartResolver【文件上传解析器】 第一步:判断当前请求是否为文件上传请求 processedRequest = this.checkMultipart(request);①使用文件上传解析器 判断并封装、返回文件上传请求 if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {判断:isMultipart方法 public boolean isMultipart(HttpServletRequest request) { return StringUtils.startsWithIgnoreCase(request.getContentType(), this.strictServletCompliance ? "multipart/form-data" : "multipart/"); }解析: return this.multipartResolver.resolveMultipart(request); public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { return new StandardMultipartHttpServletRequest(request, this.resolveLazily); }第二步:适配器调用handle方法 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());②参数解析器来解析请求中的文件内容,封装成MultipartFile ③将request中文件信息封装成一个Map 十二、异常处理 12.1 SpringBoot默认错误处理机制 默认规则:默认情况下,SpringBoot提供/error处理所有错误的映射 对于机器客户端,它将生成JSON响应,其中包含错误、HTTP状态和异常消息的详细信息;对于浏览器客户端,响应一个"whitelabel"错误视图,以HTML格式呈现相同的数据 机器客户端(用Postman模拟): 浏览器客户端: 要对其进行自定义,添加View解析为error要完全替换默认行为,可以实现ErrorController,并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容 error下的4xx/5xx页面会被自动解析 给模板引擎添加两个页面,会响应自定义内容 12.2 底层组件功能分析 进行异常处理的自动配置:ErrorMvcAutoConfiguration 容器中的组件: ①类型:DefaultErrorAttributes-->id:errorAttributes public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {②类型:BasicErrorController-->id:basicErrorController 处理默认 /error路径的请求 @Controller @RequestMapping({"${server.error.path:${error.path:/error}}"}) public class BasicErrorController extends AbstractErrorController {==>BasicErrorController中定义了要么响应页面,要么响应JSON数据 @RequestMapping( produces = {"text/html"} ) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = this.getStatus(request); Map model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = this.resolveErrorView(request, response, status, model); return modelAndView != null ? modelAndView : new ModelAndView("error", model); } @RequestMapping public ResponseEntity error(HttpServletRequest request) { HttpStatus status = this.getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity(status); } else { Map body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL)); return new ResponseEntity(body, status); } }组件:View-->id:error @Bean( name = {"error"} ) @ConditionalOnMissingBean( name = {"error"} ) public View defaultErrorView() { return this.defaultErrorView; }组件:BeanNameViewResolver(视图解析器),按照返回的视图名作为组件的id去容器中找View对象 @Bean @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(2147483637); return resolver; }如果想要返回页面,就会找error视图【StaticView】(默认是一个白页) 容器中的组件:类型:DefaultErrorViewResolver-->id:conventionErrorViewResolver 自动映射: static { Map views = new EnumMap(Series.class); views.put(Series.CLIENT_ERROR, "4xx"); views.put(Series.SERVER_ERROR, "5xx"); SERIES_VIEWS = Collections.unmodifiableMap(views); }解析的时候会自动加上error/前缀 如果发生错误,会以HTTP的状态码作为视图页地址(viewName),找到真正的页面 error.viewName.html private ModelAndView resolve(String viewName, Map model) { String errorViewName = "error/" + viewName; TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model); }容器中的组件:DefaultErrorAttributes 定义错误页面中可以包含哪些数据 public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { Map errorAttributes = this.getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE)); if (!options.isIncluded(Include.EXCEPTION)) { errorAttributes.remove("exception"); } if (!options.isIncluded(Include.STACK_TRACE)) { errorAttributes.remove("trace"); } if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) { errorAttributes.remove("message"); } if (!options.isIncluded(Include.BINDING_ERRORS)) { errorAttributes.remove("errors"); } return errorAttributes; } 12.3 异常处理流程1.执行目标方法,目标方法运行期间有任何异常,都会被catch,并且用dispatchException封装 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());2.进入视图解析流程(页面如何渲染) this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);3.处理handler发生的异常,处理完成返回ModelAndView mv = this.processHandlerException(request, response, handler, exception);遍历所有的HandlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】 系统默认的异常解析器: HandlerExceptionResolver接口的方法resolveException: 拿到原生的request,response,并返回一个视图ModelAndView public interface HandlerExceptionResolver { @Nullable ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex); }①DefaultErrorAttributes先来处理异常。把异常信息保存到request域,并返回null public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { this.storeErrorAttributes(request, ex); return null; } private void storeErrorAttributes(HttpServletRequest request, Exception ex) { request.setAttribute(ERROR_INTERNAL_ATTRIBUTE, ex); }②默认处理器中都不能处理异常,故异常会被抛出去 ③如果没有任何处理器能处理,底层就会发送/error请求,并被底层的BasicErrorController处理 i.发送/error请求 ii.解析错误视图:遍历所有的ErrorViewResolver,看谁能解析 iii.默认的DefaultViewResolver,作用是把响应状态码作为错误页的地址拼接成error/500.html等 iv.模板引擎最终响应这个页面 12.4 几种异常处理原理 12.4.1 定制错误处理逻辑自定义错误页 error/404.html、error/5xx.html 有精确的错误状态码页面就精确匹配,没有就找4xx.html、5xx.html;如果都没有则触发白页 @ControllerAdvice+@ExceptionHandler处理全局异常 @ResponseStatus+自定义异常 Spring底层的异常,如参数类型转换异常 自定义实现HandlerExceptionResolver处理异常 12.4.2 @ControllerAdvice+@ExceptionHandler处理全局异常底层ExceptionHandlerExceptionResolver提供处理zhichi @Slf4j @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler({ArithmeticException.class,NullPointerException.class}) public String handleArithException(Exception e){ log.error("异常是:{}",e); return "login"; } } 12.4.3 @ResponseStatus+自定义异常底层是 ResponseStatusExceptionResolver支持 把responsestatus注解的信息组装ModelAndView返回 底层调用response.sendError(statusCode,resolvedReason);即tomcat发送/error @ResponseStatus(value= HttpStatus.FORBIDDEN,reason="用户数量太多") public class UserTooManyException extends RuntimeException{ public UserTooManyException(){} public UserTooManyException(String message){ super(message); } }自定义异常解析器: @Order(value=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(); } } 十三、原生组件注入(Servlet、Filter、Listener) 13.1 原生注解与Spring方法注入使用原生注解: ①主程序类使用@ServletComponentScan @ServletComponentScan(basePackages = "com.example.webdemo.servlet")②编写自己的Servlet 注意使用@WebServlet注解 Servlet: @WebServlet(urlPatterns="/my") public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("666"); } }效果:直接响应,没有Spring的拦截器 Filter: @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 servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { log.info("MyFilter工作"); filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { log.info("MyFilter销毁"); } }Listener: @Slf4j @WebListener public class MyServletContextListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { log.info("MyServletContextListener监听到项目初始化完成"); } @Override public void contextDestroyed(ServletContextEvent sce) { log.info("MyServletContextListener监听到项目初始化销毁"); } }Spring方法注入: 使用返回值ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean @Configuration public class MyRegistryConfig { public ServletRegistrationBean myServlet(){ MyServlet myServlet = new MyServlet(); return new ServletRegistrationBean(myServlet,"/my","/my02"); } public FilterRegistrationBean myFilter(){ MyFilter myFilter = new MyFilter(); //拦截和myServlet定义的路径一样的路径 return new FilterRegistrationBean(myFilter,myServlet()); } public FilterRegistrationBean myFilterTest(){ MyFilter myFilter = new MyFilter(); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter); filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*")); return filterRegistrationBean; } public ServletListenerRegistrationBean myListener(){ MyServletContextListener myServletContextListener = new MyServletContextListener(); return new ServletListenerRegistrationBean(myServletContextListener); } } 13.2 DispatchServlet注入原理容器中自动配置了DispatcherServlet,属性绑定到WebMvcProperties,对应的配置项是spring.mvc 通过ServletRegistrationBean 把DispatcherServlet配置进来 默认servlet路径映射的是/路径 Tomcat处理Servlet原理: 多个Servlet都能处理到同一层路径,精确优先原则 13.3 嵌入式Servlet容器---切换web服务器与定制化默认支持的webServer Tomcat、Jetty、Undertow ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory,并引导创建服务器 原理: SpringBoot应用启动发现当前是Web应用==>web场景导入tomact web应用会创建一个web版的ioc容器 ==>ServletWebServerApplicationContext ServletWebServerApplicationContext启动的时候寻找ServletWebServerFactory(Servlet的web服务器工厂==>Servlet的web服务器) SpringBoot底层默认有很多WebServer工厂: TomcatServletWebServerFactory、JettyServletWebServerFactory、UndertowServletWebServerFactory 底层会直接有一个自动配置类:ServletWebServerFactoryAutoConfiguration ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类) ServletWebServerFactoryConfiguration配置类根据动态判断系统中到底导入了哪个web服务器包(默认web-starter导入tomcat包),所以容器中就有一个tomcat的wei服务器工厂 TomcatServletWebServerFactory创建出Tomcat服务器并启动,TomcatWebServer的构造器拥有初始化方法 内嵌服务器就是手动把启动服务器的代码调用(Tomcat核心jar包存在) 切换服务器: org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-tomcat org.springframework.boot spring-boot-starter-undertow 13.4 SpringBoot定制化组件的几种方式原理分析套路: 场景starter --- xxxAutoConfiguration --- 导入xxx组件 --- 绑定xxxProperties --- 绑定配置文件项 定制化的常见方式: ①修改配置文件 ②编写自定义的配置类 xxxConfiguation + @Bean替换、增加容器中默认组件;视图解析器 ③web应用 实现WebMvvcConfigurer 即可定制化web功能 ④@EnableWebMvc + WebMvcConfigurer——@Bean 可以全面接管SpringMVC,所有规则全部自己重新配置,实现定制和扩展功能 十四、数据访问 14.1 数据库场景的自动配置分析与整合测试1.导入JDBC场景 org.springframework.boot spring-boot-starter-jdbc2.分析自动配置 HikariCP:优秀的数据源连接池 spring-tx:事务管理 分析:为什么导入JDBC场景,官方不导入驱动? 理由:官方不知道我们接下来要操作什么数据库 导入mysql数据库: 想要修改版本: 1.直接依赖引入具体的版本(maven的就近依赖原则) 2.重新声明仲裁版本(maven属性的就近原则) mysql mysql-connector-java 8.0.26自动配置的类: DataSourceAutoConfiguration:数据源的自动配置 修改数据源相关的配置:spring.datasource 数据库连接池的配置:是自己容器中没有DataSource才自动配置的 底层配置好的数据源是Hikari DataSourceTransactionManagerAutoConfiguration:事务管理的配置 JdbcTemplateAutoConfiguration:JdbcTemplate的自动配置,可以对数据库进行crud 可以修改这个配置项@ConfigurationProperties(prefix="spring.jdbc")来修改jdbcTemplat JndiDataSourceAutoConfiguration:jndi的自动配置 XADataSourceAutoConfiguration:分布式事务的自动动配置 导入数据源配置: spring: datasource: url: jdbc:mysql://localhost:3306/company username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver测试: @SpringBootTest class WebDemoApplicationTests { @Autowired JdbcTemplate jdbcTemplate; @Test void contextLoads() { List maps = jdbcTemplate.queryForList("select * from class"); maps.forEach(System.out::println); } } 14.2 自定义方式整合Druid数据源整合第三方技术的两种方式: ①自定义 ②找starter 第一步:引入依赖 com.alibaba druid 1.1.12第二步:配置 @Configuration public class MyDataSourceConfig { @ConfigurationProperties("spring.datasource") @Bean public DataSource dataSource(){ return new DruidDataSource(); } }此时,springboot底层的数据源就变成了Druid 怎么使用Druid内置监控页面? /*配置druid的监控页功能*/ @Bean public ServletRegistrationBean statViewServlet(){ return new ServletRegistrationBean(new StatViewServlet(),"/druid/*"); }开启SQL监控: druidDataSource.setFilters("stat");配置内置监控中的Web和Spring的关联监控: @Bean public FilterRegistrationBean webStatFilter(){ FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter((Filter) new WebStatFilter()); filterRegistrationBean.setUrlPatterns(Arrays.asList("/*")); filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; } druidDataSource.setFilters("stat,wall"); 14.3 druid数据源starter整合方式引入官方的starter: com.alibaba druid-spring-boot-starter 1.1.9自动配置: 扩展配置项 spring.datasource.druid DruidSpringAopConfiguration.class: 监控SpringBean 配置项:spring.datasource.druid.aop-patterns DruidStatViewServletConfiguration.class 监控页的配置 配置项:spring.datasource.druid.stat-view-servlet 默认开启 DruidWebStatFilterConfiguration.class web监控配置 配置项:spring.datasource.druid.web-stat-filter 默认开启 DruidFilterConfiguration.class 所有Druid自己的filter的配置 配置application.yml spring: datasource: url: jdbc:mysql://localhost:3306/db1 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver druid: stat-view-servlet: enabled: true login-username: admin login-password: 123456 resetEnable: false web-stat-filter: enabled: true urlPattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' aop-patterns: com.example.webdemo.* filters: stat,wall filter: stat: slow-sql-millis: 1000 log-slow-sql: true enabled: true wall: enabled: true config: update-allow: false 14.4 整合mybatis---配置版引入依赖: org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.2配置模式: 全局配置文件 SqlSessionFactory:自动配置好了 SqlSession:自动配置了SqlSessionTemplate组合了SqlSession Mapper:只要我们写的操作MyBatis的接口标注了@Mapper注解 就会被自动扫描进来 MybatisAutoConfiguration: @EnableConfigurationProperties({MybatisProperties.class})==>MyBatis配置项绑定类 可以修改配置文件中mybatis开始的所有配置,来修改MyBaits配置 @ConfigurationProperties( prefix = "mybatis" ) public class MybatisProperties {配置application.yaml #配置mybatis的规则 mybatis: config-location: classpath:mybatis/mybatis-config.xml mapper-locations: classpath:mapper/*.xml配置mybatis核心文件: 编写代码: 实体类: @Data @NoArgsConstructor @AllArgsConstructor public class Dept { private int deptno; private String dname; private String loc; }mapper接口: @Mapper public interface DeptMapper { Dept getDept(int deptno); }xml对应接口: select * from dept where deptno=#{deptno}测试: @Controller public class IndexController { @Autowired DeptMapper deptMapper; @GetMapping("/dept") @ResponseBody public Dept getByDeptno(){ Dept dept = deptMapper.getDept(10); return dept; } }注意:可以不写全局配置文件,而所有的全局配置文件的配置都放在configuration下 #配置mybatis的规则 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true type-aliases-package: com.atguigu.test.pojo 十五、单元测试 15.1 JUnit5 简介SpringBoot 2.2.0 版本开始引入JUnit5 作为单元测试的默认库 JUnit5 = Junit Platform + JUnit Jupiter + Junit Vintage
SpringBoot整合JUnit以后: 编写测试方法:@Test注解标注即可(对应junit5版本的注解org.junit.jupiter下的注解) JUnit类具有Spring的功能,可以使用@Autowired、@Transactional等 15.2 JUnit5 常用注解@Test:表示方法是测试方法,但是与JUnit4的@Test不同,它的职责非常单一,不能声明任何属性,扩展的测试将会由Jupiter提供额外测试 @ParameterizedTest:表示方法是参数化测试 @RepeatedTest:表示方法可重复执行 @DisplayName:为测试类或者测试方法设置展示名称 @BeforeEach:表示在每个单元测试之前执行 @AfterEach:表示在每个单元测试之后执行 @BeforeAll:表示在所有单元测试之前执行 @AfterAll:表示在所有单元测试之后执行 @Tag:表示单元测试类别,类似于JUnit4中的@Categories @Disabled:表示测试类或测试方法不执行,类似于JUnit4de@Ignore @Timeout:表示测试方法运行如果超过了指定时间将会返回错误 @ExtendWith:为测试类或方法提供扩展类引用 @SpringBootTest是一个复合注解,内含@ExtendWith @SpringBootTest class TestApplicationTests { @Autowired JdbcTemplate jdbcTemplate; @Test @DisplayName("测试displayname注解") void testDisplayName(){ System.out.println(1); System.out.println(jdbcTemplate); } @Test @Disabled @DisplayName("测试方法2") void test2(){ System.out.println(2); } @Test @Timeout(value=500,unit= TimeUnit.MILLISECONDS) void testTimeout() throws InterruptedException { Thread.sleep(600); } @Test @RepeatedTest(5) void testRepeated(){ System.out.println("repeated"); } @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("所有测试已经结束了..."); } } 15.3 断言机制断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是org.junit.jupiter.api.Assertions的静态方法。 可以检察业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告 Junit5内置的断言可以分为以下几类:
2.数组断言 通过assertArrayEquals方法来判断两个对象或原始类型的数组是否相等 @Test void testArray(){ Assertions.assertArrayEquals(new int[]{1,2},new int[]{1,2}); }测试通过~ 3.组合断言 assertAll方法接受多个org.junit.jupiter.api.Executable函数式接口的实例作为要验证的断言,可以通过lambda表达式很容易的提供这些断言 assertAll 全部的断言成功,才算成功 @Test void testAll(){ assertAll("test",()->assertTrue(true&&true), ()->assertEquals(1,1)); }4.异常断言 Assertions.assertThrows(),配合函数式编程使用 断定业务逻辑一定出现异常 @Test void testException(){ assertThrows(ArithmeticException.class,()->{int i=10/0;},"业务逻辑居然正常运行了"); }5.快速失败 希望业务逻辑快速失败 @Test void testFail(){ if(2==2){ fail("测试失败"); } } 15.4 前置条件(assumptions)JUnit 5中的前置条件(assumptions 假设)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行中止。 前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要 @Test void testAssumptions(){ Assumptions.assumeTrue(true,"结果不是true"); System.out.println(111); } 15.5 嵌套测试JUnit 5可以通过Java中的内部类和@Nested注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。 在内部类中可以使用@BeforeEach和@AfterEach注解,而且嵌套的层次没有限制 在嵌套测试的情况下,外层的Test不能驱动内层的Before(After)Each/All之类的方法提前/延后执行 内层的Test可以驱动外层的Before(After)Each/All @SpringBootTest public class TestStackDemo { Stack 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()); } } } } 15.6 参数化测试 @SpringBootTest public class TestAStackDemo { Stack stack; @ParameterizedTest @DisplayName("参数化测试") @ValueSource(ints = {1,2,3,4,5}) void testParameterized(int i){ System.out.println(i); } @ParameterizedTest @DisplayName("方法式测试") @MethodSource("stringProvider") void testParameterized2(String str){ System.out.println(str); } static Stream stringProvider(){ return Stream.of("apple","banana","orange"); } }十六、指标监控 16.1 SpringBoot Actuator 与 EndPoint SpringBoot Actuator: 简介 未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能 1.x与2.x的区别
使用方法 第一步:引入场景 org.springframework.boot spring-boot-starter-actuator第二步:访问http://localhost:8080/actuator/** 暴露Endpoints 支持的暴露方式: HTTP:默认值暴露health和info Endpoint JMX:默认暴露所有的Endpoint 除了health和info,剩下的Endpoint都应该进行保护访问,如果引入SpringSecurity,则会默认配置安全访问规则 management是所有actuator的配置 management: endpoints: enabled-by-default: true #默认开启所有监控端点 web: exposure: include: '*' #以web方式暴露所有端点 16.2 禁用与开启最常用的Endpoint: Health:监控状况 Metrics:运行时指标 Loggers:日志记录 Health Endpoint健康检查端点,我们一般用于云平台。平台会定时检查应用的健康状况,我们就需要Health Endpoint为平台返回当前应用的一系列组件健康状况的集合 重要的几点: health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告 很多健康检查默认已经自动配置好了,比如数据库、redis等 可以很容易的添加自定义的健康检查机制 management是所有actuator的配置 management.endpoint.端点名.xxx 对某个端点的具体配置 management: endpoints: enabled-by-default: true #默认开启所有监控端点 web: exposure: include: '*' #以web方式暴露所有端点 endpoint: health: show-details: alwaysMetrics Endpoint 提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或push(被动获取)方式得到 通过Metrics对接多种监控系统 简化核心Metrics开发 添加自定义Metrics或者扩展已有Metrics 开启与禁用endpoints: 默认所有的Endpoint 除了shutdown以外的都是开启的 需要开启或者禁用某个Endpoint。配置模式为: management.endpoint..enabled=true management: endpoints: enabled-by-default: false #默认开启所有监控端点 web: exposure: include: '*' #以web方式暴露所有端点 endpoint: health: show-details: always enabled: true info: enabled: true beans: enabled: true 16.3 定制Endpoint定制health: @Component public class MyComHealthIndicator extends AbstractHealthIndicator { @Override protected void doHealthCheck(Health.Builder builder) throws Exception { Map map=new HashMap(); if(1==1){ // builder.up();//健康 builder.status(Status.UP); map.put("count",1); map.put("ms",100); }else{ builder.status(Status.OUT_OF_SERVICE); map.put("err","连接超时"); map.put("ms",3000); } builder.withDetail("code",100) .withDetails(map); } }定制info: 注意要开启management.info.env.enabled=true,否则收到的info页面为空{} management: info: env: enabled: true endpoints: enabled-by-default: false #默认开启所有监控端点 web: exposure: include: '*' #以web方式暴露所有端点 endpoint: health: show-details: always enabled: true beans: enabled: true info: enabled: true info: appName: boot-admin appVersion: 1.0.2
也可以通过自定义组件定制info: @Component public class ExampleInfoContributor implements InfoContributor { @Override public void contribute(Info.Builder builder) { builder.withDetail("msg","你好") .withDetail("hello","atguigu") .withDetails(Collections.singletonMap("world","666")); } }定制Metrics: 构造器注入 @Service public class DeptService { @Autowired DeptMapper deptMapper; Counter counter; public DeptService(MeterRegistry meterRegistry){ counter = meterRegistry.counter("deptService.getDept.count"); } public Dept getDept(int deptno){ counter.increment(); return deptMapper.getDept(deptno); } }也可以直接给容器中放一个MeterBinder 自定义端点不需要手动开启 @Component @Endpoint(id="myservice") public class MyServiceEndPoint { @ReadOperation public Map getDockerInfo(){ //端点的读操作 return Collections.singletonMap("docker","docker started..."); } @WriteOperation public void stopDocker(){ System.out.println("docker stopped..."); } } 16.4 Boot Admin Server第一步:导入依赖 de.codecentric spring-boot-admin-starter-server 2.3.1第二步:在启动类上加注解@EnableAdminServer @EnableAdminServer @SpringBootApplication public class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } }第三步:配置收集数据的url spring: boot: admin: client: url: http://localhost:8080后会有期 |
今日新闻 |
推荐新闻 |
专题文章 |
CopyRight 2018-2019 实验室设备网 版权所有 |