Springboot基础(一)

SpringBoot基础(一)

spring官网:https://spring.io/

https://mp.weixin.qq.com/mp/homepage?__biz=Mzg2NTAzMTExNg==&hid=1&sn=3247dca1433a891523d9e4176c90c499

该笔记记录的是SpringBoot2,更新的SpringBoot3版本需要参考最新官方文档

1、概念+入门

1.1 回顾Spring

Spring是一个开源框架,2003 年兴起的一个轻量级的Java开发框架。

目的:解决企业级应用开发的复杂性,简化开发

Spring如何简化Java开发

  1. 基于pojo(实体类)的轻量级和最小侵入性编程,所有东西可以抽象为bean。
  2. 通过IOC,依赖注入(DI)和面向接口实现松耦合。
  3. 基于切面(AOP)和惯例进行声明式编程。
  4. 通过切面和模板减少样式代码,如RedisTemplatexxxTemplate等。

1.2 什么是SpringBoot

Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序,且约定大于配置

SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。可以在一定程度上解决Spring在工程量大的情况下产生的"配置地狱"情景。

SpringBoot的优点

  • 为所有Spring开发者更快的入门
  • 开箱即用,提供各种默认配置来简化项目配置
  • 内嵌式容器简化Web项目(tomcat)
  • 没有冗余代码生成和XML配置的要求

1.3 什么是微服务

微服务是一种架构风格,它要求在开发一个应用时,这个应用必须构建成一系列更小服务的组合,可以通过http的方式进行互通。

单体应用架构

单体应用架构(all in one),代表将所有应用服务都封装在一个应用中。

无论是ERP、CRM或其他系统,一个项目的数据库访问、web访问等等功能,在这个架构下会放到一个war包内。

  • 好处:易于开发和测试,方便部署。需要扩展时,只需要war包复制多份,然后放到多个服务器上,再做个负载均衡即可。
  • 缺点:每次需要修改一小处地方时,就要停掉整个服务,重新打包、部署war包,这对于大型应用的维护很不便利。

微服务架构

微服务打破了all in one的架构方式,把每个功能元素独立出来,把独立出来的功能元素按需求进行动态组合。

在这里插入图片描述

好处:

  1. 节省了调用资源
  2. 每个功能元素的服务都是一个可替换的,可独立升级的软件代码

比如下面这个博客网站

在这里插入图片描述

网站整体由好几个功能卡片构成,左侧的"皮肤"卡片、"设置"卡片,中间的"留言板"卡片,右侧的"音乐播放器"卡片,每张卡片本身就是一个微服务,它们在前端被整合到了一个网页中。

B站的动态页面的构成方式也跟这个差不多。

Martin Flower于2014年3月25日发表论文《Microservice》,解释了什么是微服务。

1.4 第一个SpringBoot项目

环境:

  • jdk 1.8
  • maven
  • springboot2
  • IDEA

创建方式:

  1. 通过官网快速生成一个SpringBoot项目
    1. 打开https://start.spring.io/
    2. 填写项目信息
    3. 点击”Generate Project“按钮生成项目;下载此项目
    4. 解压项目包,并用IDEA以Maven项目导入,一路下一步即可,直到项目导入完毕。
    5. 如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。
  2. IDEA生成
    1. 创建一个新项目
    2. 选择spring initalizr , 可以看到默认就是去官网的快速构建工具那里生成项目
    3. 填写项目信息
    4. 选择初始化的组件(初学勾选 Web 即可)
    5. 填写项目路径
    6. 等待项目构建成功

项目初始结构:

在这里插入图片描述

pom.xml

<!--父依赖-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.6</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>



<dependencies>
    <!--web场景启动器-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!--springboot单元测试-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <!-- 剔除依赖 -->
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

<build>
    <plugins>
        <!-- 打包插件 -->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

编写一个http接口

  1. 项目启动类的同级目录下,新建一个controller包
  2. 在保重新建HelloController
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        return "Hello World";
    }
}

在这里插入图片描述

  1. 从主程序启动项目,浏览器发起请求,看页面返回;控制台会输出 Tomcat 访问的端口号

在这里插入图片描述

将项目打成jar包(点击maven的package指令)

在这里插入图片描述

打包后可以放到其他项目里进行调用

在这里插入图片描述

启动图标

可以通过修改resources/banner.txt中的内容改变启动项目时的大字图标(没有就新建)。

相关图案可以去https://www.bootschool.net/ascii 生成

banner.txt样例:

 █████ ██████   █████ ███████████ █████ ██████   █████ █████ ███████████ ██████████
▒▒███ ▒▒██████ ▒▒███ ▒▒███▒▒▒▒▒▒█▒▒███ ▒▒██████ ▒▒███ ▒▒███ ▒█▒▒▒███▒▒▒█▒▒███▒▒▒▒▒█
 ▒███  ▒███▒███ ▒███  ▒███   █ ▒  ▒███  ▒███▒███ ▒███  ▒███ ▒   ▒███  ▒  ▒███  █ ▒ 
 ▒███  ▒███▒▒███▒███  ▒███████    ▒███  ▒███▒▒███▒███  ▒███     ▒███     ▒██████   
 ▒███  ▒███ ▒▒██████  ▒███▒▒▒█    ▒███  ▒███ ▒▒██████  ▒███     ▒███     ▒███▒▒█   
 ▒███  ▒███  ▒▒█████  ▒███  ▒     ▒███  ▒███  ▒▒█████  ▒███     ▒███     ▒███ ▒   █
 █████ █████  ▒▒█████ █████       █████ █████  ▒▒█████ █████    █████    ██████████
▒▒▒▒▒ ▒▒▒▒▒    ▒▒▒▒▒ ▒▒▒▒▒       ▒▒▒▒▒ ▒▒▒▒▒    ▒▒▒▒▒ ▒▒▒▒▒    ▒▒▒▒▒    ▒▒▒▒▒▒▒▒▒▒ 


在这里插入图片描述

2、SpringBoot运行原理

下面针对前面的HelloSpringBoot项目目录进行分析

pom.xml

父依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.6</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

ctrl+鼠标左键,点击spring-boot-starter-parent,内部还有一个父依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.6.6</version>
</parent>

这里才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心。

以后导入依赖默认不需要写版本,但是如果导入的包没有在依赖中管理,就需要手动配置版本。

启动器 spring-boot-starter

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

springboot-boot-starter-xxx:spring-boot的场景启动器

spring-boot-starter-web:帮我们导入了web模块正常运行所依赖的组件

SpringBoot将所有的功能场景都抽取出来,做成一个个的starter (启动器,可以联想微服务),只需要在项目中引入这些starter即可,引入start后所有相关的依赖都会自动导入,。根据需求导入相关的场景启动器即可 。未来也可以自定义 starter。

主启动类

默认的主启动类

@SpringBootApplication   //标注一个主程序类,说明这是一个SpringBoot应用
public class HelloSpringBootApplication {

    public static void main(String[] args) {
        //启动一个方法(服务)
        SpringApplication.run(HelloSpringBootApplication.class, args);
    }
}

@SpringBootApplication

标注在某个类上说明这个类是SpringBoot的主配置类 , SpringBoot应用通过这个类启动。

进入这个注解,查看注解内部定义:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    //......
}

@ComponentScan

这个注解在Spring中很重要 ,它对应XML配置中的元素。

作用:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中。

@SpringBootConfiguration

作用:SpringBoot的配置类 ,标注在某个类上 , 表示这是一个SpringBoot的配置类;

进去这个注解查看具体定义

@Configuration
public @interface SpringBootConfiguration {}

@Component
public @interface Configuration {}

