31、SpringBoot(1)

0、微服务

0.1、微服务阶段

  • javase:OOP
  • mysql:持久化
  • html+css+js+jquery+框架:视图,框架不熟练,css不好
  • javaweb:独立开发MVC3三层架构的网站(原始)
  • ssm:框架:简化了我们的开发流程,配置也开始较为复杂
  • war:tomcat运行
  • spring再简化:SpringBoot - jar:内嵌tomcat;微服务架构!
  • 服务越来越多:springcloud

figures/31、SpringBoot(学习进度安排)

0.2、微服务架构

0.2.1、什么是微服务?

  • 微服务是一种架构风格,它要求我们在开发一个应用的时候,这个应用必须构建成一系列小服务的组合;可以通过http的方式进行互通。要说微服务架构,先得说说过去我们的单体应用架构。

0.2.2、单体应用架构

  • 所谓单体应用架构(all in one)是指,我们将一个应用的中的所有应用服务都封装在一个应用中。
  • 无论是ERP、CRM或是其他什么系统,你都把数据库访问,web访问,等等各个功能放到一个war包内。
  • 这样做的好处是:易于开发和测试;也十分方便部署;当需要扩展时,只需要将war复制多份,然后放到多个服务器上,再做个负载均衡就可以了。
  • 单体应用架构的缺点是:哪怕我要修改一个非常小的地方,我都需要停掉整个服务,重新打包、部署这个应用war包。特别是对于一个大型应用,我们不可能把所有内容都放在一个应用里面,我们如何维护、如何分工合作都是问题。

0.2.3、微服务架构

  • all in one的架构方式,我们把所有的功能单元放在一个应用里面。然后我们把整个应用部署到服务器上。如果负载能力不行,我们将整个应用进行水平复制,进行扩展,然后再负载均衡。
  • 所谓微服务架构,就是打破之前all in one的架构方式,把每个功能元素独立出来。把独立出来的功能元素的动态组合,需要的功能元素才去拿来组合,需要多一些时可以整合多个功能元素。所以微服务架构是对功能元素进行复制,而没有对整个应用进行复制。
  • 这样做的好处是:1.节省了调用资源;2.每个功能元系的服务都是一 个可替换的、 可独立升级的软件代码。

figures/31、SpringBoot(微服务架构)

0.2.4、如何构建微服务

  • 一个大型系统的微服务架构,就像一个复杂交织的神经网络,每一个神经元就是一个功能元素,它们各自完成自己的功能,然后通过http相互请求调用。比如一个电商系统,查缓存、连数据库、浏览页面、结账、支付等服务都是一个个独立的功能服务,都被微化了,它们作为一个个微服务共同构建了一个庞大的系统。如果修改其中的一个功能,只需要更新升级其中一个功能服务单元即可。
  • 但是这种庞大的系统架构给部署和运维带来很大的难度。于是,spring为我们带来了构建大型分布式微服务的全套、全程产品:
    • 构建一个个功能独立的微服务应用单元,可以使用springboot,可以帮我们快速构建一个 应用;
    • 大型分布式网络服务的调用,这部分由spring cloud来完成,实现分布式;
    • 在分布式中间,进行流式数据计算、批处理, 我们有spring cloud data flow。

figures/31、SpringBoot(Spring相关)

1、SpringBoot 简介

1.1、回顾什么是Spring

  • Spring是一个开源框架,2003 年兴起的一个轻量级的Java 开发框架,作者:Rod Johnson。

  • Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。

1.2、Spring是如何简化Java开发的

  • 为了降低Java开发的复杂性,Spring采用了以下4种关键策略:
    • 1、基于POJO的轻量级和最小侵入性编程,所有东西都是bean;
    • 2、通过IOC,依赖注入(DI)和面向接口实现松耦合;
    • 3、基于切面(AOP)和惯例进行声明式编程;
    • 4、通过切面和模版减少样式代码,RedisTemplate,xxxTemplate。

1.3、什么是SpringBoot

  • 学过javaweb的同学就知道,开发一个web应用,从最初开始接触Servlet结合Tomcat,跑出一个Hello Wolrld程序,是要经历特别多的步骤;后来就用了框架Struts,再后来是SpringMVC,到了现在的SpringBoot,过一两年又会有其他web框架出现;你们有经历过框架不断的演进,然后自己开发项目所有的技术也在不断的变化、改造吗?建议都可以去经历一遍;

  • 言归正传,什么是SpringBoot呢,就是一个javaweb的开发框架,和SpringMVC类似,对比其他javaweb框架的好处,官方说是简化开发,约定大于配置,you can “just run”,能迅速的开发web应用,几行代码开发一个http接口。

  • 所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景衍生 一种规范框架,人们只需要进行各种配置而不需要自己去实现它,这时候强大的配置功能成了优点;发展到一定程度之后,人们根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架;之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡”约定大于配置”,进而衍生出一些一站式的解决方案。

  • 是的这就是Java企业级应用 -> J2EE -> spring -> springboot的过程。

  • 随着 Spring 不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,慢慢变得不那么易用简单,违背了最初的理念,甚至人称配置地狱。Spring Boot 正是在这样的一个背景下被抽象出来的开发框架,目的为了让大家更容易的使用 Spring 、更容易的集成各种常用的中间件、开源软件;

  • Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。Spring Boot 以约定大于配置的核心思想,默认帮我们进行了很多设置,多数 Spring Boot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),Spring Boot 应用中这些第三方库几乎可以零配置的开箱即用。

  • 简单来说就是SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。Spring Boot 出生名门,从一开始就站在一个比较高的起点,又经过这几年的发展,生态足够完善,Spring Boot 已经当之无愧成为 Java 领域最热门的技术。

  • Spring Boot的主要优点:

    • 为所有Spring开发者更快的入门
    • 开箱即用,提供各种默认配置来简化项目配置
    • 内嵌式容器简化Web项目
    • 没有冗余代码生成和XML配置的要求
  • 真的很爽,我们快速去体验开发个接口的感觉吧!

2、Hello, World

2.1、准备工作

  • 我们将学习如何快速的创建一个Spring Boot应用,并且实现一个简单的Http请求处理。通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。

  • 环境准备:

    • java version
    • Maven
    • SpringBoot
  • 开发工具:

    • IDEA

2.2、创建基础项目说明

2.2.1、项目创建方式一:使用Spring Initializr 的 Web页面创建项目

  1. 打开 https://start.spring.io/

  2. 填写项目信息

  3. 点击”Generate Project“按钮生成项目;下载此项目

  4. 解压项目包,并用IDEA以Maven项目导入,一路下一步即可,直到项目导入完毕。

  5. 如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。

2.2.2、项目创建方式二:使用 IDEA 直接创建项目

  1. 创建一个新项目

  2. 选择spring initalizr, 默认是使用官网的快速构建工具实现

  3. 填写项目信息

  4. 选择初始化的组件(初学勾选 Web 即可)

  5. 填写项目路径

  6. 等待项目构建成功

2.2.3、项目结构分析

通过上面步骤完成了基础项目的创建。就会自动生成以下文件:

  1. 程序的主启动类

    package com.cwlin;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    //程序的主启动类
    @SpringBootApplication
    public class SpringBoot01HelloWorldApplication {
    
        public static void main(String[] args) {
            //SpringApplication
            SpringApplication.run(SpringBoot01HelloWorldApplication.class, args);
        }
    
    }
    
  2. application.properties 配置文件

    # 应用名称
    spring.application.name=helloWorld
    # 应用服务 WEB 访问端口
    server.port=8080
    
  3. 测试类

    package com.cwlin;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    
    //测试类
    @SpringBootTest
    class SpringBoot01HelloWorldApplicationTests {
    
        @Test
        void contextLoads() {
        }
    
    }
    
  4. pom.xml

    <!--详细内容见下一节-->
    

2.3、pom.xml 分析

  1. 打开pom.xml,查看Spring Boot项目的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--父依赖-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.cwlin</groupId>
    <artifactId>SpringBoot-01-HelloWorld</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringBoot-01-HelloWorld</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <!--web场景启动器:tomcat,dispatcherServlet,xml-->
        <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>
        <!--devtools热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!--打包插件、热部署插件-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                    <addResources>true</addResources>
                </configuration>
            </plugin>
        </plugins>

    </build>

</project>
  1. 分析上述代码,主要包含四个部分:
  • 项目元数据信息:创建时候输入的Project Metadata部分,也就是Maven项目的基本元素,包括:groupld、artifactId、version、name、description等
  • parent:继承 spring-boot-starter-parent 的依赖管理,控制版本与打包等内容
  • dependencies:项目具体依赖,这里包含了 spring-boot-starter-web 用于实现HTTP接口(该依赖中包含了Spring MVC),官网对它的描述是:使用Spring MVC构建Web(包括RESTful)应用程序的入门者,使用Tomcat作为默认嵌入式容器;spring-boot-starter-test 用于编写单元测试的依赖包。更多功能模块的使用我们将在后面逐步展开。
  • build:构建配置部分,默认使用了 spring-boot-maven-plugin,配合 spring-boot-starter-parent 就可以把Spring Boot应用打包成JAR来直接运行;配合 spring-boot-devtools 实现热部署
  1. 热部署设置:
  • 设置 application.properties

    #配置项目热部署
    spring.devtools.restart.enabled=true
    
  • 在idea中设置自动编译

    • 打开设置Settings,搜索Compliler,勾选Build project automatically;
    • 按住ctrl + shift + alt + /,出现如下图所示界面,点击Registry…;
    • 勾选compiler.automake.allow.when.app.running后关闭页面。
  • SpringBoot项目的热部署功能设置完成!!!

2.4、编写一个http接口

  1. 在主程序的同级目录下,新建一个controller包,一定要在同级目录下,否则识别不到

  2. 在包中新建一个HelloController类

    package com.cwlin.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    //import org.springframework.web.bind.annotation.RestController;
    
    @Controller
    @RequestMapping("/hello")
    public class HelloController {
        @GetMapping("/h1")
        @ResponseBody
        public String hello() {
            return "Hello World";
        }
    }
    
  3. 编写完毕后,从主程序启动项目,在浏览器发起请求:localhost:8080/hello/h1

2.5、将项目打成jar包

  • 点击 maven的 package

  • 如果遇到错误,可以配置打包时跳过项目运行测试用例

    <!--
        在工作中,很多情况下我们打包是不想执行测试用例的
        可能是测试用例不完事,或是测试用例会影响数据库数据
        跳过测试用例执
    -->
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
            <!--跳过项目运行测试用例-->
            <skipTests>true</skipTests>
        </configuration>
    </plugin>
    
  • 如果打包成功,则会在target目录下生成一个 jar 包;打成了jar包后,就可以在任何地方运行了!

2.6、彩蛋

  • 如何更改启动时显示的字符拼成的字母,SpringBoot呢?也就是 banner 图案;

  • 只需一步:到项目下的 resources 目录下新建一个banner.txt 即可。

  • 图案可以到:https://www.bootschool.net/ascii 这个网站生成,然后拷贝到文件中即可!

  • SpringBoot这么简单的东西背后一定有故事,我们之后去进行一波源码分析!

3、原理初探

3.1、pom.xml

3.1.1、父工程

  • 它主要是依赖一个父项目,主要是管理项目的资源过滤及插件
  • 以后我们导入Springboot依赖默认是不需要写版本,但是如果导入的包没有在依赖中管理着就需要手动配置版本了
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.2</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
  • 点spring-boot-starter-parent进去看,就会看到官方配置好的依赖和资源版本号
  • 这个父依赖 Spring-boot-dependencies 才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-dependencies</artifactId>
  <version>2.5.2</version>
</parent>

3.1.2、starter启动器

  • springboot-boot-starter-xxx:spring-boot的场景启动器
  • spring-boot-starter-web:导入web模块正常运行所依赖的组件
  • SpringBoot将所有的功能场景都抽取出来,做成一个个的starter(启动器),只需要在项目中引入这些starter即可,所有相关的依赖都会导入进来,我们要用什么功能就导入什么样的场景启动器即可;我们未来也可以自己自定义 starter
  • 官方启动器:Spring Boot application starters
