0、微服务
0.1、微服务阶段
- javase:OOP
- mysql:持久化
- html+css+js+jquery+框架:视图,框架不熟练,css不好
- javaweb:独立开发MVC3三层架构的网站(原始)
- ssm:框架:简化了我们的开发流程,配置也开始较为复杂
- war:tomcat运行
- spring再简化:SpringBoot - jar:内嵌tomcat;微服务架构!
- 服务越来越多:springcloud
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.每个功能元系的服务都是一 个可替换的、 可独立升级的软件代码。
0.2.4、如何构建微服务
- 一个大型系统的微服务架构,就像一个复杂交织的神经网络,每一个神经元就是一个功能元素,它们各自完成自己的功能,然后通过http相互请求调用。比如一个电商系统,查缓存、连数据库、浏览页面、结账、支付等服务都是一个个独立的功能服务,都被微化了,它们作为一个个微服务共同构建了一个庞大的系统。如果修改其中的一个功能,只需要更新升级其中一个功能服务单元即可。
- 但是这种庞大的系统架构给部署和运维带来很大的难度。于是,spring为我们带来了构建大型分布式微服务的全套、全程产品:
- 构建一个个功能独立的微服务应用单元,可以使用springboot,可以帮我们快速构建一个 应用;
- 大型分布式网络服务的调用,这部分由spring cloud来完成,实现分布式;
- 在分布式中间,进行流式数据计算、批处理, 我们有spring cloud data flow。
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、创建基础项目说明
-
Spring官方提供了非常方便的工具让我们快速构建应用
-
Spring Initializr:https://start.spring.io/
2.2.1、项目创建方式一:使用Spring Initializr 的 Web页面创建项目
-
填写项目信息
-
点击”Generate Project“按钮生成项目;下载此项目
-
解压项目包,并用IDEA以Maven项目导入,一路下一步即可,直到项目导入完毕。
-
如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。
2.2.2、项目创建方式二:使用 IDEA 直接创建项目
-
创建一个新项目
-
选择spring initalizr, 默认是使用官网的快速构建工具实现
-
填写项目信息
-
选择初始化的组件(初学勾选 Web 即可)
-
填写项目路径
-
等待项目构建成功
2.2.3、项目结构分析
通过上面步骤完成了基础项目的创建。就会自动生成以下文件:
-
程序的主启动类
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); } }
-
application.properties 配置文件
# 应用名称 spring.application.name=helloWorld # 应用服务 WEB 访问端口 server.port=8080
-
测试类
package com.cwlin; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; //测试类 @SpringBootTest class SpringBoot01HelloWorldApplicationTests { @Test void contextLoads() { } }
-
pom.xml
<!--详细内容见下一节-->
2.3、pom.xml 分析
- 打开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>
- 分析上述代码,主要包含四个部分:
- 项目元数据信息:创建时候输入的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
实现热部署。
- 热部署设置:
-
设置
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接口
-
在主程序的同级目录下,新建一个controller包,一定要在同级目录下,否则识别不到
-
在包中新建一个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"; } }
-
编写完毕后,从主程序启动项目,在浏览器发起请求: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容器中
- @AutoConfigurationPackages.Registrar.class:
-
@Import(AutoConfigurationImportSelector.class) ,
- AutoConfigurationImportSelector.class
- 找到
getCandidateConfigurations
方法:获得候选的配置
- 找到
- AutoConfigurationImportSelector.class
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配置类已经加载好的
5、总结
- springboot所有的自动配置都是在启动的时候自动扫描并加载,所有的自动配置类都在spring.factories 里,但是不一定生效,要判断条件是否成立,只要导入了对应的start,就有对应的启动器,有了启动器,我们自动装配就生效,然后就配置成功
- springboot在启动的时候,从类路径下META-INF/spring.factories 获取EnableAutoConfiguration指定的值
- 将这些值作为自动配置类导入容器,自动配置类就会生效,帮我们进行自动配置(在这里,以前我们需要手动配置的东西,现在springboot帮我们做了)
- 整个JavaEE的解决方案和自动配置都在spring-boot-autoconfigure-xxx.RELEASE.jar包下
- 它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器。这样的话,容器中会存在非常多的自动配置类(xxxAutoConfiguration,也就是@Bean),这些类给容器中导入了这个场景需要的所有组件,并自动配置好这些组件 @Configuration、JavaConfig
- 有了自动配置类,免去了我们手动编写配置文件的工作
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中需要填入= 它指定的前缀+方法
3.2.3、工作原理总结
- 读取
spring.properties
文件- SpringBoot 在启动的时候从
spring-boot-autoConfigure.jar
包下的的META-INF/spring.factories
中获取EnableAutoConfiguration
属性的值加载自动配置类 - 将这些值作为自动配置类导入容器,自动配置类就生效,帮我们进行自动配置工作
- SpringBoot 在启动的时候从
- 加载
XXXProperties
类- 根据自动配置类中指定的xxxxProperties类设置自动配置的属性值,开发者可以根据该类在yaml配置文件中修改自动配置
- 根据
@ConditionalXXX
注解决定加载哪些组件- Springboot 通过该注解指定组件加入IOC容器时锁需要具备的特定条件,这个组件会在满足条件时候加入到IOC容器内
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类主要做了以下四件事情:
- 推断应用的类型是普通的项目还是Web项目
- 查找并加载所有可用初始化器 , 设置到initializers属性中
- 找出所有的应用程序监听器,设置到listeners属性中
- 推断并设置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方法流程分析(了解)
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语法
- 空格不能省略,键值对中间必须要有一个空格
- 以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
- 属性和值的大小写都是十分敏感的。
4.3.2、yaml基本类型写入
-
字面值:普通字符串、数值、布尔类型,直接写成k:v,字符串默认不用加单引号或双引号
name: cwlin
-
“ ” 双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思;
比如:name: “cw \n lin” 输出:cw 换行 lin
-
‘ ’ 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出
比如:name: ‘cw \n lin’ 输出:cw \n lin
-
注意:设置数据库密码如果是0开头,SpringBoot会默认按照八进制来解析,yml配置时加上引号,如
'01111'
-
-
对象、Map:属性值必须和Bean中的对应一致
Student: name: cwlin age: 3 # 行内写法 Student: {name: cwlin, age: 3}
-
数组(List、Set):使用 - 表示一个元素
pets: - cat - dog - pig # 行内写法 pets: [cat, dog, pig]
4.4、yml配置bean属性,导入依赖
-
在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
-
编写一个实体类 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; }
-
在测试类下注入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 } }
-
编写一个复杂一点的实体类 Person,并使用yaml配置的方式进行注入
-
@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; }
-
-
在测试类中注入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); } }
-
查看控制台显示(结果略)
4.5、加载指定配置文件
-
@ConfigurationProperties(prefix = “person”):默认是从全局配置文件中获取值
- 文件名只能是application.yml
- 优点:只需要配置 prefix=key,会自动匹配相对应的属性
-
@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数据校验 | 支持 | 不支持 |
复杂类型封装 | 支持 | 不支持 |
- 结论:
- @ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加
- 松散绑定:yml中的 first-name = bean中的 firstName。再举个例子:yml中的last-name 和 bean中的lastName是一样的、对应的,注意:- 后面跟着的字母默认是大写的
- JSR303数据校验:这个就是我们可以在字段是增加一层过滤器验证 , 可以保证数据的合法性
- 复杂类型封装:yml中可以封装对象 , 使用value就不支持
- 建议:
- 配置yml和配置properties都可以获取到值,强烈推荐 yml;
- 如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;
- 如果说,我们专门编写了一个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 { // ...... }
-
自动装配原理的精髓
- SpringBoot启动会加载大量的自动配置类;
- 我们看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;
- 我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)
- 给容器中自动配置类添加组件的时候,会从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环境 |
@ConditionalOnJndi | JNDI存在指定项 |
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的步骤:
-
创建一个SpringBoot应用,选择我们需要的模块,SpringBoot就会默认将所需要的模块自动配置好
-
手动在配置文件中配置部分配置项目就可以运行起来了
-
专注编写业务代码,不需要考虑以前那样一大堆的配置了
自动配置:比如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文件!
-
访问: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、总结
- 在SpringBoot中,可以使用以下方式处理静态资源
- classpath:/META-INF/resources/webjars/:localhost:8080/webjars/(webjars不推荐使用,官网导包)
- /**、public、static、resources:localhost:8080/(resources根目录下的路径)
- 通过配置文件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");
}
- 编写index.html,放在public目录下,访问:http://localhost:8080/
<!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、图标
- 新版本中似乎删去了关闭SpringBoot默认图标的配置:
#关闭默认图标
spring.mvc.favicon.enabled=false
- 将favicon.icon放到resources目录下 例如:/public,/static等等
- 完成上面的步骤还不能显示,还需要在页面的head标签添加代码
<head>
<meta charset="UTF-8">
<title>登录</title>
<link rel="shortcut icon" th:href="@{/favicon.ico}"/>
<link rel="bookmark" th:href="@{/favicon.ico}"/>
</head>
- 注意:上面用的thymeleaf模板!如果不是,则要使用下面这个html
<head>
<meta charset="UTF-8">
<title>登录</title>
<link rel="shortcut icon" href="/favicon.ico"/>
<link rel="bookmark" href="/favicon.ico"/>
</head>
- 清除浏览器缓存,刷新网页,发现图标改变!
6.3、Thymeleaf 模板引擎
6.3.1、概述
- 前端交给我们的页面,是html页面。如果是我们以前开发,我们需要把他们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示、交互等。
- jsp支持非常强大的功能,包括能写Java代码,但是对于SpringBoot这个项目,第一,它是以jar的方式,不是war;第二,它用的还是嵌入式的Tomcat,因此,他现在默认是不支持jsp的。
- 如果不支持jsp,而直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦。因此,SpringBoot推荐你可以来使用模板引擎:
- 其实jsp就是一个模板引擎,还有用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但是他们的思想都是一样的:
- 模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。而这些值,从哪来呢,就是我们在后台封装一些数据。然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。不同模板引擎之间,他们的语法可能有点不一样。
- 以下主要介绍一下SpringBoot给我们推荐的Thymeleaf模板引擎,这是一个高级语言的模板引擎。
6.3.2、引入Thymeleaf
-
对于springboot来说,什么事情不都是一个start的事情。查看下面的三个网址:
- Thymeleaf 官网:https://www.thymeleaf.org/
- Thymeleaf 在Github 的主页:https://github.com/thymeleaf/thymeleaf
- Spring官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters
-
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 pressctrl-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/
-
再做一个测试:
-
编写一个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"; } }
-
编写一个测试页面 test.html 放在 templates 目录下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>测试页面</h1> </body> </html>
-
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中的测试,我们做个简单的测试:查出一些数据,在页面中展示
-
修改测试请求,增加数据传输
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"; } }
-
我们要使用thymeleaf,需要在html文件中导入命名空间的约束
xmlns:th="http://www.thymeleaf.org"
-
我们去编写下前端页面
<!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>
-
-
可以使用任意的 th:attr 来替换html中原生属性的值!
- 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、练习测试
-
我们编写一个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"; } }
-
测试页面取出数据
<!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>
-
启动项目测试:http://localhost:8080/test1
6.4、SpringMVC 自动配置原理
6.4.1、官方文档说明
-
在进行项目编写前,我们需要SpringBoot对SpringMVC做了哪些配置,包括如何扩展,如何定制
- 途径一:源码分析
- 途径二:官方文档:Spring MVC Auto-configuration
// 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); }
-
接下来,我们给容器中添加一个视图解析器,这个类会自动将它组合进来
-
在 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; } } }
-
查看自定义的视图解析器是否起作用
-
找到 DispatcherServlet 中的 doService方法 -> doDispatch方法
doDispatch(request, response);
-
在doDispatch方法上加个断点进行调试(所有的请求都会走到这个方法中)
-
-
启动项目,访问页面http://localhost:8080/,查看IDEA中的Debug信息
- 找到视图解析器 this.viewResolvers
- 找到自定义的视图解析器 MyMVCConfig$myViewResolver@6627
-
具体的操作说明如下图所示:
-
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注解的原理:
-
WebMvcAutoConfiguration 是 SpringMVC的自动配置类,里面有一个 WebMvcAutoConfigurationAdapter 类
-
这个类上有一个注解,在做其他自动配置时会导入:@Import({DelegatingWebMvcConfiguration.class})
-
点击查看 EnableWebMvcConfiguration 类,它继承了一个父类:DelegatingWebMvcConfiguration(也就是@EnableWebMvc注解导入的类)
-
这个父类 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); } } //...... }
-
在这个类中找到我们刚刚设置的viewController当做参考,发现它调用了一个
configurers.addViewControllers(registry)
@Override protected void addViewControllers(ViewControllerRegistry registry) { this.configurers.addViewControllers(registry); }
-
点击进入查看 addViewControllers 方法
@Override public void addViewControllers(ViewControllerRegistry registry) { //将所有的WebMvcConfigurer相关配置一起调用!包括我们自定义配置的和Spring配置的 for (WebMvcConfigurer delegate : this.delegates) { delegate.addViewControllers(registry); } }
-
结论:
- @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、实体类编写和数据库模拟
-
创建 spring 项目,选中相应的依赖;导入静态资源(html、css等)。
-
创建实体类 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(); } }
-
编写实体类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、首页定制
-
建议使用扩展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"); } }
-
导入 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>
- 导入约束:
-
允许在 application.properties 中修改首页根目录:
# 首页根目录 server.servlet.context-path=/cwlin
-
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、国际化
-
在 IDEA 的 setting 中 “file-encoding” 修改项目编码、配置文件编码均为 “utf-8”,否则配置文件中文会显示乱码
-
在 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会帮我们识别这是个国际化配置包,自动绑定在一起转换成如下的模式
-
-
【源码分析】
-
在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
-
-
在 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>
-
在 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>
-
【源码分析】
-
在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) { } }
-
-
将自定义组件配置到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、登陆功能实现
-
编辑
index.html
,编写一个提交地址/user/login
,并给用户名和密码的输入框添加name
用于传递参数<body class="text-center"> <form class="form-signin" th:action="@{/user/login}"> <!--......--> </form> </body>
-
在
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"; } } }
-
在
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>
-
启动主程序,访问
localhost:8080
,页面成功跳转!http://localhost:8080/cwlin/user/login?username=cwlin&password=123456
-
但是,在页面地址栏中会显示用户名和密码,这显然是不符合要求的,添加 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"; //跳转到登陆页面 } } }
-
这样,可以直接访问
http://localhost:8080/main.html
,访问成功! -
但是会出现一个问题:修改之后,无论登陆与否,都能直接访问后台页面。因此,需要设置登录拦截器!
7.4、登录拦截器
-
用户登录成功后,后台会得到用户信息;如果没有登录,则不会有任何的用户信息!因此,我们可以通过拦截器进行拦截:
- 当用户登录时,将用户信息存入session中,访问页面时首先判断session中有没有用户的信息。
- 如果没有,拦截器进行拦截;如果有,拦截器放行。
-
首先在
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"; //跳转到登陆页面 } } }
-
在
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; //放行 } } }
-
在 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"); } }
-
重启主程序进行测试,直接访问
http://localhost:8080/main.html
,页面提示:”没有权限,请重新登录“。 -
输入正确的用户名和密码,进入到
dashboard
页面。之后,如果再直接重新访问http://localhost:8080/main.html
,也可以直接进入到dashboard
页面,这是因为session里面存入了用户信息,拦截器放行通过! -
编辑
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、提取公共页面
-
在 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>
-
在其他页面(
dashboard.html
和emp/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>
-
-
在侧边栏中,实现字体高亮样式,即被点击的页面高亮
-
在普通页面中,将参数
(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}">
-
-
修改页面标题(略)
7.5.2、展示员工列表
-
编写 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"; } }
-
修改
emp/list.html
页面,展示员工列表,并修改性别和生日的显示- 展示员工列表:
th:each="emp:${emps}"
- 性别显示:
th:text="${emp.getGender()==0?'女':'男'}"
- 生日显示:
th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"
- 展示员工列表:
-
添加
新增员工
、编辑
和删除
标签,为后续做准备,以下给出主体部分的代码:<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、新增员工
-
在
list.html
中,添加新增员工
的跳转链接,点击该按钮时发起请求<h2><a class="btn btn-sm btn-success" th:href="@{add}">新增员工</a></h2>
-
在
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"; } }
-
编写
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>
-
修改前端的日期格式化(可以省略)
#日期格式化 spring.mvc.format.date=yyyy-MM-dd
-
在
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、修改员工
-
在
list.html
中,添加修改员工
的跳转链接,点击该按钮时发起请求<a class="btn btn-sm btn-primary" th:href="@{edit/{id}(id=${employee.getId()})}">编辑</a>
-
编写
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>
-
在
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、删除员工
-
在
list.html
中,添加删除员工
的跳转链接,点击该按钮时发起请求<a class="btn btn-sm btn-danger" th:href="@{remove/{id}(id=${employee.getId()})}">删除</a>
-
在
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、注销功能
-
在
commons
公共页面的顶部导航栏处中的标签中,添加href
链接,实现点击发起/user/logout
请求<a class="nav-link" th:href="@{/user/logout}">Logout</a>
-
在
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、如何写一个网站
-
前端
- 模版:自己网站搜
- 框架:组件,需要自己手动拼接:BootStrap、Layui、semantic-ui
- 栅格系统
- 导航栏
- 侧边栏
- 表单
-
设计数据库(真正的难点)
-
前端让他能够自动运行,独立化工程
-
数据接口如何对接:json、对象(all in one)
-
前后端联调测试
- 前端:至少能够通过
前端框架
组合得到一个网页- 首页:index
- 关于:about
- 博客:blog
- 提交博客:post
- 用户:user
- 等等
- 后端:必须要有一个熟悉的
后台模版
,99%公司会让你自己写:推荐X-admin网站模版 - 前后端联调:能够让网站独立运行
- 前端:至少能够通过