@Configuration说明这是一个配置类 ,配置类就是对应Spring的xml 配置文件。

@Component 说明启动类本身也是Spring中的一个组件,负责启动应用。

下面回到 SpringBootApplication 注解中继续看。

@EnableAutoConfiguration

作用:开启自动配置功能,让SpringBoot帮我们自动配置。

进入这个注解查看具体定义

@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

@AutoConfigurationPackage: 自动配置包

@Import({Registrar.class})
public @interface AutoConfigurationPackage {
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
}

@import:Spring底层注解@import , 给容器中导入一个组件

Registrar.class 作用:将主启动类的所在包及包下面 所有子包里面的 所有组件扫描到Spring容器中

@Import({AutoConfigurationImportSelector.class}):给容器导入组件。这个组件会自动配置导入选择器。

1、这个注解的源码内有一个方法:

//获得候选的配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    //返回一开始看到的启动自动导入配置文件的注解类;EnableAutoConfiguration
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
    return configurations;
}

2、这个方法又调用了SpringFactoriesLoader类的静态方法,进入这个方法查看源码

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    ClassLoader classLoaderToUse = classLoader;
    if (classLoader == null) {
        classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }

    String factoryTypeName = factoryType.getName();
    //这里又调用了loadSpringFactories方法
    return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

3、进入loadSpringFactories方法

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    Map<String, List<String>> result = (Map)cache.get(classLoader);
    if (result != null) {
        return result;
    } else {
        HashMap result = new HashMap();

        try {
            //去获取一个资源 "META-INF/spring.factories"
            Enumeration urls = classLoader.getResources("META-INF/spring.factories");

             //将读取到的资源遍历,封装成为一个Properties
            while(urls.hasMoreElements()) {
                URL url = (URL)urls.nextElement();
                UrlResource resource = new UrlResource(url);
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                Iterator var6 = properties.entrySet().iterator();

                while(var6.hasNext()) {
                    Entry<?, ?> entry = (Entry)var6.next();
                    String factoryTypeName = ((String)entry.getKey()).trim();
                    String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                    String[] var10 = factoryImplementationNames;
                    int var11 = factoryImplementationNames.length;

                    for(int var12 = 0; var12 < var11; ++var12) {
                        String factoryImplementationName = var10[var12];
                        ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
                            return new ArrayList();
                        })).add(factoryImplementationName.trim());
                    }
                }
            }

            result.replaceAll((factoryType, implementations) -> {
                return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
            });
            cache.put(classLoader, result);
            return result;
        } catch (IOException var14) {
            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
        }
    }
}

4、发现一个多次出现的文件:spring.factories,全局搜索它

在这里插入图片描述

随便点进一个类进行查看

在这里插入图片描述

可以发现这个类是JavaConfig配置类,且都注入了Bean。其他类同理

所以,自动配置的真正实现,是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中。

小结

  1. SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值
  2. 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
  3. 整个JavaEE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中;
  4. 它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并配置好这些组件 ;
  5. 有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;
@SpringBootConfiguration   //springboot的配置
	@Configuration   //spring配置类
	@Component	//说明这是一个spring的组件

@EnableAutoConfiguration   //自动配置
	@AutoConfigurationPackage   //自动配置包
		@Import({Registrar.class})     //自动配置包注册
	@Import({AutoConfigurationImportSelector.class})   //自动配置导入选择

SpringApplication

@SpringBootApplication
public class HelloSpringBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(HelloSpringBootApplication.class, args);
    }
}

SpringApplication.run分为两部分:SpringApplication的实例化、run方法的执行

SpringApplication做了四件事:

  1. 判断应用的类型是普通的项目还是web项目
  2. 查找并加载所有可用初始化工具,设置到initializers属性中
  3. 找出所有的应用程序监听器,设置到listeners属性中
  4. 推断并设置main方法的定义类,找到运行的主类

构造器:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    //......
    
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = this.deduceMainApplicationClass();
}

run方法流程

在这里插入图片描述

3、yaml配置注入

SpringBoot使用一个全局的配置文件 , 配置文件名称是固定的

  • application.properties

    • 语法结构 :key=value
  • application.yml (空格不能忽略)

    • 语法结构 :key:[空格] value

**配置文件的作用 :**修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给已经自动配置好了。

比如我们可以在配置文件中修改Tomcat 默认启动的端口号

server.port=8081

3.1 yaml概念

YAML是 “YAML Ain’t a Markup Language” (YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)

这种语言以数据作为中心,而不是以标记语言为重点

以前的配置文件,大多数都是使用xml来配置;比如一个简单的端口配置,对比下yaml和xml

xml

<server>
    <port>8081<port>
</server>

yaml

server:
  port: 8080

3.2 yaml基础语法

  1. 空格不能省略
  2. 以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
  3. 属性和值的大小写都十分敏感。

字面量:普通的值 [ 数字,布尔值,字符串 ]

字面量直接写在后面即可 , 字符串默认不用加上双引号或者单引号

k: v
  • ""双引号,会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思。比如 :name: “xxxx \n yyyy” 输出 :xxxx 换行 yyyy
  • ''单引号,不会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出。比如 :name: ‘xxxx \n yyyy’ 输出 :xxxx \n yyyy

对象、map

k:
  v1:
  v2:

对象一般写法

student:
  name: alice
  age: 20

也可以写成行内形式

student: {name: alice,age: 20}

注::后别忘了加一个空格

数组(List、set)

-表示数组中的每一个元素

pets:
  - cat
  - dog
  - pig

也可以写成行内形式

pets: [cat,dog,pig]

3.3 注入配置文件

yaml可以为实体类赋值

  1. 在springboot项目中的resources目录下新建一个文件 application.yml

  2. 编写一个实体类Student

    @Component  //将bean注册到容器中
    public class Student {
        private String name;
        private int age;
        //constructor、getter、setter......
    }
    
  3. 老方法:使用@Value注解给bean注入属性

    @Component  //将bean注册到容器中
    public class Student {
        @Value("alice")
        private String name;
        @Value("20")
        private int age;
        //constructor、getter、setter......
    }
    
  4. 测试

    @SpringBootTest
    class Springboot02ConfigApplicationTests {
        @Autowired //自动注入
        Student student;
    
        @Test
        void contextLoads() {
            System.out.println(student);
        }
    }
    

    在这里插入图片描述

  5. 再写一个Person类

    @Component
    public class Person {
        private String name;
        private int age;
        private Boolean happy;
        private Date birth;
        private Map<String,Object> maps;
        private List<Object> lists;
        private Student student;
    
        //constructor、getter、setter......
    }
    
  6. 使用yaml写好配置内容

    person:
      name: mike
      age: 40
      happy: true
      birth: 1970/1/1
      maps: {key1: value1,key2: value2}
      lists: [code,music,book]
      student: {name: 张三,age: 18}
    
  7. 将yaml注入到类中

    @Component
    @ConfigurationProperties(prefix = "person")
    public class Person {
        private String name;
        private int age;
        private Boolean happy;
        private Date birth;
        private Map<String,Object> maps;
        private List<Object> lists;
        private Student student;
    
        //constructor、getter、setter......
    }
    

    如果出现Spring Boot Configureation Annotation Processor not found in classpath, 可以根据官方文档导入下面依赖

    <!-- 导入配置文件处理器,配置文件进行绑定就会有提示,需要重启 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
    </dependency>
    

​ 为了防止中文编码失败,可以进行如下配置

在这里插入图片描述

  1. 测试

    @SpringBootTest
    class Springboot02ConfigApplicationTests {
        @Autowired //自动注入
        Person person;
    
        @Test
        void contextLoads() {
            System.out.println(person);
        }
    }
    

    在这里插入图片描述