<!--web场景启动器:tomcat,dispatcherServlet,xml-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3.2、主启动类

package com.cwlin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

//@SpringBootApplication 来标注一个主程序类,启动类下的所有资源被导入
//说明这是一个Spring Boot应用
@SpringBootApplication
public class SpringBoot01HelloWorldApplication {

    public static void main(String[] args) {
        //以为是启动了一个方法,实际上是启动了一个服务
        SpringApplication.run(SpringBoot01HelloWorldApplication.class, args);
    }

}
  • SpringBootApplication注解:
//主启动类的注解,一个tab表示点进去一次
@SpringBootApplication
    @SpringBootConfiguration //表明是一个SpringBoot配置文件
		@Configuration //再次说明这是一个Spring配置类	
			@Component //说明这也是一个Spring组件

    @EnableAutoConfiguration //自动配置,自动导入包
		@AutoConfigurationPackage //自动配置包
			@Import(AutoConfigurationPackages.Registrar.class) //自动配置“包注册”
		@Import(AutoConfigurationImportSelector.class) //自动配置导入选择,选择了什么东西

    @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
            @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

3.2.1、@SpringBootApplication

1、@SpringBootConfiguration
  • 说明里面的 @Component 这就说明,启动类本身也是Spring中的一个组件而已,负责启动应用!
@SpringBootApplication
    @SpringBootConfiguration //表明是一个SpringBoot配置文件
		@Configuration //再次说明这是一个Spring配置类	
			@Component //说明这也是一个Spring组件
2、@EnableAutoConfiguration
@EnableAutoConfiguration //自动配置,自动导入包
    @AutoConfigurationPackage //自动配置包
   		@Import(AutoConfigurationPackages.Registrar.class) //自动配置“包注册”
    @Import(AutoConfigurationImportSelector.class) //自动配置导入选择,选择了什么东西
  • @AutoConfigurationPackage

    • @AutoConfigurationPackages.Registrar.class:
      • Registrar.class作用就是将主启动所在的包及以下的所有子包都扫描进spring容器中
  • @Import(AutoConfigurationImportSelector.class) ,

    • AutoConfigurationImportSelector.class
      • 找到getCandidateConfigurations方法:获得候选的配置
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    //这里的getSpringFactoriesLoaderFactoryClass()方法
    //返回的就是我们最开始看的启动自动导入配置文件的注解类: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;
}
  • getCandidateConfigurations方法中找到SpringFactoriesLoader.loadFactoryNames,这是SpringFactoriesLoader类的静态方法
  • SpringFactoriesLoader类中的预定义的自动装配路径 FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

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());
}
  • 再从loadFactoryNames方法中,点进去找到loadSpringFactories方法
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    //获得classLoader , 我们返回可以看到这里得到的就是EnableAutoConfiguration标注的类本身
    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);
        }
    }
}
3、@ComponentScan
  • 自动扫描并加载符合条件的组件bean,并将这个组件bean注入到IOC容器中
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
4、spring.factories
  • 多次出现的 spring.factories,就是预定好的加载配置文件
  • 打开spring.factories , 看到了很多自动配置的文件;这就是自动配置根源所在!里面的配置都是config配置类已经加载好的

figures/31、SpringBoot(spring.factories配置文件)

5、总结
  • springboot所有的自动配置都是在启动的时候自动扫描并加载,所有的自动配置类都在spring.factories 里,但是不一定生效,要判断条件是否成立,只要导入了对应的start,就有对应的启动器,有了启动器,我们自动装配就生效,然后就配置成功
  1. springboot在启动的时候,从类路径下META-INF/spring.factories 获取EnableAutoConfiguration指定的值
  2. 将这些值作为自动配置类导入容器,自动配置类就会生效,帮我们进行自动配置(在这里,以前我们需要手动配置的东西,现在springboot帮我们做了)
  3. 整个JavaEE的解决方案和自动配置都在spring-boot-autoconfigure-xxx.RELEASE.jar包下
  4. 它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器。这样的话,容器中会存在非常多的自动配置类(xxxAutoConfiguration,也就是@Bean),这些类给容器中导入了这个场景需要的所有组件,并自动配置好这些组件 @Configuration、JavaConfig
  5. 有了自动配置类,免去了我们手动编写配置文件的工作

3.2.2、自动装配

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

  • 用户如何书写yaml配置,需要去查看META-INF/spring.factories下的某自动配置,如HttpEncodingAutoConfiguration

    • EnableConfigrutionProperties(xxx.class):表明这是一个自动配置类,加载某些配置
    • XXXProperties.class:封装配置文件中的属性,yml中需要填入= 它指定的前缀+方法

figures/31、SpringBoot(HttpEncodingAutoConfiguration)

figures/31、SpringBoot(HttpProperties.java)

3.2.3、工作原理总结

  1. 读取 spring.properties 文件
    1. SpringBoot 在启动的时候从 spring-boot-autoConfigure.jar 包下的的 META-INF/spring.factories 中获取 EnableAutoConfiguration 属性的值加载自动配置类
    2. 将这些值作为自动配置类导入容器,自动配置类就生效,帮我们进行自动配置工作
  2. 加载 XXXProperties
    1. 根据自动配置类中指定的xxxxProperties类设置自动配置的属性值,开发者可以根据该类在yaml配置文件中修改自动配置
  3. 根据 @ConditionalXXX 注解决定加载哪些组件
    1. Springboot 通过该注解指定组件加入IOC容器时锁需要具备的特定条件,这个组件会在满足条件时候加入到IOC容器内

figures/31、SpringBoot(工作原理总结)

3.3、SpringApplication.run(xxx.class)

  • 分析该方法主要分两部分:一是SpringApplication的实例化,二是run方法的执行。

3.3.1、run方法

package com.cwlin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

//@SpringBootApplication 来标注一个主程序类,启动类下的所有资源被导入
//说明这是一个Spring Boot应用
@SpringBootApplication
public class SpringBoot01HelloWorldApplication {

    public static void main(String[] args) {
        //以为是启动了一个方法,实际上是启动了一个服务
        //该方法返回一个ConfigurableApplicationContext对象
        //参数:当前应用的主程序类,命令行参数
        SpringApplication.run(SpringBoot01HelloWorldApplication.class, args);
    }

}

3.3.2、SpringApplication类

SpringApplication类主要做了以下四件事情:

  1. 推断应用的类型是普通的项目还是Web项目
  2. 查找并加载所有可用初始化器 , 设置到initializers属性中
  3. 找出所有的应用程序监听器,设置到listeners属性中
  4. 推断并设置main方法的定义类,找到运行的主类(主程序类可以自定义)
public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
    // ......
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.setInitializers(this.getSpringFactoriesInstances();
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = this.deduceMainApplicationClass();
}

3.3.3、run方法流程分析(了解)

figures/31、SpringBoot(run方法流程分析)

4、application.yml

4.1、配置文件

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

  • application.properties

    • 语法结构 :key=value(键值对形式)
  • application.yml

    • 语法结构 :key: value(注意冒号后面的空格!)

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

比如我们可以在配置文件中修改Tomcat 默认启动的端口号:server.port=8081

4.2、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:
    	prot: 8080
    	servlet:
        	# 原先的Tomcat工程路径在这里修改
        	context-path: /cwlin
    

4.3、yaml基本语法

4.3.1、yaml语法

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

4.3.2、yaml基本类型写入

  1. 字面值:普通字符串、数值、布尔类型,直接写成k:v,字符串默认不用加单引号或双引号

    name: cwlin
    
    • “ ” 双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思;

      比如:name: “cw \n lin” 输出:cw 换行 lin

    • ‘ ’ 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出

      比如:name: ‘cw \n lin’ 输出:cw \n lin

    • 注意:设置数据库密码如果是0开头,SpringBoot会默认按照八进制来解析,yml配置时加上引号,如'01111'

  2. 对象、Map:属性值必须和Bean中的对应一致

    Student:
    	name: cwlin
    	age: 3
    # 行内写法
    Student: {name: cwlin, age: 3}
    
  3. 数组(List、Set):使用 - 表示一个元素

    pets:
    	- cat
    	- dog
    	- pig
    # 行内写法
    pets: [cat, dog, pig]
    

4.4、yml配置bean属性,导入依赖

  1. 在resources目录下创建一个application.yml(后面用到)

    person:
      name: cwlin
      age: 18
      feeling: true
      birthday: 2021/07/20
      maps: {k1: v1, k2: v2}
      list:
        - code
        - music
        - movie
      dog:
        name: xiaoHei
        age: 3
    
  2. 编写一个实体类 Dog(预先导入lombok依赖),并通过 @Value 给Dog类这个bean注入属性值的(原本的方式)

    package com.cwlin.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Component //注册bean到容器中
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Dog {
        @Value("xiaoHei")
        private String name;
        @Value("3")
        private Integer age;
    }
    
  3. 在测试类下注入dog,并输出

    package com.cwlin;
    
    import com.cwlin.pojo.Dog;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class SpringBoot02ConfigApplicationTests {
        @Autowired //自动注入dog
        private Dog dog;
    
        @Test
        void contextLoads() {
            System.out.println(dog); //输出dog
        }
    
    }
    
  4. 编写一个复杂一点的实体类 Person,并使用yaml配置的方式进行注入

  5. @ConfigurationProperties(prefix = “person”)

    • 默认springboot项目会报错,提示找不到注解配置,需要导包,然后Idea重启

    • @ConfigurationProperties(prefix = “person”) 默认是从全局配置获取,文件名只能是application.yml

    <!--1.导入配置文件处理器,配置文件进行绑定就会有提示,需要重启-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    
    package com.cwlin.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    import java.util.List;
    import java.util.Map;
    
    @Component
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    /* @ConfigurationProperties作用:
         将配置文件中配置的每一个属性的值,映射到这个组件中;
         告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
         参数 prefix = “person”: 将配置文件中的person下面的所有属性一一对应
    */
    //2.绑定配置文件:指定yml配置中的配置名称装配
    @ConfigurationProperties(prefix = "person")
    public class Person {
        private String name;
        private Integer age;
        private Boolean feeling;
        private Date birthday;
        private Map<String,Object> maps;
        private List<Object> list;
        private Dog dog;
    }
    
  6. 在测试类中注入person,并输出person进行测试

    package com.cwlin;
    
    import com.cwlin.pojo.Dog;
    import com.cwlin.pojo.Person;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class SpringBoot02ConfigApplicationTests {
        @Autowired
        private Dog dog;
        @Autowired
        private Person person;
    
        //3.编写测试类
        @Test
        void contextLoads() {
            System.out.println(dog);
            System.out.println(person);
        }
    
    }
    
  7. 查看控制台显示(结果略)

4.5、加载指定配置文件

  1. @ConfigurationProperties(prefix = “person”):默认是从全局配置文件中获取值

    • 文件名只能是application.yml
    • 优点:只需要配置 prefix=key,会自动匹配相对应的属性
  2. @PropertySource:加载指定的配置文件

    • 定义一个application.properties,以name为例
    name=cwlin
    
    • 定义一个person.yml,yml中可以使用占位符生成随机数或其他功能
      • ${person.hello:hello}:若person.hello值存在,则取自己的值,若不存在,则取hello
      • ${random.uuid} / ${random.int}:取随机值
    person:
      name: cwlin_${random.uuid}
      age: ${random.int}
      feeling: true
      birthday: 2021/07/20
      maps: {k1: v1, k2: v2}
      lists: [code,music,movie]
      hello: happy
      dog:
        name: ${person.hello:hello}_xiaoHei
        age: 3
    
    • 指定 properties文件 或 yml文件,缺点是需要一个个手动注入属性${“xxx”}
    //加载指定的配置文件
    @PropertySource(value="classpath:application.properties")
    public class Person {
        @Value("${name}") //SpringEL表达式取出配置文件中的值
        private String name;
        @Value("#{9*2}")
        private Integer age;
        private Boolean feeling;
        private Date birthday;
        private Map<String,Object> maps;
        private List<Object> list;
        private Dog dog;
    }
    

