SpringBoot笔记
一、Spring Boot HelloWorld
-
一个功能:浏览器发送hello请求,服务器接受请求并处理,响应Hello World字符串
-
<properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> //这个依赖是让测试类@SpringBootTest能够自己找到springboot主启动类,不用再加@Runwith注解了 <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> //将应用打包成jar包的插件 <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> //@SpringBootApplication: 标注这个类是一个springboot的应用: 启动类下的所有资源被导入 //Spring Boot应用标注在某个类上说明这个类是SpringBoot的主配置类,SpringBoot 就应该运行这个类的main方法来启动SpringBoot应用 @SpringBootApplication public class HelloworldApplication { public static void main(String[] args) { //将springboot应用启动 //SpringApplication类co //run方法 SpringApplication.run(HelloworldApplication.class, args); } } @RestController //注:这里的@RestController 就是指处理器方法不跳转页面而是直接返回结果(数据), // 它的作用相当于@Controller + 处理器方法上加ResponseBody public class HelloController { @RequestMapping("/hello") public String Hello(){ return "Hello!!!朱哥"; } }
原理初探
-
POM文件
-
父项目
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> //它的父项目是 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.3.RELEASE</version> </parent> //他来真正管理Spring Boot应用里面所有的依赖版本 //所以我们以后导入依赖默认是不需要写版本的(除非没在dependencies里面管理) <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> //spring-boot-starter: springboot 场景启动器;帮我们导入了web模块正常运行所依赖的组件; //spring-boot 将所有的功能场景都抽取出来, 做成一个个的Staters(启动器), 只需要在项目里引入这些Starter相关的场景的所有依赖都会导入进来。
-
主程序类, 主入口类
-
@SpringBootApplication部分源码
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication {
-
@SpringBootConfiguration:Spring Boot的配置类;
标注在某个类上,表示这是一个Spring Boot的配置类;
@Configuration:配置类上来标注这个注解;
配置类 ----- 配置文件;配置类也是容器中的一个组件;@Component
-
-
-
@EnableAutoConfiguration:开启自动配置功能;
以前我们需要配置的东西,Spring Boot帮我们自动配置;@EnableAutoConfiguration告诉SpringBoot开启自 动配置功能;这样自动配置才能生效; ```java @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { ```
-
@AutoConfigurationPackage:自动配置包
@Import(AutoConfigurationPackages.Registrar.class):
Spring的底层注解@Import,给容器中导入一个组件,也就是 AutoConfigurationPackages.Registrar.class;@Import(AutoConfigurationPackages.Registrar.class) public @interface AutoConfigurationPackage { }
将主配置类(@SpringBootApplication标注的类)的所在包及下面所有子包里面的所有组件扫描到Spring容器
(注: 是将主程序类所在包里所有组件扫描进去,所以说那些controller包和service包一定要跟主类并列,而不是包含,否则可能出错,扫描不到容器里)
-
@Import(AutoConfigurationImportSelector.class);给容器中导入组件
AutoConfigurationImportSelector:导入哪些组件的选择器; 将所有需要导入的组件以全类名的方式返回;这些组件就会被添加到容器中; 会给容器中导入非常多的自动配置类(xxxAutoConfiguration); 就是给容器中导入这个场景需要的所有组件, 并配置好这些组件;
- 有了自动配置类,免去了我们手动编写配置注入功能组件等的工作;
SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,classLoader);
-
Spring Boot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,
从源码不难看出加载类扫描的路径
~~~java
```java
public final class SpringFactoriesLoader {
/**
* The location to look for factories.
* <p>Can be present in multiple JAR files.
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
~~~
```java
将这些值作为自动配置类导入到容器中,自动配置类就生效,帮我们进行自动配置工作;
J2EE的整体整合解决方案和自动配置都在spring-boot-autoconfigure-2.3.3.RELEASE.jar
二、配置文件(yml和properties)
1、基本介绍
-
SpringBoot使用一个全局的配置文件,配置文件名是固定的;
•application.properties
•application.yml
-
配置文件的作用:修改SpringBoot自动配置的默认值;SpringBoot在底层都给我们自动配置好;
-
以前的配置文件;大多都使用的是 xxxx.xml文件;
YAML:以数据为中心,比json、xml等更适合做配置文件; 是一个标记语言
2、yml语法
-
基本语法
-
k:(空格)v:表示一对键值对(空格必须有);
以空格的缩进来控制层级关系;只要是左对齐的一列数据,都是同一个层级server: port: 8081
-
属性和值也是大小写敏感;
-
-
值的写法
-
字面量,普通的值(数字,字符串,布尔)
k: v:字面直接来写;
字符串默认不用加上单引号或者双引号;
“”:双引号;不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思name: “zhangsan \n lisi”:输出;zhangsan 换行 lisi
‘’:单引号;会转义特殊字符,特殊字符终只是一个普通的字符串数据name: ‘zhangsan \n lisi’:输出;zhangsan \n lisi
-
对象、Map(属性和值)(键值对)
k: v:在下一行来写对象的属性和值的关系;注意缩进
对象还是k: v的方式student: name: zhuge age: 20
-
数组(List、Set)
用- 值表示数组中的一个元素
pets: -cat -dog -pig
行内写法
petss: [cat,dog,pig]
-
3、配置文件的值注入
- 这个是application.yml文件里的内容
server:
port: 8081
person:
name: zhuge
age: 20
happy: false
birth: 2020/8/24
maps: {k1: v1,k2: v2}
lists:
- code
- music
- girl
dog:
name: ${person.hello:hello}_旺财
age: 3
dog:
# 松散绑定
first-name: 阿黄
age: 3
- 这个是我们自己写的用于与配置文件进行绑定的javabean类(后面还有get、set和toString方法)
/**
* 将配置文件中配置的每一个属性的值,映射到这个组件中
* @ConfigurationProperties:告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定;
* prefix = "person":配置文件中哪个下面的所有属性进行一一映射
*
*
* 只有这个组件是容器中的组件,才能容器提供的@ConfigurationProperties功能;
*/
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
// @Email(message="邮箱格式错误")
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
- 我们可以导入配置文件处理器,以后编写配置就有提示
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
-
@Value获取值和@ConfigurationProperties获取值比较
- 配置文件yml还是properties他们都能获取到值;
- 如果说,我们只是在某个业务逻辑中需要获取一下配置文件中的某项值,使用@Value;
- 如果说,我们专门编写了一个javaBean来和配置文件进行映射,我们就直接使用@ConfigurationProperties;
-
配置文件注入值数据校验
-
@Component @ConfigurationProperties(prefix = "person") @Validated//数据校验 public class Person { /** * <bean class="Person"> * <property name="lastName" value="字面量/${key}从环境变量、配置文件中获取值/# {SpEL}"></property> * <bean/> */ //Name必须是邮箱格式 @Email(message="邮箱格式错误") //@Value("${person.name}") private String name; //@Value("#{11*2}") private Integer age; //@Value("true") private Boolean happy; private Date birth; private Map<String,Object> maps; private List<Object> lists; private Dog dog;
-
注: 2.3.0之后的版本需要导入相应的依赖才能进行数据校验
<dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.17.Final</version> <scope>compile</scope> </dependency>
-
-
@PropertySource
-
@PropertySource(value = {"classpath:application.yml"}) @Component @ConfigurationProperties(prefix = "dog") public class Dog { // @Value("旺财") private String firstName; // @Value("3") private Integer age;
@PropertySource:加载指定的配置文件;
-
4、导入Spring配置文件
-
第一种方式: 配置类@Configuration------>Spring配置文件
-
@ImportResource:导入Spring的配置文件,让配置文件里面的内容生效;
-
Spring Boot里面没有Spring的配置文件,我们自己编写的配置文件,也不能自动识别;
-
想让Spring的配置文件生效,加载进来;@ImportResource标注在一个配置类上
-
@ImportResource(locations = {"classpath:beans.xml"}) //导入Spring的配置文件让其生效
-
Spring的配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="helloService" class="com.zhuge.service.HelloService"></bean> </beans>
package com.zhuge.service; /** * @author ZCH * @date 2020/9/24 0024 - 下午 4:03 */ public class HelloService { }
-
-
第二种方式: 使用@Bean给容器中添加组件
-
@Configuration //指明当前类是一个配置类;就是来替代之前的Spring配置文件 public class MyMvcConfig implements WebMvcConfigurer { //将方法的返回值添加到容器中;容器中这个组件默认的id就是方法名 @Bean public LocaleResolver localeResolver(){ return new MyLocaleResolver(); } }
-
5、配置文件占位符
-
随机数
${random.value}、${random.int}、${random.long} ${random.int(10)}、${random.int[1024,65536]}
-
占位符获取之前配置的值, 如果没有可以使用: 指定默认值 ${person.hello:hello} 默认是hello
person: name: hello age: 20 happy: false birth: 2020/8/24 maps: {k1: v1,k2: v2} lists: - code - music - girl dog: name: ${person.hello:hello}_旺财 age: 3
6、Profile
-
多Profile文件
我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml
默认使用application.properties的配置; -
server: port: 8081 spring: profiles: active: dev #激活指定profile --- server: port: 8082 spring: profiles: dev --- server: port: 8083 spring: profiles: test #指定属于哪个环境
-
激活指定profile
-
在配置文件中指定 spring.profiles.active=dev
-
命令行:
java -jar spring-boot-02-config-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev;可以直接在测试的时候,配置传入命令行参数
-
虚拟机参数;
-Dspring.profiles.active=dev
-
7、配置文件加载位置
-
springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件
(注: 这里classpath 指的是resources文件夹)
- –file:./config/ 代表读取jar包所在目录下的config文件
- –file:./ 代表读取jar包所在目录
- –classpath:/config/ 代表类路径下的config包下的文件
- –classpath:/ 代表类路径下的文件
SpringBoot会从这四个位置全部加载主配置文件;互补配置;
-
我们还可以通过spring.config.location来改变默认的配置文件位置
-
项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;指定配置文件和默 认加载的这些配置文件共同起作用形成互补配置;
-
java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --spring.config.location=G:/application.properties
8、自动配置原理(重要)
1、@SpringBootConfiguration
见原理初探
2、@EnableAutoConfiguration
- EnableAutoConfiguration源码
-
SpringBoot启动的时候加载主配置类,开启了自动配置功能 @EnableAutoConfiguration
-
其内部有两个非常重要的注解:
- @AutoConfigurationPackage
- @Import(AutoConfigurationImportSelector.class)
3、@AutoConfigurationPackage
(自动配置包)
-
AutoConfigurationPackage源码
@Import(AutoConfigurationPackages.Registrar.class) public @interface AutoConfigurationPackage {
Spring的底层注解@Import,给容器中导入一个组件,也就是 AutoConfigurationPackages.Registrar.class;
-
Register类的内部
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0])); }
-
Register类是 AutoConfigurationPackage 下的一个静态内部类。 这个类的作用是给容器中导入组件
-
主要看 new PackageImports(metadata).getPackageNames(), 它是导入一个metadata(注释源信息)到PackageImports中,然后获取这个包的包名。 所以说这个实例对象获取的包名是什么呢?
-
注: 要在debug模式下,选中new PackageImports(metadata)所在行作为断点
当调用到register()时, 我们就会发现packageNames就是我们的主配置类所在包的包名
-
总结: @AutoConfigurationPackage 的作用就是将主配置类(@SpringBootApplication标注的类)的所在包及下面所有子包里面的所有组件扫描到Spring容器
-
4、@Import(AutoConfigurationImportSelector.class)
-
这个注解的作用是给容器中导入组件,该组件就是AutoConfigurationImportSelector:自动配置导入选择器,它可以帮我们选择要导入的组件
-
进入AutoConfigurationImportSelector的源码, 它与SpringBoot1.x 版本有一些区别, 2.x中有一个静态内部类
其大致的意思是 自动配置组 ,能够帮我们完成一系列的自动配置的操作。
-
重点回到该内部类下的**process()**方法
调用了 **getAutoConfigurationEntry()**方法, 该方法的作用就是告诉Spring容器需要导入什么组件, 并以 String[] 的形式返回全类名
-
从 标蓝的一行 可以看出, SpringBoot已经帮我们自动导入了 127 个组件, 从下图可以看出都是以全类名的形式返回。而且这些组件都是以 …AutoConfiguration 的形式命名, 也就是什么自动配置类
有了这些自动配置类, 我们就免去了手动配置注入功能组件的操作了。
-
那么为何能做到自动配置呢??(之前的AutoConfigurationImportSelector只是帮助我们选择要导入的组件,AutoConfigurationPackage是将组件扫描到Spring容器 )其实是 configurations 这个变量是由getCandidateConfigurations()方法(上图标蓝的上一行)得到的, 也就是获取候选的位置。
这里调用了
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); //传入了两个参数 protected Class<?> getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class; } protected ClassLoader getBeanClassLoader() { return this.beanClassLoader; }
于是便要知道loadFactoryNames()的作用
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) { String factoryTypeName = factoryType.getName(); return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); }
在这里又调用了loadSpringFactories(classLoader)并将类加载器作为参数. (注: classloader的实参就是getBeanClassLoader())
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap<String, String> result = cache.get(classLoader); if (result != null) { return result; } try { Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); result = new LinkedMultiValueMap<>(); while (urls.hasMoreElements()) { URL url = urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry<?, ?> entry : properties.entrySet()) { String factoryTypeName = ((String) entry.getKey()).trim(); for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryTypeName, factoryImplementationName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); } }
首先是在 133 行,从类加载器中加载一个资源,资源路径为FACTORIES_RESOURCE_LOCATION其实就是
META-INF/spring.factories
将获取到的一个资源赋值给 Enumeration类型的变量urls, 如果该变量中有下一个元素,说明这里面又包含资源,那就将这个资源加载成 properties 配置文件,并转换成为键值对即 Map类型的数据 进行返回
public final class SpringFactoriesLoader { public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; ... }
META-INF/spring.factories位置
总结:
-
SpringBoot 在启动的时候就从类路径下的 META-INF/spring.factories 中获取EnableAutoConfiguration指定的值,并将这些值加载到自动配置类导入到容器中,自动配置类 就生效,帮助我们进行自动配置功能。 而这些自动配置类 全都在 spring-boot-autoconfigure-2.3.3.RELEASE.jar 该jar包之下
-
如果要看自动配置类配置了什么功能,就点进==…AutoConfiguration==的源码即可
-
每一个这样的==…AutoConfiguration==类都是容器中的一个组件, 都加入到容器中; 用他们来做自动配置;
-
举例(以HttpEncodingAutoConfiguration(Http编码自动配置)为例)
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
@Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(ServerProperties.class) ///启动指定类的 ConfigurationProperties功能;将配置文件中对应的值和HttpEncodingProperties绑定起来;并把 HttpEncodingProperties加入到ioc容器中 @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) //Spring底层@Conditional注解),根据不同的条件,如果 满足指定的条件,整个配置类里面的配置就会生效; 判断当前应用是否是web应用,如果是,当前配置类生效 @ConditionalOnClass(CharacterEncodingFilter.class) //判断当前项目有没有这个类 CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器; @ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true) //判断配置文件中是否存在某个配置 spring.http.encoding.enabled;如果不存在,判断也是成立的 //即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的; public class HttpEncodingAutoConfiguration { //他已经和SpringBoot的配置文件映射了 private final Encoding properties; //只有一个有参构造器的情况下,参数的值就会从容器中拿 public HttpEncodingAutoConfiguration(ServerProperties properties) { this.properties = properties.getServlet().getEncoding(); } @Bean //给容器中添加一个组件,这个组件的某些值需要从properties中获取 @ConditionalOnMissingBean//判断容器没有这个组件? public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter(); filter.setEncoding(this.properties.getCharset().name()); filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST)); filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE)); return filter; } @Bean public LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() { return new LocaleCharsetMappingsCustomizer(this.properties); } static class LocaleCharsetMappingsCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered { private final Encoding properties; LocaleCharsetMappingsCustomizer(Encoding properties) { this.properties = properties; } @Override public void customize(ConfigurableServletWebServerFactory factory) { if (this.properties.getMapping() != null) { factory.setLocaleCharsetMappings(this.properties.getMapping()); } } @Override public int getOrder() { return 0; } } }
-
所有在配置文件中能配置的属性都是在xxxxProperties类中封装着‘;配置文件能配置什么就可以参照某个功 能对应的这个属性类
@ConfigurationProperties( prefix = "spring.http.encoding" //从配置文件中获取指定的值和bean的属性进行绑定 ) public class HttpEncodingProperties { public static final Charset DEFAULT_CHARSET; ... }
-
9、@Conditional注解
-
@Conditional派生注解(Spring注解版原生的@Conditional作用)
作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;@Conditional扩展注解 作用(判断是否满足当前指定条件) @ConditionalOnJava 系统的java版本是否符合要求 @ConditionalOnBean 容器中存在指定Bean; @ConditionalOnMissingBean 容器中不存在指定Bean; @ConditionalOnExpression 满足SpEL表达式指定 @ConditionalOnClass 系统中有指定的类 @ConditionalOnMissingClass 系统中没有指定的类 @ConditionalOnSingleCandidate 容器中只有一个指定的Bean,或者这个Bean是首选Bean @ConditionalOnProperty 系统中指定的属性是否有指定的值 @ConditionalOnResource 类路径下是否存在指定资源文件 @ConditionalOnWebApplication 当前是web环境 @ConditionalOnNotWebApplication 当前不是web环境 @ConditionalOnJndi JNDI存在指定项 -
我们可以在配置文件中写上一项,debug我打成true
debug=true
默认是false,开启Springboot的debug模式,会非常好用,我们来运行一下,一旦debug模式进来以后呢,控制台就会告诉我们哪些用了
哪些没用,自动配置报告
#application.properties debug=true server.port=8081 ============================ CONDITIONS EVALUATION REPORT ============================ Positive matches: ----------------- AopAutoConfiguration matched: - @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition) AopAutoConfiguration.ClassProxyingConfiguration matched: - @ConditionalOnMissingClass did not find unwanted class 'org.aspectj.weaver.Advice' (OnClassCondition) - @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition) DispatcherServletAutoConfiguration matched: - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition) - found 'session' scope (OnWebApplicationCondition) DispatcherServletAutoConfiguration.DispatcherServletConfiguration matched: - @ConditionalOnClass found required class 'javax.servlet.ServletRegistration' (OnClassCondition) - Default DispatcherServlet did not find dispatcher servlet beans (DispatcherServletAutoConfiguration.DefaultDispatcherServletCondition) DispatcherServletAutoConfiguration.DispatcherServletRegistrationConfiguration matched: - @ConditionalOnClass found required class 'javax.servlet.ServletRegistration' (OnClassCondition) - DispatcherServlet Registration did not find servlet registration bean (DispatcherServletAutoConfiguration.DispatcherServletRegistrationCondition) DispatcherServletAutoConfiguration.DispatcherServletRegistrationConfiguration#dispatcherServletRegistration matched: - @ConditionalOnBean (names: dispatcherServlet types: org.springframework.web.servlet.DispatcherServlet; SearchStrategy: all) found bean 'dispatcherServlet' (OnBeanCondition) EmbeddedWebServerFactoryCustomizerAutoConfiguration matched: - @ConditionalOnWebApplication (required) found 'session' scope (OnWebApplicationCondition) EmbeddedWebServerFactoryCustomizerAutoConfiguration.TomcatWebServerFactoryCustomizerConfiguration matched: - @ConditionalOnClass found required classes 'org.apache.catalina.startup.Tomcat', 'org.apache.coyote.UpgradeProtocol' (OnClassCondition) ErrorMvcAutoConfiguration matched: - @ConditionalOnClass found required classes 'javax.servlet.Servlet', 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition) - found 'session' scope (OnWebApplicationCondition) ErrorMvcAutoConfiguration#basicErrorController matched: - @ConditionalOnMissingBean (types: org.springframework.boot.web.servlet.error.ErrorController; SearchStrategy: current) did not find any beans (OnBeanCondition) ErrorMvcAutoConfiguration#errorAttributes matched: - @ConditionalOnMissingBean (types: org.springframework.boot.web.servlet.error.ErrorAttributes; SearchStrategy: current) did not find any beans (OnBeanCondition) ErrorMvcAutoConfiguration.DefaultErrorViewResolverConfiguration#conventionErrorViewResolver matched: - @ConditionalOnBean (types: org.springframework.web.servlet.DispatcherServlet; SearchStrategy: all) found bean 'dispatcherServlet'; @ConditionalOnMissingBean (types: org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver; SearchStrategy: all) did not find any beans (OnBeanCondition) ...
这样我们就可以很方便的知道哪些自动配置类生效;(下面还有很多没打出来)
三、Web开发
1、基本介绍
-
创建SpringBoot应用,选中我们需要的模块;
-
SpringBoot已经默认将这些场景配置好了,只需要在配置文件中指定少量配置就可以运行起来
-
自己编写业务代码;
-
自动配置原理?
这个场景SpringBoot帮我们配置了什么?能不能修改?能修改哪些配置?能不能扩展?xxx -
xxxxAutoConfiguration:帮我们给容器中自动配置组件; xxxxProperties:配置类来封装配置文件的内容;
2、SpringBoot对静态资源的映射规则
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
///可以设置和静态资源有关的参数,缓存时间等
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;//getStaticLocations下的位置
WebMvcAuotConfiguration类:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); //此处resourceProperties即是
//ResourceProperties类型的
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
//cachePeriod: 缓存时间等等可以通过上方ResourceProperties类来设置
String staticPathPattern = this.mvcProperties.getStaticPathPattern();//静态资源的路径
//静态资源文件夹映射
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
//如果静态资源路径没有被映射,就去getStaticLocations下的位置寻找(见上一个代码块)
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
//欢迎页的映射
@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;
}
-
第一种方式
所有 /webjars/**(指的是这个访问路径) ,都去 classpath:/META-INF/resources/webjars/ 找资源;
webjars:以jar包的方式引入静态资源(比如 jquery,css等等);
//引入jquery‐webjar‐‐>在访问的时候只需要写webjars下面资源的名称即可 <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.3.1</version> </dependency>
-
第二种方式
@ConfigurationProperties(prefix = "spring.mvc") public class WebMvcProperties { ... /** * Path pattern used for static resources. */ private String staticPathPattern = "/**"; ... }
“/**” 访问当前项目的任何资源,都去(静态资源的文件夹)找映射
"classpath:/META‐INF/resources/", "classpath:/resources/",(注:这里是指在maven给定的resources下再建一个resources文件夹) "classpath:/static/", "classpath:/public/" "/":当前项目的根路径
-
第三种方式
欢迎页;静态资源文件夹下的所有index.html页面;被“/**”映射;
localhost:8080/(也满足/**) 找index.html(需要放到静态资源的文件夹下)
3、模板引擎(thymeleaf)
-
导入坐标
<!-- thymeleaf, 基于3.x开发,之前的版本还得在properties标签里改版本 --> <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring5</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-java8time</artifactId> </dependency>
1、Thymeleaf使用&语法
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
只要我们把HTML页面==放在classpath:/templates/==下 , thymeleaf就能自动渲染;
-
导入thymeleaf的名称空间
<html lang="en" xmlns:th="http://www.thymeleaf.org">
2、语法规则
-
th:text ; 改变当前元素里面的文本内容 ;
th: 任意html属性;来替换原生属性的值
-
表达式
补充:(OGNL):OGNL(Object-Graph Navigation Language)的全称是对象图导航语言,它是一种功能强大的开源表达式语言,比 EL(只能从域或内置对象中)表达式更强大,使用这种表达式语言,可以通过某种表达式语法,OGNL可以存取Java任意对象的任意属性,调用Java对象的方法,同时能够自动实现必要的类型转换。如果把表达式看作是一个带有语义的字符串,那么OGNL无疑成为了这个语义字符串与Java对象之间沟通的桥梁。
Simple expressions: 1.Variable Expressions: ${...} : 获取变量值 ; OGNL; 1)获取对象的属性、调用方法 2)使用内置的基本对象 #ctx : the context object. #vars: the context variables. #locale : the context locale. #request : (only in Web Contexts) the HttpServletRequest object. #response : (only in Web Contexts) the HttpServletResponse object. #session : (only in Web Contexts) the HttpSession object. #servletContext : (only in Web Contexts) the ServletContext object #在thymeleaf官方文档里 You can read the full reference of these objects in Appendix A. 可以看用法 3)使用内置的工具对象 #execInfo : information about the template being processed. #messages : methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax. #uris : methods for escaping parts of URLs/URIs #conversions : methods for executing the configured conversion service (if any). #dates : methods for java.util.Date objects: formatting, component extraction, etc. #calendars : analogous to #dates , but for java.util.Calendar objects. #numbers : methods for formatting numeric objects. #strings : methods for String objects: contains, startsWith, prepending/appending, etc. #objects : methods for objects in general. #bools : methods for boolean evaluation. #arrays : methods for arrays. #lists : methods for lists. #sets : methods for sets. #maps : methods for maps. #aggregates : methods for creating aggregates on arrays or collections. #ids : methods for dealing with id attributes that might be repeated (for example, as a result of an iteration). #在thymeleaf官方文档里 You can read the full reference of these objects in Appendix B. 可以看用法 2.Selection Variable Expressions: *{...} :选择表达式:和${} 在功能上是一样的; 补充: 配合 <div th:object="${session.user}"> (span标签里的 * 就相当于${session.user}) <div th:object="${session.user}"> <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p> <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p> <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p> </div> 3.Message Expressions: #{...} : 获取国际化信息 4.Link URL Expressions: @{...} : 定义url <!-- Will produce 'http://localhost:8080/gtvg/order/details?orderId=3' (plus rewriting) --> <a href="details.html" th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a> <!-- Will produce '/gtvg/order/details?orderId=3' (plus rewriting) --> <a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a> #1.我们可以看出 用thymeleaf写url时可以把 http://localhost:8080/gtvg(项目名)省略掉, #2.本来地址中的请求参数?orderId=3 可以写为(orderId=${o.id})的形式 #3.如果有多个参数,可以 (execId=${execId},execType='FAST') <!-- Will produce '/gtvg/order/3/details' (plus rewriting) --> <a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a> 5.Fragment Expressions: ~{...} : 片段引用表达式 Literals(字面量) Text literals: 'one text' , 'Another one!' ,… Number literals: 0 , 34 , 3.0 , 12.3 ,… Boolean literals: true , false Null literal: null Literal tokens: one , sometext , main ,… Text operations(文本操作): String concatenation: + Literal substitutions: |The name is ${name}| Arithmetic operations (数学运算): Binary operators: + , - , * , / , % Minus sign (unary operator): - Boolean operations (布尔运算): Binary operators: and , or Boolean negation (unary operator): ! , not Comparisons and equality(比较运算): Comparators: > , < , >= , <= ( gt , lt , ge , le ) Equality operators: == , != ( eq , ne ) Conditional operators (条件运算: 三元运算符): If-then: (if) ? (then) If-then-else: (if) ? (then) : (else) Default: (value) ?: (defaultvalue) Special tokens: No-Operation: _ All these features can be combined and nested:
-
小例子
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1> Hello !!! </h1> <!--th:text 将div里面的内容设置为--> <div id="div01" class="mydiv" th:id="${hello}" th:class="${hello}" th:text="${hello}">这里显示欢迎信息</div> <!--th:属性名 将修改对应的属性值--> <hr/> <div th:text="${hello}"></div> <!--th:text 会转义特殊字符: 就是把本来的标签直接以字符串的形式输出来--> <div th:utext="${hello}"></div> <!--th:utext 不会转义, 就是说<h1>你不好</h1>中的<h1>会以html标签的效果展示出来--> <hr/> <!--th:each每次遍历都会生成当前这个标签--> <!--这里会生成四个 h4 标签--> <h4 th:text="${user}" th:each="user:${users}"></h4> <hr/> <h4> <!--这里会生成四个span标签--> <span th:each="user:${users}"> [[${user}]] </span> <!--直接在文本里写的话 [[...]] 相当于th:text [(...)]相当于th:utext --> </h4> </body> </html>
4、SpringMVC自动配置(重要)
1、Spring MVC auto-configuration
Spring Boot 自动配置好了SpringMVC
以下是SpringBoot对SpringMVC的默认配置:(WebMvcAutoConfiguration)
-
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans. ()
-
自动配置了ViewResolver(视图解析器:根据方法的返回值得到视图对象(View),视图对象决定如何 渲染(转发?重定向?))
-
ContentNegotiatingViewResolver:组合所有的视图解析器的;
WebMvcAutoConfiguration类: @Bean @ConditionalOnBean(ViewResolver.class) @ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class) //在没有名为"viewResolver",类型为ContentNegotiatingViewResolver的bean对象的情况下调用此方法 public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) { ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver(); resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class)); // ContentNegotiatingViewResolver uses all the other view resolvers to locate // a view so it should have a high precedence resolver.setOrder(Ordered.HIGHEST_PRECEDENCE); return resolver; }
public interface ViewResolver { /** 视图解析器接口只有一个方法, 就是根据名称解析出视图信息(一个视图对象View) 为了允许ViewResolver链接,ViewResolver应该 *如果未定义具有给定名称的视图,则返回{@code null}。 *一些ViewResolvers将总是尝试以给定的名称构建视图对象,就不返回{@code null} */ @Nullable View resolveViewName(String viewName, Locale locale) throws Exception; }
补充: 在IDEA中查看一个方法在哪里被调用了:
-
选中要查的方法名
-
快捷键==(Ctrl + G)==(在idea快捷键被改成了eclipse的情况下)
-
或者是右键, 选择 Find Usages
ContentNegotiatingViewResolver类: implements ViewResolver @Nullable public View resolveViewName(String viewName, Locale locale) throws Exception { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes"); List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest()); if (requestedMediaTypes != null) { List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes); //获取候选的视图对象 View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs); //选择一个最适合的视图对象 if (bestView != null) { return bestView; //然后将最适合的视图对象返回 } } String mediaTypeInfo = this.logger.isDebugEnabled() && requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : ""; if (this.useNotAcceptableStatusCode) { if (this.logger.isDebugEnabled()) { this.logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo); } return NOT_ACCEPTABLE_VIEW; } else { this.logger.debug("View remains unresolved" + mediaTypeInfo); return null; } }
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) throws Exception { List<View> candidateViews = new ArrayList(); if (this.viewResolvers != null) { Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set"); Iterator var5 = this.viewResolvers.iterator(); //使用迭代器获取所有的视图解析器对象 while(var5.hasNext()) { ViewResolver viewResolver = (ViewResolver)var5.next(); View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { candidateViews.add(view); } Iterator var8 = requestedMediaTypes.iterator(); while(var8.hasNext()) { MediaType requestedMediaType = (MediaType)var8.next(); List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType); Iterator var11 = extensions.iterator(); while(var11.hasNext()) { String extension = (String)var11.next(); String viewNameWithExtension = viewName + '.' + extension; view = viewResolver.resolveViewName(viewNameWithExtension, locale); if (view != null) { candidateViews.add(view); } } } } } if (!CollectionUtils.isEmpty(this.defaultViews)) { candidateViews.addAll(this.defaultViews); } return candidateViews;//返回给List<View> candidateViews 作为候选视图解析器对象 }
- 如何定制:我们可以自己给容器中添加一个视图解析器;自动的将其组合进来;
说明:
-
根据上面WebMvcAutoConfiguration类 以及 ContentNegotiatingViewResolver类
-
可以大致推断出如果我们自己定义了一个类实现ViewResolver接口
-
并且将该类型的对象添加到容器中(即用@Bean注解),就可以实现自己的视图解析器了。
-
由下方的代码可以知道,ContentNegotiatingViewResolver类中的initServletContext方法会通过BeanFactory工具 从容器中获取所有的视图解析器,自然也包括我们自己创建的
@Override protected void initServletContext(ServletContext servletContext) { Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values(); //通过BeanFactory工具 从容器中获取所有的视图解析器 if (this.viewResolvers == null) { this.viewResolvers = new ArrayList<>(matchingBeans.size()); for (ViewResolver viewResolver : matchingBeans) { if (this != viewResolver) { this.viewResolvers.add(viewResolver); //把获得的视图解析器放到 //private List<ViewResolver> viewResolvers; 里了 } } } else { for (int i = 0; i < this.viewResolvers.size(); i++) { ViewResolver vr = this.viewResolvers.get(i); if (matchingBeans.contains(vr)) { continue; } String name = vr.getClass().getName() + i; obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name); } } AnnotationAwareOrderComparator.sort(this.viewResolvers); this.cnmFactoryBean.setServletContext(servletContext); }
- 验证我们的想法
@SpringBootApplication public class Springboot09TestthymeleafApplication { public static void main(String[] args) { SpringApplication.run(Springboot09TestthymeleafApplication.class, args); } public ViewResolver myViewResolver(){ return new MyViewResolver(); } public static class MyViewResolver implements ViewResolver{ @Override public View resolveViewName(String s, Locale locale) throws Exception { return null; } } }
于是,我们要通过调试来确认DispatcherServlet 中的 doDispatch()方法 (因为所有请求一进来都会来到该方法)中用到的视图解析器是否包含我们自己定义的那个
可以看到我们自己定义的视图解析器也被调用了!!!
-
-
Support for serving static resources, including support for WebJars (see below).静态资源文件夹路径 /webjars
-
Static index.html support. 静态首页访问
-
Custom Favicon support (see below). favicon.ico
-
自动注册了 of Converter , GenericConverter , Formatter beans.
- Converter:转换器; public String hello(User user);类型转换使用Converter 把页面发来的文本类型转换为对应的类型)
- Formatter 格式化器;2017.12.17(从页面传来的===Date;先把字符串转为日期类型,还得按照传来的格式。
@Bean @Override public FormattingConversionService mvcConversionService() { Format format = this.mvcProperties.getFormat(); // 通过 private final WebMvcProperties mvcProperties。中的getFormat()方法 获取格式化信息 WebConversionService conversionService = new WebConversionService(new DateTimeFormatters() .dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime())); addFormatters(conversionService); //Create a new WebConversionService that configures formatters with the provided date, time, and date-time formats, or registers the default if no custom format is provided //WebConversionService是用来设置格式化器的 return conversionService; }
@ConfigurationProperties(prefix = "spring.mvc") public class WebMvcProperties { ... private final Format format = new Format(); @Deprecated /** 对@Deprecated 以及 @DeprecatedConfigurationProperty的补充说明: 1.这个注解表明当前属性被弃用了 2.弃用可以在代码中声明性地指定,方法是将@DeprecatedConfigurationProperty注释添加 到暴露不赞成使用的属性的getter中(即被@Deprecated标注了的) 3.例如下面的 spring.mvc.date‐format 被重命名为 spring.mvc.format.date */ @DeprecatedConfigurationProperty(replacement = "spring.mvc.format.date") //在文件中配置日期格式化的规则 //这个注解的意思就是说在properties文件中写日期格式的形式从过期的替换为spring.mvc.format.date //以前的是spring.mvc.date‐format public String getDateFormat() { return this.format.getDate(); } public Format getFormat() { return this.format;//注: 这个方法就是上一个代码块this.mvcProperties.getFormat();处调用的 //其实就是给WebMvcProperties中的format赋值 }
- 自己添加的格式化器转换器,我们只需要放在容器中即可
@Override public void addFormatters(FormatterRegistry registry) { ApplicationConversionService.addBeans(registry, this.beanFactory); //addFormatters()通过调用ApplicationConversionService里的addBeans()来添加格式化器 //参数为格式化器注册器 和 bean工厂(容器) //该方法由往上数第2个代码块中的addFormatters(conversionService);处调用 }
补充说明:
- 在IDEA快捷键为eclipse时,点击某个方法 按住 Ctrl + Alt + b
- 即可查看这个方法的具体实现方法
public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) { Set<Object> beans = new LinkedHashSet();//定义一个LinkedHashSet来存放组件(各种转换器) beans.addAll(beanFactory.getBeansOfType(GenericConverter.class).values()); //该方法是由 StaticListableBeanFactory 类(实现了ListableBeanFactory接口)来实现的 //用于获取所有实现了(。。。.class)的类并将它们添加到beans中去 beans.addAll(beanFactory.getBeansOfType(Converter.class).values()); beans.addAll(beanFactory.getBeansOfType(Printer.class).values()); beans.addAll(beanFactory.getBeansOfType(Parser.class).values()); Iterator var3 = beans.iterator(); while(var3.hasNext()) { Object bean = var3.next(); if (bean instanceof GenericConverter) { registry.addConverter((GenericConverter)bean); } else if (bean instanceof Converter) { registry.addConverter((Converter)bean); } else if (bean instanceof Formatter) { registry.addFormatter((Formatter)bean); } else if (bean instanceof Printer) { registry.addPrinter((Printer)bean); } else if (bean instanceof Parser) { registry.addParser((Parser)bean); } //如果各个类实例化了,就放到格式化器注册器中去 } }
-
Support for HttpMessageConverters (see below).
- HttpMessageConverter:SpringMVC用来转换Http请求和响应的;User—Json;
- HttpMessageConverters 是从容器中确定;获取所有的HttpMessageConverter;
- 自己给容器中添加HttpMessageConverter,只需要将自己的组件注册容器中
(@Bean,@Component)
-
Automatic registration of MessageCodesResolver (see below).定义错误代码生成规则
-
Automatic use of a ConfigurableWebBindingInitializer bean (see below).
我们可以配置一个ConfigurableWebBindingInitializer来替换默认的;(添加到容器)//初始化WebDataBinder; //请求数据=====JavaBean;
org.springframework.boot.autoconfigure.web:web的所有自动场景;
//If you want to keep Spring Boot MVC features, and you just want to add additional MVC configuration //(interceptors, formatters, view controllers etc.) you can add your own @Configuration class of type //WebMvcConfigurerAdapter , but without @EnableWebMvc . If you wish to provide custom instances of //RequestMappingHandlerMapping , RequestMappingHandlerAdapter or ExceptionHandlerExceptionResolver //you can declare a WebMvcRegistrationsAdapter instance providing such components. //If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with //@EnableWebMvc
2、扩展SpringMVC
<mvc:view‐controller path="/hello" view‐name="success"/>
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/hello"/>
<bean></bean>
</mvc:interceptor>
</mvc:interceptors>
-
编写一个配置类(@Configuration),是WebMvcConfigurer类型;不能标注@EnableWebMvc;
(注:WebMvcConfigurerAdapter已经过时了,官方推荐用WebMvcConfigurer)
-
既保留了所有的自动配置,也能用我们扩展的配置;
//使用WebMvcConfigurer可以来扩展SpringMvc的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer{
/**
* 当我们不需要往页面里传入什么数据的时候,只需要定义一个ViewControllers就可以了
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//浏览器发送/zhuge请求 来到 index.html页面
registry.addViewController("/zhuge").setViewName("index");
}
}
-
原理:
- WebMvcAutoConfiguration是SpringMVC的自动配置类
- 在做其他自动配置时会导入; @Import(EnableWebMvcConfiguration.class) ,比如WebMvcAutoConfigurationAdapter
@Configuration(proxyBeanMethods = false) public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();//下个代码块是实现类 //自动注入说明方法的参数便要从容器中获取,参数就是容器中所有的WebMvcConfigurer @Autowired(required = false) public void setConfigurers(List<WebMvcConfigurer> configurers) { if (!CollectionUtils.isEmpty(configurers)) { this.configurers.addWebMvcConfigurers(configurers); } } //举一个DelegatingWebMvcConfiguration中的例子 @Override protected void addViewControllers(ViewControllerRegistry registry) { this.configurers.addViewControllers(registry); }
class WebMvcConfigurerComposite implements WebMvcConfigurer { private final List<WebMvcConfigurer> delegates = new ArrayList<>(); @Override public void addViewControllers(ViewControllerRegistry registry) { for (WebMvcConfigurer delegate : this.delegates) { //从这里可以看出该方法把容器中所有WebMvcConfigurer都拿来 delegate.addViewControllers(registry); //然后再把这些Configurer的addViewControllers方法都调用一遍 } } 。。。
- 容器中所有的WebMvcConfigurer都会一起起作用;
- 我们的配置类也会被调用;
效果:SpringMVC的自动配置和我们的扩展配置都会起作用;
3、全面接管SpringMvc
SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己配置;所有的SpringMVC的自动配置都失效了我们
需要在配置类中添加==@EnableWebMvc==即可;
补充说明:
- @see : 可以在注释中实现链接跳转
//使用WebMvcConfigurer可以来扩展SpringMvc的功能
@EnableWebMvc
@Configuration
public class MyMvcConfig implements WebMvcConfigurer{
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//浏览器发送/zhuge请求 来到 index.html页面
registry.addViewController("/zhuge").setViewName("index");
}
}
/**
这个时候,我们连静态资源都访问不了了,因为SpringMVC的自动配置都失效了
*/
原理:
为什么有@EnableWebMvc自动配置就失效了?
- @EnableWebMvc的核心
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
- 回头看下WebMvcAutoConfiguration
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
//在容器中没有这个组件({WebMvcConfigurationSupport.class)的时候,这个自动配置类才生效
//然而当我们使用了@EnableWebMvc后,DelegatingWebMvcConfiguration类(WebMvcConfigurationSupport的子类在实例化时会先调用父类构造函数),所以,WebMvcAutoConfiguration就失效了
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
- @EnableWebMvc将WebMvcConfigurationSupport组件导入进来;
- 导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能;
5、如果修改Springboot的默认配置
模式:
- SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(@Bean、@Component)如
果有就用用户配置的,如果没有,才自动配置;如果有些组件可以有多个(ViewResolver)将用户配置的和自己默
认的组合起来; - 在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置
- 在SpringBoot中会有很多的xxxCustomizer帮助我们进行定制配置
6、RestfulCRUD(实验)
1、默认访问登录页面
- 默认访问登录页面
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
}
//所有的WebMvcConfigurer组件都会一起起作用
//这里其实是采用的匿名内部类的方式,只要是WebMvcConfigurer类型的都会被使用
@Bean
public WebMvcConfigurer webMvcConfigurer(){
WebMvcConfigurer configurer = new WebMvcConfigurer() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
registry.addViewController("index.html").setViewName("login");
}
};
return configurer;
}
}
- 首页 (login.html),
- 采用webjars导入的bootstrap
- 使用thymeleaf语法修改href和src
<html lang="en" xmlns:th="http://www.thymeleaf.org">
.........
<!-- Bootstrap core CSS -->
<link href="asserts/css/bootstrap.min.css" th:href="@{/webjars/bootstrap/4.5.2/css/bootstrap.min.css}" rel="stylesheet">
<!---->
<!-- Custom styles for this template -->
<link href="asserts/css/signin.css" th:href="@{/asserts/css/signin.css}" rel="stylesheet">
.........
<img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}" src="asserts/img/bootstrap-solid.svg" alt="" width="72" height="72">
-
这样做的好处
- 当我们修改了项目访问的路径时
server.servlet.context-path=/zhuge
- th:href 和 th:src会自动将添加的路径补到对应的属性值中去 (以下为网页源代码)
<!-- Bootstrap core CSS --> <link href="/zhuge/webjars/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet"> <!----> <!-- Custom styles for this template --> <link href="/zhuge/asserts/css/signin.css" rel="stylesheet"> <img class="mb-4" src="/zhuge/asserts/img/bootstrap-solid.svg" alt="" width="72" height="72">
2、国际化
- 编写国际化文件
- 使用ResourceBundleMessageSource管理国际化资源文件(以前springmvc)
- 在页面使用fmt:message取出国际化内容(以前springmvc)
步骤:
- 编写国际化配置文件,抽取页面需要显示的国际化消息
- SpringBoot自动配置好了管理国际化资源文件的组件;
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {
private static final Resource[] NO_RESOURCES = {};
/**
* Comma‐separated list of basenames (essentially a fully‐qualified classpath
* location), each following the ResourceBundle convention with relaxed support for
* slash based locations. If it doesn't contain a package qualifier (such as
* "org.mypackage"), it will be resolved from the classpath root.
*/
private String basename = "messages";//该属性是在MessageSourceAutoConProperties类下面的
//我们的配置文件可以直接放在类路径下叫messages.properties;(但是我们是放在 i18n下的,所以需要自己指定一下)
@Bean
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
if (StringUtils.hasText(properties.getBasename())) {
//设置国际化资源文件的基础名(去掉语言国家代码的)
messageSource.setBasenames(StringUtils
.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
}
if (properties.getEncoding() != null) {
messageSource.setDefaultEncoding(properties.getEncoding().name());
}
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
return messageSource;
}
。。。
}
spring.messages.basename=i18n.login
#指定一下basename(国际化配置文件的路径)
-
去页面获取国际化的值;
补充说明:
- 在IDEA里,settings里的设置仅对当前项目生效,若要修改所有的,点other settings里的default settings
- 如果中文时有乱码,就在设置file encoding里把转化为ascll码给勾上
<body class="text-center">
<form class="form-signin" action="dashboard.html">
<img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}" src="asserts/img/bootstrap-solid.svg" alt="" width="72" height="72">
<!---->
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
<label class="sr-only" th:text="#{login.username}">Username</label>
<input type="text" class="form-control" placeholder="Username" th:placeholder="#{login.username}" required="" autofocus="">
<label class="sr-only" th:text="#{login.password}">Password</label>
<input type="password" class="form-control" placeholder="Password" th:placeholder="#{login.password}" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me" > [[#{login.remember}]]
<!--注意:这里不能使用th:text="#{login.remember}"
因为text是该标签体里面的内容,而input是用户在页面输入的内容,是字节数的,没有标签体
所以我们要用行内写法
-->
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" th:text="#{login.btn}" type="submit">Sign in</button>
效果:根据浏览器语言设置的信息切换了国际化;
原理:
国际化Locale(区域信息对象);LocaleResolver(获取区域信息对象);
@Bean
@ConditionalOnMissingBean//这个注解的意思是如果springmvc容器中有了LocaleResolver组件,该类就不会被使用
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
//这里指的是如果有设置好的LocaleResolver.FIXED,就用它
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
//否则就用这个AcceptHeaderLocaleResolver的对象()
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}
AcceptHeaderLocaleResolver类:
@Override
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = getDefaultLocale();
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
//默认的就是根据请求头带来的区域信息获取Locale进行国际化
return defaultLocale;
}
Locale requestLocale = request.getLocale();
List<Locale> supportedLocales = getSupportedLocales();
if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
return requestLocale;
}
Locale supportedLocale = findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
}
return (defaultLocale != null ? defaultLocale : requestLocale);
}
这里是网页中的信息
- 点击链接切换国际化(我们要实现自己的LocaleResolver组件)
/**
* @author ZCH
* @date 2020/10/2 0002 - 下午 9:19
*
* 可以在页面的链接上携带区域信息
*/
public class MyLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String l = request.getParameter("l");
//从请求头中接收名为l的属性的值
Locale locale = Locale.getDefault();
//如果l没有值,最后就还是用浏览器默认的
if (!StringUtils.isEmpty(l)){
String[] split = l.split("_");
// “_“ 的左右两边分别是 语言代码 和 国家代码
locale = new Locale(split[0],split[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
}
MyMvcConfig类:
@Bean//将我们的区域解析器放到springmvc容器中去(注:不能直接在MyLocaleResolver类上加@Bean)
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}
3、登录
开发期间模板引擎页面修改以后,要实时生效
- 禁用模板引擎的缓存
# 禁用缓存
spring.thymeleaf.cache=false
- 页面修改完成以后ctrl+f9:重新编译;
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
- 代码实现
补充说明
- 重复提交: 多次提交表单中的数据(这里只说一种情况,就是提交完表单后,不做其他操作直接刷新页面,会提交多次表单)
- 根本原因: Servlet处理完请求后, 直接转发到目标页面, 这样整个业务只发送了一次请求,点击刷新是会一直刷新之前的请求
- 解决办法: 不用转发到另一个页面, 采用重定向的方式跳转到目标页面
- 危害:
- 数据库可能会多次保存相同的数据
- 安全问题,如多次支付等
- 服务器性能受损
@Controller
public class LoginController {
@PostMapping(value = "/user/login")
public String login(@RequestParam("username") String username,
//@RequestParam("username") 用于与表单中name为username的input标签传来的参数对应
@RequestParam("password") String password,
Map<String,Object> map){
if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
//登陆成功,防止表单重复提交,可以重定向到这个主页
return "redirect:/main.html";
}else {
//登录失败
map.put("msg","用户名或密码错误");
return "login";
}
}
}
登陆错误消息的显示
<!--判断-->
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
4、拦截器
拦截器进行登录检查(没登录的不能访问后台)
public class LoginHandlerInterceptor implements HandlerInterceptor {
//目标方法执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object user = request.getSession().getAttribute("loginUser");//没有进行过登录操作,处理器方法就没在session里添加"loginUser"(key),所以user就会为null
if (user == null){
//未登录, 返回登录页面
request.setAttribute("msg","没有权限请先登录");
request.getRequestDispatcher("/index.html").forward(request,response);
return false;
}else {
//已登录,放行请求
return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
注册拦截器
@Bean
public WebMvcConfigurer webMvcConfigurer(){
WebMvcConfigurer configurer = new WebMvcConfigurer() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
registry.addViewController("/index.html").setViewName("login");
registry.addViewController("/main.html").setViewName("dashboard");
}
//注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//静态资源: css,js
//springboot已经做好了静态资源映射(但2.x又没了)
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**")
// "/**" 表示所有的地址都被拦截
.excludePathPatterns("/index.html","/","/user/login","/webjars/bootstrap/**","/asserts/**");
//排除掉不用拦截的请求
/*
注: springboot2.x依赖的 spring 5.x版本, 针对资源的拦截器初始化时有区别,
具体源码在WebMvcConfigurationSupport中,所以我们要手动把静态资源的请求路径添上
*/
}
};
return configurer;
}
将登陆进去后的页面左上角改成当前用户的名字
if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
//登陆成功,防止表单重复提交,可以重定向到这个主页
session.setAttribute("loginUser",username);
return "redirect:/main.html";
}
//其余代码见登录中的 LoginController类
// session.setAttribute("loginUser",username);的作用是让后台左上角显示当前用户名
<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>
<!--将登陆进去后的页面左上角改成当前用户的名字-->
5、实验要求
实验要求:
-
RestfulCRUD:CRUD满足Rest风格;
URI: /资源名称/资源标识 HTTP请求方式区分对资源CRUD操作
普通CRUD(uri来区分操作) | RestfulCRUD | |
---|---|---|
查询 | getEmp | emp—GET |
添加 | addEmp?xxx | emp—POST |
修改 | updateEmp?id=xxx&xxx=xx | emp/{id}—PUT |
删除 | deleteEmp?id=1 | emp/{id}—DELETE |
- 实验的请求架构;
实验功能 | 请求URI | 请求方式 |
---|---|---|
查询所有员工 | emps | GET |
查询某个员工(来到修改页面) | emp/1 | GET |
来到添加页面 | emp | GET |
添加员工 | emp | POST |
来到修改页面(查出员工进行信息回显) | emp/1 | GET |
修改员工 | emp | PUT |
删除员工 | emp/1 | DELETE |
6、CRUD-员工列表
- 在list页面也得加入thymeleaf的语法,并把资源引用改好路径(否则会没有样式)
<html lang="en" xmlns:th="http://www.thymeleaf.org">
..
<!-- Bootstrap core CSS -->
<link href="../../static/asserts/css/bootstrap.min.css" th:href="@{/webjars/bootstrap/4.5.2/css/bootstrap.min.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="../../static/asserts/css/dashboard.css" th:href="@{/asserts/css/dashboard.css}" rel="stylesheet">
<!-- 因为我们修改了项目访问的路径(在8080/后面加了zhuge),所以原来的href肯定是访问不到静态资源的
所以thymeleaf模板后,我们也不需要../../(返回上两层目录),直接可以从项目的根路径下寻找静态资源
我们不能再用../../, 否则在网页中会是 <link href="/zhuge/../../static/asserts/css/bootstrap.min.css" rel="stylesheet"> (这样没有意义,肯定还是访问不到静态资源的) 所以还是用上面代码里的方式比较好
(拦截器里已经把静态资源对应的请求排除了)-->
- thymeleaf公共页面元素抽取
1、抽取公共片段
<div th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</div>
2、引入公共片段
<div th:insert="~{footer :: copy}"></div>
~{templatename::selector}:模板名::选择器
~{templatename::fragmentname}:模板名::片段名
3、默认效果:
insert的公共片段在div标签中
如果使用th:insert等属性进行引入,可以不用写~{}:
行内写法可以加上:[[~{}]];[(~{})];
- 三种引入公共片段的th属性:
- th:insert:将公共片段整个插入到声明引入的元素中
- th:replace:将声明引入的元素替换为公共片段
- th:include:将被引入的片段的内容包含进这个标签中
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
引入方式
<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div>
效果
<div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
</div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
<div>
© 2011 The Good Thymes Virtual Grocery
</div>
- 在dashboard.html中的公共部分代码
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
<!--这里是要被公共提取的上边框th:fragment="topbar"-->
<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>
<!--将登陆进去后的页面左上角改成当前用户的名字-->
<input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
<ul class="navbar-nav px-3">
<li class="nav-item text-nowrap">
<a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Sign out</a>
</li>
</ul>
</nav>
<nav class="col-md-2 d-none d-md-block bg-light sidebar" id="sidebar">
<!--这里是要被公共提取的侧边框 id="sidebar"-->
- 在list.html中引入公共代码
<!--引入抽取的topbar-->
<!--模板名: 会使用thymeleaf的前后缀配置规则进行解析-->
<div th:replace="~{dashboard::topbar}"></div>
<div class="container-fluid">
<div class="row">
<!--引入侧边栏-->
<div th:replace="~{dashboard::#sidebar}"></div>
<!--~{templatename::selector}:模板名::选择器-->
- 把公共部分的代码放入到一个文件夹(commons)下面
- 然后在list.html 和 dashboard.html页面进行引用
dashboard.html
<!--引入topbar-->
<div th:replace="commons/bar::topbar"></div>
<div class="container-fluid">
<div class="row">
<!--引入sidebar-->
<div th:replace="commons/bar::#sidebar"></div>
list.html
<!--引入抽取的topbar-->
<!--模板名: 会使用thymeleaf的前后缀配置规则进行解析-->
<div th:replace="commons/bar::topbar"></div>
<div class="container-fluid">
<div class="row">
<!--引入侧边栏-->
<div th:replace="commons/bar::#sidebar"></div>
<!--~{templatename::selector}:模板名::选择器-->
- 引入片段的时候传入参数(来达成选择员工管理时高亮,Dashboard不亮,或者选择Dashboard时高亮,员工管理不亮):
list.html
<!--引入侧边栏-->
<div th:replace="commons/bar::#sidebar(activeUri='emps')"></div>
<!--~{templatename::selector}:模板名::选择器-->
dashboard.html
<!--引入sidebar-->
<div th:replace="commons/bar::#sidebar(activeUri='main.html')"></div>
<!--它们在引入bar中的公共部分代码时,可以传入一个参数并给它赋值(然后在公共代码部分便可获取到对应的值并进行判断)-->
bar.html
<li class="nav-item">
<a class="nav-link active"
th:class="${activeUri=='main.html'?'nav-link active':'nav-link'}" href="#" th:href="@{/main.html}">
<!--这里是用thymeleaf语法改变a标签的class属性值,
如果接受的参数activeUri 是 main.html的话, 就将calss变成nav-link active(能够高亮),否则就没有active
-->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
Dashboard <span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="#"
th:class="${activeUri=='emps'?'nav-link active':'nav-link'}" th:href="@{/emps}">
<!--这里是用thymeleaf语法改变a标签的class属性值,
如果接受的参数activeUri 是 emps的话, 就将calss变成nav-link active(能够高亮),否则就没有active
-->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
员工管理
</a>
</li>
-
- 将员工列表里的数据改为从请求域里的Map中获取(本来是假的数据–表格)
list.html
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<h2><button class="btn btn-sm btn-success">员工添加</button> </h2>
<!--加入一个员工添加的按钮-->
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>#</th>
<th>lastName</th>
<th>email</th>
<th>gender</th>
<th>department</th>
<th>birth</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<!--从请求域中Map中获取员工信息-->
<tr th:each="emp:${emps}">
<td th:text="${emp.id}"></td>
<td>[[${emp.lastName}]]</td>
<td th:text="${emp.email}"></td>
<td th:text="${emp.gender}==1?'男':'女'"></td>
<td th:text="${emp.department.departmentName}"></td>
<td th:text="${#dates.format(emp.birth,'yyyy-MM-dd HH:mm')}"></td>
<!--用了thymeleaf工具类dates的格式化-->
<td>
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</main>
2. 处理器方法用于查询所有员工返回列表页面
//查询所有员工返回列表页面
@GetMapping("/emps")
public String list(Model model){
Collection<Employee> employees = employeeDao.getAll();
//放在请求域中
model.addAttribute("emps",employees);
//thymeleaf默认就会拼串
//"classpath:/templates/xxxx.html"
return "emp/list";
}
7、CRUD-员工添加
- 添加页面(add.html):其他部分代码与list.html里的一样,这里只是把显示员工的部分改为了一个表单
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<form>
<div class="form-group">
<label>LastName</label>
<input type="text" class="form-control" placeholder="zhangsan">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" class="form-control" placeholder="3272548251@qq.com">
</div>
<div class="form-group">
<label>Gender</label>
<div class="form-check form-check-inline">
<input type="radio" class="form-check-input" name="gender" value="1">
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input type="radio" class="form-check-input" name="gender" value="0">
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>department</label>
<!--提交的是部门的id-->
<select class="form-control">
<option th:value="${dept.id}" th:each="dept:${depts}" th:text="${dept.departmentName}">1</option>
<!--使用th:each遍历来获取每个部门的名称, 注意提交时的value是部门的id-->
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input type="text" class="form-control" placeholder="zhangsan">
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
</main>
//来到员工添加页面
@GetMapping("/emp")
public String toAddPage(Model model){
//来到添加页面,查出所有的部门,在页面显示(在表单中选择部门那一项会有下拉菜单里面显示的是部门名)
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("depts",departments);
return "emp/add";
}
//员工添加
//SpringMvc自动将请求参数和入参对象的属性进行一一绑定; 要求了请求参数的名字和javaBean入参的对象里面的属性名是一样的
@PostMapping("/emp")
public String addEmp(Employee employee){
//来到员工列表页面
System.out.println("保存的员工信息: "+employee);
//保存员工
employeeDao.save(employee);
//redirect: 表示重定向到一个地址 “/”代表当前项目路径
//forward: 表示转发到一个地址
return "redirect:/emps";
}
-
添加员工时很容易发生的一个问题
- 提交的数据格式不对:生日:日期;
2017-12-12;2017/12/12;2017.12.12; - 日期的格式化;SpringMVC将页面提交的值需要转换为指定的类型;
2017-12-12—Date; 类型转换,格式化;
默认日期是按照 / 的方式; - 其实这个问题在前面 Spring MVC auto-configuration 里有提到过
DateTimeFormatters类: public DateTimeFormatters dateFormat(String pattern) { if (isIso(pattern)) { this.dateFormatter = DateTimeFormatter.ISO_LOCAL_DATE; this.datePattern = "yyyy-MM-dd"; //可以看到我们默认的日期格式 } else { this.dateFormatter = formatter(pattern); this.datePattern = pattern; } return this; }
WebMvcAutoConfiguration类: @Bean @Override public FormattingConversionService mvcConversionService() { Format format = this.mvcProperties.getFormat();//通过mvcProperties获取格式化信息,所以我们点到getFormat里去 WebConversionService conversionService = new WebConversionService(new DateTimeFormatters() .dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime())); addFormatters(conversionService); return conversionService; } WebMvcProperties类: @ConfigurationProperties(prefix = "spring.mvc") public class WebMvcProperties { private final Format format = new Format(); 。。。 public Format getFormat() { return this.format; } 。。。 public static class Format { /** * Date format to use, for example `dd/MM/yyyy`. */ private String date; 。。。 } /* 我们可以直接在springboot的配置文件里 用spring.mvc.(WebMvcProperties里的成员变量) 来修改默认的配置 */
spring.mvc.format.date=yyyy-MM-dd
- 提交的数据格式不对:生日:日期;
8、CRUD-员工修改
- 第十四行
<tbody>
<!--从请求域中Map中获取员工信息-->
<tr th:each="emp:${emps}">
<td th:text="${emp.id}"></td>
<td>[[${emp.lastName}]]</td>
<td th:text="${emp.email}"></td>
<td th:text="${emp.gender}==1?'男':'女'"></td>
<td th:text="${emp.department.departmentName}"></td>
<td th:text="${#dates.format(emp.birth,'yyyy-MM-dd HH:mm')}"></td>
<!--用了thymeleaf工具类dates的格式化-->
<td>
<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.id}">编辑</a>
<!--在每一行员工数据的最右边添加修改链接, 并传入员工的id作为参数-->
<a class="btn btn-sm btn-danger">删除</a>
</td>
</tr>
</tbody>
- 写处理修改请求(回显)的处理器方法
//来到修改页面,查出当前员工, 在页面回显
@GetMapping("/emp/{id}")
public String toEditPage(Model model, @PathVariable("id") Integer id){
Employee employee = employeeDao.get(id);
model.addAttribute("emp",employee);
//来到添加页面,查出所有的部门,在页面显示
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("depts",departments);
//回到修改页面(add是一个修改添加二合一的页面)
return "emp/add";
}
-
因为是修改添加二合一的页面(add),所以要处理好如何判断到底是哪个
==${emp!=null}?==是判断的关键语句(三元运算)
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<!--需要区分是添加还是修改-->
<form th:action="@{/emp}" method="post">
<!--发送put请求修改员工数据-->
<!--
1、SpringMvc中配置HiddenHttpMethodFilter;(Springboot已经自动配置好了)
2、页面创建一个post表单
3、创建一个input项, name="_method";值就是我们指定的请求方式
-->
<input type="hidden" name="_method" value="put" th:if="${emp!=null}">
<div class="form-group">
<label>LastName</label>
<input name="lastName" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${emp.lastName}">
</div>
<div class="form-group">
<label>Email</label>
<input name="email" type="email" class="form-control" placeholder="3272548251@qq.com" th:value="${emp!=null}?${emp.email}">
</div>
<div class="form-group">
<label>Gender</label>
<div class="form-check form-check-inline">
<input type="radio" class="form-check-input" name="gender" value="1" th:checked="${emp!=null}?${emp.gender==1}">
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input type="radio" class="form-check-input" name="gender" value="0" th:checked="${emp!=null}?${emp.gender==0}">
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>department</label>
<!--提交的是部门的id-->
<select class="form-control" name="department.id">
<option th:selected="${emp!=null}?${dept.id == emp.id}" th:value="${dept.id}" th:each="dept:${depts}" th:text="${dept.departmentName}">1</option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input name="birth" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${#dates.format(emp.birth,'yyyy-MM-dd HH:mm')}">
</div>
<button type="submit" class="btn btn-primary" th:text="${emp!=null}?'修改':'添加'">添加</button>
</form>
- 写处理修改请求(保存)的处理器方法
//员工修改:需要提交员工id
@PutMapping("/emp")
public String updateEmployee(Employee employee){
System.out.println("修改的员工数据:"+employee);
employeeDao.save(employee);
return "redirect:/emps";
}
<input type="hidden" name="_method" value="put" th:if="${emp!=null}">
<!--其余代码见上面第二个代码块-->
<input type="hidden" name="id" th:if="${emp!=null}" th:value="${emp.id}">
<!--这个标签用于修改时回显员工id-->
9、CRUD-员工删除
- 在list页面的删除按钮的位置
list.html
<!--用了thymeleaf工具类dates的格式化-->
<td>
<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.id}">编辑</a>
<button th:attr="del_uri=@{/emp/}+${emp.id}" class="btn btn-sm btn-danger deleteBtn">删除</button>
<!--th:attr 是为了添加自定义的属性,del_uri是用来改变删除员工及他的id的请求的地址的-->
</td>
</main>
<form id="deleteEmpForm" method="post">
<input type="hidden" name="_method" value="delete">
</form>
<!--这里我猜测Springboot的自动配置的HiddenHttpMethodFilter没有帮我把post改为delete,所以处理器里我又改成了post-->
<!--用jQuery的语法来使表单发出删除员工及他的id的请求-->
<script>
$(".deleteBtn").click(function() {
//删除当前员工的
$("#deleteEmpForm").attr("action",$(this).attr("del_uri")).submit();
return false;
});
</script>
- 用于处理删除员工的处理器方法
//员工删除
@PostMapping("/emp/{id}")//老师用的是DeleteMapping(不过他的版本低,我猜可能后面Springboot的自动配置的HiddenHttpMethodFilter变了,但具体是啥我也不清楚,哈哈~)
public String deleteEmployee(@PathVariable("id") Integer id){
employeeDao.delete(id);
return "redirect:/emps";
}