3.4 加载指定的配置文件

@PropertySource :加载指定的配置文件;

@ConfigurationProperties:默认从全局配置文件中获取值;

  1. 再resources目录下新建person.properties文件

    name=张三
    
  2. 在类中指定加载person.properties文件

    @Component
    //@ConfigurationProperties(prefix = "person")
    
    @PropertySource(value="classpath:person.properties")
    public class Person {
        @Value("${name}")
        private String name;
        
        //other
        //constructor、getter、setter......
    }
    
  3. 测试
    在这里插入图片描述

3.5 配置文件占位符

yaml还可以生成随机数

person:
  name: mike${random.uuid} #随机uuid
  age: ${random.int}  #随机int
  happy: true
  birth: 1970/1/1
  maps: {key1: value1,key2: value2}
  lists: [code,music,book]
  student:
    name: ${person.hello:other}_张三
    age: 18

测试:

在这里插入图片描述

3.6 两种注入方式对比

@Configuration@Value
功能批量注入配置文件中的属性一个个属性进行指定
松散绑定(松散语法)×
SpEL×
JSR303数据校验×
复杂类型封装×
  • @ConfigurationProperties只需要在类的头部写一次即可 , @Value则需要在类的每个属性上都添加
  • 松散绑定。比如在yml中写一个last-name,这个属性名可以等价于lastName,以此增大了属性匹配范围
  • JSR303数据校验。类似于过滤器验证,保证数据的合法性,比如可以通过@Email()属性来判断属性值是否符合邮箱格式