4.6、两种方式的对比

  • 上面采用的yaml方法都是最简单的方式,是开发中最常用的,也是springboot所推荐的!
  • 除了yml,还有我们之前常用的properties配置文件;properties在写中文时会有乱码,我们需要去IDEA中设置编码格式为UTF-8。
  • @Value 和 @ConfigurationProperties 两种获取值方法的比较:
@ConfigurationProperties@Value
功能批量注入配置文件中的属性一个个指定
松散绑定(松散语法)支持不支持
SpEL不支持支持
JSR303数据校验支持不支持
复杂类型封装支持不支持
  • 结论:
    1. @ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加
    2. 松散绑定:yml中的 first-name = bean中的 firstName。再举个例子:yml中的last-name 和 bean中的lastName是一样的、对应的,注意:- 后面跟着的字母默认是大写的
    3. JSR303数据校验:这个就是我们可以在字段是增加一层过滤器验证 , 可以保证数据的合法性
    4. 复杂类型封装:yml中可以封装对象 , 使用value就不支持
  • 建议:
    1. 配置yml和配置properties都可以获取到值,强烈推荐 yml;
    2. 如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;
    3. 如果说,我们专门编写了一个JavaBean来和配置文件进行一一映射,就直接@configurationProperties,不要犹豫!

4.7、JSR303数据校验

  • 一种数据校验格式,Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理

  • 在pom.xml文件中导入依赖

    <!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
    </dependency>
    
  • 在类上绑定@Validated,在属性上使用指定的参数如 @Email(message="邮箱格式错误")

    import javax.validation.constraints.Email;
    
    @Validated //数据校验
    public class Person {
        @Email(message="邮箱格式错误") //name必须是邮箱格式
        private String name;
        private Integer age;
        private Boolean feeling;
        private Date birthday;
        private Map<String,Object> maps;
        private List<Object> list;
        private Dog dog;
    }
    
  • 常见的数据校验参数,包是 javax.validation.constraints

    // JSR303常用校验	
    @NotNull(message="名字不能为空")
    private String userName;
    @Max(value=120,message="年龄最大不能查过120")
    private int age;
    @Email(message="邮箱格式错误")
    private String email;
    
    // 空检查
    @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=)		Validates that the annotated string is between min and max included.
    
    // 日期检查
    @Past       验证 Date 和 Calendar 对象是否在当前时间之前  
    @Future     验证 Date 和 Calendar 对象是否在当前时间之后  
    @Pattern    验证 String 对象是否符合正则表达式的规则,被注解的元素符合制定的正则表达式
        // 正则检查:regexp:正则表达式;message:不符合条件时的提示信息;flags:匹配标志,表示正则表达式的相关选项
        @Pattern(regexp="\\d{4}", message="标识格式不正确", flags={}, groups={}, payload={})
    
    // 数值检查,建议使用在Stirng、Integer类型,不建议使用在int类型上
    // 当表单值为“”时,无法转换为int,但可以转换为Stirng为"",Integer为null
    
    @Min	验证 Number 和 String 对象是否大等于指定的值
    @Max	验证 Number 和 String 对象是否小等于指定的值
    @DecimalMax		被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
    @DecimalMin		被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
    @Digits							验证 Number 和 String 的构成是否合法
    @Digits(integer=,fraction=)		验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
    @Range(min=, max=)				检查数字是否介于min和max之间.
        @Range(min=10000, max=50000, message=“range.bean.wage”)
        private BigDecimal wage;
    @Valid		递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验(是否进行递归验证)
    @CreditCardNumber		信用卡验证
    @Email		验证是否是邮件地址,如果为null,不进行验证,算通过验证。
    @ScriptAssert(lang= ,script=, alias=)			通过script属性指定进行校验的方法,传递校验的参数
    @SafeHtml	校验是否包含恶意脚本
    @URL(protocol=,host=, port=,regexp=, flags=)	验证是否是合法的url地址
    
    // 等等
    @Cwlin      除上述校验规则外,我们还可以自定义一些数据校验规则
    

4.8、配置文件位置

  • springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件:
    • file:./config/appplication.yml
    • file:./application.yml(项目路径下)
    • classpath:/config/application.yml
    • classpath:/application.yml(资源路径下)
  • 上述位置的配置文件执行的优先级依次递减,高优先级的配置会覆盖低优先级的配置;SpringBoot会从这四个位置全部加载主配置文件,互补配置

4.9、多环境切换

4.9.1、properties多环境配置

  • profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境

  • 我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本,例如:

    • application-test.properties 代表测试环境配置
    • application-dev.properties 代表开发环境配置
  • 但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置文件;我们需要通过一个配置来选择需要激活的环境:

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

4.9.2、yml多环境配置

  • 和properties配置文件一样,但是使用yml去实现不需要创建多个配置文件

  • 使用 --- 来分割多个yml配置,并且用profiles来命名环境名称

  • 编写 application.yml,并激活dev的配置:

    server:
      port: 8080
    # 选择要激活哪个环境块
    spring:
      profiles:
        active: dev # 激活dev配置
    ---
    server:
      port: 8081
    spring:
      profiles: test # 配置环境的名称
    
    ---
    server:
      port: 8082
    spring:
      profiles: dev # 配置环境的名称
    
    ---
    server:
      port: 8083
    spring:
      profiles: prod # 配置环境的名称
    
  • 注意:如果yml和properties同时都配置了端口,并且没有激活其他环境,默认会使用properties配置文件的!

4.9.3、运维小技巧

  • 可以通过spring.config.location来改变默认的配置文件位置

  • 项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高

    java -jar spring-boot-config.jar --spring.config.location=F:/application.properties
    

5、进一步分析自动配置原理

5.1、HttpEncodingAutoConfiguration

  • 以**HttpEncodingAutoConfiguration(Http编码自动配置)**为例,分析自动配置原理
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure.web.servlet;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.servlet.server.Encoding;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.filter.CharacterEncodingFilter;

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

//自动配置属性:启动指定类的ConfigurationProperties功能;
  //以前的版本中是 HttpProperties.class,新版本是 ServerProperties.class;
  //点击进入ServerProperties.class查看,将配置文件中对应的值和ServerProperties绑定起来;
  //并将ServerProperties加入到ioc容器中;
@EnableConfigurationProperties({ServerProperties.class})

//Spring底层的@Conditional注解
  //根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效;
  //这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
//判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器;
@ConditionalOnClass({CharacterEncodingFilter.class})
//判断配置文件中是否存在某个配置:server.servlet.encoding.enabled;
  //如果不存在,判断也是成立的
  //即使我们配置文件中不配置 server.servlet.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;
    }

    //添加组件
    @Bean
    public HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
        return new HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer(this.properties);
    }

    static class LocaleCharsetMappingsCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {
        private final Encoding properties;

        LocaleCharsetMappingsCustomizer(Encoding properties) {
            this.properties = properties;
        }

        public void customize(ConfigurableServletWebServerFactory factory) {
            if (this.properties.getMapping() != null) {
                factory.setLocaleCharsetMappings(this.properties.getMapping());
            }

        }

        public int getOrder() {
            return 0;
        }
    }
}
  • 一句话总结 :根据当前不同的条件判断,决定这个配置类是否生效!

    • 一旦这个配置类生效,这个配置类就会给容器中添加各种组件;
    • 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
    • 所有在 properties / yml 配置文件中能配置的属性都是在xxxxProperties类中封装着;
    • 配置文件能配置什么就可以参照某个功能对应的这个属性类。
    // 从配置文件中获取指定的值和bean的属性进行绑定
    @ConfigurationProperties(
        prefix = "server",
        ignoreUnknownFields = true
    )
    public class ServerProperties {
    	// ......
    }
    
  • 自动装配原理的精髓

    1. SpringBoot启动会加载大量的自动配置类;
    2. 我们看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;
    3. 我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)
    4. 给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即
      • xxxxAutoConfigurartion:自动配置类(用来给容器中添加组件)
      • xxxxProperties:封装配置文件中相关属性(SpringBoot配置文件)

5.2、@Conditional派生注解

注意:自动配置类必须在一定的条件下才能生效

作用:必须是 @Conditional 指定的条件成立,才给容器中添加组件,配置里面的所有内容才生效

@Conditional 派生注解作用(判断是否满足当前指定条件)
@ConditionalOnJava系统的java版本是否符合要求
@ConditionalOnBean容器中存在指定Bean
@ConditionalOnMissingBean容器中不存在指定Bean
@ConditionalOnExpression满足SpEL表达式指定
@ConditionalOnClass系统中有指定的类
@ConditionalOnMissingClass系统中没有指定的类
@ConditionalOnSingleCandidate容器中只有一个指定的Bean,或者这个Bean是首选Bean
@ConditionalOnProperty系统中指定的属性是否有指定的值
@ConditionalOnResource类路径下是否存在指定资源文件
@ConditionalOnWebApplication当前是web环境
@ConditionalOnNotWebApplication当前不是web环境
@ConditionalOnJndiJNDI存在指定项

5.3、打印自动配置报告

  • **自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是所有的都生效了。**那么,怎么知道哪些自动配置类生效?

  • 可以通过在配置文件中启用 debug=true属性,来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效!

#开启springboot的调试类
debug=true
  • 自动配置报告(部分内容)
Positive matches:(正匹配:自动配置类,已经启用的并且生效的)
-----------------

   DispatcherServletAutoConfiguration matched:
      - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
      - @ConditionalOnWebApplication (required) found StandardServletEnvironment (OnWebApplicationCondition)
   // ......

Negative matches:(负匹配:没有启动、没有匹配成功的自动配置类)
-----------------
   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required classes 'javax.jms.ConnectionFactory', 'org.apache.activemq.ActiveMQConnectionFactory' (OnClassCondition)
   AopAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required classes 'org.aspectj.lang.annotation.Aspect', 'org.aspectj.lang.reflect.Advice' (OnClassCondition)
   // ......

Unconditional classes:(没有条件的类)
   // ......

6、SpringBoot Web 开发

其实SpringBoot的东西用起来非常简单,因为SpringBoot最大的特点就是自动装配。

使用SpringBoot的步骤:

  1. 创建一个SpringBoot应用,选择我们需要的模块,SpringBoot就会默认将所需要的模块自动配置好

  2. 手动在配置文件中配置部分配置项目就可以运行起来了

  3. 专注编写业务代码,不需要考虑以前那样一大堆的配置了

自动配置:比如SpringBoot到底帮我们配置了什么?我们能不能修改?我们能修改哪些配置?我们能不能扩展?

  • xxxAutoconfiguration:向容器中自动配置组件
  • 自动配置类,封装配置文件的内容:xxxProperties:自动配置类,装配配置文件中自定义的一些内容

没事就找找类,看看自动装配原理!

6.0、待解决的问题

  • 导入静态资源

  • 首页和图标定制

  • jsp、模板引擎Thymeleaf(thymeleaf依赖)

  • 装配扩展SpringMVC

  • 增删改查

  • 拦截器

  • 国际化

6.1、静态资源映射规则(源码)

  • 在SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置类里面;
  • 在 WebMvcAutoConfigurationAdapter 中有很多配置方法,其中有一个方法:addResourceHandlers 添加资源处理
  • IDEA按两下shift,搜索WebMvcAutoConfiguration - addResourceHandlers
// 2.3.7.RELEASE
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // 静态资源路径在配置文件中已经被自定义,如:spring.mvc.static-pattern=/hello/,classpath:/cwlin/,源码会失效
    if (!this.resourceProperties.isAddMappings()) {
        // 已禁用默认资源处理
        logger.debug("Default resource handling disabled");
    } else {
        // 缓存控制
        Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
        CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
        // 1.Webjars配置
        // 路径/webjars/**下的**,都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源
        if (!registry.hasMappingForPattern("/webjars/**")) {
            this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
        }

        // 2.静态资源配置
        String staticPathPattern = this.mvcProperties.getStaticPathPattern();
        if (!registry.hasMappingForPattern(staticPathPattern)) {
            this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
        }

    }
}
// 2.5.2
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
    } else {
        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);
    }
}

