概念性的东西见: https://www.yuque.com/atguigu/springboot/rmxq85
需要环境: jdk8 & 兼容java14, maven 3.3+, idea 219.1.2
maven设置:
去到 maven 的配置文件中修改添加:
<mirrors> <mirror> <id>nexus-aliyun</id> <mirrorOf>central</mirrorOf> <name>Nexus aliyun</name> <url>http://maven.aliyun.com/nexus/content/groups/public</url> </mirror> </mirrors> <profiles> <profile> <id>jdk-1.8</id> <activation> <activeByDefault>true</activeByDefault> <jdk>1.8</jdk> </activation> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion> </properties> </profile> </profiles>
Step 1: 创建 maven 工程
Step 2: 在pom文件引入依赖:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.4.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
step 3: 在 src/main/java 下创建包和主类(也叫主程序应用)
// 告诉 springboot 这是一个springboot 应用 // 主程序应用 @SpringBootApplication public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } }
step 4: 在另一个包下创建 Controller 并编写业务代码
/* 可以用 RestController 代替 @ResponseBody // 代表这个类返回的是字符串 @Controller */ @RestController public class HelloController { @RequestMapping("/hello") public String handle01() { return "hello SpringBoot"; } }
step 5: 直接运行主应用的 main 方法
step 6: 在 resource 下创建 application.properties 文件, 该文件可以在全局进行配置, 详细见官方文档
step 7: 简化部署
通过加入插件便可以生成jar包, 直接在目标服务器执行即可
(命令行输入: 如 java -jar springboot01-1.0-SNAPSHOT.jar) 注意取消cmd的快速编辑模式
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
在 target/下 SpringBoot 帮我们打包成了 jar 包, 里面有我们运行时所需的第三方 jar 包
依赖管理 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.4.RELEASE</version> </parent> 它的父项目 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.4.RELEASE</version> </parent> 几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制, 所以我们无需声明版本号
1、引入依赖默认都可以不写版本
2、引入非版本仲裁的 jar,要写版本号。
自定义修改版本号
1、查看spring-boot-dependencies里面规定当前依赖的版本 2、在当前项目里面重写配置 <properties> <mysql.version>5.1.43</mysql.version> </properties>
Staters
一组依赖的集合描述, 是 maven 的依赖特性
1、见到很多 spring-boot-starter-* : *是某种场景 2、只要引入starter,这个场景的所有常规需要的依赖我们都自动引入 3、SpringBoot所有支持的场景 https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter 4、见到的 *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器 5、所有场景启动器最底层的依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.3.4.RELEASE</version> <scope>compile</scope> </dependency>
自动配置好 Tomcat
自动配置好 SpringMVC
引入了 SpringMVC 的全套组件
如何查看 IOC 容器有那些组件:
主程序应用下的主方法: public static void main(String[] args) { // 返回 IOC 容器 ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); // 查看容器组件名 String[] names = run.getBeanDefinitionNames(); for(String s : names) { System.out.println(s); } run.getBean("yang", User.class); // 容器中只有一个组件 单实例 }
自动配置好了 SpringMVC 常用组件 (功能)
自动配置好 Web 常见功能, 如: 字符编码问题
默认的包结构
@SpringBootApplication 等同于 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan("com.atguigu.boot") // 可以在这里指定扫描路径
1. @Configuration
Spring 添加组件的方法: 创建类, 创建 spring 的配置文件, 在 xml 文件中编写<bean>
SpringBoot 添加组件的方法:
配置类组件之间无依赖关系用 Lite 模式加速容器启动过程,减少判断
配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用 Full 模式
/* 1. @Bean 是给容器注册组件, 默认单实例 2. 配置类本身也是一个组件 3. proxyBeanMethods: 代码 bean 的方法 默认为true Full(proxyBeanMethods = true) (保证每个 @Bean 方法被调用多少次返回的组件都是单实例的) Lite(proxyBeanMethods = false) (每个 @Bean 方法被调用多少次返回的组件都是新创建的, 容器不保存代理对象) 组件依赖必须使用 Full 模式默认。其他默认是 Lite 模式 */ @Configuration(proxyBeanMethods = true) // 告诉 SpringBoot 这是一个配置类 默认单实例 public class MyConfig { // Full: 外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象 @Bean // 给容器添加组件, 以方法名作为组件的 id, 返回类型就是组件类型, 返回的值就是组件在容器中的实例 public User user01() { return new User("01", 20); } @Bean("yang") public User yangyu() { return new User("yangyu", 20); } }
2. @Component @Controller @Service @Repository (适用于标注自己写的类) @Bean (适用于标注第三方包里面的组件)
3. @ComponentScan @Import (快速给容器导入组件)
@Import({User.class, DBHelper.class}) 给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名 @Import({User.class, DBHelper.class}) @Configuration(proxyBeanMethods = false) public class MyConfig { }
ImportSelector: 是一个接口, 需要自定义类进行实现, 需要重写的方法可以返回需要导入的组件的全类名数组, 需要返回哪些类的全类名就通过自己的逻辑去得到全类名然后返回一个全类名字符串数组 注意: 在config类中需要 @import 你实现的这个接口的类, 这样就会自动到容器中生成对应的实例, 你实现的这个接口的类不会在容器中生成实例
ImportBeanDefinitionRegistrar: 也是一个接口, 需要实现. 把所有需要添加到容器的 bean 调用BeanDefinitionRegistry.registerBeanDefinition 手工注册
**FactorBean (Spring 提供): **也是一个接口, 需要实现.
4. @Conditional
条件装配: 满足指定条件则进行组件注入
ConditionOnBean: 当容器中有这个组件时才…
@ConditionalOnBean(name = {"user01", "myConfig"})
1. @ImportResource
原生xml: resouces/beans.xml
@ImportResource("classpath:beans.xml") public class MyConfig {}
读取 properties 文件中的内容,并且把它封装到JavaBean中,以供随时使用;
原生java做法:
public class getProperties { public static void main(String[] args) throws FileNotFoundException, IOException { Properties pps = new Properties(); pps.load(new FileInputStream("a.properties")); Enumeration enum1 = pps.propertyNames();//得到配置文件的名字 while(enum1.hasMoreElements()) { String strKey = (String) enum1.nextElement(); String strValue = pps.getProperty(strKey); System.out.println(strKey + "=" + strValue); //封装到JavaBean。 } } }
1. @ConfigurationProperties
xxx.properties:
mycar.aaa=3 mycar.bbb=好
Car类:
@Component // 只有容器中的组件才有SpringBoot提供的强大功能 @ConfigurationProperties(prefix = "mycar") public class Car { private Integer aaa; private String bbb; }
**2. @EnableConfigurationProperties + @ConfigurationProperties **
如果是第三方类, 就在配置类上@EnableConfigurationProperties(Car.class)
和上一种区别: 要么用 @Component 加入到容器中, 要么用@EnableConfigurationProperties
@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication{}
1. @SpringBootConfiguration
其中的 @Configuration 代表当前类是一个配置类
2. @Component
指定要扫描哪些包, 参照 Spring 注解
3. EnableAutoConfiguration
@AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration {}
@Import(AutoConfigurationPackages.Registrar.class) // 给容器中导入一个组件 public @interface AutoConfigurationPackage {} // 利用Registrar给容器中导入一系列组件 // 将指定的一个包下的所有组件导入进来: MainApplication 所在包下
1、利用 getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件 2、调用 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类 3、利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) 得到所有的组件 4、从 META-INF/spring.factories 位置来加载一个文件。 默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件 spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories 文件里面写死了spring-boot一启动就要给容器中加载的所有配置类
虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration
按照条件装配规则(@Conditional),最终会按需配置。
@Bean @ConditionalOnBean(MultipartResolver.class) // 容器中有这个类型组件 @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) // 容器中没有这个名字 multipartResolver 的组件 public MultipartResolver multipartResolver(MultipartResolver resolver) { // 给 @Bean 标注的方法传入了对象参数,这个参数的值就会从容器中找。 // SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范 // Detect if the user has created a MultipartResolver but named it incorrectly return resolver; } 给容器中加入了文件上传解析器;
SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先
@Bean @ConditionalOnMissingBean public CharacterEncodingFilter characterEncodingFilter() {} 如果想自己配: public CharacterEncodingFilter filter(){ return null; }
总结:
SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿. xxxProperties和配置文件进行了绑定
生效的配置类就会给容器中装配很多组件
只要容器中有这些组件,相当于这些功能就有了
定制化配置
xxxxxAutoConfiguration —> 组件 —> xxxxProperties里面拿值 ----> application.properties
简化 JavaBean 开发
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> 并在 idea 搜索安装 lombok 插件
@NoArgsConstructor @AllArgsConstructor @Data @ToString @EqualsAndHashCode ============================================ // 简化日志开发 @Slf4j public class HelloController { @RequestMapping("/hello") public String handle01(){ log.info("请求进来了...."); return "123" } }
热更新 (实则是自动重启)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>
项目或者页面修改以后:Ctrl+F9
IDEA 快速创建 SpringBoot 应用, 可以勾选你需要的场景, 并自动生成 pom 文件
仍然是一种标记语言
非常适合用来做以数据为中心的配置文件
基本语法:
key: value;k v之间有空格
大小写敏感
使用缩进表示层级关系
缩进不允许使用tab,只允许空格
缩进的空格数不重要,只要相同层级的元素左对齐即可
'#'表示注释
字符串无需加引号,如果要加,’'与""表示字符串内容 会被 转义/不转义
数据类型:
字面量:单个的、不可再分的值。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
示例:
# yaml表示以上对象 person: userName: zhangsan boss: false birth: 2019/12/12 20:12:33 # private Date birth; age: 18 pet: # private Pet pet name: tomcat weight: 23.4 interests: [篮球,游泳] # private String[] interests; animal: # private List<String> animal; - jerry - mario score: # private Map<String, Object> score; english: first: 30 second: 40 third: 50 math: [131,140,148] chinese: {first: 128,second: 136} salarys: # private Set<Double> salarys; - 9999.98 - 9999.99 allPets: # private Map<String, List<Pet>> allPets; sick: - {name: tom} - {name: jerry,weight: 47} - name: 阿虫 weight: 77.77 health: [{name: mario,weight: 47}]
自定义的类和配置文件绑定一般没有提示
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <!-- 防止打包的时候打进去 --> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build>
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)
The auto-configuration adds the following features on top of Spring’s defaults:
ContentNegotiatingViewResolver
and BeanNameViewResolver
beans.
Converter
, GenericConverter
, and Formatter
beans.
Converter,GenericConverter,Formatter
HttpMessageConverters
(covered later in this document).
HttpMessageConverters
(后来我们配合内容协商理解原理)MessageCodesResolver
(covered later in this document).
MessageCodesResolver
(国际化用)index.html
support.Favicon
support (covered later in this document).
Favicon
ConfigurableWebBindingInitializer
bean (covered later in this document).
ConfigurableWebBindingInitializer
, (DataBinder负责将请求数据绑定到JavaBean上)If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own
@Configuration
class of typeWebMvcConfigurer
but without@EnableWebMvc
.不用@EnableWebMvc注解。使用
@Configuration
+WebMvcConfigurer
自定义规则
If you want to provide custom instances of
RequestMappingHandlerMapping
,RequestMappingHandlerAdapter
, orExceptionHandlerExceptionResolver
, and still keep the Spring Boot MVC customizations, you can declare a bean of typeWebMvcRegistrations
and use it to provide custom instances of those components.声明
WebMvcRegistrations
改变默认底层组件
If you want to take complete control of Spring MVC, you can add your own
@Configuration
annotated with@EnableWebMvc
, or alternatively add your own@Configuration
-annotatedDelegatingWebMvcConfiguration
as described in the Javadoc of@EnableWebMvc
.使用
@EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC
只要静态资源放在类路径下: /static
or /public
or /resources
or /META-INF/resources
访问 : 当前项目根路径/ + 静态资源名
原理: 静态映射/**
请求进来,先去找 Controller 看能不能处理, 不能处理的所有请求又都交给静态资源处理器. 静态资源也找不到则响应 404 页面
改变默认的静态资源地址
spring: mvc: static-path-pattern: /res/** resources: static-locations: [classpath:/haha/]
spring.resources.static-locations 用于告诉Spring Boot应该在何处查找静态资源文件,这是一个列表性的配置,查找文件时会依赖于配置的先后顺序依次进行,默认的官方配置如下:
spring.resources.static-locations=classpath:/static,classpath:/public,classpath:/resources,classpath:/META-INF/resources
为了防止在实际开发中静态资源被拦截器拦截, 会在静态资源路径上加一个前缀, 让拦截器放行指定前缀的路径的所有请求
当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找
webjar
自动映射 /webjars/**
https://www.webjars.org/
<dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.5.1</version> </dependency>
访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径
spring: # mvc: # static-path-pattern: /res/** 这个会导致welcome page功能失效 resources: static-locations: [classpath:/haha/]
favicon.ico 放在静态资源目录下即可, 且static-path-pattern: /res/** 会导致 welcome page 功能失效
SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
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 {}
配置文件的相关属性和xxx进行了绑定。WebMvcProperties==spring.mvc ResourceProperties==spring.resources
@Configuration( proxyBeanMethods = false ) @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class}) @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class, WebProperties.class}) @Order(0)
1. 只有一个有参构造器
// 有参构造器所有参数的值都会从容器中确定 // ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象 // WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象 // ListableBeanFactory beanFactory Spring的beanFactory // HttpMessageConverters 找到所有的HttpMessageConverters // ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。========= // DispatcherServletPath // ServletRegistrationBean 给应用注册Servlet、Filter.... public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) { this.resourceProperties = resourceProperties; this.mvcProperties = mvcProperties; this.beanFactory = beanFactory; this.messageConvertersProvider = messageConvertersProvider; this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); this.dispatcherServletPath = dispatcherServletPath; this.servletRegistrations = servletRegistrations; }
2. 资源处理的默认规则
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); return; } addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> { registration.addResourceLocations(this.resourceProperties.getStaticLocations()); if (this.servletContext != null) { ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION); registration.addResourceLocations(resource); } }); }
spring: # mvc: # static-path-pattern: /res/** resources: add-mappings: false 禁用所有静态资源规则
public static class Resources { 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; }
3. 欢迎页处理规则
EnableWebMvcConfiguration 类下
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, Resource welcomePage, String staticPathPattern) { //要用欢迎页功能,必须是/** if (welcomePage != null && "/**".equals(staticPathPattern)) { logger.info("Adding welcome page: " + welcomePage); setRootViewName("forward:index.html"); } else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { // 调用Controller /index logger.info("Adding welcome page template: index"); setRootViewName("index"); } }
xxxMapping
Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
核心Filter;HiddenHttpMethodFilter
用法: 表单method=post,隐藏域 _method=put
SpringBoot中手动开启
mvc: hiddenmethod: filter: enable: true
扩展:如何把_method 这个名字换成我们自己喜欢的。
// 测试代码 //@RequestMapping(value = "/user",method = RequestMethod.GET) @GetMapping("/user") public String getUser(){ return "GET-张三"; } //@RequestMapping(value = "/user",method = RequestMethod.POST) @PostMapping("/user") public String saveUser(){ return "POST-张三"; } //@RequestMapping(value = "/user",method = RequestMethod.PUT) @PutMapping("/user") public String putUser(){ return "PUT-张三"; } //@RequestMapping(value = "/user",method = RequestMethod.DELETE) @DeleteMapping("/user") public String deleteUser(){ return "DELETE-张三"; } @Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); } //创建配置类并在类中自定义filter @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); methodFilter.setMethodParam("_m"); return methodFilter; }
Rest 原理(表单提交要使用REST的时候)
Rest 使用客户端工具,
观察 DispatcherServlet 的继承树
SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet 的 doDispatch() 开始
// 找到当前请求使用哪个Handler(Controller的方法)处理 mappedHandler = getHandler(processedRequest); // HandlerMapping:处理器映射。/xxx -> xxxx
RequestMappingHandlerMapping:保存了所有 @RequestMapping 和 handler 的映射规则.
所有的请求映射都在 HandlerMapping 中.
SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping . 访问 / 能访问到 index.html;
SpringBoot 自动配置了默认的 RequestMappingHandlerMapping
请求进来,挨个尝试所有的 HandlerMapping 看是否有被请求的信息
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { for (HandlerMapping mapping : this.handlerMappings) { HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; } } } return null; }
1. 注解
@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
@PathVariable, @RequestParam, @RequestBody, @CookieValue, @RequestHeader :
// car/2/owner/zhangsan 路径变量 @GetMapping("/car/{id}/owner/{username}") public Map<String,Object> getCar(@PathVariable("id") Integer id, @PathVariable("username") String name, @PathVariable Map<String,String> pv, @RequestHeader("User-Agent") String userAgent, @RequestHeader Map<String,String> header, @RequestParam("age") Integer age, @RequestParam("inters") List<String> inters, @RequestParam Map<String,String> params, @CookieValue("_ga") String _ga, @CookieValue("_ga") Cookie cookie){ Map<String,Object> map = new HashMap<>(); // map.put("id",id); // map.put("name",name); // map.put("pv",pv); // map.put("userAgent",userAgent); // map.put("headers",header); map.put("age",age); map.put("inters",inters); // inters 是链表, 所以得到的只有一个数据 map.put("params",params); map.put("_ga",_ga); System.out.println(cookie.getName()+"===>"+cookie.getValue()); return map; } @PostMapping("/save") public Map postMethod(@RequestBody String content){ Map<String,Object> map = new HashMap<>(); map.put("content",content); return map; }
@requestAttribute
@GetMapping("/goto") public String goToPage(HttpServletRequest request){ request.setAttribute("msg","成功了..."); request.setAttribute("code",200); return "forward:/success"; //转发到 /success请求 } @GetMapping("/params") public String testParam(Map<String,Object> map, Model model, HttpServletRequest request, HttpServletResponse response){ map.put("hello","world666"); model.addAttribute("world","hello666"); request.setAttribute("message","HelloWorld"); Cookie cookie = new Cookie("c1","v1"); response.addCookie(cookie); return "forward:/success"; } @ResponseBody @GetMapping("/success") public Map success(@RequestAttribute(value = "msg",required = false) String msg, @RequestAttribute(value = "code",required = false)Integer code, HttpServletRequest request){ Object msg1 = request.getAttribute("msg"); Map<String,Object> map = new HashMap<>(); Object hello = request.getAttribute("hello"); Object world = request.getAttribute("world"); Object message = request.getAttribute("message"); map.put("reqMethod_msg",msg1); map.put("annotation_msg",msg); map.put("hello",hello); map.put("world",world); map.put("message",message); return map; }
@MatrixVariable 与 UrlPathHelper
/cars/{path}?xxx=xxx&aaa=ccc queryString 查询字符串。@RequestParam;
/cars/sell;low=34;brand=byd,audi,yd ;矩阵变量
在页面开发中, cookie 被禁用, 那么 session 里面的内容如何使用
首先, 我们可以在 session 保存数据, 且每一个人都有一个 jsessionid, 这个jsessionid 会被保存在 cookie 中, 且 cookie 每次请求都会被携带, 所以服务器会通过 cookie 的 jsessionid 来查找
我们可以通过矩阵变量的方式来解决这个问题: url重写: /abc;jsessionid=xxx 把cookie的值使用矩阵变量的方式进行传递
第一个分号前面是路径, 分号后面是矩阵变量, 多个矩阵变量通过分号区分
//1. 语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd //2. SpringBoot 默认是禁用了矩阵变量的功能 // 手动开启: 原理: 对于路径的处理: UrlPathHelper 进行解析. 其中的 removeSemicolonContent (移除分号内容) 用来支持矩阵变量的 //3. 矩阵变量必须有 url 路径变量才能被解析 @GetMapping("/cars/{path}") public Map carsSell(@MatrixVariable("low") Integer low, @MatrixVariable("brand") List<String> brand, @PathVariable("path") String path){ Map<String,Object> map = new HashMap<>(); map.put("low",low); map.put("brand",brand); map.put("path",path); return map; } // /boss/1;age=20/2;age=10 @GetMapping("/boss/{bossId}/{empId}") public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge, @MatrixVariable(value = "age",pathVar = "empId") Integer empAge){ Map<String,Object> map = new HashMap<>(); map.put("bossAge",bossAge); map.put("empAge",empAge); return map; }
手动开启矩阵变量:
@Configuration(proxyBeanMethods = false) public class WebConfig /*implements WebMvcConfigurer*/ { //1、WebMvcConfigurer定制化SpringMVC的功能 @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除;后面的内容。矩阵变量功能就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } } }
2. Servlet API
WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
ServletRequestMethodArgumentResolver 可以解析以上的部分参数
@Override public boolean supportsParameter(MethodParameter parameter) { Class<?> paramType = parameter.getParameterType(); return (WebRequest.class.isAssignableFrom(paramType) || ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType) || HttpSession.class.isAssignableFrom(paramType) || (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) || Principal.class.isAssignableFrom(paramType) || InputStream.class.isAssignableFrom(paramType) || Reader.class.isAssignableFrom(paramType) || HttpMethod.class == paramType || Locale.class == paramType || TimeZone.class == paramType || ZoneId.class == paramType); }
3. 复杂参数
Map、**Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、**Errors/BindingResult、RedirectAttributes (重定向携带数据)、ServletResponse (response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
Map<String,Object> map, Model model, HttpServletRequest request 都是可以给request域中放数据, request.getAttribute();
Map, Model 类型的参数, 会返回 mavContainer.getModel(); (BindingAwareModelMap 类型, 是Model, 也是Map)
Model 存的是数据, View 存的是视图
4. 自定义对象参数
可以自动类型转换与格式化,可以级联封装
/** * 姓名: <input name="userName"/> <br/> * 年龄: <input name="age"/> <br/> * 生日: <input name="birth"/> <br/> * 宠物姓名:<input name="pet.name"/><br/> * 宠物年龄:<input name="pet.age"/> */ @Data public class Person { private String userName; private Integer age; private Date birth; private Pet pet; } @Data public class Pet { private String name; private String age; }
5. POJO 封装过程
ServletModelAttributeMethodProcessor 参数处理见下文
HandlerMapping 中找到能处理请求的 Handler(Controller.method())
为当前Handler 找一个适配器 HandlerAdapter; RequestMappingHandlerAdapter
适配器执行目标方法并确定方法参数的每一个值
1. HandlerAdapter
0 - 支持方法上标注 @RequestMapping
1 - 支持函数式编程的
2. 执行目标方法
// Actually invoke the handler. // DispatcherServlet 类中的 doDispatch 方法 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
mav = invokeHandlerMethod(request, response, handlerMethod); // 执行目标方法 // ServletInvocableHandlerMethod 真正执行目标方法 Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); // 获取方法的参数值 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
3. 参数解析器 - HandlerMethodArgumentResolver
确定将要执行的目标方法的每一个参数的值是什么
SpringMVC目标方法能写多少种参数类型. 取决于参数解析器
参数解析器接口:
4. 返回值处理器
5. 确定目标方法每一个参数的值
============InvocableHandlerMethod============ protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } 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) { continue; } 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 ex) { // Leave stack trace for later, exception may actually be resolved and handled... if (logger.isDebugEnabled()) { String exMsg = ex.getMessage(); if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { logger.debug(formatArgumentError(parameter, exMsg)); } } throw ex; } } return args; }
6. 遍历找到哪个参数解析器能支持解析这个参数
@Nullable private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { if (resolver.supportsParameter(parameter)) { result = resolver; this.argumentResolverCache.put(parameter, result); break; } } } return result; }
7. 解析这个参数的值
调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可
8. 自定义类型参数 封装 POJO
ServletModelAttributeMethodProcessor 这个参数解析器支持
是否为简单类型:
public static boolean isSimpleValueType(Class<?> type) { return (Void.class != type && void.class != type && (ClassUtils.isPrimitiveOrWrapper(type) || Enum.class.isAssignableFrom(type) || CharSequence.class.isAssignableFrom(type) || Number.class.isAssignableFrom(type) || Date.class.isAssignableFrom(type) || Temporal.class.isAssignableFrom(type) || URI.class == type || URL.class == type || Locale.class == type || Class.class == type)); }
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中
GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean – Integer)
如: 文件上传: byte – > file
@FunctionalInterface public interface Converter<S, T>
未来我们可以给 WebDataBinder 里面放自己的 Converter;
private static final class StringToNumber<T extends Number> implements Converter<String, T>
自定义 Converter:
//1、WebMvcConfigurer定制化SpringMVC的功能 @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除;后面的内容。矩阵变量功能就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new Converter<String, Pet>() { @Override public Pet convert(String source) { // 啊猫,3 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; } }); } }; }
将所有的数据都放在 ModelAndViewContainer; 包含要去的页面地址View. 还包含Model数据
3.5.1 处理派发结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
InternalResourceView: @Override protected void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { // Expose the model object as request attributes. exposeModelAsRequestAttributes(model, request); // Expose helpers as request attributes, if any. exposeHelpers(request); // Determine the path for the request dispatcher. String dispatcherPath = prepareForRendering(request, response); // Obtain a RequestDispatcher for the target resource (typically a JSP). RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath); if (rd == null) { throw new ServletException("Could not get RequestDispatcher for [" + getUrl() + "]: Check that the corresponding file exists within your web application archive!"); } // If already included or response already committed, perform include, else forward. if (useInclude(request, response)) { response.setContentType(getContentType()); if (logger.isDebugEnabled()) { logger.debug("Including [" + getUrl() + "]"); } rd.include(request, response); } else { // Note: The forwarded resource is supposed to determine the content type itself. if (logger.isDebugEnabled()) { logger.debug("Forwarding to [" + getUrl() + "]"); } rd.forward(request, response); } }
暴露模型作为请求域属性 // Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception { // model中的所有数据遍历挨个放在请求域中 model.forEach((name, value) -> { if (value != null) { request.setAttribute(name, value); } else { request.removeAttribute(name); } }); }
数据响应
1. jackson.jar + @ResponseBody
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
web 场景自动引入了 json 场景
返回值解析器
try { this.returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); }
@Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { // 通过这个方法来决定 HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); if (handler == null) { throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName()); } handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); }
================= RequestResponseBodyMethodProcessor 类============== @Override 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); }
返回值解析器原理
SpringMVC 支持返回哪些返回值 由 returnValueHandlers 决定
ModelAndView Model View ResponseEntity ResponseBodyEmitter StreamingResponseBody HttpEntity HttpHeaders Callable DeferredResult ListenableFuture CompletionStage WebAsyncTask 有 @ModelAttribute 且为对象类型的 @ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
HTTPMessageConverter 原理
MessageConverter 规范
HttpMessageConverter: 看是否支持将此 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 - MultiValueMap
7 - true
8 - true
9 - 支持注解方式xml处理的
最终 MappingJackson2HttpMessageConverter 把对象转为 JSON (利用底层的 jackson 的 objectMapper 转换的)
根据客户端接收能力不同,返回不同媒体类型的数据
1. 引入 xml 依赖
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency>
2. postman 测试返回 json 和 xml
只需要改变请求头中Accept字段. Http协议中规定的,告诉服务器本客户端可以接收的数据类型: application/xml
3. 开启浏览器参数方式协商功能
spring: contentnegotiation: favor-parameter: true #开启请求参数内容协商模式
http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
确定客户端接收什么样的内容类型;
内容协商原理
实现多协议数据兼容。json、xml、x-guigu
@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理
Processor 处理方法返回值。通过 MessageConverter 处理
所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
内容协商找到最终的 messageConverter;
SpringMVC的什么功能。一个入口给容器中添加一个 WebMvcConfigurer
@Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new GuiguMessageConverter()); } } } ============ GuiguMessageConverter ============ public class GuiguMessageConverter implements HttpMessageConverter<Person> { @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 * @return */ @Override public List<MediaType> getSupportedMediaTypes() { return MediaType.parseMediaTypes("application/x-guigu"); } @Override public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return null; } @Override public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { //自定义协议数据的写出 String data = person.getUserName()+";"+person.getAge()+";"+person.getBirth(); //写出去 OutputStream body = outputMessage.getBody(); body.write(data.getBytes()); } } ================== WebConfig ================= /** * 自定义内容协商策略 * @param configurer */ @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { //Map<String, MediaType> mediaTypes Map<String, MediaType> mediaTypes = new HashMap<>(); mediaTypes.put("json",MediaType.APPLICATION_JSON); mediaTypes.put("xml",MediaType.APPLICATION_XML); mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu")); //指定支持解析哪些参数对应的哪些媒体类型 ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes); // parameterStrategy.setParameterName("gg"); HeaderContentNegotiationStrategy headeStrategy = new HeaderContentNegotiationStrategy(); configurer.strategies(Arrays.asList(parameterStrategy,headeStrategy)); }
有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?(提示: 参照SpringBoot官方文档web开发内容协商章节)
视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染
1. 视图解析
1. 视图解析原理流程
1、目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址
2、任何目标方法执行完成以后都会返回 ModelAndView (数据和视图地址)
3、processDispatchResult() 处理派发结果 (页面该如何响应)
1、render(mv, request, response); 进行页面渲染逻辑
1、根据一些值(ViewName, )得到 View 对象【定义了页面的渲染逻辑】
1、所有的视图解析器尝试是否能根据当前返回值得到 View 对象
2、得到了 redirect:/main.html --> Thymeleaf new RedirectView()
3、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象
4、view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作
RedirectView 如何渲染【重定向到一个页面】
1、获取目标url地址
2、response.sendRedirect(encodedURL);
ThymeleafViewResolver:
自定义视图解析器+自定义视图: 大厂学院
现代化、服务端Java模板引擎
1. 基本语法
表达式 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域, session 域, 对象等值 |
选择变量 | *{…} | 获取上下文对象值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成链接 |
片段表达式 | ~{…} | jsp:include 作用, 引入公共页面片段 |
字面量
文本值: ‘one text’ , 'Another one!'
数字: 0 , 34 , 3.0 , 12.3
布尔值: true , false
空值: null
变量: one, two 变量不能有空格
文本操作
字符串拼接: +
变量替换: The name is ${name}
数学运算
+ , - , * , / , %
布尔运算
and, or, !, not
比较运算
比较: > , < , >= , <= ( gt , lt , ge , le **)**等式: == , != ( eq , ne )
条件运算
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
2. 设置属性值 - th:attr
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" /> 代替写法 <input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/> <form action="subscribe.html" th:action="@{/subscribe}">
所有h5兼容的标签写法:
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes
3. 迭代
<tr th:each="prod : ${prods}"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td> </tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td> </tr>
4. 条件运算
<a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}"> <p th:case="'admin'">User is an administrator</p> <p th:case="#{roles.manager}">User is a manager</p> <p th:case="*">User is some other thing</p> </div>
5. 属性优先级
不按书写顺序定义优先级, 当同一个标签中有多个th:*属性时,Thymleaf在处理这些属性时也有优先级的问题
Order | Feature | Attributes |
---|---|---|
1 | Fragment inclusion | th:include th:replace |
2 | Fragment iteration | th:each |
3 | Conditional evaluation | th:if th:unless th:switch th:case |
4 | Local variable definition | th:object th:with |
5 | General attribute modification | th:attr th:attrprepend th:attrappend |
6 | Specific attribute modification | th:value th:href th:src … |
7 | Text (tag body modification) | th:text th:utext |
8 | Fragment specification | th:fragment |
9 | Fragment removal | th:remove |
1. 引入 starter
<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 { }
自动配好的策略
1、所有thymeleaf的配置值都在 ThymeleafProperties
2、配置好了 SpringTemplateEngine
3、配好了 ThymeleafViewResolver
4、我们只需要直接开发页面
public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_SUFFIX = ".html"; // xxx.html
1. 创建项目, 把所有静态资源放到 static 文件夹下
2. 路径构建
th:action="@{/login}"
3. 模板抽取
th:insert/replace/include **注意三者区别 用 div 把要抽取的内容包起来 **
<head th:fragment="commonheader"> <!--common--> <link href="css/style.css" rel="stylesheet" th:href="@{/css/style.css}"> <link href="css/style-responsive.css" rel="stylesheet" th:href="@{/css/style-responsive.css}"> <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> <script src="js/html5shiv.js" th:src="@{/js/html5shiv.js}"></script> <script src="js/respond.min.js" th:src="@{/js/respond.min.js}"></script> <![endif]--> </head> <div id="leftmenu" class="left-side sticky-left-side"> ... </div> ========= main.html ========= <div th:include="common :: commonheader"></div> <!--因为使用 id 声明, 所以用 # 选择器来选择--> <div th:replace="common :: #leftmenu"></div>
4. 页面跳转
@PostMapping("/login") public String main(User user, HttpSession session, Model model){ if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){ //把登陆成功的用户保存起来 session.setAttribute("loginUser",user); //登录成功重定向到main.html; 重定向防止表单重复提交 return "redirect:/main"; }else { model.addAttribute("msg","账号密码错误"); //回到登录页面 return "login"; } }
5. 数据渲染
@GetMapping("/dynamic_table") public String dynamic_table(Model model){ //表格内容的遍历 List<User> users = Arrays.asList(new User("zhangsan", "123456"), new User("lisi", "123444"), new User("haha", "aaaaa"), new User("hehe ", "aaddd")); model.addAttribute("users",users); return "table/dynamic_table"; }
<thead> <tr> <th>#</th> <th>用户名</th> <th>密码</th> </tr> </thead> <tbody> <tr class="gradeX" th:each="user, userstat:${users}"> <td th:text="${userstat.count}">Trident</td> <td th:text="${user.userName}">Internet</td> <td>[[${user.password}]]</td> </tr> </tbody>
@Slf4j public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException { String requestURI = request.getRequestURI(); log.info("preHandle 拦截的请求路径是{}",requestURI); // 登录判断 HttpSession session = request.getSession(); Object obj = session.getAttribute("user"); if(obj != null) { return true; } // 请求转发到登录页 request.setAttribute("msg", "请先登录"); request.getRequestDispatcher("/").forward(request, response); return false; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { log.info("postHandle 执行{}",modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) { log.info("afterCompletion 执行异常{}",ex); } } ============== AdminWebConfig ============== @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") .excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**"); }
1、根据当前请求,找到 **HandlerExecutionChain【**可以处理请求的handler以及handler的所有 拦截器】
2、先来顺序执行所有拦截器的 preHandle 方法
3、如果任何一个拦截器返回false. 直接跳出不执行目标方法
4、所有拦截器都返回True. 执行目标方法
5、倒序执行所有拦截器的postHandle方法。
6、前面的步骤有任何异常都会直接倒序触发 afterCompletion
7、页面成功渲染完成以后,也会倒序触发 afterCompletion
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data"> <div class="form-group"> <label for="exampleInputEmail1">邮箱</label> <input type="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email"> </div> <div class="form-group"> <label for="exampleInputPassword1">名字</label> <input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password"> </div> <div class="form-group"> <label for="exampleInputFile">头像</label> <input type="file" name="headerImg" id="exampleInputFile"> </div> <div class="form-group"> <label for="exampleInputFile">照片集</label> <input type="file" name="photos" multiple> </div> <button type="submit" class="btn btn-primary">提交</button> </form>
/** * MultipartFile 自动封装上传过来的文件 * @param email * @param username * @param headerImg * @param photos * @return */ @PostMapping("/upload") public String upload(@RequestParam("email") String email, @RequestParam("username") String username, @RequestPart("headerImg") MultipartFile headerImg, @RequestPart("photos") MultipartFile[] photos) throws IOException { log.info("上传的信息:email={},username={},headerImg={},photos={}", email,username,headerImg.getSize(),photos.length); if(!headerImg.isEmpty()){ //保存到文件服务器,OSS服务器 String originalFilename = headerImg.getOriginalFilename(); headerImg.transferTo(new File("H:\\cache\\"+originalFilename)); } if(photos.length > 0){ for (MultipartFile photo : photos) { if(!photo.isEmpty()){ String originalFilename = photo.getOriginalFilename(); photo.transferTo(new File("H:\\cache\\"+originalFilename)); } } } return "main"; }
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
原理步骤
请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
参数解析器 (RequestPartMethodArgumentResolver) 来解析请求中的文件内容封装成MultipartFile
**将request中文件信息封装为一个Map;**MultiValueMap<String, MultipartFile>
FileCopyUtils: 实现文件流的拷贝
1. 默认规则
默认情况下,Spring Boot提供/error
处理所有错误的映射
对于机器客户端,它将生成 JSON 响应,其中包含错误,HTTP 状态和异常消息的详细信息. 对于浏览器客户端,响应一个 whitelabel 错误视图,以 HTML 格式呈现相同的数据
要对其进行自定义,添加View
解析为error
要完全替换默认行为,可以实现 ErrorController
并注册该类型的Bean定义,或添加ErrorAttributes
类型的组件以使用现有机制但替换其内容。
error/下的4xx,5xx页面会被自动解析;
2. 定制错误处理逻辑
自定义错误页
@ControllerAdvice + @ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler({ArithmeticException.class,NullPointerException.class}) //处理异常 public String handleArithException(Exception e){ return "login"; //视图地址 } }
@ResponseStatus + 自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus 注解的信息底层调用 response.sendError(statusCode, resolvedReason); tomcat 发送的 /error
@ResponseStatus(value= HttpStatus.FORBIDDEN,reason = "用户数量太多") public class UserTooManyException extends RuntimeException { public UserTooManyException(){} public UserTooManyException(String message){ super(message); } }
Spring 底层的异常,如参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常
@Order(value= Ordered.HIGHEST_PRECEDENCE) //优先级,数字越小优先级越高 @Component public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { response.sendError(511,"我喜欢的错误"); // try - catch return new ModelAndView(); } }
ErrorViewResolver 实现自定义处理异常
response.sendError: error请求就会转给controller
你的异常没有任何人能处理。tomcat 底层 response.sendError. error请求就会转给controller
basicErrorController 要去的页面地址是 ErrorViewResolver
3. 异常处理自动配置原理
如果想要返回页面;就会找error视图 (StaticView) (默认是一个白页)
4. 异常处理步骤流程
执行目标方法,目标方法运行期间有任何异常都会被catch, 而且标志当前请求结束; 并且异常会被赋值给 dispatchException
进入视图解析流程 (页面渲染)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
系统默认的异常解析器
1、DefaultErrorAttributes 先来处理异常. 把异常信息保存到rrequest域,并且返回null
HadnlerExceptionResolverComposite { 0. ExceptionHandlerExceptionResolver 1. ResponseStatusExceptionResolver 2. DefaultHandlerExceptionResolver }
2、默认没有人能处理异常,所以异常会被抛出
1、如果没有人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController 处理
2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析
3、默认的 DefaultErrorViewResolver 中的 ErrorViewResolver 把响应状态码作为错误页的地址,error/500.html
4、模板引擎最终响应这个页面 error/500.html
@ServletComponentScan(basePackages = “com.atguigu.admin”) :指定原生Servlet组件都放在那里
@WebServlet(urlPatterns = “/my”):效果:直接响应,没有经过Spring的拦截器
@WebFilter(urlPatterns={"/css/*","/images/*"})
@WebListener
====== Servlet ====== //@WebServlet(urlPatterns = "/my") public class MyServlet extends HttpServlet { // 实现方法 } ====== Filter ====== //@WebFilter(urlPatterns={"/css/*","/images/*"}) // my public class MyFilter implements Filter { // 实现方法 } ====== lisenter ====== //@WebListener public class MySwervletContextListener implements ServletContextListener { // 实现方法 }
建议使用 RegistrationBean 方式
ServletRegistrationBean
, FilterRegistrationBean
, and ServletListenerRegistrationBean
====== MyRegistConfig ====== // (proxyBeanMethods = true):保证依赖的组件始终是单实例的 @Configuration(proxyBeanMethods = true) public class MyRegistConfig { @Bean public ServletRegistrationBean myServlet(){ MyServlet myServlet = new MyServlet(); return new ServletRegistrationBean(myServlet,"/my","/my02"); } @Bean public FilterRegistrationBean myFilter(){ MyFilter myFilter = new MyFilter(); // return new FilterRegistrationBean(myFilter,myServlet()); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter); filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*")); return filterRegistrationBean; } @Bean public ServletListenerRegistrationBean myListener(){ MySwervletContextListener mySwervletContextListener = new MySwervletContextListener(); return new ServletListenerRegistrationBean(mySwervletContextListener); } }
扩展:DispatchServlet 如何注册进来
容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。
通过 ServletRegistrationBean<DispatcherServlet> 把 DispatcherServlet 配置进来。
默认映射的是 /路径。
Tomcat-Servlet:
多个Servlet都能处理到同一层路径,精确优选原则
默认支持的webServer
Tomcat
, Jetty
, or Undertow
ServletWebServerApplicationContext
容器启动寻找 ServletWebServerFactory
并引导创建服务器切换服务器
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency>
原理:
ServletWebServerApplicationContext
ServletWebServerApplicationContext
启动的时候寻找 ServletWebServerFactory(Servlet 的web服务器工厂---> Servlet 的web服务器)
TomcatServletWebServerFactory
, JettyServletWebServerFactory
, or UndertowServletWebServerFactory
底层直接会有一个自动配置类: ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryAutoConfiguration
导入了ServletWebServerFactoryConfiguration (配置类)
ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
TomcatServletWebServerFactory 创建出Tomcat服务器并启动; TomcatWebServer 的构造器拥有初始化方法 initialize---this.tomcat.start();
内嵌服务器, 就是手动把启动服务器的代码调用 (tomcat核心jar包存在)
ServletWebServerFactory 进行绑定
xxxxxCustomizer:定制化器,可以改变xxxx的默认规则
@Component public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> { @Override public void customize(ConfigurableServletWebServerFactory server) { server.setPort(9000); } }
定制化方式:
修改配置文件;
xxxxxCustomizer;
编写自定义的配置类 xxxConfiguration;+ @Bean替换、增加容器中默认组件;视图解析器
Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件 (重要)
@Configuration public class AdminWebConfig implements WebMvcConfigurer
@EnableWebMvc + WebMvcConfigurer —— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能
原理
场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项