小结:

  • 配置yml和配置properties都可以获取到值 ,最好使用yml
  • 如果在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @Value
  • 如果专门编写了一个JavaBean来和配置文件进行一一映射,就用`@configurationProperties

3.7 JSR303数据校验(*)

Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。这里来写个注解让的name只能支持Email格式

@Component
@ConfigurationProperties(prefix = "person")
@Validated //数据校验
public class Person {
    @Email(message="邮箱格式错误")  //判定是否符合邮箱格式
    private String name;
    //......
    //constructor、getter、setter......
}

在这里插入图片描述

在这里插入图片描述

常见参数

空检查
@Null       验证对象是否为null
@NotNull    验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank   检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty   检查约束元素是否为NULL或者是EMPTY.
    
Booelan检查
@AssertTrue     验证 Boolean 对象是否为 true  
@AssertFalse    验证 Boolean 对象是否为 false  
    
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内  
@Length(min=, max=) string is between min and max included.

日期检查
@Past       验证 Date 和 Calendar 对象是否在当前时间之前  
@Future     验证 Date 和 Calendar 对象是否在当前时间之后  
@Pattern    验证 String 对象是否符合正则表达式的规则

......

多环境切换

编写主配置文件时,可以将文件名固定为application-{title}.properties/yml,用来指定多个环境版本。

比如:

  • 可以用application-test.properties 代表测试环境配置
  • 可以用application-dev.properties代表开发环境配置

SpringBoot默认使用application.properties主配置文件

通过下面配置来选择要激活的环境

#比如在配置文件中指定使用dev环境,我们可以通过设置不同的端口号进行测试;
#启动SpringBoot,就可以看到已经切换到dev下的配置了;
spring.profiles.active=dev

yaml的多文档块

使用yml可以不需要创建多个文件,文件内部通过---划分配置内容

server:
  port: 8081
#选择要激活那个环境块
spring:
  profiles:
    active: prod

---

server:
  port: 8083
spring:
  profiles: dev #配置环境的名称

---

server:
  port: 8084
spring:
  profiles: prod  #配置环境的名称

注:如果yml和properties都配置了端口,并且没有激活其他环境,会默认使用properties配置文件。

配置文件加载位置

springboot 启动会扫描以下位置的application.properties/yml文件作为Spring boot的默认配置文件。

优先级1(最优先):
	file:./config/
	项目路径下的config文件夹配置文件
优先级2:
	file:./
	项目路径下配置文件
优先级3:
	classpath:/config/
	资源路径下的config文件夹配置文件
优先级4:
	classpath:/
	资源路径下配置文件

SpringBoot会从这四个位置全部加载主配置文件,互补配置。

4、自动配置原理

官方配置的详细介绍https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties

HttpEncodingAutoConfiguration(Http编码自动配置)为例,查看它的源码

//表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件
@Configuration(
    proxyBeanMethods = false
)

/* 启动指定类的ConfigurationProperties功能:
	进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来,
	并把HttpProperties加入到ioc容器中
*/
@EnableConfigurationProperties({ServerProperties.class})


/* Spring底层@Condition注解
	根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会失效。
	这里会判定当前的应用是不是web应用,如果时,当前配置类生效
*/
@ConditionalOnWebApplication(
    type = Type.SERVLET
)

//判断当前项目有没有这个类CharacterEncodingFilter,即SpringMVC中解决乱码的过滤器
@ConditionalOnClass({CharacterEncodingFilter.class})


/* 判断配置文件中是否存在某个配置:spring.http.encoding.enabled
	如果不存在,判断也是成立的
	即使配置文件中不配置spring.http.encoding.enabled=true,也是默认生效的
*/
@ConditionalOnProperty(
    prefix = "server.servlet.encoding",
    value = {"enabled"},
    matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
    // 和SpringBoot中的配置文件形成映射
    private final Encoding properties;
	// 只有一个有参构造器的情况下,参数值会从容器中拿
    public HttpEncodingAutoConfiguration(ServerProperties properties) {
        this.properties = properties.getServlet().getEncoding();
    }

    //给容器中添加一个组件,这个组件的某些值需要从properties中获取
    @Bean
    @ConditionalOnMissingBean //判断容器中有没有这个组件
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE));
        return filter;
    }
    //......
}

小结:根据当前不同的条件判断,决定这个配置类是否生效。

  • 一但这个配置类生效,这个配置类就会给容器中添加各种组件
  • 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性和配置文件绑定
  • 所有在配置文件中能配置的属性都封装在xxxxProperties类中(上面的例子是ServerProperties)
  • 配置文件能配置什么就可以参照某个功能对应的这个属性类
@ConfigurationProperties(
    prefix = "server",
    ignoreUnknownFields = true
)
public class ServerProperties {
    private Integer port;
    private InetAddress address;
	//......
}
  1. SpringBoot启动会加载大量的自动配置类
  2. 根据需求,查看相关的功能有没有在SpringBoot默认写好的自动配置类当中
  3. 然后再看这个自动配置类中到底配置了哪些组件
  4. 给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。只需要在配置文件中指定这些属性的值即可。

xxxxAutoConfigurartion:自动配置类,给容器中添加组件

xxxxProperties:封装配置文件中相关属性

@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属性,可以让控制台打印自动配置报告,这样可以更容易知道哪些自动配置类有效。

#开启springboot的调试类
debug=true

Positive matches:(自动配置类启用的:正匹配)

Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)

Unconditional classes: (没有条件的类)

在这里插入图片描述

5、自定义Starter

启动器模块是一个空的jar文件,仅提供辅助性依赖管理,这些依赖可能用于自动装配或者其他类库。

命名约定:

官方命名:

  • 前缀:spring-boot-starter-xxx(如spring-boot-starter-web)

自定义命名:

  • xxx-spring-boot-starter(如mybatis-spring-boot-starter)

5.1 编写启动器

  1. 新建空项目spring-boot-starter-diy

  2. 新建一个普通Maven模块:infinite-spring-boot-starter

  3. 新建一个Springboot模块:infinite-spring-boot-starter-autoconfigure
    在这里插入图片描述

  4. 在starter中导入autoconfig的依赖

  5. 删除autoconfigure项目下多余的文件,pom中只留下starter依赖,这是所有启动器的基本配置

  6. starter模块中编写一个自己的服务

    package com.infinite;
    public class HelloService {
        HelloProperties helloProperties;
    
        public HelloProperties getHelloProperties() {
            return helloProperties;
        }
    
        public void setHelloProperties(HelloProperties helloProperties) {
            this.helloProperties = helloProperties;
        }
    
        public String sayHello(String name){
            return helloProperties.getPrefix() + name + helloProperties.getSuffix();
        }
    }
    
  7. 同级目录下编写HelloProperties配置类

    @ConfigurationProperties(prefix="infinite.hello")
    public class HelloProperties {
        private String prefix;
        private String suffix;
    
        public String getPrefix(){
            return prefix;
        }
        public void setPrefix(String prefix){
            this.prefix=prefix;
        }
        public String getSuffix(){
            return suffix;
        }
        public void setSuffix(String suffix){
            this.suffix=suffix;
        }
    }
    
  8. 同级目录下编写自动配置类并注入bean,测试

    @Configuration
    @ConditionalOnWebApplication   //使web应用生效
    @EnableConfigurationProperties(HelloProperties.class)
    public class HelloServiceAutoConfiguration {
        @Autowired
        HelloProperties helloProperties;
    
        @Bean
        public HelloService helloService(){
            HelloService helloService = new HelloService();
            helloService.setHelloProperties(helloProperties);
            return helloService;
        }
    }
    
  9. resources中编写一个自己的META-INF/spring.factories文件

    #Auto Configure
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.infinite.HelloServiceAutoConfiguration
    
  10. 编写完成后,安装到maven仓库中
    在这里插入图片描述

5.2 测试自己写的启动器

  1. 新建一个SpringBoot项目(web)

  2. 导入启动器

    <dependency>
        <groupId>org.example</groupId>
        <artifactId>infinite-spring-boot-starter</artifactId>
        <version>1.0-SNAPSHOT</version>
        <scope>compile</scope>
    </dependency>
    
  3. 编写一个HelloController类测试自己写的接口

    package com.infinite.controller;
    @RestController
    public class HelloController {
        @Autowired
        HelloService helloService;
    
        @RequestMapping("/hello")
        public String hello(){
            return helloService.sayHello("zxc");
        }
    }
    
  4. 编写配置文件application.properties

    infinite.hello.prefix="xxxx"
    infinite.hello.suffix="yyyy"
    
  5. 启动项目进行测试

在这里插入图片描述

6、SpringBoot Web开发

web开发的一些需求:

  • 导入静态资源
  • 首页(Index)
  • jsp、模板引擎Thymeleaf
  • 装配扩展SpringMVC
  • CURD
  • 国际化(如页面中英文切换)

6.1 导入静态资源

注:先搭建一个普通的springboot的web项目,并测试。

6.1.1 WebMvcAutoConfiguration类

以前写web项目时,会将所有的页面导入到webapp目录下。

现在的pom以jar为打包方式,SpringBoot可以依靠这种方式写页面但是SpringBoot对于静态资源放置的位置有所规定。

静态资源映射规则:SpringBoot中,SpringMVC的web配置都写在WebMvcAutoConfiguration类中,打开它的源码,有几个相关的方法(springboot版本不同,源码也可能有区别)

public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        // 警用默认资源处理
        logger.debug("Default resource handling disabled");
    } else {
        // 配置webjars
        this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
        //静态资源配置
        this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
            registration.addResourceLocations(this.resourceProperties.getStaticLocations());
            if (this.servletContext != null) {
                ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
                registration.addResourceLocations(new Resource[]{resource});
            }

        });
    }
}

private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
    this.addResourceHandler(registry, pattern, (registration) -> {
        registration.addResourceLocations(locations);
    });
}

private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, Consumer<ResourceHandlerRegistration> customizer) {
    if (!registry.hasMappingForPattern(pattern)) {
        ResourceHandlerRegistration registration = registry.addResourceHandler(new String[]{pattern});
        customizer.accept(registration);
        //缓存控制
        registration.setCachePeriod(this.getSeconds(this.resourceProperties.getCache().getPeriod()));   
        registration.
            setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
        registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
        this.customizeResourceHandlerRegistration(registration);
    }
}

第7行及相关方法的代码设置了webjars目录的寻找路径。这里代表所有形如/webjars/**的路径,都会转为classpath:/META-INF/resources/webjars/路径进行资源寻找。

6.1.2 webjars

Webjars本质就是以jar包的方式引入静态资源 ,通过pom映入相关资源的依赖即可引入需要的静态资源。

https://www.webjars.org

比如要使用jquery,可以去官网找到依赖并写进pom

<dependency>
    <groupId>org.webjars.npm</groupId>
    <artifactId>jquery</artifactId>
    <version>3.6.0</version>
</dependency>

在这里插入图片描述

启动测试,根据图中resources下的路径找到jquery.js文件。浏览器输入localhost:8080/webjars/jquery/3.6.0/dist/jquery.js显示文件具体内容

在这里插入图片描述

再导入redis-fast-driver,同理也可以查询相关的文件

在这里插入图片描述

6.1.3 导入自己的静态资源

WebMvcAAutoConfiguration类中有一个方法getStaticLocations(),它从属于WebProperties类下的Resources类,进入这个方法

public String[] getStaticLocations() {
    return this.staticLocations;
}

这个staticLocations,是当前所在的Resources类的一个属性

private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{
    "classpath:/META-INF/resources/", 
    "classpath:/resources/", 
    "classpath:/static/", 
    "classpath:/public/"
};
private String[] staticLocations;

CLASSPATH_RESOURCE_LOCATIONS存放了目前可识别的静态资源的路径,在resources(classpath)目录下创建对应的路径,写入静态资源文件。启动测试后,浏览器输入localhost:8080/文件名,会依次从上面的四个路径中寻找相关文件。文件重名情况下,按照路径查找的优先级,显示首次找到的文件。

在这里插入图片描述

6.1.4 自定义静态资源路径

可以通过application.properties配置文件来指定哪些文件夹用于放静态资源文件

spring.web.resources.static-locations=classpath:/xxxyyy/,classpath:/infinite/

一旦自己定义了静态文件夹的路径,原来的自动配置会失效

在这里插入图片描述

6.1.5 首页处理

继续读WebMvcAutoConfiguration类的源码,其中有一个与首页相关的映射

 @Bean
        public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
            WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
            welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
            welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
            return welcomePageHandlerMapping;
        }

进入getWelcomePage()方法

private Resource getWelcomePage() {
    String[] var1 = this.resourceProperties.getStaticLocations();
    int var2 = var1.length;

    for(int var3 = 0; var3 < var2; ++var3) {
        String location = var1[var3];
        Resource indexHtml = this.getIndexHtml(location);
        if (indexHtml != null) {
            return indexHtml;
        }
    }

    ServletContext servletContext = this.getServletContext();
    if (servletContext != null) {
        return this.getIndexHtml((Resource)(new ServletContextResource(servletContext, "/")));
    } else {
        return null;
    }
}

// 首页即index.html
private Resource getIndexHtml(String location) {
    return this.getIndexHtml(this.resourceLoader.getResource(location));
}

private Resource getIndexHtml(Resource location) {
    try {
        Resource resource = location.createRelative("index.html");
        if (resource.exists() && resource.getURL() != null) {
            return resource;
        }
    } catch (Exception var3) {
    }

    return null;
}

静态目录下的所有index.html都会由\**映射。输入localhost/8080/,默认查找index.html

在这里插入图片描述

默认状态下的查找优先级:

classpath:/META-INF/resources/>classpath:/resources/>classpath:/static/ >classpath:/public/

网站图标

Spring Boot在配置的静态内容位置中查找 favicon.ico。如果存在这样的文件,它将自动用作应用程序的favicon。

  1. 在静态资源目录下放置图标
  2. 清除浏览器缓存,刷新网页
    在这里插入图片描述

6.2 Thymeleaf

模板引擎

之前用过的jsp也是一种模板引擎,模板引擎的主要目的是将写好的页面模板和后台数据进行整合后,生成前端显示的页面。

在这里插入图片描述

SpringBoot推荐使用Thymeleaf模板引擎

引入Thymeleaf

Thymeleaf官网:https://www.thymeleaf.org/

Thymeleaf的Github 主页:https://github.com/thymeleaf/thymeleaf

<!--thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

使用Thymeleaf

打开Thymeleaf的自动配置类:ThymeleafProperties

@ConfigurationProperties(
    prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING;
    //默认前缀
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    //默认后缀
    public static final String DEFAULT_SUFFIX = ".html";
    private boolean checkTemplate = true;
    private boolean checkTemplateLocation = true;
    private String prefix = "classpath:/templates/";
    private String suffix = ".html";
    private String mode = "HTML";
    private Charset encoding;
    //......
}

只需要把html页面放在类路径(classpath)下的templates目录下,thymeleaf就可以帮我们自动渲染页面

使用例

  1. 编写一个TestController

    package com.infinite.controller;
    @Controller
    public class TestController {
        @RequestMapping("/t1")
        public String test1(){
            // classpath:/template/test.html
            return "test";
        }
    }
    
  2. 编写一个测试页面 test.html 放在 templates 目录下

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <h1>TEST1</h1>
    
    </body>
    </html>
    
  3. 测试

在这里插入图片描述

6.2.1 Thymeleaf语法

Thymeleaf 官网:https://www.thymeleaf.org/

查找数据
  1. 修改测试请求,增加数据传输

    @Controller
    public class TestController {
        @GetMapping("/t1")
        public String test(Model model){
            //存入数据
            model.addAttribute("msg","Hello,Thymeleaf");
            //classpath:/templates/test.html
            return "test";
        }
    }
    
  2. 使用Thymeleaf前,要在html文件中导入命名空间的约束,方便提示

    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
  3. 前端页面

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
        <head>
            <meta charset="UTF-8">
            <title>TEST</title>
        </head>
        <body>
            <h1>测试页面</h1>
    
            <!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样-->
            <div th:text="${msg}"></div>
        </body>
    </html>
    
  4. 测试

在这里插入图片描述

基本语法

标签

html的标签元素前添加th:,说明这个标签会由Thymeleaf管理。

在这里插入图片描述

表达式

Simple expressions:(表达式语法)
Variable Expressions: ${...}: 获取变量值;OGNL
    1)、获取对象的属性、调用方法
    2)、使用内置的基本对象:#18
         #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.

    3)、内置的一些工具对象:
      #execInfo : information about the template being processed.
      #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.
==================================================================================

  Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
  Message Expressions: #{...}:获取国际化内容
  Link URL Expressions: @{...}:定义URL;
  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: _
使用例
  1. 编写给TestController添加新方法

    @RequestMapping("/t2")
    public String test2(Map<String,Object> map){
        //存入数据
        map.put("msg","<h1>Hello</h1>");
        map.put("users", Arrays.asList("alice","bob"));
        //classpath:/template/test.html
        return "test";
    }
    
  2. 测试页面取出数据

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>TEST</title>
    </head>
    <body>
    <h1>测试页面</h1>
    
    <!--不转义-->
    <div th:text="${msg}"></div>
    <!--转义-->
    <div th:utext="${msg}"></div>
    
    <!--遍历数据-->
    <!--th:each每次遍历都会生成当前这个标签:官网#9-->
    <h4 th:each="user:${users}" th:text="${user}"></h4>
    
    <h4>
        <!--行内写法:官网#12-->
        <span th:each="user:${users}">[[${user}]]</span>
    </h4>
    
    </body>
    </html>
    
  3. 测试

在这里插入图片描述

6.3 MVC自动配置

注:多读源码和官方文档

官网文档:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration

6.3.1 官方介绍

原文

在这里插入图片描述

翻译

自动配置在Spring默认设置的基础上添加了以下功能:

  • 包含视图解析器ContentNegotiatingViewResolverBeanNameViewResolver的bean
  • 支持静态资源的路径,以及webjars
  • 自动注册Converter转换器(网页将数据传到后台自动封装成为对象的类,比如把"1"转换为int类型)、Formatter格式化器(比如页面传来一个2022-2-2,它会自动格式化为Date对象)
  • 支持HttpMessageConverters,它在SpringMVC中用于转换Http请求和相应,如对象和json字符串的转换。
  • 自动注册MessageCodesResolver,用于定义错误代码生成规则
  • 支持首页定制
  • 支持图标定制
  • 自动初始化数据绑定器ConfigurableWebBindingInitializer

如果你希望保留Spring Boot MVC功能,并且希望添加其他MVC配置(拦截器、格式化程序、视图控制器和其他功能),则可以添加自己的@configuration类,类型为WebMvcConfiguer,但不添加@EnableWebMvc。如果希望提供RequestMappingHandlerMappingRequestMappingHandlerAdapterExceptionHandlerExceptionResolver的自定义实例,则可以声明WebMVCregistrationAdapter实例来提供此类组件。

如果你想完全控制Spring MVC,可以添加自己的@Configuration,并用@EnableWebMvc进行注释。

6.3.2 ContentNegotiatingViewResolver(内容协商视图解析器)

探究源码

自动配置ViewResolver,即之前学习SpringMVC时用到的视图解析器。

这个类可以根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发、重定向)

WebMvcAutoConfiguration中搜索这个类,有如下相关方法

@Bean
@ConditionalOnBean({ViewResolver.class})
@ConditionalOnMissingBean(
    name = {"viewResolver"},
    value = {ContentNegotiatingViewResolver.class}
)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
    ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
    resolver.      setContentNegotiationManager((ContentNegotiationManager)beanFactory.getBean(ContentNegotiationManager.class));
    
    resolver.setOrder(-2147483648);
    return resolver;
}

进入这个类,找到对应的解析视图的代码

@Nullable //参数可以为null
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;
        }
    }
	//......
}

上面代码第8行有一个getCandidateViews()方法,它用于获取所有的视图解析器,并通过while循环逐个解析

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;
}

综上可以大致了解到ContentNegotiatingViewResolver组合所有的视图解析器的功能。

下面再了解一下它是怎么进行组合的。类内有一个属性viewResolvers,它通过下面方法赋值

protected void initServletContext(ServletContext servletContext) {
    //这里从beanFactory工具中获取容器中的所有视图解析器
    // ViewRescolver.class 用于组合所有视图解析器
    Collection<ViewResolver> matchingBeans =BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(),ViewResolver.class).values();
    ViewResolver viewResolver;
    if (this.viewResolvers == null) {
        this.viewResolvers = new ArrayList(matchingBeans.size());
    	//......
    }
    //......
}
自定义视图解析器

可以自己给容器中添加一个视图解析器,ContentNegotiatingViewResolver类就会进行自动组合

  1. 在主程序中写一个视图解析器

    package com.infinite.config;
    
    @Configuration
    public class MyMvcConfig {
        @Bean
        public ViewResolver myViewResolver(){
            return new MyViewResolver();
        }
    
        //写一个静态内部类
        //视图解析器需要实现ViewResolver接口
        private static class MyViewResolver implements ViewResolver{
            @Override
            public View resolveViewName(String s, Locale locale) throws Exception{
                return null;
            }
        }
    }
    
  2. debug追踪

进入DispatcherServlet类中(所有相关的请求会经过这个类),对doDispatcher()方法进行断点调试

在这里插入图片描述

debug

在这里插入图片描述

打开this.viewResolvers可以看到自己定义的视图解析器

在这里插入图片描述

若想自定义组件,通过@Configuration注解就能进行组件添加。

6.3.3 修改SpringBoot的默认配置

SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(如用户自己配置@bean),如果有就用用户配置的,如果没有就自动配置。

如果有些组件可以存在多个,比如视图解析器,就将用户配置的和自己默认的组合起来。

扩展SpringMVC,首先要写一个@Configuration注解类,并且要实现WebMvcConfigurer接口,并且不能标注@EnableWebMvc(源码中有相关定义)。

//config.MyMvcConfig
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 浏览器发送/test ,就会跳转到test页面;
        registry.addViewController("/test").setViewName("test");
    }
}

测试

在这里插入图片描述

这样就实现了SpringMVC的扩展,既保留了SpringBoot所有的自动配置,也能用我们扩展的配置。

原理

WebMvcAutoConfiguration 是 SpringMVC的自动配置类,里面有一个类WebMvcAutoConfigurationAdapter

@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
@EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class})
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {......}

这个类上有一个注解,在做其他自动配置是会导入:@Import(EnableWebMvcConfiguration.class)

点进EnableWebMvcConfiguration,可以看到它继承了DelegatingWebMvcConfiguration,进入这个父类

@Configuration(
    proxyBeanMethods = false
)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();

    public DelegatingWebMvcConfiguration() {
    }

    @Autowired(
        required = false
    )
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.configurers.addWebMvcConfigurers(configurers);
        }

    }
    //......
    protected void addViewControllers(ViewControllerRegistry registry) {
        this.configurers.addViewControllers(registry);
    }
    //......
}

进入addViewControllers()方法

//class WebMvcConfigurerComposite implements WebMvcConfigurer
public void addViewControllers(ViewControllerRegistry registry) {
    Iterator var2 = this.delegates.iterator();
    while(var2.hasNext()) {
        // 将所有的WebMvcConfigurer相关配置来一起调用。包括自己配置的和Spring给自动配置的
        WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
        delegate.addViewControllers(registry);
    }
}

可见,所有的WebMvcConfiguration都会被调用,包括自己配置的和Spring给自动配置的。

6.3.4 全面接管SpringMVC(*)

全面接管,即去除所有自动配置,完全由自己配置

只需在自定义的配置类中加一个@EnableWebMvc就能实现。此时启动测试,首页将不能访问,因为自动配置全部失效了。

查看@EnableWebMvc的源码

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}

发现它导入了一个类,进入类DelegatingWebMvcConfiguration

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
	//......
}

这个类继承了WebMvcConfigurationSupport

回看WebMvcAutoConfiguration的源码

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)

@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
// 容器中没有这个组件的时候,这个自动配置类才生效
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
    //......
}

第10行的注解表明,容器中没有WebMvcConfigurationSupport组件的时候,这个自动配置类才生效。而前面的@EnableWebMvc恰好导入了这个组件,这就使得自动装配类无效。

6.4 小项目:员工管理系统

6.4.1 准备工作

前端模板:https://blog.csdn.net/ln82799/article/details/120855879

将已有的前端模板添加到项目中

在这里插入图片描述

编写EmployeeDepartment实体类,并实现相关的Dao层

在这里插入图片描述

Department

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
    private Integer id;
    private String departmentName;
}

Employee

@Data
@NoArgsConstructor
public class Employee {
    private Integer id;
    private String lastName;
    private String email;
    private Integer gender; //0:female, 1:male
    private Department department;
    private Date birth;

    public Employee(Integer id, String lastName, String email, Integer gender, Department department) {
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = gender;
        this.department = department;
        //默认创建日期
        this.birth = new Date();
    }
}

DepartmentDao

//部门Dao
@Repository
public class DepartmentDao {
    //模拟数据库中的数据
    private static Map<Integer, Department> departments=null;
    static{
        departments=new HashMap<Integer,Department>();//创建一个部门表

        departments.put(101,new Department(101,"教学部"));
        departments.put(102,new Department(102,"市场部"));
        departments.put(103,new Department(103,"教研部"));
        departments.put(104,new Department(104,"运营部"));
        departments.put(105,new Department(105,"后勤部"));
    }

    //获取所有部门的信息
    public Collection<Department> getDepartments(){
        return departments.values();
    }

    //通过id查询相关部门
    public Department getDepartmentById(Integer id){
        return departments.get(id);
    }
}

EmployeeDao

@Repository
public class EmployeeDao {

    //模拟数据库中的数据
    private static Map<Integer, Employee> employees=null;
    //员工所属的部门
    @Autowired
    private DepartmentDao departmentDao;
    static{
        employees=new HashMap<Integer,Employee>();//创建一个部门表

        employees.put(1001,new Employee(1001,"Alice","1@qq.com",0,new Department(101,"教学部")));
        employees.put(1002,new Employee(1002,"Bob","2@qq.com",1,new Department(102,"市场部")));
        employees.put(1003,new Employee(1003,"Mike","3@qq.com",1,new Department(103,"教研部")));
        employees.put(1004,new Employee(1004,"John","4@qq.com",1,new Department(104,"运营部")));
        employees.put(1005,new Employee(1005,"Eve","5@qq.com",0,new Department(105,"后勤部")));
    }

    //主键自增
    private static Integer initID=1006;

    //增加一个员工
    public void save(Employee employee){
        if(employee.getId()==null){
            employee.setId(initID++);
        }
        employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getId()));
        employees.put(employee.getId(),employee);
    }

    //查询全部员工
    public Collection<Employee> getAll(){
        return employees.values();
    }

    //通过id查询员工
    public Employee getEmployeeById(Integer id){
        return employees.get(id);
    }

    //删除指定员工
    public void delete(Integer id){
        employees.remove(id);
    }
}

6.4.2 实现首页

编写MyMvcConfig的方法实现页面跳转

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
    }
}

index.html中的相关标签改成thymeleaf的标签(其他静态文件可以顺便一起处理),如下例:

<!--html格式-->
<link href="/css/bootstrap.min.css" rel="stylesheet">
<!--thymeleaf格式-->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">

注:thymeleaf的url内容需要写在@{}`中。