6.1.1、第一种:Webjars配置

  • Webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。

  • 使用SpringBoot需要使用Webjars,网站:https://www.webjars.org

  • 要使用jQuery,只需要引入jQuery对应版本的pom依赖即可

    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>jquery</artifactId>
        <version>3.6.0</version>
    </dependency>
    
  • 导入完毕,查看webjars目录结构,并访问Jquery.js文件!

    figures/31、SpringBoot(webjars目录结构)

  • 访问:http://localhost:8080/webjars/jquery/3.6.0/jquery.js(只要是静态资源,SpringBoot就会去对应的路径寻找资源)

6.1.2、第二种:静态资源配置

  • 找到 WebMvcProperties类 -> getStaticPathPattern方法 -> staticPathPattern属性,发现第二种映射规则:/** , 访问当前的项目任意资源,它会去找 ResourceProperties 这个类,从 WebMvcAutoConfiguration 类中点击进去查看源码:
public String getStaticPathPattern() {
    return this.staticPathPattern;
}

private String staticPathPattern;
public WebMvcProperties() {
    this.staticPathPattern = "/**";
    // ......
}

private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{
    "classpath:/META-INF/resources/", 
    "classpath:/resources/", 
    "classpath:/static/", 
    "classpath:/public/"
};
private String[] staticLocations;
public ResourceProperties() {
    this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
    // ......
}
  • ResourceProperties类可以设置和我们静态资源有关的参数,这里面指向了它会去寻找资源的文件夹,即上面数组的内容。

  • 我们可以在resources根目录下新建相应的文件夹,都可以存放我们的静态文件

    "classpath:/META-INF/resources/" //Webjars配置路径
    "classpath:/resources/" //用于存放upload上传的文件
    "classpath:/static/" //用于存放静态资源,比如图片、视频等
    "classpath:/public/" //用于存放公共资源,比如js文件
    
  • 访问 http://localhost:8080/1.js , WebMvcAutoConfiguration类会去这些文件夹中寻找对应的静态资源文件,resources根目录下的三个路径优先级如下:

    resources > static(默认创建) > public
    

6.1.3、总结

  1. 在SpringBoot中,可以使用以下方式处理静态资源
    • classpath:/META-INF/resources/webjars/:localhost:8080/webjars/(webjars不推荐使用,官网导包)
    • /**、public、static、resources:localhost:8080/(resources根目录下的路径)
  2. 通过配置文件application.properties自定义静态资源路径,指定哪些文件夹是需要我们放静态资源文件的
    • spring.resources.static-locations=classpath:/coding/,classpath:/cwlin/
    • 一旦自定义了静态资源路径,原来的自动配置就都会失效了!

6.2、首页和图标定制

6.2.1、首页

  • IDEA按两下shift,搜索WebMvcAutoConfigurationAdapter - welcomePageHandlerMapping
  • welcomePageHandlerMapping 是欢迎页的映射,就是我们的首页!
@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;
}

private Optional<Resource> getWelcomePage() {
    String[] locations = WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations());
    // ::是java8 中新引入的运算符
    // Class::function的时候function是属于Class的,应该是静态方法。
    // this::function的funtion是属于这个对象的。
    // 简而言之,就是一种语法糖而已,是一种简写
    return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}

private Resource getIndexHtml(String location) {
    // 欢迎页就是一个location下的 index.html,即静态资源路径下的所有 index 页面
    return this.resourceLoader.getResource(location + "index.html");
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Welcome Page</h1>
</body>
</html>
  • 或者放在templates文件夹下,需要通过controller来跳转!同时,需要模板引擎的支持(thymeleaf依赖),这里没有实现!
package com.cwlin.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
// 在templates目求下的所有页面,只能通过controller来跳转!
// 这个需要模板引擎的支持,thymeleaf依赖
public class IndexController {
    @RequestMapping("/index")
    public String index(){
        return "index";
    }
}

6.2.2、图标

  1. 新版本中似乎删去了关闭SpringBoot默认图标的配置:
#关闭默认图标
spring.mvc.favicon.enabled=false
  1. 将favicon.icon放到resources目录下 例如:/public,/static等等
  2. 完成上面的步骤还不能显示,还需要在页面的head标签添加代码
<head>
   <meta charset="UTF-8">
   <title>登录</title>
   <link rel="shortcut icon" th:href="@{/favicon.ico}"/>
   <link rel="bookmark" th:href="@{/favicon.ico}"/>
</head>
  1. 注意:上面用的thymeleaf模板!如果不是,则要使用下面这个html
<head>
   <meta charset="UTF-8">
   <title>登录</title>
   <link rel="shortcut icon" href="/favicon.ico"/>
   <link rel="bookmark" href="/favicon.ico"/>
</head> 
  1. 清除浏览器缓存,刷新网页,发现图标改变!

6.3、Thymeleaf 模板引擎

6.3.1、概述

  • 前端交给我们的页面,是html页面。如果是我们以前开发,我们需要把他们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示、交互等。
  • jsp支持非常强大的功能,包括能写Java代码,但是对于SpringBoot这个项目,第一,它是以jar的方式,不是war;第二,它用的还是嵌入式的Tomcat,因此,他现在默认是不支持jsp的
  • 如果不支持jsp,而直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦。因此,SpringBoot推荐你可以来使用模板引擎:
  • 其实jsp就是一个模板引擎,还有用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但是他们的思想都是一样的:

figures/31、SpringBoot(模板引擎)

  • 模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。而这些值,从哪来呢,就是我们在后台封装一些数据。然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。不同模板引擎之间,他们的语法可能有点不一样。
  • 以下主要介绍一下SpringBoot给我们推荐的Thymeleaf模板引擎,这是一个高级语言的模板引擎。

6.3.2、引入Thymeleaf

  • 对于springboot来说,什么事情不都是一个start的事情。查看下面的三个网址:

  • All official starters follow a similar naming pattern; spring-boot-starter-*, where * is a particular type of application. This naming structure is intended to help when you need to find a starter. The Maven integration in many IDEs lets you search dependencies by name. For example, with the appropriate Eclipse or Spring Tools plugin installed, you can press ctrl-space in the POM editor and type “spring-boot-starter” for a complete list.

  • 引入对应的pom依赖,可以适当点进源码看看

    <!--thymeleaf-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    
  • 观察6.2.1中首页定制:将index.html放在templates文件夹下,通过controller来跳转。测试首页:http://localhost:8080/

  • 再做一个测试:

    1. 编写一个TestController

      package com.cwlin.controller;
      
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.RequestMapping;
      
      @Controller
      public class TestController {
          @RequestMapping("/test")
          public String test1(){
              //classpath:/templates/test.html
              return "test";
          }
      }
      
    2. 编写一个测试页面 test.html 放在 templates 目录下

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>Title</title>
      </head>
      <body>
      <h1>测试页面</h1>
      </body>
      </html>
      
    3. Rerun项目,请求测试页:http://localhost:8080/test

6.3.3、Thymeleaf 分析

  • 按照SpringBoot的自动配置原理,看一下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;
        private boolean cache;
        private Integer templateResolverOrder;
        private String[] viewNames;
        private String[] excludedViewNames;
        private boolean enableSpringElCompiler;
        private boolean renderHiddenMarkersBeforeCheckboxes;
        private boolean enabled;
        private final ThymeleafProperties.Servlet servlet;
        private final ThymeleafProperties.Reactive reactive;
    
        public ThymeleafProperties() {
            this.encoding = DEFAULT_ENCODING;
            this.cache = true;
            this.renderHiddenMarkersBeforeCheckboxes = false;
            this.enabled = true;
            this.servlet = new ThymeleafProperties.Servlet();
            this.reactive = new ThymeleafProperties.Reactive();
        }
        
        // ......
    }
    
  • thymeleaf默认的前缀和后缀如下:

    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html";
    
  • 我们只需要把我们的html页面放在类路径(resources)下的templates文件夹下,thymeleaf就可以帮我们自动渲染了。

6.3.4、Thymeleaf 语法学习

  • 参考 Thymeleaf 官方文档:https://www.thymeleaf.org/

  • 基于6.3.2中的测试,我们做个简单的测试:查出一些数据,在页面中展示

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

      package com.cwlin.controller;
      
      import org.springframework.stereotype.Controller;
      import org.springframework.ui.Model;
      import org.springframework.web.bind.annotation.RequestMapping;
      
      @Controller
      public class TestController {
          @RequestMapping("/test")
          public String test1(Model model){
              model.addAttribute("msg","hello, Thymeleaf!");
              //classpath:/templates/test.html
              return "test";
          }
      }
      
    2. 我们要使用thymeleaf,需要在html文件中导入命名空间的约束

      xmlns:th="http://www.thymeleaf.org"
      
    3. 我们去编写下前端页面

      <!DOCTYPE html>
      <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head>
          <meta charset="UTF-8">
          <title>Title</title>
      </head>
      <body>
      <h1>测试页面</h1>
      <!--所有的html元素都可以被thymeleaf替换接管,th:元素名-->
      <h2 th:text="${msg}"></h2>
      </body>
      </html>
      
    4. 启动测试:http://localhost:8080/test

  • 可以使用任意的 th:attr 来替换html中原生属性的值!

figures/31、SpringBoot(Thymeleaf 语法学习)

  • 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.
      #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).
=================================================================================================

    Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
    	补充:配合 th:object="${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>
    Message Expressions: #{...}:获取国际化内容
    Link URL Expressions: @{...}:定义URL;
    		@{/order/process(execId=${execId},execType='FAST')}
    Fragment Expressions: ~{...}:片段引用表达式
    		<div th:insert="~{commons :: main}">...</div>

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: _ //分割表达式

6.3.5、练习测试

  1. 我们编写一个Controller,放一些数据

    package com.cwlin.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import java.util.Arrays;
    
    @Controller
    public class Test1Controller {
        @RequestMapping("/test1")
        public String test1(Model model){
            model.addAttribute("msg","<h1>hello, Thymeleaf!<h1>");
            model.addAttribute("users", Arrays.asList("cwlin","coder_lcw"));
            //classpath:/templates/test.html
            return "test1";
        }
    }
    
  2. 测试页面取出数据

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <h1>测试页面</h1>
    <!--所有的html元素都可以被thymeleaf替换接管,th:元素名-->
    <!--text=默认是转义文本-->
    <h2 th:text="${msg}"></h2>
    
    <!--utext=默认是不转义文本-->
    <h2 th:utext="${msg}"></h2>
    
    <hr>
    <!--th:each每次遍历都会生成当前这个标签:官网#9-->
    <!--推荐使用-->
    <h3 th:each="user: ${users}" th:text="${user}"></h3>
    
    <h3>
        <!--行内写法:官网#12-->
        <span th:each="user:${users}"> [[${user}]] </span>
    </h3>
    </body>
    </html>
    
  3. 启动项目测试:http://localhost:8080/test1

6.4、SpringMVC 自动配置原理

6.4.1、官方文档说明

  • 在进行项目编写前,我们需要SpringBoot对SpringMVC做了哪些配置,包括如何扩展,如何定制

    // Spring Boot为Spring MVC提供了自动配置,它可以很好地与大多数应用程序一起工作。
    Spring Boot provides auto-configuration for Spring MVC that works well with most applications.
    // 自动配置在Spring默认设置的基础上添加了以下功能:
    The auto-configuration adds the following features on top of Spring’s defaults:
        // 包含视图解析器
        Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
        // 支持静态资源文件夹的路径,以及webjars
        Support for serving static resources, including support for WebJars 
        // 自动注册了Converter:
        // 转换器,这就是我们网页提交数据到后台自动封装成为对象的东西,比如把"1"字符串自动转换为int类型
        // Formatter:【格式化器,比如页面给我们了一个2019-8-10,它会给我们自动格式化为Date对象】
        Automatic registration of Converter, GenericConverter, and Formatter beans.
        // HttpMessageConverters
        // SpringMVC用来转换Http请求和响应的的,比如我们要把一个User对象转换为JSON字符串,可以去看官网文档解释;
        Support for HttpMessageConverters (covered later in this document).
        // 定义错误代码生成规则的
        Automatic registration of MessageCodesResolver (covered later in this document).
        // 首页定制
        Static index.html support.
        // 图标定制
        Custom Favicon support (covered later in this document).
        // 初始化数据绑定器:帮我们把请求数据绑定到JavaBean中!
        Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
    
    /*
    如果您希望保留Spring Boot MVC功能,并且希望添加其他MVC配置(拦截器、格式化程序、视图控制器和其他功能),则可以添加自己的@configuration类,类型为webmvcconfiguer,但不添加@EnableWebMvc。
    如果希望提供 RequestMappingHandlerMapping、RequestMappingHandlerAdapter或ExceptionHandlerExceptionResolver 的自定义实例,并且保留Spring Boot MVC功能,则可以声明WebMVCregistrationAdapter实例来提供此类组件。
    如果您想完全控制Spring MVC,可以添加自己的@Configuration,并用@EnableWebMvc进行注释;或者如@EnableWebMvc的Javadoc所述,添加自己的@Configuration注释@DelegatingWebMvcConfiguration
    */
    If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.
    If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.
    If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.
    

