回顾什么是Spring
Spring是一个开源框架,2003 年兴起的一个轻量级的Java 开发框架,作者:Rod Johnson 。
Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。
Spring是如何简化Java开发的
为了降低Java开发的复杂性,Spring采用了以下4种关键策略:
1、基于POJO的轻量级和最小侵入性编程,所有东西都是bean;
2、通过IOC,依赖注入(DI)和面向接口实现松耦合;
3、基于切面(AOP)和惯例进行声明式编程
4、通过切面和模版减少样式代码,RedisTemplate,xxxTemplate
什么是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 Boot应用,并且实现一个简单的Http请求处理。通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。
我的环境准备:
开发工具:
创建基础项目说明
Spring官方提供了非常方便的工具让我们快速构建应用
Spring Initializr:https://start.spring.io/
**项目创建方式一:**使用Spring Initializr 的 Web页面创建项目
1、打开 https://start.spring.io/
2、填写项目信息
3、点击”Generate Project“按钮生成项目;下载此项目
4、解压项目包,并用IDEA以Maven项目导入,一路下一步即可,直到项目导入完毕。
5、如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。
**项目创建方式二:**使用 IDEA 直接创建项目
1、创建一个新项目
2、选择spring initalizr , 可以看到默认就是去官网的快速构建工具那里实现
3、填写项目信息
4、选择初始化的组件(初学勾选 Web 即可)
5、填写项目路径
6、等待项目构建成功
项目结构分析:
通过上面步骤完成了基础项目的创建。就会自动生成以下文件。
1、程序的主启动类
2、一个 application.properties 配置文件
3、一个 测试类
4、一个 pom.xml
pom.xml 分析
打开pom.xml,看看Spring Boot项目的依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.5.4version>
<relativePath/>
parent>
<groupId>com.edgargroupId>
<artifactId>helloworldartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>helloworldname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-resources-pluginartifactId>
<version>3.1.0version>
plugin>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
折叠
编写一个http接口
1、在主程序的同级目录下,新建一个controller包,一定要在同级目录下,否则识别不到
2、在包中新建一个HelloController类
package com.edgar.helloworld.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// 自动装配:原理!
@RestController
public class HelloController {
// 接口:http://localhost:8080/hello
@RequestMapping("/hello")
public String hello() {
// 调用业务,接受前端的参数!
return "hello,world";
}
}
3、编写完毕后,从主程序启动项目,浏览器发起请求,看页面返回;控制台输出了 Tomcat 访问的端口号!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uqbdvesd-1663510275474)(SpringBoot.assets/hbnhrV.png)]
简单几步,就完成了一个web接口的开发,SpringBoot就是这么简单。所以我们常用它来建立我们的微服务项目!
将项目打成jar包,点击 maven的 package
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j79N2IFt-1663510275475)(SpringBoot.assets/hbuiRA.png)]
如果打包成功,则会在target目录下生成一个 jar 包
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KrzpIgoT-1663510275475)(SpringBoot.assets/hbu3Mq.png)]
打成了jar包后,就可以在任何地方运行了!OK
# 启动jar包
java -jar helloworld-0.0.1-SNAPSHOT.jar
彩蛋
如何更改启动时显示的字符拼成的字母,SpringBoot呢?也就是 banner 图案;
只需一步:到项目下的 resources 目录下新建一个banner.txt ,然后把图案复制粘贴到文件中即可。
图案可以到:https://www.bootschool.net/ascii 这个网站生成,然后拷贝到文件中即可!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LZ2Qu6bt-1663510275475)(SpringBoot.assets/hbK9mV.png)]
其中它主要是依赖一个父项目,主要是管理项目的资源过滤及插件!
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.5.4version>
<relativePath/>
parent>
点进去,发现还有一个父依赖
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.5.4version>
parent>
这里才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心;
以后我们导入依赖默认是不需要写版本;但是如果导入的包没有在依赖中管理着就需要手动配置版本了;
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
spring-boot-starter-xxx:就是SpringBoot的场景启动器
spring-boot-starter-web:帮我们导入了web模块正常运行所依赖的组件;
SpringBoot将所有的功能场景都抽取出来,做成一个个的starter(启动器),只需要在项目中引入这些starter,即可将所有相关的依赖都导入进来,我们要用什么功能就导入什么样的场景启动器即可;我们未来也可以自己自定义starter;
分析完了 pom.xml 来看看这个启动类
//@SpringBootApplication 来标注一个主程序类
//说明这是一个Spring Boot应用
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
//以为是启动了一个方法,没想到启动了一个服务
SpringApplication.run(SpringbootApplication.class, args);
}
}
但是**一个简单的启动类并不简单!**我们来分析一下这些注解都干了什么
作用:标注在某个类上说明这个类是SpringBoot的主启动类,SpringBoot就应该运行这个类的main方法来启动SpringBoot应用;
进入这个注解:可以看到上面还有很多其他注解!
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
这个注解在Spring中很重要,它对应XML配置中的元素。
作用:自动扫描并加载符合条件的组件或者bean,将这个bean定义到IOC容器中
作用:SpringBoot的配置类,标注在某个类上,表示这是一个SpringBoot的配置类;
我们继续进去这个注解查看
// 点进去得到下面的 @Component
@Configuration
public @interface SpringBootConfiguration {}
@Component
public @interface Configuration {}
这里的 @Configuration,说明这是一个配置类,配置类就是对应Spring的xml配置文件;
里面的 @Component 这就说明,启动类本身也是Spring中的一个组件而已,负责启动应用!
我们回到 SpringBootApplication 注解中继续看。
@EnableAutoConfiguration :开启自动配置功能
以前我们需要自己配置的东西,而现在SpringBoot可以自动帮我们配置;
@EnableAutoConfiguration告诉SpringBoot开启自动配置功能,这样自动配置才能生效;
点进注解继续查看:
@AutoConfigurationPackage :自动配置包
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
}
@import :Spring底层注解@import , 给容器中导入一个组件
Registrar.class 作用:将主动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器;
这个分析完了,退到上一步,继续看
@Import({AutoConfigurationImportSelector.class}) :给容器导入组件 ;
AutoConfigurationImportSelector :自动配置导入选择器,那么它会导入哪些组件的选择器呢?我们点击去这个类看源码:
1、这个类中有一个这样的方法
// 获得候选的配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//这里的getSpringFactoriesLoaderFactoryClass()方法
//返回的就是我们最开始看的启动自动导入配置文件的注解类;EnableAutoConfiguration
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
2、这个方法又调用了 SpringFactoriesLoader 类的静态方法!我们进入SpringFactoriesLoader类loadFactoryNames() 方法
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
//这里它又调用了 loadSpringFactories 方法
return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
3、我们继续点击查看 loadSpringFactories 方法
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
//获得classLoader , 我们返回可以看到这里得到的就是EnableAutoConfiguration标注的类本身
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
//去获取一个资源 "META-INF/spring.factories"
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
LinkedMultiValueMap result = new LinkedMultiValueMap();
//将读取到的资源遍历,封装成为一个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 factoryClassName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
String factoryName = var9[var11];
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}
折叠
4、发现一个多次出现的文件:spring.factories,全局搜索它
我们根据源头打开spring.factories , 看到了很多自动配置的文件;这就是自动配置根源所在!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-txtjAESP-1663510275476)(SpringBoot.assets/49oe4P.png)]
WebMvcAutoConfiguration
我们在上面的自动配置类随便找一个打开看看,比如 :WebMvcAutoConfiguration
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fYipVOUh-1663510275476)(SpringBoot.assets/49oNCV.png)]
可以看到这些一个个的都是JavaConfig配置类,而且都注入了一些Bean,可以找一些自己认识的类,看着熟悉以下!
所以,自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中。
结论:
1、SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值
2、将这些值作为自动配置类导入容器,自动配置类就生效,帮我们进行自动配置工作;
3、整个J2EE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中;
4、它会给容器中导入非常多的自动配置类(xxxAutoConfiguration),就是给容器中导入这个场景需要的所有组件,并配置好这些组件;
5、有了自动配置类,免去了我们手动编写配置注入功能组件等的工作;
现在大家应该大概的了解了下,SpringBoot的运行原理,后面我们还会深化一次!
不简单的方法
我最初以为就是运行了一个main方法,没想到却开启了一个服务;
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
SpringApplication.run分析
分析该方法主要分两部分,一部分是SpringApplication的实例化,二是run方法的执行;
SpringApplication
这个类主要做了以下四件事情:
1、推断应用的类型是普通的项目还是Web项目
2、查找并加载所有可用初始化器 , 设置到initializers属性中
3、找出所有的应用程序监听器,设置到listeners属性中
4、推断并设置main方法的定义类,找到运行的主类
查看构造器:
public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
// ......
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.setInitializers(this.getSpringFactoriesInstances();
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = this.deduceMainApplicationClass();
}
run方法流程分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7dnTUURK-1663510275477)(SpringBoot.assets/49TjW6.jpg)]
SpringBoot使用一个全局的配置文件,配置文件名称是固定的
**配置文件的作用:**修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们自动配置好了
比如我们可以在配置文件中修改Tomcat默认启动的端口号!测试一下!
server.port=8081
YAML是 “YAML Ain’t a Markup Language” (YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)
这种语言以数据作为中心,而不是以标记语言作为重点!
以前的配置文件,大多数都是使用xml来配置;比如一个简单的端口配置,我们来对比下yaml和xml
传统的xml配置:
<server>
<port>8081<port>
server>
yaml配置:
server:
prot: 8080
说明:语法要求严格!
1、空格不能省略
2、以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
3、属性和值的大小写都是十分敏感的。
字面量:普通的值 [ 数字,布尔值,字符串 ]
字面量直接写在后面就可以,字符串默认不用加上双引号或者单引号;
k: v
注意:
对象、Map(键值对)
#对象、Map格式
k:
v1:
v2:
在下一行来写对象的属性和值的关系,注意缩进;比如:
student:
name: edgar
age: 3
行内写法
student: {name: edgar,age: 3}
数组( List、set )
用 - 值表示数组中的一个元素,比如:
pets:
- cat
- dog
- pig
行内写法
pets: [cat,dog,pig]
修改SpringBoot的默认端口号
配置文件中添加,端口号的参数,就可以切换端口;
server:
port: 8082
yaml文件更强大的地方在于,它可以给我们的实体类直接注入匹配值!
1、在springboot项目中的resources目录下新建一个文件 application.yml
2、编写一个实体类 Dog;
package com.edgar.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component //注册bean到容器中
public class Dog {
private String name;
private Integer age;
//有参无参构造、get、set方法、toString()方法
}
3、思考,我们原来是如何给bean注入属性值的!@Value,给狗狗类测试一下:
package com.edgar.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component //注册bean到容器中
public class Dog {
@Value("旺财")
private String name;
@Value("3")
private Integer age;
//有参无参构造、get、set方法、toString()方法
}
4、在SpringBoot的测试类下注入狗狗输出一下;
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired //将狗狗自动注入进来
private Dog dog;
@Test
void contextLoads() {
System.out.println(dog); //打印看下狗狗对象
}
}
结果成功输出,@Value注入成功,这是我们原来的办法对吧。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kFNHFH9M-1663510275477)(SpringBoot.assets/4FkawQ.png)]
5、我们在编写一个复杂一点的实体类:Person 类
package com.edgar.pojo;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Component
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;
//有参无参构造、get、set方法、toString()方法
}
6、我们来使用yaml配置的方式进行注入,大家写的时候注意区别和优势,我们编写一个application.yaml配置!
person:
name: edgar
age: 3
happy: false
birth: 2021/09/13
maps: {k1: v1,k2: v2}
lists:
- code
- music
- girl
dog:
name: 旺财
age: 3
7、我们刚才已经把person这个对象的所有值都写好了,我们现在来注入到我们的类中!
package com.edgar.pojo;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.Map;
/*
@ConfigurationProperties作用:
将配置文件中配置的每一个属性的值,映射到这个组件中;
告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
参数 prefix = “person” : 将配置文件中的person下面的所有属性一一对应
*/
@Component //注册bean
@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;
//有参无参构造、get、set方法、toString()方法
}
折叠
8、IDEA 提示,springboot配置注解处理器没有找到,让我们看文档,我们可以查看文档,找到一个依赖!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9jlpki64-1663510275477)(SpringBoot.assets/4Fk76K.jpg)]
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
9、确认以上配置都OK之后,我们去测试类中测试一下:
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired // 将person自动注入进来
private Person person;
@Test
void contextLoads() {
System.out.println(person); // 打印看下person对象
}
}
结果:所有值全部注入成功!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DIwiIJwV-1663510275478)(SpringBoot.assets/4FACX8.png)]
yaml配置注入到实体类完全OK!
将配置文件的key 值 和 属性的值设置为不一样,则结果输出为null,注入失败
**@PropertySource :**加载指定的配置文件;
@configurationProperties:默认从全局配置文件中获取值;
1、我们去在resources目录下新建一个edgar.properties文件
name=edgar
2、然后在我们的代码中指定加载edgar.properties文件
@PropertySource(value = "classpath:edgar.properties")
@Component // 注册bean
public class Person {
// SPEL表达式取出配置文件的值
@Value("${name}")
private String name;
......
}
3、再次输出测试一下:指定配置文件绑定成功!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Ea79oUu-1663510275478)(SpringBoot.assets/4FAJhR.png)]
注意:application.properties和application.yaml都可以使用 @Value(“${name}”)取值,无需**@PropertySource**去指定配置文件,且 properties的优先级比yaml高
【注意】properties配置文件在写中文的时候,可能会有乱码,我们需要去IDEA中设置编码格式为UTF-8;
settings–>FileEncodings 中配置;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rAhtZnAi-1663510275478)(SpringBoot.assets/4FAzE4.png)]
@Value这个使用起来并不友好!我们需要为每个属性单独注解赋值,比较麻烦;我们来看个功能对比图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WJB5WaWQ-1663510275478)(SpringBoot.assets/4FEVbD.jpg)]
1、@ConfigurationProperties只需要写一次即可,@Value则需要每个字段都添加
2、松散绑定:这个什么意思呢? 比如我的yml中写的last-name,这个和lastName是一样的, - 后面跟着的字母默认是大写的。这就是松散绑定。可以测试一下
3、JSR303数据校验,这个就是我们可以在字段是增加一层过滤器验证,可以保证数据的合法性
4、复杂类型封装,yaml中可以封装对象,使用value就不支持
结论:
配置yaml和配置properties都可以获取到值,强烈推荐 yaml;
如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;
如果说,我们专门编写了一个JavaBean来和配置文件进行一一映射,就直接@configurationProperties,不要犹豫!
SpringBoot中可以用@Validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。我们这里来写个注解让我们的name只能支持Email格式;
@Component //注册bean
@ConfigurationProperties(prefix = "person")
@Validated //数据校验
public class Person {
@Email(message="邮箱格式错误") //name必须是邮箱格式
private String name;
}
如果@Email注解爆红,添加依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
dependency>
运行结果 :default message [不是一个合法的电子邮件地址];
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2AIgZpFx-1663510275479)(SpringBoot.assets/4AJeo9.png)]
使用数据校验,可以保证数据的正确性;
@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 对象是否符合正则表达式的规则
.......等等
除此以外,我们还可以自定义一些数据校验规则
profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境;
我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml,用来指定多个环境版本;
**例如:**编写多个配置文件:
application-test.properties 代表测试环境配置
application-dev.properties 代表开发环境配置
但是SpringBoot并不会直接启动这些配置文件,它默认使用application.properties主配置文件;
我们需要通过一个配置来选择需要激活的环境:
#比如在配置文件中指定使用dev环境,我们可以通过设置不同的端口号进行测试;
#我们启动SpringBoot,就可以看到已经切换到dev下的配置了;
spring.profiles.active=dev
和properties配置文件中一样,但是使用yml去实现不需要创建多个配置文件,更加方便了
server:
port: 8081
#选择要激活那个环境块
spring:
profiles:
active: dev #开发环境
---
server:
port: 8082
spring:
profiles: dev #配置环境的名称
---
server:
port: 8083
spring:
profiles: test #测试环境
springboot 2.4版本往后,不推荐使用:
spring:
profiles: dev #配置环境的名称
替代方式:
spring:
config:
activate:
on-profile: dev #配置环境的名称
具体示例:
server:
port: 8081
spring:
profiles:
active: dev
---
server:
port: 8082
spring:
config:
activate:
on-profile: dev
---
server:
port: 8083
spring:
config:
activate:
on-profile: test
注意:如果yml和properties同时都配置了端口,并且没有激活其他环境 , 默认会使用properties配置文件的!
外部加载配置文件的方式十分多,我们选择最常用的即可,在开发的资源文件中进行配置!
官方外部配置文件说明参考文档:https://docs.spring.io/spring-boot/docs/2.1.10.RELEASE/reference/htmlsingle/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M0VVKn1R-1663510275479)(SpringBoot.assets/4Av50s.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ETTGudsl-1663510275479)(SpringBoot.assets/1659427737987.png)]
springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件:
优先级1:项目路径下的config文件夹配置文件
优先级2:项目路径下配置文件
优先级3:资源路径下的config文件夹配置文件
优先级4:资源路径下配置文件
优先级由高到低,高优先级的配置会覆盖低优先级的配置;优先级1是最高;
SpringBoot会从这四个位置全部加载主配置文件;互补配置;
我们在最低级的配置文件中设置一个项目访问路径的配置来测试互补问题;
#配置项目的访问路径
server.servlet.context-path=/edgar
指定位置加载配置文件
我们还可以通过spring.config.location来改变默认的配置文件位置
项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高
java -jar spring-boot-config.jar --spring.config.location=F:/application.properties
配置文件到底能写什么?怎么写?
SpringBoot官方文档中有大量的配置,我们无法全部记住
官方文档地址:https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.server
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CVEO3Vl4-1663510275479)(SpringBoot.assets/4epMS1.png)]
我们以**HttpEncodingAutoConfiguration(Http编码自动配置)**为例解释自动配置原理;
// 表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件;
@Configuration(
proxyBeanMethods = false
)
// 启动指定可使用的自动配置类 ServerProperties
// 进入这个 ServerProperties.class 查看,将配置文件中对应的值和 ServerProperties.class 绑定起来;
// 并把 ServerProperties.class 加入到ioc容器中
@EnableConfigurationProperties({ServerProperties.class})
// Spring底层@Conditional注解
// 根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效;
// 这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效
@ConditionalOnWebApplication(
type = Type.SERVLET
)
// 判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器
@ConditionalOnClass({CharacterEncodingFilter.class})
// 判断配置文件中是否存在某个配置:server.servlet.encoding.enabled;
// 如果不存在,判断也是成立的
// 即使我们配置文件中不配置server.servlet.encoding.enabled=true,也是默认生效的;
@ConditionalOnProperty(
prefix = "server.servlet.encoding",
value = {"enabled"},
matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
private final Encoding properties;
// 只有一个有参构造器的情况下,参数的值就会从容器中拿
public HttpEncodingAutoConfiguration(ServerProperties properties) {
this.properties = properties.getServlet().getEncoding();
}
// 给容器中添加一个组件,这个组件的某些值需要从properties中获取
@Bean
@ConditionalOnMissingBean // 判断容器没有这个组件?
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE));
return filter;
}
//。。。。。。
}
折叠
一句话总结 :根据当前不同的条件判断,决定这个自动配置类是否生效!
即.yml/.yaml/.properties
绑定的;@ConfigurationProperties(
prefix = "server",
ignoreUnknownFields = true
)
public class ServerProperties {
// ......
}
我们去配置文件里面试试前缀,看提示!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n8IfRVsf-1663510275480)(SpringBoot.assets/4eCvdO.png)]
这就是自动装配的原理!
1、SpringBoot启动会加载大量的自动配置类
2、我们看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;
3、我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在其中,我们就不要再手动配置了)
4、给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;
**xxxAutoConfigurartion:自动配置类;**给容器中添加组件
xxxProperties:封装配置文件中相关属性;
了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;
@Conditional派生注解(Spring注解版原生的@Conditional作用)
作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;
@Conditional扩展注解 | 作用(判断是否满足当前指定条件) |
---|---|
@ConditionalOnJava | 系统的java版本是否符合要求 |
@ConditionalOnBean | 容器中存在指定Bean; |
@ConditionalOnMissingBean | 容器中不存在指定Bean; |
@ConditionalOnExpression | 满足SpEL表达式指定 |
@ConditionalOnClass | 系统中有指定的类 |
@ConditionalOnMissingClass | 系统中没有指定的类 |
@ConditionalOnSingleCandidate | 容器中只有一个指定的Bean,或者这个Bean是首选Bean |
@ConditionalOnProperty | 系统中指定的属性是否有指定的值 |
@ConditionalOnResource | 类路径下是否存在指定资源文件 |
@ConditionalOnWebApplication | 当前是web环境 |
@ConditionalOnNotWebApplication | 当前不是web环境 |
@ConditionalOnJndi | JNDI存在指定项 |
那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是所有的都生效了。
我们怎么知道哪些自动配置类生效?
我们可以通过启用 debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效;
#开启springboot的调试类
debug=true
启动应用,控制台输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F0NkKkfT-1663510275480)(SpringBoot.assets/4eArgH.png)]
在上面的输出中:
其实SpringBoot的东西用起来非常简单,因为SpringBoot最大的特点就是自动装配。
使用SpringBoot的步骤:
1、创建一个SpringBoot应用,选择我们需要的模块,SpringBoot就会默认将我们的需要的模块自动配置好
2、手动在配置文件中配置部分配置项目就可以运行起来了
3、专注编写业务代码,不需要考虑以前那样一大堆的配置了。
要熟悉掌握开发,之前学习的自动配置的原理一定要搞明白!
比如SpringBoot到底帮我们配置了什么?我们能不能修改?我们能修改哪些配置?我们能不能扩展?
没事就找找类,看看自动装配原理!
我们之后来进行一个单体项目的小项目测试,让大家能够快速上手开发!
我们来搭建一个普通的SpringBoot项目,回顾一下HelloWorld程序!
首先: 创建Springboot项目,添加web依赖。
编写controller测试访问是否正常
@RestController
public class HelloController {
@GetMapping("/hello")
public String Hello(){
return "hello,world!!!";
}
}
测试成功!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UdVEZVNo-1663510275480)(SpringBoot.assets/1659926605809.png)]
写请求非常简单,那我们要引入我们前端资源,我们项目中有许多的静态资源,比如css,js等文件,这个SpringBoot怎么处理呢?
如果我们是一个web应用,我们的main下会有一个webapp,我们以前都是将所有的页面导在这里面的,对吧!但是我们现在的pom呢,打包方式是为jar的方式,那么这种方式SpringBoot能不能来给我们写页面呢?当然是可以的,但是SpringBoot对于静态资源放置的位置,是有规定的!
我们先来聊聊这个静态资源映射规则:
SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration
这个配置类里面;
我们可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;
有一个方法:addResourceHandlers 添加资源处理
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
// 已禁用默认资源处理
logger.debug("Default resource handling disabled");
} else {
// webjars 配置
this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
// 静态资源配置
this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
registration.addResourceLocations(new Resource[]{resource});
}
});
}
}
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
this.addResourceHandler(registry, pattern, (registration) -> {
registration.addResourceLocations(locations);
});
}
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, Consumer<ResourceHandlerRegistration> customizer) {
if (!registry.hasMappingForPattern(pattern)) {
ResourceHandlerRegistration registration = registry.addResourceHandler(new String[]{pattern});
customizer.accept(registration);
registration.setCachePeriod(this.getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
this.customizeResourceHandlerRegistration(registration);
}
}
读一下源代码:比如所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源;
Webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。
使用SpringBoot需要使用Webjars,我们可以去搜索一下:
网站:https://www.webjars.org/
要使用jQuery,我们只要要引入jQuery对应版本的pom依赖即可!
<dependency>
<groupId>org.webjarsgroupId>
<artifactId>jqueryartifactId>
<version>3.6.0version>
dependency>
导入完毕,查看webjars目录结构,并访问Jquery.js文件!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0TkUfivb-1663510275480)(SpringBoot.assets/4uSzCQ.png)]
访问:只要是静态资源,SpringBoot就会去对应的路径寻找资源,我们这里访问:http://localhost:8080/webjars/jquery/3.6.0/jquery.js
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Ze9YHgw-1663510275480)(SpringBoot.assets/4upn29.png)]
那我们项目中要是使用自己的静态资源该怎么导入呢?我们看下一行代码;
我们去找staticPathPattern发现第二种映射规则 :/** , 访问当前的项目任意资源,它会去找 WebProperties这个类,我们可以点进去看一下分析:
public String[] getStaticLocations() {
return this.staticLocations;
}
public Resources() {
this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
this.addMappings = true;
this.customized = false;
this.chain = new WebProperties.Resources.Chain();
this.cache = new WebProperties.Resources.Cache();
}
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
};
WebProperties可以设置和我们静态资源有关的参数;这里面指向了它会去寻找资源的文件夹,即上面数组的内容。
所以得出结论,以下四个目录存放的静态资源可以被我们识别:
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
我们可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WEbbZ9wX-1663510275481)(SpringBoot.assets/1659928070340.png)]
比如我们访问 http://localhost:8080/1.js , 他就会去这些文件夹中寻找对应的静态资源文件;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h9hOr3dD-1663510275481)(SpringBoot.assets/1659928104436.png)]
优先级从高到低:resources > static(默认) > public
我们也可以自己通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在application.properties中配置;
spring.web.resources.static-locations=classpath:/coding/,classpath:/edgar/
一旦自己定义了静态文件夹的路径,原来的自动配置就都会失效了!
总结:
在springboot中,我们可以使用以下方式处理静态资源
静态资源文件夹说完后,我们继续向下看源码!可以看到一个欢迎页的映射,就是我们的首页!
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this.getInterceptors());
return welcomePageHandlerMapping;
}
点进去继续看
private Optional<Resource> getWelcomePage() {
String[] locations = WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations());
// // ::是java8 中新引入的运算符
// Class::function的时候function是属于Class的,应该是静态方法。
// this::function的funtion是属于这个对象的。
// 简而言之,就是一种语法糖而已,是一种简写
return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}
// 欢迎页就是一个location下的的 index.html 而已
private Resource getIndexHtml(String location) {
return this.resourceLoader.getResource(location + "index.html");
}
欢迎页,静态资源文件夹下的所有 index.html 页面;被 /** 映射。
比如我访问 http://localhost:8080/ ,就会找静态资源文件夹下的 index.html
新建一个 index.html ,在我们上面的3个目录中任意一个;然后访问测试 http://localhost:8080/ 看结果!
与其他静态资源一样,Spring Boot在配置的静态内容位置中查找 favicon.ico。如果存在这样的文件,它将自动用作应用程序的favicon。
1、关闭SpringBoot默认图标(如今是springboot的新版本不支持该配置,可以直接从第2步开始操作)
#关闭默认图标
spring.mvc.favicon.enabled=false
2、自己放一个图标在静态资源目录下,我放在 static目录下,图片名:favicon.ico
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pf9nJZnK-1663510275481)(SpringBoot.assets/4M0oDO.png)]
3、清除浏览器缓存!刷新网页,发现图标已经变成自己的了!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FPllzhT3-1663510275481)(SpringBoot.assets/4MBPaQ.png)]
前端交给我们的页面,是html页面。如果是我们以前开发,我们需要把它们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示,及交互等。
jsp支持非常强大的功能,包括能写Java代码,但是呢,我们现在的这种情况,SpringBoot这个项目首先是以jar的方式,不是war,像第二,我们用的还是嵌入式的Tomcat,所以能,它现在默认时不支持jsp的。
那不支持jsp,如果我们直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦,那怎么办呢?
SpringBoot推荐使用模板引擎:
模板引擎,我们其实大家听到很多,其实jsp就是一个模板引擎,还有用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但再多的模板引擎,他们的思想都是一样的,什么样一个思想呢我们来看一下这张图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W7zSoJCz-1663510275481)(SpringBoot.assets/4MvlRJ.jpg)]
模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。而这些值,从哪来呢,就是我们在后台封装一些数据。然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。只不过呢,就是说不同模板引擎之间,他们可能这个语法有点不一样。其他的我就不介绍了,我主要来介绍一下SpringBoot给我们推荐的Thymeleaf模板引擎,这模板引擎呢,是一个高级语言的模板引擎,他的这个语法更简单。而且呢,功能更强大。
我们呢,就来看一下这个模板引擎,那既然要看这个模板引擎。首先,我们来看SpringBoot里边怎么用。
怎么引入呢,对于springboot来说,什么事情不都是一个starter的事情嘛,我们去在项目中引入一下。给大家三个网址:
Thymeleaf 官网:https://www.thymeleaf.org/
Thymeleaf 在Github 的主页:https://github.com/thymeleaf/thymeleaf
Spring官方文档:找到我们对应的版本
https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter
找到对应的pom依赖:可以适当点进源码看下本来的包!
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
Maven会自动下载jar包,我们可以去看下下载的东西;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PJAwCEgN-1663510275481)(SpringBoot.assets/4MvOFU.png)]
前面呢,我们已经引入了Thymeleaf,那这个要怎么使用呢?
我们首先得按照SpringBoot的自动配置原理看一下我们这个Thymeleaf的自动配置规则,在按照那个规则,我们进行使用。
我们去找一下Thymeleaf的自动配置类:ThymeleafProperties
@ConfigurationProperties(
prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
private boolean checkTemplate = true;
private boolean checkTemplateLocation = true;
private String prefix = "classpath:/templates/";
private String suffix = ".html";
private String mode = "HTML";
private Charset encoding;
private boolean cache;
private Integer templateResolverOrder;
private String[] viewNames;
private String[] excludedViewNames;
private boolean enableSpringElCompiler;
private boolean renderHiddenMarkersBeforeCheckboxes;
private boolean enabled;
private final ThymeleafProperties.Servlet servlet;
private final ThymeleafProperties.Reactive reactive;
// ......
}
我们可以在其中看到默认的前缀和后缀!
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
我们只需要把我们的html页面放在类路径下的templates下,thymeleaf就可以帮我们自动渲染了。
使用thymeleaf什么都不需要配置,只需要将它放在指定的文件夹下即可!
测试
1、编写一个TestController
@Controller
public class TestController {
@RequestMapping("/t1")
public String test1(){
//classpath:/templates/test.html
return "test";
}
}
2、编写一个测试页面 test.html 放在 templates 目录下
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>测试页面h1>
body>
html>
3、启动项目请求测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PP1gb4VB-1663510275482)(SpringBoot.assets/4MznCF.png)]
要学习语法,还是参考官网文档最为准确,我们找到对应的版本看一下;
Thymeleaf 官网:https://www.thymeleaf.org/ , 简单看一下官网!我们去下载Thymeleaf的官方文档!
我们做个最简单的练习 :我们需要查出一些数据,在页面中展示
1、修改测试请求,增加数据传输;
@Controller
public class TestController {
@RequestMapping("/t1")
public String test1(Model model){
//存入数据
model.addAttribute("msg","Hello,Thymeleaf");
//classpath:/templates/test.html
return "test";
}
}
2、我们要使用thymeleaf,需要在html文件中导入命名空间的约束,方便提示。
我们可以去官方文档的#3 Using Texts中看一下命名空间拿来过来:
xmlns:th="http://www.thymeleaf.org"
3、我们去编写下前端页面
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>edgartitle>
head>
<body>
<h1>测试页面h1>
<div th:text="${msg}">div>
body>
html>
4、启动测试!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TCbsj3E2-1663510275482)(SpringBoot.assets/4Mz4rn.png)]
OK,入门搞定,我们来认真研习一下Thymeleaf的使用语法!
1、我们可以使用任意的 th:attr 来替换Html中原生属性的值!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1VpQYuVf-1663510275482)(SpringBoot.assets/4MzL24.jpg)]
2、我们能写哪些表达式呢?
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: _
折叠
练习测试:
1、 我们编写一个Controller,放一些数据
@RequestMapping("/t2")
public String test2(Map<String,Object> map){
//存入数据
map.put("msg","Hello
");
map.put("users", Arrays.asList("qinjiang","kuangshen"));
//classpath:/templates/test.html
return "test";
}
2、测试页面取出数据
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>狂神说title>
head>
<body>
<h1>测试页面h1>
<div th:text="${msg}">div>
<div th:utext="${msg}">div>
<h4 th:each="user :${users}" th:text="${user}">h4>
<h4>
<span th:each="user:${users}">[[${user}]]span>
h4>
body>
html>
3、启动项目测试!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qv24CGD4-1663510275482)(SpringBoot.assets/4Qpm6J.png)]
我们看完语法,很多样式,我们即使现在学习了,也会忘记,所以我们在学习过程中,需要使用什么,根据官方文档来查询,才是最重要的,要熟练使用官方文档!
在进行项目编写前,我们还需要知道一个东西,就是SpringBoot对我们的SpringMVC还做了哪些配置,包括如何扩展,如何定制。
只有把这些都搞清楚了,我们在之后使用才会更加得心应手。途径一:源码分析,途径二:官方文档!
地址 :https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-web-applications.spring-mvc.auto-configuration
Spring MVC Auto-configuration
// Spring Boot为Spring MVC提供了自动配置,它可以很好地与大多数应用程序一起工作。
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.
// 自动配置在Spring默认设置的基础上添加了以下功能:
The auto-configuration adds the following features on top of Spring’s defaults:
// 包含视图解析器
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
// 支持静态资源文件夹的路径,以及webjars
Support for serving static resources, including support for WebJars
// 自动注册了Converter:
// 转换器,这就是我们网页提交数据到后台自动封装成为对象的东西,比如把"1"字符串自动转换为int类型
// Formatter:【格式化器,比如页面给我们了一个2019-8-10,它会给我们自动格式化为Date对象】
Automatic registration of Converter, GenericConverter, and Formatter beans.
// HttpMessageConverters
// SpringMVC用来转换Http请求和响应信息的,比如我们要把一个User对象转换为JSON字符串,可以去看官网文档解释;
Support for HttpMessageConverters (covered later in this document).
// 定义错误代码生成规则的
Automatic registration of MessageCodesResolver (covered later in this document).
// 首页定制
Static index.html support.
// 图标定制
Custom Favicon support (covered later in this document).
// 初始化数据绑定器:帮我们把请求数据绑定到JavaBean中!
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
/*
如果您希望保留Spring Boot MVC功能,并且希望添加其他MVC配置(拦截器、格式化程序、视图控制器和其他功能),则可以添加自己
的@configuration类,类型为webmvcconfiguer,但不添加@EnableWebMvc。如果希望提供
RequestMappingHandlerMapping、RequestMappingHandlerAdapter或ExceptionHandlerExceptionResolver的自定义
实例,则可以声明WebMVCregistrationAdapter实例来提供此类组件。
*/
If you want to keep Spring Boot MVC features and you want to add additional MVC configuration
(interceptors, formatters, view controllers, and other features), you can add your own
@Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide
custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or
ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.
// 如果您想完全控制Spring MVC,可以添加自己的@Configuration,并用@EnableWebMvc进行注释。
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.
我们来仔细对照,看一下它怎么实现的,它告诉我们SpringBoot已经帮我们自动配置好了SpringMVC,然后自动配置了哪些东西呢?
自动配置了ViewResolver,就是我们之前学习的SpringMVC的视图解析器;
即根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发,重定向)。
我们去看看这里的源码:我们找到 WebMvcAutoConfiguration , 然后搜索ContentNegotiatingViewResolver。找到如下方法!
@Bean
@ConditionalOnBean({ViewResolver.class})
@ConditionalOnMissingBean(
name = {"viewResolver"},
value = {ContentNegotiatingViewResolver.class}
)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver.setContentNegotiationManager((ContentNegotiationManager)beanFactory.getBean(ContentNegotiationManager.class));
// ContentNegotiatingViewResolver使用所有其他视图解析器来定位视图,因此它应该具有较高的优先级
resolver.setOrder(-2147483648);
return resolver;
}
我们可以点进这类看看!找到对应的解析视图的代码;
@Override
@Nullable // 注解说明:@Nullable 即参数可为null
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) {
// 获取候选的视图对象
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
// 选择一个最适合的视图对象,然后把这个对象返回
View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;
}
}
我们继续点进去看,他是怎么获得候选的视图的呢?
getCandidateViews中看到他是把所有的视图解析器拿来,进行while循环,挨个解析!
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
throws Exception {
List<View> candidateViews = new ArrayList<>();
if (this.viewResolvers != null) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
for (MediaType requestedMediaType : requestedMediaTypes) {
List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
for (String extension : extensions) {
String viewNameWithExtension = viewName + '.' + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
if (view != null) {
candidateViews.add(view);
}
}
}
}
}
if (!CollectionUtils.isEmpty(this.defaultViews)) {
candidateViews.addAll(this.defaultViews);
}
return candidateViews;
}
所以得出结论:ContentNegotiatingViewResolver 这个视图解析器就是用来组合所有的视图解析器的
我们再去研究下它的组合逻辑,看到有个属性viewResolvers,看看它是在哪里进行赋值的!
@Override
protected void initServletContext(ServletContext servletContext) {
// 这里它是从beanFactory工具中获取容器中的所有视图解析器
// ViewRescolver.class 把所有的视图解析器来组合的
Collection<ViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList<>(matchingBeans.size());
for (ViewResolver viewResolver : matchingBeans) {
if (this != viewResolver) {
this.viewResolvers.add(viewResolver);
}
}
}
// ......
}
既然它是在容器中去找视图解析器,我们是否可以猜想,我们就可以去实现一个视图解析器了呢?
我们可以自己给容器中去添加一个视图解析器;这个类就会帮我们自动的将它组合进来;我们去实现一下
1、我们在我们的主程序中去写一个视图解析器来试试;
package com.edgar.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Locale;
// 如果我们要扩展SpringMVC,diy一些定制化的功能,只需要写这个组件,然后将它交给springboot,springboot就会帮我们自动装配。
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
//ViewResolver 实现了视图解析器接口的类,可以把它看作视图解析器
@Bean //放到bean中
public ViewResolver myViewResolver(){
return new MyViewResolver();
}
//我们写一个静态内部类,定义一个自己的视图解析器MyViewResolver就需要实现ViewResolver接口
private static class MyViewResolver implements ViewResolver {
@Override
public View resolveViewName(String s, Locale locale) throws Exception {
return null;
}
}
}
2、怎么看我们自己写的视图解析器有没有起作用呢?
我们给 DispatcherServlet 中的 doDispatch方法 加个断点进行调试一下,因为所有的请求都会走到这个方法中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7jr0J607-1663510275482)(SpringBoot.assets/4J2o4A.png)]
3、我们启动我们的项目,然后随便访问一个页面,看一下Debug信息;
找到this
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XC0fVLjY-1663510275482)(SpringBoot.assets/4J2qjf.png)]
找到视图解析器,我们看到我们自己定义的就在这里了;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s0XolHAb-1663510275483)(SpringBoot.assets/4J2Ou8.png)]
所以说,我们如果想要使用自己定制化的东西,我们只需要给容器中添加这个组件就好了!剩下的事情SpringBoot就会帮我们做了!
找到格式化转换器:
@Bean
public FormattingConversionService mvcConversionService() {
// 拿到配置文件中的格式化规则
Format format = this.mvcProperties.getFormat();
WebConversionService conversionService = new WebConversionService((new DateTimeFormatters()).dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
this.addFormatters(conversionService);
return conversionService;
}
点进去this.mvcProperties.getFormat()
:
public WebMvcProperties.Format getFormat() {
return this.format;
}
点击this.format
,跳转至:
@ConfigurationProperties(
prefix = "spring.mvc"
)
public class WebMvcProperties {
private final WebMvcProperties.Format format;
}
可以看到在我们的WebMvcProperties文件中,我们可以通过application.properties文件手动配置它!
如果配置了自己的格式化方式,就会注册到Bean中生效,我们可以在配置文件中配置日期格式化的规则:
# Date format to use, for example `dd/MM/yyyy`.
spring.mvc.format.date
# Date-time format to use, for example `yyyy-MM-dd HH:mm:ss`.
spring.mvc.format.date-time
# Time format to use, for example `HH:mm:ss`.
spring.mvc.format.time
其余的就不一一举例了,大家可以下去多研究探讨即可!
这么多的自动配置,原理都是一样的,通过这个WebMVC的自动配置原理分析,我们要学会一种学习方式,通过源码探究,得出结论;这个结论一定是属于自己的,而且一通百通。
SpringBoot的底层,大量用到了这些设计细节思想,所以,没事需要多阅读源码!得出结论;
SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(如果用户自己配置@bean),如果有就用用户配置的,如果没有就用自动配置的;
如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来!
扩展使用SpringMVC 官方文档如下:
If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.
我们要做的就是编写一个@Configuration注解类,并且类型要为WebMvcConfigurer,还不能标注@EnableWebMvc注解;我们去自己写一个;我们新建一个包叫config,写一个类MyMvcConfig;
// 因为类型要求为WebMvcConfigurer,所以我们实现其接口
// 可以使用自定义类扩展MVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 浏览器发送/test , 就会跳转到test页面;
registry.addViewController("/test").setViewName("test");
}
}
我们去浏览器访问一下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-45LBc53G-1663510275483)(SpringBoot.assets/4JWYLT.png)]
](https://imgtu.com/i/4JWYLT)
确实也跳转过来了!所以说,我们要扩展SpringMVC,官方就推荐我们这么去使用,既保SpringBoot留所有的自动配置,也能用我们扩展的配置!
我们可以去分析一下原理:
1、WebMvcAutoConfiguration 是 SpringMVC的自动配置类,里面有一个类WebMvcAutoConfigurationAdapter
2、这个类上有一个注解,在做其他自动配置时会导入:@Import(EnableWebMvcConfiguration.class)
3、我们点进EnableWebMvcConfiguration这个类看一下,它继承了一个父类:DelegatingWebMvcConfiguration
这个父类中有这样一段代码:
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
// 从容器中获取所有的webmvcConfigurer
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
// ......
}
所以得出结论:所有的WebMvcConfiguration都会被作用,不止Spring自己的配置类,我们自己的配置类当然也会被调用;
官方文档:
If you want to take complete control of Spring MVC
you can add your own @Configuration annotated with @EnableWebMvc.
全面接管即:SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己去配置!
只需在我们的配置类中要加一个@EnableWebMvc。
我们看下如果我们全面接管了SpringMVC了,我们之前SpringBoot给我们配置的静态资源映射一定会无效,我们可以去测试一下;
不加注解之前,访问首页:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qlPK5cSg-1663510275483)(SpringBoot.assets/4Jf7C9.png)]
给配置类加上注解:@EnableWebMvc
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lNL06HbA-1663510275483)(SpringBoot.assets/4JfH3R.png)]
我们发现所有的SpringMVC自动配置都失效了!回归到了最初的样子;
当然,我们开发中,不推荐使用全面接管SpringMVC
思考问题?为什么加了一个注解,自动配置就失效了!我们看下源码:
1、这里发现它是导入了一个类,我们可以继续进去看
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}
2、它继承了一个父类 WebMvcConfigurationSupport
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
// ......
}
3、我们来回顾一下Webmvc自动配置类
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
// ConditionalOnMissingBean 当Spring容器中不存在WebMvcConfigurationSupport.class的bean才为true
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
// ......
}
总结:
@EnableWebMvc将WebMvcConfigurationSupport组件导入进来了,而WebMvcAutoConfiguration自动配置类要生效需要Spring容器中不存在WebMvcConfigurationSupport的bean,
有的时候,我们的网站会去涉及中英文甚至多语言的切换,这时候我们就需要学习国际化了!
先在IDEA中统一设置properties的编码问题!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dzWeWeNm-1663510275483)(SpringBoot.assets/4fv8SJ.png)]
编写国际化配置文件,抽取页面需要显示的国际化页面消息。我们可以去登录页面查看一下,哪些内容我们需要编写国际化的配置!
1 、新建项目,添加web依赖
2 、 导入静态资源(D:\Code\IdeaProjects\SpingBoot-Web静态资源)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3hm059OH-1663510275484)(SpringBoot.assets/1660025378689.png)]
3 、编写pojo层,dao层代码
Department.java
/**
* 部门表
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
private Integer id;
private String departmentName;
}
Employee.java
package com.hlx.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 员工表
*/
@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();
}
}
DepartmentDao.java
package com.hlx.dao;
import com.hlx.pojo.Department;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* 部门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
package com.hlx.dao;
import com.hlx.pojo.Department;
import com.hlx.pojo.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
//员工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","[email protected]",0,new Department(101,"教学部")));
employees.put(1002,new Employee(1002,"BB","[email protected]",1,new Department(102,"市场部")));
employees.put(1003,new Employee(1003,"CC","[email protected]",0,new Department(103,"教研部")));
employees.put(1004,new Employee(1004,"DD","[email protected]",1,new Department(104,"运营部")));
employees.put(1005,new Employee(1005,"EE","[email protected]",0,new Department(105,"后勤部")));
}
//主键自增
private static Integer initId = 1006;
//增加一个员工
public void 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 delete(Integer id){
employees.remove(id);
}
}
1、我们在resources资源文件下新建一个i18n目录,存放国际化配置文件
2、建立一个login.properties文件,还有一个login_zh_CN.properties;发现IDEA自动识别了我们要做国际化操作;文件夹变了!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-weF28iaX-1663510275484)(SpringBoot.assets/4fj5s1.png)]
3、我们可以在这上面去新建一个文件;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y3u1WtTE-1663510275484)(SpringBoot.assets/4fvaTK.png)]
弹出如下页面:我们再添加一个英文的;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sNJzsBmY-1663510275484)(SpringBoot.assets/4fvq00.png)]
这样就快捷多了!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3aAHAQHI-1663510275484)(SpringBoot.assets/4fxEtO.png)]
4、接下来,我们就来编写配置,我们可以看到idea的i18n的配置文件下面有另外一个视图;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2NuuMbaJ-1663510275485)(SpringBoot.assets/4fxfD1.png)]
这个视图我们点击 + 号就可以直接添加属性了;我们新建一个login.tip,可以看到边上有三个文件框可以输入
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bULvqRMG-1663510275485)(SpringBoot.assets/4fzTZq.png)]
我们添加一下首页的内容!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-79gaghs4-1663510275485)(SpringBoot.assets/4hSjtf.png)]
然后依次添加其他页面内容即可!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pgpEmL2q-1663510275485)(SpringBoot.assets/4hp3Ax.png)]
然后去查看我们的配置文件;
login.properties :默认
login.btn=登录
login.password=密码
login.remember=记住我
login.tip=请登录
login.username=用户名
英文:
login.btn=Sign in
login.password=Password
login.remember=Remember me
login.tip=Please sign in
login.username=Username
中文:
login.btn=登录
login.password=密码
login.remember=记住我
login.tip=请登录
login.username=用户名
OK,配置文件步骤搞定!
我们去看一下SpringBoot对国际化的自动配置!这里又涉及到一个类:MessageSourceAutoConfiguration
里面有一个方法,这里发现SpringBoot已经自动配置好了管理我们国际化资源文件的组件 ResourceBundleMessageSource;
// 获取 properties 传递过来的值进行判断
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
if (StringUtils.hasText(properties.getBasename())) {
// 设置国际化文件的基础名(去掉语言国家代码的)
messageSource.setBasenames(
StringUtils.commaDelimitedListToStringArray(
StringUtils.trimAllWhitespace(properties.getBasename())));
}
if (properties.getEncoding() != null) {
messageSource.setDefaultEncoding(properties.getEncoding().name());
}
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
return messageSource;
}
我们真实 的情况是放在了i18n目录下,所以我们要去配置这个messages的路径;
spring.messages.basename=i18n.login
去页面获取国际化的值,查看Thymeleaf的文档,找到国际化配置中 消息表达式 message取值操作为:#{…}。我们去页面测试下:
IDEA还有提示,非常智能的!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CvrXOT1F-1663510275485)(SpringBoot.assets/4hkhb4.png)]
我们可以去启动项目,访问一下,发现已经自动识别为中文的了!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Na1C0Nhf-1663510275486)(SpringBoot.assets/4hkXrD.png)]
但是我们想要更好!可以根据按钮自动切换中文英文!
在Spring中有一个国际化的Locale (区域信息对象);里面有一个叫做LocaleResolver
(获取区域信息对象)的解析器!
我们去我们webmvc自动配置文件,寻找一下!看到SpringBoot默认配置:
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
// 容器中没有就自己配,有的话就用用户配置的
if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
// 接收头国际化分解
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}
AcceptHeaderLocaleResolver 这个类中有一个方法
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = this.getDefaultLocale();
// 默认的就是根据请求头带来的区域信息获取Locale进行国际化
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
return defaultLocale;
} else {
Locale requestLocale = request.getLocale();
List<Locale> supportedLocales = this.getSupportedLocales();
if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
} else {
return defaultLocale != null ? defaultLocale : requestLocale;
}
} else {
return requestLocale;
}
}
}
那假如我们现在想点击链接让我们的国际化资源生效,就需要让我们自己的Locale生效!
我们去自己写一个自己的LocaleResolver,可以在链接上携带区域信息!
修改一下前端页面的跳转连接:
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">Englisha>
我们去写一个处理的组件类!
package com.edgar.config;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;
// 可以在链接上携带区域信息
public class MyLocaleResolver implements LocaleResolver {
// 解析请求
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 获取请求中的语言参数
String language = request.getParameter("l");
Locale locale = Locale.getDefault(); // 如果没有就使用默认的
// 如果请求的链接携带了国际化的参数
if (StringUtils.hasLength(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下添加bean;
// 自定义的国际化组件就生效了!
@Bean
public LocaleResolver localeResolver (){
return new MyLocaleResolver();
}
我们重启项目,来访问一下,发现点击按钮可以实现成功切换!搞定收工!
1、在templates目录下新建首页index.html
注意:所有页面的静态资源都需要使用thymeleaf接管,添加xmlns:th="http://www.thymeleaf.org"
注释 th:href
th:src
所有的url路径:@{}
DOCTYPE html>
<html lang="en-US" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Signin Template for Bootstraptitle>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<link th:href="@{/css/signin.css}" rel="stylesheet">
head>
<body class="text-center">
<form class="form-signin" th:action="@{/user/login}">
<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">h1>
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}">p>
<input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
<input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" th:text="#{login.remember}">
label>
div>
<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">button>
<p class="mt-5 mb-3 text-muted">© 2017-2018p>
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">Englisha>
form>
body>
html>
2、在启动类的同级目录下新建config目录,在config目录下编写MyMvcConfig类,实现WebMvcConfigurer接口的addViewControllers方法,在类上添加@Configuration注释,
package com.edgar.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 使用自定义列扩展MVC的功能
*/
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 浏览器发送 /index,/index.html 就会跳转到index页面
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
}
}
3、关闭thymeleaf模板引擎的缓存
# 关闭模板引擎的缓存
spring.thymeleaf.cache=false
延申 : 为什么要关闭thymeleaf模板引擎的缓存?
thymeleaf是一个模板引擎,缓存的意思是加载一次模板之后便不会在加载了,对于生产环境应该加上缓存,但是在开发过程中如果打开缓存,不方便开发人员调试。试想一下,改一行html,就需要重启服务器,肯定是不方便的。
总结一下:本地开发环境下,需要把缓存关闭,否则调试成本太大。其他环境下缓存都需要打开
4、测试成功!
请求地址http://localhost:8080/
或http://localhost:8080/index.html
都会返回首页。
扩展:
我们也可以在properies文件中设置server.servlet.context-path=/demo
server.servlet.context-path=/demo
此时,请求地址将更改为http://localhost:8080/demo/xxxx
定义: server.servlet.context-path=
# Context path of the application. 应用的上下文路径,也可以称为项目路径,是构成url地址的一部分。
server.servlet.context-path
不配置时,默认为 /
,如:localhost:8080/xxxxxx
当server.servlet.context-path
有配置时,比如 /demo,此时的访问方式为localhost:8080/demo/xxxxxx
后续的thymleaf中的href和action等无需添加/demo 。
1、首页中的form表单的请求地址 /user/login
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nua8wO2e-1663510275486)(SpringBoot.assets/45qcyn.png)]
2、在启动类的同级目录下新建controller目录,在controller目录下编写LoginController类
package com.edgar.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpSession;
@Controller
public class LoginController {
@RequestMapping("/user/login")
@ResponseBody
public String login(
@RequestParam("username") String username,
@RequestParam("password") String password,
Model model,
HttpSession session) {
// 具体的业务
if (StringUtils.hasLength(username) && "123456".equals(password)) {
return "dashboard"; // 登录成功,跳转至templates目录下的dashboard.html页面
} else {
// 告诉用户,你登陆失败了!
model.addAttribute("msg","用户名或者密码错误!");
return "index";
}
}
}
3、测试成功!
如果登录成功会跳转至 dashboard.html 页面,否也会在首页红字提示 用户名或者密码错误!
1、在config目录下新建LoginHandlerInterceptor类实现HandlerInterceptor接口
package com.edgar.config;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 登陆成功之后,应该有用户的 session
Object loginUser = request.getSession().getAttribute("loginUser");
if (loginUser == null) { // 没有登陆
request.setAttribute("msg","没有权限,请先登录");
request.getRequestDispatcher("/index.html").forward(request,response); // 请求转发到首页
return false;
} else {
return true;
}
}
}
2、通过MVC扩展类,将LoginHandlerInterceptor组件注册到Spring容器中
package com.edgar.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor())
.addPathPatterns("/**") // 拦截所有的路径
.excludePathPatterns("/index.html","/","/user/login","/css/**","/img/**","/js/**"); // 排除对配置路径的拦截
}
}
登录成功后左上角显示当前登录用户
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cjrjwgpk-1663510275486)(SpringBoot.assets/1660112864741.png)]
3、测试成功!
除了配置的排除路径以外,如果用户没有登录成功,其它请求路径都会请求转发到首页!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QlCYsPZF-1663510275487)(SpringBoot.assets/45OmE6.png)]
编写ctroller层代码EmployeeController.java
@Controller
public class EmployeeController {
@Autowired
EmployeeDao employeeDao;
@RequestMapping("/emps")
public String list(Model model){
Collection<Employee> employees = employeeDao.getAll();
System.out.println("employees = " + employees);
model.addAttribute("emps",employees);
return "list";
}
}
观察html页面,发现list.html和dashboard.html中含有大量相同代码,导航栏和侧边栏可以使用thymeleaf模板引擎抽取出来,整合到commons.html中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s7YNmUFl-1663510275487)(SpringBoot.assets/1660112534673.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qto05fqR-1663510275487)(SpringBoot.assets/1660112578630.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uC9eacE9-1663510275488)(SpringBoot.assets/1660112637929.png)]
设置侧边栏点击变色效果,以及点击首页按钮返回首页
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JrhV2e2V-1663510275488)(SpringBoot.assets/1660112976551.png)]
设置鼠标点击“员工管理”变色,并跳转到视图控制器页面 /emps
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3wGQ73NY-1663510275488)(SpringBoot.assets/1660113029713.png)]
员工展示 ,其中 emps = employeeDao.getAll()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eJW27jmL-1663510275488)(SpringBoot.assets/1660113229271.png)]
测试效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LGIn5o4G-1663510275489)(SpringBoot.assets/1660113351266.png)]
编写controller
/**
* 跳转到添加员工页面
* @return
*/
@GetMapping("/add")
public String toAddpage(Model model){
//查出所有部门的信息
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments",departments);
return "add";
}
/**
*提交添加操作
* @param employee
* @return
*/
@PostMapping("/sumbitadd")
public String addEmp(Employee employee){
System.out.println("employee = " + employee);
// 调用底层业务方法保存员工信息
employeeDao.save(employee);
return "redirect:/emps";
}
新建一个add.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>Dashboard Template for Bootstraptitle>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<link th:href="@{/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::sidebar(active='list.html')}">div>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<form th:action="@{/sumbitadd}" method="post">
<div class="form-group">
<label>employeeNamelabel>
<label>
<input type="text" name="lastName" class="form-control" placeholder="员工姓名">
label>
div>
<div class="form-group">
<label>emaillabel>
<label>
<input type="text" name="email" class="form-control" placeholder="请输入邮箱">
label>
div>
<div class="form-group">
<label>genderlabel>
<div class="form-check form-check-inline">
<label>
<input class="form-check-input" type="radio" name="gender" value="1">
label>
<label class="form-check-label">男label>
div>
<div class="form-check form-check-inline">
<label>
<input class="form-check-input" type="radio" name="gender" value="0">
label>
<label class="form-check-label">女label>
div>
div>
<div class="form-group">
<label>departmentlabel>
<label>
<select class="form-control" name="department.id">
<option th:each="dept:${departments}"
th:text="${dept.getDepartmentName()}"
th:value="${dept.getId()}">option>
select>
label>
div>
<div class="form-group">
<label>birthlabel>
<label>
<input type="text" name="birth" class="form-control" placeholder="日期格式:1990/01/01">
label>
div>
<div>
<button type="submit" class="btn btn-primary">添加button>
div>
form>
main>
div>
div>
<script type="text/javascript" src="asserts/js/jquery-3.2.1.slim.min.js">script>
<script type="text/javascript" src="asserts/js/popper.min.js">script>
<script type="text/javascript" src="asserts/js/bootstrap.min.js">script>
<script type="text/javascript" src="asserts/js/feather.min.js">script>
<script>
feather.replace()
script>
<script type="text/javascript" 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页面增加一个添加按钮及跳转链接
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SUl90x3a-1663510275489)(SpringBoot.assets/1660122247408.png)]
修改add页面的表单内容(对照员工实体类为表单添加name属性并设置跳转链接以及提交方式)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qcxg0OY0-1663510275489)(SpringBoot.assets/1660122390452.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DdzAMXJv-1663510275490)(SpringBoot.assets/1660122444167.png)]
表单中的·日期格式可通过properies文件修改
#设置日期格式
spring.mvc.date-format=yyyy-MM-dd
编写controller层代码
/**
* 点击修改,跳转到员工的修改页面
* @param id
* @param model
* @return
*/
@GetMapping("/emp/{id}")
public String toUpdateEmp(@PathVariable("id") Integer id, Model model){
// 查出原来的数据
Employee employee = employeeDao.getEmployeeById(id);
model.addAttribute("emp",employee);
// 查出所有的部门信息
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments",departments);
return "update";
}
/**
*提交修改操作
* @param employee
* @return
*/
@PostMapping("/updateEmp")
public String updateEmp(Employee employee){
System.out.println("employee = " + employee);
// 调用底层业务方法保存员工信息
employeeDao.save(employee);
return "redirect:/emps";
}
复制添加add.html更改为修改页面update.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>Dashboard Template for Bootstraptitle>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<link th:href="@{/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::sidebar(active='list.html')}">div>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<form th:action="@{/updateEmp}" method="post">
<input type="hidden" name="id" th:value="${emp.getId()}">
<div class="form-group">
<label>employeeNamelabel>
<label>
<input th:value="${emp.getLastName()}" type="text" name="lastName" class="form-control" placeholder="员工姓名">
label>
div>
<div class="form-group">
<label>emaillabel>
<label>
<input th:value="${emp.getEmail()}" type="text" name="email" class="form-control" placeholder="请输入邮箱">
label>
div>
<div class="form-group">
<label>genderlabel>
<div class="form-check form-check-inline">
<label>
<input th:checked="${emp.getGender()==1}" class="form-check-input" type="radio" name="gender" value="1">
label>
<label class="form-check-label">男label>
div>
<div class="form-check form-check-inline">
<label>
<input th:checked="${emp.getGender()==0}" class="form-check-input" type="radio" name="gender" value="0">
label>
<label class="form-check-label">女label>
div>
div>
<div class="form-group">
<label>departmentlabel>
<label>
<select class="form-control" name="department.id">
<option th:selected="${dept.getId()==emp.getDepartment().getId()}" th:each="dept:${departments}"
th:text="${dept.getDepartmentName()}"
th:value="${dept.getId()}">option>
select>
label>
div>
<div class="form-group">
<label>birthlabel>
<label>
<input th:value="${#dates.format(emp.getBirth(),'yyyy/MM/dd')}" type="text" name="birth" class="form-control" placeholder="日期格式:1990/01/01">
label>
div>
<div>
<button type="submit" class="btn btn-primary">添加button>
div>
form>
main>
div>
div>
<script type="text/javascript" src="asserts/js/jquery-3.2.1.slim.min.js">script>
<script type="text/javascript" src="asserts/js/popper.min.js">script>
<script type="text/javascript" src="asserts/js/bootstrap.min.js">script>
<script type="text/javascript" src="asserts/js/feather.min.js">script>
<script>
feather.replace()
script>
<script type="text/javascript" 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
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J5L2JG0R-1663510275490)(SpringBoot.assets/1660183804838.png)]
修改update.html页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cSzUwWiX-1663510275490)(SpringBoot.assets/1660183991773.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7kuUyEyb-1663510275490)(SpringBoot.assets/1660184065947.png)]
修改list.tml页面的删除按钮
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CfhW4Fjc-1663510275491)(SpringBoot.assets/1660184602225.png)]
编写ctroller层代码
/**
* 删除员工
* @param id
* @return
*/
@GetMapping("/delEmp/{id}")
public String delEmp(@PathVariable("id") Integer id){
employeeDao.delete(id);
return "redirect:/emps";
}
tymeleaf模板自带处理404错误的方法,只需要在templates下新建error文件夹,并把404.html放在error下即可生效。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bOvUvF4J-1663510275491)(SpringBoot.assets/1660185482484.png)]
编写ctroller层代码
/**
* 注销用户
* @param session
* @return
*/
@RequestMapping("/user/logout")
public String loginOut(HttpSession session){
session.invalidate();
return "redirect:/index.html";
}
在commons.html中修改注销按钮
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MYoSyVUy-1663510275491)(SpringBoot.assets/1660185391928.png)]
对于数据访问层,无论是 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.2.5.RELEASE/reference/htmlsingle/#using-boot-starter
1、我去新建一个项目测试:springboot-04-data ; 引入相应的模块!基础模块
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vFKBMrZg-1663510275491)(SpringBoot.assets/4Let8P.png)]
2、项目建好之后,发现自动帮我们导入了如下的启动器:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
3、编写yaml配置文件连接数据库;
spring:
datasource:
username: root
password: 123456
# 假如时区报错了,就增加一个时区的配置 serverTimezone=UTC
url: jdbc:mysql://127.0.0.1:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
4、配置完这一些东西后,我们就可以直接去使用了,因为SpringBoot已经默认帮我们进行了自动配置;去测试类测试一下
@SpringBootTest
class Springboot04DataApplicationTests {
@Autowired //自动注入数据源(yaml中连接数据库的配置)
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
// 查看一下默认的数据源 : com.zaxxer.hikari.HikariDataSource
System.out.println(dataSource.getClass());
// 获得数据库连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
// xxxx Template : SpringBoot已经配置好的模板bean,拿来即用 CRUD 列如:jdbcTemplate
// 关闭连接
connection.close();
}
}
结果:我们可以看到他默认给我们配置的数据源为 : class com.zaxxer.hikari.HikariDataSource , 我们并没有手动配置
我们来全局搜索一下,找到数据源的所有自动配置都在 :DataSourceAutoConfiguration文件:
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}
这里导入的类都在 DataSourceConfiguration 配置类下,可以看出 Spring Boot 2.5.5 默认使用HikariDataSource 数据源,而以前版本,如 Spring Boot 1.5 默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源;
HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀;
可以使用 spring.datasource.type 指定自定义的数据源类型,值为 要使用的连接池实现的完全限定名。
关于数据源我们并不做介绍,有了数据库连接,显然就可以 CRUD 操作数据库了。但是我们需要先了解一个对象 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 包下的 JdbcTemplateConfiguration 类
JdbcTemplate主要提供以下几类方法:
编写一个Controller,注入 jdbcTemplate,编写测试方法进行访问测试;
package com.edgar.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@RestController
public class JDBCController {
/**
* Spring Boot 默认提供了数据源,默认提供了 org.springframework.jdbc.core.JdbcTemplate
* JdbcTemplate 中会自己注入数据源,用于简化 JDBC操作
* 还能避免一些常见的错误,使用起来也不用再自己来关闭数据库连接
*/
@Autowired
JdbcTemplate jdbcTemplate;
// 查询数据库的所有信息
// 没有实体类,数据库中的东西,怎么获取? 万能的Map
@GetMapping("/userList")
public List<Map<String, Object>> userList() {
String sql = "select * from mybatis.user";
List<Map<String, Object>> list_maps = jdbcTemplate.queryForList(sql);
return list_maps;
}
// 新增一个用户
@GetMapping("/addUser")
public String addUser() {
String sql = "insert into mybatis.user(id,name,pwd) values (8,'edgar',123123)";
jdbcTemplate.update(sql);
return "addUser-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] = "小明2";
objects[1] = "zzzzzzzz";
jdbcTemplate.update(sql,objects);
return "updateUser-OK";
}
// 删除用户
@GetMapping("/deleteUser/{id}")
public String deleteUser(@PathVariable("id") int id) {
String sql = "delete from mybatis.user where id = ? ";
jdbcTemplate.update(sql,id);
return "deleteUser-OK";
}
}
测试请求,结果正常;
到此,CURD的基本操作,使用 JDBC 就搞定了。
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 基本配置参数如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pk68wH1I-1663510275492)(SpringBoot.assets/4vNfdH.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pBPbRV3j-1663510275492)(SpringBoot.assets/4vNoWt.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N14a3sbL-1663510275492)(SpringBoot.assets/4vNFIA.jpg)]
1、添加上 Druid 数据源依赖。
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.21version>
dependency>
2、切换数据源;之前已经说过 Spring Boot 2.0 以上默认使用 com.zaxxer.hikari.HikariDataSource 数据源,但可以 通过 spring.datasource.type 指定数据源。
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource # 自定义数据源
3、数据源切换之后,在测试类中注入 DataSource,然后获取到它,输出一看便知是否成功切换;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wnlq4B5B-1663510275493)(SpringBoot.assets/4vUchn.jpg)]
4、切换成功!既然切换成功,就可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数 等设置项;可以查看源码
spring:
datasource:
username: root
password: 123456
#?serverTimezone=UTC解决时区的报错
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
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
5、导入Log4j 的依赖
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
6、解决log4j警告,在SpringBoot项目的resources目录下创建一个log4j.properties文件。
log4j.rootLogger=DEBUG, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
7、现在需要程序员自己为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了;我们需要 自己添加 DruidDataSource 组件到容器中,并绑定属性;
package com.edgar.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DruidConfig {
/*
将自定义的 Druid数据源添加到容器中,不再让 Spring Boot 自动创建
绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource从而让它们生效
@ConfigurationProperties(prefix = "spring.datasource"):作用就是将 全局配置文件中
前缀为 spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中
*/
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource() {
return new DruidDataSource();
}
}
8、去测试类中测试一下;看是否成功!
package com.edgar;
import com.alibaba.druid.pool.DruidDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@SpringBootTest
class Springboot04DataApplicationTests {
@Autowired
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
//看一下默认数据源
System.out.println(dataSource.getClass());
//获得连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());
//关闭连接
connection.close();
}
}
输出结果 :可见配置参数已经生效!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WE0ScFa6-1663510275493)(SpringBoot.assets/4vajGn.png)]
Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的 web 页面。
所以第一步需要在DruidConfig.java配置类中设置 Druid 的后台管理页面,比如 登录账号、密码 等;配置后台管理;
//配置 Druid 监控管理后台的Servlet;
//内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式
@Bean
public ServletRegistrationBean statViewServlet() {
ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
// 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet
// 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
Map<String, String> initParams = new HashMap<>();
initParams.put("loginUsername", "admin"); //后台管理界面的登录账号
initParams.put("loginPassword", "123456"); //后台管理界面的登录密码
//后台允许谁可以访问
//initParams.put("allow", "localhost"):表示只有本机可以访问
//initParams.put("allow", ""):为空或者为null时,表示允许所有访问
initParams.put("allow", "");
//deny:Druid 后台拒绝谁访问
//initParams.put("kuangshen", "192.168.1.20");表示禁止此ip访问
//设置初始化参数
bean.setInitParameters(initParams);
return bean;
}
配置完毕后,我们可以选择访问 :http://localhost:8080/druid/login.html
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y6TWWZJ0-1663510275493)(SpringBoot.assets/4vdnsK.png)]
进入之后
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gJWnlpRI-1663510275493)(SpringBoot.assets/4vd3id.png)]
访问http://localhost:8080/druid/wall.html查看防御墙
配置 Druid web 监控 filter 过滤器
在DruidConfig.java配置类中配置 filter 过滤器
//配置 Druid 监控 之 web 监控的 filter
//WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计
@Bean
public FilterRegistrationBean webStatFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());
//exclusions:设置哪些请求进行过滤排除掉,从而不进行统计
Map<String, String> initParams = new HashMap<>();
initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
bean.setInitParameters(initParams);
//"/*" 表示过滤所有请求
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
平时在工作中,按需求进行配置即可,主要用作监控!
官方文档: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
github官方文档:https://github.com/mybatis/spring-boot-starter/blob/master/mybatis-spring-boot-autoconfigure/src/site/markdown/index.md
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YgMyrGEO-1663510275494)(SpringBoot.assets/5nHUgJ.png)]
新建springboot-05-mybatis ,导入Spring Web依赖,JDBC API MySQL Driver
1、导入 MyBatis 所需要的依赖
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.0version>
dependency>
2、配置数据库连接信息(这里使用的的是druid的数据源,druid的配置请回顾 SpringBoot12:整合Druid
)
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://139.224.1.140:3306/mybatis?useUnicode=true&characterEncoding=utf8&useSSL=false
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
#mybatis配置
mybatis:
type-aliases-package: com.edgar.pojo
mapper-locations: classpath:mybatis/mapper/*.xml
折叠
3、测试数据库是否连接成功!
package com.edgar;
import com.alibaba.druid.pool.DruidDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@SpringBootTest
class Springboot05MybatisApplicationTests {
@Autowired
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
//看一下默认数据源
System.out.println(dataSource.getClass());
//获得连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());
//关闭连接
connection.close();
}
}
4、创建实体类
User.java
package com.edgar.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
5、创建mapper目录以及对应的 Mapper 接口
UserMapper.java
package com.edgar.mapper;
import com.edgar.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
//@Mapper : 表示本类是一个 MyBatis 的 Mapper
//或者在Spring boot的启动类上添加@MapperScan("com.hlx.mapper")
@Mapper
@Repository
public interface UserMapper {
List<User> queryUserList();
User queryUserById(int id);
int addUser(User user);
int updateUser(User user);
int deleteUser(int id);
}
添加mybaits配置到application.yml中
#mybatis配置
mybatis:
type-aliases-package: com.edgar.pojo
mapper-locations: classpath:mybatis/mapper/*.xml
6、对应的Mapper映射文件
路径:src/main/resources/mybatis/mapper/UserMapper.xml
UserMapper.xml
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.edgar.mapper.UserMapper">
<select id="queryUserList" resultType="User"> //resultType="User" 输出
select * from mybatis.user;
select>
//parameterType="int" 插入
<select id="queryUserById" resultType="User" parameterType="int">
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 name=#{name},pwd=#{pwd} where id=#{id}
update>
<delete id="deleteUser" parameterType="int">
delete from mybatis.user where id=#{id}
delete>
mapper>
8、编写 UserController 进行测试!
package com.edgar.controller;
import com.edgar.mapper.UserMapper;
import com.edgar.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class UserController {
@Autowired
UserMapper userMapper;
@GetMapping("/queryUserList")
public List<User> queryUserList(){
List<User> userList = userMapper.queryUserList();
return userList;
}
@GetMapping("/addUser")
public String addUser(){
userMapper.addUser(new User(7,"小毛","333333"));
return "OK";
}
@GetMapping("/updateUser")
public String updateUser(){
userMapper.updateUser(new User(7,"小毛","4444444"));
return "Ok";
}
@GetMapping("/deleteUser")
public String deleteUser(){
userMapper.deleteUser(7);
return "OK";
}
}
启动项目访问进行测试!
在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。
市面上存在比较有名的:Shiro,Spring Security !
这里需要阐述一下的是,每一个框架的出现都是为了解决某一问题而产生了,那么Spring Security框架的出现是为了解决什么问题呢?
首先我们看下它的官网介绍:Spring Security官网地址
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。
Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求
从官网的介绍中可以知道这是一个权限框架。想我们之前做项目是没有使用框架是怎么控制权限的?对于权限 一般会细分为功能权限,访问权限,和菜单权限。代码会写的非常的繁琐,冗余。
怎么解决之前写权限代码繁琐,冗余的问题,一些主流框架就应运而生而Spring Scecurity就是其中的一种。
Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说**,Web 应用的安全性包括用户认证(Authentication)和用户授权**(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
1、新建一个初始的springboot项目,导入web模块,thymeleaf模块
2、导入静态资源
index.html
|views
|level1
1.html
2.html
3.html
|level2
1.html
2.html
3.html
|level3
1.html
2.html
3.html
Login.html
3、controller跳转!
package com.edgar.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class RouterController {
@RequestMapping({"/","/index"})
public String index(){
return "index";
}
@RequestMapping("/toLogin")
public String toLogin(){
return "views/login";
}
@RequestMapping("/level1/{id}")
public String level1(@PathVariable("id") int id){
return "views/level1/"+id;
}
@RequestMapping("/level2/{id}")
public String level2(@PathVariable("id") int id){
return "views/level2/"+id;
}
@RequestMapping("/level3/{id}")
public String level3(@PathVariable("id") int id){
return "views/level3/"+id;
}
}
4、测试实验环境是否OK!
Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
“授权” (Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
这个概念是通用的,而不是只在Spring Security 中存在。
目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能
1、引入 Spring Security 模块
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
参考官网:https://docs.spring.io/spring-security/site/docs/current/reference/html5/#jc
2、Custom DSLs (官网简写的配置类)
@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.apply(customDsl())
.flag(true)
.and()
...;
}
}
3、编写基础配置类
package com.edgar.config;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@EnableWebSecurity // 开启WebSecurity模式
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
4、定制请求的授权规则
package com.edgar.config;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity // 开启WebSecurity模式
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");
}
}
5、测试一下:发现除了首页都进不去了!因为我们目前没有登录的角色,因为请求需要登录的角色拥有对应的权限才可以!
6、在configure()方法中加入以下配置,开启自动配置的登录功能!
// 开启自动配置的登录功能
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http.formLogin();
7、测试一下:发现,没有权限的时候,会跳转到登录的页面!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3P7D3aIG-1663510275494)(SpringBoot.assets/553B6A.png)]
8、查看刚才登录页的注释信息;
我们可以定义认证规则,重写configure(AuthenticationManagerBuilder auth)方法
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿....
auth.inMemoryAuthentication()
.withUser("edgar").password("123456").roles("vip2","vip3")
.and()
.withUser("admin").password("123456").roles("vip1","vip2","vip3")
.and()
.withUser("guest").password("123456").roles("vip1","vip2");
}
9、测试,我们可以使用这些账号登录进行测试!发现会报错!
There is no PasswordEncoder mapped for the id “null”
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xt2pO5R7-1663510275494)(SpringBoot.assets/55Gpvj.jpg)]
10、原因,我们要将前端传过来的密码进行某种方式加密,否则就无法登录,修改代码
// 认证 , springboot 2.1.X 可以直接使用
// 密码编码
// 在spring security 5.0+ 新增了很多加密方式
// 要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
// spring security 官方推荐的是使用bcrypt加密方式。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 这些数据应该从数据库中读 auth.jdbcAuthentication() 示例:https://blog.csdn.net/qq_41754409/article/details/101115900
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("edgar").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
11、测试,发现,登录成功,并且每个角色只能访问自己认证下的规则!搞定
1、开启自动配置的注销的功能
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//....
//开启自动配置的注销的功能
// /logout 注销请求
http.logout();
}
2、我们在前端,增加一个注销的按钮,index.html 导航栏中
<a class="item" th:href="@{/logout}">
<i class="sign-out icon">i> 注销
a>
3、我们可以去测试一下,登录成功后点击注销,发现注销完毕会跳转到登录页面!
4、但是,我们想让他注销成功后,依旧可以跳转到首页,该怎么处理呢?
// .logoutSuccessUrl("/"); 注销成功来到首页
http.logout().logoutSuccessUrl("/");
5、测试,注销完毕后,发现跳转到首页OK
6、我们现在又来一个需求:用户没有登录的时候,导航栏上只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如edgar这个用户,它只有 vip2,vip3功能,那么登录则只显示这两个功能,而vip1的功能菜单不显示!这个就是真实的网站情况了!该如何做呢?
我们需要结合thymeleaf中的一些功能
sec:authorize=“isAuthenticated()”:是否认证登录!来显示不同的页面
Maven依赖:
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
<version>3.0.4.RELEASEversion>
dependency>
7、修改我们的 前端页面
导入命名空间
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
修改导航栏,增加认证判断
<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>
8、重启测试,我们可以登录试试看,登录成功后确实,显示了我们想要的页面;
9、如果注销404了,就是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在spring security中关闭csrf功能;我们试试:在 配置中增加 http.csrf().disable();
http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.logout().logoutSuccessUrl("/");
10、我们继续将下面的角色功能块认证完成!
<div class="column" sec:authorize="hasRole('vip1')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 1h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon">i> Level-1-1a>div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon">i> Level-1-2a>div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon">i> Level-1-3a>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 2h5>
<hr>
<div><a th:href="@{/level2/1}"><i class="bullhorn icon">i> Level-2-1a>div>
<div><a th:href="@{/level2/2}"><i class="bullhorn icon">i> Level-2-2a>div>
<div><a th:href="@{/level2/3}"><i class="bullhorn icon">i> Level-2-3a>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 3h5>
<hr>
<div><a th:href="@{/level3/1}"><i class="bullhorn icon">i> Level-3-1a>div>
<div><a th:href="@{/level3/2}"><i class="bullhorn icon">i> Level-3-2a>div>
<div><a th:href="@{/level3/3}"><i class="bullhorn icon">i> Level-3-3a>div>
div>
div>
div>
div>
11、测试一下!
12、权限控制和注销搞定!
现在的情况,我们只要登录之后,关闭浏览器,再登录,就会让我们重新登录,但是很多网站的情况,就是有一个记住密码的功能,这个该如何实现呢?很简单
1、开启记住我功能
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//。。。。。。。。。。。
//记住我
http.rememberMe();
}
2、我们再次启动项目测试一下,发现登录页多了一个记住我功能,我们登录之后关闭 浏览器,然后重新打开浏览器访问,发现用户依旧存在!
思考:如何实现的呢?其实非常简单
我们可以查看浏览器的cookie
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qRA7UdkP-1663510275495)(SpringBoot.assets/5Tu8ET.png)]
3、我们点击注销的时候,可以发现,spring security 帮我们自动删除了这个 cookie
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lNDVaUc8-1663510275495)(SpringBoot.assets/5TKKiD.png)]
4、结论:登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了。如果点击注销,则会删除这个cookie,具体的原理我们在JavaWeb阶段都讲过了,这里就不在多说了!
现在这个登录页面都是spring security 默认的,怎么样可以使用我们自己写的Login界面呢?
1、在刚才的登录页配置后面指定 loginpage
http.formLogin().loginPage("/toLogin");
2、然后前端也需要指向我们自己定义的 login请求
<a class="item" th:href="@{/toLogin}">
<i class="address card icon">i> 登录
a>
3、我们登录,需要将这些信息发送到哪里,我们也需要配置,login.html 配置提交请求及方式,方式必须为post:
在 loginPage()源码中的注释上有写明:
*
* - /login GET - the login form
* - /login POST - process the credentials and if valid authenticate the user
* - /login?error GET - redirect here for failed authentication attempts
* - /login?logout GET - redirect here after successfully logging out
*
4、这个请求提交上来,我们还需要验证处理,怎么做呢?我们可以查看formLogin()方法的源码!我们配置接收登录的用户名和密码的参数!
http.formLogin()
.loginPage("/toLogin")
.usernameParameter("user")
.passwordParameter("pwd")
.loginProcessingUrl("/login"); // 登陆表单提交请求
5、在登录页增加记住我的多选框
<input type="checkbox" name="remember"> 记住我
6、后端验证处理!
//定制记住我的参数!
http.rememberMe().rememberMeParameter("remember");
7、测试,OK
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V7Ad58sN-1663510275495)(SpringBoot.assets/IISKL8.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7BUXE6WY-1663510275495)(SpringBoot.assets/IISweU.png)]
从外部来看Shiro,即从应用程序角度来观察如何使用shiro完成工作:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wnOAJ7cF-1663510275496)(SpringBoot.assets/IISj0S.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qOwW4Eqh-1663510275496)(SpringBoot.assets/IIpa9A.png)]
查看官网文档:http://shiro.apache.org/tutorial.html
官方的quickstart:https://github.com/apache/shiro/tree/master/samples/quickstart/
创建一个maven父工程:springboot-07-shiro,用于学习Shiro,删掉不必要的东西
创建一个普通的Maven子工程:hello-shiro
根据官方文档,我们来导入Shiro的依赖
<dependencies>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>1.8.0version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>jcl-over-slf4jartifactId>
<version>2.0.0-alpha5version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>2.0.0-alpha5version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
dependencies>
编写Shiro配置
log4j.properties
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n
# General Apache libraries
log4j.logger.org.apache=WARN
# Spring
log4j.logger.org.springframework=WARN
# Default Shiro logging
log4j.logger.org.apache.shiro=INFO
# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
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
编写我们的QuickStrat
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;
/**
* Simple Quickstart application showing how to use Shiro's API.
* 简单的快速启动应用程序,演示如何使用Shiro的API。
*
* @since 0.9 RC2
*/
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
// The easiest way to create a Shiro SecurityManager with configured
// realms, users, roles and permissions is to use the simple INI config.
// We'll do that by using a factory that can ingest a .ini file and
// return a SecurityManager instance:
// 使用类路径根目录下的shiro.ini文件
// Use the shiro.ini file at the root of the classpath
// (file: and url: prefixes load from files and urls respectively):
DefaultSecurityManager securityManager = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
securityManager.setRealm(iniRealm);
SecurityUtils.setSecurityManager(securityManager);
// get the currently executing user:取当前正在执行的用户
Subject currentUser = SecurityUtils.getSubject();
// 用会话做一些事情(不需要web或EJB容器!!!
// Do some stuff with a Session (no need for a web or EJB container!!!)
Session session = currentUser.getSession(); // 设置Session的值!
session.setAttribute("someKey", "aValue"); // 设置Session的值!
String value = (String) session.getAttribute("someKey"); // 从session中获取值
if (value.equals("aValue")) { // 判断session中是否存在这个值!
log.info("Retrieved the correct value! [" + value + "]");
}
// 测试当前的用户是否已经被认证,即是否已经登录!
// let's login the current user so we can check against roles and permissions:
if (!currentUser.isAuthenticated()) { // isAuthenticated();是否认证
// 将用户名和密码封装为 UsernamePasswordToken ;
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true); // 记住我功能
try {
currentUser.login(token); // 执行登录,可以登录成功的!
} catch (UnknownAccountException uae) { // 如果没有指定的用户,则UnknownAccountException异常
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) { // 如果没有指定的用户,则UnknownAccountException异常
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?
}
}
//说出他们是谁:
//say who they are:
//打印他们的标识主体(在本例中为用户名):
//print their identifying principal (in this case, a username):
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);
}
}
折叠
测试运行一下,成功!
注意:shiro的默认日志jar包是commons-logging ,我这边使用的是log4j
导入了一堆包!
类的描述
/**
* Simple Quickstart application showing how to use Shiro's API.
* 简单的快速启动应用程序,演示如何使用Shiro的API。
*/
创建SecurityManager的实例对象
// The easiest way to create a Shiro SecurityManager with configured
// realms, users, roles and permissions is to use the simple INI config.
// We'll do that by using a factory that can ingest a .ini file and
// return a SecurityManager instance:
// 使用类路径根目录下的shiro.ini文件
// Use the shiro.ini file at the root of the classpath
// (file: and url: prefixes load from files and urls respectively):
DefaultSecurityManager securityManager = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
securityManager.setRealm(iniRealm);
SecurityUtils.setSecurityManager(securityManager);
获取当前的Subject
// get the currently executing user:取当前正在执行的用户
Subject currentUser = SecurityUtils.getSubject();
session的操作
// 用会话做一些事情(不需要web或EJB容器!!!
// Do some stuff with a Session (no need for a web or EJB container!!!)
Session session = currentUser.getSession(); // 设置Session的值!
session.setAttribute("someKey", "aValue"); // 设置Session的值!
String value = (String) session.getAttribute("someKey"); // 从session中获取值
if (value.equals("aValue")) { // 判断session中是否存在这个值!
log.info("Retrieved the correct value! [" + value + "]");
}
用户认证功能
// 测试当前的用户是否已经被认证,即是否已经登录!
// let's login the current user so we can check against roles and permissions:
if (!currentUser.isAuthenticated()) { // isAuthenticated();是否认证
// 将用户名和密码封装为 UsernamePasswordToken ;
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true); // 记住我功能
try {
currentUser.login(token); // 执行登录,可以登录成功的!
} catch (UnknownAccountException uae) { // 如果没有指定的用户,则UnknownAccountException异常
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?
}
}
//说出他们是谁:
//say who they are:
//打印他们的标识主体(在本例中为用户名):
//print their identifying principal (in this case, a username):
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);
OK,一个简单的Shiro程序体验,我们就在官方的带领下初步认识了!
搭建一个SpringBoot项目springboot-08-shiro、选中web模块即可!
导入Maven依赖 thymeleaf
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
编写一个页面 index.html templates
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>首页h1>
<p th:text="${msg}">p>
body>
html>
编写controller进行访问测试
package com.edgar.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello,Shiro");
return "index";
}
}
测试访问首页!
回顾核心API:
步骤:
导入Shiro 和 spring整合的依赖
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.4.1version>
dependency>
编写Shiro 配置类 config包
package com.edgar.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
// 创建 ShiroFilterFactoryBean:3
// 创建 DefaultWebSecurityManager:2
// 创建realm对象,需要自定义类:1
}
我们倒着来,先想办法创建一个 realm
对象
我们需要自定义一个 realm 的类,用来编写一些查询的方法,或者认证与授权的逻辑
package com.edgar.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;
// 自定义的 UserRealm
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("执行了=>认证doGetAuthenticationInfo");
return null;
}
}
将这个类注册到我们的Bean中! ShiroConfig
package com.edgar.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
// 创建 ShiroFilterFactoryBean:3
// 创建 DefaultWebSecurityManager:2
// 创建realm对象,需要自定义类:1
@Bean(name = "userRealm")
public UserRealm userRealm() {
return new UserRealm();
}
}
接下来我们该去创建 DefaultWebSecurityManager
了
// 创建 DefaultWebSecurityManager:2
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 关联UserRealm
securityManager.setRealm(userRealm);
return securityManager;
}
接下来我们该去创建 ShiroFilterFactoryBean
了
// 创建 ShiroFilterFactoryBean:3
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
最后完整的配置:
package com.edgar.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;
@Configuration
public class ShiroConfig {
// 创建 ShiroFilterFactoryBean:3
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
// 创建 DefaultWebSecurityManager:2
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 关联UserRealm
securityManager.setRealm(userRealm);
return securityManager;
}
// 创建 realm对象,需要自定义类:1
@Bean(name = "userRealm")
public UserRealm userRealm() {
return new UserRealm();
}
}
编写两个页面、在templates目录下新建一个 user 目录 add.html
update.html
<body>
<h1>addh1>
body>
<body>
<h1>updateh1>
body>
编写跳转到页面的controller
@RequestMapping("/user/add")
public String add(){
return "user/add";
}
@RequestMapping("/user/update")
public String update(){
return "user/update";
}
在index页面上,增加跳转链接
<a th:href="@{/user/add}">adda> | <a th:href="@{/user/update}">updatea>
测试页面跳转是否OK
准备添加Shiro的内置过滤器
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
// 添加shiro的内置过滤器
/*
anon:无需认证就可以访问
authc:必须认证了才能访问
user:必须拥有 记住我 功能才能用
perms:拥有对某个资源的权限才能访问
role:拥有某个角色权限才能访问
*/
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/add","authc");
filterMap.put("/user/update","authc");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}
再起启动测试,访问链接进行测试!拦截OK!但是发现,点击后会跳转到404页面,这 个不是我们想要的效果,我们需要自己定义一个Login页面!
我们编写一个自己的Login页面
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>登录h1>
<hr>
<form action="">
<p>用户名:<input type="text" name="username">p>
<p>密码:<input type="text" name="password">p>
<p><input type="submit">p>
form>
body>
html>
编写跳转的controller
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
在shiro中配置一下! ShiroFilterFactoryBean()
方法下面
// 设置登陆的请求
bean.setLoginUrl("/toLogin");
再次测试,成功的跳转到了我们指定的Login页面!
优化一下代码,我们这里的拦截可以使用 通配符来操作
Map<String, String> filterMap = new LinkedHashMap<>();
//filterMap.put("/user/add","authc");
//filterMap.put("/user/update","authc");
filterMap.put("/user/*","authc");
bean.setFilterChainDefinitionMap(filterMap);
测试,完全OK!
编写一个登录的controller
@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 ice) { // 密码不存在
model.addAttribute("msg","密码错误");
return "login";
}
}
在前端修改对应的信息输出或者请求!
登录页面增加一个 msg 提示:
<p th:text="${msg}" style="color: red">p>
给表单增加一个提交地址:
<form th:action="@{/login}">
<p>用户名:<input type="text" name="username">p>
<p>密码:<input type="text" name="password">p>
<p><input type="submit">p>
form>
理论,假设我们提交了表单,他会经过我们刚才编写的UserRealm,我们提交测试一下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-589c6D3r-1663510275496)(SpringBoot.assets/IX13VA.png)]
确实执行了我们的认证逻辑!
在 UserRealm 中编写用户认证的判断逻辑
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthenticationInfo");
//假设数据库的用户名和密码
String name = "root";
String password = "123456";
// 1.判断用户名
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
if (!name.equals(usernamePasswordToken.getUsername())) {
// 用户名不存在
return null; // shiro底层就会抛出 UnknownAccountException
}
// 2. 验证密码,我们可以使用一个AuthenticationInfo实现类SimpleAuthenticationInfo
// shiro会自动帮我们验证!重点是第二个参数就是要验证的密码!
return new SimpleAuthenticationInfo("",password,"");
}
测试一下!成功实现登录的认证操作!
导入Mybatis相关依赖
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.21version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.0version>
dependency>
编写配置文件-连接配置 application.yml
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://139.224.1.140:3306/mybatis?useUnicode=true&characterEncoding=utf8&useSSL=false
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
编写mybatis的配置 application.properties
#别名配置
mybatis.type-aliases-package=com.edgar.pojo
mybatis.mapper-locations=classpath:mapper/*.xml
编写实体类,引入Lombok
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.20version>
dependency>
package com.edgar.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
编写Mapper接口
package com.edgar.mapper;
import com.edgar.pojo.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
User queryUserByName(String name);
}
编写Mapper配置文件
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.edgar.mapper.UserMapper">
<select id="queryUserByName" parameterType="String" resultType="user">
select * from mybatis.user where name = #{name}
select>
mapper>
编写UserService 层
package com.edgar.service;
import com.edgar.pojo.User;
public interface UserService {
User queryUserByName(String name);
}
package com.edgar.service;
import com.edgar.mapper.UserMapper;
import com.edgar.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);
}
}
好了,一口气写了这些常规操作,可以去测试一下了,保证能够从数据库中查询出来
package com.edgar;
import com.edgar.pojo.User;
import com.edgar.service.UserService;
import com.edgar.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("edgar");
System.out.println(user);
}
}
完全OK,成功查询出来了!
改造UserRealm,连接到数据库进行真实的操作!
package com.edgar.config;
import com.edgar.pojo.User;
import com.edgar.service.UserServiceImpl;
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.springframework.beans.factory.annotation.Autowired;
// 自定义的 UserRealm
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("执行了=>认证doGetAuthenticationInfo");
// 1.判断用户名
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
// 连接真实数据库
User user = userService.queryUserByName(usernamePasswordToken.getUsername());
if (user==null) { // 没有这个人
return null; // UnknownAccountException
}
// 2. 验证密码,我们可以使用一个AuthenticationInfo实现类SimpleAuthenticationInfo
// shiro会自动帮我们验证!重点是第二个参数就是要验证的密码!
return new SimpleAuthenticationInfo("",user.getPwd(),"");
}
}
测试,现在查询都是从数据库查询的了!
参考博客:https://blog.csdn.net/qq_34021712/article/details/84571067
思考?这个Shiro,是怎么帮我们实现密码自动比对的呢?
我们可以去 realm的父类 AuthorizingRealm
的父类 AuthenticatingRealm
中找一个方法 核心:
getCredentialsMatcher() 翻译过来:获取证书匹配器
我们去看这个接口 CredentialsMatcher 有很多的实现类,MD5盐值加密
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yMd7k9Mq-1663510275497)(SpringBoot.assets/oSEnc4.png)]
我们的密码一般都不能使用明文保存?需要加密处理;思路分析
使用shiro的过滤器来拦截请求即可!
在 ShiroFilterFactoryBean
中添加一个过滤器
// 授权过滤器
filterMap.put("/user/add","perms[user:add]");//注意,授权代码需要放在拦截的前面才会生效
我们再次启动测试一下,访问add,发现以下错误!未授权错误!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rAUVETg1-1663510275497)(SpringBoot.assets/o9TA8f.png)]
注意:当我们实现权限拦截后,shiro会自动跳转到未授权的页面,但我们没有这个页面,所以401 了
配置一个未授权的提示的页面,增加一个controller提示
@RequestMapping("/noauth")
@ResponseBody
public String unauthorized(){
return "未经授权无法访问此页面";
}
然后在 shiroFilterFactoryBean
中配置一个未授权的请求页面!
bean.setUnauthorizedUrl("/noauth");
测试,现在没有授权,可以跳转到我们指定的位置了!
在UserRealm 中添加授权的逻辑,增加授权的字符串!
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
// 给资源进行授权
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 添加资源的授权字符串
info.addStringPermission("user:add");
return info;
}
我们再次登录测试,发现登录的用户是可以进行访问add 页面了!授权成功! 问题,我们现在完全是硬编码,无论是谁登录上来,都可以实现授权通过,但是真实的业务情况应该 是,每个用户拥有自己的一些权限,从而进行操作,所以说,权限,应该在用户的数据库中,正常的情 况下,应该数据库中是由一个权限表的,我们需要联表查询,但是这里为了大家操作理解方便一些,我 们直接在数据库表中增加一个字段来进行操作!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oHB5RAPW-1663510275497)(SpringBoot.assets/o9Tviq.png)]
修改实体类,增加一个字段
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
private String perms;
}
我们现在需要在自定义的授权认证中,获取登录的用户,从而实现动态认证授权操作!
在用户登录认证的时候,将用户放在 Principal 中,改造下之前的代码
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对象
User currentUser = (User) subject.getPrincipal();
// 设置当前用户的权限
info.addStringPermission(currentUser.getPerms());
return info;
}
我们给数据库中的用户增加一些权限
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9XZWhdrF-1663510275497)(SpringBoot.assets/o9HnNn.png)]
在过滤器中,将 update 请求也进行权限拦截下
// 授权过滤器
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/update","perms[user:update]");
我们启动项目,登录不同的账户,进行测试一下!
测试完美通过OK!
整合参考:https://github.com/theborakompanioni/thymeleaf-extras-shiro
根据权限展示不同的前端页面
添加Maven的依赖;
<dependency>
<groupId>com.github.theborakompanionigroupId>
<artifactId>thymeleaf-extras-shiroartifactId>
<version>2.1.0version>
dependency>
配置一个shiro的Dialect ,在shiro的配置中增加一个Bean
// 配置ShiroDialect: 用于 thymeleaf 和 shiro 标签配合使用
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
修改前端的配置
导入命名空间
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
增加权限判断
<div shiro:hasPermission="'user:add'">
<a th:href="@{/user/add}">adda>
div>
<div shiro:hasPermission="'user:update'">
<a th:href="@{/user/update}">updatea>
div>
我们在去测试一下,可以发现,现在首页什么都没有了,因为我们没有登录,我们可以写个登录 ,来判断这个Shiro的效果!登录后,可以看到不同的用户,有不同的效果,现在就已经接近完美了~!还不是最完美。
<p>
<a th:href="@{/toLogin}">登录a>
p>
guest
仅当当前Subject
被视为“访客”时,标签才会显示其包装的内容
标签参考:https://shiro.apache.org/web.html#Web-JSP%252FGSPTagLibrary
<p shiro:guest>
<a th:href="@{/toLogin}">登录a>
p>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oNC2wKSx-1663510275498)(SpringBoot.assets/oZ0plQ.png)]
学习目标:
前后端分离
产生的问题
解决方案
Swagger
SpringBoot集成Swagger => springfox,两个jar包
使用Swagger
要求:jdk 1.8 + 否则swagger2无法运行;springboot版本2.1.7.RELEASE
步骤:
1、新建一个SpringBoot-web项目springboot-09-swagger
2、添加Maven依赖
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.9.2version>
dependency>
3、编写HelloController,测试确保运行成功!
4、要使用Swagger,我们需要编写一个配置类-SwaggerConfig来配置 Swagger
@Configuration //配置类
@EnableSwagger2// 开启Swagger2的自动配置
public class SwaggerConfig {
}
5、访问测试 :http://localhost:8080/swagger-ui.html ,可以看到swagger的界面;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zdpGodQS-1663510275498)(SpringBoot.assets/1660707245085.png)]
1、Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger。
// 配置docket以配置Swagger具体参数
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2);
}
2、可以通过apiInfo()属性配置文档信息
// 配置Swagger信息=apiInfo
private ApiInfo apiInfo(){
// 联系人信息 姓名,访问链接,邮箱
Contact contact = new Contact("edgar", "https://www.cnblogs.com/edgarstudy/", "[email protected]");
return new ApiInfo(
"Edgar的SwaggerAPI文档", // 标题
"即使再小的帆也能远航", // 描述
"1.0", //版本
"https://www.cnblogs.com/edgarstudy/", // 组织链接
contact, //联系人信息
"Apache 2.0", // 许可
"http://www.apache.org/licenses/LICENSE-2.0", // 许可链接
new ArrayList() // 扩展
);
}
3、Docket 实例关联上 apiInfo()
// 配置docket以配置Swagger具体参数
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo());
}
4、重启项目,访问测试 http://localhost:8080/swagger-ui.html 看下效果;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KhV27OFJ-1663510275498)(SpringBoot.assets/oeGkUe.png)]
1、构建Docket时通过select()方法配置怎么扫描接口。
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
// basePackage 指定要扫描的包
.apis(RequestHandlerSelectors.basePackage("com.edgar.swagger.controller"))
.build();
}
2、重启项目测试,由于我们配置根据包的路径扫描接口,所以我们只能看到一个类
3、除了通过包路径配置.basePackage()
扫描接口外,还可以通过配置其他方式扫描接口,这里注释一下所有的配置方式:
any() // 扫描所有,项目中的所有接口都会被扫描到
none() // 不扫描接口
// 通过方法上的注解扫描,如withMethodAnnotation(GetMapping.class)只扫描get请求
withMethodAnnotation(final Class<? extends Annotation> annotation)
// 通过类上的注解扫描,如.withClassAnnotation(Controller.class)只扫描有controller注解的类中的接口
withClassAnnotation(final Class<? extends Annotation> annotation)
basePackage(final String basePackage) // 根据包路径扫描接口
4、除此之外,我们还可以配置接口扫描过滤:
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
.apis(RequestHandlerSelectors.basePackage("com.edgar.swagger.controller"))
// 配置如何通过path过滤,即这里只扫描请求以/edgar开头的接口
.paths(PathSelectors.ant("/edgar/**"))
.build();
}
5、这里的可选值还有
any() // 任何请求都扫描
none() // 任何请求都不扫描
regex(final String pathRegex) // 通过正则表达式控制
ant(final String antPattern) // 通过ant()控制
1、通过enable()方法配置是否启用swagger,如果是false,swagger将不能在浏览器中访问了
// 配置docket以配置Swagger具体参数
@Bean
public Docket docket(Environment environment){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(false) // 配置是否启用Swagger,如果是false,在浏览器将无法访问
.select() // 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
.apis(RequestHandlerSelectors.basePackage("com.edgar.swagger.controller"))
// 配置如何通过path过滤,即这里只扫描请求以/edgar开头的接口
// .paths(PathSelectors.ant("/edgar/*"))
.build();
}
2、如何动态配置当项目处于test、dev环境时显示swagger,处于prod时不显示?
// 配置docket以配置Swagger具体参数
@Bean
public Docket docket(Environment environment){
// 设置要显示的Swagger环境
Profiles profiles = Profiles.of("dev","test");
// 通过environment.acceptsProfiles判断是否处在自己设定的环境当中
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(flag) // 配置是否启用Swagger,如果是false,在浏览器将无法访问
.select() // 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
// basePackage:指定要扫描的包
// any():扫描全部
// none():都不扫描
// withClassAnnotation():扫描类上的注解,参数是一个注解的反射对象
// withMethodAnnotation():扫描方法上的注解
.apis(RequestHandlerSelectors.basePackage("com.edgar.swagger.controller"))
// 配置如何通过path过滤,即这里只扫描请求以/edgar开头的接口
// .paths(PathSelectors.ant("/edgar/*"))
.build();
}
3、配置文件 application.properties
spring.profiles.active=dev
配置文件``application-dev.properties`
server.port=8081
配置文件application-pro.properties
server.port=8082
4、访问http://localhost:8081/swagger-ui.html
测试一下,可正常访问,将 application.properties
文件改成
spring.profiles.active=pro
,访问http://localhost:8082/swagger-ui.html
测试一下,效果如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zSCHp3mZ-1663510275498)(SpringBoot.assets/ouQiB8.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2zlj8SPF-1663510275499)(SpringBoot.assets/oQgya6.png)]
1、如果没有配置分组,默认是default。通过groupName()方法即可配置分组:
@Bean
public Docket docket(Environment environment) {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.groupName("edgar") // 配置分组
// 省略配置....
}
2、重启项目查看分组
3、如何配置多个分组?配置多个分组只需要配置多个docket即可:
@Bean
public Docket docket1(){
return new Docket(DocumentationType.SWAGGER_2).groupName("A");
}
@Bean
public Docket docket2(){
return new Docket(DocumentationType.SWAGGER_2).groupName("B");
}
@Bean
public Docket docket3(){
return new Docket(DocumentationType.SWAGGER_2).groupName("C");
}
4、重启项目查看即可
1、新建一个实体类
package com.edgar.swagger.pojo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("用户实体类")
public class User {
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
}
2、只要这个实体在请求接口的返回值上(即使是泛型),都能映射到实体项中:
// 只要我们的接口中,返回值中存在实体类,它就会被扫描到Swagger中
@PostMapping("/user")
public User user(){
return new User();
}
3、重启查看测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-poUNd4M9-1663510275499)(SpringBoot.assets/oQ2EQJ.png)]
注:并不是因为@ApiModel这个注解让实体显示在这里了,而是只要出现在接口方法的返回值上的实体都会显示在这里,而@ApiModel和@ApiModelProperty这两个注解只是为实体添加注释的。
@ApiModel为类添加注释
@ApiModelProperty为类属性添加注释
Swagger的所有注解定义在io.swagger.annotations包下
下面列一些经常用到的,未列举出来的可以另行查阅说明:
Swagger注解 | 简单说明 |
---|---|
@Api(tags = “xxx模块说明”) | 作用在模块类上 |
@ApiOperation(“xxx接口说明”) | 作用在接口方法上 |
@ApiModel(“xxxPOJO说明”) | 作用在模型类上:如VO、BO |
@ApiModelProperty(value = “xxx属性说明”,hidden = true) | 作用在类方法和属性上,hidden设置为true可以隐藏该属性 |
@ApiParam(“xxx参数说明”) | 作用在参数、方法和字段上,类似@ApiModelProperty |
我们也可以给请求的接口配置一些注释
@GetMapping("/hello2")
@ApiOperation("Hello控制方法")
public String hello(String username){
return "hello"+username;
}
这样的话,可以给一些比较难理解的属性或者接口,增加一些配置信息,让人更容易阅读!
相较于传统的Postman或Curl方式测试接口,使用swagger简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。
Swagger是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出Word接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭Swagger,一来出于安全考虑二来也可以节省运行时内存。
我们可以导入不同的包实现不同的皮肤定义:
1、默认的 访问 http://localhost:8080/swagger-ui.html
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2nO3uxOy-1663510275499)(SpringBoot.assets/oQfg0A.png)]
2、bootstrap-ui 访问 http://localhost:8080/doc.html
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>swagger-bootstrap-uiartifactId>
<version>1.9.1version>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TKBrd6Ib-1663510275499)(SpringBoot.assets/oQfI1S.png)]
3、mg-ui 访问 http://localhost:8080/document.html
<dependency>
<groupId>com.zyplayergroupId>
<artifactId>swagger-mg-uiartifactId>
<version>1.0.6version>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DwNC7bdk-1663510275500)(SpringBoot.assets/oQfb0s.png)]
4、Layui-ui 访问 http://localhost:8080/docs.html
com.github.caspar-chen
swagger-ui-layer
1.1.3
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M4aQGSVP-1663510275500)(SpringBoot.assets/olJ9b9.png)]
注意:Layui-ui 不能分组,否则打不开页面
新建一个springboot项目springboot-09-test,添加web依赖
1、创建一个service包
2、创建一个类AsyncService
异步处理还是非常常用的,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。
编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况
@Service
public class AsyncService {
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据正在处理....");
}
}
3、编写controller包
4、编写AsyncController类
我们去写一个Controller测试一下
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;
@RequestMapping("/hello")
public String hello(){
asyncService.hello(); // 停止三秒
return "OK";
}
}
5、访问http://localhost:8080/hello进行测试,3秒后出现OK,这是同步等待的情况。
问题:我们如果想让用户直接得到消息,就在后台使用多线程的方式进行处理即可,但是每次都需要自己手动去编写多线程的实现的话,太麻烦了,我们只需要用一个简单的办法,在我们的方法上加一个简单的注解即可,如下:
6、给hello方法添加@Async注解;
// 告诉Spring这是一个异步方法
@Async
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据正在处理....");
}
SpringBoot就会自己开一个线程池,进行调用!但是要让这个注解生效,我们还需要在主程序上添加一个注解@EnableAsync ,开启异步注解功能;
@EnableAsync // 开启异步注解功能
@SpringBootApplication
public class Springboot09AsyncApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot09AsyncApplication.class, args);
}
}
7、重启测试,网页瞬间响应,后台代码依旧执行!
邮件发送,在我们的日常开发中,也非常的多,Springboot也帮我们做了支持
测试:
1、引入pom依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
dependency>
看它引入的依赖,可以看到 jakarta.mail
<dependency>
<groupId>com.sun.mailgroupId>
<artifactId>jakarta.mailartifactId>
<version>1.6.7version>
<scope>compilescope>
dependency>
2、查看自动配置类:MailSenderAutoConfiguration
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XT1eMM8L-1663510275500)(SpringBoot.assets/oJHEJU.png)]
这个类中存在bean,JavaMailSenderImpl
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cV7iEQvb-1663510275500)(SpringBoot.assets/oJHMe1.png)]
然后我们去看下配置文件
@ConfigurationProperties(
prefix = "spring.mail"
)
public class MailProperties {
private static final Charset DEFAULT_CHARSET;
private String host;
private Integer port;
private String username;
private String password;
private String protocol = "smtp";
private Charset defaultEncoding;
private Map<String, String> properties;
private String jndiName;
}
3、配置文件:
[email protected]
spring.mail.password=eccvnqzboomveijb
spring.mail.host=smtp.qq.com
# qq需要配置ssl
spring.mail.properties.mail.smtp.ssl.enabl=true
获取授权码:在QQ邮箱中的设置->账户->开启pop3和smtp服务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SgbKlsEL-1663510275501)(SpringBoot.assets/oJbQXj.png)]
4、Spring单元测试
package com.edgar;
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 Springboot09AsyncApplicationTests {
@Autowired
JavaMailSenderImpl mailSender;
@Test
void contextLoads() {
// 一个简单的邮件~
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setSubject("小狂神你好呀~");
mailMessage.setText("谢谢你的狂神说系列课程~");
mailMessage.setTo("[email protected]");
mailMessage.setFrom("[email protected]");
mailSender.send(mailMessage);
}
@Test
void contextLoads2() throws MessagingException {
// 一个复杂的邮件~
MimeMessage mimeMessage = mailSender.createMimeMessage();
// 组装
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
helper.setSubject("小狂神你好呀~plus");
helper.setText("谢谢你的狂神说系列课程~
",true);
// 附件
helper.addAttachment("1.jpg",new File("C:\\Users\\86152\\Pictures\\tool\\1.jpg"));
helper.addAttachment("2.jpg",new File("C:\\Users\\86152\\Pictures\\tool\\1.jpg"));
helper.setTo("[email protected]");
helper.setFrom("[email protected]");
mailSender.send(mimeMessage);
}
}
查看邮箱,邮件接收成功!
我们只需要使用Thymeleaf进行前后端结合即可开发自己网站邮件收发功能了!
项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring为我们提供了异步执行任务调度的方式,提供了两个接口。
两个注解:
Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式:
(1) Seconds Minutes Hours DayofMonth Month DayofWeek Year
(2)Seconds Minutes Hours DayofMonth Month DayofWeek
一、结构
corn从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份
二、各字段的含义
字段 | 允许值 | 允许的特殊字符 |
---|---|---|
秒(Seconds) | 0~59的整数 | , - * / 四个字符 |
分(Minutes) | 0~59的整数 | , - * / 四个字符 |
小时(Hours) | 0~23的整数 | , - * / 四个字符 |
日期(DayofMonth) | 1~31的整数(但是你需要考虑你月的天数) | ,- * ? / L W C 八个字符 |
月份(Month) | 1~12的整数或者 JAN-DEC | , - * / 四个字符 |
星期(DayofWeek) | 1~7的整数或者 SUN-SAT (1=SUN) | , - * ? / L C # 八个字符 |
年(可选,留空)(Year) | 1970~2099 | , - * / 四个字符 |
注意事项:
每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:
(1):表示匹配该域的任意值。假如在Minutes域使用, 即表示每分钟都会触发事件。
(2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。
(3)-:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次
(4)/:表示起始时间开始触发,然后每隔固定时间触发一次。例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.
(5),:表示列出枚举值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
(6)L:表示最后,只能出现在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。
(7)W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。
(8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。
(9)#:用于确定每个月第几个星期几,只能出现在DayofWeek域。例如在4#2,表示某月的第二个星期三。
三、常用表达式例子
(0)0/20 * * * * ? 表示每20秒 调整任务
(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触发
1、创建一个ScheduledService
我们里面存在一个hello方法,他需要定时执行,怎么处理呢?
// 秒 分 时 日 月 周几
// 0 * * * * MON-FRI
// 注意cron表达式的用法;
@Scheduled(cron = "0/10 * * * * ?")
public void hello(){
System.out.println("hello,你被执行了~");
}
2、这里写完定时任务之后,我们需要在主程序上增加@EnableScheduling 开启定时任务功能
@EnableAsync // 开启异步注解功能
@EnableScheduling // 开启定时功能的注解
@SpringBootApplication
public class Springboot09AsyncApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot09AsyncApplication.class, args);
}
}
3、我们来详细了解下cron表达式;
http://www.bejson.com/othertools/cron/
在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。。。
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。
在Dubbo的官网文档有这样一张图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3wiQCNvx-1663510275501)(SpringBoot.assets/oavsfJ.jpg)]
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YrCTVuT2-1663510275501)(SpringBoot.assets/oavofH.jpg)]
适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。
缺点:
1、性能扩展比较难
2、协同开发问题
3、不利于升级维护
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6T566fhI-1663510275502)(SpringBoot.assets/oaxIbV.jpg)]
通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。
缺点:公用模块无法重复利用,开发性的浪费
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的**分布式服务框架(RPC)**是关键。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n4YKx0nL-1663510275502)(SpringBoot.assets/oazAxA.jpg)]
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oyN0DWI2-1663510275502)(SpringBoot.assets/oazDz9.jpg)]
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;
推荐阅读文章:https://www.jianshu.com/p/2accc2840a1b
RPC基本原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O8JARaZV-1663510275503)(SpringBoot.assets/ow0uVg.jpg)]
步骤解析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oSZJEUC5-1663510275503)(SpringBoot.assets/owB260.png)]
RPC两个核心模块:通讯,序列化。
Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
dubbo官网 https://dubbo.apache.org/zh/
1.了解Dubbo的特性
2.查看官方文档
dubbo基本概念
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b64lBu3B-1663510275503)(SpringBoot.assets/oBOZO1.png)]
服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
l 服务容器负责启动,加载,运行服务提供者。
l 服务提供者在启动时,向注册中心注册自己提供的服务。
l 服务消费者在启动时,向注册中心订阅自己所需的服务。
l 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
l 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
l 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
点进dubbo官方文档,推荐我们使用Zookeeper 注册中心
什么是zookeeper呢?可以查看官方文档
1、下载zookeeper :https://downloads.apache.org/zookeeper/, 我们下载 apache-zookeeper-3.5.9-bin.tar.gz , 最新版!解压zookeeper
2、运行/bin/zkServer.cmd ,初次运行会报错,没有zoo.cfg配置文件;
可能遇到问题:闪退 !
解决方案:编辑zkServer.cmd文件末尾添加pause 。这样运行出错就不会退出,会提示错误信息,方便找到原因。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9D5IU2V7-1663510275503)(SpringBoot.assets/oDpBVA.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g6xabqUD-1663510275504)(SpringBoot.assets/oD93LQ.png)]
3、修改zoo.cfg配置文件
将conf文件夹下面的zoo_sample.cfg复制一份改名为zoo.cfg即可。
注意几个重要位置:
dataDir=./ 临时数据存储的目录(可写相对路径)
clientPort=2181 zookeeper的端口号
修改完成后再次启动zookeeper
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eUd9g428-1663510275504)(SpringBoot.assets/oDCV6U.png)]
4、使用zkCli.cmd测试
ls /:列出zookeeper根下保存的所有节点
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]
create –e /edgar 123:创建一个edgar 节点,值为123
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qc2cIXLW-1663510275504)(SpringBoot.assets/oDCJXD.png)]
get /edgar :获取/edgar 节点的值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ovitoBNw-1663510275504)(SpringBoot.assets/oDCN0H.png)]
我们再来查看一下节点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qaPVfgRJ-1663510275505)(SpringBoot.assets/oDC0ht.png)]
dubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。
但是为了让用户更好的管理监控众多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin,不过这个监控即使不装也不影响使用。
我们这里来安装一下:
1、下载dubbo-admin
地址 :https://github.com/apache/dubbo-admin/tree/master
2、解压进入目录
修改 dubbo-admin\src\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
3、在项目目录下打包dubbo-admin
mvn clean package -Dmaven.test.skip=true
第一次打包的过程有点慢,需要耐心等待!直到成功!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XxsdhrMc-1663510275505)(SpringBoot.assets/oyeNo6.png)]
4、执行 dubbo-admin\target 下的dubbo-admin-0.0.1-SNAPSHOT.jar
java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
【注意:zookeeper的服务一定要打开!】
执行完毕,我们去访问一下 http://localhost:7001/ , 这时候我们需要输入登录账户和密码,我们都是默认的root-root;
登录成功后,查看界面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Wg1JAns-1663510275505)(SpringBoot.assets/oynBqA.png)]
安装完成!
1. 启动zookeeper !
2. IDEA创建一个空项目;
3.创建一个模块,实现服务提供者:provider-server , 选择web依赖即可
4.项目创建完毕,我们写一个服务,比如卖票的服务;
编写接口
package com.edgar.service;
public interface TicketService {
String getTicket();
}
编写实现类
package com.edgar.service;
import org.springframework.stereotype.Service;
@Service // 交给Spring容器管理
public class TicketServiceImpl implements TicketService {
@Override
public String getTicket() {
return "《狂神说Java》";
}
}
5.创建一个模块,实现服务消费者:customer-server , 选择web依赖即可
6.项目创建完毕,我们写一个服务,比如用户的服务;
编写service
package com.edgar.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 想拿到provider-server提供的票
}
需求:现在我们的用户想使用买票的服务,这要怎么弄呢 ?
1、将服务提供者注册到注册中心,我们需要整合Dubbo和zookeeper,所以需要导包
我们从dubbo官网进入github,看下方的帮助文档,找到dubbo-springboot,找到依赖包
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubbo-spring-boot-starterartifactId>
<version>2.7.14version>
dependency>
zookeeper的包我们去maven仓库下载,zkclient;
<dependency>
<groupId>com.github.sgroschupfgroupId>
<artifactId>zkclientartifactId>
<version>0.1version>
dependency>
【新版的坑】zookeeper及其依赖包,解决日志冲突,还需要剔除日志依赖;
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>2.12.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-recipesartifactId>
<version>2.12.0version>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.14version>
<exclusions>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
exclusion>
exclusions>
dependency>
2、在springboot配置文件中配置dubbo相关属性!
# 当前应用名字
dubbo.application.name=provider-server
# 注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
# 扫描指定包下服务
dubbo.scan.base-packages=com.edgar.service
3、在service的实现类中配置服务注解,发布服务!注意导包问题
package com.edgar.service;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;
@DubboService // 可以被扫描到,在项目一启动就自动注册到注册中心
@Service // 交给Spring容器管理
public class TicketServiceImpl implements TicketService {
@Override
public String getTicket() {
return "《狂神说Java》";
}
}
逻辑理解 :应用启动起来,dubbo就会扫描指定的包下带有@DubboService注解的服务,将它发布在指定的注册中心中!
1、导入依赖,和之前的依赖一样;
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubbo-spring-boot-starterartifactId>
<version>2.7.14version>
dependency>
<dependency>
<groupId>com.github.sgroschupfgroupId>
<artifactId>zkclientartifactId>
<version>0.1version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>2.12.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-recipesartifactId>
<version>2.12.0version>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.14version>
<exclusions>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
exclusion>
exclusions>
dependency>
折叠
2、配置参数
# 当前应用名字
dubbo.application.name=customer-server
# 注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
3. 本来正常步骤是需要将服务提供者的接口打包,然后用pom文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0OxFZjeY-1663510275505)(SpringBoot.assets/og8QFe.png)]
4. 完善消费者的服务类
package com.edgar.service;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 想拿到provider-server提供的票
@DubboReference
TicketService ticketService;
public void bugTicket(){
String ticket = ticketService.getTicket();
System.out.println("在注册中心买到"+ticket);
}
}
5. 测试类编写;
package com.edgar.customerserver;
import com.edgar.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.stereotype.Service;
@SpringBootTest
class CustomerServerApplicationTests {
@Autowired
UserService userService;
@Test
void contextLoads() {
userService.bugTicket();
}
}
1. 开启zookeeper
2. 打开dubbo-admin实现监控【可以不用做】
3. 开启服务者
4. 消费者消费测试,结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EgK5MoXZ-1663510275506)(SpringBoot.assets/og8hY4.png)]
监控中心 :
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wr55Y0vj-1663510275506)(SpringBoot.assets/og87Ox.png)]
ok , 这就是SpingBoot + dubbo + zookeeper实现分布式开发的应用,其实就是一个服务拆分的思想;