application.properties中设置关闭thymeleaf缓存

#关闭缓存以防样式不更新
spring.thymeleaf.cache=false

在这里插入图片描述

6.4.3 页面国际化

先确认idea当前为UTF-8编码

在这里插入图片描述

resource目录下创建i18n目录,在该目录下创建login.propertieslogin_en_US.propertieslogin_zh_CN.properties分别代表原页面内容、英文页面内容、中文页面内容

在这里插入图片描述

实现国际化配置的类是MessageSourceAutoConfigurationMessageSourceProperties。在配置文件中设置相关属性

spring.messages.basename=i18n.login

修改html文件,在文本相关的标签中写入国际化属性,如

<!--例1-->
<!--原语句-->
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<!--修改后-->
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>

<!--例2-->
<!--修改前-->
<input type="checkbox" value="remember-me"> Remember me
<!--修改后-->
<input type="checkbox" value="remember-me"> [[#{login.remember}]]

注:国际化配置需要#进行引用。特定文本对应已经写好的特定的属性,比如要实现Please sign in的国际化,需要先在i18n中提前规定好它对应的属性(这里是login.tip)并完善内容,然后再写到对应的标签中。

在这里插入图片描述

给上图的两个按钮对应的源码标签中写入传给后端的请求

<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>

与国际化切换相关的类是AcceptHeaferLocaleContextResolver,可以对照它的源码,自定义一个config.MyLocalResolver类,需要实现LocalResolver接口

public class MyLocalResolver implements LocaleResolver {
    //解析请求
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        //获取请求中的语言参数
        String language=request.getParameter("l");
        //如果没有请求就使用默认语言
        Locale locale=Locale.getDefault();
        //如果请求的链接非空,就执行请求
        if(!StringUtils.isEmpty(language)){
            //比如language的值是"zh_CN",可以拆成语言文化代码和国家
            String[] split=language.split("_");
            locale=new Locale(split[0],split[1]);
        }

        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {}
}

在同目录下的MyMvcConfig中注册MyLocalResolver

//自定义的国际化组件
@Bean
public LocaleResolver localeResolver(){
    return new MyLocalResolver();
}

测试

在这里插入图片描述

小结

  1. 配置i18n文件。并修改相关的html文件,使用#{}给出对应文本在i18n文件中的属性
  2. 自定义LocaleResolver组件实现语言切换
  3. 把组件注册到spring容器中

6.4.4 登录功能

基本功能

修改index.html中的form标签内容

<form class="form-signin" th:action="@{/user/login}">

修改html中用户名、密码输入框标签的名称

<input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
<input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">

创建controller.LoginController

@Controller
public class LoginController {
    @RequestMapping("user/login")
    public String login(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            Model model){
        if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
            return "redirect:/main.html";
        }
        else{
            //登录失败
            model.addAttribute("msg","用户名或密码错误");
            return "index";
        }
    }
}

为了避免登录后用户名和密码出现在url的值中,可以设置重定向功能,第7行的main.html是一个请求,将它配置到MyMvcConfig中实现页面跳转

public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/").setViewName("index");
    registry.addViewController("/index.html").setViewName("index");
    registry.addViewController("/main.html").setViewName("dashboard");
}