6.4.2、ContentNegotiatingViewResolver 内容协商视图解析器

  • 自动配置ViewResolver,就是之前学习的SpringMVC的视图解析器,即根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发,重定向)。

  • 双击Shift键,找到 WebMvcAutoConfiguration, 然后搜索 ContentNegotiatingViewResolver,找到viewResolver方法:

    @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;
    }
    
  • 点击查看 ContentNegotiatingViewResolver类 -> ViewResolver接口,找到对应的解析视图的代码

    // ViewResolver接口
    View resolveViewName(String viewName, Locale locale) throws Exception;
    
    // ContentNegotiatingViewResolver类
    @Override
    @Nullable // 注解说明:@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 = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
       if (requestedMediaTypes != null) {
          // 获取候选的视图对象
          List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
          // 选择一个最适合的视图对象,然后把这个对象返回
          View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
          if (bestView != null) {
             return bestView;
          }
       }
    
       String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
             " given " + requestedMediaTypes.toString() : "";
    
       if (this.useNotAcceptableStatusCode) {
          if (logger.isDebugEnabled()) {
             logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
          }
          return NOT_ACCEPTABLE_VIEW;
       }
       else {
          logger.debug("View remains unresolved" + mediaTypeInfo);
          return null;
       }
    }
    
  • 继续点进去查看 getCandidateViews方法,如何获得候选的视图

    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");
          for (ViewResolver viewResolver : this.viewResolvers) {
             View view = viewResolver.resolveViewName(viewName, locale);
             if (view != null) {
                candidateViews.add(view);
             }
             for (MediaType requestedMediaType : requestedMediaTypes) {
                List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
                for (String extension : extensions) {
                   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;
    }
    
  • 在getCandidateViews中,把所有的视图解析器进行for循环,逐个解析!

    for (ViewResolver viewResolver : this.viewResolvers)
    
  • 得出结论:ContentNegotiatingViewResolver 这个视图解析器就是用来组合所有的视图解析器的

  • 再去研究下他的组合逻辑,看到有个属性this.viewResolvers,看看它是在哪里进行赋值的!

    @Override
    protected void initServletContext(ServletContext servletContext) {
       // 这里它是从beanFactory工具中获取容器中的所有视图解析器
       // ViewRescolver.class 把所有的视图解析器来组合的
       Collection<ViewResolver> matchingBeans =
             BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
       if (this.viewResolvers == null) {
          this.viewResolvers = new ArrayList<>(matchingBeans.size());
          for (ViewResolver viewResolver : matchingBeans) {
             if (this != viewResolver) {
                this.viewResolvers.add(viewResolver);
             }
          }
       }
       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);
    }
    
  • 接下来,我们给容器中添加一个视图解析器,这个类会自动将它组合进来

    1. 在 MyMVCConfig类 中,写一个视图解析器,不能标注@EnableWebMvc

      package com.cwlin.config;
      
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.web.servlet.View;
      import org.springframework.web.servlet.ViewResolver;
      import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
      
      import java.util.Locale;
      
      //如果想要diy一些定制化的功能,只需要写这个组件,然后将它交给springboot,springboot就会帮我们自动装配
      //全面扩展SpringMVC  DispatchServlet
      @Configuration
      public class MyMVCConfig implements WebMvcConfigurer {
          //viewResolver实现了视图解析器接口类,就可以把它看作是视图解析器
      
          @Bean
          public ViewResolver myViewResolver(){
              return new MyViewResolver();
          }
      
          //静态内部类,视图解析器就需要实现ViewResolver接口
          private static class MyViewResolver implements ViewResolver {
              @Override
              public View resolveViewName(String s, Locale locale) {
                  return null;
              }
          }
      }
      
    2. 查看自定义的视图解析器是否起作用

      1. 找到 DispatcherServlet 中的 doService方法 -> doDispatch方法

        doDispatch(request, response);
        
      2. 在doDispatch方法上加个断点进行调试(所有的请求都会走到这个方法中)

    3. 启动项目,访问页面http://localhost:8080/,查看IDEA中的Debug信息

      1. 找到视图解析器 this.viewResolvers
      2. 找到自定义的视图解析器 MyMVCConfig$myViewResolver@6627
    4. 具体的操作说明如下图所示:

      figures/31、SpringBoot(查看自定义视图解析器)

6.4.3、转换器和格式化器

  • 双击Shift键,找到 WebMvcAutoConfiguration, 然后搜索 FormattingConversionService,找到格式化转换器:

    @Bean
    public FormattingConversionService mvcConversionService() {
        Format format = this.mvcProperties.getFormat();
        WebConversionService conversionService = new WebConversionService((new DateTimeFormatters()).dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
        this.addFormatters(conversionService);
        return conversionService;
    }
    
  • 点击 dateFormat() 方法,查看自动配置的日期格式

    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;
    }
    
  • 在Properties配置文件中,自定义配置日期的格式化规则,springmvc会将它注册到Bean中生效

    # 自定义配置日期格式化
    spring.mvc.format.date=dd/MM/yyyy
    

6.4.4、修改SpringBoot的默认配置

  • 这么多的自动配置,原理都是一样的,通过这个WebMVC的自动配置原理分析,我们要学会一种学习方式,通过源码探究,得出结论;这个结论一定是属于自己的,而且一通百通。
  • SpringBoot的底层,大量用到了这些设计细节思想,所以,没事需要多阅读源码!
  • SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的 @bean 。如果有,就用用户配置的;如果没有,就用自动配置的。
  • 如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来!

6.4.5、扩展使用SpringMVC

  • 官方文档如下:

    • If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.
    • 我们要做的就是编写一个@Configuration注解类,并且是WebMvcConfigurer类型,不能标注@EnableWebMvc注解
  • 新建一个config包,编写一个MyMVCConfig类,重写addViewControllers视图跳转方法

    @Configuration
    public class MyMVCConfig implements WebMvcConfigurer {
        //视图跳转
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            //浏览器请求/cwlin,会跳转到test页面
            registry.addViewController("/cwlin").setViewName("test");
        }
    }
    
  • 启动项目,访问:http://localhost:8080/cwlin,成功跳转!

  • 要扩展SpringMVC,官方推荐这么使用,既保留SpringBoot所有的自动配置,也能使用扩展的自定义配置!

  • 分析一下@EnableWebMvc注解的原理:

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

    2. 这个类上有一个注解,在做其他自动配置时会导入:@Import({DelegatingWebMvcConfiguration.class})

    3. 点击查看 EnableWebMvcConfiguration 类,它继承了一个父类:DelegatingWebMvcConfiguration(也就是@EnableWebMvc注解导入的类)

    4. 这个父类 DelegatingWebMvcConfiguration 中,有这样一段代码:

      @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);
            }
         }
         
         //......
      }
      
    5. 在这个类中找到我们刚刚设置的viewController当做参考,发现它调用了一个 configurers.addViewControllers(registry)

      @Override
      protected void addViewControllers(ViewControllerRegistry registry) {
         this.configurers.addViewControllers(registry);
      }
      
    6. 点击进入查看 addViewControllers 方法

      @Override
      public void addViewControllers(ViewControllerRegistry registry) {
         //将所有的WebMvcConfigurer相关配置一起调用!包括我们自定义配置的和Spring配置的
         for (WebMvcConfigurer delegate : this.delegates) {
            delegate.addViewControllers(registry);
         }
      }
      
    7. 结论:

      • @EnableWebMvc 导入了一个DelegatingWebMcvConfiguration类,从容器中获取所有的webmvcconfig,继承了WebMcvConfigurationSupport。
      • 在类中,包含注解 @ConditionalOnMissingBean({WebMvcConfigurationSupport.class}),这个注解的意思是:容器中没有这个组件的时候,这个自动配置类(WebMvcAutoConfiguration)才生效;
      • 即:如果含有WebMcvConfigurationSupport.class,那么所有的自定义配置都不生效!
      • 所有的WebMvcConfiguration都会被作用,不止Spring自己的配置类,我们自己的配置类当然也会被调用。

6.4.6、全面接管SpringMVC

  • 官方文档:
    • If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.
    • 全面接管,即:SpringBoot对SpringMVC的自动配置不需要了,所有配置都是我们自定义!
    • 在我们的配置类中要加一个@EnableWebMvc
  • 如果我们全面接管了SpringMVC了,SpringBoot给我们配置的静态资源映射一定会失效
  • 在开发中,不推荐使用全面接管SpringMVC

7、员工管理系统

7.0、实体类编写和数据库模拟

  1. 创建 spring 项目,选中相应的依赖;导入静态资源(html、css等)。

  2. 创建实体类 Department 和 Employee

    package com.cwlin.system.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    //部门
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Department {
        private Integer id;
        private String departmentName;
    }
    
    package com.cwlin.system.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.util.Calendar;
    import java.util.Date;
    import java.util.Random;
    
    //员工
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Employee {
        private Integer id;
        private String lastName;
        private String email;
        private Integer gender; //0:女,1:男
        private Department department;
        private Date birthday;
    
        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;
            //默认日期
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(new Date());
            calendar.add(Calendar.YEAR, new Random().nextInt(35) - 45);
            this.birthday = calendar.getTime();
        }
    }
    
  3. 编写实体类Mapper,并在Mapper层模拟数据库表数据

    package com.cwlin.system.mapper;
    
    import com.cwlin.system.pojo.Department;
    import org.springframework.stereotype.Repository;
    
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    
    @Repository
    public class DepartmentMapper {
        //模糊数据库中的数据
        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);
        }
    }
    
    package com.cwlin.system.mapper;
    
    import com.cwlin.system.pojo.Department;
    import com.cwlin.system.pojo.Employee;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Repository;
    
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    
    @Repository
    public class EmployeeMapper {
        //模糊数据库中的数据
        private static Map<Integer, Employee> employees = null;
        //员工有所属的部门
        @Autowired
        private DepartmentMapper departmentMapper;
        static {
            employees = new HashMap<Integer, Employee>(); //创建一个部门表
            employees.put(1001,new Employee(1001,"AA","123456@qq.com",1,new Department(101,"计财处")));
            employees.put(1002,new Employee(1002,"BB","234567@qq.com",0,new Department(102,"科技处")));
            employees.put(1003,new Employee(1003,"CC","345678@qq.com",0,new Department(103,"设备处")));
            employees.put(1004,new Employee(1004,"DD","456789@qq.com",1,new Department(104,"教务处")));
            employees.put(1005,new Employee(1005,"EE","567890@qq.com",0,new Department(105,"党政办")));
        }
    
        //主键自增
        private static Integer initId = 1006;
    
        //增加一个员工
        public void addEmployee(Employee employee){
            if(employee.getId() == null){
                employee.setId(initId++);
            }
            employee.setDepartment(departmentMapper.getDepartmentById(employee.getDepartment().getId()));
            employees.put(employee.getId(),employee);
        }
    
        //查询全部员工信息
        public Collection<Employee> getEmployees(){
            return employees.values();
        }
    
        //通过id查询员工
        public Employee getEmployeeById(Integer id){
            return employees.get(id);
        }
    
        //删除员工
        public void removeEmployeeById(Integer id){
            employees.remove(id);
        }
    }
    

