SpringBoot基础(一)
该笔记记录的是SpringBoot2,更新的SpringBoot3版本需要参考最新官方文档
文章目录
- SpringBoot基础(一)
- 1、概念+入门
- 1.1 回顾Spring
- 1.2 什么是SpringBoot
- 1.3 什么是微服务
- 1.4 第一个SpringBoot项目
- 2、SpringBoot运行原理
- pom.xml
- 主启动类
- 默认的主启动类
- @SpringBootApplication
- 小结
- SpringApplication
- 3、yaml配置注入
- 3.1 yaml概念
- 3.2 yaml基础语法
- 3.3 注入配置文件
- 3.4 加载指定的配置文件
- 3.5 配置文件占位符
- 3.6 两种注入方式对比
- 3.7 JSR303数据校验(*)
- 4、自动配置原理
- 5、自定义Starter
- 5.1 编写启动器
- 5.2 测试自己写的启动器
- 6、SpringBoot Web开发
- 6.1 导入静态资源
- 6.1.1 WebMvcAutoConfiguration类
- 6.1.2 webjars
- 6.1.3 导入自己的静态资源
- 6.1.4 自定义静态资源路径
- 6.1.5 首页处理
- 6.2 Thymeleaf
- 使用例
- 6.2.1 Thymeleaf语法
- 查找数据
- 基本语法
- 使用例
- 6.3 MVC自动配置
- 6.3.1 官方介绍
- 6.3.2 ContentNegotiatingViewResolver(内容协商视图解析器)
- 探究源码
- 自定义视图解析器
- 6.3.3 修改SpringBoot的默认配置
- 原理
- 6.3.4 全面接管SpringMVC(*)
- 6.4 小项目:员工管理系统
- 6.4.1 准备工作
- 6.4.2 实现首页
- 6.4.3 页面国际化
- 6.4.4 登录功能
- 基本功能
- 登录拦截器
- 6.4.5 展示员工列表(CRUD)
- 提取重复页面
- 侧边栏选中时高亮
- 查
- 增
- 改
- 删
- 6.4.6 404处理
- 6.4.7 注销功能
- 删
- 6.4.6 404处理
- 6.4.7 注销功能
1、概念+入门
1.1 回顾Spring
Spring是一个开源框架,2003 年兴起的一个轻量级的Java开发框架。
目的:解决企业级应用开发的复杂性,简化开发
Spring如何简化Java开发
- 基于pojo(实体类)的轻量级和最小侵入性编程,所有东西可以抽象为bean。
- 通过IOC,依赖注入(DI)和面向接口实现松耦合。
- 基于切面(AOP)和惯例进行声明式编程。
- 通过切面和模板减少样式代码,如
RedisTemplate
,xxxTemplate
等。
1.2 什么是SpringBoot
Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序,且约定大于配置。
SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。可以在一定程度上解决Spring在工程量大的情况下产生的"配置地狱"情景。
SpringBoot的优点
- 为所有Spring开发者更快的入门
- 开箱即用,提供各种默认配置来简化项目配置
- 内嵌式容器简化Web项目(tomcat)
- 没有冗余代码生成和XML配置的要求
1.3 什么是微服务
微服务是一种架构风格,它要求在开发一个应用时,这个应用必须构建成一系列更小服务的组合,可以通过http的方式进行互通。
单体应用架构
单体应用架构(all in one),代表将所有应用服务都封装在一个应用中。
无论是ERP、CRM或其他系统,一个项目的数据库访问、web访问等等功能,在这个架构下会放到一个war包内。
- 好处:易于开发和测试,方便部署。需要扩展时,只需要war包复制多份,然后放到多个服务器上,再做个负载均衡即可。
- 缺点:每次需要修改一小处地方时,就要停掉整个服务,重新打包、部署war包,这对于大型应用的维护很不便利。
微服务架构
微服务打破了all in one的架构方式,把每个功能元素独立出来,把独立出来的功能元素按需求进行动态组合。
好处:
- 节省了调用资源
- 每个功能元素的服务都是一个可替换的,可独立升级的软件代码
比如下面这个博客网站
网站整体由好几个功能卡片构成,左侧的"皮肤"卡片、"设置"卡片,中间的"留言板"卡片,右侧的"音乐播放器"卡片,每张卡片本身就是一个微服务,它们在前端被整合到了一个网页中。
B站的动态页面的构成方式也跟这个差不多。
Martin Flower于2014年3月25日发表论文《Microservice》,解释了什么是微服务。
1.4 第一个SpringBoot项目
环境:
- jdk 1.8
- maven
- springboot2
- IDEA
创建方式:
- 通过官网快速生成一个SpringBoot项目
- 打开https://start.spring.io/
- 填写项目信息
- 点击”Generate Project“按钮生成项目;下载此项目
- 解压项目包,并用IDEA以Maven项目导入,一路下一步即可,直到项目导入完毕。
- 如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。
- IDEA生成
- 创建一个新项目
- 选择spring initalizr , 可以看到默认就是去官网的快速构建工具那里生成项目
- 填写项目信息
- 选择初始化的组件(初学勾选 Web 即可)
- 填写项目路径
- 等待项目构建成功
项目初始结构:
pom.xml
<!--父依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!--web场景启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--springboot单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<!-- 剔除依赖 -->
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
编写一个http接口
- 在项目启动类的同级目录下,新建一个controller包
- 在保重新建
HelloController
类
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "Hello World";
}
}
- 从主程序启动项目,浏览器发起请求,看页面返回;控制台会输出 Tomcat 访问的端口号
将项目打成jar包(点击maven的package指令)
打包后可以放到其他项目里进行调用
启动图标
可以通过修改resources/banner.txt
中的内容改变启动项目时的大字图标(没有就新建)。
相关图案可以去https://www.bootschool.net/ascii 生成
banner.txt样例:
█████ ██████ █████ ███████████ █████ ██████ █████ █████ ███████████ ██████████
▒▒███ ▒▒██████ ▒▒███ ▒▒███▒▒▒▒▒▒█▒▒███ ▒▒██████ ▒▒███ ▒▒███ ▒█▒▒▒███▒▒▒█▒▒███▒▒▒▒▒█
▒███ ▒███▒███ ▒███ ▒███ █ ▒ ▒███ ▒███▒███ ▒███ ▒███ ▒ ▒███ ▒ ▒███ █ ▒
▒███ ▒███▒▒███▒███ ▒███████ ▒███ ▒███▒▒███▒███ ▒███ ▒███ ▒██████
▒███ ▒███ ▒▒██████ ▒███▒▒▒█ ▒███ ▒███ ▒▒██████ ▒███ ▒███ ▒███▒▒█
▒███ ▒███ ▒▒█████ ▒███ ▒ ▒███ ▒███ ▒▒█████ ▒███ ▒███ ▒███ ▒ █
█████ █████ ▒▒█████ █████ █████ █████ ▒▒█████ █████ █████ ██████████
▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒
2、SpringBoot运行原理
下面针对前面的HelloSpringBoot项目目录进行分析
pom.xml
父依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
ctrl+鼠标左键,点击spring-boot-starter-parent
,内部还有一个父依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.6</version>
</parent>
这里才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心。
以后导入依赖默认不需要写版本,但是如果导入的包没有在依赖中管理,就需要手动配置版本。
启动器 spring-boot-starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
springboot-boot-starter-xxx
:spring-boot的场景启动器
spring-boot-starter-web
:帮我们导入了web模块正常运行所依赖的组件
SpringBoot将所有的功能场景都抽取出来,做成一个个的starter (启动器,可以联想微服务),只需要在项目中引入这些starter即可,引入start后所有相关的依赖都会自动导入,。根据需求导入相关的场景启动器即可 。未来也可以自定义 starter。
主启动类
默认的主启动类
@SpringBootApplication //标注一个主程序类,说明这是一个SpringBoot应用
public class HelloSpringBootApplication {
public static void main(String[] args) {
//启动一个方法(服务)
SpringApplication.run(HelloSpringBootApplication.class, args);
}
}
@SpringBootApplication
标注在某个类上说明这个类是SpringBoot的主配置类 , SpringBoot应用通过这个类启动。
进入这个注解,查看注解内部定义:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
//......
}
@ComponentScan
这个注解在Spring中很重要 ,它对应XML配置中的元素。
作用:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中。
@SpringBootConfiguration
作用:SpringBoot的配置类 ,标注在某个类上 , 表示这是一个SpringBoot的配置类;
进去这个注解查看具体定义
@Configuration
public @interface SpringBootConfiguration {}
@Component
public @interface Configuration {}
@Configuration
说明这是一个配置类 ,配置类就是对应Spring的xml 配置文件。
@Component
说明启动类本身也是Spring中的一个组件,负责启动应用。
下面回到 SpringBootApplication 注解中继续看。
@EnableAutoConfiguration
作用:开启自动配置功能,让SpringBoot帮我们自动配置。
进入这个注解查看具体定义
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
@AutoConfigurationPackage
: 自动配置包
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
@import
:Spring底层注解@import , 给容器中导入一个组件
Registrar.class
作用:将主启动类的所在包及包下面 所有子包里面的 所有组件扫描到Spring容器中
@Import({AutoConfigurationImportSelector.class})
:给容器导入组件。这个组件会自动配置导入选择器。
1、这个注解的源码内有一个方法:
//获得候选的配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//返回一开始看到的启动自动导入配置文件的注解类;EnableAutoConfiguration
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
2、这个方法又调用了SpringFactoriesLoader
类的静态方法,进入这个方法查看源码
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
//这里又调用了loadSpringFactories方法
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
3、进入loadSpringFactories方法
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
//去获取一个资源 "META-INF/spring.factories"
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
//将读取到的资源遍历,封装成为一个Properties
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
4、发现一个多次出现的文件:spring.factories
,全局搜索它
随便点进一个类进行查看
可以发现这个类是JavaConfig配置类,且都注入了Bean。其他类同理
所以,自动配置的真正实现,是从classpath中搜寻所有的META-INF/spring.factories
配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure.
包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中。
小结
- SpringBoot在启动的时候从类路径下的
META-INF/spring.factories
中获取EnableAutoConfiguration
指定的值 - 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
- 整个JavaEE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中;
- 它会给容器中导入非常多的自动配置类 (
xxxAutoConfiguration
), 就是给容器中导入这个场景需要的所有组件 , 并配置好这些组件 ; - 有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;
@SpringBootConfiguration //springboot的配置
@Configuration //spring配置类
@Component //说明这是一个spring的组件
@EnableAutoConfiguration //自动配置
@AutoConfigurationPackage //自动配置包
@Import({Registrar.class}) //自动配置包注册
@Import({AutoConfigurationImportSelector.class}) //自动配置导入选择
SpringApplication
@SpringBootApplication
public class HelloSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(HelloSpringBootApplication.class, args);
}
}
SpringApplication.run
分为两部分:SpringApplication的实例化、run方法的执行
SpringApplication
做了四件事:
- 判断应用的类型是普通的项目还是web项目
- 查找并加载所有可用初始化工具,设置到
initializers
属性中 - 找出所有的应用程序监听器,设置到
listeners
属性中 - 推断并设置main方法的定义类,找到运行的主类
构造器:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
//......
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = this.deduceMainApplicationClass();
}
run方法流程:
3、yaml配置注入
SpringBoot使用一个全局的配置文件 , 配置文件名称是固定的
- application.properties
- 语法结构 :key=value
- application.yml (空格不能忽略)
- 语法结构 :key:[空格] value
**配置文件的作用 :**修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给已经自动配置好了。
比如我们可以在配置文件中修改Tomcat 默认启动的端口号
server.port=8081
3.1 yaml概念
YAML是 “YAML Ain’t a Markup Language” (YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)
这种语言以数据作为中心,而不是以标记语言为重点
以前的配置文件,大多数都是使用xml来配置;比如一个简单的端口配置,对比下yaml和xml
xml
<server>
<port>8081<port>
</server>
yaml
server:
port: 8080
3.2 yaml基础语法
- 空格不能省略
- 以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
- 属性和值的大小写都十分敏感。
字面量:普通的值 [ 数字,布尔值,字符串 ]
字面量直接写在后面即可 , 字符串默认不用加上双引号或者单引号
k: v
-
""
双引号,会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思。比如 :name: “xxxx \n yyyy” 输出 :xxxx 换行 yyyy -
''
单引号,不会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出。比如 :name: ‘xxxx \n yyyy’ 输出 :xxxx \n yyyy
对象、map
k:
v1:
v2:
对象一般写法
student:
name: alice
age: 20
也可以写成行内形式
student: {name: alice,age: 20}
注::
后别忘了加一个空格
数组(List、set)
用-
表示数组中的每一个元素
pets:
- cat
- dog
- pig
也可以写成行内形式
pets: [cat,dog,pig]
3.3 注入配置文件
yaml可以为实体类赋值
- 在springboot项目中的resources目录下新建一个文件
application.yml
- 编写一个实体类Student
@Component //将bean注册到容器中
public class Student {
private String name;
private int age;
//constructor、getter、setter......
}
- 老方法:使用
@Value
注解给bean注入属性
@Component //将bean注册到容器中
public class Student {
@Value("alice")
private String name;
@Value("20")
private int age;
//constructor、getter、setter......
}
- 测试
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired //自动注入
Student student;
@Test
void contextLoads() {
System.out.println(student);
}
}
- 再写一个Person类
@Component
public class Person {
private String name;
private int age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Student student;
//constructor、getter、setter......
}
- 使用yaml写好配置内容
person:
name: mike
age: 40
happy: true
birth: 1970/1/1
maps: {key1: value1,key2: value2}
lists: [code,music,book]
student: {name: 张三,age: 18}
- 将yaml注入到类中
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private int age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Student student;
//constructor、getter、setter......
}
如果出现Spring Boot Configureation Annotation Processor not found in classpath
, 可以根据官方文档导入下面依赖
<!-- 导入配置文件处理器,配置文件进行绑定就会有提示,需要重启 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
为了防止中文编码失败,可以进行如下配置
- 测试
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired //自动注入
Person person;
@Test
void contextLoads() {
System.out.println(person);
}
}
3.4 加载指定的配置文件
@PropertySource
:加载指定的配置文件;
@ConfigurationProperties
:默认从全局配置文件中获取值;
- 再resources目录下新建
person.properties
文件
name=张三
- 在类中指定加载
person.properties
文件
@Component
//@ConfigurationProperties(prefix = "person")
@PropertySource(value="classpath:person.properties")
public class Person {
@Value("${name}")
private String name;
//other
//constructor、getter、setter......
}
- 测试
3.5 配置文件占位符
yaml还可以生成随机数
person:
name: mike${random.uuid} #随机uuid
age: ${random.int} #随机int
happy: true
birth: 1970/1/1
maps: {key1: value1,key2: value2}
lists: [code,music,book]
student:
name: ${person.hello:other}_张三
age: 18
测试:
3.6 两种注入方式对比
@Configuration | @Value | |
功能 | 批量注入配置文件中的属性 | 一个个属性进行指定 |
松散绑定(松散语法) | √ | × |
SpEL | × | √ |
JSR303数据校验 | √ | × |
复杂类型封装 | √ | × |
-
@ConfigurationProperties
只需要在类的头部写一次即可 , @Value则需要在类的每个属性上都添加 - 松散绑定。比如在yml中写一个last-name,这个属性名可以等价于lastName,以此增大了属性匹配范围
- JSR303数据校验。类似于过滤器验证,保证数据的合法性,比如可以通过
@Email()
属性来判断属性值是否符合邮箱格式
小结:
- 配置yml和配置properties都可以获取到值 ,最好使用yml
- 如果在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @Value
- 如果专门编写了一个JavaBean来和配置文件进行一一映射,就用`@configurationProperties
3.7 JSR303数据校验(*)
Springboot中可以用@validated
来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。这里来写个注解让的name只能支持Email格式
@Component
@ConfigurationProperties(prefix = "person")
@Validated //数据校验
public class Person {
@Email(message="邮箱格式错误") //判定是否符合邮箱格式
private String name;
//......
//constructor、getter、setter......
}
常见参数
空检查
@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.
Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
......
多环境切换
编写主配置文件时,可以将文件名固定为application-{title}.properties/yml
,用来指定多个环境版本。
比如:
- 可以用
application-test.properties
代表测试环境配置 - 可以用
application-dev.properties
代表开发环境配置
SpringBoot默认使用application.properties
主配置文件
通过下面配置来选择要激活的环境
#比如在配置文件中指定使用dev环境,我们可以通过设置不同的端口号进行测试;
#启动SpringBoot,就可以看到已经切换到dev下的配置了;
spring.profiles.active=dev
yaml的多文档块
使用yml可以不需要创建多个文件,文件内部通过---
划分配置内容
server:
port: 8081
#选择要激活那个环境块
spring:
profiles:
active: prod
---
server:
port: 8083
spring:
profiles: dev #配置环境的名称
---
server:
port: 8084
spring:
profiles: prod #配置环境的名称
注:如果yml和properties都配置了端口,并且没有激活其他环境,会默认使用properties配置文件。
配置文件加载位置
springboot 启动会扫描以下位置的application.properties/yml文件作为Spring boot的默认配置文件。
优先级1(最优先):
file:./config/
项目路径下的config文件夹配置文件
优先级2:
file:./
项目路径下配置文件
优先级3:
classpath:/config/
资源路径下的config文件夹配置文件
优先级4:
classpath:/
资源路径下配置文件
SpringBoot会从这四个位置全部加载主配置文件,互补配置。
4、自动配置原理
官方配置的详细介绍https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties
以HttpEncodingAutoConfiguration(Http编码自动配置)
为例,查看它的源码
//表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件
@Configuration(
proxyBeanMethods = false
)
/* 启动指定类的ConfigurationProperties功能:
进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来,
并把HttpProperties加入到ioc容器中
*/
@EnableConfigurationProperties({ServerProperties.class})
/* Spring底层@Condition注解
根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会失效。
这里会判定当前的应用是不是web应用,如果时,当前配置类生效
*/
@ConditionalOnWebApplication(
type = Type.SERVLET
)
//判断当前项目有没有这个类CharacterEncodingFilter,即SpringMVC中解决乱码的过滤器
@ConditionalOnClass({CharacterEncodingFilter.class})
/* 判断配置文件中是否存在某个配置:spring.http.encoding.enabled
如果不存在,判断也是成立的
即使配置文件中不配置spring.http.encoding.enabled=true,也是默认生效的
*/
@ConditionalOnProperty(
prefix = "server.servlet.encoding",
value = {"enabled"},
matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
// 和SpringBoot中的配置文件形成映射
private final Encoding properties;
// 只有一个有参构造器的情况下,参数值会从容器中拿
public HttpEncodingAutoConfiguration(ServerProperties properties) {
this.properties = properties.getServlet().getEncoding();
}
//给容器中添加一个组件,这个组件的某些值需要从properties中获取
@Bean
@ConditionalOnMissingBean //判断容器中有没有这个组件
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE));
return filter;
}
//......
}
小结:根据当前不同的条件判断,决定这个配置类是否生效。
- 一但这个配置类生效,这个配置类就会给容器中添加各种组件
- 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性和配置文件绑定
- 所有在配置文件中能配置的属性都封装在
xxxxProperties
类中(上面的例子是ServerProperties
) - 配置文件能配置什么就可以参照某个功能对应的这个属性类
@ConfigurationProperties(
prefix = "server",
ignoreUnknownFields = true
)
public class ServerProperties {
private Integer port;
private InetAddress address;
//......
}
- SpringBoot启动会加载大量的自动配置类
- 根据需求,查看相关的功能有没有在SpringBoot默认写好的自动配置类当中
- 然后再看这个自动配置类中到底配置了哪些组件
- 给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。只需要在配置文件中指定这些属性的值即可。
xxxxAutoConfigurartion
:自动配置类,给容器中添加组件
xxxxProperties
:封装配置文件中相关属性
@Conditional(*)
作用:只有@Conditional
指定的条件成立时,才给容器中添加组件,相关配置内容才会生效
@Conditional扩展注解 | 作用(判断是否满足当前指定的条件) |
@ConditionalOnJava | 系统的java版本是否符合要求 |
@ConditionalOnBean | 容器中存在指定的Bean |
@ConditionalOnMissingBean | 容器中不存在指定的Bean |
@ConditionalOnExpression | 满足SpEL表达式指定 |
@ConditionalOnClass | 系统中有指定的类 |
@ConditionalOnMissingClass | 系统中没有指定的类 |
@ConditionalOnSingleCandidate | 容器中只有一个指定的Bean,或者这个Bean是首选的Bean |
@ConditionalOnProperty | 系统中指定的属性是否有指定的值 |
@ConditionalOnResource | 类路径下是否存在指定的资源文件 |
@ConditionalOnWebApplication | 当前是web环境 |
@ConditionalOnNotWebApplication | 当前不是web环境 |
@ConditionalOnJndi | JNDI存在指定项 |
通过启用debug=true
属性,可以让控制台打印自动配置报告,这样可以更容易知道哪些自动配置类有效。
#开启springboot的调试类
debug=true
Positive matches
:(自动配置类启用的:正匹配)
Negative matches
:(没有启动,没有匹配成功的自动配置类:负匹配)
Unconditional classes
: (没有条件的类)
5、自定义Starter
启动器模块是一个空的jar文件,仅提供辅助性依赖管理,这些依赖可能用于自动装配或者其他类库。
命名约定:
官方命名:
- 前缀:spring-boot-starter-xxx(如spring-boot-starter-web)
自定义命名:
- xxx-spring-boot-starter(如mybatis-spring-boot-starter)
5.1 编写启动器
- 新建空项目
spring-boot-starter-diy
- 新建一个普通Maven模块:
infinite-spring-boot-starter
- 新建一个Springboot模块:
infinite-spring-boot-starter-autoconfigure
- 在starter中导入autoconfig的依赖
- 删除autoconfigure项目下多余的文件,pom中只留下starter依赖,这是所有启动器的基本配置
- 在
starter
模块中编写一个自己的服务
package com.infinite;
public class HelloService {
HelloProperties helloProperties;
public HelloProperties getHelloProperties() {
return helloProperties;
}
public void setHelloProperties(HelloProperties helloProperties) {
this.helloProperties = helloProperties;
}
public String sayHello(String name){
return helloProperties.getPrefix() + name + helloProperties.getSuffix();
}
}
- 同级目录下编写
HelloProperties
配置类
@ConfigurationProperties(prefix="infinite.hello")
public class HelloProperties {
private String prefix;
private String suffix;
public String getPrefix(){
return prefix;
}
public void setPrefix(String prefix){
this.prefix=prefix;
}
public String getSuffix(){
return suffix;
}
public void setSuffix(String suffix){
this.suffix=suffix;
}
}
- 同级目录下编写自动配置类并注入bean,测试
@Configuration
@ConditionalOnWebApplication //使web应用生效
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {
@Autowired
HelloProperties helloProperties;
@Bean
public HelloService helloService(){
HelloService helloService = new HelloService();
helloService.setHelloProperties(helloProperties);
return helloService;
}
}
- 在
resources
中编写一个自己的META-INF/spring.factories
文件
#Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.infinite.HelloServiceAutoConfiguration
- 编写完成后,安装到maven仓库中
5.2 测试自己写的启动器
- 新建一个SpringBoot项目(web)
- 导入启动器
<dependency>
<groupId>org.example</groupId>
<artifactId>infinite-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
- 编写一个
HelloController
类测试自己写的接口
package com.infinite.controller;
@RestController
public class HelloController {
@Autowired
HelloService helloService;
@RequestMapping("/hello")
public String hello(){
return helloService.sayHello("zxc");
}
}
- 编写配置文件
application.properties
infinite.hello.prefix="xxxx"
infinite.hello.suffix="yyyy"
- 启动项目进行测试
6、SpringBoot Web开发
web开发的一些需求:
- 导入静态资源
- 首页(Index)
- jsp、模板引擎Thymeleaf
- 装配扩展SpringMVC
- CURD
- 国际化(如页面中英文切换)
6.1 导入静态资源
注:先搭建一个普通的springboot的web项目,并测试。
6.1.1 WebMvcAutoConfiguration类
以前写web项目时,会将所有的页面导入到webapp目录下。
现在的pom以jar为打包方式,SpringBoot可以依靠这种方式写页面但是SpringBoot对于静态资源放置的位置有所规定。
静态资源映射规则:SpringBoot中,SpringMVC的web配置都写在WebMvcAutoConfiguration
类中,打开它的源码,有几个相关的方法(springboot版本不同,源码也可能有区别)
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
// 警用默认资源处理
logger.debug("Default resource handling disabled");
} else {
// 配置webjars
this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
//静态资源配置
this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
registration.addResourceLocations(new Resource[]{resource});
}
});
}
}
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
this.addResourceHandler(registry, pattern, (registration) -> {
registration.addResourceLocations(locations);
});
}
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, Consumer<ResourceHandlerRegistration> customizer) {
if (!registry.hasMappingForPattern(pattern)) {
ResourceHandlerRegistration registration = registry.addResourceHandler(new String[]{pattern});
customizer.accept(registration);
//缓存控制
registration.setCachePeriod(this.getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.
setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
this.customizeResourceHandlerRegistration(registration);
}
}
第7行及相关方法的代码设置了webjars目录的寻找路径。这里代表所有形如/webjars/**
的路径,都会转为classpath:/META-INF/resources/webjars/
路径进行资源寻找。
6.1.2 webjars
Webjars本质就是以jar包的方式引入静态资源 ,通过pom映入相关资源的依赖即可引入需要的静态资源。
比如要使用jquery,可以去官网找到依赖并写进pom
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>
启动测试,根据图中resources下的路径找到jquery.js
文件。浏览器输入localhost:8080/webjars/jquery/3.6.0/dist/jquery.js
显示文件具体内容
再导入redis-fast-driver
,同理也可以查询相关的文件
6.1.3 导入自己的静态资源
WebMvcAAutoConfiguration
类中有一个方法getStaticLocations()
,它从属于WebProperties
类下的Resources
类,进入这个方法
public String[] getStaticLocations() {
return this.staticLocations;
}
这个staticLocations
,是当前所在的Resources
类的一个属性
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
};
private String[] staticLocations;
CLASSPATH_RESOURCE_LOCATIONS
存放了目前可识别的静态资源的路径,在resources(classpath)
目录下创建对应的路径,写入静态资源文件。启动测试后,浏览器输入localhost:8080/文件名
,会依次从上面的四个路径中寻找相关文件。文件重名情况下,按照路径查找的优先级,显示首次找到的文件。
6.1.4 自定义静态资源路径
可以通过application.properties
配置文件来指定哪些文件夹用于放静态资源文件
spring.web.resources.static-locations=classpath:/xxxyyy/,classpath:/infinite/
一旦自己定义了静态文件夹的路径,原来的自动配置会失效
6.1.5 首页处理
继续读WebMvcAutoConfiguration
类的源码,其中有一个与首页相关的映射
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
return welcomePageHandlerMapping;
}
进入getWelcomePage()
方法
private Resource getWelcomePage() {
String[] var1 = this.resourceProperties.getStaticLocations();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
String location = var1[var3];
Resource indexHtml = this.getIndexHtml(location);
if (indexHtml != null) {
return indexHtml;
}
}
ServletContext servletContext = this.getServletContext();
if (servletContext != null) {
return this.getIndexHtml((Resource)(new ServletContextResource(servletContext, "/")));
} else {
return null;
}
}
// 首页即index.html
private Resource getIndexHtml(String location) {
return this.getIndexHtml(this.resourceLoader.getResource(location));
}
private Resource getIndexHtml(Resource location) {
try {
Resource resource = location.createRelative("index.html");
if (resource.exists() && resource.getURL() != null) {
return resource;
}
} catch (Exception var3) {
}
return null;
}
静态目录下的所有index.html
都会由\**
映射。输入localhost/8080/
,默认查找index.html
默认状态下的查找优先级:
classpath:/META-INF/resources/
>classpath:/resources/
>classpath:/static/
>classpath:/public/
网站图标
Spring Boot在配置的静态内容位置中查找 favicon.ico。如果存在这样的文件,它将自动用作应用程序的favicon。
- 在静态资源目录下放置图标
- 清除浏览器缓存,刷新网页
6.2 Thymeleaf
模板引擎
之前用过的jsp也是一种模板引擎,模板引擎的主要目的是将写好的页面模板和后台数据进行整合后,生成前端显示的页面。
SpringBoot推荐使用Thymeleaf模板引擎
引入Thymeleaf
Thymeleaf官网:https://www.thymeleaf.org/
Thymeleaf的Github 主页:https://github.com/thymeleaf/thymeleaf
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
使用Thymeleaf
打开Thymeleaf的自动配置类:ThymeleafProperties
@ConfigurationProperties(
prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING;
//默认前缀
public static final String DEFAULT_PREFIX = "classpath:/templates/";
//默认后缀
public static final String DEFAULT_SUFFIX = ".html";
private boolean checkTemplate = true;
private boolean checkTemplateLocation = true;
private String prefix = "classpath:/templates/";
private String suffix = ".html";
private String mode = "HTML";
private Charset encoding;
//......
}
只需要把html页面放在类路径(classpath)下的templates目录下,thymeleaf就可以帮我们自动渲染页面
使用例
- 编写一个
TestController
package com.infinite.controller;
@Controller
public class TestController {
@RequestMapping("/t1")
public String test1(){
// classpath:/template/test.html
return "test";
}
}
- 编写一个测试页面
test.html
放在templates
目录下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>TEST1</h1>
</body>
</html>
- 测试
6.2.1 Thymeleaf语法
Thymeleaf 官网:https://www.thymeleaf.org/
查找数据
- 修改测试请求,增加数据传输
@Controller
public class TestController {
@GetMapping("/t1")
public String test(Model model){
//存入数据
model.addAttribute("msg","Hello,Thymeleaf");
//classpath:/templates/test.html
return "test";
}
}
- 使用Thymeleaf前,要在html文件中导入命名空间的约束,方便提示
<html lang="en" xmlns:th="http://www.thymeleaf.org">
- 前端页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>TEST</title>
</head>
<body>
<h1>测试页面</h1>
<!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样-->
<div th:text="${msg}"></div>
</body>
</html>
- 测试
基本语法
标签
html的标签元素前添加th:
,说明这个标签会由Thymeleaf管理。
表达式
Simple expressions:(表达式语法)
Variable Expressions: ${...}: 获取变量值;OGNL
1)、获取对象的属性、调用方法
2)、使用内置的基本对象:#18
#ctx : the context object.
#vars: the context variables.
#locale : the context locale.
#request : (only in Web Contexts) the HttpServletRequest object.
#response : (only in Web Contexts) the HttpServletResponse object.
#session : (only in Web Contexts) the HttpSession object.
#servletContext : (only in Web Contexts) the ServletContext object.
3)、内置的一些工具对象:
#execInfo : information about the template being processed.
#uris : methods for escaping parts of URLs/URIs
#conversions : methods for executing the configured conversion service (if any).
#dates : methods for java.util.Date objects: formatting, component extraction, etc.
#calendars : analogous to #dates , but for java.util.Calendar objects.
#numbers : methods for formatting numeric objects.
#strings : methods for String objects: contains, startsWith, prepending/appending, etc.
#objects : methods for objects in general.
#bools : methods for boolean evaluation.
#arrays : methods for arrays.
#lists : methods for lists.
#sets : methods for sets.
#maps : methods for maps.
#aggregates : methods for creating aggregates on arrays or collections.
==================================================================================
Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
Message Expressions: #{...}:获取国际化内容
Link URL Expressions: @{...}:定义URL;
Fragment Expressions: ~{...}:片段引用表达式
Literals(字面量)
Text literals: 'one text' , 'Another one!' ,…
Number literals: 0 , 34 , 3.0 , 12.3 ,…
Boolean literals: true , false
Null literal: null
Literal tokens: one , sometext , main ,…
Text operations:(文本操作)
String concatenation: +
Literal substitutions: |The name is ${name}|
Arithmetic operations:(数学运算)
Binary operators: + , - , * , / , %
Minus sign (unary operator): -
Boolean operations:(布尔运算)
Binary operators: and , or
Boolean negation (unary operator): ! , not
Comparisons and equality:(比较运算)
Comparators: > , < , >= , <= ( gt , lt , ge , le )
Equality operators: == , != ( eq , ne )
Conditional operators:条件运算(三元运算符)
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
Special tokens:
No-Operation: _
使用例
- 编写给
TestController
添加新方法
@RequestMapping("/t2")
public String test2(Map<String,Object> map){
//存入数据
map.put("msg","<h1>Hello</h1>");
map.put("users", Arrays.asList("alice","bob"));
//classpath:/template/test.html
return "test";
}
- 测试页面取出数据
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>TEST</title>
</head>
<body>
<h1>测试页面</h1>
<!--不转义-->
<div th:text="${msg}"></div>
<!--转义-->
<div th:utext="${msg}"></div>
<!--遍历数据-->
<!--th:each每次遍历都会生成当前这个标签:官网#9-->
<h4 th:each="user:${users}" th:text="${user}"></h4>
<h4>
<!--行内写法:官网#12-->
<span th:each="user:${users}">[[${user}]]</span>
</h4>
</body>
</html>
- 测试
6.3 MVC自动配置
注:多读源码和官方文档
官网文档:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration
6.3.1 官方介绍
原文
翻译
自动配置在Spring默认设置的基础上添加了以下功能:
- 包含视图解析器
ContentNegotiatingViewResolver
和BeanNameViewResolver
的bean - 支持静态资源的路径,以及webjars
- 自动注册
Converter
转换器(网页将数据传到后台自动封装成为对象的类,比如把"1"转换为int类型)、Formatter
格式化器(比如页面传来一个2022-2-2,它会自动格式化为Date对象) - 支持
HttpMessageConverters
,它在SpringMVC中用于转换Http请求和相应,如对象和json字符串的转换。 - 自动注册
MessageCodesResolver
,用于定义错误代码生成规则 - 支持首页定制
- 支持图标定制
- 自动初始化数据绑定器
ConfigurableWebBindingInitializer
如果你希望保留Spring Boot MVC功能,并且希望添加其他MVC配置(拦截器、格式化程序、视图控制器和其他功能),则可以添加自己的@configuration
类,类型为WebMvcConfiguer
,但不添加@EnableWebMvc
。如果希望提供RequestMappingHandlerMapping
、RequestMappingHandlerAdapter
或ExceptionHandlerExceptionResolver
的自定义实例,则可以声明WebMVCregistrationAdapter
实例来提供此类组件。
如果你想完全控制Spring MVC,可以添加自己的@Configuration
,并用@EnableWebMvc
进行注释。
6.3.2 ContentNegotiatingViewResolver(内容协商视图解析器)
探究源码
自动配置ViewResolver
,即之前学习SpringMVC时用到的视图解析器。
这个类可以根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发、重定向)
在WebMvcAutoConfiguration
中搜索这个类,有如下相关方法
@Bean
@ConditionalOnBean({ViewResolver.class})
@ConditionalOnMissingBean(
name = {"viewResolver"},
value = {ContentNegotiatingViewResolver.class}
)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver. setContentNegotiationManager((ContentNegotiationManager)beanFactory.getBean(ContentNegotiationManager.class));
resolver.setOrder(-2147483648);
return resolver;
}
进入这个类,找到对应的解析视图的代码
@Nullable //参数可以为null
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
if (requestedMediaTypes != null) {
//获取候选的视图对象
List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
//选择一个最合适的视图对象并返回
View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;
}
}
//......
}
上面代码第8行有一个getCandidateViews()
方法,它用于获取所有的视图解析器,并通过while循环逐个解析
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) throws Exception {
List<View> candidateViews = new ArrayList();
if (this.viewResolvers != null) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
Iterator var5 = this.viewResolvers.iterator();
//循环解析
while(var5.hasNext()) {
ViewResolver viewResolver = (ViewResolver)var5.next();
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
Iterator var8 = requestedMediaTypes.iterator();
while(var8.hasNext()) {
MediaType requestedMediaType = (MediaType)var8.next();
List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
Iterator var11 = extensions.iterator();
while(var11.hasNext()) {
String extension = (String)var11.next();
String viewNameWithExtension = viewName + '.' + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
if (view != null) {
candidateViews.add(view);
}
}
}
}
}
if (!CollectionUtils.isEmpty(this.defaultViews)) {
candidateViews.addAll(this.defaultViews);
}
return candidateViews;
}
综上可以大致了解到ContentNegotiatingViewResolver
组合所有的视图解析器的功能。
下面再了解一下它是怎么进行组合的。类内有一个属性viewResolvers
,它通过下面方法赋值
protected void initServletContext(ServletContext servletContext) {
//这里从beanFactory工具中获取容器中的所有视图解析器
// ViewRescolver.class 用于组合所有视图解析器
Collection<ViewResolver> matchingBeans =BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(),ViewResolver.class).values();
ViewResolver viewResolver;
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList(matchingBeans.size());
//......
}
//......
}
自定义视图解析器
可以自己给容器中添加一个视图解析器,ContentNegotiatingViewResolver
类就会进行自动组合
- 在主程序中写一个视图解析器
package com.infinite.config;
@Configuration
public class MyMvcConfig {
@Bean
public ViewResolver myViewResolver(){
return new MyViewResolver();
}
//写一个静态内部类
//视图解析器需要实现ViewResolver接口
private static class MyViewResolver implements ViewResolver{
@Override
public View resolveViewName(String s, Locale locale) throws Exception{
return null;
}
}
}
- debug追踪
进入DispatcherServlet
类中(所有相关的请求会经过这个类),对doDispatcher()
方法进行断点调试
debug
打开this.viewResolvers可以看到自己定义的视图解析器
若想自定义组件,通过@Configuration
注解就能进行组件添加。
6.3.3 修改SpringBoot的默认配置
SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(如用户自己配置@bean),如果有就用用户配置的,如果没有就自动配置。
如果有些组件可以存在多个,比如视图解析器,就将用户配置的和自己默认的组合起来。
扩展SpringMVC,首先要写一个@Configuration
注解类,并且要实现WebMvcConfigurer
接口,并且不能标注@EnableWebMvc
(源码中有相关定义)。
//config.MyMvcConfig
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 浏览器发送/test ,就会跳转到test页面;
registry.addViewController("/test").setViewName("test");
}
}
测试
这样就实现了SpringMVC的扩展,既保留了SpringBoot所有的自动配置,也能用我们扩展的配置。
原理
WebMvcAutoConfiguration
是 SpringMVC的自动配置类,里面有一个类WebMvcAutoConfigurationAdapter
。
@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
@EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class})
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {......}
这个类上有一个注解,在做其他自动配置是会导入:@Import(EnableWebMvcConfiguration.class)
。
点进EnableWebMvcConfiguration
,可以看到它继承了DelegatingWebMvcConfiguration
,进入这个父类
@Configuration(
proxyBeanMethods = false
)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
public DelegatingWebMvcConfiguration() {
}
@Autowired(
required = false
)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
//......
protected void addViewControllers(ViewControllerRegistry registry) {
this.configurers.addViewControllers(registry);
}
//......
}
进入addViewControllers()
方法
//class WebMvcConfigurerComposite implements WebMvcConfigurer
public void addViewControllers(ViewControllerRegistry registry) {
Iterator var2 = this.delegates.iterator();
while(var2.hasNext()) {
// 将所有的WebMvcConfigurer相关配置来一起调用。包括自己配置的和Spring给自动配置的
WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
delegate.addViewControllers(registry);
}
}
可见,所有的WebMvcConfiguration
都会被调用,包括自己配置的和Spring给自动配置的。
6.3.4 全面接管SpringMVC(*)
全面接管,即去除所有自动配置,完全由自己配置
只需在自定义的配置类中加一个@EnableWebMvc
就能实现。此时启动测试,首页将不能访问,因为自动配置全部失效了。
查看@EnableWebMvc
的源码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}
发现它导入了一个类,进入类DelegatingWebMvcConfiguration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
//......
}
这个类继承了WebMvcConfigurationSupport
回看WebMvcAutoConfiguration
的源码
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
// 容器中没有这个组件的时候,这个自动配置类才生效
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
//......
}
第10行的注解表明,容器中没有WebMvcConfigurationSupport
组件的时候,这个自动配置类才生效。而前面的@EnableWebMvc
恰好导入了这个组件,这就使得自动装配类无效。
6.4 小项目:员工管理系统
6.4.1 准备工作
前端模板:
将已有的前端模板添加到项目中
编写Employee
、Department
实体类,并实现相关的Dao层
Department
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
private Integer id;
private String departmentName;
}
Employee
@Data
@NoArgsConstructor
public class Employee {
private Integer id;
private String lastName;
private String email;
private Integer gender; //0:female, 1:male
private Department department;
private Date birth;
public Employee(Integer id, String lastName, String email, Integer gender, Department department) {
this.id = id;
this.lastName = lastName;
this.email = email;
this.gender = gender;
this.department = department;
//默认创建日期
this.birth = new Date();
}
}
DepartmentDao
//部门Dao
@Repository
public class DepartmentDao {
//模拟数据库中的数据
private static Map<Integer, Department> departments=null;
static{
departments=new HashMap<Integer,Department>();//创建一个部门表
departments.put(101,new Department(101,"教学部"));
departments.put(102,new Department(102,"市场部"));
departments.put(103,new Department(103,"教研部"));
departments.put(104,new Department(104,"运营部"));
departments.put(105,new Department(105,"后勤部"));
}
//获取所有部门的信息
public Collection<Department> getDepartments(){
return departments.values();
}
//通过id查询相关部门
public Department getDepartmentById(Integer id){
return departments.get(id);
}
}
EmployeeDao
@Repository
public class EmployeeDao {
//模拟数据库中的数据
private static Map<Integer, Employee> employees=null;
//员工所属的部门
@Autowired
private DepartmentDao departmentDao;
static{
employees=new HashMap<Integer,Employee>();//创建一个部门表
employees.put(1001,new Employee(1001,"Alice","1@qq.com",0,new Department(101,"教学部")));
employees.put(1002,new Employee(1002,"Bob","2@qq.com",1,new Department(102,"市场部")));
employees.put(1003,new Employee(1003,"Mike","3@qq.com",1,new Department(103,"教研部")));
employees.put(1004,new Employee(1004,"John","4@qq.com",1,new Department(104,"运营部")));
employees.put(1005,new Employee(1005,"Eve","5@qq.com",0,new Department(105,"后勤部")));
}
//主键自增
private static Integer initID=1006;
//增加一个员工
public void save(Employee employee){
if(employee.getId()==null){
employee.setId(initID++);
}
employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getId()));
employees.put(employee.getId(),employee);
}
//查询全部员工
public Collection<Employee> getAll(){
return employees.values();
}
//通过id查询员工
public Employee getEmployeeById(Integer id){
return employees.get(id);
}
//删除指定员工
public void delete(Integer id){
employees.remove(id);
}
}
6.4.2 实现首页
编写MyMvcConfig
的方法实现页面跳转
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
}
}
将index.html
中的相关标签改成thymeleaf的标签(其他静态文件可以顺便一起处理),如下例:
<!--html格式-->
<link href="/css/bootstrap.min.css" rel="stylesheet">
<!--thymeleaf格式-->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
注:thymeleaf的url内容需要写在@{}`中。
在application.properties
中设置关闭thymeleaf缓存
#关闭缓存以防样式不更新
spring.thymeleaf.cache=false
6.4.3 页面国际化
先确认idea当前为UTF-8编码
在resource
目录下创建i18n
目录,在该目录下创建login.properties
、login_en_US.properties
、login_zh_CN.properties
分别代表原页面内容、英文页面内容、中文页面内容
实现国际化配置的类是MessageSourceAutoConfiguration
、MessageSourceProperties
。在配置文件中设置相关属性
spring.messages.basename=i18n.login
修改html文件,在文本相关的标签中写入国际化属性,如
<!--例1-->
<!--原语句-->
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<!--修改后-->
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
<!--例2-->
<!--修改前-->
<input type="checkbox" value="remember-me"> Remember me
<!--修改后-->
<input type="checkbox" value="remember-me"> [[#{login.remember}]]
注:国际化配置需要#
进行引用。特定文本对应已经写好的特定的属性,比如要实现Please sign in
的国际化,需要先在i18n
中提前规定好它对应的属性(这里是login.tip
)并完善内容,然后再写到对应的标签中。
给上图的两个按钮对应的源码标签中写入传给后端的请求
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
与国际化切换相关的类是AcceptHeaferLocaleContextResolver
,可以对照它的源码,自定义一个config.MyLocalResolver
类,需要实现LocalResolver
接口
public class MyLocalResolver implements LocaleResolver {
//解析请求
@Override
public Locale resolveLocale(HttpServletRequest request) {
//获取请求中的语言参数
String language=request.getParameter("l");
//如果没有请求就使用默认语言
Locale locale=Locale.getDefault();
//如果请求的链接非空,就执行请求
if(!StringUtils.isEmpty(language)){
//比如language的值是"zh_CN",可以拆成语言文化代码和国家
String[] split=language.split("_");
locale=new Locale(split[0],split[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {}
}
在同目录下的MyMvcConfig
中注册MyLocalResolver
//自定义的国际化组件
@Bean
public LocaleResolver localeResolver(){
return new MyLocalResolver();
}
测试
小结:
- 配置i18n文件。并修改相关的html文件,使用
#{}
给出对应文本在i18n文件中的属性 - 自定义
LocaleResolver
组件实现语言切换 - 把组件注册到spring容器中
6.4.4 登录功能
基本功能
修改index.html
中的form标签内容
<form class="form-signin" th:action="@{/user/login}">
修改html
中用户名、密码输入框标签的名称
<input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
<input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">
创建controller.LoginController
类
@Controller
public class LoginController {
@RequestMapping("user/login")
public String login(
@RequestParam("username") String username,
@RequestParam("password") String password,
Model model){
if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
return "redirect:/main.html";
}
else{
//登录失败
model.addAttribute("msg","用户名或密码错误");
return "index";
}
}
}
为了避免登录后用户名和密码出现在url的值中,可以设置重定向功能,第7行的main.html
是一个请求,将它配置到MyMvcConfig
中实现页面跳转
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
registry.addViewController("/main.html").setViewName("dashboard");
}
controller.LoginController
中,若登录失败,则会返回一个msg给前端。在index.html
中加入一个标签用于登录失败时显示这个返回值
<!--如果msg的值为空,则不显示消息-->
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
测试
登录拦截器
创建一个config.LoginHandlerInterceptor
类
public class LoginHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//登录成功后,要获取用户的Session
Object loginUser = request.getSession().getAttribute("loginUser");
if(loginUser==null){ //假如还没有登录
request.setAttribute("msg","用户尚未登录");
request.getRequestDispatcher("/index.html").forward(request,response);
return false;
}
else{
return true;
}
}
}
在MyMvcConfig
中注册
//拦截器设置
@Override
public void addInterceptors(InterceptorRegistry registry) {
//第二个方法是选择拦截的路径,第三个方法是不拦截提及的路径
registry.addInterceptor(new LoginHandlerInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/index.html","/","/user/login","/css/**","/js/**","/img/**");
}
将dashboard.html
中的Company name替换为登录用户的用户名(用Ctrl+F快速查找,替换成[[${session.loginUser}]]
)
<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>
这里的session需要在LoginController
中添加
@Controller
public class LoginController {
@RequestMapping("user/login")
public String login(
@RequestParam("username") String username,
@RequestParam("password") String password,
Model model, HttpSession session){
if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
// 将用户名加入session,前端可以通过session获取用户名
session.setAttribute("loginUser",username);
return "redirect:/main.html";
}
else{
//登录失败
model.addAttribute("msg","用户名或密码错误");
return "index";
}
}
}
6.4.5 展示员工列表(CRUD)
提取重复页面
将dashaboard.html
和list.html
(这里新建一个emp目录,将list.html
移进该目录)中Customer文本对应的列表框替换为如下内容
<li class="nav-item">
<a class="nav-link" th:href="@{/emps}">
员工管理
</a>
</li>
为了实现/emps
请求,需要创建一个controller.EmployeeController
类
@Controller
public class EmployeeController {
@Autowired
EmployeeDao employeeDao;
@RequestMapping("/emps")
public String list(Model model){
Collection<Employee> employees = employeeDao.getAll();
model.addAttribute("emps",employees);
return "emp/list";
}
}
接下来要将dashaboard.html
中的导航栏与侧边栏的代码通过thymeleaf的th:fragment
功能在list.html
中进行复用。
导航栏
在dashaboard.html
查找并更改相关标签
<!--更改前-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0">
<!--更改后-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
list.html
中会有一段完全相同的导航栏代码,删除并用以下代码实现复用。引用需要写在~{}
中
<div th:insert="~{dashboard::topbar}"></div>
侧边栏
在dashaboard.html
查找并更改相关标签
<!--更改前-->
<nav class="col-md-2 d-none d-md-block bg-light sidebar">
<!--更改后-->
<nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">
list.html
中会有一段完全相同的侧边栏代码,删除并用以下代码实现复用
<div th:insert="~{dashboard::sidebar}"></div>
为了更好地实现复用,可以创建一个common/common.html
文件,用于专门存放导航栏和侧边栏等需要复用的代码,设置对应的th:fragment
,其他html文件需要用到时通过<div th:insert="~{路径::引用名}"></div>"
引入即可。
导航栏和侧边栏的显示效果没变,主要是复用后代码量会减少。
侧边栏选中时高亮
将dashboard.html
中的侧边栏引用改为如下格式
<div th:replace="~{commons/commons::sidebar(active='main.html')}"></div>
active
用于标记当前侧边栏选中了哪个选项,选中的项需要高亮表示
将common.html
中带有a class="nav-link"
的标签进行如下修改
<!--修改前-->
<a class="nav-link active" th:href="@{/index.html}">
<!--修改后-->
<a th:class="${active=='main.html'?'nav-link active':'nav-link'}" th:href="@{/index.html}">
对员工管理
那一项做同样处理
<a th:class="${active=='list.html'?'nav-link active':'nav-link'}" th:href="@{/emps}">
修改list.html
中对侧边栏的引用
<div th:replace="~{commons/commons::sidebar(active='list.html')}"></div>
测试
查
接下来要将上图中表的内容替换为之前已经写在dao层的数据
进入list.html
,在之前写进侧边栏引用的下方,已经写好了表的呈现形式,将这块代码修改为
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<h2>Section title</h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>id</th>
<th>lastName</th>
<th>email</th>
<th>gender</th>
<th>department</th>
<th>birth</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<!--emps来自于EmployeeController类-->
<tr th:each="emp:${emps}">
<td th:text="${emp.getId()}"></td>
<td>[[${emp.getLastName()}]]</td>
<td th:text="${emp.getEmail()}"></td>
<td th:text="${emp.getGender()==0?'女':'男'}"></td>
<td th:text="${emp.department.getDepartmentName()}"></td>
<td th:text="${emp.getBirth()}"></td>
<td>
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</main>
测试
增
在list.html
中,将<h2>Section title</h2>
替换成
<h2><a class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a> </h2>
为了实现/emp
请求,需要在EmployeeController
中添加相关方法
@Autowired
DepartmentDao departmentDao;
@RequestMapping("/emp")
public String toAddPage(Model model){
//查询所有部门的信息
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments",departments);
return "emp/add";
}
复制一份list.html
到同级目录下,命名为add.html
,将其中表单部分替换为
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<form th:action="@{/emp}" method="post">
<div class="form-group">
<label>LastName</label>
<input type="text" name="lastName" class="form-control" placeholder="Tim" required>
</div>
<div class="form-group">
<label>Email</label>
<input type="email" name="email" class="form-control" placeholder="123@qq.com" required>
</div>
<div class="form-group">
<label>Gender</label>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="1" checked="checked">
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="0">
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>Department</label>
<select class="form-control" name="department.id" required>
<option th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option>
</select>
</div>
<div class="form-group">
<label>birth</label>
<input type="text" name="birth" class="form-control" placeholder="1970-1-1">
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
</main>
测试
下面要实现添加功能
在add.html
表单代码块中,设置整个表单的提交方式
<form th:action="@{/emp}" method="post">
在EmployeeController
中添加相关的方法
@PostMapping("/emp")
public String addEmp(Employee employee){
//添加一条记录
employeeDao.save(employee);
return "redirect:/emps";
}
测试
改
思路和增相同
在list.html
中,修改先前写好的"编辑"按钮
<button class="btn btn-sm btn-primary" th:href="@{/upd/{id}(id=${emp.getId()})}">编辑</button>
为了实现上述的Restful风格请求,需要在EmployeeController
中添加相关方法
//跳转到员工的修改页面
@GetMapping("/emp/{id}")
public String toUpdateEmp(@PathVariable("id")Integer id,Model model){
//查出原来的数据
Employee employee = employeeDao.getEmployeeById(id);
model.addAttribute("emp",employee);
//查出所有部门的信息
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments",departments);
return "emp/update";
}
复制add.html
到同级目录下,命名为update.html
,将表单代码修改为
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<form th:action="@{/updateEmp}" method="post">
<!--设置id的输入。若不写则会被dao层判断为新记录-->
<input type="hidden" name="id" th:values="${emp.getId()}">
<div class="form-group">
<label>LastName</label>
<input th:text="${emp.getLastName()}" type="text" name="lastName" class="form-control" placeholder="Tim" required>
</div>
<div class="form-group">
<label>Email</label>
<input th:text="${emp.getEmail()}" type="email" name="email" class="form-control" placeholder="123@qq.com" required>
</div>
<div class="form-group">
<label>Gender</label>
<div class="form-check form-check-inline">
<input th:checked="${emp.getGender()==1}" class="form-check-input" type="radio" name="gender" value="1">
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input th:checked="${emp.getGender()==0}" class="form-check-input" type="radio" name="gender" value="0">
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>Department</label>
<select class="form-control" name="department.id" required>
<option th:selected="${dept.getId()==emp.getDepartment().getId()}" th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option>
</select>
</div>
<div class="form-group">
<label>birth</label>
<input th:value="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm')}" type="text" name="birth" class="form-control" placeholder="1970-1-1">
</div>
<button type="submit" class="btn btn-primary">修改</button>
</form>
</main>
为了实现上面代码中的/updateEmp
请求,需要在EmployeeController
中添加相关方法
//实现修改功能
@PostMapping("/updateEmp")
public String updateEmp(Employee employee){
employeeDao.save(employee);
return "redirect:/emps";
}
测试效果跟增差不多
删
进入list.html
,修改删除标签
<a class="btn btn-sm btn-danger" th:href="@{/del/{id}(id=${emp.getId()})">删除</a>
在EmployeeController
中添加相关方法
//删除员工
@GetMapping("/del/{id}")
public String deleteEmp(@PathVariable("id")Integer id){
employeeDao.delete(id);
return "redirect:/emps";
}
6.4.6 404处理
在templates
目录中新建error
目录,将404.html
移入该目录,出现404时会直接跳到这个页面。其他错误码同理。
6.4.7 注销功能
进入commons.html
页面,修改注销按钮
<a class="nav-link" th:href="@{/user/logout}">注销</a>
在LoginController
中添加对应方法
@RequestMapping("/user.logout")
public String logout(HttpSession session){
session.invalidate();
//返回登录页面
return "redirect:/index.html";
}
html到同级目录下,命名为
update.html`,将表单代码修改为
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<form th:action="@{/updateEmp}" method="post">
<!--设置id的输入。若不写则会被dao层判断为新记录-->
<input type="hidden" name="id" th:values="${emp.getId()}">
<div class="form-group">
<label>LastName</label>
<input th:text="${emp.getLastName()}" type="text" name="lastName" class="form-control" placeholder="Tim" required>
</div>
<div class="form-group">
<label>Email</label>
<input th:text="${emp.getEmail()}" type="email" name="email" class="form-control" placeholder="123@qq.com" required>
</div>
<div class="form-group">
<label>Gender</label>
<div class="form-check form-check-inline">
<input th:checked="${emp.getGender()==1}" class="form-check-input" type="radio" name="gender" value="1">
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input th:checked="${emp.getGender()==0}" class="form-check-input" type="radio" name="gender" value="0">
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>Department</label>
<select class="form-control" name="department.id" required>
<option th:selected="${dept.getId()==emp.getDepartment().getId()}" th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option>
</select>
</div>
<div class="form-group">
<label>birth</label>
<input th:value="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm')}" type="text" name="birth" class="form-control" placeholder="1970-1-1">
</div>
<button type="submit" class="btn btn-primary">修改</button>
</form>
</main>
为了实现上面代码中的/updateEmp
请求,需要在EmployeeController
中添加相关方法
//实现修改功能
@PostMapping("/updateEmp")
public String updateEmp(Employee employee){
employeeDao.save(employee);
return "redirect:/emps";
}
测试效果跟增差不多
删
进入list.html
,修改删除标签
<a class="btn btn-sm btn-danger" th:href="@{/del/{id}(id=${emp.getId()})">删除</a>
在EmployeeController
中添加相关方法
//删除员工
@GetMapping("/del/{id}")
public String deleteEmp(@PathVariable("id")Integer id){
employeeDao.delete(id);
return "redirect:/emps";
}
6.4.6 404处理
在templates
目录中新建error
目录,将404.html
移入该目录,出现404时会直接跳到这个页面。其他错误码同理。
6.4.7 注销功能
进入commons.html
页面,修改注销按钮
<a class="nav-link" th:href="@{/user/logout}">注销</a>
在LoginController
中添加对应方法
@RequestMapping("/user.logout")
public String logout(HttpSession session){
session.invalidate();
//返回登录页面
return "redirect:/index.html";
}