Spring是一个开源框架,2003 年兴起的一个轻量级的Java 开发框架,作者:Rod Johnson。
Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。
学过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 Boot应用,并且实现一个简单的Http请求处理。通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。
环境准备:
开发工具:
Spring官方提供了非常方便的工具让我们快速构建应用
Spring Initializr:https://start.spring.io/
打开 https://start.spring.io/
填写项目信息
点击”Generate Project“按钮生成项目;下载此项目
解压项目包,并用IDEA以Maven项目导入,一路下一步即可,直到项目导入完毕。
如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。
创建一个新项目
选择spring initalizr, 默认是使用官网的快速构建工具实现
填写项目信息
选择初始化的组件(初学勾选 Web 即可)
填写项目路径
等待项目构建成功
通过上面步骤完成了基础项目的创建。就会自动生成以下文件:
程序的主启动类
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
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.5.2version>
<relativePath/>
parent>
<groupId>com.cwlingroupId>
<artifactId>SpringBoot-01-HelloWorldartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>SpringBoot-01-HelloWorldname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>11java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<optional>trueoptional>
<scope>runtimescope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<fork>truefork>
<addResources>trueaddResources>
configuration>
plugin>
plugins>
build>
project>
spring-boot-starter-parent
的依赖管理,控制版本与打包等内容spring-boot-starter-web
用于实现HTTP接口(该依赖中包含了Spring MVC),官网对它的描述是:使用Spring MVC构建Web(包括RESTful)应用程序的入门者,使用Tomcat作为默认嵌入式容器;spring-boot-starter-test
用于编写单元测试的依赖包。更多功能模块的使用我们将在后面逐步展开。spring-boot-maven-plugin
,配合 spring-boot-starter-parent
就可以把Spring Boot应用打包成JAR来直接运行;配合 spring-boot-devtools
实现热部署。设置 application.properties
#配置项目热部署
spring.devtools.restart.enabled=true
在idea中设置自动编译
SpringBoot项目的热部署功能设置完成!!!
在主程序的同级目录下,新建一个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
点击 maven的 package
如果遇到错误,可以配置打包时跳过项目运行测试用例
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-surefire-pluginartifactId>
<configuration>
<skipTests>trueskipTests>
configuration>
plugin>
如果打包成功,则会在target目录下生成一个 jar 包;打成了jar包后,就可以在任何地方运行了!
如何更改启动时显示的字符拼成的字母,SpringBoot呢?也就是 banner 图案;
只需一步:到项目下的 resources 目录下新建一个banner.txt 即可。
图案可以到:https://www.bootschool.net/ascii 这个网站生成,然后拷贝到文件中即可!
SpringBoot这么简单的东西背后一定有故事,我们之后去进行一波源码分析!
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.5.2version>
<relativePath/>
parent>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.5.2version>
parent>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
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);
}
}
//主启动类的注解,一个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) })
@SpringBootApplication
@SpringBootConfiguration //表明是一个SpringBoot配置文件
@Configuration //再次说明这是一个Spring配置类
@Component //说明这也是一个Spring组件
@EnableAutoConfiguration //自动配置,自动导入包
@AutoConfigurationPackage //自动配置包
@Import(AutoConfigurationPackages.Registrar.class) //自动配置“包注册”
@Import(AutoConfigurationImportSelector.class) //自动配置导入选择,选择了什么东西
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class) ,
getCandidateConfigurations
方法:获得候选的配置List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//这里的getSpringFactoriesLoaderFactoryClass()方法
//返回的就是我们最开始看的启动自动导入配置文件的注解类:EnableAutoConfiguration
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
getCandidateConfigurations
方法中找到SpringFactoriesLoader.loadFactoryNames
,这是SpringFactoriesLoader类的静态方法SpringFactoriesLoader
类中的预定义的自动装配路径 FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
//这里它又调用了 loadSpringFactories 方法
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
loadFactoryNames
方法中,点进去找到loadSpringFactories
方法private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
//获得classLoader , 我们返回可以看到这里得到的就是EnableAutoConfiguration标注的类本身
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
//去获取一个资源 "META-INF/spring.factories"
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
//将读取到的资源遍历,封装成为一个Properties
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
spring.factories
,就是预定好的加载配置文件自动配置真正实现是从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中需要填入= 它指定的前缀+方法spring.properties
文件
spring-boot-autoConfigure.jar
包下的的 META-INF/spring.factories
中获取 EnableAutoConfiguration
属性的值加载自动配置类XXXProperties
类
@ConditionalXXX
注解决定加载哪些组件
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);
}
}
SpringApplication类主要做了以下四件事情:
public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
// ......
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.setInitializers(this.getSpringFactoriesInstances();
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = this.deduceMainApplicationClass();
}
SpringBoot使用一个全局的配置文件,配置文件名称是固定的:
application.properties
application.yml
**配置文件的作用 :**修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们自动配置好了
比如我们可以在配置文件中修改Tomcat 默认启动的端口号:server.port=8081
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
字面值:普通字符串、数值、布尔类型,直接写成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]
在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
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
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);
}
}
查看控制台显示(结果略)
@ConfigurationProperties(prefix = “person”):默认是从全局配置文件中获取值
@PropertySource:加载指定的配置文件
name=cwlin
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
//加载指定的配置文件
@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;
}
@ConfigurationProperties | @Value | |
---|---|---|
功能 | 批量注入配置文件中的属性 | 一个个指定 |
松散绑定(松散语法) | 支持 | 不支持 |
SpEL | 不支持 | 支持 |
JSR303数据校验 | 支持 | 不支持 |
复杂类型封装 | 支持 | 不支持 |
一种数据校验格式,Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理
在pom.xml文件中导入依赖
<dependency>
<groupId>javax.validationgroupId>
<artifactId>validation-apiartifactId>
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 除上述校验规则外,我们还可以自定义一些数据校验规则
profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境
我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本,例如:
但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置文件;我们需要通过一个配置来选择需要激活的环境:
# 比如在配置文件中指定使用dev环境,我们可以通过设置不同的端口号进行测试;
# 我们启动SpringBoot,就可以看到已经切换到dev下的配置了;
spring.profiles.active=dev
和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配置文件的!
可以通过spring.config.location来改变默认的配置文件位置
项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高
java -jar spring-boot-config.jar --spring.config.location=F:/application.properties
//
// 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;
}
}
}
一句话总结 :根据当前不同的条件判断,决定这个配置类是否生效!
// 从配置文件中获取指定的值和bean的属性进行绑定
@ConfigurationProperties(
prefix = "server",
ignoreUnknownFields = true
)
public class ServerProperties {
// ......
}
自动装配原理的精髓
注意:自动配置类必须在一定的条件下才能生效
作用:必须是 @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:(正匹配:自动配置类,已经启用的并且生效的)
-----------------
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:(没有条件的类)
// ......
其实SpringBoot的东西用起来非常简单,因为SpringBoot最大的特点就是自动装配。
使用SpringBoot的步骤:
创建一个SpringBoot应用,选择我们需要的模块,SpringBoot就会默认将所需要的模块自动配置好
手动在配置文件中配置部分配置项目就可以运行起来了
专注编写业务代码,不需要考虑以前那样一大堆的配置了
自动配置:比如SpringBoot到底帮我们配置了什么?我们能不能修改?我们能修改哪些配置?我们能不能扩展?
没事就找找类,看看自动装配原理!
导入静态资源
首页和图标定制
jsp、模板引擎Thymeleaf(thymeleaf依赖)
装配扩展SpringMVC
增删改查
拦截器
国际化
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);
}
}
Webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。
使用SpringBoot需要使用Webjars,网站:https://www.webjars.org
要使用jQuery,只需要引入jQuery对应版本的pom依赖即可
<dependency>
<groupId>org.webjarsgroupId>
<artifactId>jqueryartifactId>
<version>3.6.0version>
dependency>
导入完毕,查看webjars目录结构,并访问Jquery.js文件!
访问:http://localhost:8080/webjars/jquery/3.6.0/jquery.js(只要是静态资源,SpringBoot就会去对应的路径寻找资源)
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
spring.resources.static-locations=classpath:/coding/,classpath:/cwlin/
WebMvcAutoConfigurationAdapter - 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");
}
Title
Welcome Page
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";
}
}
#关闭默认图标
spring.mvc.favicon.enabled=false
<head>
<meta charset="UTF-8">
<title>登录title>
<link rel="shortcut icon" th:href="@{/favicon.ico}"/>
<link rel="bookmark" th:href="@{/favicon.ico}"/>
head>
<head>
<meta charset="UTF-8">
<title>登录title>
<link rel="shortcut icon" href="/favicon.ico"/>
<link rel="bookmark" href="/favicon.ico"/>
head>
对于springboot来说,什么事情不都是一个start的事情。查看下面的三个网址:
All official starters follow a similar naming pattern; spring-boot-starter-*
, where *
is a particular type of application. This naming structure is intended to help when you need to find a starter. The Maven integration in many IDEs lets you search dependencies by name. For example, with the appropriate Eclipse or Spring Tools plugin installed, you can press ctrl-space
in the POM editor and type “spring-boot-starter” for a complete list.
引入对应的pom依赖,可以适当点进源码看看
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
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 目录下
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>测试页面h1>
body>
html>
Rerun项目,请求测试页:http://localhost:8080/test
按照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就可以帮我们自动渲染了。
参考 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"
我们去编写下前端页面
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>测试页面h1>
<h2 th:text="${msg}">h2>
body>
html>
启动测试:http://localhost:8080/test
可以使用任意的 th:attr 来替换html中原生属性的值!
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}:
Name: Sebastian.
Surname: Pepper.
Nationality: Saturn.
Message Expressions: #{...}:获取国际化内容
Link URL Expressions: @{...}:定义URL;
@{/order/process(execId=${execId},execType='FAST')}
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: _ //分割表达式
我们编写一个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","hello, Thymeleaf!"
);
model.addAttribute("users", Arrays.asList("cwlin","coder_lcw"));
//classpath:/templates/test.html
return "test1";
}
}
测试页面取出数据
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>测试页面h1>
<h2 th:text="${msg}">h2>
<h2 th:utext="${msg}">h2>
<hr>
<h3 th:each="user: ${users}" th:text="${user}">h3>
<h3>
<span th:each="user:${users}"> [[${user}]] span>
h3>
body>
html>
启动项目测试:http://localhost:8080/test1
在进行项目编写前,我们需要SpringBoot对SpringMVC做了哪些配置,包括如何扩展,如何定制
// Spring Boot为Spring MVC提供了自动配置,它可以很好地与大多数应用程序一起工作。
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.
// 自动配置在Spring默认设置的基础上添加了以下功能:
The auto-configuration adds the following features on top of Spring’s defaults:
// 包含视图解析器
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
// 支持静态资源文件夹的路径,以及webjars
Support for serving static resources, including support for WebJars
// 自动注册了Converter:
// 转换器,这就是我们网页提交数据到后台自动封装成为对象的东西,比如把"1"字符串自动转换为int类型
// Formatter:【格式化器,比如页面给我们了一个2019-8-10,它会给我们自动格式化为Date对象】
Automatic registration of Converter, GenericConverter, and Formatter beans.
// HttpMessageConverters
// SpringMVC用来转换Http请求和响应的的,比如我们要把一个User对象转换为JSON字符串,可以去看官网文档解释;
Support for HttpMessageConverters (covered later in this document).
// 定义错误代码生成规则的
Automatic registration of MessageCodesResolver (covered later in this document).
// 首页定制
Static index.html support.
// 图标定制
Custom Favicon support (covered later in this document).
// 初始化数据绑定器:帮我们把请求数据绑定到JavaBean中!
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
/*
如果您希望保留Spring Boot MVC功能,并且希望添加其他MVC配置(拦截器、格式化程序、视图控制器和其他功能),则可以添加自己的@configuration类,类型为webmvcconfiguer,但不添加@EnableWebMvc。
如果希望提供 RequestMappingHandlerMapping、RequestMappingHandlerAdapter或ExceptionHandlerExceptionResolver 的自定义实例,并且保留Spring Boot MVC功能,则可以声明WebMVCregistrationAdapter实例来提供此类组件。
如果您想完全控制Spring MVC,可以添加自己的@Configuration,并用@EnableWebMvc进行注释;或者如@EnableWebMvc的Javadoc所述,添加自己的@Configuration注释@DelegatingWebMvcConfiguration
*/
If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.
If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.
自动配置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信息
具体的操作说明如下图所示:
双击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
官方文档如下:
新建一个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);
}
}
结论:
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
,这个注解的意思是:容器中没有这个组件的时候,这个自动配置类(WebMvcAutoConfiguration)才生效;创建 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","[email protected]",1,new Department(101,"计财处")));
employees.put(1002,new Employee(1002,"BB","[email protected]",0,new Department(102,"科技处")));
employees.put(1003,new Employee(1003,"CC","[email protected]",0,new Department(103,"设备处")));
employees.put(1004,new Employee(1004,"DD","[email protected]",1,new Department(104,"教务处")));
employees.put(1005,new Employee(1005,"EE","[email protected]",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);
}
}
建议使用扩展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
<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 Bootstraptitle>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<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
在 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}
表示首页标题
<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>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<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 inh1>
<label class="sr-only">Usernamelabel>
<input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
<label class="sr-only">Passwordlabel>
<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 inbutton>
<p class="mt-5 mb-3 text-muted">© 2020-2021p>
<a class="btn btn-sm">中文a>
<a class="btn btn-sm">Englisha>
form>
body>
html>
在 index.html 中,添加中英文切换标签链接用于传递参数 lang,thymeleaf中的?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')}">Englisha>
【源码分析】
在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");
}
}
编辑 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 inh1>
<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
,访问成功!
但是会出现一个问题:修改之后,无论登陆与否,都能直接访问后台页面。因此,需要设置登录拦截器!
用户登录成功后,后台会得到用户信息;如果没有登录,则不会有任何的用户信息!因此,我们可以通过拦截器进行拦截:
首先在 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
接口
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 outa>
li>
ul>
nav>
在 templates 目录中新建一个 commons
文件夹,新建 commons.html
用来放置页面的公共代码,利用 th:fragment
提取顶部导航栏和侧边栏代码,这样其他页面可以通过插入 commons.html
中的模板实现代码复用
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<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}">Logouta>
li>
ul>
nav>
<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 reportsspan>
<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
:加入元素,原有模块还是存在
<nav th:fragment="navbar">
<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}">
修改页面标题(略)
编写 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 listh2>
<h2><a class="btn btn-sm btn-success">新增员工a>h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Idth>
<th>LastNameth>
<th>Emailth>
<th>Genderth>
<th>Departmentth>
<th>Birthdayth>
<th>Operationsth>
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>
在 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 employeeh2>
<form th:action="@{add}" method="post">
<div class="form-group">
<label>LastNamelabel>
<input type="text" name="lastName" class="form-control" placeholder="lastName: Lin">
div>
<div class="form-group">
<label>Emaillabel>
<input type="email" name="email" class="form-control" placeholder="email: [email protected]">
div>
<div class="form-group">
<label>Genderlabel><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>Departmentlabel>
<select class="form-control" name="department.id">
<option value="" disabled selected hidden>department: please select a optionoption>
<option th:each="department:${departments}"
th:text="${department.getDepartmentName()}"
th:value="${department.getId()}">
option>
select>
div>
<div class="form-group">
<label>Birthdaylabel>
<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";
}
}
在 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 employeeh2>
<form th:action="@{edit}" method="post">
<input type="hidden" name="id" th:value="${employee.getId()}">
<div class="form-group">
<label>LastNamelabel>
<input th:value="${employee.getLastName()}" type="text" name="lastName" class="form-control"
placeholder="lastName: Lin">
div>
<div class="form-group">
<label>Emaillabel>
<input th:value="${employee.getEmail()}" type="email" name="email" class="form-control"
placeholder="email: [email protected]">
div>
<div class="form-group">
<label>Genderlabel><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>departmentlabel>
<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>Birthdaylabel>
<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)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";
}
}
在 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";
}
}
在 commons
公共页面的顶部导航栏处中的标签中,添加 href
链接,实现点击发起 /user/logout
请求
<a class="nav-link" th:href="@{/user/logout}">Logouta>
在 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";
}
}
前端
设计数据库(真正的难点)
前端让他能够自动运行,独立化工程
数据接口如何对接:json、对象(all in one)
前后端联调测试
前端框架
组合得到一个网页
后台模版
,99%公司会让你自己写:推荐X-admin网站模版