7.1、首页定制

  1. 建议使用扩展MVC配置的首页访问方式,template下的文件必须配置Controller跳转才能访问

    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
        //视图跳转,首页定制
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("index");
            registry.addViewController("/index.html").setViewName("index");
        }
    }
    
  2. 导入 Thymeleaf 依赖,在 index.html 中操作如下,并给出部分代码

    • 导入约束: xmlns:th="http://www.thymeleaf.org
    • 前端 url 表达式语法:@{…},/表示默认从 static 下寻找
    <!DOCTYPE html>
    <!--thymeleaf约束-->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="">
        <title>Signin Template for Bootstrap</title>
        <!-- Bootstrap core CSS -->
        <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
        <!-- Custom styles for this template -->
        <link th:href="@{/css/signin.css}" rel="stylesheet">
    </head>
    
  3. 允许在 application.properties 中修改首页根目录:

    # 首页根目录
    server.servlet.context-path=/cwlin
    
  4. application.properties 中 Thymeleaf配置内容:

    # THYMELEAF (ThymeleafAutoConfiguration)
    # 开启模板缓存(默认值:true)
    spring.thymeleaf.cache=true
    # 检查模板是否存在,然后再呈现
    spring.thymeleaf.check-template=true
    # 检查模板位置是否正确(默认值:true)
    spring.thymeleaf.check-template-location=true
    #Content-Type 的值(默认值:text/html)
    spring.thymeleaf.servlet.content-type=text/html
    # 开启 MVC Thymeleaf 视图解析(默认值:true)
    spring.thymeleaf.enabled=true
    # 模板编码
    spring.thymeleaf.encoding=UTF-8
    # 要被排除在解析之外的视图名称列表,⽤逗号分隔
    spring.thymeleaf.excluded-view-names=
    # 要运⽤于模板之上的模板模式。另⻅ StandardTemplate-ModeHandlers(默认值:HTML5)
    spring.thymeleaf.mode=HTML5
    # 在构建 URL 时添加到视图名称前的前缀(默认值:classpath:/templates/)
    spring.thymeleaf.prefix=classpath:/templates/
    # 在构建 URL 时添加到视图名称后的后缀(默认值:.html)
    spring.thymeleaf.suffix=.html
    

7.2、国际化

  1. 在 IDEA 的 setting 中 “file-encoding” 修改项目编码、配置文件编码均为 “utf-8”,否则配置文件中文会显示乱码

  2. 在 resources 路径下创建 i18n 文件夹,用于放置国际化相关的三个配置文件(点击Resource Bundle进入可视化编辑页面)

    • login.properties:无语言配置时候生效

      login.tip=请登录
      login.password=密码
      login.remember=记住我
      login.btn=登录
      login.username=用户名
      login.title=登录到员工管理系统
      
    • login_en_US.properties:英文生效

      login.tip=Please sign in
      login.password=Password
      login.remember=Remember me
      login.btn=Login
      login.username=
      login.title=Login to Employee Management System
      
    • login_zh_CN.properties:中文生效

      login.tip=请登录
      login.password=密码
      login.remember=记住我
      login.btn=登录
      login.username=用户名
      login.title=登录到员工管理系统
      
    • 命名方式是下划线的组合:文件名_语言_国家.properties;以此方式命名,IDEA会帮我们识别这是个国际化配置包,自动绑定在一起转换成如下的模式

      figures/31、SpringBoot(国际化配置文件)

  3. 【源码分析】

    • 在Spring程序中,国际化主要是通过ResourceBundleMessageSource这个类来实现

    • 而 Spring Boot 通过 MessageSourceAutoConfiguration 自动配置管理国际化资源文件的组件

    • 下面重点了解 messageSource() 这个方法:

      @Configuration(
          proxyBeanMethods = false
      )
      @ConditionalOnMissingBean(
          name = {"messageSource"},
          search = SearchStrategy.CURRENT
      )
      @AutoConfigureOrder(-2147483648)
      @Conditional({MessageSourceAutoConfiguration.ResourceBundleCondition.class})
      @EnableConfigurationProperties
      public class MessageSourceAutoConfiguration {
          private static final Resource[] NO_RESOURCES = new Resource[0];
      
          public MessageSourceAutoConfiguration() {
          }
      
          @Bean
          @ConfigurationProperties(
              prefix = "spring.messages"
          )
          public MessageSourceProperties messageSourceProperties() {
              return new MessageSourceProperties();
          }
      
          
      	// 获取 properties 传递过来的值进行判断
          @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;
          }
          
          //......
      }
      
    • 查看 MessageSourceProperties 类,类中首先声明了一个属性 basename,默认值为 messages

      public class MessageSourceProperties {
          private String basename = "messages";
          //......
      }
      
    • 在 application.properties 中,添加国际化资源路径,绑定配置文件的位置

      # 国际化资源路径
      spring.messages.basename=i18n.login
      
  4. 在 index.html 前端页面中,使用 th:text="#{login.xxx}" 等接收配置文件里的参数,如 #{login.title} 表示首页标题

    <!DOCTYPE html>
    <!--thymeleaf约束-->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="">
        <title th:text="#{login.title}"></title>
        <!-- Bootstrap core CSS -->
        <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
        <!-- Custom styles for this template -->
        <link th:href="@{/css/signin.css}" rel="stylesheet">
    </head>
    
    <body class="text-center">
    <form class="form-signin" th:action="dashboard.html">
        <img class="mb-4" th:src="@{/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>
        <!--<p style="color: red" th:text="${msg}"></p>-->
        <label class="sr-only">Username</label>
        <input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
        <label class="sr-only">Password</label>
        <input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">
        <div class="checkbox mb-3">
            <label>
                <input type="checkbox" th:text="#{login.remember}">
            </label>
        </div>
        <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
        <p class="mt-5 mb-3 text-muted">© 2020-2021</p>
        <a class="btn btn-sm">中文</a>
        <a class="btn btn-sm">English</a>
    </form>
    </body>
    
    </html>
    
  5. 在 index.html 中,添加中英文切换标签链接用于传递参数 lang,thymeleaf中的?key=value用(key=value)简化

    <!--这里传入参数不需要使用?使用key=value-->
    <a class="btn btn-sm" th:href="@{/index.html(lang='zh_CN')}">中文</a>
    <a class="btn btn-sm" th:href="@{/index.html(lang='en_US')}">English</a>
    
  6. 【源码分析】

    • 在Spring中有关于国际化的两个类:

      • Locale:代表地区,每一个Locale对象都代表了一个特定的地理、政治和文化地区
      • LocaleResolver:地区解析器
    • 双击 shift 搜索 WebMvcAutoConfiguration,找到方法localeResolver()

      @Bean
      @ConditionalOnMissingBean
      @ConditionalOnProperty(
          prefix = "spring.mvc",
          name = {"locale"}
      )
      public LocaleResolver localeResolver() {
          //如果用户配置了,则使用用户配置的地区解析器
          if (this.mvcProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.LocaleResolver.FIXED) {
              return new FixedLocaleResolver(this.mvcProperties.getLocale());
          }
          //如果用户没有配置,则使用默认的地区解析器
          else {
              AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
              localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
              return localeResolver;
          }
      }
      
    • 查看默认的地区解析器 AcceptHeaderLocaleResolver,发现它继承了LocaleResolver接口,从而实现地区解析

      public class AcceptHeaderLocaleResolver implements LocaleResolver {
          private final List<Locale> supportedLocales = new ArrayList(4);
          @Nullable
          private Locale defaultLocale;
          
          //......
      
          public AcceptHeaderLocaleResolver() {
          }
          
          public Locale resolveLocale(HttpServletRequest request) {
              Locale defaultLocale = this.getDefaultLocale();
              //默认就是根据请求头带来的区域信息获取Locale进行国际化
              if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
                  return defaultLocale;
              } else {
                  Locale requestLocale = request.getLocale();
                  List<Locale> supportedLocales = this.getSupportedLocales();
                  if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
                      Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
                      if (supportedLocale != null) {
                          return supportedLocale;
                      } else {
                          return defaultLocale != null ? defaultLocale : requestLocale;
                      }
                  } else {
                      return requestLocale;
                  }
              }
          }
          
          //......
      }
      
    • 因此,配置国际化组件 MyLocaleResolver.java,继承LocaleResolver接口,重写 resolveLocale 方法,从而实现中英文切换

      package com.cwlin.system.config;
      
      import org.springframework.util.StringUtils;
      import org.springframework.web.servlet.LocaleResolver;
      
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.util.Locale;
      
      //可以在链接上携带区域信息
      public class MyLocaleResolver implements LocaleResolver {
          //解析请求
          @Override
          public Locale resolveLocale(HttpServletRequest httpServletRequest) {
              String language = httpServletRequest.getParameter("lang");
              Locale locale = Locale.getDefault(); // 如果没有获取到就使用系统默认的
              //如果请求链接不为空
              if (!StringUtils.isEmpty(language)){
                  //分割请求参数: zh_CN
                  String[] split = language.split("_");
                  //国家,地区
                  locale = new Locale(split[0],split[1]);
              }
              return locale;
          }
      
          @Override
          public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
      
          }
      }
      
  7. 将自定义组件配置到spring IOC容器中:@Bean

    package com.cwlin.system.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.LocaleResolver;
    import org.springframework.web.servlet.View;
    import org.springframework.web.servlet.ViewResolver;
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    import java.util.Locale;
    
    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
        // 国际化解析器注册进组件
        @Bean
        public LocaleResolver localeResolver() {
            return new MyLocaleResolver();
        }
    
        //视图跳转,首页定制
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("index");
            registry.addViewController("/index.html").setViewName("index");
        }
    }
    

7.3、登陆功能实现

  1. 编辑 index.html,编写一个提交地址 /user/login,并给用户名和密码的输入框添加name用于传递参数

    <body class="text-center">
    <form class="form-signin" th:action="@{/user/login}">
        <!--......-->
    </form>
    </body>
    
  2. controller 包下新建 loginController 类,处理登录请求

    package com.cwlin.system.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.util.StringUtils;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    
    @Controller
    public class LoginController {
        @RequestMapping("/user/login")
        //注意:这里不能用@ResponseBody
        public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model){
            //具体业务:判断登录用户信息
            if(("cwlin".equals(username) || "Cwlin".equals(username)) && !StringUtils.isEmpty(password)){
                //登录成功
                return "dashboard";
            } else{
                //登录失败
                model.addAttribute("msg"," 用户名或者密码错误!");
                return "index";
            }
        }
    }
    
  3. index.html 中添加一个标签用来显示 controller 返回的错误信息,即登录出现错误时,让页面弹出提示:” 用户名或者密码错误!“

    <body class="text-center">
    <form class="form-signin" th:action="@{/user/login}">
        <img class="mb-4" th:src="@{/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>
        <p style="color: red" th:text="${msg}"></p>
        <!--......-->
    </form>
    </body>
    
  4. 启动主程序,访问 localhost:8080,页面成功跳转!

    http://localhost:8080/cwlin/user/login?username=cwlin&password=123456
    
  5. 但是,在页面地址栏中会显示用户名和密码,这显然是不符合要求的,添加 MyMvcConfig 中的视图控制;登录之后直接重定向跳转到 main.html,修改 LoginController

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
        registry.addViewController("/main.html").setViewName("dashboard");
    }
    
    @Controller
    public class LoginController {
        @RequestMapping("/user/login")
        //注意:这里不能用@ResponseBody
        public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model){
            //具体业务:判断登录用户信息
            if(("cwlin".equals(username) || "Cwlin".equals(username)) && !StringUtils.isEmpty(password)){
                //登录成功
                //return "dashboard";
                return "redirect:/main.html"; //重定向到main.html页面
            } else{
                //登录失败
                model.addAttribute("msg"," 用户名或者密码错误!");
                return "index"; //跳转到登陆页面
            }
        }
    }
    
  6. 这样,可以直接访问 http://localhost:8080/main.html,访问成功!

  7. 但是会出现一个问题:修改之后,无论登陆与否,都能直接访问后台页面。因此,需要设置登录拦截器

