Spring是一个开源框架,2003 年兴起的一个轻量级的Java 开发框架,作者:Rod Johnson 。
Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。
为了降低Java开发的复杂性,Spring采用了以下4种关键策略:
1、基于POJO的轻量级和最小侵入性编程,所有东西都是bean;
2、通过IOC,依赖注入(DI)和面向接口实现松耦合;
3、基于切面(AOP)和惯例进行声明式编程;
4、通过切面和模版减少样式代码,RedisTemplate,xxxTemplate;
SpringBoot的最大特点就是“自动装配”,最多的东西就是xxxAutoConfiguration,用于像容器中自动配置组件。
SpringBoot官方文档:https://docs.spring.io/spring-boot/docs/2.6.4/reference/htmlsingle/
学过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为什么能简化开发?
答:自动装配原理
微服务和整体架构之间的区别在于,微服务由许多较小的,松散耦合的服务组成一个应用程序,与大型,紧密耦合的应用程序的整体方法相反。
微服务是一种架构风格,它要求我们在开发一个应用的时候,这个应用必须构建成一系列小服务的组合;可以通过http的方式进行互通。要说微服务架构,先得说说过去我们的单体应用架构。
所谓单体应用架构(all in one)是指,我们将一个应用中的所有应用服务都封装到一个应用中。
无论是ERP、CRM或是其他什么系统,你都把数据库访问,web访问,等等各个功能都放到一个war包内。
这样做的好处是,易于开发和测试;也十分方便部署;当需要扩展时,值需要将war复制多份,然后放到多个服务器上,再做个负载均衡就可以了。
单体应用架构的缺点是,哪怕我要修改一个非常小的地方,我都需要停掉整个服务,再重新打包,部署这个应用war包。特点是对于一个大型应用,我们不可能把所有内容都放在一个应用里面,我们如何维护,如何分工合作都是问题。
微服务架构是一项在云中部署应用和服务的新技术。大部分围绕微服务的争论都集中在容器或其他技术是否能很好的实施微服务,而红帽说API应该是重点。
微服务可以在“自己的程序”中运行,并通过“轻量级设备与HTTP型API进行沟通”。关键在于该服务可以在自己的程序中运行。通过这一点我们就可以将服务公开与微服务架构(在现有系统中分布一个API)区分开来。在服务公开中,许多服务都可以被内部独立进程所限制。如果其中任何一个服务需要增加某种功能,那么就必须缩小进程范围。在微服务架构中,只需要在特定的某种服务中增加所需功能,而不影响整体进程的架构。
all in one 的架构方式,我们把所有的功能单元放在一个应用里面。然后我们把整个应用部署到服务器上,如果负载能力不行,我们将整个应用进行水平复制,进行扩展,然后在负载均衡。
所谓微服务架构,就是打破之前 all in one 的架构方式,把每个功能元素独立出来,把独立出来的功能元素的动态组合,需要的功能元素才去拿来组合,需要多一些时可以整合多个功能元素。所以微服务架构是对功能元素进行复制,而没有对整个应用进行复制。
这样做的好处是:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7LBM65F8-1647620460267)(D:\Typora\图片位置\sketch.png)]
Martin Fowler于2014年3月25日写的《microservices》,详细的阐述了什么是微服务。
原文地址:https://martinfowler.com/articles/microservices.html
翻译:https://www.cnblogs.com/liuning8023/p/4493156.html
一个大型系统的微服务架构,就像一个复杂交织的神经网络,每一个神经元就是一个功能元素,它们各自完成自己的功能,然后通过http相互请求调用。比如一个电商系统、查缓存、连数据库、浏览页面、结账、支付等服务都是一个个独立的功能服务,都被微化了,他们作为一个个微服务共同构建了一个庞大的系统。如果修改其中的一个功能,只需要更新升级其中一个功能服务单元即可。
但是这种庞大的系统架构给部署和运维带来很大的难度,于是,spring为我们带来了构建大型分布式微服务的全套、全程产品:
构建一个个功能独立的微服务应用单元,可以使用springboot,可以帮我们快速构建一个应用;
大型分布式网络服务的调用,这部分由spring cloud来完成,实现分布式;
在分布式中间,进行流试数据计算、批处理,我们有spring cloud data flow。
spring为我们想清楚了整个从开始构建应用到大型分布式应用全流程方案。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qy1OqdUK-1647620460268)(D:\Typora\图片位置\image-20220312133625577.png)]
我们将学习如何快速的创建一个Spring Boot应用,并且实现一个简单的Http请求处理。通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。
我的环境准备:
开发工具:
Spring官方提供了非常方便的工具在idea中让我们快速构建应用
Spring Initializr:https://start.spring.io/
**项目创建方式一:**使用Spring Initializr 的 Web页面创建项目
1、打开 https://start.spring.io/
2、填写项目信息
3、点击”Generate Project“按钮生成项目;下载此项目
4、解压项目包,并用IDEA以Maven项目导入,一路下一步即可,直到项目导入完毕。
5、如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TgjsD1bA-1647620460268)(D:\Typora\图片位置\image-20220312135134715.png)]
**项目创建方式二:**使用 IDEA 直接创建项目
1、创建一个新项目
2、选择spring initalizr , 可以看到默认就是去官网的快速构建工具那里实现。
3、填写项目信息
4、选择初始化的组件(初学勾选 Web 即可)
5、填写项目路径
6、等待项目构建成功
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mvb1U8qm-1647620460269)(D:\Typora\图片位置\image-20220312141929829.png)]
项目结构分析:
通过上面步骤完成了基础项目的创建。就会自动生成以下文件。
1、程序的主启动类。(Springboot01HelloworldApplication)
2、一个 application.properties 配置文件
3、一个 测试类。(Springboot01HelloworldApplicationTests)
4、一个 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.6.4version>
<relativePath/>
parent>
<groupId>com.wanggroupId>
<artifactId>springboot-01-helloworldartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>springboot-01-helloworldname>
<description>springboot-01-helloworlddescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
在主程序的同级目录下,新建一个controller包,一定要在同级目录下,否则识别不到
在包中新建一个HelloController类
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "Hello World";
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W6WrmOxU-1647620460269)(D:\Typora\图片位置\image-20220312144219965.png)]
简单几步,就完成了一个web接口的开发,SpringBoot就是这么简单。所以我们常用它来建立我们的微服务项目!
==1. tomcat的端口号可以在application.properties文件中进行添加修改:==因为有时候我们需要开很多项目
#更改项目端口号
server.port =8081
2. 将项目打成jar包,点击 maven的 package
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hrLHokqo-1647620460270)(D:\Typora\图片位置\image-20220312150013247.png)]
如果成功了,打成了jar包后,就可以在任何地方运行了!OK
但是如果没成功,如果遇到以下错误,可以配置打包时 跳过项目运行测试用例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3a6AL7TN-1647620460270)(D:\Typora\图片位置\image-20220312150051526.png)]
解决:在pom.xml中添加
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-surefire-pluginartifactId>
<configuration>
<skipTests>trueskipTests>
configuration>
plugin>
SpringBoot这么简单的东西背后一定有故事,我们之后去进行一波源码分析!
如何更改启动时显示的字符拼成的字母,SpringBoot呢?也就是 banner 图案;
只需一步:到项目下的 resources 目录下新建一个banner.txt 即可。
图案可以到:https://www.bootschool.net/ascii 这个网站生成,然后拷贝到文件中即可!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4qLninlZ-1647620460271)(D:\Typora\图片位置\image-20220312145301034.png)]
我们之前写的HelloSpringBoot,到底是怎么运行的呢,Maven项目,我们一般从pom.xml文件探究起;
其中它主要是依赖一个父项目,主要是管理项目的资源过滤及插件!
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.4version>
<relativePath/>
parent>
点进去,发现还有一个父依赖。这里才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心;
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.6.4version>
parent>
再点进去,发现这里会有很多的jar包版本
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XO96VnQq-1647620460271)(D:\Typora\图片位置\image-20220312163714303.png)]
以后我们导入依赖默认是不需要写版本;但是如果导入的包没有在依赖中管理着就需要手动配置版本了;
问题:既然这么多自动配置,为什么有的没有生效,需要导入对应的start才能有作用?
核心注解:@ConditionalOnXXX:如果这里面的条件都满足,才会生效。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
springboot-boot-starter-xxx:就是spring-boot的场景启动器
spring-boot-starter-web:帮我们导入了web模块正常运行所依赖的组件;
SpringBoot将所有的功能场景都抽取出来,做成一个个的starter (启动器),只需要在项目中引入这些starter即可,所有相关的依赖都会导入进来 , 我们要用什么功能就导入什么样的场景启动器即可 ;我们未来也可以自己自定义 starter;
默认的主启动类是:SpringApplication
//@SpringBootApplication 来标注一个主程序类,说明这是一个Spring Boot应用
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
//以为是启动了一个方法,没想到启动了一个服务
SpringApplication.run(SpringbootApplication.class, args);
}
}
但是**一个简单的启动类并不简单!**我们来分析一下这些注解都干了什么
作用**:**标注在某个类上说明这个类是SpringBoot的主配置类 , SpringBoot就应该运行这个类的main方法来启动SpringBoot应用;
进入这个注解:可以看到上面还有很多其他注解!
@SpringBootConfiguration //springboot的配置
@EnableAutoConfiguration //自动配置
@ComponentScan //扫描当前主启动类同级的包
@Component //说明这也是一个spring的组件
@AutoConfigurationPackage //自动配置包
@Import({Registrar.class}) //导入选择器
@Import({AutoConfigurationImportSelector.class}) //自动导入包的核心
这个注解在Spring中很重要 ,它对应XML配置中的元素。
作用:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中
作用:SpringBoot的配置类 ,标注在某个类上 , 表示这是一个SpringBoot的配置类;
我们继续进去这个注解查看
@Configuration
public @interface SpringBootConfiguration {}
// 再点进去@Configuration得到下面的 @Component
@Component
public @interface Configuration {}
这里的 @Configuration,说明这是一个配置类 ,配置类就是对应Spring的xml 配置文件;
里面的 @Component 这就说明,启动类本身也是Spring中的一个组件而已,负责启动应用!
我们回到 SpringBootApplication 注解中继续看。
开启自动配置功能
以前我们需要自己配置的东西,而现在SpringBoot可以自动帮我们配置 ;@EnableAutoConfiguration告诉SpringBoot开启自动配置功能,这样自动配置才能生效;
点进注解接续查看:
自动配置包
@Import({Registrar.class})
public @interface AutoConfigurationPackage {}
@import :Spring底层注解@import , 给容器中导入一个组件
Registrar.class 作用:将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器 ;
这个分析完了,退到上一步,继续看
给容器导入组件 ;
AutoConfigurationImportSelector :自动配置导入选择器,那么它会导入哪些组件的选择器呢?我们点击去这个类看源码:
// 获得候选的配置
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;
}
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
//这里它又调用了 loadSpringFactories 方法
return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
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);
}
}
}
我们根据源头打开META-INF下的spring.factories ,看到了很多自动配置的文件;这就是自动配置的核心文件以及根源所在!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eJBNfTwu-1647620460272)(D:\Typora\图片位置\image-20220312171636610.png)]
我们在上面的自动配置类随便找一个打开看看,比如 :WebMvcAutoConfiguration
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TbvzSGBu-1647620460273)(D:\Typora\图片位置\image-20220312172424613.png)]
可以看到这些一个个的都是JavaConfig配置类,而且都注入了一些Bean,可以找一些自己认识的类,看着熟悉一下!
//所有的资源加载到配置类中
Properties properties=PropertiesLoaderUtils.loadProperties(resource);
所以,自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中。
结论:
现在应该大概的了解了下,SpringBoot的运行原理,后面我们还会深化一次!
最初以为就是运行了一个main方法,没想到却开启了一个服务;
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
//该方法返回一个ConfigurableApplicationContext的对象
//参数一:应用入口的类 参数类:命令行参数
SpringApplication.run(SpringbootApplication.class, args);
}
}
SpringApplication.run分析:
分析该方法主要分两部分,一部分是SpringApplication的实例化,二是run()方法的执行;
这个类主要做了以下四件事情:
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();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OC5A9GPm-1647620460273)(D:\Typora\图片位置\image-20220312181008557.png)]
跟着源码和这幅图就可以一探究竟了!
问题:关于springboot,谈谈你的理解:(上网查查)
自动装配
run()方法;
springboot是通过main方法下的SpringApplication.run方法启动的,启动的时候他会调用refshContext方法,先刷新容器,然后根据解析注解或者解析配置文件的形式祖册bean,而它是通过启动类的SpringBootApplication注解进行开始解析的,他会根据EnableAutoConfiguration开启自动化配置,里面有个核心方法ImportSelect选择性的导入,根据loadFanctoryNames根据classpash路径以MATA-INF/spring.factorces下面以什么什么EnableAutoConfiguration开头的key去加载里面所有对应的自动化配置,他并不是把这一百二十多个自动化配置全部导入,在他每个自动化配置里面都有条件判断注解,先判断是否引入相互的jar包,再判断容器是否有bean再进行注入到bean容器
SpringBoot使用一个全局的配置文件 , 配置文件名称是固定的
application.properties,【properties只能存放键值对】
application.yml
**配置文件的作用 :**修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们自动配置好了;
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
基础格式:
k:(空格)v
说明:语法要求严格!
1、空格不能省略
2、以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
3、属性和值的大小写都是十分敏感的。
字面量直接写在后面就可以 , 字符串默认不用加上双引号或者单引号;
k: v
注意:
“ ” 双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思;
比如 :name: “wang\n shen” 输出 :wang换行 shen
’’ 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出
比如 :name: ‘wang\n shen’ 输出 :wang\n shen
#对象、Map格式
k:
v1:
v2:
第一种:写对象的属性和值得关系,注意缩进;比如:
student:
name: qinjiang
age: 3
第二种:行内写法
student: {name: qinjiang,age: 3}
用 - 值表示数组中的一个元素,比如:
pets:
- cat
- dog
- pig
行内写法
pets: [cat,dog,pig]
配置文件中添加,端口号的参数,就可以切换端口;
server:
port: 8082
yaml文件更强大的地方在于,它可以给我们的实体类直接注入匹配值!
在springboot项目中的resources目录下新建一个文件 application.yaml
编写一个实体类 Dog;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component //添加到spring组件,注册bean到容器中
public class Dog {
private String name;
private Integer age;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component //添加到spring组件,注册bean到容器中
public class Dog {
@Value("旺财")
private String name;
@Value("20")
private Integer age;
}
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired //将狗狗自动注入进来
private Dog dog;
@Test
void contextLoads() {
System.out.println(dog); //打印看下狗狗对象
}
}
结果成功输出,@Value注入成功,这是我们原来的办法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlMtVhZd-1647620460274)(D:\Typora\图片位置\image-20220312190537967.png)]
@Data
@AllArgsConstructor
@NoArgsConstructor
@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;
}
person:
name: liupeiwang
age: 21
happy: false
birth: 1999//11/08
maps: {k1: v1, k2: v2}
lists:
- code
- music
- girl
dog:
name: 旺财
age: 20
/*
@ConfigurationProperties作用:
将配置文件中配置的每一个属性的值,映射到这个组件中;
告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
参数 prefix = “person” : 将配置文件中的person下面的所有属性一一对应
只有这个组件是容器中的组件,才能使用容器提供的@ConfigurationProperties功能
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ME5tkWOa-1647620460275)(D:\Typora\图片位置\image-20220312192212474.png)]
在pom.xml导入依赖即可:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired
private Person Person; //将person自动注入进来
@Test
void contextLoads() {
System.out.println(Person); //打印person信息
}
}
结果:所有值全部注入成功!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rdnQlf4q-1647620460276)(D:\Typora\图片位置\image-20220312192009169.png)]
yaml配置注入到实体类完全OK!
课堂测试:
1、将配置文件的key 值 和 属性的值设置为不一样,则结果输出为null,注入失败
2、在配置一个person2,然后将 @ConfigurationProperties(prefix = “person2”) 指向我们的person2;
**@PropertySource :**加载指定的配置文件;
@configurationProperties:默认从全局配置文件中获取值;
name=liupeiwang
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
//@ConfigurationProperties(prefix = "person")
//加载指定的配置文件
@PropertySource(value = "classpath:liupeiwang.properties")
public class Person {
@Value("${name}")
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
}
配置文件还可以编写占位符生成随机数
person:
name: liupeiwang${random.uuid} # 随机uuid
age: ${random.int} # 随机int
happy: false
birth: 1999//11/08
maps: {k1: v1, k2: v2}
lists:
- code
- music
- girl
dog:
name: ${person.hello:other}_旺财
age: 20
我们上面采用的yaml方法都是最简单的方式,开发中最常用的;也是springboot所推荐的!那我们来唠唠其他的实现方式,道理都是相同的;写还是那样写;配置文件除了yml还有我们之前常用的properties , 我们没有讲,我们来唠唠!
【注意】properties配置文件在写中文的时候,会有乱码 , 我们需要去IDEA中设置编码格式为UTF-8; settings–>FileEncodings 中配置;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BZKNw9IW-1647620460277)(D:\Typora\图片位置\image-20220312195224810.png)]
新建一个实体类User
@Component //注册bean
public class User {
private String name;
private int age;
private String sex;
//有参无参构造方法,toString
编辑配置文件 user.properties
user1.name=kuangshen
user1.age=18
user1.sex=男
我们在User类上使用@Value来进行注入!
@Component //注册bean
@PropertySource(value = "classpath:user.properties")
public class User {
//直接使用@value
@Value("${user.name}") //从配置文件中取值
private String name;
@Value("#{9*2}") // #{SPEL} Spring表达式
private int age;
@Value("男") // 字面量
private String sex;
}
Springboot测试
@SpringBootTest
class DemoApplicationTests {
@Autowired
User user;
@Test
public void contextLoads() {
System.out.println(user);
}
}
结果正常输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uhoh6RGj-1647620460277)(D:\Typora\图片位置\image-20220312201111180.png)]
@Value这个使用起来并不友好!我们需要为每个属性单独注解赋值,比较麻烦;我们来看个功能对比图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XcpsHQ7U-1647620460278)(D:\Typora\图片位置\image-20220312201332226.png)]
@ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加
松散绑定:这个什么意思呢? 比如我的yml中写的last-name,这个和lastName是一样的, - 后面跟着的字母默认是大写的。这就是松散绑定。可以测试一下
JSR303数据校验 , 这个就是我们可以在字段是增加一层过滤器验证 , 可以保证数据的合法性
复杂类型封装,yml中可以封装对象 , 使用value就不支持
结论:
配置yml和配置properties都可以获取到值 ,强烈推荐 yml;
如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;
如果说,我们专门编写了一个JavaBean来和配置文件进行一一映射,就直接@configurationProperties,不要犹豫!
Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。我们这里来写个注解让我们的name只能支持Email格式;
@Component //注册bean
@ConfigurationProperties(prefix = "person")
@Validated //数据校验
public class Person {
@Email(message="邮箱格式错误") //name必须是邮箱格式
private String name;
}
运行结果 :default message [不是一个合法的电子邮件地址];
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hzMmD8Aj-1647620460279)(D:\Typora\图片位置\image-20220312215631807.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 对象是否符合正则表达式的规则
.......等等
除此以外,我们还可以自定义一些数据校验规则
JSR303数据校验在这里:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-69p098CL-1647620460279)(D:\Typora\图片位置\image-20220312221151379.png)]
profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境;
我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本;
例如:
application-test.properties 代表测试环境配置
application-dev.properties 代表开发环境配置
但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置文件;
我们需要通过一个配置来选择需要激活的环境:
#比如在配置文件中指定使用dev环境,我们可以通过设置不同的端口号进行测试;
#我们启动SpringBoot,就可以看到已经切换到dev下的配置了;
#springboot的多环境配置,可以选择激活哪一个配置文件
spring.profiles.active=dev
和properties配置文件中一样,但是使用yml去实现不需要创建多个配置文件,更加方便了 !
server:
port: 8081
#选择要激活那个环境块
spring:
profiles:
active: prod
---
server:
port: 8083
spring:
profiles: dev #配置环境的名称
---
server:
port: 8084
spring:
profiles: test #配置环境的名称
注意:如果yml和properties同时都配置了端口,并且没有激活其他环境 , 默认会使用properties配置文件的!
外部加载配置文件的方式十分多,我们选择最常用的即可,在开发的资源文件中进行配置!
官方外部配置文件说明参考文档:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cceHNWq0-1647620460279)(D:\Typora\图片位置\image-20220313011702711.png)]
springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件:
优先级1:项目路径下的config文件夹配置文件
优先级2:项目路径下配置文件
优先级3:资源路径下的config文件夹配置文件
优先级4:资源路径下配置文件
优先级由高到底,高优先级的配置会覆盖低优先级的配置;
SpringBoot会从这四个位置全部加载主配置文件;互补配置;
我们在最低级的配置文件中设置一个项目访问路径的配置来测试互补问题;
#配置项目的访问路径
server.servlet.context-path=/kuang
指定位置加载配置文件
我们还可以通过spring.config.location来改变默认的配置文件位置
项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高
java -jar spring-boot-config.jar --spring.config.location=F:/application.properties
配置文件到底能写什么?怎么写?
SpringBoot官方文档中有大量的配置,我们无法全部记住
每一个xxxAutoConfiguration类都是容器中的一个组件,最后都加入到容器中,用他们来做自动配置。
每一个自动配置类可以进行自动配置功能。
我们以**HttpEncodingAutoConfiguration(Http编码自动配置)**为例解释自动配置原理;
//表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件;
@Configuration
//启动指定类的ConfigurationProperties功能;
//进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来;
//并把HttpProperties加入到ioc容器中
@EnableConfigurationProperties({HttpProperties.class})
//Spring底层@Conditional注解
//根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效;
//这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效
@ConditionalOnWebApplication(
type = Type.SERVLET
)
//判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器;
@ConditionalOnClass({CharacterEncodingFilter.class})
//判断配置文件中是否存在某个配置:spring.http.encoding.enabled;
//如果不存在,判断也是成立的
//即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的;
@ConditionalOnProperty(
prefix = "spring.http.encoding",
value = {"enabled"},
matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
//他已经和SpringBoot的配置文件映射了
private final Encoding properties;
//只有一个有参构造器的情况下,参数的值就会从容器中拿
public HttpEncodingAutoConfiguration(HttpProperties properties) {
this.properties = properties.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.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
return filter;
}
//。。。。。。。
}
一句话总结 :根据当前不同的条件判断,决定这个配置类是否生效!
//从配置文件中获取指定的值和bean的属性进行绑定
@ConfigurationProperties(prefix = "spring.http")
public class HttpProperties {
// .....
}
我们去配置文件里面试试前缀,看提示!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0eSL3KL9-1647620460280)(D:\Typora\图片位置\image-20220313015446706.png)]
这就是自动装配的原理!
1、SpringBoot启动会加载大量的自动配置类
2、我们看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;
3、我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)
4、给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;
**xxxxAutoConfigurartion:自动配置类;**给容器中添加组件
xxxxProperties:封装配置文件中相关属性;
了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;
@Conditional派生注解(Spring注解版原生的@Conditional作用)
作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TJqXV1vh-1647620460281)(D:\Typora\图片位置\image-20220313020003883.png)]
那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是所有的都生效了。
我们怎么知道哪些自动配置类生效?
我们可以通过在配置文件中启用 debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效,又有哪些没有生效;
#开启springboot的调试类debug=true
其中配置报告中的提示代表:
Positive matches:(自动配置类启用的:正匹配)
Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)
Unconditional classes: (没有条件的类)
我们分析完毕了源码以及自动装配的过程,我们可以尝试自定义一个启动器来玩玩!
启动器模块是一个 空 jar 文件,仅提供辅助性依赖管理,这些依赖可能用于自动装配或者其他类库;
命名归约:
官方命名:
自定义命名:
在IDEA中新建一个空项目 spring-boot-starter-diy
新建一个普通Maven模块:wang-spring-boot-starter
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RmARDPsv-1647620460281)(D:\Typora\图片位置\640.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PCvV6XxE-1647620460281)(D:\Typora\图片位置\640-16471806646862.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N2H4jW0w-1647620460282)(D:\Typora\图片位置\640-16471806842134.jpeg)]
<dependencies>
<dependency>
<groupId>com.kuanggroupId>
<artifactId>kuang-spring-boot-starter-autoconfigureartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
dependencies>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hUtjRdKS-1647620460282)(D:\Typora\图片位置\640-16471807326226.jpeg)]
package com.wang;
public class HelloService {
HelloProperties helloProperties;
public HelloProperties getHelloProperties() {
return helloProperties;
}
public void setHelloProperties(HelloProperties helloProperties) {
this.helloProperties = helloProperties;
}
public String sayHello(String name){
return helloProperties.getPrefix() + name + helloProperties.getSuffix();
}
}
package com.wang;
import org.springframework.boot.context.properties.ConfigurationProperties;
// 前缀 wang.hello
@ConfigurationProperties(prefix = "kuang.hello")
public class HelloProperties {
private String prefix;
private String suffix;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
package com.wang;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnWebApplication //web应用生效
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {
@Autowired
HelloProperties helloProperties;
@Bean
public HelloService helloService(){
HelloService service = new HelloService();
service.setHelloProperties(helloProperties);
return service;
}
}
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.wang.HelloServiceAutoConfiguration
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xuWgfGiA-1647620460283)(D:\Typora\图片位置\640-16471808729998.jpeg)]
新建一个SpringBoot 项目
导入我们自己写的启动器
<dependency>
<groupId>com.kuanggroupId>
<artifactId>kuang-spring-boot-starterartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
package com.wang.controller;
@RestController
public class HelloController {
@Autowired
HelloService helloService;
@RequestMapping("/hello")
public String hello(){
return helloService.sayHello("zxc");
}
}
wang.hello.prefix="ppp"
wang.hello.suffix="sss"
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f2z1JzHn-1647620460283)(D:\Typora\图片位置\640-164718098956010.png)]
开始学习SpringBoot与Web开发,从这一章往后,就属于我们实战部分的内容了;
其实SpringBoot的东西用起来非常简单,因为SpringBoot最大的特点就是自动装配。
使用SpringBoot的步骤:
1、创建一个SpringBoot应用,选择我们需要的模块,SpringBoot就会默认将我们的需要的模块自动配置好
2、手动在配置文件中配置部分配置项目就可以运行起来了
3、专注编写业务代码,不需要考虑以前那样一大堆的配置了。
要熟悉掌握开发,之前学习的自动配置的原理一定要搞明白!
比如SpringBoot到底帮我们配置了什么?我们能不能修改?我们能修改哪些配置?我们能不能扩展?
没事就找找类,看看自动装配原理!
我们来进行一个单体项目的小项目测试,让大家能够快速上手开发!
首先,我们搭建一个普通的SpringBoot项目,回顾一下HelloWorld程序!
写请求非常简单,那我们要引入我们前端资源,我们项目中有许多的静态资源,比如css,js等文件,这个SpringBoot怎么处理呢?
如果我们是一个web应用,我们的main下会有一个webapp,我们以前都是将所有的页面导在这里面的,但是我们现在的pom呢,打包方式是为jar的方式,那么这种方式SpringBoot能不能来给我们写页面呢?当然是可以的,但是SpringBoot对于静态资源放置的位置,是有规定的!
我们先来聊聊这个静态资源映射规则:
SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置类里面;
我们可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;
源码中的WebMvcAutoConfiguration类中有一个方法:addResourceHandlers 添加资源处理
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
// 已禁用默认资源处理
logger.debug("Default resource handling disabled");
return;
}
// 缓存控制
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
// webjars 配置
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
// 静态资源配置
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
读一下源代码:比如所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源;
Webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。
使用SpringBoot需要使用Webjars,我们可以去搜索一下:
网站:https://www.webjars.org
要使用jQuery,我们只要要引入jQuery对应版本的pom依赖即可!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zMhiYGDz-1647620460283)(D:\Typora\图片位置\image-20220313142402034.png)]
<dependency>
<groupId>org.webjars.npmgroupId>
<artifactId>jqueryartifactId>
<version>3.6.0version>
dependency>
导入完毕,查看webjars目录结构,并访问Jquery.js文件!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qB0k4nWT-1647620460284)(D:\Typora\图片位置\image-20220313142757750.png)]
访问:只要是静态资源,SpringBoot就会去对应的路径寻找资源,我们这里访问:http://localhost:8080/webjars/jquery/3.6.0/dist/jquery.js
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YYhwt88O-1647620460284)(D:\Typora\图片位置\image-20220313143722589.png)]
那我们项目中要是使用自己的静态资源该怎么导入呢?我们看下一行代码;
我们去找staticPathPattern发现第二种映射规则 :/** , 访问当前的项目任意资源,它会去找 resourceProperties 这个类,我们可以点进去看一下分析:
// 进入方法
public String[] getStaticLocations() {
return this.staticLocations;
}
// 找到对应的值
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
// 找到路径
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
};
ResourceProperties 可以设置和我们静态资源有关的参数;这里面指向了它会去寻找资源的文件夹,即上面数组的内容。
所以得出结论,以下四个目录存放的静态资源均可以被我们识别:
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
优先级:resources>static(系统默认推荐使用的)>public
我们可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pXo8mzR2-1647620460285)(D:\Typora\图片位置\image-20220313145547476.png)]
比如我们访问 http://localhost:8080/1.js , 他就会去这些文件夹中寻找对应的静态资源文件;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YXfX1pZs-1647620460285)(D:\Typora\图片位置\image-20220313145656663.png)]
我们也可以自己通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在application.properties中配置;一旦自己定义了静态文件夹的路径,那默认的原来的自动配置就都会失效了!就失效了,所以一般不建议自定义资源。
spring.resources.static-locations=classpath:/coding/,classpath:/wang/
静态资源文件夹说完后,我们继续向下看WebMvcAutoConfiguration源码!可以看到一个欢迎页的映射,就是我们的首页!
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
return welcomePageHandlerMapping;
}
点进去继续看
private Optional<Resource> getWelcomePage() {
String[] locations = 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页面,位置在这三个下都行[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lAJL244F-1647620460286)(D:\Typora\图片位置\image-20220313151801036.png)]
新建一个 index.html ,在我们上面的3个目录中(根目录不行)任意一个;
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>首页h1>
body>
html>
然后访问测试:http://localhost:8080/ 看结果!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tTgJby23-1647620460286)(D:\Typora\图片位置\image-20220313151643481.png)]
与其他静态资源一样,Spring Boot在配置的静态内容位置中查找 favicon.ico。如果存在这样的文件,它将自动用作应用程序的favicon。
#关闭默认图标
spring.mvc.favicon.enabled=false
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EclXrAHf-1647620460286)(D:\Typora\图片位置\image-20220313153529780.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gTmEDsli-1647620460287)(D:\Typora\图片位置\image-20220313213818803.png)]
前端交给我们的页面,是html页面。如果是我们以前开发,我们需要把他们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示,及交互等。
jsp支持非常强大的功能,包括能写Java代码,但是呢,我们现在的这种情况,SpringBoot这个项目首先是以jar的方式,不是war,像第二,我们用的还是嵌入式的Tomcat,所以呢,他现在默认是不支持jsp的。
那不支持jsp,如果我们直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦,那怎么办呢?
SpringBoot推荐你可以来使用模板引擎:
模板引擎,我们其实大家听到很多,其实jsp就是一个模板引擎,还有用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但再多的模板引擎,他们的思想都是一样的,什么样一个思想呢我们来看一下这张图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WjITa0th-1647620460287)(D:\Typora\图片位置\image-20220313154226299.png)]
模板引擎的作用:就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。而这些值,从哪来呢,就是我们在后台封装一些数据。然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。只不过呢,就是说不同模板引擎之间,他们可能这个语法有点不一样。其他的我就不介绍了,我主要来介绍一下SpringBoot给我们推荐的Thymeleaf模板引擎,这模板引擎呢,是一个高级语言的模板引擎,他的这个语法更简单。而且呢,功能更强大。
我们呢,就来看一下这个模板引擎,那既然要看这个模板引擎。首先,我们来看SpringBoot里边怎么用。
第一步,引入Thymeleaf。怎么引入呢,对于springboot来说,什么事情不都是一个start的事情嘛,我们去在项目中引入一下。有三个网址:
Thymeleaf 官网:https://www.thymeleaf.org/
Thymeleaf 在Github 的主页:https://github.com/thymeleaf/thymeleaf
Springboot官方文档:找到我们对应的版本
https://docs.spring.io/spring-boot/docs/2.6.4/reference/htmlsingle/#using.build-systems.starters
找到对应的pom依赖:可以适当点进源码看下本来的包!
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
Maven会自动下载jar包,我们可以去看下下载的东西;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hkw9UNd2-1647620460288)(D:\Typora\图片位置\image-20220313161211580.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CIyVtBWL-1647620460288)(D:\Typora\图片位置\image-20220313161449763.png)]
前面呢,我们已经引入了Thymeleaf,那这个要怎么使用呢?
注意:在系统配置的templates目录下的所有页面,只能通过controller来实现跳转!并且需要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;
}
我们可以在其中看到默认的前缀(prefix)和后缀(suffix)!
我们只需要把我们的html页面放在类路径下的templates下,thymeleaf就可以帮我们自动渲染了。
使用thymeleaf什么都不需要配置,只需要将他放在指定的文件夹下即可!
//在templates目录下的所有页面,只能通过controller来实现跳转!
//这个需要模板引擎的支持! thymeleaf
@Controller
public class TestController {
//网址输入:http://localhost:8080/t1,返回test.html页面
@RequestMapping("/t1")
public String test(){
//classpath:/templates/test.html
return "test";
}
}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>测试页面h1>
body>
html>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xfee5kWw-1647620460288)(D:\Typora\图片位置\image-20220313163052811.png)]
结论:只要需要使用thymeleaf ,只需要导入对应的依赖就可以了!我们将html放在我们的templates目录下即可。
关闭默认网页图标
spring.mvc.favicon.enabled=false
#关闭模板引擎的缓存
spring.thymeleaf.cache=false
#设置了这个,访问首页就需要输入http://localhost:8080/wang
server.servlet.context-path=/wang
#时间日期格式化;一旦自定义设置了,spring默认的/格式的就失效了; spring默认的日期格式是dd/MM/yyyy,
spring.mvc.format.date=yyyy-MM-dd
# 修改默认端口
server.port=9999
#随机端口,随机端口的好处就是:
1:如果在同一台服务器上多个服务如果用同一个端口会造成端口冲突
2:在微服务项目和开发中,开发人员是不会去记住ip和端口的。我们一般在真实的开发环境下,设置一个随机端口,就不用去管理端口了。也不会造成端口的冲突。
server.port=${random.int[8080,8999]}
要学习语法,还是参考官网文档最为准确,我们找到对应的版本看一下;
Thymeleaf 官网:https://www.thymeleaf.org/ , 简单看一下官网!我们去下载Thymeleaf的官方文档!
我们要使用thymeleaf,需要导入命名空间。我们可以去官方文档的#3中看一下命名空间拿过来:
在html文件中导入命名空间的约束:
xmlns:th="http://www.thymeleaf.org"
使用链接需要导入:
th:href="@{/css/gtvg.css}"
使用文本需要导入:
th:text="#{home.welcome}"
我们做个最简单的练习 :我们需要查出一些数据,在页面中展示
xmlns:th="http://www.thymeleaf.org"
<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>
body>
html>
@Controller
public class IndexController {
@RequestMapping("/t2")
public String test(Model model){
//存入数据
model.addAttribute("msg","Hello,Thymeleaf");
//classpath:/templates/test.html
return "test";
}
}
启动测试:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vzoGxaXr-1647620460289)(D:\Typora\图片位置\image-20220313171111735.png)]
(注意:所有的html元素都可以被thymeleaf元素替换: th:元素名)
OK,入门搞定,我们来认真研习一下Thymeleaf的使用语法!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1lnvjKKM-1647620460289)(D:\Typora\图片位置\image-20220313171526941.png)]
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: _
练习测试:
@Controller
public class aaaController {
@RequestMapping("/t3")
public String test(Model model){
//存入数据
model.addAttribute("msg","Hello,springboot
");
model.addAttribute("users", Arrays.asList("liupeiwang","songxiumei"));
//classpath:/templates/test.html
return "test";
}
}
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>刘培旺title>
head>
<body>
<div th:text="${msg}">div>
<div th:utext="${msg}">div>
<h3 th:each="user :${users}" th:text="${user}">h3>
<h4>
<span th:each="user:${users}">[[${user}]]span>
h4>
body>
html>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a08iuoc3-1647620460289)(D:\Typora\图片位置\image-20220313173100899.png)]
我们看完语法,很多样式,我们即使现在学习了,也会忘记,所以我们在学习过程中,需要使用什么,根据官方文档来查询,才是最重要的,要熟练使用官方文档!
在进行项目编写前,我们还需要知道一个东西,就是SpringBoot对我们的SpringMVC还做了哪些配置,包括如何扩展,如何定制。
只有把这些都搞清楚了,我们在之后使用才会更加得心应手。途径一:源码分析,途径二:官方文档!
地址 :https://docs.spring.io/spring-boot/docs/2.6.4/reference/htmlsingle/#web.servlet.spring-mvc.auto-configuration
// Spring MVC 自动配置
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(beanFactory.getBean(ContentNegotiationManager.class));
// ContentNegotiatingViewResolver使用所有其他视图解析器来定位视图,因此它应该具有较高的优先级
resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
return resolver;
}
我们可以点进这类看看!找到对应的解析视图的代码;
@Nullable // 注解说明:@Nullable 即参数可为null
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
if (requestedMediaTypes != null) {
// 获取候选的视图对象
List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
// 选择一个最适合的视图对象,然后把这个对象返回
View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;
}
}
// .....
}
我们继续点进去看,他是怎么获得候选的视图的呢?
getCandidateViews中看到他是把所有的视图解析器拿来,进行while循环,挨个解析!
Iterator var5 = this.viewResolvers.iterator();
所以得出结论:**ContentNegotiatingViewResolver 这个视图解析器就是用来组合所有的视图解析器的**
我们再去研究下它的组合逻辑,看到有个属性viewResolvers,看看它是在哪里进行赋值的!
protected void initServletContext(ServletContext servletContext) {
// 这里它是从beanFactory工具中获取容器中的所有视图解析器
// ViewRescolver.class 把所有的视图解析器来组合的
Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(), ViewResolver.class).values();
ViewResolver viewResolver;
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList(matchingBeans.size());
}
// ...............
}
既然它是在容器中去找视图解析器,我们是否可以猜想,我们就可以去实现一个视图解析器了呢?
我们可以自己给容器中去添加一个视图解析器;这个类就会帮我们自动的将它组合进来;我们去实现一下
//如果想diy一些定制化工鞥,只要写这个组件,然后将它交给springboot,springboot就会帮我们自动装配!
//扩展 springmvc dispatchservet
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
//ViewResolver 实现了视图解析器接口的类,我们就可以把它看做视图解析器
@Bean
public ViewResolver myViewResolver(){
return new MyViewResolver();
}
//自定义了一个自己的视图解析器MyViewResolver
public static class MyViewResolver implements ViewResolver {
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
return null;
}
}
}
我们给 DispatcherServlet 中的 doDispatch方法 加个断点进行调试一下,因为所有的请求都会走到这个方法中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KnXD7xiO-1647620460290)(D:\Typora\图片位置\image-20220313211139296.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-koK2zJoj-1647620460290)(D:\Typora\图片位置\image-20220313211649949.png)]
所以说,我们如果想要使用自己定制化的东西,我们只需要给容器中添加这个组件就好了!剩下的事情SpringBoot就会帮我们做了!
找到格式化转换器:
@Bean
@Override
public FormattingConversionService mvcConversionService() {
// 拿到配置文件中的格式化规则
WebConversionService conversionService =
new WebConversionService(this.mvcProperties.getDateFormat());
addFormatters(conversionService);
return conversionService;
}
可以看到在我们的Properties文件中,我们可以进行自动配置它!
点进去:
public String getDateFormat() {
return this.dateFormat;
}
/**
* Date format to use. For instance, `dd/MM/yyyy`. 默认的
*/
private String dateFormat;
如果配置了自己的格式化方式,就会注册到Bean中生效,我们可以在配置文件中配置日期格式化的规则:
# 自定义的配置日期格式化!
spring.mvc.format.date=dd/MM/yyyy
这么多的自动配置,原理都是一样的,通过这个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,写一个类MySelfMvcConfig;
//如果我们要扩展springmvc,官方建议我们这样去做
//应为类型要求为WebMvcConfigurer,所以我们实现其接口
//可以使用自定义类扩展MVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 浏览器发送/test , 就会跳转到test页面;
registry.addViewController("/test").setViewName("test");
}
}
我们去浏览器访问一下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pP6dCfvR-1647620460290)(D:\Typora\图片位置\image-20220313214103575.png)]
确实也跳转过来了!所以说,我们要扩展SpringMVC,官方就推荐我们这么去使用,既保SpringBoot留所有的自动配置,也能用我们扩展的配置!
我们可以去分析一下原理:
WebMvcAutoConfiguration 是 SpringMVC的自动配置类,里面有一个类WebMvcAutoConfigurationAdapter
这个类上有一个注解,在做其他自动配置时会导入:@Import(EnableWebMvcConfiguration.class)
我们点进EnableWebMvcConfiguration这个类看一下,它继承了一个父类:DelegatingWebMvcConfiguration
这个父类中有这样一段代码:
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
// 从容器中获取所有的webmvcConfigurer
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
}
protected void addViewControllers(ViewControllerRegistry registry) {
this.configurers.addViewControllers(registry);
}
public void addViewControllers(ViewControllerRegistry registry) {
Iterator var2 = this.delegates.iterator();
while(var2.hasNext()) {
// 将所有的WebMvcConfigurer相关配置来一起调用!包括我们自己配置的和Spring给我们配置的
WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
delegate.addViewControllers(registry);
}
}
所以得出结论:所有的WebMvcConfiguration都会被作用,不止Spring自己的配置类,我们自己的配置类当然也会被调用;
官方文档:
If you want to take complete control of Spring MVCyou can add your own @Configuration annotated with @EnableWebMvc.
全面接管即:SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己去配置!
只需在我们的配置类中要加一个@EnableWebMvc。
如果我们全面接管了SpringMVC了,我们之前SpringBoot给我们配置的静态资源映射一定会无效。
注意:
在springboot中,有非常多的xxx Configuration (配置类)来帮助我们进行扩展配置,只要看见了,我们就要注意了,看看它到底给扩展了什么功能。不管是在源码中,还是在项目中,都要注意
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-63AAlZkU-1647620460291)(D:\Typora\图片位置\image-20220313215615559.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.6.4/reference/htmlsingle/#data.sql.jdbc
创建测试项目测试数据源:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pgaaDhTw-1647620460291)(D:\Typora\图片位置\image-20220314230636976.png)]
<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>
spring:
datasource:
username: root
password: 123456
#?serverTimezone=UTC解决时区的报错
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
package com.wang;
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 Springboot05DataApplicationTests {
//DI注入数据源
@Autowired
DataSource dataSource;
@Test
public void contextLoads() throws SQLException {
//启动看控制台最下面查看一下默认数据源:com.zaxxer.hikari.HikariDataSource
System.out.println(dataSource.getClass());
//获得数据库连接
Connection connection =dataSource.getConnection();
System.out.println(connection);
//关闭连接
connection.close();
}
}
结果:我们可以看到他默认给我们配置的数据源为 : class com.zaxxer.hikari.HikariDataSource ,但是我们并没有手动配置,我们来全局搜索一下,找到数据源的所有自动配置都在 :DataSourceAutoConfiguration文件:
@Import(
{Hikari.class, Tomcat.class, Dbcp2.class, Generic.class, DataSourceJmxConfiguration.class}
)
protected static class PooledDataSourceConfiguration {
protected PooledDataSourceConfiguration() {
}
}
这里导入的类都在 DataSourceConfiguration 配置类下,可以看出 Spring Boot 2.6.4 默认使用HikariDataSource 数据源,而以前版本,如 Spring Boot 1.5 默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源;
HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀;
可以使用 spring.datasource.type 指定自定义的数据源类型,值为 要使用的连接池实现的完全限定名。
关于数据源我们并不做介绍,有了数据库连接,显然就可以 CRUD 操作数据库了。但是我们需要先了解一个对象 JdbcTemplate
有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库;
即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。
数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。
Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用
JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类
只要看到以***Template结尾的,拿来即用就行,都是官方为我们配置好的
JdbcTemplate主要提供以下几类方法:
测试:
编写一个JDBCController,注入 jdbcTemplate,编写测试方法进行访问测试;
package com.wang.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@RestController
public class JDBCController {
/**
* Spring Boot 默认提供了数据源,默认提供了 org.springframework.jdbc.core.JdbcTemplate
* JdbcTemplate 中会自己注入数据源,用于简化 JDBC操作
* 还能避免一些常见的错误,使用起来也不用再自己来关闭数据库连接
*/
@Autowired
JdbcTemplate jdbcTemplate;
//查询user表中所有数据
//List 中的1个 Map 对应数据库的 1行数据
//Map 中的 key 对应数据库的字段名,value 对应数据库的字段值
//输入:http://localhost:8080/userList 进行查询数据
@GetMapping("/userList")
public List<Map<String, Object>> userList(){
String sql = "select * from user";
List<Map<String, Object>> list_maps = jdbcTemplate.queryForList(sql);
return list_maps;
}
//新增一个用户
//输入:http://localhost:8080/addUser 进行新增
@GetMapping("/addUser")
public String addUser(){
//插入语句,注意时间问题
String sql = "insert into mybatis.user(id,name,pwd) values (9,'小天','123456')";
jdbcTemplate.update(sql);
//查询
return "add-Ok";
}
//修改用户信息
//输入:http://localhost:8080/updateUser/4 进行修改4号
@GetMapping("/updateUser/{id}")
public String updateUser(@PathVariable("id") int id){
//插入语句
String sql = "update mybatis.user set name=?,pwd=? where id="+id;
//数据封装
Object[] objects = new Object[2];
objects[0] = "六六六";
objects[1] = "111111";
jdbcTemplate.update(sql,objects);
//查询
return "updateUser-Ok";
}
//删除用户
//输入:http://localhost:8080/deleteUser/4 进行删除4号
@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/
和其它连接池一样DRUID的DataSource类为:com.alibaba.druid.pool.DruidDataSource 基本配置参数如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pJx4tx1H-1647620460292)(D:\Typora\图片位置\image-20220315002235659.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U3ouPAJ4-1647620460292)(D:\Typora\图片位置\image-20220315002505863.png)]
配置 | 缺省值 | 说明 |
---|---|---|
testWhileIdle | false | 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 |
timeBetweenEvictionRunsMillis | 1分钟(1.0.14) | 有两个含义:(1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableldleTimeMillis则关闭物理连接(2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 |
numTestsPerEvictionRun | 不再使用,一个DruidDataSource只支持一个EvictionRun | |
minEvictableIdleTimeMillis | 30分钟(1.0.14) | 连接保持空闲而不被驱逐的最长时间 |
connectionInitSqls | 物理连接初始化的时候执行的sql | |
exceptionSorter | 根据dbType自动识别 | 当数据库抛出一些不可恢复的异常时,抛弃连接 |
filters | 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall | |
proxyFilters | 类型是List |
|
removeAbandoned | false | 开启回收不活跃的连接池 |
removeAbandonedTimeout | 当程序从池中获取到连接开始算起,超过此值后,将强制回收不活跃的连接池(单位秒)。 |
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
dependency>
spring:
datasource:
username: root
password: 123456
#?serverTimezone=UTC解决时区的报错
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource # 自定义数据源
//DI注入数据源
@Autowired
DataSource dataSource;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KeCGesGw-1647620460292)(D:\Typora\图片位置\image-20220315122646732.png)]
我们可以配置一些参数来测试一下:
spring:
datasource:
username: root
password: 123456
#?serverTimezone=UTC解决时区的报错
url: jdbc:mysql://localhost:3306/mybatis?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
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
package com.wang.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();
}
}
package com.wang;
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 Springboot05DataApplicationTests {
//DI注入数据源
@Autowired
DataSource dataSource;
@Test
public 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-js98rRe9-1647620460293)(D:\Typora\图片位置\image-20220315125052429.png)]
Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的 web 页面。
所以第一步需要设置 Druid 的后台管理页面,比如 登录账号、密码 等;配置后台管理;
//配置 Druid 监控管理后台的Servlet;
//因为SpringBoot内置了Servlet容器。所以没有web.xml文件,所以使用替代方法 Spring Boot 的注册 Servlet 方式
//后台监控:web.xml
@Bean
public ServletRegistrationBean statViewServlet() {
ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
// 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet
// 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
//后台需要有人登录,账号密码配置
HashMap<String, String> initParameters = new HashMap<>();
//增加配置
initParameters.put("loginUsername", "admin"); //后台管理界面的登录账号,登录的key是固定的loginUsername和loginPassword
initParameters.put("loginPassword", "123456"); //后台管理界面的登录密码
initParameters.put("allow", "");
//后台允许谁可以访问
//initParams.put("allow", "localhost"):表示只有本机可以访问
//initParams.put("allow", ""):为空或者为null时,表示允许所有访问
//deny:Druid 后台拒绝谁访问
//initParams.put("liupeiwang", "192.168.11.123");表示禁止此ip访问
//设置初始化参数
bean.setInitParameters(initParameters);
return bean;
}
配置完毕后,我们可以先选择访问 :http://localhost:8080/druid,回车后,我们即会进入到http://localhost:8080/druid/login.html
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aD9BlnKu-1647620460293)(D:\Typora\图片位置\image-20220315131131071.png)]
再输入我们上面设置的用户名admin和密码123456,登录。进入之后
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fSuWrWNC-1647620460294)(D:\Typora\图片位置\image-20220315131506215.png)]
//配置 Druid 监控 之 web 监控的 filter
//WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计
@Bean
public FilterRegistrationBean webStatFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());
//exclusions:设置哪些请求进行过滤排除掉用exclusions,从而不进行统计
Map<String, String> initParameters = new HashMap<>();
//这些东西不进行统计
initParameters.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
bean.setInitParameters(initParameters);
//"/*" 表示过滤所有请求
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
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cYSv4aGA-1647620460294)(D:\Typora\图片位置\image-20220315145944747.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UMqZ7msp-1647620460294)(D:\Typora\图片位置\image-20220315145402769.png)]
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.2version>
dependency>
# 数据库连接信息
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
package com.wang;
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.SQLException;
@SpringBootTest
class Springboot06MybatisApplicationTests {
@Autowired
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
System.out.println(dataSource.getClass());
System.out.println(dataSource.getConnection());
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PiX9KHBb-1647620460295)(D:\Typora\图片位置\image-20220315153507227.png)]
User.java
package com.wang.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;
}
UserMapper.java接口
package com.wang.mapper;
import com.wang.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
//@Mapper : 这个注解表示本类是一个 MyBatis 的 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);
}
UserMapper.xml
<mapper namespace="com.wang.mapper.UserMapper">
<select id="queryUserList" resultType="User">
select * from mybatis.user
select>
<select id="queryUserById" resultType="User">
select * from mybatis.user where id= #{id}
select>
<insert id="addUser" parameterType="User">
insert into mybatis.user (id,name,pwd) values (#{id},#{name},#{pwd})
insert>
<update id="updateUser" parameterType="User">
update mybatis.user set name=#{name},pwd=#{pwd} where id=#{id}
update>
<delete id="deleteUser" parameterType="int">
delete from mybatis.user where id=#{id}
delete>
mapper>
package com.wang.controller;
import com.wang.mapper.UserMapper;
import com.wang.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
private UserMapper userMapper;
//查找所有
@GetMapping("/queryUserList")
public List<User> queryUserList(){
List<User> userList = userMapper.queryUserList();
for (User user : userList) {
System.out.println(user);
}
return userList;
}
//根据id选择用户
@GetMapping("queryUserById")
public String queryUserById(){
User user = userMapper.queryUserById(1);
System.out.println(user);
return "queryUserById-ok";
}
//添加一个用户
@GetMapping("/addUser")
public String addUser(){
userMapper.addUser(new User(10,"毛毛","123456"));
return "addUser-ok";
}
//修改一个用户
@GetMapping("/updateUser")
public String updateUser(){
userMapper.updateUser(new User(10,"兔兔","123"));
return "updateUser-ok";
}
//根据id删除用户
@GetMapping("/deleteUser")
public String deleteUser(){
userMapper.deleteUser(9);
return "deleteUser-ok";
}
}
# 数据库连接信息
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 整合mybatis
mybatis.type-aliases-package=com.wang.pojo
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
测试结果完成,搞定收工!
在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。安全不属于功能性需求,因为就算没设置安全,我们的网站也照样可以跑起来。
市面上存在比较有名的:Shiro,Spring Security !
这里需要阐述一下的是,每一个框架的出现都是为了解决某一问题而产生了,那么Spring Security框架的出现是为了解决什么问题呢?
首先我们看下它的官网介绍:Spring Security官网地址:https://spring.io/projects/spring-security 还有https://docs.spring.io/spring-security/reference/index.html 还有 https://docs.spring.io/spring-security/reference/reactive/getting-started.html
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements
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),可以对应用中的领域对象进行细粒度的控制。
新建一个初始的springboot-07-security项目,选择web模块,选择thymeleaf模块
导入静态资源
controller跳转!RouterController.java
package com.wang.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;
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jzEqZdYj-1647620460295)(D:\Typora\图片位置\image-20220315173606475.png)]
Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
“授权” (Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
这个概念是通用的,而不是只在Spring Security 中存在。
目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
参考官网:https://spring.io/projects/spring-security
查看我们自己项目中的版本,找到对应的帮助文档:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-inkKK0nb-1647620460296)(D:\Typora\图片位置\image-20220315202339051.png)]
package com.wang.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 {
}
}
package com.wang.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") //vip1可以访问
.antMatchers("/level2/**").hasRole("vip2") //vip2可以访问
.antMatchers("/level3/**").hasRole("vip3"); //vip3可以访问
}
}
测试一下:发现除了首页都进不去了!因为我们目前没有登录的角色,因为请求需要登录的角色拥有对应的权限才可以!
在configure()方法中也就是SecurityConfig类中继续加入以下配置,开启自动配置的登录功能!
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
//没有权限默认会到登录页面
http.formLogin();
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sw5CcfWq-1647620460296)(D:\Typora\图片位置\image-20220315204154827.png)]
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿....
auth.inMemoryAuthentication()
.withUser("liupeiwang").password("123456").roles("vip2","vip3")
.and()
.withUser("root").password("123456").roles("vip1","vip2","vip3")
.and()
.withUser("guest").password("123456").roles("vip1","vip2");
}
There is no PasswordEncoder mapped for the id “null”
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WPOEnaL5-1647620460296)(D:\Typora\图片位置\image-20220315205250171.png)]
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿....
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("liupeiwang").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
}
测试,发现,登录成功,并且每个角色只能访问自己认证下的规则!搞定
授权和认证加起来总的就是:
package com.wang.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 {
//定制请求的授权规则
// 首页所有人可以访问,功能页只有对应有权限的人才能访问
http.authorizeRequests().antMatchers("/").permitAll() //所有人可访问
.antMatchers("/level1/**").hasRole("vip1") //vip1可以访问
.antMatchers("/level2/**").hasRole("vip2") //vip2可以访问
.antMatchers("/level3/**").hasRole("vip3"); //vip3可以访问
//没有权限默认会到登录页面
http.formLogin();
}
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿....
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("liupeiwang").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
}
}
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//....
//开启自动配置的注销的功能
// /logout 注销请求
http.logout();
}
<a class="item" th:href="@{/logout}">
<i class="sign-out icon">i> 注销
a>
我们可以去测试一下,登录成功后点击注销,发现注销完毕会跳转到登录页面!
但是,我们想让他注销成功后,依旧可以跳转到首页,该怎么处理呢?
// .logoutSuccessUrl("/"); 注销成功来到首页
http.logout().logoutSuccessUrl("/");
测试,注销完毕后,发现跳转到首页OK
我们现在又来一个需求:用户没有登录的时候,导航栏上只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如liupeiwang这个用户,它只有 vip2,vip3功能,那么登录则只显示这两个功能,而vip1的功能菜单不显示!这个就是真实的网站情况了!该如何做呢?
我们需要结合thymeleaf中的一些功能
sec:authorize=“isAuthenticated()”:是否认证登录!来显示不同的页面
Maven依赖:
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
<version>3.0.4.RELEASEversion>
dependency>
导入命名空间:
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
修改导航栏,增加认证判断
<div class="right menu">
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/login}">
<i class="address card icon">i> 登录
a>
div>
<div sec:authorize="isAuthenticated()">
<a class="item">
<i class="address card icon">i>
用户名:<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="address card icon">i> 注销
a>
div>
div>
重启测试,我们可以登录试试看,登录成功后确实,显示了我们想要的页面;
如果注销404了,就是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在spring security中关闭csrf功能;我们试试:在 配置中增加 http.csrf().disable();
http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.logout().logoutSuccessUrl("/");
<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>
测试一下!
权限控制和注销搞定!
现在的情况,我们只要登录之后,关闭浏览器,再登录,就会让我们重新登录,但是很多网站的情况,就是有一个记住密码的功能,这个该如何实现呢?很简单
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//。。。。。。。。。。。
//记住我
http.rememberMe();
}
思考:如何实现的呢?其实非常简单
我们可以查看浏览器的cookie
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eICoQjnj-1647620460297)(D:\Typora\图片位置\image-20220315223433183.png)]
4、结论:登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了。如果点击注销,则会删除这个cookie,具体的原理我们在JavaWeb阶段都讲过了,这里就不在多说了!
现在这个登录页面都是spring security 默认的,怎么样可以使用我们自己写的Login界面呢?
http.formLogin().loginPage("/toLogin");
<a class="item" th:href="@{/toLogin}">
<i class="address card icon">i> 登录
a>
我们登录,需要将这些信息发送到哪里,我们也需要配置,login.html 配置提交请求及方式,方式必须为post:
在 loginPage()源码中的注释上有写明:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BoqL1jhm-1647620460297)(D:\Typora\图片位置\image-20220315223611610.png)]
<form th:action="@{/login}" method="post">
<div class="field">
<label>Usernamelabel>
<div class="ui left icon input">
<input type="text" placeholder="Username" name="username">
<i class="user icon">i>
div>
div>
<div class="field">
<label>Passwordlabel>
<div class="ui left icon input">
<input type="password" name="password">
<i class="lock icon">i>
div>
div>
<input type="submit" class="ui blue submit button"/>
form>
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin")
.loginProcessingUrl("/login"); // 登陆表单提交请求
<input type="checkbox" name="remember"> 记住我
//定制记住我的参数!
http.rememberMe().rememberMeParameter("remember");
完整配置代码:
package com.wang.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
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");
//开启自动配置的登录功能:如果没有权限,就会跳转到登录页面!
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin")
.loginProcessingUrl("/login"); // 登陆表单提交请求
//开启自动配置的注销的功能
// /logout 注销请求
// .logoutSuccessUrl("/"); 注销成功来到首页
http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.logout().logoutSuccessUrl("/");
//记住我
http.rememberMe().rememberMeParameter("remember");
}
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿....
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
}
}
Apache Shiro 是一个Java 的安全(权限)框架。
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。
Shiro可以完成,认证,授权,加密,会话管理,Web集成,缓存等。
下载地址:https://shiro.apache.org/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hmVJL9rm-1647620460297)(D:\Typora\图片位置\image-20220315230056841.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KoYNOZgH-1647620460298)(D:\Typora\图片位置\image-20220315230134807.png)]
Authentication:身份认证、登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能否进行什么操作,如:验证某个用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限!
Session Manager:会话管理,即用户登录后就是第一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通的JavaSE环境,也可以是Web环境;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息,拥有的角色、权限不必每次去查,这样可以提高效率
Concurrency:Shiro支持多线程应用的并发验证,即,如在一个线程中开启另一个线程,能把权限自动的传播过去
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
从外部来看Shiro,即从应用程序角度来观察如何使用shiro完成工作:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uOAIBiD9-1647620460298)(D:\Typora\图片位置\image-20220315230352129.png)]
subject:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject,Subject代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等,与Subject的所有交互都会委托SecurityManager;Subject其实是一个门面,SecurityManageer 才是实际的执行者
SecurityManager:安全管理器,即所有与安全有关的操作都会与SercurityManager交互,并且它管理着所有的Subject,可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC的DispatcherServlet的角色
Realm:Shiro从Realm获取安全数据(如用户,角色,权限),就是说SecurityManager 要验证用户身份,那么它需要从Realm 获取相应的用户进行比较,来确定用户的身份是否合法;也需要从Realm得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把Realm看成DataSource;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y5w1kjGr-1647620460298)(D:\Typora\图片位置\image-20220315230506282.png)]
查看官网文档:http://shiro.apache.org/tutorial.html
官方的quickstart:https://github.com/apache/shiro/tree/main/samples/quickstart
创建一个maven父工程springboot-08-shiro,用于学习Shiro,删掉不必要的src
创建一个普通的Maven子工程:hello-shiro
根据官方文档,先我们来导入Shiro的依赖
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>1.8.0version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>jcl-over-slf4jartifactId>
<version>1.7.36version>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-slf4j-implartifactId>
<version>2.17.2version>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-coreartifactId>
<version>2.17.2version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
log4j2.xml
<Configuration name="ConfigTest" status="ERROR" monitorInterval="5">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
Console>
Appenders>
<Loggers>
<Logger name="org.springframework" level="warn" additivity="false">
<AppenderRef ref="Console"/>
Logger>
<Logger name="org.apache" level="warn" additivity="false">
<AppenderRef ref="Console"/>
Logger>
<Logger name="net.sf.ehcache" level="warn" additivity="false">
<AppenderRef ref="Console"/>
Logger>
<Logger name="org.apache.shiro.util.ThreadContext" level="warn" additivity="false">
<AppenderRef ref="Console"/>
Logger>
<Root level="info">
<AppenderRef ref="Console"/>
Root>
Loggers>
Configuration>
shiro.ini
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Simple Quickstart application showing how to use Shiro's 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:
// Use the shiro.ini file at the root of the classpath
// (file: and url: prefixes load from files and urls respectively):
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
// for this simple example quickstart, make the SecurityManager
// accessible as a JVM singleton. Most applications wouldn't do this
// and instead rely on their container configuration or web.xml for
// webapps. That is outside the scope of this simple quickstart, so
// we'll just do the bare minimum so you can continue to get a feel
// for things.
SecurityUtils.setSecurityManager(securityManager);
// Now that a simple Shiro environment is set up, let's see what you can do:
// get the currently executing user:
//获取当前的用户对象Subject
Subject currentUser = SecurityUtils.getSubject();
// Do some stuff with a Session (no need for a web or EJB container!!!)
//通过当前用户拿到Session
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}
// let's login the current user so we can check against roles and permissions:
//判断当前的用户是否被认证
if (!currentUser.isAuthenticated()) {
//Token:令牌,没有获取,随机配置的
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);//设置记住我
try {
currentUser.login(token);//执行登录操作
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
//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);
}
}
测试运行一下
如果报错,则导入一下 commons-logging 的依赖,我没报错,不用加
<dependency>
<groupId>commons-logginggroupId>
<artifactId>commons-loggingartifactId>
<version>1.2version>
dependency>
默认是在test,然后重启,那么我们的quickstart就结束了,默认的日志消息!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZSNEEUg4-1647620460299)(D:\Typora\图片位置\image-20220316152200713.png)]
导入了一堆包!
类的描述
/**
Simple Quickstart application showing how to use Shiro's API.
简单的快速启动应用程序,演示如何使用Shiro的API。
*/
// 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):
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
// for this simple example quickstart, make the SecurityManager
// accessible as a JVM singleton. Most applications wouldn't do this
// and instead rely on their container configuration or web.xml for
// webapps. That is outside the scope of this simple quickstart, so
// we'll just do the bare minimum so you can continue to get a feel
// for things.
SecurityUtils.setSecurityManager(securityManager);
// 现在已经建立了一个简单的Shiro环境,让我们看看您可以做什么:
// Now that a simple Shiro environment is set up, let's see what you can do:
// 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) { //密码不对的异常!
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项目叫shiro-springboot、选中web模块即可!
导入Maven依赖 thymeleaf
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
<version>2.6.4version>
dependency>
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>首页h1>
<p th:text="${msg}">p>
body>
html>
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello,Shiro");
return "index";
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eo8NwSBI-1647620460299)(D:\Typora\图片位置\image-20220316172919037.png)]
回顾核心三个API(重要记住):
Subject:用户主体
SecurityManager:安全管理器
Realm:Shiro 连接数据
步骤:
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.8.0version>
dependency>
package com.wang.config;
import org.springframework.context.annotation.Configuration;
//声明为配置类
@Configuration
public class ShiroConfig {
//创建 ShiroFilterFactoryBean
// 创建 DefaultWebSecurityManager
// 创建 realm 对象,需要自定义类
}
我们倒着来,先想办法创建一个 realm 对象
我们需要自定义一个 realm 的类,用来编写一些查询的方法,或者认证与授权的逻辑
package com.wang.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;
//自定义的Realm
public class UserRealm extends AuthorizingRealm {
//执行授权逻辑
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权逻辑PrincipalCollection");
return null;
}
//执行认证逻辑
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了=>认证逻辑AuthenticationToken");
return null;
}
}
package com.wang.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//声明为配置类
@Configuration
public class ShiroConfig {
//创建 ShiroFilterFactoryBean
// 创建 DefaultWebSecurityManager
// 创建 realm 对象,需要自定义类
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
}
// 创建 DefaultWebSecurityManager
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultSecurityManager(@Qualifier("userRealm")UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联Realm
securityManager.setRealm(userRealm);
return securityManager;
}
//创建 ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
最后上完整的配置:
package com.wang.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
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
// 创建 DefaultWebSecurityManager
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultSecurityManager(@Qualifier("userRealm")UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联Realm
securityManager.setRealm(userRealm);
return securityManager;
}
// 创建 realm 对象,需要自定义类
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
}
<body>
<h1>addh1>
body>
<body>
<h1>updateh1>
body>
@RequestMapping("/user/add")
public String toAdd(){
return "user/add";
}
@RequestMapping("/user/update")
public String toUpdate(){
return "user/update";
}
<a th:href="@{/user/add}">adda> |
<a th:href="@{/user/update}">updatea>
//设置安全管理器
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!但是发现,点击后会跳转到一个Login.jsp页面,这
个不是我们想要的效果,我们需要自己定义一个Login页面!
我们编写一个自己的Login页面,在templates/login.html
<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>
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
//修改到要跳转的login页面;
bean.setLoginUrl("/toLogin");
再次测试,成功的跳转到了我们指定的Login页面!
可以在ShiroConfig中优化一下代码,我们这里的拦截可以使用 通配符来操作
Map<String,String> filterMap = new LinkedHashMap<String, String>();
//filterMap.put("/user/add","authc");
//filterMap.put("/user/update","authc");
filterMap.put("/user/*","authc"); //user目录下的全部拦截
bean.setFilterChainDefinitionMap(filterMap);
//登录操作
@RequestMapping("/login")
public String login(String username,String password,Model model){
//使用shiro,编写认证操作
//1. 获取Subject,获取当前的用户
Subject subject = SecurityUtils.getSubject();
//2. 封装用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//3. 执行登录的方法,只要没有异常就代表登录成功!
try {
subject.login(token); //登录成功!返回首页
return "index";
} catch (UnknownAccountException e) { //用户名不存在
model.addAttribute("msg","用户名不存在");
return "login";
} catch (IncorrectCredentialsException e) { //密码错误
model.addAttribute("msg","密码错误");
return "login";}
}
}
登录页面login.html增加一个 msg 提示:
<p style="color:red;" th:text="${msg}">p>
给login.html表单增加一个提交地址:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>>登录页面title>
head>
<body>
<h1>登录页面h1>
<hr>
<form th:action="@{/login}">
<p> 用户名: <input type="text" name="username"> p>
<p> 密码: <input type="text" name="password"> p>
<p> <input type="submit"> p>
form>
body>
html>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LOwf56bU-1647620460300)(D:\Typora\图片位置\image-20220316213757711.png)]
确实执行了我们的认证逻辑!
//执行认证逻辑
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了=>认证逻辑AuthenticationToken");
//假设数据库的用户名和密码
String name = "root";
String password = "123456";
//1.判断用户名
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
if (!userToken.getUsername().equals(name)) {
// 用户名不存在
return null; //shiro底层就会抛出 UnknownAccountException
}
//2. 验证密码,我们可以使用一个AuthenticationInfo实现类 SimpleAuthenticationInfo
// shiro会自动帮我们验证!重点是第二个参数就是要验证的密码!
return new SimpleAuthenticationInfo("", password, "");
}
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
dependency>
spring:
datasource:
username: root
password: 123456
#?serverTimezone=UTC解决时区的报错
url: jdbc:mysql://localhost:3306/mybatis?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
#别名配置
mybatis.type-aliases-package=com.wang.pojo
mybatis.mapper-locations=classpath:mapper/*.xml
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.22version>
<scope>providedscope>
dependency>
package com.wang.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;
}
package com.wang.mapper;
import com.wang.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Repository
@Mapper
public interface UserMapper {
public User queryUserByName(String name);
}
<mapper namespace="com.wang.mapper.UserMapper">
<select id="queryUserByName" parameterType="String" resultType="User">
select * from mybatis.user where name = #{name}
select>
mapper>
public interface UserService {
public User queryUserByName(String name);
}
package com.wang.service;
import com.wang.mapper.UserMapper;
import com.wang.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);
}
}
@SpringBootTest
class ShiroSpringbootApplicationTests {
@Autowired
UserServiceImpl userService;
@Test
void contextLoads() {
System.out.println(userService.queryUserByName("旺旺"));
}
}
完全OK,成功查询出来了!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nUjfqgvd-1647620460301)(D:\Typora\图片位置\image-20220316223134971.png)]
//自定义的Realm
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
//执行授权逻辑
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权逻辑PrincipalCollection");
return null;
}
//执行认证逻辑
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了=>认证逻辑AuthenticationToken");
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
//连接真实数据库
User user=userService.queryUserByName(userToken.getUsername());
if (user == null){
//用户名不存在
return null; //shiro底层就会抛出,UnknownAccountException
}
return new SimpleAuthenticationInfo("",user.getPwd(),"");
}
}
思考?这个Shiro,是怎么帮我们实现密码自动比对的呢?
我们可以去 realm的父类 AuthorizingRealm 的父类 AuthenticatingRealm 中找一个方法
核心: getCredentialsMatcher() 翻译过来:获取证书匹配器
我们去看这个接口 CredentialsMatcher 有很多的实现类,MD5盐值加密
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yx2Pu3wp-1647620460301)(D:\Typora\图片位置\image-20220316235339019.png)]
我们的密码一般都不能使用明文保存?需要加密处理;思路分析
如何把一个字符串加密为MD5
替换当前的Realm 的 CredentialsMatcher 属性,直接使用 Md5CredentialsMatcher 对象,
并设置加密算法
使用shiro的过滤器来拦截请求即可!
//授权过滤器,正常的情况下,没有授权会跳转到401未授权页面
filterMap.put("/user/add","perms[user:add]"); //记得注意顺序!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l0ghGnWH-1647620460302)(D:\Typora\图片位置\image-20220317000412659.png)]
了
@RequestMapping("/noauth")
@ResponseBody
public String noAuth(){
return "未经授权不能访问此页面";
}
然后再 在ShiroConfig的shiroFilterFactoryBean 中配置一个未授权的请求页面!
//未授权请求页面
bean.setUnauthorizedUrl("/noauth");
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-37vk3YPP-1647620460302)(D:\Typora\图片位置\image-20220317001045560.png)]
//执行授权逻辑
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权逻辑PrincipalCollection");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermission("user:add");
return info;
}
我们再次登录测试,发现登录的用户是可以进行访问add 页面了!授权成功!
问题,我们现在完全是硬编码,无论是谁登录上来,都可以实现授权通过,但是真实的业务情况应该
是,每个用户拥有自己的一些权限,从而进行操作,所以说,权限,应该在用户的数据库中,正常的情
况下,应该数据库中是由一个权限表的,我们需要联表查询,但是这里为了大家操作理解方便一些,我
们直接在数据库表中增加一个字段来进行操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C7tc6k8L-1647620460303)(D:\Typora\图片位置\image-20220317001822208.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SlHPylju-1647620460303)(D:\Typora\图片位置\image-20220317001923657.png)]
package com.wang.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;
private String perms;
}
在用户登录授权的时候,将用户放在 Principal 中,改造下之前的代码:在UserRealm中
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
然后再授权的地方获得这个用户,从而获得它的权限:
//执行授权逻辑
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权逻辑PrincipalCollection");
//给资源进行授权
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermission("user:add");
//拿到当前登录的这个对象
Subject subject = SecurityUtils.getSubject(); //获得当前对象
User currentUser = (User) subject.getPrincipal(); //拿到User对象
//设置当前用户的权限
info.addStringPermission(currentUser.getPerms());
return info;
}
//授权过滤器,正常的情况下,没有授权会跳转到401未授权页面
filterMap.put("/user/add","perms[user:add]"); //记得注意顺序!
filterMap.put("/user/update","perms[user:update]");
我们启动项目,登录不同的账户,进行测试一下!
测试完美通过OK!
根据权限展示不同的前端页面
<dependency>
<groupId>com.github.theborakompanionigroupId>
<artifactId>thymeleaf-extras-shiroartifactId>
<version>2.1.0version>
dependency>
//整合ShiroDialect:方言,用于 thymeleaf 和 shiro 标签配合使用
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect(); }
}
<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的效果!登录后,可以看到不同的用户,有不同的效果,现在就已经接近完美
了~!还不是最完美
为了完美,我们在用户登录后应该把信息放到Session中,在UserRealm中,我们完善下!在执行认证逻辑时候,加
入session
Subject currentSubject = SecurityUtils.getSubject();
Session session = currentSubject.getSession();
session.setAttribute("loginUser",user);
<p th:if="${session.loginUser==null}">
<a th:href="@{/toLogin}">登录a>
p>
SpringSecurity 和 Shiro 两个安全的框架,主要是想让大家多一些思路,其实什么都不用,我们靠拦截器也可以实现这些功能对吧,但是可能需要花费大量的时间和代码,还有就是Bug多,思考不全,而现在,我们两个框架都会使用了,也给大家对比的进行学习了,当然真实的工作中,可能代码会更加的复杂。需要大家在工作中再多去练习和使用,将这些框架可以运用到自己的项目中才是王道,不然学了也是白学对吧,几天就忘记了,没有什么用,关于底层的实现原理,也希望大家下去可以多看源码学习,后面的学习中已经带大家看了很多源码了,希望大家能够自己多去总结和吸收!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3XxtOKdX-1647620460303)(D:\Typora\图片位置\image-20220317201855702.png)]
学习目标:
前后端分离
产生的问题
解决方案
Swagger
使用Swagger
要求:jdk 1.8 + 否则swagger3无法运行
步骤:
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-boot-starterartifactId>
<version>3.0.0version>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8xHAYCeh-1647620460304)(D:\Typora\图片位置\image-20220317233116310.png)]
@RestController
public class HelloController {
@RequestMapping(value = "/hello")
public String hello() {
return "Hello";
}
}
@Configuration //配置类
public class SwaggerConfig {
}
@EnableOpenApi //开启swagger3配置
@SpringBootApplication
public class SwaggerDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SwaggerDemoApplication.class, args);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0anYG9bl-1647620460304)(D:\Typora\图片位置\image-20220317233428647.png)]
在SwaggerConfig中配置:
package com.wang.swagger.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
@Configuration //配置类
public class SwaggerConfig {
// Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger
@Bean
public Docket docket() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo()); //Docket 实例关联上 apiInfo()
}
//配置Swagger信息=apiInfo
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("刘培旺的Swagger3接口文档")
.description("刘培旺最帅")
.termsOfServiceUrl("https://editor.swagger.io/")
.version("1.0")
.contact(new Contact("刘培旺。", "https://www.baidu.com", "[email protected]"))
.license("Apache 2.0")
.licenseUrl("https://github.com/")
.build();
}
}
重启项目,访问测试http://localhost:8080/swagger-ui/ 看下效果;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ahG0eMsU-1647620460305)(D:\Typora\图片位置\image-20220318003552604.png)]
// Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger
@Bean
public Docket docket() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo()) //Docket 实例关联上 apiInfo()
.select() // 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
//basePackage:指定扫描的包
.apis(RequestHandlerSelectors.basePackage("com.wang.swagger.controller"))
.build();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xu1fh7gH-1647620460305)(D:\Typora\图片位置\image-20220318122825312.png)]
// 扫描所有,项目中的所有接口都会被扫描到
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) // 根据包路径扫描接口
// Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger
@Bean
public Docket docket() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo()) //Docket 实例关联上 apiInfo()
.select() // 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
//basePackage:指定扫描的包 any(): // 任何请求都扫描,扫描全部
//none(): 任何请求都不扫描,都不扫描
.apis(RequestHandlerSelectors.basePackage("com.wang.swagger.controller"))
//paths:过滤什么路径, 配置如何通过path过滤,即这里只扫描请求以/wang开头的接口
.paths(PathSelectors.ant("/wang/**"))
.build();
}
any() // 任何请求都扫描
none() // 任何请求都不扫描
regex(final String pathRegex) // 通过正则表达式控制
ant(final String antPattern) // 通过ant()控制,通过ant加上指定的路径,任何请求都会扫描。不扫描请求,通过正则来匹配
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DJVwv3yA-1647620460306)(D:\Typora\图片位置\image-20220318123938411.png)]
// Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger
@Bean
public Docket docket() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo()) //Docket 实例关联上 apiInfo()
.enable(false) //配置是否启用Swagger,如果是false,在浏览器将无法访问
.select() // 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
//basePackage:指定扫描的包 any(): // 任何请求都扫描,扫描全部
//none(): 任何请求都不扫描,都不扫描
.apis(RequestHandlerSelectors.basePackage("com.wang.swagger.controller"))
//paths:过滤什么路径, 配置如何通过path过滤,即这里只扫描请求以/wang开头的接口
// .paths(PathSelectors.ant("/wang/**"))
.build();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M5LOBglp-1647620460306)(D:\Typora\图片位置\image-20220318124717361.png)]
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
// Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swagger
@Bean
public Docket docket(Environment environment) {
// 设置要显示swagger的环境
Profiles profiles = Profiles.of("dev", "test");
// 判断当前是否处于该生产环境
// 通过 enable() 接收此参数判断是否要显示
//通过environment.acceptsProfiles判断是否处在自己设定的环境当中
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo())
.enable(flag) //配置是否启用Swagger,如果是false,在浏览器将无法访问
.select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
.apis(RequestHandlerSelectors.basePackage("com.wang.swagger.controller"))
// 配置如何通过path过滤,即这里只扫描请求以/wang开头的接口
// .paths(PathSelectors.ant("/wang/**"))
.build();
}
#激活dev环境
spring.profiles.active=dev
运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TNbfqBYj-1647620460307)(D:\Typora\图片位置\image-20220318164123279.png)]
// Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swagger
@Bean
public Docket docket() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo()) //Docket 实例关联上 apiInfo()
.groupName("宋秀梅") //分组,默认是default注意写的位置在.apiInfo(apiInfo()) 下面
.enable(true) //配置是否启用Swagger,如果是false,在浏览器将无法访问
.select() // 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
//basePackage:指定扫描的包 any(): // 任何请求都扫描,扫描全部
//none(): 任何请求都不扫描,都不扫描
.apis(RequestHandlerSelectors.basePackage("com.wang.swagger.controller"))
//paths:过滤什么路径, 配置如何通过path过滤,即这里只扫描请求以/wang开头的接口
// .paths(PathSelectors.ant("/wang/**"))
.build();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IXdzP41Q-1647620460307)(D:\Typora\图片位置\image-20220318165741367.png)]
//配置多个api分组
@Bean
public Docket docket1(){
return new Docket(DocumentationType.OAS_30).groupName("group1");
}
@Bean
public Docket docket2(){
return new Docket(DocumentationType.OAS_30).groupName("group2");
}
@Bean
public Docket docket3() {
return new Docket(DocumentationType.OAS_30).groupName("group3");
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gq4ULABO-1647620460307)(D:\Typora\图片位置\image-20220318170103373.png)]
@ApiModel("用户实体") //相当也给文档加的注释
public class User {
@ApiModelProperty("用户名") //相当也给文档加的注释
public String username;
@ApiModelProperty("密码") //相当也给文档加的注释
public String password;
}
//只要我们的接口中,返回值中存在实体类,他就会被扫描到Swagger
@PostMapping(value = "/getUser")
public User user(){
return new User();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dhh9hClL-1647620460308)(D:\Typora\图片位置\image-20220318171657662.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 |
我们也可以给请求的接口配置一些注释
@ApiOperation("刘培旺的接口")
@PostMapping("/wang")
@ResponseBody
public String wang(@ApiParam("这个名字会被返回")String username){
return username;
}
总结
注意一点:在正式发布项目的时候,要关闭Swagger!!!出于安全考虑,而且还能不能让用户也看见吧!而且还能节省运行的内存。
相较于传统的Postman或Curl方式测试接口,使用swagger简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。
Swagger是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出Word接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭Swagger,一来出于安全考虑二来也可以节省运行时内存。
我们可以导入不同的包实现不同的皮肤定义:
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-boot-starterartifactId>
<version>3.0.0version>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NhoetPOn-1647620460308)(D:\Typora\图片位置\image-20220318173203558.png)]
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>swagger-bootstrap-uiartifactId>
<version>1.9.6version>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rvvGZ7iz-1647620460309)(D:\Typora\图片位置\image-20220318173256746.png)]
<dependency>
<groupId>com.github.caspar-chengroupId>
<artifactId>swagger-ui-layerartifactId>
<version>1.1.3version>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9zlBLkav-1647620460309)(D:\Typora\图片位置\image-20220318174518357.png)]
<dependency>
<groupId>com.zyplayergroupId>
<artifactId>swagger-mg-uiartifactId>
<version>2.0.2version>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BJUvuJeO-1647620460309)(D:\Typora\图片位置\image-20220318175012280.png)]
前言
在我们的工作中,常常会用到异步处理任务,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。还有一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息。还有就是邮件的发送,微信的前身也是邮件服务呢?这些东西都是怎么实现的呢?其实SpringBoot都给我们提供了对应的支持,我们上手使用十分的简单,只需要开启一些注解支持,配置一些配置文件即可!那我们来看看吧~
异步处理还是非常常用的,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。
编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况;
@Service
public class AsyncService {
public void hello(){
try {
Thread.sleep(3000); //休息3秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据正在处理");
}
}
编写controller包
编写AsyncController类
我们去写一个Controller测试一下
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;
@RequestMapping("/111")
public String hello(){
asyncService.hello(); //停止3秒,转圈~
return "success";
}
}
问题:我们如果想让用户直接得到消息,就在后台使用多线程的方式进行处理即可,但是每次都需要自己手动去编写多线程的实现的话,太麻烦了,我们只需要用一个简单的办法,在我们的方法上加一个简单的注解即可,如下:
@Service
public class AsyncService {
@Async //告诉Spring这是一个异步方法
public void hello(){
try {
Thread.sleep(3000); //休息3秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据正在处理");
}
}
SpringBoot就会自己开一个线程池,进行调用!但是要让这个注解生效,我们还需要在主程序上添加一个注解@EnableAsync ,开启异步注解功能;
@EnableAsync //开启异步注解功能
@SpringBootApplication
public class SpringbootTaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTaskApplication.class, args);
}
}
邮件发送,在我们的日常开发中,也非常的多,Springboot也帮我们做了支持
测试:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
<version>2.6.4version>
dependency>
看它引入的依赖,可以看到 jakarta.mail
<dependency>
<groupId>com.sun.mailgroupId>
<artifactId>jakarta.mailartifactId>
<version>1.6.7version>
<scope>compilescope>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TvYiEH6A-1647620460310)(D:\Typora\图片位置\image-20220318202103171.png)]
再查找点进去MailSenderJndiConfiguration,这个类中存在bean,JavaMailSenderImpl
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2SqqCbJj-1647620460310)(D:\Typora\图片位置\image-20220318203030712.png)]
我们再在MailSenderAutoConfiguration源码中找到MailProperties查看他的源码,看见可以配置的内容
@ConfigurationProperties(
prefix = "spring.mail"
)
public class MailProperties {
private static final Charset DEFAULT_CHARSET;
private String host; //主机
private Integer port; //端口号
private String username; //qq号
private String password; //授权码
private String protocol = "smtp"; //协议
private Charset defaultEncoding; //默认编码
private Map<String, String> properties; //特性
private String jndiName; //jndi名称
MailSenderAutoConfiguration源码中下面可以看见只要我们写spring.~~~就可以进行配置了
@ConditionalOnProperty(
prefix = "spring.mail",
name = {"host"}
)
[email protected]
spring.mail.password=jplidjifidxgdjia
spring.mail.host=smtp.qq.com
# qq需要配置ssl,开启加密验证
spring.mail.properties.mail.smtp.ssl.enable=true
获取授权码:在QQ邮箱中的设置->账户->开启pop3和smtp服务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5lHzS5eL-1647620460310)(D:\Typora\图片位置\image-20220318203521128.png)]
package com.wang;
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 Springboot09TimeApplicationTests {
@Autowired
JavaMailSenderImpl mailSender;
@Test
void contextLoads() {
//邮件设置1:一个简单的邮件~
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setSubject("刘培旺你好呀!"); //设置主题
mailMessage.setText("谢谢宋秀梅"); //设置文本
mailMessage.setTo("[email protected]"); //设置发送人
mailMessage.setFrom("[email protected]"); //设置发送人
mailSender.send(mailMessage);
}
@Test
public void contextLoads2() throws MessagingException {
//邮件设置2:一个复杂的邮件
MimeMessage mimeMessage = mailSender.createMimeMessage();
//组装
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("通知-明天来刘培旺这听课");
helper.setText("今天 7:30来开会",true);
//发送附件
helper.addAttachment("旺旺旺.jpg",new File("C:\\Users\\LEGION\\Desktop\\抖音\\3d酷炫相册\\原图片\\01.png"));
helper.addAttachment("秀秀秀.jpg",new File("C:\\Users\\LEGION\\Desktop\\抖音\\3d酷炫相册\\原图片\\06.png"));
helper.setTo("[email protected]");
helper.setFrom("[email protected]");
mailSender.send(mimeMessage);
}
}
查看邮箱,邮件接收成功!
我们只需要使用Thymeleaf进行前后端结合即可开发自己网站邮件收发功能了!
项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring为我们提供了异步执行任务调度的方式,提供了两个接口。
两个注解:
**cron表达式:书写顺序是 秒 分 时 日 月 周几 **
字段 | 允许值 | 允许的特殊字符 |
---|---|---|
秒(Seconds) | 0~59的整数 | , - * / 四个字符 |
分(Minutes) | 0~59的整数 | , - * / 四个字符 |
小时(Hours) | 0~23的整数 | , - * / 四个字符 |
日期(DayofMonth) | 1~31的整数(但是你需要考虑你月的天数) | ,- * ? / L W C 八个字符 |
月份(Month) | 1~12的整数或者 JAN-DEC | , - * / 四个字符 |
星期(DayofWeek) | 0~7的整数或者 SUN-SAT (0和7都代表星期天) | , - * ? / L C # 八个字符 |
年(可选,留空)(Year) | 1970~2099 | , - * / 四个字符 |
特殊字符 | 意义 |
---|---|
* | 匹配所有的值。如:*在分钟的字段域里表示 每分钟 |
? | 只在日期域和星期域中使用。它被用来指定“非明确的值” |
- | 指定一个范围。如:“10-12”在小时域意味着“10点、11点、12点” |
, | 指定几个可选值。如:“MON,WED,FRI”在星期域里表示“星期一、星期三、星期五” |
/ | 指定增量。如:“0/15”在秒域意思是没分钟的0,15,30和45秒。“5/15”在分钟域表示没小时的5,20,35和50。符号“”在“/”前面(如:/10)等价于0在“/”前面(如:0/10) |
L | 表示day-of-month和day-of-week域,但在两个字段中的意思不同,例如day-of-month域中表示一个月的最后一天。如果在day-of-week域表示‘7’或者‘SAT’,如果在day-of-week域中前面加上数字,它表示一个月的最后几天,例如‘6L’就表示一个月的最后一个星期五 |
W | 只允许日期域出现。这个字符用于指定日期的最近工作日。例如:如果你在日期域中写 “15W”,表示:这个月15号最近的工作日。所以,如果15号是周六,则任务会在14号触发。如果15好是周日,则任务会在周一也就是16号触发。如果是在日期域填写“1W”即使1号是周六,那么任务也只会在下周一,也就是3号触发,“W”字符指定的最近工作日是不能够跨月份的。字符“W”只能配合一个单独的数值使用,不能够是一个数字段,如:1-15W是错误的 |
LW | L和W可以在日期域中联合使用,LW表示这个月最后一周的工作日 |
# | 只允许在星期域中出现。这个字符用于指定本月的某某天。例如:“6#3”表示本月第三周的星期五(6表示星期五,3表示第三周)。“2#1”表示本月第一周的星期一。“4#5”表示第五周的星期三 |
C | 允许在日期域和星期域出现。这个字符依靠一个指定的“日历”。也就是说这个表达式的值依赖于相关的“日历”的计算结果,如果没有“日历”关联,则等价于所有包含的“日历”。如:日期域是“5C”表示关联“日历”中第一天,或者这个月开始的第一天的后5天。星期域是“1C”表示关联“日历”中第一天,或者星期的第一天的后1天,也就是周日的后一天(周一) |
测试步骤:
我们里面存在一个hello方法,他需要定时执行,怎么处理呢?
package com.wang.service;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
public class ScheduledService {
//秒 分 时 日 月 周几
//30 15 10 * * ? 每天的10点15分30秒被执行
//0 15 10 ? * 1-6 每个月的周一到周六的10点15分执行一次
//注意cron表达式的用法; *代表任意
@Scheduled(cron = "0/6 * * * * ?") //每隔6秒执行一次
//在一个特定的时间执行这个方法
public void hello(){
System.out.println("hello,你被执行了");
}
}
//常用表达式例子
// (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触发
@EnableScheduling //放主程序上,开启定时注解功能
@SpringBootApplication
public class Springboot09TimeApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot09TimeApplication.class, args);
}
}
http://www.bejson.com/othertools/cron/
(1)0/2 * * * * ? 表示每2秒 执行任务
(1)0 0/2 * * * ? 表示每2分钟 执行任务
(1)0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务
(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
(4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
(6)0 0 12 ? * WED 表示每个星期三中午12点
(7)0 0 12 * * ? 每天中午12点触发
(8)0 15 10 ? * * 每天上午10:15触发
(9)0 15 10 * * ? 每天上午10:15触发
(10)0 15 10 * * ? 每天上午10:15触发
(11)0 15 10 * * ? 2005 2005年的每天上午10:15触发
(12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
(13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
(14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
(15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
(18)0 15 10 15 * ? 每月15日上午10:15触发
(19)0 15 10 L * ? 每月最后一日的上午10:15触发
(20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
(22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
分布式系统(distributed system)是建立在网络之上的软件系统。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。。。
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。
在Dubbo的官网文档有这样一张图
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fjDUDO17-1647620460311)(D:\Typora\图片位置\image-20220318232622168.png)]
适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。
缺点:
性能扩展比较难
协同开发问题
不利于升级维护
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KuAcpQ7P-1647620460312)(D:\Typora\图片位置\image-20220318232645674.png)]
通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。
缺点:公用模块无法重复利用,开发性的浪费
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的**分布式服务框架(RPC)**是关键。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wJqyY9VJ-1647620460312)(D:\Typora\图片位置\image-20220318232801899.png)]
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ywX1t5R8-1647620460313)(D:\Typora\图片位置\image-20220318232841117.png)]
通信有两种协议:HTTP,RPC
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;
推荐阅读文章:https://www.jianshu.com/p/2accc2840a1b
RPC基本原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DxkEjnVM-1647620460313)(D:\Typora\图片位置\image-20220318233353740.png)]
步骤解析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RxTzrkU3-1647620460313)(D:\Typora\图片位置\image-20220318233358136.png)]
**RPC两个核心模块:通讯,序列化。**
为什么要序列化?:数据传输需要转换,方便信息传输
Apache Dubbo |ˈdʌbəʊ| 提供了六大核心能力:面向接口代理的高性能RPC调用,智能容错和负载均衡,服务自动注册和发现,高度可扩展能力,运行期流量调度,可视化的服务治理与运维。
Dubbo 提供的基础能力包括:
dubbo官网:https://dubbo.apache.org/zh/
Dubbo3 定义了全新的 RPC 通信协议 – Triple,一句话概括 Triple:它是基于 HTTP/2 上构建的 RPC 协议,完全兼容 gRPC,并在此基础上扩展出了更丰富的语义。 使用 Triple 协议,用户将获得以下能力
Dubbo 的 6 大特性:
1、面向接口代理的高性能 RPC 调用;
2、服务自动注册与发现;
3、智能负载均衡策略;
4、高度可扩展能力;
5、运行期流量调度;
6、可视化的服务治理与运维;
Dubbo 架构图:
服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
调用关系说明
l 服务容器负责启动,加载,运行服务提供者。
l 服务提供者在启动时,向注册中心注册自己提供的服务。
l 服务消费者在启动时,向注册中心订阅自己所需的服务。
l 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
l 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
l 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
点进dubbo官方文档,推荐我们使用Zookeeper 注册中心下载
什么是zookeeper呢?可以查看zookeeper官方文档:https://zookeeper.apache.org/doc/r3.8.0/index.html
2. 这里写完定时任务之后,我们需要在==主程序上增加@EnableScheduling 开启定时任务功能==
```java
@EnableScheduling //放主程序上,开启定时注解功能
@SpringBootApplication
public class Springboot09TimeApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot09TimeApplication.class, args);
}
}
http://www.bejson.com/othertools/cron/
(1)0/2 * * * * ? 表示每2秒 执行任务
(1)0 0/2 * * * ? 表示每2分钟 执行任务
(1)0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务
(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
(4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
(6)0 0 12 ? * WED 表示每个星期三中午12点
(7)0 0 12 * * ? 每天中午12点触发
(8)0 15 10 ? * * 每天上午10:15触发
(9)0 15 10 * * ? 每天上午10:15触发
(10)0 15 10 * * ? 每天上午10:15触发
(11)0 15 10 * * ? 2005 2005年的每天上午10:15触发
(12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
(13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
(14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
(15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
(18)0 15 10 15 * ? 每月15日上午10:15触发
(19)0 15 10 L * ? 每月最后一日的上午10:15触发
(20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
(22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
分布式系统(distributed system)是建立在网络之上的软件系统。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。。。
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。
在Dubbo的官网文档有这样一张图
[外链图片转存中…(img-6dkC1emG-1647620460311)]
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
[外链图片转存中…(img-fjDUDO17-1647620460311)]
适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。
缺点:
性能扩展比较难
协同开发问题
不利于升级维护
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。
[外链图片转存中…(img-KuAcpQ7P-1647620460312)]
通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。
缺点:公用模块无法重复利用,开发性的浪费
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的**分布式服务框架(RPC)**是关键。
[外链图片转存中…(img-wJqyY9VJ-1647620460312)]
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。
[外链图片转存中…(img-ywX1t5R8-1647620460313)]
通信有两种协议:HTTP,RPC
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;
推荐阅读文章:https://www.jianshu.com/p/2accc2840a1b
RPC基本原理
[外链图片转存中…(img-DxkEjnVM-1647620460313)]
步骤解析:
[外链图片转存中…(img-RxTzrkU3-1647620460313)]
**RPC两个核心模块:通讯,序列化。**
为什么要序列化?:数据传输需要转换,方便信息传输
Apache Dubbo |ˈdʌbəʊ| 提供了六大核心能力:面向接口代理的高性能RPC调用,智能容错和负载均衡,服务自动注册和发现,高度可扩展能力,运行期流量调度,可视化的服务治理与运维。
Dubbo 提供的基础能力包括:
dubbo官网:https://dubbo.apache.org/zh/
Dubbo3 定义了全新的 RPC 通信协议 – Triple,一句话概括 Triple:它是基于 HTTP/2 上构建的 RPC 协议,完全兼容 gRPC,并在此基础上扩展出了更丰富的语义。 使用 Triple 协议,用户将获得以下能力
Dubbo 的 6 大特性:
1、面向接口代理的高性能 RPC 调用;
2、服务自动注册与发现;
3、智能负载均衡策略;
4、高度可扩展能力;
5、运行期流量调度;
6、可视化的服务治理与运维;
Dubbo 架构图:
[外链图片转存中…(img-yxkPvkVg-1647620460314)]
服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
调用关系说明
l 服务容器负责启动,加载,运行服务提供者。
l 服务提供者在启动时,向注册中心注册自己提供的服务。
l 服务消费者在启动时,向注册中心订阅自己所需的服务。
l 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
l 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
l 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
点进dubbo官方文档,推荐我们使用Zookeeper 注册中心下载
什么是zookeeper呢?可以查看zookeeper官方文档:https://zookeeper.apache.org/doc/r3.8.0/index.html