controller.LoginController中,若登录失败,则会返回一个msg给前端。在index.html中加入一个标签用于登录失败时显示这个返回值

<!--如果msg的值为空,则不显示消息-->
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

测试

在这里插入图片描述

登录拦截器

创建一个config.LoginHandlerInterceptor

public class LoginHandlerInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //登录成功后,要获取用户的Session
        Object loginUser = request.getSession().getAttribute("loginUser");

        if(loginUser==null){ //假如还没有登录
            request.setAttribute("msg","用户尚未登录");
            request.getRequestDispatcher("/index.html").forward(request,response);
            return false;
        }
        else{
            return true;
        }
    }
}

MyMvcConfig中注册

//拦截器设置
@Override
public void addInterceptors(InterceptorRegistry registry) {
    //第二个方法是选择拦截的路径,第三个方法是不拦截提及的路径
    registry.addInterceptor(new LoginHandlerInterceptor())
        .addPathPatterns("/**")
      .excludePathPatterns("/index.html","/","/user/login","/css/**","/js/**","/img/**");
}

dashboard.html中的Company name替换为登录用户的用户名(用Ctrl+F快速查找,替换成[[${session.loginUser}]])

<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>

这里的session需要在LoginController中添加

@Controller
public class LoginController {
    @RequestMapping("user/login")
    public String login(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            Model model, HttpSession session){
        if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
            // 将用户名加入session,前端可以通过session获取用户名
            session.setAttribute("loginUser",username);
            return "redirect:/main.html";
        }
        else{
            //登录失败
            model.addAttribute("msg","用户名或密码错误");
            return "index";
        }
    }
}