7.4、登录拦截器

  1. 用户登录成功后,后台会得到用户信息;如果没有登录,则不会有任何的用户信息!因此,我们可以通过拦截器进行拦截:

    • 当用户登录时,将用户信息存入session中,访问页面时首先判断session中有没有用户的信息。
    • 如果没有,拦截器进行拦截;如果有,拦截器放行。
  2. 首先在 LoginController 中,当用户登录成功后,将用户信息存入session中

    @Controller
    public class LoginController {
        @RequestMapping("/user/login")
        //注意:这里不能用@ResponseBody
        public String login(@RequestParam("username") String username, @RequestParam("password") String password,
                            Model model, HttpSession session){
            //具体业务:判断登录用户信息
            if(("cwlin".equals(username) || "Cwlin".equals(username)) && !StringUtils.isEmpty(password)){
                //登录成功
                //return "dashboard";
                session.setAttribute("loginUser",username);
                return "redirect:/main.html"; //重定向到main.html页面
            } else{
                //登录失败
                model.addAttribute("msg"," 用户名或者密码错误!");
                return "index"; //跳转到登陆页面
            }
        }
    }
    
  3. config 包下,新建一个自定义的登录拦截器类 LoginHandlerInterceptor,并继承HandlerInterceptor接口

    • 获取存入的session进行判断:如果为空,则返回错误消息,并且返回到首页,拦截;如果不为空,则放行。
    package com.cwlin.system.config;
    
    import org.springframework.web.servlet.HandlerInterceptor;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    public class LoginHandlerInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //登录成功之后应该有用户的session,session放在LoginController中
            Object loginUser = request.getSession().getAttribute("loginUser");
            if (loginUser == null){ //没有登录
                request.setAttribute("msg","没有权限,请重新登录"); //显示信息
                request.getRequestDispatcher("/index.html").forward(request,response); //返回登录页面
                return false; //拦截
            }else {
                return true; //放行
            }
        }
    }
    
  4. 在 MyMvcConfig 配置类中注册拦截器,重写关于拦截器的方法,添加自定义拦截器,注意屏蔽主页、静态资源和相关请求的拦截

    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
        //国际化解析器注册进组件
        @Bean
        public LocaleResolver localeResolver() {
            return new MyLocaleResolver();
        }
    
        //注册拦截器
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            //配置自定义拦截器,设置拦截哪些请求
            registry.addInterceptor(new LoginHandlerInterceptor())
                    .addPathPatterns("/**")
                    .excludePathPatterns("/index.html","/","/user/login","/css/**","/js/**","/img/**");
            //静态资源:*.css、*.js、*.img也不被拦截,为了登录失败时页面可以正常加载
        }
    
        //视图跳转,首页定制
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("index");
            registry.addViewController("/index.html").setViewName("index");
            registry.addViewController("/main.html").setViewName("dashboard");
        }
    }
    
  5. 重启主程序进行测试,直接访问 http://localhost:8080/main.html,页面提示:”没有权限,请重新登录“。

  6. 输入正确的用户名和密码,进入到 dashboard 页面。之后,如果再直接重新访问 http://localhost:8080/main.html,也可以直接进入到 dashboard 页面,这是因为session里面存入了用户信息,拦截器放行通过!

  7. 编辑 dashboard.html 中顶部导航栏,将 Company Name 替换为登陆用户名 [[${session.loginUser}]]

    <!--顶部导航栏-->
    <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" 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" th:href="@{/user/logout}">log out</a>
            </li>
        </ul>
    </nav>
    

7.5、CRUD

7.5.1、提取公共页面

  1. 在 templates 目录中新建一个 commons 文件夹,新建 commons.html 用来放置页面的公共代码,利用 th:fragment 提取顶部导航栏和侧边栏代码,这样其他页面可以通过插入 commons.html 中的模板实现代码复用

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <!--顶部导航栏,利用th:fragment提取出来,命名为topbar-->
    <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" 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" th:href="@{/user/logout}">Logout</a>
            </li>
        </ul>
    </nav>
    <!--侧边栏,利用th:fragment提取出来,命名为sidebar-->
    <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar(active)">
        <div class="sidebar-sticky">
            <ul class="nav flex-column">
                <li class="nav-item">
                    <a th:class="${active=='Home page'?'nav-link active':'nav-link'}" th:href="@{/main.html}">
                        <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>
                        Home page <span class="sr-only">(current)</span>
                    </a>
                </li>
                <li class="nav-item">
                    <a th:class="${active=='Departments'?'nav-link active':'nav-link'}" th:href="@{/departments/list}">
                        <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-file">
                            <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
                            <polyline points="13 2 13 9 20 9"></polyline>
                        </svg>
                        Departments
                    </a>
                </li>
                <li class="nav-item">
                    <a th:class="${active=='Employees'?'nav-link active':'nav-link'}" th:href="@{/employees/list}">
                        <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>
                        Employees
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" th:href="@{/main.html}">
                        <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-shopping-cart">
                            <circle cx="9" cy="21" r="1"></circle>
                            <circle cx="20" cy="21" r="1"></circle>
                            <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
                        </svg>
                        Products
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" th:href="@{/main.html}">
                        <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-bar-chart-2">
                            <line x1="18" y1="20" x2="18" y2="10"></line>
                            <line x1="12" y1="20" x2="12" y2="4"></line>
                            <line x1="6" y1="20" x2="6" y2="14"></line>
                        </svg>
                        Reports
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" th:href="@{/main.html}">
                        <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-layers">
                            <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
                            <polyline points="2 17 12 22 22 17"></polyline>
                            <polyline points="2 12 12 17 22 12"></polyline>
                        </svg>
                        Integrations
                    </a>
                </li>
            </ul>
    
            <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
                <span>Saved reports</span>
                <a class="d-flex align-items-center text-muted"
                   href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <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-plus-circle">
                        <circle cx="12" cy="12" r="10"></circle>
                        <line x1="12" y1="8" x2="12" y2="16"></line>
                        <line x1="8" y1="12" x2="16" y2="12"></line>
                    </svg>
                </a>
            </h6>
            <ul class="nav flex-column mb-2">
                <li class="nav-item">
                    <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                        <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-file-text">
                            <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                            <polyline points="14 2 14 8 20 8"></polyline>
                            <line x1="16" y1="13" x2="8" y2="13"></line>
                            <line x1="16" y1="17" x2="8" y2="17"></line>
                            <polyline points="10 9 9 9 8 9"></polyline>
                        </svg>
                        Current month
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                        <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-file-text">
                            <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                            <polyline points="14 2 14 8 20 8"></polyline>
                            <line x1="16" y1="13" x2="8" y2="13"></line>
                            <line x1="16" y1="17" x2="8" y2="17"></line>
                            <polyline points="10 9 9 9 8 9"></polyline>
                        </svg>
                        Last quarter
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                        <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-file-text">
                            <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                            <polyline points="14 2 14 8 20 8"></polyline>
                            <line x1="16" y1="13" x2="8" y2="13"></line>
                            <line x1="16" y1="17" x2="8" y2="17"></line>
                            <polyline points="10 9 9 9 8 9"></polyline>
                        </svg>
                        Social engagement
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                        <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-file-text">
                            <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                            <polyline points="14 2 14 8 20 8"></polyline>
                            <line x1="16" y1="13" x2="8" y2="13"></line>
                            <line x1="16" y1="17" x2="8" y2="17"></line>
                            <polyline points="10 9 9 9 8 9"></polyline>
                        </svg>
                        Year-end sale
                    </a>
                </li>
            </ul>
        </div>
    </nav>
    </html>
    
  2. 在其他页面( dashboard.htmlemp/list.html 等)中,使用 th:replace 插入 commons.html 中的模板

    • th:replace:替换全部元素,原有模块已经不存在;th:insert:加入元素,原有模块还是存在

      <!--公共页面提供 fragment-->
      <nav th:fragment="navbar">
      <!--需要插入公共页面的其他页面,commons包下的commons.html是提取得到的公共页面-->
      <div th:replace="~{commons/commons::navbar}"></div>
      <div th:insert="~{commons/commons::navbar}"></div>   
      
  3. 在侧边栏中,实现字体高亮样式,即被点击的页面高亮

    • 在普通页面中,将参数 (active='main.html') 传递给公共页面

      <!--侧边栏-->
      <div th:replace="~{commons/commons::sidebar(active='main.html')}"></div>
      
    • 在公共页面中,使用 th:fragment="sidebar(active)" 接收参数,并使用 th:class="${active=='main.html'?'nav-link active':'nav-link'}" 显示高亮

      <a th:class="${active=='main.html'?'nav-link active':'nav-link'}" th:href="@{/main.html}">
      
  4. 修改页面标题(略)

7.5.2、展示员工列表

  1. 编写 EmployeeController,在 EmployeeMapper 类中已经定义了所有的方法,这里可以直接使用(@Autowired)

    package com.cwlin.system.controller;
    
    import com.cwlin.system.mapper.EmployeeMapper;
    import com.cwlin.system.pojo.Employee;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import java.util.Collection;
    
    @Controller
    @RequestMapping("/employees")
    public class EmployeeController {
        @Autowired
        EmployeeMapper employeeMapper;
    
        @RequestMapping("/list")
        public String list(Model model){
            Collection<Employee> employees = employeeMapper.getEmployees();
            model.addAttribute("employees",employees);
            return "emp/list";
        }
    }
    
  2. 修改 emp/list.html 页面,展示员工列表,并修改性别和生日的显示

    • 展示员工列表:th:each="emp:${emps}"
    • 性别显示:th:text="${emp.getGender()==0?'女':'男'}"
    • 生日显示:th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"
  3. 添加 新增员工编辑删除 标签,为后续做准备,以下给出主体部分的代码:

    <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
        <h2>Employee list</h2>
        <h2><a class="btn btn-sm btn-success">新增员工</a></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>Birthday</th>
                    <th>Operations</th>
                </tr>
                </thead>
                <tbody>
                <tr th:each="employee:${employees}">
                    <td th:text="${employee.getId()}"></td>
                    <td th:text="${employee.getLastName()}"></td>
                    <td th:text="${employee.getEmail()}"></td>
                    <td th:text="${employee.getGender()==0?'':''}"></td>
                    <td th:text="${employee.getDepartment().getDepartmentName()}"></td>
                    <td th:text="${#dates.format(employee.getBirthday(),'yyyy-MM-dd')}"></td>
                    <td>
                        <a class="btn btn-sm btn-primary">编辑</a>
                        <a class="btn btn-sm btn-danger">删除</a>
                    </td>
                </tr>
                </tbody>
            </table>
        </div>
    </main>
    

