Springboot学习笔记(一)

    科技2022-07-11  97

    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环境@ConditionalOnJndiJNDI存在指定项

    我们可以在配置文件中写上一项,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查询getEmpemp—GET添加addEmp?xxxemp—POST修改updateEmp?id=xxx&xxx=xxemp/{id}—PUT删除deleteEmp?id=1emp/{id}—DELETE 实验的请求架构; 实验功能请求URI请求方式查询所有员工empsGET查询某个员工(来到修改页面)emp/1GET来到添加页面empGET添加员工empPOST来到修改页面(查出员工进行信息回显)emp/1GET修改员工empPUT删除员工emp/1DELETE

    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"; }
    Processed: 0.217, SQL: 8