6.4.5 展示员工列表(CRUD)

提取重复页面

dashaboard.htmllist.html(这里新建一个emp目录,将list.html移进该目录)中Customer文本对应的列表框替换为如下内容

<li class="nav-item">
    <a class="nav-link" th:href="@{/emps}">
        员工管理
    </a>
</li>

为了实现/emps请求,需要创建一个controller.EmployeeController

@Controller
public class EmployeeController {
    @Autowired
    EmployeeDao employeeDao;
    
    @RequestMapping("/emps")
    public String list(Model model){
        Collection<Employee> employees = employeeDao.getAll();
        model.addAttribute("emps",employees);
        return "emp/list";
    }
}

在这里插入图片描述

接下来要将dashaboard.html中的导航栏与侧边栏的代码通过thymeleaf的th:fragment功能在list.html中进行复用。

导航栏

dashaboard.html查找并更改相关标签

<!--更改前-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0">
<!--更改后-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">

list.html中会有一段完全相同的导航栏代码,删除并用以下代码实现复用。引用需要写在~{}

<div th:insert="~{dashboard::topbar}"></div>

侧边栏

dashaboard.html查找并更改相关标签

<!--更改前-->
<nav class="col-md-2 d-none d-md-block bg-light sidebar">
<!--更改后-->
<nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">

list.html中会有一段完全相同的侧边栏代码,删除并用以下代码实现复用

<div th:insert="~{dashboard::sidebar}"></div>

为了更好地实现复用,可以创建一个common/common.html文件,用于专门存放导航栏和侧边栏等需要复用的代码,设置对应的th:fragment,其他html文件需要用到时通过<div th:insert="~{路径::引用名}"></div>"引入即可。

导航栏和侧边栏的显示效果没变,主要是复用后代码量会减少。

侧边栏选中时高亮

dashboard.html中的侧边栏引用改为如下格式

<div th:replace="~{commons/commons::sidebar(active='main.html')}"></div>

active用于标记当前侧边栏选中了哪个选项,选中的项需要高亮表示

common.html中带有a class="nav-link"的标签进行如下修改

<!--修改前-->
<a class="nav-link active" th:href="@{/index.html}">
<!--修改后-->
<a th:class="${active=='main.html'?'nav-link active':'nav-link'}" th:href="@{/index.html}">

员工管理那一项做同样处理

<a th:class="${active=='list.html'?'nav-link active':'nav-link'}" th:href="@{/emps}">

修改list.html中对侧边栏的引用

<div th:replace="~{commons/commons::sidebar(active='list.html')}"></div>

测试

在这里插入图片描述

接下来要将上图中表的内容替换为之前已经写在dao层的数据

进入list.html,在之前写进侧边栏引用的下方,已经写好了表的呈现形式,将这块代码修改为

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <h2>Section title</h2>
    <div class="table-responsive">
        <table class="table table-striped table-sm">
            <thead>
                <tr>
                    <th>id</th>
                    <th>lastName</th>
                    <th>email</th>
                    <th>gender</th>
                    <th>department</th>
                    <th>birth</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                <!--emps来自于EmployeeController类-->
                <tr th:each="emp:${emps}">
                    <td th:text="${emp.getId()}"></td>
                    <td>[[${emp.getLastName()}]]</td>
                    <td th:text="${emp.getEmail()}"></td>
                    <td th:text="${emp.getGender()==0?'':''}"></td>
                    <td th:text="${emp.department.getDepartmentName()}"></td>
                    <td th:text="${emp.getBirth()}"></td>
                    <td>
                        <button class="btn btn-sm btn-primary">编辑</button>
                        <button class="btn btn-sm btn-danger">删除</button>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</main>

测试

在这里插入图片描述

list.html中,将<h2>Section title</h2>替换成

<h2><a class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a> </h2>

为了实现/emp请求,需要在EmployeeController中添加相关方法

@Autowired
DepartmentDao departmentDao;