7.5.3、新增员工

  1. list.html 中,添加 新增员工 的跳转链接,点击该按钮时发起请求

    <h2><a class="btn btn-sm btn-success" th:href="@{add}">新增员工</a></h2>
    
  2. EmployeeController 中编写对应的方法,处理点击 新增员工 的请求,注意:下拉框中的内容不应该是1、2、3、4、5,而应该是所有的部门名称,因此需要遍历得到所有部门名称。

    package com.cwlin.system.controller;
    
    import com.cwlin.system.mapper.DepartmentMapper;
    import com.cwlin.system.mapper.EmployeeMapper;
    import com.cwlin.system.pojo.Department;
    import com.cwlin.system.pojo.Employee;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import java.util.Collection;
    
    @Controller
    @RequestMapping("/employees")
    public class EmployeeController {
        @Autowired
        EmployeeMapper employeeMapper;
    
        @Autowired
        DepartmentMapper departmentMapper;
    
        @RequestMapping("/list")
        public String list(Model model){
            Collection<Employee> employees = employeeMapper.getEmployees();
            model.addAttribute("employees",employees);
            return "emp/list";
        }
    
        //跳转到emp/add.html
        @GetMapping("/add")
        public String add(Model model){
            //查询所有部门信息
            Collection<Department> departments = departmentMapper.getDepartments();
            model.addAttribute("departments",departments);
            return "emp/add";
        }
    }
    
  3. 编写 add.html,实现新增员工功能,设置部门默认为空,并返回部门编号 department.id,以下给出主体部分的代码:

    <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
        <h2>Add an employee</h2>
        <form th:action="@{add}" method="post">
            <div class="form-group">
                <label>LastName</label>
                <input type="text" name="lastName" class="form-control" placeholder="lastName: Lin">
            </div>
            <div class="form-group">
                <label>Email</label>
                <input type="email" name="email" class="form-control" placeholder="email: xxxxxx@qq.com">
            </div>
            <div class="form-group">
                <label>Gender</label><br/>
                <div class="form-check form-check-inline">
                    <input 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 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>
                <!--注意这里的name是department.id,因为传入的参数为id-->
                <select class="form-control" name="department.id">
                    <option value="" disabled selected hidden>department: please select a option</option>
                    <option th:each="department:${departments}"
                            th:text="${department.getDepartmentName()}"
                            th:value="${department.getId()}">
                    </option>
                </select>
            </div>
            <div class="form-group">
                <label>Birthday</label>
                <!--springboot默认的日期格式为yyyy/MM/dd-->
                <input type="text" name="birthday" class="form-control" placeholder="birthday: yyyy-MM-dd">
            </div>
            <button type="submit" class="btn btn-primary">添加</button>
        </form>
    </main>
    
  4. 修改前端的日期格式化(可以省略)

    #日期格式化
    spring.mvc.format.date=yyyy-MM-dd
    
  5. EmployeeController 中编写对应的方法,获取form提交的数据,实现新增员工操作,并返回到员工列表页面

    package com.cwlin.system.controller;
    
    import com.cwlin.system.mapper.DepartmentMapper;
    import com.cwlin.system.mapper.EmployeeMapper;
    import com.cwlin.system.pojo.Department;
    import com.cwlin.system.pojo.Employee;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import java.util.Collection;
    
    @Controller
    @RequestMapping("/employees")
    public class EmployeeController {
        @Autowired
        EmployeeMapper employeeMapper;
    
        @Autowired
        DepartmentMapper departmentMapper;
    
        @RequestMapping("/list")
        public String list(Model model){
            Collection<Employee> employees = employeeMapper.getEmployees();
            model.addAttribute("employees",employees);
            return "emp/list";
        }
    
        //跳转到emp/add.html
        @GetMapping("/add")
        public String add(Model model){
            //查询所有部门信息
            Collection<Department> departments = departmentMapper.getDepartments();
            model.addAttribute("departments",departments);
            return "emp/add";
        }
    
        //获取form提交的数据,新增员工
        @PostMapping("/add")
        public String added(Employee employee){
            //新增员工操作
            employeeMapper.addEmployee(employee);
            return "redirect:/employees/list";
        }
    }
    

7.5.4、修改员工

  1. list.html 中,添加 修改员工 的跳转链接,点击该按钮时发起请求

    <a class="btn btn-sm btn-primary" th:href="@{edit/{id}(id=${employee.getId()})}">编辑</a>
    
  2. 编写 edit.html,实现修改员工功能,注意:隐藏查询得到的employee.id,但是要传递回后端,否则会变为 新增员工 操作,以下给出主体部分的代码:

    <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
        <h2>Edit the employee</h2>
        <form th:action="@{edit}" method="post">
            <!--隐藏查询得到的employee.id-->
            <input type="hidden" name="id" th:value="${employee.getId()}">
            <div class="form-group">
                <label>LastName</label>
                <input th:value="${employee.getLastName()}" type="text" name="lastName" class="form-control"
                       placeholder="lastName: Lin">
            </div>
            <div class="form-group">
                <label>Email</label>
                <input th:value="${employee.getEmail()}" type="email" name="email" class="form-control"
                       placeholder="email: xxxxxx@qq.com">
            </div>
            <div class="form-group">
                <label>Gender</label><br/>
                <div class="form-check form-check-inline">
                    <input th:checked="${employee.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="${employee.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>
                <!--注意这里的name是department.id,因为传入的参数为id-->
                <select class="form-control" name="department.id">
                    <option th:each="department:${departments}"
                            th:selected="${department.getId()==employee.department.getId()}"
                            th:text="${department.getDepartmentName()}"
                            th:value="${department.getId()}">
                    </option>
                </select>
            </div>
            <div class="form-group">
                <label>Birthday</label>
                <!--springboot默认的日期格式为yy/MM/dd-->
                <input th:value="${#dates.format(employee.getBirthday(),'yyyy-MM-dd')}" type="text" name="birthday"
                       class="form-control"
                       placeholder="birthday: yyyy-MM-dd">
            </div>
            <button type="submit" class="btn btn-primary">修改</button>
        </form>
    </main>
    
  3. EmployeeController 中编写对应的方法:

    • 处理点击 修改员工 的请求(GET)
    • 获取form提交的数据,实现修改员工操作,并返回到员工列表页面(POST)
    package com.cwlin.system.controller;
    
    import com.cwlin.system.mapper.DepartmentMapper;
    import com.cwlin.system.mapper.EmployeeMapper;
    import com.cwlin.system.pojo.Department;
    import com.cwlin.system.pojo.Employee;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.Collection;
    
    @Controller
    @RequestMapping("/employees")
    public class EmployeeController {
        @Autowired
        EmployeeMapper employeeMapper;
    
        @Autowired
        DepartmentMapper departmentMapper;
    
        @RequestMapping("/list")
        public String list(Model model){
            Collection<Employee> employees = employeeMapper.getEmployees();
            model.addAttribute("employees",employees);
            return "emp/list";
        }
    
        //跳转到emp/add.html
        @GetMapping("/add")
        public String add(Model model){
            //查询所有部门信息
            Collection<Department> departments = departmentMapper.getDepartments();
            model.addAttribute("departments",departments);
            return "emp/add";
        }
    
        //获取form提交的数据,新增员工
        @PostMapping("/add")
        public String added(Employee employee){
            //新增员工操作
            employeeMapper.addEmployee(employee);
            return "redirect:/employees/list";
        }
    
        //跳转到emp/edit.html
        @GetMapping("/edit/{id}")
        public String edit(@PathVariable("id") Integer id, Model model){
            //查询员工的原有信息
            Employee employee = employeeMapper.getEmployeeById(id);
            model.addAttribute("employee",employee);
            //查询所有部门信息
            Collection<Department> departments = departmentMapper.getDepartments();
            model.addAttribute("departments",departments);
            return "emp/edit";
        }
    
        //获取form提交的数据,修改员工
        @PostMapping("/edit/{id}")
        public String edited(Employee employee){
            //修改员工操作,即等同于添加员工操作
            employeeMapper.addEmployee(employee);
            return "redirect:/employees/list";
        }
    }
    

7.5.5、删除员工

  1. list.html 中,添加 删除员工 的跳转链接,点击该按钮时发起请求

    <a class="btn btn-sm btn-danger" th:href="@{remove/{id}(id=${employee.getId()})}">删除</a>
    
  2. EmployeeController 中编写对应的方法,处理点击 删除员工 的请求,实现删除员工操作,并返回到员工列表页面

    package com.cwlin.system.controller;
    
    import com.cwlin.system.mapper.DepartmentMapper;
    import com.cwlin.system.mapper.EmployeeMapper;
    import com.cwlin.system.pojo.Department;
    import com.cwlin.system.pojo.Employee;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.Collection;
    
    @Controller
    @RequestMapping("/employees")
    public class EmployeeController {
        @Autowired
        EmployeeMapper employeeMapper;
    
        @Autowired
        DepartmentMapper departmentMapper;
    
        @RequestMapping("/list")
        public String list(Model model){
            Collection<Employee> employees = employeeMapper.getEmployees();
            model.addAttribute("employees",employees);
            return "emp/list";
        }
    
        //跳转到emp/add.html
        @GetMapping("/add")
        public String add(Model model){
            //查询所有部门信息
            Collection<Department> departments = departmentMapper.getDepartments();
            model.addAttribute("departments",departments);
            return "emp/add";
        }
    
        //获取form提交的数据,新增员工
        @PostMapping("/add")
        public String added(Employee employee){
            //新增员工操作
            employeeMapper.addEmployee(employee);
            return "redirect:/employees/list";
        }
    
        //跳转到emp/edit.html
        @GetMapping("/edit/{id}")
        public String edit(@PathVariable("id") Integer id, Model model){
            //查询员工的原有信息
            Employee employee = employeeMapper.getEmployeeById(id);
            model.addAttribute("employee",employee);
            //查询所有部门信息
            Collection<Department> departments = departmentMapper.getDepartments();
            model.addAttribute("departments",departments);
            return "emp/edit";
        }
    
        //获取form提交的数据,修改员工
        @PostMapping("/edit/{id}")
        public String edited(Employee employee){
            //修改员工操作,即等同于添加员工操作
            employeeMapper.addEmployee(employee);
            return "redirect:/employees/list";
        }
    
        //删除员工
        @GetMapping("/remove/{id}")
        public String removed(@PathVariable("id") Integer id){
            //修改员工操作,即等同于添加员工操作
            employeeMapper.removeEmployeeById(id);
            return "redirect:/employees/list";
        }
    }
    

7.5.6、展示部门列表

  • 继续完善部门相关的CRUD操作,这里不再给出详细内容!

7.6、404错误页面

  • 在templates下新建error文件夹,里面放入需要处理的错误,如404.html;访问错误的端口时,SpringBoot就会自动进行404处理。

7.7、注销功能

  1. commons 公共页面的顶部导航栏处中的标签中,添加 href 链接,实现点击发起 /user/logout 请求

    <a class="nav-link" th:href="@{/user/logout}">Logout</a>
    
  2. LoginController 中编写 logout 方法,注销掉session,并返回到登录页面

    package com.cwlin.system.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.util.StringUtils;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    
    import javax.servlet.http.HttpSession;
    
    @Controller
    public class LoginController {
        @RequestMapping("/user/login")
        //注意:这里不能用@ResponseBody
        public String login(@RequestParam("username") String username, @RequestParam("password") String password,
                            Model model, HttpSession session){
            //具体业务:判断登录用户信息
            if(("cwlin".equals(username) || "Cwlin".equals(username)) && !StringUtils.isEmpty(password)){
                //登录成功
                //return "dashboard";
                session.setAttribute("loginUser",username);
                return "redirect:/main.html"; //重定向到main.html页面
            } else{
                //登录失败
                model.addAttribute("msg"," 用户名或者密码错误!");
                return "index"; //跳转到登陆页面
            }
        }
    
        @RequestMapping("/user/logout")
        public String logout(HttpSession session, Model model){
            //注销session
            session.invalidate();
            model.addAttribute("msg",null);
            return "redirect:/index.html";
        }
    }
    

7.8、如何写一个网站

  1. 前端

    1. 模版:自己网站搜
    2. 框架:组件,需要自己手动拼接:BootStrap、Layui、semantic-ui
      • 栅格系统
      • 导航栏
      • 侧边栏
      • 表单
  2. 设计数据库(真正的难点)

  3. 前端让他能够自动运行,独立化工程

  4. 数据接口如何对接:json、对象(all in one)

  5. 前后端联调测试

    1. 前端:至少能够通过 前端框架 组合得到一个网页
      • 首页:index
      • 关于:about
      • 博客:blog
      • 提交博客:post
      • 用户:user
      • 等等
    2. 后端:必须要有一个熟悉的 后台模版 ,99%公司会让你自己写:推荐X-admin网站模版
    3. 前后端联调:能够让网站独立运行
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值