狂神说学习笔记:SpringBoot
SpringBoot
1、SpringBoot简介
1.1、回顾什么是Spring
Spring是一个开源框架,2003 年兴起的一个轻量级的Java 开发框架,作者:Rod Johnson 。
Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。
1.2、Spring是如何简化Java开发的
为了降低Java开发的复杂性,Spring采用了以下4种关键策略:
- 基于POJO的轻量级和最小侵入性编程,所有东西都是bean;
- 通过IOC,依赖注入(DI)和面向接口实现松耦合;
- 基于切面(AOP)和惯例进行声明式编程;
- 通过切面和模版减少样式代码,RedisTemplate,xxxTemplate
1.3、什么是SpringBoot
学过javaweb的同学就知道,开发一个web应用,从最初开始接触Servlet结合Tomcat, 跑出一个Hello Wolrld程序,是要经历特别多的步骤;后来就用了框架Struts,再后来是SpringMVC,到了现在的SpringBoot,过一两年又会有其他web框架出现;你们有经历过框架不断的演进,然后自己开发项目所有的技术也在不断的变化、改造吗?建议都可以去经历一遍;
言归正传,什么是SpringBoot呢,就是一个javaweb的开发框架,和SpringMVC类似,对比其他javaweb框架的好处,官方说是简化开发,约定大于配置, you can "just run",能迅速的开发web应用,几行代码开发一个http接口。
所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景 衍生 一种规范框架,人们只需要进行各种配置而不需要自己去实现它,这时候强大的配置功能成了优点;发展到一定程度之后,人们根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架;之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡“约定大于配置”,进而衍生出一些一站式的解决方案。
是的这就是Java企业级应用 -> J2EE -> spring -> springboot的过程。
随着 Spring 不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,慢慢变得不那么易用简单,违背了最初的理念,甚至人称配置地狱。Spring Boot 正是在这样的一个背景下被抽象出来的开发框架,目的为了让大家更容易的使用 Spring 、更容易的集成各种常用的中间件、开源软件;
Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。Spring Boot 以约定大于配置的核心思想,默认帮我们进行了很多设置,多数 Spring Boot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),Spring Boot 应用中这些第三方库几乎可以零配置的开箱即用。
简单来说就是SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。
Spring Boot 出生名门,从一开始就站在一个比较高的起点,又经过这几年的发展,生态足够完善,Spring Boot 已经当之无愧成为 Java 领域最热门的技术。
Spring Boot的主要优点:
- 为所有Spring开发者更快的入门
- 开箱即用,提供各种默认配置来简化项目配置
- 内嵌式容器简化Web项目
- 没有冗余代码生成和XML配置的要求
2、第一个SpringBoor程序
准备工作
我们将学习如何快速的创建一个Spring Boot应用,并且实现一个简单的Http请求处理。通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。
官方提供了一个快速生成的网站,IDEA集成了这样网站
2.1、项目创建方式
2.1.1、项目创建方式一:使用 Spring Initializr 的 Web 页面创建项目
Spring Initializr:https://start.spring.io/
2、填写项目信息,添加依赖 Spring Web
3、点击”Generate Project“按钮生成项目;下载此项目
4、解压项目包,并用 IDEA 以 Maven 项目导入,一路下一步即可,直到项目导入完毕。
5、如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。
2.1.2、项目创建方式二:使用 IDEA 直接创建项目
1、创建一个新项目
2、选择 spring initalizr , 可以看到默认就是去官网的快速构建工具那里实现
3、填写项目信息
4、选择初始化的组件(初学勾选 Web 即可)
5、填写项目路径
6、等待项目构建成功
2.1.3、项目结构分析
通过上面步骤完成了基础项目的创建。就会自动生成以下文件。
1、程序的主启动类
2、一个 application.properties 配置文件
3、一个 测试类
4、一个 pom.xml
2.2、项目创建详情(IDEA快速创建)
2.2.1、项目创建过程
-
选择Spring Initializr,输入项目详细信息
-
选择web支持(先不选)
-
删除多余文件
-
在pom.xml中配置
<!--web支持--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
用来支持web
2.2.2、更改项目的端口号
-
在application.properties
# 更改项目端口号 server.port=8081
2.3、项目编写
2.3.1、编写流程
-
在主程序的同级目录下,新建一个controller包,一定要在同级目录下,否则识别不到
-
在包中新建一个HelloController类
@RestController @RequestMapping("/hello") public class HelloController { @GetMapping("/hello") @ResponseBody public String hello() { return "Hello"; } }
-
编写完毕后,从主程序启动项目,浏览器发起请求,看页面返回;控制台输出了 Tomcat 访问的端口号!
2.3.2、打包发布
将项目打成jar包,点击 maven的 package·
如果打包成功,则会在target目录下生成一个 jar 包
-
成功提示
-
打包好的路径
打成了jar包后,在存放路径处 shift+鼠标右键 用 Powershell 窗口 运行以下命令
# 输入包名前几个字母,按Tab键能补全包名
java -jar 包名.jar
2.3.3、彩蛋——修改控制台Banner样式
如何更改启动时显示的字符拼成的字母,SpringBoot呢?也就是 banner 图案
SpringBoot Banner在线生成工具:https://www.bootschool.net/ascii
-
在 resources 下创建
banner.txt
,在其中添加想要的样式,例如:.===;========.__, (\__)___________| L__________________,--,--/ /-,-,-\ \-, ________ =====)o o o o ======== )) ____,===,___""" "7_/_,_,_,_,'---,-, `--._,_,_,-,--,--'' (____| _ \___\oo ; ; ; ; ; ;_____ T| `-'--'-/_,-------| ) ___--,__,------._ \__ |I| \==----/ \\ )\--\_ `-._`-'I| /=[JW]/ `"==.- -\ `-.L| /==---/ \- -\ '-.__/ \__7
-
当文件图标下面出现角标(如图所示),表示已经被应用
-
应用效果
2.4、运行原理初探
2.4.1、pom.xml
父依赖
其中它主要是依赖一个父项目,主要是管理项目的资源过滤及插件!
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
点进去,发现还有一个父依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.7</version>
</parent>
再点进去,这里才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心
<artifactId>spring-boot-dependencies</artifactId>
- spring-boot-dependencies:核心依赖在父工程中
- spring-boot-dependencies 中管理依赖的版本,所以在引入 SpringBoot 依赖的时候不需要指定版本
- spring-boot-starter-parent
- spring-boot-starter-parent 中配置资源过滤器
spring-boot-starter 启动器
<!--启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
- 启动器:就是 SpringBoot 的启动场景
- springboot-boot-starter-xxx 就是SpringBoot的场景启动器
- 比如 spring-boot-starter-web,会自动导入web环境所有的依赖
- springboot会将所有的功能场景,变成一个一个的启动器
- 需要使用什么功能,就需要找到对应的启动器
官网starter参考:https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters
2.4.2、主启动类
默认的主启动类
//@SpringBootApplication : 标注这个类是一个springboot的应用,启动类下的所有资源被导入
@SpringBootApplication
public class Springboot01HelloworldApplication {
public static void main(String[] args) {
//将springboot应用启动
SpringApplication.run(Springboot01HelloworldApplication.class, args);
}
}
注解
-
@SpringBootApplication:标注在某个类上说明这个类是SpringBoot的主配置类 , SpringBoot就应该运行这个类的main方法来启动SpringBoot应用
-
@SpringBootConfiguration:SpringBoot的配置类 ,标注在某个类上 , 表示这是一个SpringBoot的配置类
- @Configuration:说明这是一个配置类 ,配置类就是对应Spring的xml 配置文件
- @Component :说明启动类本身也是Spring中的一个组件而已,负责启动应用
- @Configuration:说明这是一个配置类 ,配置类就是对应Spring的xml 配置文件
-
@ComponentScan:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中
-
@EnableAutoConfiguration:开启自动配置功能
-
@AutoConfigurationPackage:自动配置包
- @Import({Registrar.class}):给容器导入注册组件
- Registrar.class 作用:将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器
- @Import({Registrar.class}):给容器导入注册组件
-
@Import({AutoConfigurationImportSelector.class}):给容器导入选择器组件
-
AutoConfigurationImportSelector :自动配置导入选择器
-
此类中有个获取所有的配置的方法
//获取候选配置 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; } //上面的 getSpringFactoriesLoaderFactoryClass 就是下面这个 protected Class<?> getSpringFactoriesLoaderFactoryClass() { //返回给注解类 EnableAutoConfiguration return EnableAutoConfiguration.class; }
META-INF/spring.factories:自动配置的核心文件
-
这个方法又调用了 SpringFactoriesLoader 类的静态方法!我们进入SpringFactoriesLoader类loadFactoryNames() 方法
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()); }
-
继续点击查看 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); } } }
-
-
-
-
-
自动配置原理分析图解:
spring.factories
我们根据源头打开spring.factories , 看到了很多自动配置的文件;这就是自动配置根源所在!
我们在上面的自动配置类随便找一个打开看看,比如 :AopAutoConfiguration
可以看到这些一个个的都是JavaConfig配置类,而且都注入了一些Bean,可以找一些自己认识的类,看着熟悉一下!
所以,自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中。
结论
- SpringBoot所有的自动配置都是在启动的时候扫描并加载:
spring.factories
- 所有的自动配置类都在这里面,但不一定自动生效,要判断条件是否成立,只要导入对应的start,就有对应的启动器,有了启动器,自动装配就会生效,然后就配置成功了
- SpringBoot 在启动的时候从类路径下的 META-INF/spring.factories 中获取 EnableAutoConfiguration 指定的值
- 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作
- 以前我们需要自动配置的东西,现在 springboot 帮我们做了
- 整个 JavaEE 的整体解决方案和自动配置都在 springboot-autoconfigure 的jar包中
- 它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器中
- 它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并配置好这些组件
- 有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作
2.4.3、SpringApplication
Run
最初以为是一个main方法,其实是开启了一个服务
@SpringBootApplication
public class Springboot01HelloworldApplication {
public static void main(String[] args) {
//将springboot应用启动
//参数一:应用入口的类 参数二:命令行参数
SpringApplication.run(Springboot01HelloworldApplication.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语法
3.1、概述
3.1.1、配置文件
SpringBoot使用一个全局的配置文件 , 配置文件名称是固定的
- application.properties
- 语法结构 :key=value
- application.yml
- 语法结构 :key:空格 value
配置文件的作用 :修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们自动配置好了
3.1.2、YAML概述
YAML是 "YAML Ain't a Markup Language" (YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)
YAML A Markup Language:是一个标记语言
YAML is not Markup Language:不是一个标记语言
这种语言以数据作为中心,而不是以标记语言为重点!
以前的配置文件,大多数都是使用xml来配置;比如一个简单的端口配置,我们来对比下yaml和xml
yaml配置:
server:
prot: 8080
xml配置:
<server>
<port>8081<port>
</server>
YAML语法
基础语法:
k:(空格) v
以此来表示一对键值对(空格不能省略);以空格的缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的
注意:属性和值的大小写都是十分敏感的。例子:
server:
port: 8081
path: /hello
值的写法
字面量:普通的值【数字,布尔值,字符串】
k: v
字面量直接写在后面就可以,字符串默认不用加上双引号或单引号;
"" 双引号,不会转义字符串里面的特殊字符,特殊字符会作为本身想表示的意思;
比如:name: "D \n t" 输出:D 换行 t
对象的写法
# 普通的写法
student:
name: Dt
age: 18
# 对象的行内写法
student: {name: Dt, age: 18}
数组的写法
# 普通的写法
pets:
- cat
- dog
- pig
# 数组的行内写法
pets: [cat,dog,pig]
相比 yaml
,properties
只能保存键值对
语法要求严格!
- 空格不能省略
- 以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的
- 属性和值的大小写都是十分敏感的
3.2、yaml给实体类赋值
-
创建 Person 和 Dog 实体类
Dog.java
@Component public class Dog { private String name; private Integer age; public Dog() { } public Dog(String name, Integer age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "Dog{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
Person.java
@Component @ConfigurationProperties(prefix = "person") public class Person { private String name; private Integer age; private Boolean happy; private Date birth; private Map<String, Object> maps; private List<Object> lists; private Dog dog; public Person() { } public Person(String name, Integer age, Boolean happy, Date birth, Map<String, Object> maps, List<Object> lists, Dog dog) { this.name = name; this.age = age; this.happy = happy; this.birth = birth; this.maps = maps; this.lists = lists; this.dog = dog; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Boolean getHappy() { return happy; } public void setHappy(Boolean happy) { this.happy = happy; } public Date getBirth() { return birth; } public void setBirth(Date birth) { this.birth = birth; } public Map<String, Object> getMaps() { return maps; } public void setMaps(Map<String, Object> maps) { this.maps = maps; } public List<Object> getLists() { return lists; } public void setLists(List<Object> lists) { this.lists = lists; } public Dog getDog() { return dog; } public void setDog(Dog dog) { this.dog = dog; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + ", happy=" + happy + ", birth=" + birth + ", maps=" + maps + ", lists=" + lists + ", dog=" + dog + '}'; } }
-
利用 YAML 注入
person: name: Dt age: 3 happy: false birth: 2022/05/02 maps: {k1: v1, k2: v2} list: - code - music - girl dog: name: 旺财 age: 3
-
springboot配置注解处理器没有找到
解决办法:使用 @ConfigurationProperties 需要导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>
-
测试
@SpringBootTest class Springboot02ConfigApplicationTests { @Autowired private Person person; @Test void contextLoads() { System.out.println(person); } }
-
结果:所有值都注入成功
3.2.1、配置文件占位符
- 配置文件还可以编写占位符生成随机数
person:
name: Dt${random.uuid}
age: ${random.int}
happy: false
birth: 2022/05/02
maps: {k1: v1, k2: v2}
lists:
- code
- music
- girl
dog:
name: ${person.hello:hhh}_旺财
age: 3
- ${random.uuid}:表示随机UUID
- ${random.int}:表示随机整数
- ${person.hello:hhh}:表示 person 中的 hello 如果不存在就取默认值 hhh,存在就取存在的值
3.3、回顾properties配置
我们上面采用的 yaml 方法都是最简单的方式,开发中最常用的;也是 springboot 所推荐的!
那我们来唠唠其他的实现方式,道理都是相同的;
配置文件除了yml还有我们之前常用的properties , 我们没有讲,我们来唠唠!
-
【注意】properties配置文件在写中文的时候,会有乱码 , 我们需要去IDEA中设置编码格式为UTF-8
settings-->FileEncodings 中配置;
-
新建一个实体类 User
@Component public class User { private String name; private Integer age; private String gender; @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + ", gender='" + gender + '\'' + '}'; } }
-
编辑配置文件
user.properties
name=Dt age=18 gender=male
-
我们在User类上使用
@PropertySource
指定配置文件,@Value
来进行注入@Component @PropertySource(value = "classpath:user.properties") //加载指定配置文件 public class User { @Value("${name}") //占位符 private String name; @Value("${age}") private Integer age; @Value("${gender}") private String gender; @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + ", gender='" + gender + '\'' + '}'; } }
-
Springboot测试
@SpringBootTest class Springboot02ConfigApplicationTests { @Autowired private User user; @Test void contextLoads() { System.out.println(user); } }
-
结果:注入成功
3.4、@ConfigurationProperties 与 @Value 对比
@Value
这个使用起来并不友好!我们需要为每个属性单独注解赋值,比较麻烦;
我们来看个功能对比图
@ConfigurationProperties
只需要写一次即可 ,@Value
则需要每个字段都添加- 松散绑定(Relaxed binding)
比如例子 user: userName 也可以表示为 user: user-name(大写用-符号),user: username 表示为 user: user_name(小写用_符号),比如支持这种写法的就是支持松散绑定 - JSR303数据校验 , 这个就是我们可以在字段是增加一层过滤器验证 , 可以保证数据的合法性
- 复杂类型封装,yaml 中可以封装对象 , 使用 value 就不支持
结论
配置yml和配置properties都可以获取到值 , 强烈推荐 yml;
如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;
如果说,我们专门编写了一个JavaBean来和配置文件进行一一映射,就直接@configurationProperties,不要犹豫!
4、JSR303校验及多环境切换
4.1、JSR303校验
Springboot中可以用 @Validated
来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。
-
使用
@Validated
需要导入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
-
我们这里来写个注解让我们的 name 只能支持Email格式
@Component @ConfigurationProperties(prefix = "person") @Validated //数据校验 public class Person { @Email(message = "邮箱格式错误") //校验不通过的提示 private String name; }
-
结果:
使用数据校验,可以保证数据的正确性;
常见参数
@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=) string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
.......等等
除此以外,我们还可以自定义一些数据校验规则
4.2、多环境切换
profile 是 Spring 对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境
4.2.1、配置文档加载位置
外部加载配置文件的方式十分多,我们选择最常用的即可,在开发的资源文件中进行配置!
-
五种配置文件位置及运行优先级(低->高)
classpath:/
classpath:/config/
file:./
file:/config/
file:./cofig/*/
-
springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件:
优先级1:项目路径下的config文件夹的直接子目录配置文件 优先级2:项目路径下的config文件夹配置文件 优先级3:项目路径下配置文件 优先级4:资源路径下的config文件夹配置文件 优先级5:资源路径下配置文件
优先级由高到底,高优先级的配置会覆盖低优先级的配置;
SpringBoot会从这五个位置全部加载主配置文件;互补配置;
4.2.2、多配置文件
我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本
例如:
- application-test.properties 代表测试环境配置
- application-dev.properties 代表开发环境配置
但是Springboot并不会直接启动这些配置文件,它默认使用 application.properties 主配置文件
我们需要通过一个配置来选择需要激活的环境:
# springboot 的多环境配置:可以选择激活哪一个配置文件
spring.profiles.active=test
相比 properties 需要写多个 properties文件,yaml 可以实现多文档模块
4.2.3、yaml多文档模块
和properties配置文件中一样,但是使用yml去实现不需要创建多个配置文件,更加方便了
server:
port: 8081
spring:
profiles:
active: dev # 选择要激活dev环境块
---
server:
port: 8082
spring:
profiles: dev # 配置环境的名称
---
server:
port: 8083
spring:
profiles: test # 配置环境的名称
注意:如果yml和properties同时都配置了端口,并且没有激活其他环境 , 默认会使用properties配置文件的!
5、自动装配原理
5.1、自动装配原理详解
5.1.1、概述
根据当前不同的条件判断,决定这个配置类是否生效!
- 一但这个配置类生效;这个配置类就会给容器中添加各种组件;
- 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
- 所有在配置文件中能配置的属性都是在xxxxProperties类中封装着;
- 配置文件能配置什么就可以参照某个功能对应的这个属性类
原理流程
-
我们在
spring.factories
里随便点击一个自动装配类看它的源码,例如HttpEncodingAutoConfiguration
-
查看源码,我们发现
@EnableConfigurationProperties
是从ServerProperties
属性封装类中读取的属性来装配(结合下面的构造器引用的也是ServerProperties
) -
我们再点进
ServerProperties
的源码,我们可以发现这里的原理和yaml给实体类赋值(点击跳转)中给实体类赋值的方式一致:读取配置文件中前缀为 server 的赋值属性 -
通过对比我们可以确认结论正确
这个就是自动装配的原理,也就是为什么我们在 properties 或 yaml 配置文件中配置能生效的原因
B站弹幕总结:自动配置类的配置本质就是配置组件,而自动配置类会依赖properties(这其中有很多属性),在yml文件写上自己想要的属性就好
5.1.2、精髓
1、SpringBoot启动会加载大量的自动配置类
2、我们看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;
3、我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)
4、给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;
- xxxxAutoConfigurartion:自动配置类;给容器中添加组件
- xxxxProperties:封装配置文件中相关属性;
5.2、@Conditional
了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;
@Conditional派生注解(Spring注解版原生的@Conditional作用)
作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;
那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是所有的都生效了。
我们怎么知道哪些自动配置类生效?
我们可以通过启用 debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效;
application.yaml
#开启springboot的调试类
debug: true
开启后就可以在控制台查看日志了。
-
Positive matches:(自动配置类启用的:正匹配)
-
Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)
-
Unconditional classes: (没有条件的类)
6、SpringBoot Web开发
核心:自动装配
springboot到底帮我们配置了什么?我们能不能进行修改?能修改哪些东西?能不能扩展?
- xxxxAutoConfiguration.. 向容器中自动配置组件
- xxxxProperties:自动配置类,装配配置文件中自定义的一些内容
6.1、Web开发静态资源处理
6.1.1、web开发要解决的问题
- 导入静态资源
- 首页
- jsp,模板引擎Thymeleaf
- 装配扩展SpringMVC
- 增删改查
- 拦截器
- 国际化
6.1.2、静态资源映射规则
首先,我们搭建一个普通的SpringBoot项目,回顾一下HelloWorld程序!
写请求非常简单,那我们要引入我们前端资源,我们项目中有许多的静态资源,比如css,js等文件,这个SpringBoot怎么处理呢?
如果我们是一个web应用,我们的main下会有一个webapp,我们以前都是将所有的页面导在这里面的,对吧!但是我们现在的pom呢,打包方式是为jar的方式,那么这种方式SpringBoot能不能来给我们写页面呢?当然是可以的,但是SpringBoot对于静态资源放置的位置,是有规定的!
我们先来聊聊这个静态资源映射规则:
SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置类里面;
我们可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;
有一个方法:addResourceHandlers
添加资源处理
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
// 已禁用默认资源处理
logger.debug("Default resource handling disabled");
return;
}
// webjars 配置
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
// 静态资源配置
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
registration.addResourceLocations(resource);
}
});
}
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
addResourceHandler(registry, pattern, (registration) -> registration.addResourceLocations(locations));
}
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern,
Consumer<ResourceHandlerRegistration> customizer) {
if (registry.hasMappingForPattern(pattern)) {
return;
}
ResourceHandlerRegistration registration = registry.addResourceHandler(pattern);
customizer.accept(registration);
//缓存控制
registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
customizeResourceHandlerRegistration(registration);
}
读一下源代码:比如所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源;
6.1.3、什么是webjars
Webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。
使用SpringBoot需要使用Webjars,我们可以去搜索一下:
要使用jQuery,我们只要要引入jQuery对应版本的pom依赖即可!
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.1</version>
</dependency>
-
导入完毕,查看webjars目录结构,并访问Jquery.js文件!
-
访问:只要是静态资源,SpringBoot就会去对应的路径寻找资源,我们这里访问:
6.1.4、第二种静态资源映射规律
-
在springboot,我们可以使用一下方式处理静态数据
- webjars
localhost:8081/webjars/
- public,static,/**,resources
localhost:8081/
- webjars
-
优先级:resources>static(默认)>public
优先级和源码里的数组顺序一致:
6.1.5、自定义静态资源路径
我们也可以自己通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在application.properties中配置;
spring.web.resources.static-locations=/hello/,classpath:/dt/
一旦自己定义了静态文件夹的路径,原来的自动配置就都会失效了!
6.1.6、拓展:网站图标说明(了解即可)
与其他静态资源一样,Spring Boot在配置的静态内容位置中查找 favicon.ico。如果存在这样的文件,它将自动用作应用程序的favicon。
1、关闭SpringBoot默认图标
#关闭默认图标
spring.mvc.favicon.enabled=false
2、自己放一个图标在静态资源目录下,我放在 public 目录下
3、清除浏览器缓存!刷新网页,发现图标已经变成自己的了!
注:maven pom.xml
中的spring-boot-starter-parent的版本要低才行,已经被淘汰(了解即可)
新版本的 springboot 直接把图标命名为 favicon.ico
放在 classpath:/static/
下就可以了
注意:favicon.ico 尺寸不能太大
6.2、模板引擎Thymeleaf
6.2.1、模板引擎介绍
前端交给我们的页面,是html页面。如果是我们以前开发,我们需要把他们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示,及交互等。
jsp支持非常强大的功能,包括能写Java代码,但是呢,我们现在的这种情况,SpringBoot这个项目首先是以jar的方式,不是war,像第二,我们用的还是嵌入式的Tomcat,所以呢,他现在默认是不支持jsp的。
那不支持jsp,如果我们直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦,那怎么办呢?
SpringBoot推荐你可以来使用模板引擎:
模板引擎,我们其实大家听到很多,其实jsp就是一个模板引擎,还有用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但再多的模板引擎,他们的思想都是一样的,什么样一个思想呢我们来看一下这张图:
模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。而这些值,从哪来呢,就是我们在后台封装一些数据。然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。只不过呢,就是说不同模板引擎之间,他们可能这个语法有点不一样。其他的我就不介绍了,我主要来介绍一下SpringBoot给我们推荐的Thymeleaf模板引擎,这模板引擎呢,是一个高级语言的模板引擎,他的这个语法更简单。而且呢,功能更强大。
我们呢,就来看一下这个模板引擎,那既然要看这个模板引擎。首先,我们来看SpringBoot里边怎么用。
6.2.2、引入Thymeleaf
怎么引入呢,对于springboot来说,什么事情不都是一个start的事情嘛,我们去在项目中引入一下。给大家三个网址:
Thymeleaf 官网:https://www.thymeleaf.org/
Thymeleaf 在Github 的主页:https://github.com/thymeleaf/thymeleaf
Spring官方文档:找到我们对应的版本
https://docs.spring.io/spring-boot/docs/2.6.7/reference/htmlsingle/#using.build-systems.starters
找到对应的pom依赖:可以适当点进源码看下本来的包!
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Maven会自动下载jar包,我们可以去看下下载的东西;
6.2.3、测试Thymeleaf
通过查看ThymeleafProperties
的源码,我们了解到 html 文件要存放在 templates下
测试
-
编写一个indexController
@Controller public class IndexController { @RequestMapping("/test") public String index(Model model) { model.addAttribute("msg", "Hello, Test"); return "test"; } }
-
编写一个测试页面 index.html 放在 templates 目录下(使用thymeleaf需要导入约束:
xmlns:th="http://www.thymeleaf.org"
)<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!--所有的html元素都可以被 thymeleaf 替换接管: th:元素名--> <!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样--> <div th:text="${msg}"></div> </body> </html>
-
启动项目请求测试
6.2.4、Thymeleaf的使用语法
-
我们可以使用任意的 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. #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: _
6.2.5、Thymeleaf语法测试
-
indexController
@Controller public class IndexController { @RequestMapping("/test") public String index(Model model) { model.addAttribute("msg", "<h1>Hello, Test</h1>"); model.addAttribute("users", Arrays.asList("dt","ky")); return "test"; } }
-
index.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!--所有的html元素都可以被 thymeleaf 替换接管: th:元素名--> <!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样--> <div th:text="${msg}"></div> <!--不转义--> <div th:utext="${msg}"></div> <hr> <!--遍历数据--> <!--th:each每次遍历都会生成当前这个标签:官网 #9Local Variables--> <h3 th:each="user:${users}" th:text="${user}"></h3> <!--行内写法:官网 #12Inlining--> <span th:each="user:${users}">[[ ${user} ]]</span> </body> </html>
-
启动项目测试!
我们看完语法,很多样式,我们即使现在学习了,也会忘记,所以我们在学习过程中,需要使用什么,根据官方文档来查询,才是最重要的,要熟练使用官方文档!
6.3、MVC配置原理
学习网站:狂神说SpringBoot12:MVC自动配置原理 (qq.com)
在springboot中,有非常多的xxxx Configuration 帮助我们进行扩展配置,只要看到了这个定性,我们就要注意了!
6.3.1、官网阅读
在进行项目编写前,我们还需要知道一个东西,就是SpringBoot对我们的SpringMVC还做了哪些配置,包括如何扩展,如何定制。
只有把这些都搞清楚了,我们在之后使用才会更加得心应手。途径一:源码分析,途径二:官方文档!
Spring MVC Auto-configuration
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.
-- Spring Boot为Spring MVC提供了自动配置,它可以很好地与大多数应用程序一起工作
The auto-configuration adds the following features on top of Spring’s defaults:
-- 自动配置在 Spring 默认设置的基础上增加了以下特性:
Inclusion of
ContentNegotiatingViewResolver
andBeanNameViewResolver
beans包含视图解析器
Support for serving static resources, including support for WebJars
支持静态资源,包括 webjars
Automatic registration of
Converter
,GenericConverter
, andFormatter
beans自动注册转换器,通用转换器,格式化器
- 转换器,这就是我们网页提交数据到后台自动封装成为对象的东西,比如把"1"字符串自动转换为int类型
- 格式化器,比如页面给我们了一个2019-8-10,它会给我们自动格式化为Date对象
Support for
HttpMessageConverters
支持网页信息转换器
- pringMVC用来转换Http请求和响应的的,比如我们要把一个User对象转换为JSON字符串
Automatic registration of
MessageCodesResolver
定义错误代码生成规则的
Static
index.html
support静态首页支持
Automatic use of a
ConfigurableWebBindingInitializer
bean初始化数据绑定器:帮我们把请求数据绑定到JavaBean中
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 typeWebMvcConfigurer
but without@EnableWebMvc
.-- 如果您希望保留 Spring Boot MVC 定制并进行更多的 MVC 定制(拦截器、格式化程序、视图控制器和其他特性) ,可以添加您自己的
WebMvcConfigurer
类型的@Configuration
,但不要添加@EnableWebMvc
。If you want to provide custom instances of
RequestMappingHandlerMapping
,RequestMappingHandlerAdapter
, orExceptionHandlerExceptionResolver
, and still keep the Spring Boot MVC customizations, you can declare a bean of typeWebMvcRegistrations
and use it to provide custom instances of those components.-- 如果你想提供自定义的
RequestMappingHandlerMapping
、RequestMappingHandlerAdapter
或ExceptionHandlerExceptionResolver
定制,你可以声明一个类型为WebMvcRegistrations
的 bean,并使用它来提供这些组件的自定义实例。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
-annotatedDelegatingWebMvcConfiguration
as described in the Javadoc of@EnableWebMvc
.-- 如果你想完全控制 Spring MVC,你可以添加你自己的注释
@Configuration
和@EnableWebMvc
,或者像在@EnableWebMvc
的 Javadoc 中描述的那样添加你自己的@Configuration
注释DelegatingWebMvcConfiguration
。
我们来仔细对照,看一下它怎么实现的,它告诉我们SpringBoot已经帮我们自动配置好了SpringMVC,然后自动配置了哪些东西呢?
ContentNegotiatingViewResolver 内容协商视图解析器
自动配置了ViewResolver,就是我们之前学习的SpringMVC的视图解析器;
即根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发,重定向)。
public interface ViewResolver
// 实现了视图解析器的类,我们也可以把它看作视图解析器
我们去看看这里的源码:我们找到 WebMvcAutoConfiguration , 然后搜索ContentNegotiatingViewResolver。找到如下方法!
@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
// ContentNegotiatingViewResolver uses all the other view resolvers to locate
// ContentNegotiatingViewResolver使用所有其他视图解析器来定位视图
// a view so it should have a high precedence
// 因此它应该具有较高的优先级
resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
return resolver;
}
我们可以点进这类看看!找到对应的解析视图的代码
@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 = 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;
}
}
String mediaTypeInfo = this.logger.isDebugEnabled() && requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : "";
if (this.useNotAcceptableStatusCode) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
}
return NOT_ACCEPTABLE_VIEW;
} else {
this.logger.debug("View remains unresolved" + mediaTypeInfo);
return null;
}
}
我们继续点getCandidateViews
进去看,他是怎么获得候选的视图的呢?
getCandidateViews中看到他是把所有的视图解析器拿来,进行while循环,挨个解析!
Iterator var5 = this.viewResolvers.iterator();
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());
Iterator var3 = matchingBeans.iterator();
while(var3.hasNext()) {
viewResolver = (ViewResolver)var3.next();
if (this != viewResolver) {
this.viewResolvers.add(viewResolver);
}
}
} else {
for(int i = 0; i < this.viewResolvers.size(); ++i) {
viewResolver = (ViewResolver)this.viewResolvers.get(i);
if (!matchingBeans.contains(viewResolver)) {
String name = viewResolver.getClass().getName() + i;
this.obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolver, name);
}
}
}
AnnotationAwareOrderComparator.sort(this.viewResolvers);
this.cnmFactoryBean.setServletContext(servletContext);
}
既然它是在容器中去找视图解析器,我们是否可以猜想,我们就可以去实现一个视图解析器了呢?
我们可以自己给容器中去添加一个视图解析器;这个类就会帮我们自动的将它组合进来;我们去实现一下
-
我们在我们的主程序中去写一个视图解析器来试试;
//扩展 springmvc @Configuration public class MyMvcConfig implements WebMvcConfigurer { @Bean public ViewResolver myViewResolver() { return new MyViewResolver(); } //自定义的视图解析器 public static class MyViewResolver implements ViewResolver { @Override public View resolveViewName(String viewName, Locale locale) throws Exception { return null; } } }
-
怎么看我们自己写的视图解析器有没有起作用呢?
我们给 DispatcherServlet 中的 doDispatch方法 加个断点进行调试一下,因为所有的请求都会走到这个方法中
-
我们启动我们的项目,然后随便访问一个页面,看一下Debug信息
找到 this
找到视图解析器,我们看到我们自己定义的就在这里了
所以说,我们如果想要使用自己定制化的东西,我们只需要给容器中添加这个组件就好了!剩下的事情SpringBoot就会帮我们做了!
Formatter 格式化器
我们去看看这里的源码:我们找到 WebMvcAutoConfiguration , 然后搜索Formatter。找到如下方法!
@Bean
@Override
public FormattingConversionService mvcConversionService() {
// 拿到配置文件中的格式化规则
Format format = this.mvcProperties.getFormat();
WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()
.dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
addFormatters(conversionService);
return conversionService;
}
点击format.getDate()
去:
public static class Format {
/**
* Date format to use, for example 'dd/MM/yyyy'.
*/
private String date;
...
public String getDate() {
return this.date;
}
...
}
可以看到在我们的Properties文件中,我们可以进行自动配置它!
如果配置了自己的格式化方式,就会注册到Bean中生效,我们可以在配置文件中配置日期格式化的规则:
# 自定义配置日期格式化
spring.mvc.format.date=
public void setDate(String date) {
this.date = date;
}
其余的就不一一举例了,大家可以下去多研究探讨即可!
修改SpringBoot的默认配置
这么多的自动配置,原理都是一样的,通过这个WebMVC的自动配置原理分析,我们要学会一种学习方式,通过源码探究,得出结论;这个结论一定是属于自己的,而且一通百通。
SpringBoot的底层,大量用到了这些设计细节思想,所以,没事需要多阅读源码!得出结论;
SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(如果用户自己配置@bean),如果有就用用户配置的,如果没有就用自动配置的;
如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来!
扩展使用SpringMVC
官方文档如下:
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.
我们要做的就是编写一个@Configuration注解类,并且类型要为WebMvcConfigurer,还不能标注@EnableWebMvc注解;我们去自己写一个;我们新建一个包叫config,写一个类MyMvcConfig;
// 应为类型要求为WebMvcConfigurer,所以我们实现其接口
// 扩展 springmvc
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 浏览器发送/dt , 就会跳转到test页面;
registry.addViewController("/dt").setViewName("test");
}
}
我们去浏览器访问一下:
确实也跳转过来了,只是没有传参!所以说,我们要扩展SpringMVC,官方就推荐我们这么去使用,既保SpringBoot留所有的自动配置,也能用我们扩展的配置!
我们可以去分析一下原理:
-
WebMvcAutoConfiguration
是 SpringMVC的自动配置类,里面有一个类WebMvcAutoConfigurationAdapter
-
这个类上有一个注解,在做其他自动配置时会导入:
@Import(EnableWebMvcConfiguration.class)
-
我们点进
EnableWebMvcConfiguration
这个类看一下,它继承了一个父类:DelegatingWebMvcConfiguration
这个父类中有这样一段代码:
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当做参考,发现它调用了一个
protected void addViewControllers(ViewControllerRegistry registry) { this.configurers.addViewControllers(registry); }
-
我们点
addViewControllers
进去看一下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自己的配置类,我们自己的配置类当然也会被调用;
全面接管SpringMVC
官方文档:
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
.
全面接管即:SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己去配置!
只需在我们的配置类中要加一个@EnableWebMvc。
我们看下如果我们全面接管了SpringMVC了,我们之前SpringBoot给我们配置的静态资源映射一定会无效,我们可以去测试一下;
不加注解之前,访问首页:
给配置类加上注解:@EnableWebMvc
我们发现所有的SpringMVC自动配置都失效了!回归到了最初的样子;
当然,我们开发中,不推荐使用全面接管SpringMVC
思考问题?为什么加了一个注解,自动配置就失效了!我们看下注解源码:
-
这里发现它是导入了一个类,我们可以继续进去看
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented @Import({DelegatingWebMvcConfiguration.class}) public @interface EnableWebMvc { }
-
它继承了一个父类 WebMvcConfigurationSupport
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { ... }
-
我们来回顾一下Webmvc自动配置类
@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration { ... }
发现
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
,只有WebMvcConfigurationSupport.class
不存在的时候才生效,而使用了@EnableWebMvc
会自动导入DelegatingWebMvcConfiguration.class
,DelegatingWebMvcConfiguration
本身有继承了WebMvcConfigurationSupport
,所以WebMvc自动配置类就没有生效,导致访问首页出现了404
总结一句话:@EnableWebMvc
将WebMvcConfigurationSupport
的组件导入进来了;
而导入的WebMvcConfigurationSupport
只是 SpringMVC 最基本的功能!
在SpringBoot中会有非常多的扩展配置,只要看见了这个,我们就应该多留心注意~
7、员工管理系统
7.1、准备工作
bootstrap模板:
https://getbootstrap.com/docs/4.0/examples/
或者 百度链接:https://pan.baidu.com/s/1URaFmN6rt2AzDOwVogWBNQ 提取码:bruc
清理多余的文件或者直接创建新的项目,把 asserts 放 static 下,html 放 templates 下(样式加载不出来可以尝试重启IDEA)
项目结构:
7.1.1、导入依赖
导入 lombok 依赖,还要在 IDEA 上安装 lombok 插件
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
导入 Thymeleaf 模板依赖
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
7.1.2、POJO
Department.java
//部门表
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
private Integer id;
private String departmentName;
}
employee.java
//员工表
@Data
@NoArgsConstructor
public class Employee {
private Integer id;
private String lastName;
private String email;
private Integer gender; // 0:女 1:男
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();
}
}
7.1.3、DAO
DepartmentDao.java
//部门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.java
//员工dao
@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, "AA", "A46294093@qq.com", 0, new Department(101, "教学部")));
employees.put(1002, new Employee(1002, "BB", "B46294093@qq.com", 1, new Department(102, "市场部")));
employees.put(1003, new Employee(1003, "CC", "C46294093@qq.com", 0, new Department(103, "教研部")));
employees.put(1004, new Employee(1004, "DD", "D46294093@qq.com", 1, new Department(104, "运营部")));
employees.put(1005, new Employee(1005, "EE", "E46294093@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);
}
//通过Id删除员工
public void deleteEmployeeById(Integer id) {
employees.remove(id);
}
}
7.2、首页实现
-
视图解析
@Configuration public class MyMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); registry.addViewController("/index.html").setViewName("index"); } }
-
修改 html 的引用静态资源 为 Thymeleaf 动态引用,为了防止在不同根目录下找不到静态资源
index.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>Signin Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/asserts/css/signin.css}" rel="stylesheet"> </head> <body class="text-center"> <form class="form-signin" action="dashboard.html"> <img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72"> <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1> <label class="sr-only">Username</label> <input type="text" class="form-control" placeholder="Username" required="" autofocus=""> <label class="sr-only">Password</label> <input type="password" class="form-control" placeholder="Password" required=""> <div class="checkbox mb-3"> <label> <input type="checkbox" value="remember-me"> Remember me </label> </div> <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> <p class="mt-5 mb-3 text-muted">© 2017-2018</p> <a class="btn btn-sm">中文</a> <a class="btn btn-sm">English</a> </form> </body> </html>
404.html
<!DOCTYPE html> <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ --> <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>Dashboard Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet"> <style type="text/css"> /* Chart.js */ @-webkit-keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } @keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } .chartjs-render-monitor { -webkit-animation: chartjs-render-animation 0.001s; animation: chartjs-render-animation 0.001s; } </style> </head> <body> <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0"> <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Company name</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" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Sign out</a> </li> </ul> </nav> <div class="container-fluid"> <div class="row"> <nav class="col-md-2 d-none d-md-block bg-light sidebar"> <div class="sidebar-sticky"> <ul class="nav flex-column"> <li class="nav-item"> <a class="nav-link active" 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-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> Dashboard <span class="sr-only">(current)</span> </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"> <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> Orders </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-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" 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-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> Customers </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-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" 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-layers"> <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon> <polyline points="2 17 12 22 22 17"></polyline> <polyline points="2 12 12 17 22 12"></polyline> </svg> Integrations </a> </li> </ul> <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> <span>Saved reports</span> <a class="d-flex align-items-center text-muted" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg> </a> </h6> <ul class="nav flex-column mb-2"> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Current month </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Last quarter </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Social engagement </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Year-end sale </a> </li> </ul> </div> </nav> <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4"> <h1>404</h1> </main> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}" ></script> <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}" ></script> <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}" ></script> <!-- Icons --> <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}" ></script> <script> feather.replace() </script> <!-- Graphs --> <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}" ></script> <script> var ctx = document.getElementById("myChart"); var myChart = new Chart(ctx, { type: 'line', data: { labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], datasets: [{ data: [15339, 21345, 18483, 24003, 23489, 24092, 12034], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: false } }] }, legend: { display: false, } } }); </script> </body> </html>
dashboard.html
<!DOCTYPE html> <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ --> <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>Dashboard Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet"> <style type="text/css"> /* Chart.js */ @-webkit-keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } @keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } .chartjs-render-monitor { -webkit-animation: chartjs-render-animation 0.001s; animation: chartjs-render-animation 0.001s; } </style> </head> <body> <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0"> <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Company name</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" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Sign out</a> </li> </ul> </nav> <div class="container-fluid"> <div class="row"> <nav class="col-md-2 d-none d-md-block bg-light sidebar"> <div class="sidebar-sticky"> <ul class="nav flex-column"> <li class="nav-item"> <a class="nav-link active" 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-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> Dashboard <span class="sr-only">(current)</span> </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"> <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> Orders </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-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" 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-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> Customers </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-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" 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-layers"> <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon> <polyline points="2 17 12 22 22 17"></polyline> <polyline points="2 12 12 17 22 12"></polyline> </svg> Integrations </a> </li> </ul> <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> <span>Saved reports</span> <a class="d-flex align-items-center text-muted" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg> </a> </h6> <ul class="nav flex-column mb-2"> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Current month </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Last quarter </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Social engagement </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Year-end sale </a> </li> </ul> </div> </nav> <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4"> <div class="chartjs-size-monitor" style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: hidden; pointer-events: none; visibility: hidden; z-index: -1;"> <div class="chartjs-size-monitor-expand" style="position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;"> <div style="position:absolute;width:1000000px;height:1000000px;left:0;top:0"></div> </div> <div class="chartjs-size-monitor-shrink" style="position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;"> <div style="position:absolute;width:200%;height:200%;left:0; top:0"></div> </div> </div> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom"> <h1 class="h2">Dashboard</h1> <div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-group mr-2"> <button class="btn btn-sm btn-outline-secondary">Share</button> <button class="btn btn-sm btn-outline-secondary">Export</button> </div> <button class="btn btn-sm btn-outline-secondary dropdown-toggle"> <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-calendar"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> This week </button> </div> </div> <canvas class="my-4 chartjs-render-monitor" id="myChart" width="1076" height="454" style="display: block; width: 1076px; height: 454px;"></canvas> </main> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}" ></script> <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}" ></script> <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}" ></script> <!-- Icons --> <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}" ></script> <script> feather.replace() </script> <!-- Graphs --> <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}" ></script> <script> var ctx = document.getElementById("myChart"); var myChart = new Chart(ctx, { type: 'line', data: { labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], datasets: [{ data: [15339, 21345, 18483, 24003, 23489, 24092, 12034], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: false } }] }, legend: { display: false, } } }); </script> </body> </html>
list.html
<!DOCTYPE html> <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ --> <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>Dashboard Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet"> <style type="text/css"> /* Chart.js */ @-webkit-keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } @keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } .chartjs-render-monitor { -webkit-animation: chartjs-render-animation 0.001s; animation: chartjs-render-animation 0.001s; } </style> </head> <body> <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0"> <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Company name</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" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Sign out</a> </li> </ul> </nav> <div class="container-fluid"> <div class="row"> <nav class="col-md-2 d-none d-md-block bg-light sidebar"> <div class="sidebar-sticky"> <ul class="nav flex-column"> <li class="nav-item"> <a class="nav-link active" 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-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> Dashboard <span class="sr-only">(current)</span> </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"> <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> Orders </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-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" 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-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> Customers </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-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" 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-layers"> <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon> <polyline points="2 17 12 22 22 17"></polyline> <polyline points="2 12 12 17 22 12"></polyline> </svg> Integrations </a> </li> </ul> <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> <span>Saved reports</span> <a class="d-flex align-items-center text-muted" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg> </a> </h6> <ul class="nav flex-column mb-2"> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Current month </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Last quarter </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Social engagement </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Year-end sale </a> </li> </ul> </div> </nav> <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>#</th> <th>Header</th> <th>Header</th> <th>Header</th> <th>Header</th> </tr> </thead> <tbody> <tr> <td>1,001</td> <td>Lorem</td> <td>ipsum</td> <td>dolor</td> <td>sit</td> </tr> <tr> <td>1,002</td> <td>amet</td> <td>consectetur</td> <td>adipiscing</td> <td>elit</td> </tr> <tr> <td>1,003</td> <td>Integer</td> <td>nec</td> <td>odio</td> <td>Praesent</td> </tr> <tr> <td>1,003</td> <td>libero</td> <td>Sed</td> <td>cursus</td> <td>ante</td> </tr> <tr> <td>1,004</td> <td>dapibus</td> <td>diam</td> <td>Sed</td> <td>nisi</td> </tr> <tr> <td>1,005</td> <td>Nulla</td> <td>quis</td> <td>sem</td> <td>at</td> </tr> <tr> <td>1,006</td> <td>nibh</td> <td>elementum</td> <td>imperdiet</td> <td>Duis</td> </tr> <tr> <td>1,007</td> <td>sagittis</td> <td>ipsum</td> <td>Praesent</td> <td>mauris</td> </tr> <tr> <td>1,008</td> <td>Fusce</td> <td>nec</td> <td>tellus</td> <td>sed</td> </tr> <tr> <td>1,009</td> <td>augue</td> <td>semper</td> <td>porta</td> <td>Mauris</td> </tr> <tr> <td>1,010</td> <td>massa</td> <td>Vestibulum</td> <td>lacinia</td> <td>arcu</td> </tr> <tr> <td>1,011</td> <td>eget</td> <td>nulla</td> <td>Class</td> <td>aptent</td> </tr> <tr> <td>1,012</td> <td>taciti</td> <td>sociosqu</td> <td>ad</td> <td>litora</td> </tr> <tr> <td>1,013</td> <td>torquent</td> <td>per</td> <td>conubia</td> <td>nostra</td> </tr> <tr> <td>1,014</td> <td>per</td> <td>inceptos</td> <td>himenaeos</td> <td>Curabitur</td> </tr> <tr> <td>1,015</td> <td>sodales</td> <td>ligula</td> <td>in</td> <td>libero</td> </tr> </tbody> </table> </div> </main> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}"></script> <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}"></script> <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}"></script> <!-- Icons --> <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}"></script> <script> feather.replace() </script> <!-- Graphs --> <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}"></script> <script> var ctx = document.getElementById("myChart"); var myChart = new Chart(ctx, { type: 'line', data: { labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], datasets: [{ data: [15339, 21345, 18483, 24003, 23489, 24092, 12034], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: false } }] }, legend: { display: false, } } }); </script> </body> </html>
注意点:所有页面的静态资源都需要使用 Thymeleaf 接管;@{}
7.3、国际化
国际化:internationalization(简称 i18n,以 i 为开头,n 为结尾,中间18个字母)
在资讯领域,国际化(i18n)指让产品(出版物,软件,硬件等)无需做大的改变就能够适应不同的语言和地区的需要。对程序来说,在不修改内部代码的情况下,能根据不同语言及地区显示相应的界面。
我们需要使前端页面国际化,在此之前IDEA需要先安装 Resource Bundle Editor 插件来实现 properties 文件的可视化界面
-
在 resource 下创建一个 i18n 的包,添加
login.properties
和login_zh_CN.properties
我们发现他们直接被整合起来 -
右键 Resource Bundle 'login' 可以直接创建 properties 文件
-
点开 properties,左下角是可视化配置
-
点开可视化界面,添加 tip,会发现右边有多个语言的配置框,输入不同的参数
-
我们会发现不同的 properties 里有着刚才输入不同的配置
login.properties
login.btn=登录 login.password=密码 login.remember=记住我 login.tip=请登录 login.username=用户名
login_en_US.properties
login.btn=Sign in login.password=Password login.remember=Remember me login.tip=Please sign in login.username=Username
login_zh_CN.properties
login.btn=登录 login.password=密码 login.remember=记住我 login.tip=请登录 login.username=用户名
-
在
application.properties
里配置我们文件的真实路径# 我们配置文件的真实位置 spring.messages.basename=i18n.login
-
修改首页的国际化部分(Thymeleaf获取国际化内容:
#{}
)例如: ============= <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> ============ <input type="text" class="form-control" th:placeholder="Username" required="" autofocus=""> 改成 <input type="text" class="form-control" th:placeholder="#{login.username}" required="" autofocus=""> ============ <input type="password" class="form-control" th:placeholder="Password" required=""> 改成 <input type="password" class="form-control" th:placeholder="#{login.password}" required=""> ============ <input type="checkbox" value="remember-me"> Remember me 改成 <input type="checkbox" value="remember-me"> [[#{login.remember}]] ============= <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> 改成 <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
-
修改首页下面的 a 标签
<a class="btn btn-sm" th:href="@{/index.html(lang='zh_CN')}">中文</a> <a class="btn btn-sm" th:href="@{/index.html(lang='en_US')}">English</a>
Thymeleaf 的携带参数用括号括起来
-
在 config 包下创建
MyLocaleResolver
public class MyLocaleResolver implements LocaleResolver { //解析请求 @Override public Locale resolveLocale(HttpServletRequest request) { //获取请求中的语言参数 String language = request.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 request, HttpServletResponse response, Locale locale) { } }
-
接着在容器
MyMvcConfig
中注册MyLocaleResolver
@Bean public LocaleResolver localeResolver() { return new MyLocaleResolver(); }
-
测试
注意点:
- 我们需要配置 i18n 文件
- 我们如果需要在项目中进行按钮自动切换,我们需要自定义一个组件
LocaleResolver
- 记得将自己写的组件配置到 spring 容器中
@Bean
#{}
使用国际化内容
7.4、登录功能实现
实现登录时对用户名密码判断,如果错误就返回错误信息,成功就重定向到首页
-
修改
index.html
,为用户名和密码输入框添加 name 属性,用来传参。并在主页设置一个 msg 来接收错误参数<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>Signin Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/asserts/css/signin.css}" rel="stylesheet"> </head> <body class="text-center"> <form class="form-signin" th:action="@{/user/login}"> <img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72"> <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1> <!--如果msg的值为空,则不显示消息--> <p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p> <label class="sr-only">Username</label> <input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus=""> <label class="sr-only">Password</label> <input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required=""> <div class="checkbox mb-3"> <label> <input type="checkbox" value="remember-me"> [[#{login.remember}]] </label> </div> <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button> <p class="mt-5 mb-3 text-muted">© 2017-2018</p> <a class="btn btn-sm" th:href="@{/index.html(lang='zh_CN')}">中文</a> <a class="btn btn-sm" th:href="@{/index.html(lang='en_US')}">English</a> </form> </body> </html>
-
编写一个 LoginController
//@Contriller + @ResponseBody = @RestController @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"; } } }
-
在自定义视图解析器 MyMvcConfig 中添加以下代码,用来实现登录后的重定向
registry.addViewController("/main.html").setViewName("dashboard");
7.5、登录拦截器
想要自定义拦截器,必须实现 HandlerInterceptor 接口
-
编写一个
LoginHandlerInterceptor
public class LoginHandlerInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //登录成功之后,应该有用户的Session if (request.getSession().getAttribute("loginUser") == null) { request.setAttribute("msg", "没有权限,请先登录"); request.getRequestDispatcher("/index.html").forward(request, response); return false; } else { return true; } } }
-
修改
LoginController
,将登录成功后的用户名放在 Session 中@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.setAttribute("loginUser", username); return "redirect:/main.html"; } else { // 告诉用户,登录失败 model.addAttribute("msg", "用户名或者密码错误"); return "index"; } }
-
把该拦截器注册到容器中,并设置拦截规则
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginHandlerInterceptor()) .addPathPatterns("/**").excludePathPatterns("/index.html", "/", "/user/login", "/asserts/**"); }
7.6、账号注销
通过移除 Session 的 loginUser 来实现账号注销
-
在
LoginController
中添加如下代码,@RequestMapping("/user/out") public String out(HttpSession session) { session.removeAttribute("loginUser"); return "redirect:/"; }
-
修改
dashboard.html
<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a> <a class="nav-link" th:href="@{/user/out}">Sign out</a>
7.7、展示员工列表
7.7.1、提取公共页面
提取公共页面有利于我们代码的复用,简化各个html的代码量
-
在 templates 下创建 commons 文件夹,在里面创建一个
commons.html
来存放公共页面(由于头部导航栏和侧边栏在各个页面都存在,我们可以直接提取出来当做公共页面)提取:th:fragment="topbar" th:fragment="siderbar(active)" 调用:th:replace="~{commons/commons :: topbar}" 如果要传递参数,可以直接使用()传参,接收判断即可 (传递不同参数) th:replace="~{commons/commons :: siderbar(active='main.html')}" th:replace="~{commons/commons :: siderbar(active='list.html')}" (判断参数内容) th:class="${active=='main.html' ? 'nav-link active' : 'nav-link'}" th:class="${active=='list.html' ? 'nav-link active' : 'nav-link'}"
commons.html
:<!DOCTYPE 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/out}">注销</a> </li> </ul> </nav> <!--侧边栏--> <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="siderbar(active)"> <div class="sidebar-sticky"> <ul class="nav flex-column"> <li class="nav-item"> <a th:class="${active=='main.html' ? 'nav-link active' : 'nav-link'}" th:href="@{/index.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> 首页 <span class="sr-only">(current)</span> </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"> <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> Orders </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-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 th:class="${active=='list.html' ? 'nav-link active' : 'nav-link'}" th:href="@{/emps}"> <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> 员工管理 </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-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" 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-layers"> <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon> <polyline points="2 17 12 22 22 17"></polyline> <polyline points="2 12 12 17 22 12"></polyline> </svg> Integrations </a> </li> </ul> <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> <span>Saved reports</span> <a class="d-flex align-items-center text-muted" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg> </a> </h6> <ul class="nav flex-column mb-2"> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Current month </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Last quarter </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Social engagement </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Year-end sale </a> </li> </ul> </div> </nav> </html>
-
将
list.html
放入 emp 文件夹下,在dashboard.html
和list.html
中调用公共页面dashboard.html
:<!DOCTYPE html> <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ --> <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>Dashboard Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet"> <style type="text/css"> /* Chart.js */ @-webkit-keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } @keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } .chartjs-render-monitor { -webkit-animation: chartjs-render-animation 0.001s; animation: chartjs-render-animation 0.001s; } </style> </head> <body> <div th:replace="~{commons/commons :: topbar}"></div> <div class="container-fluid"> <div class="row"> <!--传递参数active给组件--> <div th:replace="~{commons/commons :: siderbar(active='main.html')}"></div> <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4"> <div class="chartjs-size-monitor" style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: hidden; pointer-events: none; visibility: hidden; z-index: -1;"> <div class="chartjs-size-monitor-expand" style="position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;"> <div style="position:absolute;width:1000000px;height:1000000px;left:0;top:0"></div> </div> <div class="chartjs-size-monitor-shrink" style="position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;"> <div style="position:absolute;width:200%;height:200%;left:0; top:0"></div> </div> </div> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom"> <h1 class="h2">Dashboard</h1> <div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-group mr-2"> <button class="btn btn-sm btn-outline-secondary">Share</button> <button class="btn btn-sm btn-outline-secondary">Export</button> </div> <button class="btn btn-sm btn-outline-secondary dropdown-toggle"> <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-calendar"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> This week </button> </div> </div> <canvas class="my-4 chartjs-render-monitor" id="myChart" width="1076" height="454" style="display: block; width: 1076px; height: 454px;"></canvas> </main> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}" ></script> <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}" ></script> <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}" ></script> <!-- Icons --> <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}" ></script> <script> feather.replace() </script> <!-- Graphs --> <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}" ></script> <script> var ctx = document.getElementById("myChart"); var myChart = new Chart(ctx, { type: 'line', data: { labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], datasets: [{ data: [15339, 21345, 18483, 24003, 23489, 24092, 12034], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: false } }] }, legend: { display: false, } } }); </script> </body> </html>
list.html
:<!DOCTYPE html> <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ --> <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>Dashboard Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet"> <style type="text/css"> /* Chart.js */ @-webkit-keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } @keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } .chartjs-render-monitor { -webkit-animation: chartjs-render-animation 0.001s; animation: chartjs-render-animation 0.001s; } </style> </head> <body> <div th:replace="~{commons/commons :: topbar}"></div> <div class="container-fluid"> <div class="row"> <!--插入抽取的部分--> <div th:replace="~{commons/commons :: siderbar(active='list.html')}"></div> <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> </tr> </thead> <tbody> </tbody> </table> </div> </main> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}"></script> <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}"></script> <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}"></script> <!-- Icons --> <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}"></script> <script> feather.replace() </script> <!-- Graphs --> <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}"></script> <script> var ctx = document.getElementById("myChart"); var myChart = new Chart(ctx, { type: 'line', data: { labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], datasets: [{ data: [15339, 21345, 18483, 24003, 23489, 24092, 12034], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: false } }] }, legend: { display: false, } } }); </script> </body> </html>
7.7.2、修改员工展示列表
-
编写 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"; } }
-
修改点击侧边栏:员工管理 发出的请求为
th:href="@{/emps}"
-
修改
list.html
中列表 main<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> <tr th:each="emp : ${emps}"> <td th:text="${emp.getId()}"></td> <td th:text="${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="${#dates.format(emp.getBirth(), 'yyyy-MM-dd HH:mm:ss')}"></td> <td> <button class="btn btn-sm btn-primary">编辑</button> <button class="btn btn-sm btn-danger">删除</button> </td> </tr> </tbody> </table> </div> </main>
-
效果
7.8、增加员工实现
-
在
list.html
增加添加按钮<h2><a class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a></h2>
-
在 EmployeeController 类里加入跳转请求
@GetMapping("/emp") public String toAddPage(Model model) { //查出所有部门的信息 Collection<Department> departments = departmentDao.getDepartments(); model.addAttribute("departments", departments); return "emp/add"; }
注意:这里是 Get 方法
-
在 templates/emp 下编写一个
add.html
<!DOCTYPE html> <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ --> <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>Dashboard Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet"> <style type="text/css"> /* Chart.js */ @-webkit-keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } @keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } .chartjs-render-monitor { -webkit-animation: chartjs-render-animation 0.001s; animation: chartjs-render-animation 0.001s; } </style> </head> <body> <div th:replace="~{commons/commons :: topbar}"></div> <div class="container-fluid"> <div class="row"> <!--插入抽取的部分--> <div th:replace="~{commons/commons :: siderbar(active='list.html')}"></div> <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>Last Name</label> <input type="text" name="lastName" class="form-control" placeholder="Dt"> </div> <div class="form-group"> <label>Email</label> <input type="email" name="email" class="form-control" placeholder="746294093@qq.com"> </div> <div class="form-group"> <label>Gender</label><br> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="gender" value="1"> <label class="form-check-label">男</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="gender" value="0"> <label class="form-check-label">女</label> </div> </div> <div class="form-group"> <label>Department</label> <!--我们在controller接收的是一个Employee,所以我们需要提交的是其中的一个属性--> <select class="form-control" name="department.id"> <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="yyyy/MM/dd or yyyy/MM/dd HH:mm:ss"> </div> <button type="submit" class="btn btn-primary">添加</button> </form> </main> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}"></script> <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}"></script> <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}"></script> <!-- Icons --> <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}"></script> <script> feather.replace() </script> <!-- Graphs --> <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}"></script> <script> var ctx = document.getElementById("myChart"); var myChart = new Chart(ctx, { type: 'line', data: { labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], datasets: [{ data: [15339, 21345, 18483, 24003, 23489, 24092, 12034], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: false } }] }, legend: { display: false, } } }); </script> </body> </html>
-
在 EmployeeController 类里加入增加员工请求
@PostMapping("/emp") public String addEmp(Employee employee) { employeeDao.save(employee); //调用底层业务方法保存员工的信息 return "redirect:/emps"; }
-
我们可以在
application.properties
中设置读取日期的格式# 格式化时间格式 spring.mvc.format.date=yyyy-MM-dd
7.9、修改员工信息
-
修改
list.html
的编辑按钮为 a 标签<a class="btn btn-sm btn-primary" th:href="@{/emp/} + ${emp.getId()}">编辑</a>
-
在 EmployeeController 类里加入跳转请求
@GetMapping("/emp/{id}") public String toUpdateEmp(@PathVariable("id") Integer id, Model model) { //根据id查出原来的数据 Employee employee = employeeDao.getEmployeeById(id); model.addAttribute("emp", employee); //查出所有部门的信息 Collection<Department> departments = departmentDao.getDepartments(); model.addAttribute("departments", departments); return "emp/update"; }
注意:这里是 Get 方法
-
在 templates/emp 下编写一个
update.html
<!DOCTYPE html> <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ --> <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>Dashboard Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet"> <style type="text/css"> /* Chart.js */ @-webkit-keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } @keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } .chartjs-render-monitor { -webkit-animation: chartjs-render-animation 0.001s; animation: chartjs-render-animation 0.001s; } </style> </head> <body> <div th:replace="~{commons/commons :: topbar}"></div> <div class="container-fluid"> <div class="row"> <!--插入抽取的部分--> <div th:replace="~{commons/commons :: siderbar(active='list.html')}"></div> <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4"> <form th:action="@{/emp}" method="post"> <input type="hidden" th:value="${emp.getId()}" name="id"> <div class="form-group"> <label>Last Name</label> <input type="text" th:value="${emp.getLastName()}" name="lastName" class="form-control" placeholder="Dt"> </div> <div class="form-group"> <label>Email</label> <input type="email" th:value="${emp.getEmail()}" name="email" class="form-control" placeholder="746294093@qq.com"> </div> <div class="form-group"> <label>Gender</label><br> <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> <!--我们在controller接收的是一个Employee,所以我们需要提交的是其中的一个属性--> <select class="form-control" name="department.id"> <option th:selected="${emp.getDepartment().getId()==dept.getId()}" 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" th:value="${#dates.format(emp.getBirth(), 'yyyy/MM/dd HH:mm:ss')}" name="birth" class="form-control" placeholder="yyyy/MM/dd or yyyy/MM/dd HH:mm:ss"> </div> <button type="submit" class="btn btn-primary">添加</button> </form> </main> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}"></script> <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}"></script> <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}"></script> <!-- Icons --> <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}"></script> <script> feather.replace() </script> <!-- Graphs --> <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}"></script> <script> var ctx = document.getElementById("myChart"); var myChart = new Chart(ctx, { type: 'line', data: { labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], datasets: [{ data: [15339, 21345, 18483, 24003, 23489, 24092, 12034], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: false } }] }, legend: { display: false, } } }); </script> </body> </html>
-
由于方法的复用,我们不需要写额外的请求方法来修改员工信息,直接调用增加员工的请求,效果是一致的。
因为dao层模拟数据库用的是Map,而Map的特点是:主键唯一。所以只要保持主键一致就能达到覆盖修改的效果,就不需要再单独编写新的方法
7.10、删除员工实现
-
修改
list.html
的删除按钮为 a 标签<a class="btn btn-sm btn-danger" th:href="@{/deleteEmp/} + ${emp.getId()}">删除</a>
-
在 EmployeeController 类里加入增加员工请求
@GetMapping("/deleteEmp/{id}") public String deleteEmp(@PathVariable("id") Integer id) { employeeDao.deleteEmployeeById(id); return "redirect:/emps"; }
7.11、404处理
-
在templates下建立一个error文件夹,把
404.html
放进去springboot会自动帮我们配置处理,在出现404的时候会自动跳转到我们写好的
404.html
中 -
修改
404.html
的公共页面<!DOCTYPE html> <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ --> <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>Dashboard Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet"> <style type="text/css"> /* Chart.js */ @-webkit-keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } @keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } .chartjs-render-monitor { -webkit-animation: chartjs-render-animation 0.001s; animation: chartjs-render-animation 0.001s; } </style> </head> <body> <div th:replace="~{commons/commons :: topbar}"></div> <div class="container-fluid"> <div class="row"> <!--插入抽取的部分--> <div th:replace="~{commons/commons :: siderbar(active='404.html')}"></div> <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4"> <h1>404</h1> </main> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}" ></script> <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}" ></script> <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}" ></script> <!-- Icons --> <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}" ></script> <script> feather.replace() </script> <!-- Graphs --> <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}" ></script> <script> var ctx = document.getElementById("myChart"); var myChart = new Chart(ctx, { type: 'line', data: { labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], datasets: [{ data: [15339, 21345, 18483, 24003, 23489, 24092, 12034], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: false } }] }, legend: { display: false, } } }); </script> </body> </html>
8、如何写一个网站
前端:
- 模板:别人写好的,我们拿来改成自己需要的
- 框架:组件:我们自己手动拼接! Bootstrap,Layui,semantic-ui,element-ui
- 栅格系统
- 导航栏
- 侧边栏
- 表单
8.1、开发网站心得
- 前端搞定:页面长什么样:数据
- 设计数据库(数据库设计难点!)
- 前端让他能够自动运行,独立化工程
- 数据接口如何对接:json,对象 all in one!
- 前后端联调测试!‘
小心得
- 有一套自己熟悉的后台模板:工作必要!——>X-admin
- 前端界面:至少自己能够通过前端框架,组合出来一个网站页面
- index - about - blog - post - user
- 让这个页面能够独立运行!
8.2、后面的学习内容
- 整合JDBC
- 整合Mybatis 重点
- 整合Druid 重点
- Shiro:安全 重点
- Spring Security:安全 重点
- 异步任务~,邮件发送,定时任务
- Swagger
- Dubbo + Zookeeper
9、SpringBoot整合
9.1、整合JDBC
9.1.1、简介
对于数据访问层,无论是 SQL(关系型数据库) 还是 NOSQL(非关系型数据库),Spring Boot 底层都是采用 Spring Data 的方式进行统一处理。
Spring Boot 底层都是采用 Spring Data 的方式进行统一处理各种数据库,Spring Data 也是 Spring 中与 Spring Boot、Spring Cloud 等齐名的知名项目。
Sping Data 官网:https://spring.io/projects/spring-data
数据库相关的启动器 :可以参考官方文档:
https://docs.spring.io/spring-boot/docs/2.6.7/reference/htmlsingle/#using.build-systems.starters
9.1.2、JDBC
去新建一个项目测试:springboot-04-data;引入相关模块!基础模块(这里也要勾选Spring-Web)
项目建好后,发现自动帮我们导入了如下的启动器
<!--JDBC-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--MySQL-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
application.yml
配置文件连接数据库
spring:
datasource:
username: root
password: 123456
# &serverTimezone=UTC 解决时区的报错
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8
# com.mysql.jdbc.Driver MySQL5.0+
# com.mysql.cj.jdbc.Driver MySQL8.0+兼容5.0+
driver-class-name: com.mysql.cj.jdbc.Driver
测试类测试
@SpringBootTest
class Springboot04DataApplicationTests {
//DI注入数据源
@Autowired
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
//查看默认的数据源
System.out.println(dataSource.getClass());
//获得数据库连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
//关闭连接
connection.close();
}
}
小结
Spring Boot 2.6.7 默认使用HikariDataSource 数据源,而以前版本,如 Spring Boot 1.5 默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源
HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀;
可以使用 spring.datasource.type 指定自定义的数据源类型,值为 要使用的连接池实现的完全限定名。
关于数据源我们并不做介绍,有了数据库连接,显然就可以 CRUD 操作数据库了。但是我们需要先了解一个对象 JdbcTemplate
9.1.3、JDBCTemplate
1、有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库;
2、即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。
3、数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。
4、Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用
5、JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateAutoConfiguration 类
JdbcTemplate主要提供以下几类方法:
- execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
- update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
- query方法及queryForXXX方法:用于执行查询相关语句;
- call方法:用于执行存储过程、函数相关语句。
测试
JdbcController类
@RestController
public class JdbcController {
@Autowired
JdbcTemplate jdbcTemplate;
//查询数据库的所有信息
//没有实体类,数据库中的东西,怎么获取? Map
@GetMapping("/userList")
public List<Map<String, Object>> userList() {
String sql = "select * from mybatis.user";
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
return maps;
}
@GetMapping("/addUser")
public String addUser() {
String sql = "insert into mybatis.user(id,name,pwd) values (6,'小明','123456')";
jdbcTemplate.update(sql);
return "add-ok";
}
@GetMapping("/updateUser/{id}")
public String updateUser(@PathVariable("id") int id) {
String sql = "update mybatis.user set name = ?,pwd = ? where id = " + id;
//封装
Object[] objects = new Object[2];
objects[0] = "大明";
objects[1] = "654321";
jdbcTemplate.update(sql, objects);
return "update-ok";
}
@GetMapping("/deleteUser/{id}")
public String deleteUser(@PathVariable("id") int id) {
String sql = "delete from mybatis.user where id = ?";
jdbcTemplate.update(sql, id);
return "delete-ok";
}
}
- 进行测试即可完成CURD的基本操作!
9.2、整合Druid数据源
9.2.1、简介
Java程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。
Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。
Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。
Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。
Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控。
Github地址:https://github.com/alibaba/druid/
com.alibaba.druid.pool.DruidDataSource 基本配置参数如下:
9.2.2、配置数据源
添加上 Druid 数据源依赖
<!--Druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.9</version>
</dependency>
查看项目依赖,导入成功
在 application.yaml
里配置数据源
spring:
datasource:
username: root
password: 123456
# &serverTimezone=UTC 解决时区的报错
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8
# com.mysql.jdbc.Driver MySQL5.0+
# com.mysql.cj.jdbc.Driver MySQL8.0+兼容5.0+
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
#拓展功能
#Spring Boot 默认是不注入这些属性值的,需要自己绑定
#druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#特定功能
#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
我们还需要导入 Log4j 的依赖
<!--Log4j-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
DruidConfig 配置类
-
现在需要程序员自己为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了;我们需要自己添加 DruidDataSource 组件到容器中,并绑定属性
@Configuration public class DruidConfig { @ConfigurationProperties(prefix = "spring.datasource") @Bean public DataSource druidDataSource() { return new DruidDataSource(); } }
-
配置Druid数据源监控
Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的 web 页面。所以第一步需要设置 Druid 的后台管理页面,比如 登录账号、密码 等;配置后台管理;
//后台监控:web.xml ServletRegistrationBean //因为SpringBoot 内置了 servlet 容器,所以没有 web.xml,所以只能替代方法:ServletRegistrationBean @Bean public ServletRegistrationBean statViewServlet() { ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); //后台需要有人登录,账号密码配置 HashMap<String , String > initParameters = new HashMap<>(); //增加配置 initParameters.put("loginUsername", "admin"); //登录的key是固定的:loginUsername loginUsername initParameters.put("loginPassword", "123456"); //为空表示允许谁可以访问 initParameters.put("allow", ""); //只允许本机访问 initParameters.put("allow", "localhost"); //禁止谁能访问 initParameters.put("kuangshen", "192.168.11.123"); bean.setInitParameters(initParameters); //设置初始化参数 return bean; }
访问后台路径:http://localhost:8081/druid/
-
配置 Druid web 监控 filter 过滤器
//filter //WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计 @Bean public FilterRegistrationBean webStatFilter() { FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>(); bean.setFilter(new WebStatFilter()); //可以过滤哪些请求呢 HashMap<String , String > initParameters = new HashMap<>(); //exclusions:设置哪些请求进行过滤排除掉,从而不进行统计 initParameters.put("exclusions", "*.js,*.css,/druid/**"); bean.setInitParameters(initParameters); return bean; }
平时在工作中,按需求进行配置即可,主要用作监控!
9.3、整合Mybatis
9.3.1、简介
官方文档:http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
Maven仓库地址:https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter/2.1.1
9.3.2、测试
导入 MyBatis 所需要的依赖
<!--mybatis-spring-boot-starter-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
在application.yml
里编写数据库配置
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
- 在 test 里测试是否连上数据库
创建一个 User 实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
创建一个 UserMapper 类
@Mapper //表示是一个mybatis的mapper类
@Repository
public interface UserMapper {
List<User> queryUserList();
User queryUserById(int id);
int addUser(User user);
int updateUser(User user);
int deleteUser(int id);
}
创建以下结构文件
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dt.mapper.UserMapper">
<select id="queryUserList" resultType="User">
select * from mybatis.user
</select>
<select id="queryUserById" resultType="User">
select * from mybatis.user where id = #{id}
</select>
<insert id="addUser" parameterType="User">
insert into mybatis.user(id, name, pwd)
values (#{id}, #{name}, #{pwd});
</insert>
<update id="updateUser" parameterType="User">
update mybatis.user
<set>
<if test="name != null">name = #{name},</if>
<if test="pwd != null">pwd = #{pwd}</if>
</set>
where id = #{id}
</update>
<delete id="deleteUser">
delete from mybatis.user where id = #{id}
</delete>
</mapper>
在 application.yml
中配置 Mybatis
mybatis:
type-aliases-package: com.dt.pojo
mapper-locations: classpath:mybatis/mapper/*.xml
创建一个 UserController 类
@RestController
public class UserController {
//这里省略了service层,是为了方便教学
@Autowired
private UserMapper userMapper;
@GetMapping("/queryUserList")
public List<User> queryUserList() {
return userMapper.queryUserList();
}
@GetMapping("/queryUserById/{id}")
public User queryUserById(@PathVariable("id") int id) {
return userMapper.queryUserById(id);
}
@GetMapping("/addUser")
public String addUser(User user) {
userMapper.addUser(new User(6, "小明", "123456"));
return "add-ok";
}
@GetMapping("/updateUser")
public String updateUser(User user) {
userMapper.updateUser(new User(6, "大明", "654321"));
return "update-ok";
}
@GetMapping("/deleteUser/{id}")
public String deleteUser(@PathVariable("id") int id) {
userMapper.deleteUser(id);
return "delete-ok";
}
}
测试是否成功
10、SpringSecurity
10.1、SpringSecurity简介
Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
- WebSecurityConfigurerAdapter:自定义Security策略
- AuthenticationManagerBuilder:自定义认证策略
- @EnableWebSecurity:开启WebSecurity模式
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
“授权” (Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
这个概念是通用的,而不是只在Spring Security 中存在。
web安全简介
在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。
市面上存在比较有名的:Shiro,Spring Security !
Shiro 与 Spring Security:相似,除了类不一样,名字不一样
作用:认证,授权
10.2、SpringSecurity环境搭建
-
创建一个新项目
springboot-06-springsecurity
,添加web模块、Thymeleaf模块、SpringSecurity模块或者自己添加依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency>
-
导入静态资源:
资源地址:https://gitee.com/KyDestroy/spring-boot-study/tree/master
注意:由于semantic-ui官网域名改变,html中引用的js链接
https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css
一律改成https://cdn.bootcdn.net/ajax/libs/semantic-ui/2.4.1/semantic.min.css
-
在
application.properties
中关闭模板引擎的缓存,方便调试spring.thymeleaf.cache=false
-
创建一个 RouterController 类
@Controller public class RouterController { @RequestMapping({"/","/index"}) public String index() { return "index"; } @RequestMapping("/toLogin") public String toLogin() { return "views/login"; } //通过动态传参整合了9个页面的跳转 @RequestMapping("/level{num}/{id}") public String level(@PathVariable("num") int num, @PathVariable("id") int id) { return "views/level" + num + "/" + id; } }
10.3、认证授权和认证
目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能
-
确保引入 Spring Security 模块
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
-
编写 Spring Security 配置类
-
可以参考https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#:~:text=The%20custom%20DSL%20can%20then%20be%20used%20like%20this%3A来编写
-
需要基础
WebSecurityConfigurerAdapter
并重写configure(HttpSecurity http)
和configure(AuthenticationManagerBuilder auth)
方法,使用方法可以直接点开源码查看注释
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { //授权 @Override protected void configure(HttpSecurity http) throws Exception { //首页所有人可以访问,功能页只有对应有权限的人才能访问 //请求授权的规则 http.authorizeRequests() .antMatchers("/").permitAll() .antMatchers("/level1/**").hasRole("vip1") .antMatchers("/level2/**").hasRole("vip2") .antMatchers("/level3/**").hasRole("vip3"); //没有权限默认会到登录页面,需要开启登录的页面 http.formLogin(); } //认证 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //这些数据正常应该从数据库中读 //Spring security 5.0中新增了多种加密方式,也改变了密码的格式。 //要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密 //spring security 官方推荐的是使用 bcrypt加密方式 auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) .withUser("dt1").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1") .and() .withUser("dt2").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2") .and() .withUser("dt3").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3"); } }
-
-
测试,发现,登录成功,并且每个角色只能访问自己认证下的规则!
-
如果需要用数据库来配置认证,就如官网的说明
10.4、注销及权限控制
10.4.1、注销
-
在 SecurityConfig 类里设置注销
//授权 @Override protected void configure(HttpSecurity http) throws Exception { ... //开启了注销功能,跳到首页 http.logout().logoutSuccessUrl("/"); }
-
在 index.html 页面添加以下代码
<!--注销--> <a class="item" th:href="@{/logout}"> <i class="sign-out icon"></i> 注销 </a>
-
测试成功,注销后自动跳回首页
10.4.2、权限控制
-
导入 security-thymeleaf 整合包依赖
<!--security-thymeleaf整合包--> <!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version> </dependency>
注意:高版本 springboot 用的是
thymeleaf-extras-springsecurity5
而不是thymeleaf-extras-springsecurity4
-
修改 index.html 导航栏
-
导入命名空间
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
-
增加认证判断
<div class="ui segment" id="index-header-nav" th:fragment="nav-menu"> <div class="ui secondary menu"> <a class="item" th:href="@{/index}">首页</a> <!--登录注销--> <div class="right menu"> <!--如果未登录--> <div sec:authorize="!isAuthenticated()"> <a class="item" th:href="@{/toLogin}"> <i class="address card icon"></i> 登录 </a> </div> <!--如果登录:用户名--> <div sec:authorize="isAuthenticated()"> <a class="item"> 用户名:<span sec:authentication="principal.username"></span> 角色:<span sec:authentication="principal.authorities"></span> </a> </div> <!--如果登录:注销--> <div sec:authorize="isAuthenticated()"> <a class="item" th:href="@{/logout}"> <i class="sign-out icon"></i> 注销 </a> </div> </div> </div> </div>
-
角色功能块认证
sec:authorize="hasRole('xxx')"
<!--菜单根据用户的角色动态的实现--> <div class="column" sec:authorize="hasRole('vip1')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 1</h5> <hr> <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div> <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div> <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div> </div> </div> </div> </div> <div class="column" sec:authorize="hasRole('vip2')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 2</h5> <hr> <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div> <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div> <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div> </div> </div> </div> </div> <div class="column" sec:authorize="hasRole('vip3')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 3</h5> <hr> <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div> <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div> <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div> </div> </div> </div> </div>
-
-
SecurityConfig 配置类
//防止网站攻击,关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求 http.csrf().disable(); //开启了注销功能,跳到首页 http.logout().logoutSuccessUrl("/");
-
测试
-
vip1
-
vip2
-
vip3
-
10.5、记住我及首页定制
10.5.1、"记住我"功能实现
-
SecurityConfig 配置类
- 在 SecurityConfig 配置类中的 configure 方法中定义方法
//授权 @Override protected void configure(HttpSecurity http) throws Exception { ... //开启 记住我 功能 http.rememberMe(); }
-
运行测试
- 登录时点击记住我功能,之后关闭浏览器,然后重新打开浏览器访问,发现用户依旧存在,因为spring security帮我们将登录信息存储在Cookie中14天!!!
- 点击注销时,spring security 帮我们自动删除了这个 Cookie!
10.5.2、定制登录页功能实现
-
SecurityConfig 配置类
- 在 SecurityConfig 配置类中的 configure 方法中定义方法
//授权 @Override protected void configure(HttpSecurity http) throws Exception { ... //没有权限默认会到登录页面,需要开启登录的页面 //loginPage() 定制登录页 http.formLogin().loginPage("/toLogin"); ... }
-
login.html
更改- 表单的账号密码部分
<form th:action="@{/login}" method="post"> <div class="field"> <label>Username</label> <div class="ui left icon input"> <input type="text" placeholder="Username" name="username"> <i class="user icon"></i> </div> </div> <div class="field"> <label>Password</label> <div class="ui left icon input"> <input type="password" name="password"> <i class="lock icon"></i> </div> </div> <div class="field"> <div class="ui checkbox"> <input type="checkbox" name="remember"> <label>记住我</label> </div> </div> <input type="submit" class="ui blue submit button"/> </form>
-
修改 SecurityConfig 配置类
//授权 @Override protected void configure(HttpSecurity http) throws Exception { ... //没有权限默认会到登录页面,需要开启登录的页面 //loginPage() 定制登录页 //loginProcessingUrl() 处理表单请求 //usernameParameter() 指定接收的用户名参数名 //passwordParameter() 指定接收的密码参数名 http.formLogin() .usernameParameter("username") .passwordParameter("password") .loginPage("/toLogin") .loginProcessingUrl("/login"); ... //开启 记住我 功能 //rememberMeParameter() 指定接收的记住我参数名 http.rememberMe().rememberMeParameter("remember"); }
-
测试
-
登录页面
-
登录成功后进入首页
-
总结:Spring Security 的使用多看源码
11、Shiro
11.1、Shiro简介
11.1.1、什么是Shiro
- Apache Shiro是一个Java 的安全(权限)框架。
- Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。
- Shiro可以完成,认证,授权,加密,会话管理,Web集成,缓存等.
- 下载地址: http://shiro.apache.org/
11.1.2、Shiro的功能
- Authentication: 身份认证、登录,验证用户是不是拥有相应的身份;
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能否进行什么操作,如:验证某个用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限!
- Session Manager: 会话管理,即用户登录后就是第1次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通的JavaSE环境,也可以是Web环境;
- Cryptography: 加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储;
- Web Support: Web支持,可以非常容易的集成到Web环境;
- Caching: 缓存,比如用户登录后,其用户信息,拥有的角色、权限不必每次去查,这样可以提高效率
- Concurrency: Shiro支持多线程应用的并发验证,即,如在一个线程中开启另-一个线程,能把权限自动的传 播过去
- Testing:提供测试支持;
- RunAs:允许一个用户假装为另-一个用户(如果他们允许)的身份进行访问;
- Remember Me:记住我,这个是非常常见的功能,即一次登录后, 下次再来的话不用登录了
11.1.3、Shiro架构
-
外部结构
从外部来看
Shiro
,即从应用程序角度来观察如何使用shiro
完成工作:- subject: 应用代码直接交互的对象是Subject, 也就是说Shiro的对外API核心就是Subject, Subject代表了当前的用户,这个用户不-定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等,与Subject的所有交互都会委托给SecurityManager; Subject其实是一一个门面, SecurityManageer 才是 实际的执行者
- SecurityManager: 安全管理器,即所有与安全有关的操作都会与SercurityManager交互, 并且它管理着所有的Subject,可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC的DispatcherServlet的角色
- Realm: Shiro从Realm获取安全数据 (如用户,角色,权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较,来确定用户的身份是否合法;也需要从Realm得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把Realm看DataSource
-
内部结构
- Subject: 任何可以与应用交互的用户;
- Security Manager:相当于SpringMVC中的DispatcherSerlet; 是Shiro的心脏, 所有具体的交互都通过Security Manager进行控制,它管理者所有的Subject, 且负责进行认证,授权,会话,及缓存的管理。
- Authenticator:负责Subject认证, 是-一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
- Authorizer:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中 的那些功能;
- Realm: 可以有-一个或者多个的realm, 可以认为是安全实体数据源,即用于获取安全实体的,可以用JDBC实现,也可以是内存实现等等,由用户提供;所以- -般在应用中都需要实现自己的realm
- SessionManager:管理Session生 命周期的组件,而Shiro并不仅仅可以用在Web环境,也可以用在普通的JavaSE环境中
- CacheManager: 缓存控制器,来管理如用户,角色,权限等缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能;
- Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于密码加密, 解密等
11.2、Shiro快速开始
11.2.1、准备工作
官方项目路径:shiro/pom.xml at main · apache/shiro · GitHub
-
创建一个普通的maven项目,然后删除
src
目录,创建一个新model——>hello-shiro -
pom.xml
<dependencies> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.9.0</version> </dependency> <!-- configure logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.36</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.17.2</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.17.2</version> </dependency> </dependencies>
-
log4j2.xml
和shiro.ini
-
log4j2.xml
<Configuration name="ConfigTest" status="ERROR" monitorInterval="5"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> </Appenders> <Loggers> <Logger name="org.springframework" level="warn" additivity="false"> <AppenderRef ref="Console"/> </Logger> <Logger name="org.apache" level="warn" additivity="false"> <AppenderRef ref="Console"/> </Logger> <Logger name="net.sf.ehcache" level="warn" additivity="false"> <AppenderRef ref="Console"/> </Logger> <Logger name="org.apache.shiro.util.ThreadContext" level="warn" additivity="false"> <AppenderRef ref="Console"/> </Logger> <Root level="info"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>
-
shiro.ini
[users] # user 'root' with password 'secret' and the 'admin' role root = secret, admin # user 'guest' with the password 'guest' and the 'guest' role guest = guest, guest # user 'presidentskroob' with password '12345' ("That's the same combination on # my luggage!!!" ;)), and role 'president' presidentskroob = 12345, president # user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz' darkhelmet = ludicrousspeed, darklord, schwartz # user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz' lonestarr = vespa, goodguy, schwartz # ----------------------------------------------------------------------------- # Roles with assigned permissions # # Each line conforms to the format defined in the # org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc # ----------------------------------------------------------------------------- [roles] # 'admin' role has all permissions, indicated by the wildcard '*' admin = * # The 'schwartz' role can do anything (*) with any lightsaber: schwartz = lightsaber:* # The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with # license plate 'eagle5' (instance specific id) goodguy = winnebago:drive:eagle5
-
-
java包下的Quickstart运行类
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.text.IniRealm; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Quickstart { private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class); public static void main(String[] args) { DefaultSecurityManager defaultSecurityManager=new DefaultSecurityManager(); IniRealm iniRealm=new IniRealm("classpath:shiro.ini"); defaultSecurityManager.setRealm(iniRealm); SecurityUtils.setSecurityManager(defaultSecurityManager); // 通过 SecurityUtils 获得当前用户的对象 subject Subject currentUser = SecurityUtils.getSubject(); // 通过当前用户拿到Session Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("Subject=>session [" + value + "]"); } // 判断当前的用户是否被认证 if (!currentUser.isAuthenticated()) { // Token:令牌 UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); token.setRememberMe(true); //设置记住我 try { currentUser.login(token); //执行登录操作 } catch (UnknownAccountException uae) { log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } // ... catch more exceptions here (maybe custom ones specific to your application? catch (AuthenticationException ae) { //unexpected condition? error? } } // 打印他们的识别主体(在本例中为用户名): log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); //test a role: if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } // 粗粒度 //test a typed permission (not instance-level) if (currentUser.isPermitted("lightsaber:wield")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } // 细粒度 //a (very powerful) Instance Level permission: if (currentUser.isPermitted("winnebago:drive:eagle5")) { log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " + "Here are the keys - have fun!"); } else { log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!"); } // 注销 //all done - log out! currentUser.logout(); // 结束 System.exit(0); } }
-
运行结果
11.2.2、Quickstart类简述原理
-
将在SpringBoot中集成~
//1、获取当前用户的对象 Subject Subject currentUser = SecurityUtils.getSubject(); //2、通过当前用户拿到Session Session session = currentUser.getSession(); //3、判断当前的用户是否被认证 currentUser.isAuthenticated() //4、获得当前用户的认证 currentUser.getPrincipal() //5、当前用户是否有某个角色 currentUser.hasRole("schwartz") //6、根据不同的角色获取不同的权限 currentUser.isPermitted("lightsaber:wield") //7、注销 currentUser.logout();
11.3、SpringBoot整合Shiro环境搭建
11.3.1、基础配置
-
新建SpringBoot项目,勾选
web
和thymeleaf
-
templates
下新建index.html
<!DOCTYPE html> <html lang="en" xmlns:th="https://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>首页</h1> <p th:text="${msg}"></p> </body> </html>
-
controller
包下新建MyController
package com.dt.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; /** * Created with IntelliJ IDEA. * * @author Dt * @Project springboot-08-shiro * @Title: MyController * @Package com.dt.controller * @Description: TODO * @date 2022/6/6 22:08 */ @Controller public class MyController { @RequestMapping({"/", "/index"}) public String toIndex(Model model) { model.addAttribute("msg", "Hello,Shiro!"); return "index"; } }
-
测试
11.3.2、测试
-
shiro-spring的maven包
<!--Shiro整合Spring的包--> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.9.0</version> </dependency>
-
配置类
-
UserRealm
package com.dt.config; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; /** * Created with IntelliJ IDEA. * * @author Dt * @Project springboot-08-shiro * @Title: UserRealm * @Package com.dt.config * @Description: TODO * @date 2022/6/6 22:38 */ // 自定义的Realm public class UserRealm extends AuthorizingRealm { // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了=>授权doGetAuthorizationInfo"); return null; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行了=>认证doGetAuthorizationInfo"); return null; } }
-
ShiroConfig
package com.dt.config; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * Created with IntelliJ IDEA. * * @author Dt * @Project springboot-08-shiro * @Title: ShiroConfig * @Package com.dt.config * @Description: TODO * @date 2022/6/6 22:33 */ @Configuration public class ShiroConfig { //ShiroFilterFactoryBean 第三步 @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 设置安全管理器 bean.setSecurityManager(defaultWebSecurityManager); return bean; } //DefaultWebSecurityManager 第二步 @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 关联UserRealm securityManager.setRealm(userRealm); return securityManager; } //创建 realm 对象,需要自定义 第一步 @Bean public UserRealm userRealm() { return new UserRealm(); } }
-
-
导入静态资源
-
add 和 update页面只有一个普通的h1标签
-
index.html
<!DOCTYPE html> <html lang="en" xmlns:th="https://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>首页</h1> <p th:text="${msg}"></p> <a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a> </body> </html>
-
-
MyController控制类添加跳转
@RequestMapping("/user/add") public String add() { return "user/add"; } @RequestMapping("/user/update") public String update() { return "user/update"; }
-
测试
- 可以正常地进行页面跳转!为后面做准备
11.4、Shiro实现登录拦截
具体实现
-
ShiroConfig类
-
在 ShiroFilterFactoryBean 方法中加入下面代码
// 添加Shiro的内置过滤器 /* anno:无需认证就可以访问 authc:必须认证才能访问 user:必须拥有 记住我 功能才能用 perms:拥有对某个资源的权限才能访问 role:拥有某个角色权限才能访问 */ Map<String, String> filterMap = new LinkedHashMap<>(); // filterMap.put("/user/add", "authc"); // filterMap.put("/user/update", "authc"); filterMap.put("/user/*", "authc"); bean.setFilterChainDefinitionMap(filterMap); // 设置登录的请求 bean.setLoginUrl("/toLogin");
-
-
MyController类中添加跳转事务
@RequestMapping("/toLogin") public String toLogin() { return "login"; }
-
在templates包下添加login页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>登录</h1> <hr> <form action=""> <p>用户名:<input type="text" name="username"></p> <p>密码:<input type="password" name="password"></p> <p><input type="submit"></p> </form> </body> </html>
-
测试
-
点击update时
-
页面被拦截,回到登录页面
-
11.5、Shiro实现用户认证
实现流程
-
UserRealm类
-
认证方法
// 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行了=>认证doGetAuthorizationInfo"); // 用户名,密码 可以数据库中取 String name = "root"; String password = "123456"; UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken; if (!userToken.getUsername().equals(name)) { return null; // 抛出异常 UnknownAccountException } // 密码认证,shiro做 return new SimpleAuthenticationInfo("",password,""); }
-
-
MyController页面跳转类
@RequestMapping("/login") public String login(String username, String password, Model model) { // 获取当前的用户 Subject subject = SecurityUtils.getSubject(); // 封装用户的登录数据 UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { subject.login(token); // 执行登录方法,如果没有异常就说明ok了 return "index"; } catch (UnknownAccountException e) { // 用户名不存在 model.addAttribute("msg", "用户名不存在"); return "login"; } catch (IncorrectCredentialsException e) { // 密码错误 model.addAttribute("msg", "密码错误"); return "login"; } }
-
更改
login.html
<!DOCTYPE html> <html lang="en" xmlns:th="https://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>登录</h1> <hr> <p th:text="${msg}" style="color: red"></p> <form th:action="@{/login}"> <p>用户名:<input type="text" name="username"></p> <p>密码:<input type="password" name="password"></p> <p><input type="submit"></p> </form> </body> </html>
-
测试
-
当用户成功登录后才有权限使用
add
和update
功能原理:调用 Shiro 中的认证功能!
-
11.6、Shiro整合Mybatis
11.6.1、项目框架
-
项目结构
-
数据库的user表
11.6.2、项目实现流程
-
maven导入包
<!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.9</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
-
在
application.properties
配置别名和mapper位置mybatis.type-aliases-package=com.dt.pojo mybatis.mapper-locations=classpath:mapper/*.xml
-
在
application.yml
配置 druidspring: datasource: username: root password: 123456 # &serverTimezone=UTC 解决时区的报错 url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8 # com.mysql.jdbc.Driver MySQL5.0+ # com.mysql.cj.jdbc.Driver MySQL8.0+兼容5.0+ driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #拓展功能 #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true #特定功能 #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
-
POJO层
- 实体类User
package com.dt.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class User { private Integer id; private String name; private String pwd; }
-
Mapper层
- UserMapper接口类
package com.dt.mapper; import com.dt.pojo.User; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; @Mapper public interface UserMapper { User queryUserByName(String name); }
- UserMapper接口实现类
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.dt.mapper.UserMapper"> <select id="queryUserByName" resultType="User"> select * from mybatis.user where name = #{name} </select> </mapper>
-
Service层
- UserService业务接口类
package com.dt.service; import com.dt.pojo.User; public interface UserService { User queryUserByName(String name); }
- UserService业务接口实现类
UserServiceImpl
package com.dt.service; import com.dt.mapper.UserMapper; import com.dt.pojo.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserServiceImpl implements UserService { @Autowired UserMapper userMapper; @Override public User queryUserByName(String name) { return userMapper.queryUserByName(name); } }
-
UserRealm类
package com.dt.config; import com.dt.pojo.User; import com.dt.service.UserService; import com.dt.service.UserServiceImpl; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; // 自定义的Realm public class UserRealm extends AuthorizingRealm { @Autowired UserServiceImpl userService; // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了=>授权doGetAuthorizationInfo"); return null; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行了=>认证doGetAuthorizationInfo"); UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken; // 连接真实数据库 User user = userService.queryUserByName(userToken.getUsername()); if (user == null) { return null; // UnknownAccountException } // MD5 及 MD5盐值加密 // 密码认证,shiro做,加密了 return new SimpleAuthenticationInfo("", user.getPwd(), ""); } }
-
测试
package com.dt; import com.dt.pojo.User; import com.dt.service.UserServiceImpl; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class ShiroSpringbootApplicationTests { @Autowired UserServiceImpl userService; @Test void contextLoads() { User user = userService.queryUserByName("罗翔"); System.out.println(user); } }
成功连接上数据库
11.7、Shiro请求授权实现
实现流程
-
数据库层面
-
添加多一个字段,用来存储用户的授权
注意:应该是 perms
-
项目所要用的数据库
-
-
实体类User添加新字段
package com.dt.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class User { private Integer id; private String name; private String pwd; private String perms; }
-
ShiroConfig配置类
-
ShiroFilterFactoryBean 方法
@Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 设置安全管理器 bean.setSecurityManager(defaultWebSecurityManager); // 添加Shiro的内置过滤器 /* anno:无需认证就可以访问 authc:必须认证才能访问 user:必须拥有 记住我 功能才能用 perms:拥有对某个资源的权限才能访问 role:拥有某个角色权限才能访问 */ Map<String, String> filterMap = new LinkedHashMap<>(); // 权限授权 正常的情况下,没有授权会跳转到未授权页面 filterMap.put("/user/add", "perms[user:add]"); filterMap.put("/user/update", "perms[user:update]"); // filterMap.put("/user/add", "authc"); // filterMap.put("/user/update", "authc"); // 拦截 filterMap.put("/user/*", "authc"); bean.setFilterChainDefinitionMap(filterMap); // 设置登录的请求 bean.setLoginUrl("/toLogin"); // 设置未授权页面 bean.setUnauthorizedUrl("/unauth"); return bean; }
-
只要添加授权代码
// 权限授权 正常的情况下,没有授权会跳转到未授权页面 filterMap.put("/user/add", "perms[user:add]"); filterMap.put("/user/update", "perms[user:update]"); // 设置未授权页面 bean.setUnauthorizedUrl("/unauth");
-
-
UserRealm配置类
-
认证方法中返回值修改
// MD5 及 MD5盐值加密 // 密码认证,shiro做,加密了 return new SimpleAuthenticationInfo(user, user.getPwd(), "");
-
授权方法修改
// 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了=>授权doGetAuthorizationInfo"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // info.addStringPermission("user:add"); // 拿到当前登录的这个对象 Subject subject = SecurityUtils.getSubject(); User currentUser = (User) subject.getPrincipal(); //拿到User对象 // 设置当前用户的权限 info.addStringPermission(currentUser.getPerms()); return info; }
-
-
MyController类设置无授权处理
@RequestMapping("/unauth") @ResponseBody public String unauthorized() { return "未经授权无法访问此页面"; }
-
测试
以 KyDestroy 为例
-
进入add界面成功
-
进入update界面失败
-
11.8、Shiro整合Thymeleaf
实现流程
-
导入shiro-thymeleaf整合包
<!--shiro-thymeleaf整合--> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.1.0</version> </dependency>
-
ShiroConfig类
//整合ShiroDialect:用来整合shiro thymeleaf @Bean public ShiroDialect shiroDialect() { return new ShiroDialect(); }
-
修改登陆页面
index.html
<!DOCTYPE html> <html lang="en" xmlns:th="https://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>首页</h1> <p th:text="${msg}"></p> <div shiro:notAuthenticated> <a th:href="@{/toLogin}">登录</a> </div> <div shiro:hasPermission="user:add"> <a th:href="@{/user/add}">add</a> </div> <div shiro:hasPermission="user:update"> <a th:href="@{/user/update}">update</a> </div> </body> </html>
-
测试
-
没登录的状态下
-
拥有add权限登录的状态下
-
拥有update权限登录的状态下
-
12、Swagger
学习目标:
- 了解Swagger的作用和概念
- 了解前后端分离
- 在SpringBoot中集成Swagger
12.1、Swagger简介
1、前后端分离:
- Vue + Springboot 开发模式
- 后端时代:前端只用管理静态页面;html—>后端。模板引擎 JSP—>后端是主力
2、前后端分离时代:
- 前端 :前端控制层、视图层【前端团队】
- 伪造后端数据,json。已经存在了,不需要后端,前端工程依旧能跑起来
- 后端:后端控制层、服务层、数据访问层【后端团队】
- 前后端通过API进行交互【交互方式】
- 前后端相对独立且松耦合;
- 前后端甚至可以部署在不同的服务器上
3、产生的问题:
- 前后端集成,前端或者后端无法做到“及时协商,尽早解决”,最终导致问题集中爆发
4、解决方案
- 首先定义schema [ 计划的提纲 ],并实时跟踪最新的API,降低集成风险
- 早些年:指定word计划文档
- 前后端分离:
- 前端测试后端接口:postman
- 后端提供接口,需要实施更新最新的消息及改动!
5、Swagger诞生
- 号称世界上最流行的API框架
- Restful Api 文档在线自动生成器 => API 文档 与API 定义同步更新
- 直接运行,在线测试API
- 支持多种语言 (如:Java,PHP等)
- 官网:https://swagger.io/
12.2、第一个Swagger程序
12.2.1、前期准备
SpringBoot集成Swagger => springfox,两个jar包
-
Springfox-swagger2
-
swagger-springmvc
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency>
注意:3.0用
springfox-boot-starter
12.2.2、项目测试
-
创建一个普通的SpringBoot项目,支持web应用
-
添加Maven依赖
-
编写HelloController,测试确保运行成功!
package com.dt.swagger.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @RequestMapping("/hello") public String toHello() { return "Hello"; } }
-
配置Swagger ==》Config
-
SwaggerConfig
package com.dt.swagger.config; import org.springframework.context.annotation.Configuration; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration //配置类 @EnableSwagger2 //开启Swagger2的自动配置 public class SwaggerConfig { }
-
-
需要在
application.properties
中配置spring.mvc.pathmatch.matching-strategy=ant_path_matcher
-
在运行主类添加
@EnableOpenApi
-
测试运行:http://localhost:8080/swagger-ui/index.html (Swagger3.0版本的访问链接)
12.3、Swagger的配置
12.3.1、配置基本页面
-
SwaggerConfig配置类
-
Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger
-
Docket 实例关联上 apiInfo()
// 配置了Swagger的Docket的bean实例 @Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()); }
-
可以通过apiInfo()属性配置文档信息
// 配置Swagger信息 apiInfo public ApiInfo apiInfo() { // 作者信息 Contact contact = new Contact("Dt", "https://gitee.com/KyDestroy", "746294093@qq.com"); return new ApiInfo( "Dt的SwaggerAPI文档", "正经人谁写文档", "v1.0", "https://gitee.com/KyDestroy", contact, "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", new ArrayList()); }
-
12.3.2、配置扫描接口
基本方法
-
SwaggerConfig配置类
@Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) //enable设置是否启动Swagger .enable(false) //通过.select()方法,去配置扫描接口 .select() //RequestHandlerSelectors:配置要扫描接口的方式 .apis(RequestHandlerSelectors.basePackage("com.dt.swagger.controller")) //paths() 过滤什么路径,即这里只扫描请求以/dt开头的接口 .paths(PathSelectors.ant("/dt/**")) .build(); }
-
RequestHandlerSelectors配置的一些方法
basePackage() //指定要扫描的包路径 any() //扫描全部 none() //都不扫描 withClassAnnotation() //扫描方法上的注解,如withClassAnnotation(Controller.class)只扫描有controller注解的类中的接口 withMethodAnnotation() //扫描类上的注解,参数数注解的反射对象,如withMethodAnnotation(GetMapping.class)只扫描get请求
-
PathSelectors配置的一些方法
any() // 任何请求都扫描 none() // 任何请求都不扫描 regex(final String pathRegex) // 通过正则表达式控制 ant(final String antPattern) // 通过ant()控制
-
12.3.3、配置Swagger开关
Swagger在生产环境中使用,在发布的时候不使用
-
判断是不是生产环境 flag=false
-
注入enable(flag)
-
创建 dev(生产环境)和 prod(发布环境)的配置文件
application-dev.properties
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
application-prod.properties
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
在
application.properties
中选择环境spring.mvc.pathmatch.matching-strategy=ant_path_matcher # 选择配置环境 spring.profiles.active=dev
-
修改SwaggerConfig配置类
@Bean public Docket docket(Environment environment) { // 设置要显示的Swagger环境 Profiles profiles = Profiles.of("dev"); // 获取项目的环境,通过environment.acceptsProfiles判断是否处于设定的环境中 boolean flag = environment.acceptsProfiles(profiles); return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) //通过flag来开启或关闭Swagger .enable(flag) .select() .apis(RequestHandlerSelectors.basePackage("com.dt.swagger.controller")) .build(); }
通过enable()方法配置是否启用swagger,如果是false,swagger将不能在浏览器中访问了
测试:http://localhost:8080/swagger-ui/index.html
-
环境为 dev 时,开启Swagger
-
环境为 prod 时,关闭Swagger
12.3.4、配置API分组
实现步骤
-
在SwaggerConfig配置类的docket方法中
如果没有配置分组,默认是default。通过groupName()方法即可配置分组
@Bean public Docket docket(Environment environment) { return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()) .groupName("hello") // 配置分组 // 省略配置.... }
-
配置多个分组方法
-
配置多个docket
@Bean public Docket docket1(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group1"); } @Bean public Docket docket2(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group2"); } @Bean public Docket docket3(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group3"); }
-
12.3.5、常见注解
注解的简单说明
-
Swagger的所有注解定义在io.swagger.annotations包下(下面是常见的)
Swagger注解 简单说明 @Api(tags = "xxx模块说明") 作用在模块类上 @ApiOperation("xxx接口说明") 作用在接口方法上 @ApiModel("xxxPOJO说明") 作用在模型类上:如VO、BO @ApiModelProperty(value = "xxx属性说明",hidden = true) 作用在类方法和属性上,hidden设置为true可以隐藏该属性 @ApiParam("xxx参数说明") 作用在参数、方法和字段上,类似@ApiModelProperty
举例讲解上述五个注解
-
HelloController页面控制跳转类
package com.dt.swagger.controller; import com.dt.swagger.pojo.User; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Api(tags = "Hello控制类") @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello"; } // 只要我们的接口中,存在实体类,他就会被扫描到Swagger中 @PostMapping("/user") public User User() { return new User(); } @ApiOperation("接口方法") @GetMapping("/hello2") public String hello2(@ApiParam("用户名") String name) { return "Hello" + name; } }
-
User类
package com.dt.swagger.pojo; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @ApiModel("用户实体类") public class User { @ApiModelProperty("用户名") public String username; @ApiModelProperty("密码") public String password; }
-
测试
-
模块类中
-
实体类模型中
-
12.3.6、拓展
测试数据
- 可以在Swagger界面测试请求
-
测试前准备
-
HelloController页面控制跳转类
@ApiOperation("Post测试方法") @PostMapping("/post") public User post(User user) { return user; }
-
User类中添加Set方法
package com.dt.swagger.pojo; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @ApiModel("用户实体类") public class User { @ApiModelProperty("用户名") public String username; @ApiModelProperty("密码") public String password; public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } }
-
-
测试步骤
①点击Try it out
②输入伪造的数据
③发现响应体中成功传入数据
皮肤
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
- bootstrap-ui 访问 http://localhost:8080/doc.html
<!-- 引入swagger-bootstrap-ui包 /doc.html-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.1</version>
</dependency>
- Layui-ui 访问 http://localhost:8080/docs.html
<!-- 引入swagger-ui-layer包 /docs.html-->
<dependency>
<groupId>com.github.caspar-chen</groupId>
<artifactId>swagger-ui-layer</artifactId>
<version>1.1.3</version>
</dependency>
- mg-ui 访问 http://localhost:8080/document.html
<!-- 引入swagger-ui-layer包 /document.html-->
<dependency>
<groupId>com.zyplayer</groupId>
<artifactId>swagger-mg-ui</artifactId>
<version>1.0.6</version>
</dependency>
12.4、Swagger总结
- 我们可以通过Swagger给一些比较难理解的属性或者接口,增加注释信息
- 接口文档要实时更新
- 可以在线测试
Swagger是一个优秀的工具,几乎所有大公司都有使用它!
【注意点】在正式发布的时候,关闭Swagger!!!处于安全考虑,也同时节省运行内存!!!
13、异步、邮件、定时任务
13.1、异步任务
为什么要使用异步处理任务?
异步处理任务,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。
两个注解:
- @EnableAsync //SpringBoot启动类
- @Async //执行方法
测试
-
在业务层 service 中定义AsyncService类
package com.dt.service; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @Service public class AsyncService { //告诉Spring这是一个异步方法 @Async public void hello() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("数据正在处理...."); } }
-
在控制层 controller 中定义AsyncController控制类
package com.dt.controller; import com.dt.service.AsyncService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class AsyncController { @Autowired AsyncService asyncService; @RequestMapping("/hello") public String hello() { asyncService.hello(); //停止三秒 return "OK"; } }
-
在主启动类 Springboor09TestApplication 添加开启异步测试
package com.dt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; @EnableAsync // 开启异步注解功能 @SpringBootApplication public class Sprintboot09TestApplication { public static void main(String[] args) { SpringApplication.run(Sprintboot09TestApplication.class, args); } }
-
测试:http://localhost:8080/hello
- 网页瞬间响应,后台代码依旧执行
13.2、邮件任务
13.2.1、概述
邮件发送,在我们的日常开发中,也非常的多,Springboot也帮我们做了支持
- 邮件发送需要引入spring-boot-start-mail
- SpringBoot 自动配置MailSenderAutoConfiguration
- 定义MailProperties内容,配置在application.yml中
- 自动装配JavaMailSender
- 测试邮件发送
13.2.2、使用步骤
-
导入spring-boot-starter-mail依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
-
配置文件
application.yaml
版spring: mail: username: 746294093@qq.com password: gnbjkeydbntsbdga host: smtp.qq.com # QQ需要单独开启加密授权验证 properties: mail: smtp: ssl: enable: true
application.properties
版spring.mail.username=746294093@qq.com spring.mail.password=gnbjkeydbntsbdga spring.mail.host=smtp.qq.com # QQ需要单独开启加密授权验证 spring.mail.properties.mail.smtp.ssl.enable=true
- 获取授权码:在QQ邮箱中的设置->账户->开启pop3和smtp服务
-
测试类中进行测试
package com.dt; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.MimeMessageHelper; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.io.File; @SpringBootTest class Sprintboot09TestApplicationTests { @Autowired JavaMailSenderImpl mailSender; @Test void contextLoads() { // 一个简单的邮件 SimpleMailMessage message = new SimpleMailMessage(); message.setSubject("SpringBoot邮件任务(简单邮件)"); message.setText("Every dog has his day!"); message.setTo("746294093@qq.com"); message.setFrom("746294093@qq.com"); mailSender.send(message); } @Test void contextLoads2() throws MessagingException { // 一个复杂的邮件 MimeMessage mimeMessage = mailSender.createMimeMessage(); // 组装 MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); helper.setSubject("SpringBoot邮件任务(复杂邮件)"); helper.setText("<p style='color:red'>Hello,World</p>", true); // 发送附件 helper.addAttachment("1.jpg", new File("C:\\Users\\Administrator\\Pictures\\iTab-g71y17.jpg")); helper.addAttachment("2.gif", new File("C:\\Users\\Administrator\\Pictures\\微信图片_20220419124659.gif")); helper.setTo("746294093@qq.com"); helper.setFrom("746294093@qq.com"); mailSender.send(mimeMessage); } }
-
测试:
-
简单的邮件发送
-
复杂的邮件发送
查看邮箱,邮件接收成功!
我们只需要使用Thymeleaf进行前后端结合即可开发自己网站邮件收发功能了!
-
拓展:封装成一个方法
@Autowired
JavaMailSenderImpl mailSender;
/**
*
* @param html : 是否支持多文本显示
* @param subject : 标题
* @param content : 内容
* @param to : 收件人
* @param from : 发件人
* @throws MessagingException
*/
public void setMail(Boolean html, String subject,
String content, String to, String from) throws MessagingException {
// 一个复杂的邮件
MimeMessage mimeMessage = mailSender.createMimeMessage();
// 组装
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, html);
helper.setSubject(subject);
helper.setText(content, true);
// 发送附件
helper.addAttachment("1.jpg", new File("C:\\Users\\Administrator\\Pictures\\iTab-g71y17.jpg"));
helper.addAttachment("2.gif", new File("C:\\Users\\Administrator\\Pictures\\微信图片_20220419124659.gif"));
helper.setTo(to);
helper.setFrom(from);
mailSender.send(mimeMessage);
}
13.3、定时执行任务
13.3.1、概述
项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring为我们提供了异步执行任务调度的方式,提供了两个接口。
- TaskExecutor接口
- TaskScheduler接口
两个注解:
- @EnableScheduling //SpringBoot启动类
- @Scheduled //执行方法
13.3.2、使用步骤
-
ScheduledService业务类
package com.dt.service; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @Service public class ScheduledService { // cron 表达式 // 秒 分 时 日 月 星期 @Scheduled(cron = "0 * * * * 0-7") public void hello() { System.out.println("Hello被执行了"); } }
-
在SpringBoot主启动类中添加注释
@EnableScheduling //开启定时执行功能
-
我们来详细了解下cron表达式
-
常用的表达式
(1)0/2 * * * * ? 表示每2秒 执行任务 (1)0 0/2 * * * ? 表示每2分钟 执行任务 (1)0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务 (2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业 (3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作 (4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 (5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时 (6)0 0 12 ? * WED 表示每个星期三中午12点 (7)0 0 12 * * ? 每天中午12点触发 (8)0 15 10 ? * * 每天上午10:15触发 (9)0 15 10 * * ? 每天上午10:15触发 (10)0 15 10 * * ? 每天上午10:15触发 (11)0 15 10 * * ? 2005 2005年的每天上午10:15触发 (12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发 (13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发 (14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 (15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发 (16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发 (17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发 (18)0 15 10 15 * ? 每月15日上午10:15触发 (19)0 15 10 L * ? 每月最后一日的上午10:15触发 (20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发 (21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发 (22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
14、Dubbo和Zookeeper集成
14.1、分布式理论
14.1.1、什么是分布式系统?
在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
分布式系统(distributed system)是建立在网络之上的软件系统。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。。。
14.1.2、Dubbo文档
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。
在Dubbo的官网文档有这样一张图
单一应用架构
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。
1、性能扩展比较难
2、协同开发问题
3、不利于升级维护
垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。
通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。
缺点:公用模块无法重复利用,开发性的浪费
分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。
流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。
14.2、什么是RPC
14.2.1、概述
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;
推荐阅读文章:https://www.jianshu.com/p/2accc2840a1b
14.2.2、RPC基本原理
步骤解析:
RPC两个核心模块:通讯,序列化
14.3、Dubbo
14.3.1、什么是dubbo
Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
dubbo官网 http://dubbo.apache.org/zh-cn/index.html
1.了解Dubbo的特性
2.查看官方文档
14.3.2、dubbo基本概念
服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
调用关系说明
-
服务容器负责启动,加载,运行服务提供者。
-
服务提供者在启动时,向注册中心注册自己提供的服务。
-
服务消费者在启动时,向注册中心订阅自己所需的服务。
-
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
-
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
-
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
14.4、Dubbo环境搭建(Window下安装zookeeper)
点进dubbo官方文档,推荐我们使用Zookeeper 注册中心
下载链接:Apache ZooKeeper
下载版本:3.8.0
- 注意:3.5版本后要下载bin文件
-
解压后打开 conf,将
zoo_sample.cfg
复制一份改名为zoo.cfg
-
将
dataDir=./
删掉,加入下列命令dataDir=D:\Environment\apache-zookeeper-3.8.0-bin\data audit.enable=true
-
打开bin文件夹的
zkServer.cmd
(注意要用管理员模式) -
使用
zkCli.cmd
测试ls /
:列出Zookeeper根下保存的所有节点[zk: localhost:2181(CONNECTED) 0] ls / [zookeeper]
create -e /dt 123
:创建一个 /dt 节点,值为123get /dt
:获取 /dt 节点的值我们再来看下节点
14.5、Windows下安装dubbo-admin
dubbo本身并不是服务软件。它其实就是一个jar包,能够帮助你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。
但是为了让用户更好的管理监控更多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin,不过这个监控即使不装也不影响使用。
我们这里来安装一下:
-
下载dubbo-admin
-
解压进入目录
修改 dubbo-admin\scr\main\resources\application.properties 指定zookeeper地址
server.port=7001 spring.velocity.cache=false spring.velocity.charset=UTF-8 spring.velocity.layout-url=/templates/default.vm spring.messages.fallback-to-system-locale=false spring.messages.basename=i18n/message spring.root.password=root spring.guest.password=guest # 注册中心的地址 dubbo.registry.address=zookeeper://127.0.0.1:2181
-
在项目目录下打包 dubbo-admin
mvn clean package -Dmaven.test.skip=true
- 如果打包失败,切换环境变量的jdk版本为1.8
-
执行 dubbo/target 下的
dubbo-admin-0.0.1-SNAPSHOT.jar
java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
- 注意:Zookeeper的服务一定要打开
执行完毕,我们去访问一下 http://localhost:7001/,这时候我们需要输入登录账号的密码,默认root-root
登录成功后,查看界面
14.6、总结
-
小结
zookeeper:注册中心
dubbo-admin:一个监控管理后台,查看我们注册了哪些服务,哪些服务被消费了
Dubbo:jar包
-
步骤
前提:zookeeper服务已经开启!
- 提供者提供服务
- 导入依赖
- 配置注册中心的地址,以及服务发现名,和要扫描的包~
- 在想要被注册的服务上面~ 增加一个注解 @DubboService
- 消费者如何消费
- 导入依赖
- 配置注册中心的地址,配置自己的服务名
- 从远程注入服务~ @DubboReference
- 提供者提供服务
14.7、服务注册与发现实战
14.7.1、步骤
-
创建一个空项目
-
File->New->Module
-
添加web支持
-
删除多余的文件
-
导入依赖
<!--导入依赖:Dubbo + Zookeeper--> <!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.15</version> </dependency> <!--zkclient--> <!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient --> <dependency> <groupId>com.github.sgroschupf</groupId> <artifactId>zkclient</artifactId> <version>0.1</version> </dependency>
注意:dubbo-spring-boot-starter不要用3.0版本以上会报错
[新版的坑] Zookeeper及其依赖包,解决日志冲突,还需要剔除日志依赖
<!--日志会冲突,需要导入如下依赖--> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.14</version> <!--排除这个slf4j-log4j12--> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency>
-
项目结构
-
Service层
-
TicketService
package com.dt.service; public interface TicketService { String getTicket(); }
-
TicketServiceImpl
package com.dt.service; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.stereotype.Component; @Component // 使用了Dubbo后尽量不要用Servic注解是为了与Dubbo的Service区分 @DubboService //可以被扫描到,在项目启动就自动注册到注册中心 public class TicketServiceImpl implements TicketService { @Override public String getTicket() { return "Hello Java"; } }
-
-
application.properties
# 应用名称 spring.application.name=provider-server # 应用服务 WEB 访问端口 server.port=8001 # 服务应用名字 dubbo.application.name=provider-server # 注册中心地址 dubbo.registry.address=zookeeper://localhost:2181 # 哪些服务要被注册 dubbo.scan.base-packages=com.dt.service
-
-
再File->New->Module
-
一样添加添加web支持
-
一样删除多余的文件
-
给消费者导入依赖(跟生产者一致)
-
项目结构
-
Service层
-
UserService
package com.dt.service; import org.apache.dubbo.config.annotation.DubboReference; import org.springframework.stereotype.Component; @Component // 放到容器中 public class UserService { // 想拿到provider-server提供的票,要去注册中心拿到服务 @DubboReference // 引用, Pom坐标,可以定义路径相同的接口名 TicketService ticketService; public void buyTicket() { String ticket = ticketService.getTicket(); System.out.println("在注册中心拿到=>" + ticket); } }
-
TicketService
package com.dt.service; public interface TicketService { String getTicket(); }
-
-
application.properties
修改端口号# 应用名称 spring.application.name=consumer-server # 应用服务 WEB 访问端口 server.port=8002 # 消费者去哪里拿服务,需要暴露自己的名字 dubbo.application.name=consumer-server # 注册中心的地址 dubbo.registry.address=zookeeper://localhost:2181
-
-
测试
-
开启ZooKeeper
-
命令行运行
dubbo-admin-0.0.1-SNAPSHOT.jar
java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
-
运行 ProviderServerApplication 启动类(生产者)
-
访问:http://localhost:7001/governance/providers
我们能发现
TicketServiceImpl
已经注册到Dubbo里 -
运行 ConsumerServerApplicationTests 测试类 (消费者)
package com.dt; import com.dt.service.UserService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class ConsumerServerApplicationTests { @Autowired UserService userService; @Test void contextLoads() { userService.buyTicket(); } }
-
结果
-
15、回顾和展望
三层架构 + MVC
架构 -->解耦
开发框架
Spring
IOC AOP
IOC : 控制反转
泡温泉,泡茶泡友
附近的人,打招呼。加微信,聊天,天天聊>约泡
浴场(容器):温泉,茶庄泡友
直接进温泉,就有人和你一起了!
原来我们都是自己一步步操作,现在交给容器了!我们需要什么就去拿就可以了
AOP:切面(本质,动态代理)
为了解什么?不影响业本来的情况下,实现动态增加功能,大量应用在日志,事务等等
Spring是一个轻量级的Java开源框架,容器
目的:解决企业开发的复杂性问题
Spring是春天,但配置文件繁琐
SpringBoot
SpringBoot ,新代javaEE的开发标准,开箱即用!>拿过来就可以用,
它自动帮我们配置了非常多的东西,我们拿来即用,
特性:约定大于配置!
随着公司体系越来越大,用户越来越多
微服务架构—>新架构
模块化,功能化!
用户,支付,签到,娱乐…;
如果一台服务器解决不了就再增加一台服务器! --横向扩展
假设A服务器占用98%资源B服务器只占用了10%.–负载均衡;
将原来的整体项,分成模块化,用户就是一个单独的项目,签到也是一个单独的项目,项目和项目之前需要通信,如何通信
用户非常多而到十分少给用户多一点服务器,给签到少一点服务器
微服务架构问题?
分布式架构会遇到的四个核心题?
1.这么多服务,客户端该如何去访?
2.这么多服务,服务之间如何进行通信?
3.这么多服务,如何治理呢?
4.服务挂了,怎么办?
解决方案:
Springcloud是一套生态,就是来解决以上分布式架构的4个问题
想使用Spring Clould ,必须要掌握 springBoot , 因为Springcloud是基于springBoot ;
1. spring Cloud NetFlix ,出来了一套解决方案!一站式解决方案。可以直接使用
Api网关 , zuul组件
Feign --> Httpclient ---> http通信方式,同步并阻塞
服务注册与发现 , Eureka
熔断机制 , Hystrix
2018年年底,NetFlix 宣布无限期停止维护。生态不再维护,就会脱节。
2. Apache Dubbo zookeeper ,第二套解决方案
API:没有!要么找第三方组件,要么自己实现
Dubbo 是一个高性能的基于Java实现的RPC通信框架!2.6.x
服务注册与发现 , zookeeper :动物管理者 ( Hadoop , Hive )
没有:借助了Hystrix
不完善,Dubbo
3. SpringCloud Alibaba 一站式解决方案
目前又提出了新的思路:
服务网格:也许是下一代维服务标准,Service mesh
代表解决方案:istio(未来可能需要掌握)
万变不离其宗,一通百通!
1.API网关 , 服务路由
2.HTTP,RPC框架,异步调用
3.服务注册与发现,高可用
4.熔断机制,服务降级
为什么要解决这个问题?因为网络是不可靠的!!!