@RequestMapping("/emp")
public String toAddPage(Model model){
    //查询所有部门的信息
    Collection<Department> departments = departmentDao.getDepartments();
    model.addAttribute("departments",departments);
    return "emp/add";
}

复制一份list.html到同级目录下,命名为add.html,将其中表单部分替换为

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <form th:action="@{/emp}" method="post">
        <div class="form-group">
            <label>LastName</label>
            <input type="text" name="lastName" class="form-control" placeholder="Tim" required>
        </div>
        <div class="form-group">
            <label>Email</label>
            <input type="email" name="email" class="form-control" placeholder="123@qq.com" required>
        </div>
        <div class="form-group">
            <label>Gender</label>
            <div class="form-check form-check-inline">
                <input class="form-check-input" type="radio" name="gender" value="1" checked="checked">
                <label class="form-check-label"></label>
            </div>
            <div class="form-check form-check-inline">
                <input class="form-check-input" type="radio" name="gender" value="0">
                <label class="form-check-label"></label>
            </div>
        </div>
        <div class="form-group">
            <label>Department</label>
            <select class="form-control" name="department.id" required>
                <option th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option>
            </select>
        </div>
        <div class="form-group">
            <label>birth</label>
            <input type="text" name="birth" class="form-control" placeholder="1970-1-1">
        </div>
        <button type="submit" class="btn btn-primary">添加</button>
    </form>
</main>

测试

在这里插入图片描述

下面要实现添加功能

add.html表单代码块中,设置整个表单的提交方式

<form th:action="@{/emp}" method="post">

EmployeeController中添加相关的方法

@PostMapping("/emp")
public String addEmp(Employee employee){
    //添加一条记录
    employeeDao.save(employee);
    return "redirect:/emps";
}

测试

在这里插入图片描述

思路和增相同

list.html中,修改先前写好的"编辑"按钮

<button class="btn btn-sm btn-primary" th:href="@{/upd/{id}(id=${emp.getId()})}">编辑</button>

为了实现上述的Restful风格请求,需要在EmployeeController中添加相关方法

//跳转到员工的修改页面
@GetMapping("/emp/{id}")
public String toUpdateEmp(@PathVariable("id")Integer id,Model model){
    //查出原来的数据
    Employee employee = employeeDao.getEmployeeById(id);
    model.addAttribute("emp",employee);
    //查出所有部门的信息
    Collection<Department> departments = departmentDao.getDepartments();
    model.addAttribute("departments",departments);

    return "emp/update";
}

复制add.html到同级目录下,命名为update.html,将表单代码修改为

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <form th:action="@{/updateEmp}" method="post">
        <!--设置id的输入。若不写则会被dao层判断为新记录-->
        <input type="hidden" name="id" th:values="${emp.getId()}">
        <div class="form-group">
            <label>LastName</label>
            <input th:text="${emp.getLastName()}" type="text" name="lastName" class="form-control" placeholder="Tim" required>
        </div>
        <div class="form-group">
            <label>Email</label>
            <input th:text="${emp.getEmail()}" type="email" name="email" class="form-control" placeholder="123@qq.com" required>
        </div>
        <div class="form-group">
            <label>Gender</label>
            <div class="form-check form-check-inline">
                <input th:checked="${emp.getGender()==1}" class="form-check-input" type="radio" name="gender" value="1">
                <label class="form-check-label"></label>
            </div>
            <div class="form-check form-check-inline">
                <input th:checked="${emp.getGender()==0}" class="form-check-input" type="radio" name="gender" value="0">
                <label class="form-check-label"></label>
            </div>
        </div>
        <div class="form-group">
            <label>Department</label>
            <select class="form-control" name="department.id" required>
                <option th:selected="${dept.getId()==emp.getDepartment().getId()}" th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option>
            </select>
        </div>
        <div class="form-group">
            <label>birth</label>
            <input th:value="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm')}" type="text" name="birth" class="form-control" placeholder="1970-1-1">
        </div>
        <button type="submit" class="btn btn-primary">修改</button>
    </form>
</main>

为了实现上面代码中的/updateEmp请求,需要在EmployeeController中添加相关方法

//实现修改功能
@PostMapping("/updateEmp")
public String updateEmp(Employee employee){
    employeeDao.save(employee);
    return "redirect:/emps";
}

测试效果跟增差不多

进入list.html,修改删除标签

<a class="btn btn-sm btn-danger" th:href="@{/del/{id}(id=${emp.getId()})">删除</a>

EmployeeController中添加相关方法

//删除员工
@GetMapping("/del/{id}")
public String deleteEmp(@PathVariable("id")Integer id){
    employeeDao.delete(id);
    return "redirect:/emps";
}

6.4.6 404处理

templates目录中新建error目录,将404.html移入该目录,出现404时会直接跳到这个页面。其他错误码同理。

6.4.7 注销功能

进入commons.html页面,修改注销按钮

<a class="nav-link" th:href="@{/user/logout}">注销</a>

LoginController中添加对应方法

@RequestMapping("/user.logout")
public String logout(HttpSession session){
    session.invalidate();
    //返回登录页面
    return "redirect:/index.html";
}

html到同级目录下,命名为update.html`,将表单代码修改为

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <form th:action="@{/updateEmp}" method="post">
        <!--设置id的输入。若不写则会被dao层判断为新记录-->
        <input type="hidden" name="id" th:values="${emp.getId()}">
        <div class="form-group">
            <label>LastName</label>
            <input th:text="${emp.getLastName()}" type="text" name="lastName" class="form-control" placeholder="Tim" required>
        </div>
        <div class="form-group">
            <label>Email</label>
            <input th:text="${emp.getEmail()}" type="email" name="email" class="form-control" placeholder="123@qq.com" required>
        </div>
        <div class="form-group">
            <label>Gender</label>
            <div class="form-check form-check-inline">
                <input th:checked="${emp.getGender()==1}" class="form-check-input" type="radio" name="gender" value="1">
                <label class="form-check-label"></label>
            </div>
            <div class="form-check form-check-inline">
                <input th:checked="${emp.getGender()==0}" class="form-check-input" type="radio" name="gender" value="0">
                <label class="form-check-label"></label>
            </div>
        </div>
        <div class="form-group">
            <label>Department</label>
            <select class="form-control" name="department.id" required>
                <option th:selected="${dept.getId()==emp.getDepartment().getId()}" th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option>
            </select>
        </div>
        <div class="form-group">
            <label>birth</label>
            <input th:value="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm')}" type="text" name="birth" class="form-control" placeholder="1970-1-1">
        </div>
        <button type="submit" class="btn btn-primary">修改</button>
    </form>
</main>

为了实现上面代码中的/updateEmp请求,需要在EmployeeController中添加相关方法

//实现修改功能
@PostMapping("/updateEmp")
public String updateEmp(Employee employee){
    employeeDao.save(employee);
    return "redirect:/emps";
}

测试效果跟增差不多

进入list.html,修改删除标签

<a class="btn btn-sm btn-danger" th:href="@{/del/{id}(id=${emp.getId()})">删除</a>

EmployeeController中添加相关方法

//删除员工
@GetMapping("/del/{id}")
public String deleteEmp(@PathVariable("id")Integer id){
    employeeDao.delete(id);
    return "redirect:/emps";
}

6.4.6 404处理

templates目录中新建error目录,将404.html移入该目录,出现404时会直接跳到这个页面。其他错误码同理。

6.4.7 注销功能

进入commons.html页面,修改注销按钮

<a class="nav-link" th:href="@{/user/logout}">注销</a>

LoginController中添加对应方法

@RequestMapping("/user.logout")
public String logout(HttpSession session){
    session.invalidate();
    //返回登录页面
    return "redirect:/index.html";
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值