继续按狂神的进度追SpringBoot
Redis部分需要等学习完Redis后补充
1、微服务阶段
1.1、背景
JavaSE:OOP
Mysql:持久化
html+css+js+jquery+框架:视图层
javaweb:独立开发MVC三层架构的网站(原始)
ssm:框架,简化了开发流程,配置较为复杂
新服务架构:服务网格
spring再简化:SpringBoot:-jar:内嵌tomcat,微服务架构,简化原理:自动装配(重要,面试谈资)
服务越来越多,管理:SpringCloud
集成web开发,业务的核心
集成数据库Druid
分布式开开:Dubbo+ZooKeeper
接口文档:Swagger
任务调度
安全:SpringSecurity登录验证、Shiro
微服务
Restful风格
Eureka落地实现
Nginx
Ribbon
Feign
HyStrix:服务容灾机制
Zuul:路由网关
SpringCloud config:与git结合配置
Spring是为了解决企业级应用开发的复杂性而创建的,简化开发
Spring简化Java开发
1.基于POJO的轻量级和最小侵入性编程
2.通过IOC、依赖注入(DI)和面向接口实现松耦合
3.基于切面(AOP)和管理进行声明式编程
4.通过切面和模板template减少样式代码
构建一个功能独立的微服务应用单元,可以使用SpringBoot,可以帮助我们快速构建一个应用;
大型分布式网络服务的调用,由SpringCloud完成,实现分布式
在分布式中间,进行流式数据计算,批处理,有Spring Cloud Data Flow
Spring提供从开始构建应用到大型分布式应用全流程方案
1.2、SpringBoot
为了提高开发效率,嫌弃之前Spring配置过于麻烦,提倡“约定大于配置(核心思想)”,进而衍生出一些一站式解决方案
目的是为了更容易使用Spring,更容易集成各种常用的中间件、开源软件
SpringBoot基于Spring开发,本身并不提供Spring框架的核心特性以及扩展功能,只是用于快速、敏捷的开发费新一代基于Spring框架的应用程序。并不是用来替代Spring的解决方案,而是和Spring框架结合用于提升Spring开发者体验的工具。
集成大量第三方库配置(Redis、MongoDB、RabbitMQ、Jpa、Quartz等等)
它默认配置了很多框架的使用方式,就像Maven整合了所有的jar包,SpringBoot整合了所有的框架
主要优点:
-
为所有Spring开发者更快入门
-
开箱即用,提供各种默认配置来简化项目配置
-
内嵌式容器简化web项目
-
没有冗余代码生成和XML配置的要求
1.3、微服务
高内聚,低耦合
微服务是一种架构风格,要求开发应用时,这个应用必须构建成一系列小服务的组合;可以通过http的方式进行互通。
把每个功能独立开来,把独立出来的功能元素动态组合,需要的功能原色才去拿来组合,需要多一些可以整合多个功能元素。
微服务架构是对功能元素进行赋值,而没有对整个应用进行复制。
好处:
1.节省了调用资源
2.每个功能元素的服务都是可替换的,可独立升级的软件代码
如何构建微服务
一个大型系统的微服务架构,就像一个复杂交织的神经网络,每一个神经元就是一个功能元素,各自完成自己的功能,通过http相互请求调用。
2、第一个SpringBoot程序
2.1、环境搭建
jdk1.8
maven
springboot
IDEA
官方提供一个快速生成的网站,IDEA进行集成
可以在官网quickstart直接下载后,导入IDEA开发

直接使用IDEA创建一个SpringBoot项目(一般开发一般直接在IDEA中创建)

2.2、构建基础架构

2.3、controller测试
package com.zhou.helloworld.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @Name HelloController * @Description 接口:http://localhost:8080/hello * 实现自动装配 * @Author 88534 * @Date 2021/9/28 21:47 */ @RestController public class HelloController { @RequestMapping("/hello") public String hello(){ return "hello,world"; } }
2.4、在主程序入口中启动
package com.zhou.helloworld; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 程序的主入口,SpringBootApplication注解封装了SSM的组件 */ @SpringBootApplication public class HelloworldApplication { public static void main(String[] args) { SpringApplication.run(HelloworldApplication.class, args); } }
得到输出结果:hello,world
3、SpringBoot自动装配原理
3.1、pom.xml
-
spring-boot-dependencies:核心依赖在父工程中
-
引入SpringBoot依赖的时候,不需要指定版本,因为有这些版本仓库
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.5</version> <relativePath/> <!-- lookup parent from repository --> </parent>
点击spring-boot-starter-parent,还有一个父依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.5.5</version> </parent>
这里才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心;
以后导入依赖默认是不需要写版本;但是如果导入的包没有在依赖中管理着就需要手动配置版本了;
3.2、启动器spring-boot-starter
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency>
spring-boot-starter-web自动导入web环境所有的依赖
SpringBoot会将所有的功能场景都变成一个个启动器
需要使用什么功能,就只需要找到对应的启动器starter
springboot-boot-starter-xxx:就是spring-boot的场景启动器
3.3、主程序、主启动类SpringBootApplication
package com.zhou; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @SpringBootApplication 标注这个类是一个SpringBoot应用,启动类下的所有资源被导入 */ @SpringBootApplication public class SpringBoot01HelloworldApplication { /** * 利用反射将SpringBoot应用启动 */ public static void main(String[] args) { SpringApplication.run(SpringBoot01HelloworldApplication.class, args); } }
点击注解@SpringBootApplication打开源码

@SpringBootConfiguration 配置
@Configuration Spring配置类 配置类就是对应Spring的xml 配置文件
@Component 是Spring的一个组件
@EnableAutoConfiguration 自动配置
@AutoConfigurationPackage 自动配置包
@Import({Registrar.class}) Spring底层注解@import , 给容器中导入一个组件
@Import({AutoConfigurationImportSelector.class}) 自动配置导入选择,给容器导入组件
@ComponentScan 对应XML配置中的元素
作用:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中
META-INF/spring.factories:自动配置的核心文件

自动配置有的没有生效,需要导入对应的start才能有作用
核心注解:@ConditionalOnXXX
作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;
测试哪些自动配置生效
通过启用 debug=true属性;来让控制台打印自动配置报告
Positive matches:(自动配置类启用的:正匹配)
Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)
Unconditional classes: (没有条件的类)
3.4、配置类properties
-
一旦这个配置类生效;这个配置类就会给容器中添加各种组件;
-
这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
-
所有在配置文件中能配置的属性都是在xxxxProperties类中封装着;
-
配置文件能配置什么就可以参照某个功能对应的这个属性类
自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中。
3.5、SpringApplication.run
-
推断应用的类型是普通的项目还是Web项目
-
查找并加载所有可用初始化器 ,设置到initializers属性中
-
找出所有的应用程序监听器,设置到listeners属性中
-
推断并设置main方法的定义类,找到运行的主类
run方法流程

3.6、总结
1、SpringBoot启动会加载大量的自动配置类
2、看需要的功能有没有在SpringBoot默认写好的自动配置类当中;
3、看这个自动配置类中到底配置了哪些组件;(只要要用的组件存在在其中,就不需要再手动配置了)
4、给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。只需要在配置文件中指定这些属性的值即可;
xxxxAutoConfigurartion:自动配置类;给容器中添加组件
xxxxProperties:封装配置文件中相关属性;
结论:SpringBoot所有的自动配置都在启动的时候扫描并加载spring.factories所有的自动配置类,但是不一定生效,要判断条件是否成立,只要导入了对应的start,就有对应的启动器了,有了启动器,自动装配就会生效,配置成功
-
springboot在启动的时候,从类路径下/META-INF/spring.factories获取指定的值
-
将这些自动配置的类导入容器,自动配置就会生效,进行自动配置
-
整合JavaEE,解决方案和自动配置的东西都在spring-boot-autoconfigure-2.5.5.RELEASE.jar包下
-
把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器
-
容器中存在非常多的XXXAutoConfiguration,给容器中导入此场景需要的所有组件
-
有了自动配置类,免去手动配置注入功能组件的工作
4、yaml配置注入
SpringBoot使用一个全局的配置文件 , 配置文件名称是固定的
-
application.properties
-
语法结构 :key=value
-
-
application.yml(yaml)
-
语法结构 :key:空格 value(空格不能省略)
-
配置文件的作用 :修改SpringBoot自动配置的默认值
yaml也可以给传统实体类赋值
传统xml配置:
<server>
<port>8081<port>
</server>
yaml配置:注入到配置类中
server
4.1、语法
说明:语法要求严格!key: value
-
空格不能省略
-
以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
-
属性和值的大小写都是十分敏感的。
server:
port: 8081
# 普通的key-value
name: zhou
# 对象
student1:
name: zhou
age: 3
# 行内写法
student2: {name: zhou, age: 3}
# 数组List、Set
pets1:
- cat
- dog
- pig
pets2: [cat,dog,pig]
注意:
-
“ ” 双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思;
比如 :name: "Spring \n Boot" 输出 :Spring 换行 Boot
-
'' 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出
比如 :name: ‘Spring \n Boot ’ 输出 :Spring \n Boot
4.2、给实体类直接注入匹配值
原生bean方式,在属性上加入@Value、@Value("${xxx.xxx}")(配置文件中取值)
现在可以使用yaml文件注入
加入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>
创建实体类
dog
package com.zhou.pojo; import org.springframework.stereotype.Component; /** * @Name Dog * @Description * @Author 88534 * @Date 2021/9/29 12:17 */ @Component public class Dog { private String name; private Integer age; public Dog() { } public Dog(String name, Integer age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "Dog{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
person有一个dog
可以加上@ConfigurationProperties(prefix = "person")进行yaml引入
package com.zhou.pojo; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Date; import java.util.List; import java.util.Map; /** * @Name Person * @Description * @Author 88534 * @Date 2021/9/29 12:24 */ @Component @ConfigurationProperties(prefix = "person") public class Person { private String name; private Integer age; private Boolean happy; private Date birth; private Map<String,Object> maps; private List<Object> lists; private Dog dog; public Person() { } public Person(String name, Integer age, Boolean happy, Date birth, Map<String, Object> maps, List<Object> lists, Dog dog) { this.name = name; this.age = age; this.happy = happy; this.birth = birth; this.maps = maps; this.lists = lists; this.dog = dog; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Boolean getHappy() { return happy; } public void setHappy(Boolean happy) { this.happy = happy; } public Date getBirth() { return birth; } public void setBirth(Date birth) { this.birth = birth; } public Map<String, Object> getMaps() { return maps; } public void setMaps(Map<String, Object> maps) { this.maps = maps; } public List<Object> getLists() { return lists; } public void setLists(List<Object> lists) { this.lists = lists; } public Dog getDog() { return dog; } public void setDog(Dog dog) { this.dog = dog; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + ", happy=" + happy + ", birth=" + birth + ", maps=" + maps + ", lists=" + lists + ", dog=" + dog + '}'; } }
yaml文件填入数值,非常简便
person:
name: zhou
age: 20
happy: true
birth: 2000/1/1
maps: {k1: v1,k2: v2}
lists:
- code
- music
- girl
dog:
name: wang
age: 3
输出结果:
Person{name='zhou', age=20, happy=true, birth=Sat Jan 01 00:00:00 CST 2000, maps={k1=v1, k2=v2}, lists=[code, music, girl], dog=Dog{name='wang', age=3}}
4.3、加载指定的配置文件
@PropertySource :加载指定的配置文件;在resource目录下再创建一个yaml文件写入值
@PropertySource(value = "classpath:xxx.properties")
@ConfigurationProperties:默认从全局配置文件中获取值
可以批量注入配置文件中的属性,支持松散语法(松散绑定),支持复杂类型封装(封装对象),支持JSR303校验
@Value则需要一个个指定,不支持松散语法和复杂类型封装,不支持JSR303校验
yml中写的last-name,这个和lastName是一样的,“ - ”后面跟着的字母默认是大写的。这就是松散绑定。
【注意】properties配置文件在写中文的时候,会有乱码 , 需要去IDEA中设置编码格式为UTF-8;
settings-->FileEncodings 中配置:
配置文件还可以编写占位符生成随机数random,可以引入设置的标签hello
person:
name: ${random.uuid}
age: ${random.int(0,100)}
happy: true
birth: 2000/1/1
maps: {k1: v1,k2: v2}
hello: happy
lists:
- code
- music
- girl
dog:
name: ${person.hello}_旺财
age: ${random.int(0,10)}
输出结果:
Person{name='5d48b0a6-929e-4fa4-86ab-d5cd8ff1f8d3', age=70, happy=true, birth=Sat Jan 01 00:00:00 CST 2000, maps={k1=v1, k2=v2}, lists=[code, music, girl], dog=Dog{name='happy_旺财', age=2}}
4.4、JSR303校验
SpringBoot中可以用@Validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。
加入该注解后
属性前加入@Email,检验是否是合法的电子邮件地址,@Email(message="xxx")加入报错提示
常见参数:
@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=)
//日期检查
@Past //验证 Date 和 Calendar 对象是否在当前时间之前
@Future //验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern //验证 String 对象是否符合正则表达式的规则
除此以外,我们还可以自定义一些数据校验规则
4.5、多环境配置切换
profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境
在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本
使用Properties时
例如:
application-test.properties 代表测试环境配置
application-dev.properties 代表开发环境配置
但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置文件;
需要通过一个配置来选择需要激活的环境:
# 比如在配置文件中指定使用dev环境,可以通过设置不同的端口号进行测试;
# 启动SpringBoot,就可以看到已经切换到dev下的配置了;
spring.profiles.active=dev
使用yml时,可以利用多文档块,一个yml文件实现不同环境配置,不需要创建多个配置文件
server:
port: 8081
# 选择要激活那个环境块
spring:
profiles:
active: prod
---
server:
port: 8083
spring:
profiles: dev # 配置环境的名称
---
server:
port: 8084
spring:
profiles: prod # 配置环境的名称(选择)
注意:如果yml和properties同时都配置了端口,并且没有激活其他环境 , 默认会使用properties配置文件
4.6、配置文件位置
在开发的资源文件中进行配置
springboot 启动会扫描以下位置的application.properties或者application.yml文件作为SpringBoot的默认配置文件:
优先级1:项目路径下的config文件夹配置 file:./config/
文件优先级2:项目路径下配置 file:./
文件优先级3:资源路径下的config文件夹配置 classpath:/config/
文件优先级4:资源路径下配置文件 classpath:/
优先级由高到低,高优先级的配置会覆盖低优先级的配置;
SpringBoot会从这四个位置全部加载主配置文件;互补配置;
运维小技巧:指定位置加载配置文件
通过spring.config.location来改变默认的配置文件位置
项目打包好以后,可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;相同配置,外部指定的配置文件优先级最高
java -jar spring-boot-config.jar --spring.config.location=F:/application.properties
5、SpringBoot Web开发
webapp自动装配
xxxAutoConfiguration:向容器中自动配置组件
xxxxProperties:自动配置类,装配配置文件中自定义的一些内容
要解决的问题:
-
导入静态资源
-
首页
-
jsp、模板引擎Thymeleaf
-
装配扩展SpringMVC
-
增删改查
-
拦截器
-
国际化
6、静态资源
在SpringBoot,可以使用一下方式处理静态资源
-
webjars
<dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.4.1</version> </dependency>
-
在resources文件夹下建立public、static、resource、/**,可以在localhost:8080/目录下访问,优先级为resource>static>public
7、thymeleaf模板引擎
前端html页面
以前的开发,需要转成jsp页面
jsp的好处就是当查出一些数据转发到JSP页面以后,可以轻松实现数据的展示及交互
但如今SpringBoot用的是jar,不是war;使用嵌入式Tomcat,不支持jsp,不能直接用纯静态页面
所以需要模板引擎
模板template
<html>
……
Hello ${user}
……
</html>
数据Data
model.addAttribute("user","zhangsan")
两者通过模板引擎TemplateEngine
实现输出Output
<html>
……
Hello zhangsan
……
</html>
模板引擎的作用就是写一个页面模板,写一写表达式(动态的值)
组装数据,将模板和数据交给模板引擎,按照表达式解析,填充到指定位置,生成需要的内容
7.1、引入Thymeleaf
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
在html中引入
<html lang="en" xmlns:th="http://www.thymeleaf.org">
这样才可以在其他标签里面使用th:这样的语法.这是下面语法的前提.
7.2、基本语法
7.2.1、th:text
测试环境:controller
package com.zhou.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; /** * @Name IndexController * @Description 在templates目录下的所有页面,只能通过controller跳转 * @Author 88534 * @Date 2021/9/30 16:34 */ @Controller public class IndexController { @RequestMapping("/index") public String index(Model model){ model.addAttribute("msg","<h1>index</h1>"); return "index"; } }
html:index
<!DOCTYPE html> <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> </body> </html>
输出结果:
-- text:纯文本
<h1>index</h1>
-- utest: 会编译识别标签
index
7.2.2、th:each
循环、遍历集合,每个数据自动换行
controller
model.addAttribute("users", Arrays.asList("zhou","li"));
html
<h3 th:each="user:${users}" th:text="${user}"></h3>
输出效果:
7.2.3、th:object
自定义变量,减少引用对象的繁琐
常规写法
<p>Name: <span th:text="${user.name}">Jack</span>.</p> <p>Age: <span th:text="${user.age}">21</span>.</p> <p>friend: <span th:text="${user.friend.name}">Rose</span>.</p>
改造写法
<h2 th:object="${user}"> <p>Name: <span th:text="*{name}">Jack</span>.</p> <p>Age: <span th:text="*{age}">21</span>.</p> <p>friend: <span th:text="*{friend.name}">Rose</span>.</p> </h2>
7.2.4、方法
支持方法的直接调用
<h2 th:object="${user}"> <p>FirstName: <span th:text="*{name.split(' ')[0]}">Jack</span>.</p> <p>LastName: <span th:text="*{name.split(' ')[1]}">Li</span>.</p> </h2>
调用了name(是一个字符串)的split方法
7.2.5、动静结合
在标签内加入的内容不会在Thymeleaf环境下识别,但如果加载静态页面会识别
静态页面中,th指令不被识别,但是浏览器也不会报错,把它当做一个普通属性处理。这样标签的默认值请登录就会展现在页面。如果是在Thymeleaf环境下,th指令就会被识别和解析,而th:text的含义就是替换所在标签中的文本内容,于是th:text展示的值就替代了标签中默认的字。
Thymeleaf中提供了一些内置对象,并且在这些对象中提供了一些方法,方便我们来调用。获取这些对象,需要使用#对象名来引用。
环境相关对象 #ctx: 获取Thymeleaf自己的Context对象 #requset: 如果是web程序,可以获取HttpServletRequest对象 #response: 如果是web程序,可以获取HttpServletReponse对象 #session: 如果是web程序,可以获取HttpSession对象 #servletContext: 如果是web程序,可以获取HttpServletContext对象
Thymeleaf提供的全局对象: #dates: 处理java.util.date的工具对象
如:<td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td>
#calendars: 处理java.util.calendar的工具对象 #numbers: 用来对数字格式化的方法 #strings: 用来处理字符串的方法 #bools: 用来判断布尔值的方法 #arrays: 用来护理数组的方法 #lists: 用来处理List集合的方法 #sets: 用来处理set集合的方法 #maps: 用来处理map集合的方法
举例 在环境变量中添加日期类型对象
@GetMapping("show3") public String show3(Model model){ model.addAttribute("today", new Date()); return "show3"; }
在页面中处理
<p> 今天是: <span th:text="${#dates.format(today,'yyyy-MM-dd')}">2021/10/5</span> </p>
输出:
今天是:2021-10-5
7.2.6、拼接
<span th:text="'欢迎您:' + ${user.name} + '!'"></span> ----------------------- <span th:text="|欢迎您:${user.name}!|"></span>
7.2.7、运算
比较运算
支持的比较运算:>, <, >= and <= ,但是>, <不能直接使用,因为xml会解析为标签,要使用别名。
注意 == and !=不仅可以比较数值,类似于equals的功能。
可以使用的别名:gt (>), lt (<), ge (>=), le (<=), not (!), Also eq (==), neq/ne (!=).
条件运算
二元(if)?(then)
三元运算符的三个部分:(conditon) ? (then) : (else)
condition:条件
then:条件成立的结果
else:不成立的结果
其中的每一个部分都可以是Thymeleaf中的任意表达式。
<span th:text="${user.sex} ? '男':'女'"></span>
默认:(value)?:(defaultValue)
7.2.8、逻辑判断th:if、th:unless
<span th:if="${user.age} < 24">小鲜肉</span>
如果表达式的值为true,则标签会渲染到页面,否则不进行渲染。
以下情况被认定为true:
表达式值为true 表达式值为非0数值 表达式值为非0字符 表达式值为字符串,但不是"false","no","off" 表达式不是布尔、字符串、数字、字符中的任何一种
其它情况包括null都被认定为false
7.2.9、分支控制th:switch
<div th:switch="${user.role}"> <p th:case="'admin'">用户是管理员</p> <p th:case="'manager'">用户是经理</p> <p th:case="*">用户是别的玩意</p> </div>
与Java的switch类似,一旦有一个th:case成立,其它的则不再判断
另外th:case="*"表示默认,放最后。类似default
7.3、th标签总结
th标签在原来的html标签基础上改造,只需前边加th:即可
常用th标签表
| 关键字 | 功能介绍 | 案例 |
|---|---|---|
| th:id | 替换id | <input th:id="'xxx' + ${collect.id}"/> |
| th:text | 文本替换 | <p th:text="${collect.description}">description</p> |
| th:utext | 支持html的文本替换 | <p th:utext="${htmlcontent}">content</p> |
| th:object | 替换对象 | <div th:object="${session.user}"> |
| th:value | 属性赋值 | <input th:value="${user.name}" /> |
| th:with | 变量赋值运算 | <div th:with="isEven=${prodStat.count}%2==0"></div> |
| th:style | 设置样式 | th:style="'display:' + @{(${sitrue} ? 'none' : 'inline-block')} + ''" |
| th:onclick | 点击事件 | <th:onclick="'getCollect()'" |
| th:each | 属性赋值 | **<tr th:each="user,userStat:${users}">** |
| th:if | 判断条件 | **<a th:if="${userId == collect.userId}" >** |
| th:unless | 和th:if判断相反 | <a th:href="@{/login}" th:unless=${session.user != null}>Login</a> |
| th:href | 链接地址 | **<a th:href="@{/login}" th:unless=${session.user != null}>Login</a>** |
| th:switch | 多路选择 配合th:case 使用 | <div th:switch="${user.role}"> |
| th:case | th:switch的一个分支 | <p th:case="'admin'">User is an administrator</p> |
| th:fragment | 布局标签,定义一个代码片段,方便其它地方引用 | <div th:fragment="alert"> |
| th:insert | 插入fragment,实现代码复用 | <div th:insert="~{xxx::alert}"></div> |
| th:include | 布局标签,替换内容到引入的文件 | <head th:include="layout :: htmlhead" th:with="title='xx'"></head> |
| th:replace | 布局标签,替换整个标签到引入的文件 | <div th:replace="fragments/header :: title"></div> |
| th:selected | selected选择框 选中 | th:selected="(${xxx.id} == ${configObj.dd})" |
| th:src | 图片类地址引入 | <img class="img-responsive" alt="App Logo" th:src="@{/img/logo.png}" /> |
| th:inline | 定义js脚本可以使用变量 | <script type="text/javascript" th:inline="javascript"> |
| th:action | 表单提交的地址 | <form action="subscribe.html" th:action="@{/subscribe}"> |
| th:remove | 删除某个属性 | <tr th:remove="all"> 1.all:删除包含标签和所有的孩子。 2.body:不包含标记删除,但删除其所有的孩子。 3.tag:包含标记的删除,但不删除它的孩子。 4.all-but-first:删除所有包含标签的孩子,除了第一个。 5.none:什么也不做。这个值是有用的动态评估。 |
| th:attr | 设置标签属性,多个属性可以用逗号分隔 | 比如 th:attr="src=@{/image/aa.jpg},title=#{logo}",此标签不太优雅,一般用的比较少。 |
7.4、@{}、${}、*{}、#{}的含义
1、*{}的使用:选择表达式很像变量表达式,不过它用一个预先选择的对象来代替上下文变量容器来执行以动态变换容器内容
先在表单标签获取后台传来的${xxx}对象,然后使用th:value的*{attr}与th:object=${}配合用于表单字段绑定、 属性绑定、集合绑定等。
2、@{}的使用:用于获取链接、路径(默认于/resource/static目录下)地址。
常用于th:href="@{xxx}",th:src="@{xxx}"
3、${}的使用:和常规的使用差不多,用于字符串的替换或对象获取或属性绑定等,具体作用根据标签而定
4、@{}和${}的结合使用:在a标签的href中直接直接写对应值会导致解析失败,使用${}框选对应值
5、#{}实现国际化,可以进行不同语言的转换翻译
7.5、thymeleaf中[[${}]]与[(${})]的区别
[[…]]会被转义,[(…)]不会。
假设在后台传入msg的值为
<b>AAA</b>
在前台这样使用
[[${msg}]]___[(${msg})]
则最终的效果会是这样
<b>AAA</b>___AAA
8、自定义配置
增加Configuration注解实现自定义配置
实现WebMvcConfigurer可以添加webMVC配置
可以通过自定义配置实现自动装配
用视图解析器举例:
package com.zhou.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Locale; /** * @Name MyMvcConfig * @Description 添加自定义配置 * @Author 88534 * @Date 2021/10/5 21:21 */ @Configuration public class MyMvcConfig implements WebMvcConfigurer { //implements ViewResolver 视图解析器 /** * diy 定制化功能,只需要写这个组件,通过bean交给SpringBoot管理,就能实现自动装配 * 扩展 SpringMVC DispatcherServlet * @return */ @Bean public ViewResolver myViewResolver(){ return new MyViewResolver(); } /** * 自定义自己的视图解析器 */ public static class MyViewResolver implements ViewResolver { @Override public View resolveViewName(String viewName, Locale locale) throws Exception { return null; } } }
SpringBoot在自动配置组件时,优先看容器中有没有用户自己配置的(在yaml文件内),如果用户自己配置需要@Bean,没有配置就用自动配置的内容。
9、后端如何使用前端
-
模板:别人写好的,改成自己需要的:模板之家……http://www.cssmoban.com/
-
框架:组件:自己手动组合、拼接(栅格系统,分栏)
-
BootStrap 12栏
-
LayUI 12栏
-
Semantic-UI 16栏
-
ElementUI 24栏
-
后端需要了解的
页面长什么样子,数据的位置
设计数据库(难点)
-
前端能够自动运行,独立化工程
-
数据接口如何对接:json,对象始终只有一个
-
前后端联调测试
后端需要准备的
-
有一套自己熟悉的后台模板,工作必要
-
X-admin http://x.xuebingsi.com/
-
-
前端界面:至少自己能够通过前端框架,组合出来一个网站页面
-
首页index
-
关于、介绍about
-
博客blog
-
提交post
-
用户user
-
-
让这个网站能够独立运行
Spring Data
10、整合JDBC
对于数据访问层,无论是 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
新建项目,引入Web、JDBC、MySQL Driver
pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <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.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.zhou</groupId> <artifactId>SpringBoot-04-Data</artifactId> <version>0.0.1-SNAPSHOT</version> <name>SpringBoot-04-Data</name> <description>整合JDBC</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.yml引入数据库配置
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
JDBCTemplate
1、有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库;
2、即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。
3、数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。
4、Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用
5、JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类
JdbcTemplate主要提供以下几类方法:
-
execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
-
update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
-
query方法及queryForXXX方法:用于执行查询相关语句;
-
call方法:用于执行存储过程、函数相关语句。
Controller层
由于业务比较简单,省去了必要的service和dao层,直接与数据库进行交互,注入sql语句
package com.zhou.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; /** * @Name JDBCController * @Description * @Author 88534 * @Date 2021/10/11 11:05 */ @RestController public class JDBCController { @Autowired JdbcTemplate jdbcTemplate; @GetMapping("/userList") public List<Map<String,Object>> userList(){ String sql = "select * from mybatis.user"; List<Map<String, Object>> list_maps = jdbcTemplate.queryForList(sql); return list_maps; } @GetMapping("/addUser") public String addUser(){ String sql = "insert into mybatis.user(id,name,pwd) values(5,'zhao','zhao123')"; jdbcTemplate.update(sql); return "Add Ok!"; } @GetMapping("/updateUser/{id}") public String updateUser(@PathVariable("id")Integer id){ String sql = "update mybatis.user set name = ?, pwd = ? where id = " + id; // 封装 Object[] objects = new Object[2]; objects[0] = "zhou2"; objects[1] = "zzzzz"; jdbcTemplate.update(sql,objects); return "Update Ok!"; } @GetMapping("/deleteUser/{id}") public String deleleUser(@PathVariable("id")Integer id){ String sql = "delete from mybatis.user where id = ?"; jdbcTemplate.update(sql,id); return "Delete Ok!"; } }
通过输入对应的网址可在数据库内进行检索得到
此外还可以对连接池进行测试
test目录下
package com.zhou; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; @SpringBootTest class SpringBoot04DataApplicationTests { @Autowired DataSource dataSource; @Test void contextLoads() throws SQLException { // 查看一下默认的数据源:class com.zaxxer.hikari.HikariDataSource System.out.println(dataSource.getClass()); // 获得数据库连接:HikariProxyConnection@1181947538 wrapping com.mysql.cj.jdbc.ConnectionImpl@782bf610 Connection connection = dataSource.getConnection(); System.out.println(connection); // 关闭连接 connection.close(); } }
默认使用数据源是Hikari
HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀
11、整合Druid数据源
Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。
Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。
Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源。
Github地址:https://github.com/alibaba/druid/
加入依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency>
配置文件中加入Druid数据源
可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数等设置项
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
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
为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了;需要自行添加 DruidDataSource 组件到容器中,并绑定属性;
Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的 web 页面。
所以需要设置 Druid 的后台管理页面,比如 登录账号、密码等;配置后台管理;
新建config/DruidConfig,使用@Bean绑定
package com.zhou.config; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * @Name DruidConfig * @Description * @Author 88534 * @Date 2021/10/11 13:39 */ @Configuration public class DruidConfig { /** * 引入数据源实体 * @return */ @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ return new DruidDataSource(); } /** * 后台监控,相当于web.xml注册,ServletRegistrationBean * 由于SpringBoot内置了Servlet容器,所以没有web.xml,替代方法ServletRegistrationBean * @return */ @Bean public ServletRegistrationBean statViewServlet(){ ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); // 后台需要有人登录,账号密码配置 HashMap<String,String> initParameters = new HashMap<>(); // 增加配置 // 登录key,固定的loginUsername loginPassword initParameters.put("loginUsername","admin"); initParameters.put("loginPassword","123456"); // 后台允许谁可以访问 // initParams.put("allow", "localhost"):表示只有本机可以访问 // initParams.put("allow", ""):为空或者为null时,表示允许所有访问 initParams.put("allow", ""); // deny:Druid 后台拒绝谁访问 // initParams.put("kuangshen", "192.168.1.20");表示禁止此ip访问 // 设置初始化参数 bean.setInitParameters(initParameters); return bean; } /** * filter * @return */ @Bean public FilterRegistrationBean webStatFilter(){ FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new WebStatFilter()); // 过滤请求 Map<String,String> initParameters = new HashMap<>(); // exclusions:设置哪些请求进行过滤排除掉,从而不进行统计 initParameters.put("exclusions","*.js,*.css,/druid/*"); // "/*" 表示过滤所有请求 // bean.setUrlPatterns(Arrays.asList("/*")); bean.setInitParameters(initParameters); return bean; } }
配置完毕后,我们可以选择访问 :http://localhost:8080/druid/
自动跳转到http://localhost:8080/druid/login.html

输入之前设置的账号名密码,进入监控页面

执行之前设置的http://localhost:8080/userList
可以在后台检测到此结果

除此之外,还会有一些防火墙等监控功能
12、整合MyBatis框架
官方文档:http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
新建一个项目,选择对应的依赖

流程:
1.导入包pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.zhou</groupId> <artifactId>Spring-05-MyBatis</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Spring-05-MyBatis</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
2.配置文件application.properties
需要确保mapper所在路径再resources/mybatis/mapper内
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 整合mybatis
mybatis.type-aliases-package=com.zhou.pojo
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
3.实体类User,导入Lombok,需要有参无参构造注解
package com.zhou.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @Name User * @Description User实体类 * @Author 88534 * @Date 2021/10/11 15:18 */ @Data @AllArgsConstructor @NoArgsConstructor public class User { private int id; private String name; private String pwd; }
4.Mapper层UserMapper.xml,编写sql
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!--namespace绑定一个对应的Dao/Mapper接口--> <mapper namespace="com.zhou.mapper.UserMapper"> <!-- 查询语句,id为方法名,resultType为返回类型,需要精确目录--> <select id="queryUserList" resultType="com.zhou.pojo.User"> select * from mybatis.user </select> <insert id="addUser" parameterType="com.zhou.pojo.User"> insert into mybatis.user (id,name,pwd) values (#{id},#{name},#{pwd}); </insert> <update id="updateUser" parameterType="com.zhou.pojo.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> <select id="queryUserById" resultType="com.zhou.pojo.User" parameterType="int"> select * from mybatis.user where id = #{id}; </select> </mapper>
5.service层调用dao层
用UserMapper接口代替
package com.zhou.mapper; import com.zhou.pojo.User; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; import java.util.List; /** * @Name UserMapper * @Description @Mapper表示这是一个mybatis的mapper类 * @Author 88534 * @Date 2021/10/11 15:25 */ @Mapper @Repository public interface UserMapper { List<User> queryUserList(); User queryUserById(int id); int addUser(User user); int updateUser(User user); int deleteUser(int id); }
6.controller层调用service层
package com.zhou.controller; import com.zhou.mapper.UserMapper; import com.zhou.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; /** * @Name UserController * @Description * @Author 88534 * @Date 2021/10/11 23:06 */ @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; } @GetMapping("/addUser") public String addUser(){ userMapper.addUser(new User(5,"wu","456789")); return "Add Ok!"; } @GetMapping("/updateUser") public String updateUser(){ userMapper.updateUser(new User(5,"wu","567890")); return "Update Ok!"; } @GetMapping("/deleteUser") public String deleteUser(){ userMapper.deleteUser(5); return "Delete Ok!"; } }
13、SpringSecurity
在web开发中,安全第一位。过滤器、拦截器
功能性需求:否
做网站,安全应该在什么时候考虑?设计之初
-
漏洞、隐私泄露
-
架构一旦确定后
在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。
常用框架
-
shiro
-
SpringSecurity
很像,除了类不一样,名字不一样
主要做认证authentication、授权(vip1、vip2、vip3)
-
功能权限
-
访问权限
-
菜单权限
-
……拦截器、过滤器:大量的原生代码,产生冗余------简化
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。
Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求。
一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
13.1、认识SpringSecurity
Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
-
WebSecurityConfigurerAdapter:自定义Security策略
-
AuthenticationManagerBuilder:自定义认证策略
-
@EnableWebSecurity:开启WebSecurity模式(@EnableXXX 开启某个功能)
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
“授权” (Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
这个概念是通用的,而不是只在Spring Security 中存在。
13.2、环境搭建
新建一个项目,导入相关依赖
<?xml version="1.0" encoding="UTF-8"?> <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.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.zhou</groupId> <artifactId>SpringBoot-06-Security</artifactId> <version>0.0.1-SNAPSHOT</version> <name>SpringBoot-06-Security</name> <description>安全</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring5</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-java8time</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
跳转层controller
前往登录页面,各子网页目录
package com.zhou.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; /** * @Name RouterController * @Description * @Author 88534 * @Date 2021/10/12 10:46 */ @Controller public class RouterController { @RequestMapping({"/", "/index"}) public String index(){ return "index"; } @RequestMapping("/toLogin") public String login(){ return "views/login"; } @RequestMapping("/level{id_1}/{id_2}") public String level(@PathVariable("id_1")int id_1,@PathVariable("id_2")int id_2){ return "views/level"+id_1+"/"+id_2; } }
application.properties
spring.thymeleaf.cache=false
前端页面素材:
https://gitee.com/ENNRIAAA/spring-security-material/repository/archive/master.zip
13.3、安全登录注销
未登录无法进入子页面,登录后进入主页,右上角显示用户名和权限
需要对账号进行不同权限管理(vip1~3),设置账号密码(可以在数据库内进行),密码进行加密
package com.zhou.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; /** * @Name SecurityConfig * @Description AOP添加拦截器 链式编程 * @Author 88534 * @Date 2021/10/12 11:04 */ @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 授权 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 首页所有人可以访问,功能页只有对应有权限的人才能访问 // 请求授权的规则 http.authorizeRequests() .antMatchers("/").permitAll() .antMatchers("/level1/**").hasRole("vip1") .antMatchers("/level2/**").hasRole("vip2") .antMatchers("/level3/**").hasRole("vip3"); // 没有权限默认会到登录页面 // http.formLogin().loginPage("/toLogin");定制登录页面 // loginPage指定登录界面toLogin,loginProcessingUrl指定处理登录请求表单提交@{/login},全部保证安全都是用POST请求 // usernameParameter和passwordParameter指定的参数与前端表单提交的name一致,默认username和password时可不写 http.formLogin().loginPage("/toLogin").loginProcessingUrl("/login").usernameParameter("user").passwordParameter("pwd"); // 防止网络工具:get,关闭csrf功能,防止跨域伪造攻击(CSR) http.csrf().disable(); // 注销,开启注销功能,success注销完成后跳转的界面 http.logout().logoutSuccessUrl("/toLogin"); // 开启“记住我“功能 使用cookie,默认保存2周,关闭浏览器后仍然能重新进入登录后界面 // rememberMeParameter自定义接收前端的参数,默认是remember-me http.rememberMe().rememberMeParameter("remember"); } /** * 认证 SpringBoot 2.1.x可以直接使用 * 密码编码PasswordEncoder,密码需要加密 * 在Spring Security 5.0+ 新增了很多加密方法 * spring security 官方推荐的是使用bcrypt加密方式。 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 这些数据正常来说,应该从数据库中取出,使用加密 auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) .withUser("zhou").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"); } }
登录不同账号只能浏览不同vip范围的子网页
登录界面:login.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>登录</title> <!--semantic-ui--> <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet"> </head> <body> <!--主容器--> <div class="ui container"> <div class="ui segment"> <div style="text-align: center"> <h1 class="header">登录</h1> </div> <div class="ui placeholder segment"> <div class="ui column very relaxed stackable grid"> <div class="column"> <div class="ui form"> <form th:action="@{/login}" method="post"> <div class="field"> <label>Username</label> <div class="ui left icon input"> <input type="text" placeholder="Username" name="user"> <i class="user icon"></i> </div> </div> <div class="field"> <label>Password</label> <div class="ui left icon input"> <input type="password" name="pwd"> <i class="lock icon"></i> </div> </div> <div class="field"> <input type="checkbox" class="selected" name="remember"> 记住我 </div> <input type="submit" class="ui blue submit button"/> </form> </div> </div> </div> </div> <div style="text-align: center"> <div class="ui label"> </i>注册 </div> <br><br> </div> <div class="ui segment" style="text-align: center"> <h3>Spring Security Study by zhou</h3> </div> </div> </div> <script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script> <script th:src="@{/qinjiang/js/semantic.min.js}"></script> </body> </html>
登录成功界面index.html
sec:authorize="isAuthenticated()":是否认证登录!来显示不同的页面
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>首页</title> <!--semantic-ui--> <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet"> <link th:href="@{/qinjiang/css/qinstyle.css}" rel="stylesheet"> </head> <body> <!--主容器--> <div class="ui container"> <div class="ui segment" id="index-header-nav" th:fragment="nav-menu"> <div class="ui secondary menu"> <a class="item" th:href="@{/index}">首页</a> <!--登录注销--> <div class="right menu"> <!-- 如果未登录,显示登录按钮--> <div sec:authorize="!isAuthenticated()"> <a class="item" th:href="@{/toLogin}"> <i class="address card icon"></i> 登录 </a> </div> <!-- 如果已登录,显示用户名和注销按钮--> <div sec:authorize="isAuthenticated()"> <a class="item"> 用户名:<span sec:authentication="principal.username"></span> 角色:<span sec:authentication="principal.authorities"></span> </a> </div> <div sec:authorize="isAuthenticated()"> <a class="item" th:href="@{/logout}"> <i class="sign-out icon"></i> 注销 </a> </div> </div> </div> </div> <div class="ui segment" style="text-align: center"> <h3>Spring Security Study by zhou</h3> </div> <div> <br> <div class="ui three column stackable grid"> <div class="column" sec:authorize="hasRole('vip1')"> <div class="ui raised segment"> <div class="ui"> <div class="content" sec:authorize="hasRole('vip1')"> <h5 class="content">Level 1</h5> <hr> <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div> <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div> <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div> </div> </div> </div> </div> <div class="column" sec:authorize="hasRole('vip2')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 2</h5> <hr> <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div> <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div> <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div> </div> </div> </div> </div> <div class="column" sec:authorize="hasRole('vip3')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 3</h5> <hr> <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div> <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div> <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div> </div> </div> </div> </div> </div> </div> </div> <script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script> <script th:src="@{/qinjiang/js/semantic.min.js}"></script> </body> </html>
勾选”记住我“后登录,可找到对应cookie,浏览器退出后重新打开能直接进入首页,无需登录,只有注销删除cookie后才需要重新登录
登录后只显示对应权限的链接,实现了权限控制
展示界面:
14、Shiro
Apache Shiro是一个Java的安全(权限)框架
可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境
可以完成认证、授权、加密、会话管理、Web继承、缓存等
14.1、功能
-
Authentication:身份认证、登录,验证用户是不是拥有相应身份
-
Authorization:授权,即权限认证,验证某个已认证的用户是否拥有某个权限,即判断某个用户能否进行什么操作。如验证某个用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限
-
Session Manager:会话管理,即用户登录后就是第一次会话,在没有退出之前,它的所有信息都会在会话中,会话可以是普通的JavaSE环境,也可以是Web环境。
-
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储
-
Web Support:Web支持,可以非常容易的集成到Web环境
-
Caching:缓存,比如用户登录后,其用户信息,拥有的角色,权限不必每次去查,这样可以提高效率
-
Concurrency:支持多线程应用的并发验证,即,如在一个线程中开启另一个线程,能把权限自动的传播过去
-
Testing:提供测试支持
-
Run As:允许一个用户假装为另一个用户(如果允许)的身份进行访问
-
Remember Me:记住我,即一次登录后,下次再来就不需要重新登录了
14.2、Shiro架构(外部)
Application Code ---> Subject(the current 'user') ---> Shiro Security Manager(manages all subject) ---> Realm(access your security data)
-
Subject:应用代码直接交互的对象是subject,是Shiro的对外API核心,代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等。与Subject的所有交互都会委托给SecurityManager(实际的执行者)
-
SecurityManager:安全管理器,即所有与安全有关的操作都会与SecurityManager交互,并且管理着所有的Subject。它是Shiro的核心,负责与Shiro的其他组件进行交互,相当于SpringMVC的DispatcherServlet的角色
-
Realm:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较,来确定用户的身份是否合法,也需要从Realm得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把Realm看成DataSource
14.3、Shiro架构(内部)
将上述模块继续细分:
Subject:任何可以与应用交互的“用户”
Security Manager(核心):所有具体的交互都通过其进行控制,它管理者所有的Subject,且负责认证、授权、会话及缓存的管理
Authenticator:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作,即控制着用户能访问应用中哪些功能
Realm:可以有一个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的,可以用JDBC实现,也可以是内存实现等等,由用户提供,所以一般在应用中都需要实现自己的Realm
SessionManager:管理Session生命周期的组件,而Shiro并不仅仅可以用在Web环境,也可以用在普通的JavaSE环境中
CacheManager:缓存控制器,来管理如用户、角色、权限等缓存的,因为这些数据基本上很少改变,放到缓存中可以提高访问的性能
Cryptography:密码模块,Shiro提高了一些常见的加密组件用于密码加密、解密等
14.4、HelloWorld
案例参考quickstart
在GitHub下载:https://github.com/apache/shiro
1.导入依赖
<?xml version="1.0" encoding="UTF-8"?> <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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>SpringBoot-08-Shiro</artifactId> <groupId>com.zhou</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>HelloShiro</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.8.0</version> </dependency> <!-- configure logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.5.6</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.21</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency> </dependencies> </project>
2.配置文件
resources/log4j.properties
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n
# General Apache libraries
log4j.logger.org.apache=WARN
# Spring
log4j.logger.org.springframework=WARN
# Default Shiro logging
log4j.logger.org.apache.shiro=INFO
# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
resources/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
3.HelloWorld
java/Quickstart.java
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) { Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); // 获取当前的用户对象subject Subject currentUser = SecurityUtils.getSubject(); // 通过当前用户拿到Session Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("Subject=>Session [" + value + "]"); } // 判断当前用户是否被认证 if (!currentUser.isAuthenticated()) { // Token:令牌 UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");//longstarr222 // 设置“记住我” token.setRememberMe(true); try { // 执行登录操作 currentUser.login(token); } catch (UnknownAccountException uae) { log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } // ... catch more exceptions here (maybe custom ones specific to your application? catch (AuthenticationException ae) { //unexpected condition? error? } } log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); // 角色管理 if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } // 权限管理 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."); } 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!"); } // 注销并退出 currentUser.logout(); System.exit(0); } }
登录成功:
2021-10-18 14:44:21,788 INFO [org.apache.shiro.session.mgt.AbstractValidatingSessionManager] - Enabling session validation scheduler...
2021-10-18 14:44:22,109 INFO [Quickstart] - Subject=>Session [aValue]
2021-10-18 14:44:22,109 INFO [Quickstart] - User [lonestarr] logged in successfully.
2021-10-18 14:44:22,110 INFO [Quickstart] - May the Schwartz be with you!
2021-10-18 14:44:22,110 INFO [Quickstart] - You may use a lightsaber ring. Use it wisely.
2021-10-18 14:44:22,110 INFO [Quickstart] - You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. Here are the keys - have fun!
登录失败:
2021-10-18 14:20:43,231 INFO [org.apache.shiro.session.mgt.AbstractValidatingSessionManager] - Enabling session validation scheduler...
2021-10-18 14:20:43,570 INFO [Quickstart] - Subject=>Session [aValue]
2021-10-18 14:20:43,570 INFO [Quickstart] - There is no user with username of lonestarr222
2021-10-18 14:20:43,570 INFO [Quickstart] - User [null] logged in successfully.
2021-10-18 14:20:43,570 INFO [Quickstart] - Hello, mere mortal.
2021-10-18 14:20:43,570 INFO [Quickstart] - Sorry, lightsaber rings are for schwartz masters only.
2021-10-18 14:20:43,570 INFO [Quickstart] - Sorry, you aren't allowed to drive the 'eagle5' winnebago!
翻译:
账号:孤独的斯塔尔(Lone Starr)进行登录操作!
如果拥有角色:Schwarz(原力)
登录成功提示:愿原力与你同在
登录失败提示:你好,凡人(无原力者)
如果有此权限,提示:你可以使用光剑,用它秀吧!
如果没有此权限:抱歉,光剑是绝地武士专属武器
(winnebago:Lone Starr的飞船名字,eagle5:船长Lone Starr的驾驶员代号)
精炼:
Subject currentUser = SecurityUtils.getSubject(); Session session = currentUser.getSession(); currentUser.isAuthenticated(); currentUser.getPrincipal(); currentUser.hasRole("schwartz"); currentUser.isPermitted("lightsaber:wield"); currentUser.logout();
14.5、SpringBoot整合Shiro环境搭建
14.5.1、环境搭建
新建一个SpringBoot项目,勾选Web和Thymeleaf依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.zhou</groupId> <artifactId>Shiro-SpringBoot</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Shiro-SpringBoot</name> <description>Shiro整合SpringBoot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.7.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring5</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-java8time</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.16</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
resources目录下创建几个展示网页
index.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <h1>首页</h1> <p th:text="${msg}"></p> <hr> <a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a> </body> </html>
user/add.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>增加一个用户</title> </head> <body> <h1>add</h1> </body> </html>
user/update.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>更新一个用户</title> </head> <body> <h1>update</h1> </body> </html>
controller层
package com.zhou.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; /** * @Name MyController * @Description * @Author 88534 * @Date 2021/10/18 15:05 */ @Controller public class MyController { @RequestMapping({"/","/index"}) public String toIndex(Model model){ model.addAttribute("msg","Hello,Shiro"); return "index"; } @RequestMapping("/user/add") public String add(){ return "user/add"; } @RequestMapping("/user/update") public String update(){ return "user/update"; } }
Config层设置Realm、SecurityManager
UserRealm
package com.zhou.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; /** * @Name UserRealm * @Description 自定义的UserRealm * @Author 88534 * @Date 2021/10/18 17:04 */ public class UserRealm extends AuthorizingRealm { /** * 授权 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了=>授权doGetAuthorizationInfo"); return null; } /** * 认证 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行了=>认证doGetAuthenticationInfo"); return null; } }
ShiroConfig
package com.zhou.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; /** * @Name ShiroConfig * @Description 自下而上配置 * @Author 88534 * @Date 2021/10/18 17:03 */ @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 设置安全管理器 bean.setSecurityManager(defaultWebSecurityManager); return bean; } @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 关联UserRealm securityManager.setRealm(userRealm); return securityManager; } /** * 创建Realm对象,需要自定义类 * @return */ @Bean public UserRealm userRealm(){ return new UserRealm(); } }
测试效果:
点击add和update可以切换两个对应页面
14.5.2、实现登录拦截
ShiroConfig中改写getShiroFilterFactoryBean
@Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 设置安全管理器 bean.setSecurityManager(defaultWebSecurityManager); // 添加shiro的内置过滤器 /* anon:无需认证就可以访问 authc:必须认证了才能访问 user:必须拥有“记住我”功能才能用 perms:拥有对某个资源的权限才能访问 role:拥有某个角色权限才能访问 */ Map<String,String> filterMap = new HashMap<>(); //filterMap.put("/user/add","authc"); //filterMap.put("/user/update","authc"); // 支持通配符 filterMap.put("/user/*","authc"); bean.setFilterChainDefinitionMap(filterMap); // 设置登录的请求 bean.setLoginUrl("/toLogin"); return bean; }
增加登录后跳转页面login.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登录</title> </head> <body> <h1>登录</h1> <hr> <form action=""> <p>用户名:<input type="text" name="username"></p> <p>密码:<input type="password" name="password"></p> <p><input type="submit"></p> </form> </body> </html>
controller增加到登录界面的跳转
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
进行测试:
点击add、update后,直接跳转到登录界面
14.5.3、实现用户认证
Realm增加认证部分
/** * 认证 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行了=>认证doGetAuthenticationInfo"); // 获取当前的用户名、密码,一般从数据库中取 String username = "root"; String password = "123456"; UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken; if (!usernamePasswordToken.getUsername().equals(username)){ // 抛出异常:UnknownAccountException return null; } // 密码认证由shiro自主完成 return new SimpleAuthenticationInfo("",password,""); }
controller增加点击提交后的action跳转
form表单action增加跳转<form th:action="@{/login}">
@RequestMapping("/login")
public String login(String username,String password, Model model){
// 获取当前的用户
Subject subject = SecurityUtils.getSubject();
// 封装用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
// 执行登录方法,如果没有异常则OK,进入首页
try {
subject.login(token);
return "index";
} catch (UnknownAccountException e) {
// 用户名不存在
model.addAttribute("msg","用户名错误");
return "login";
} catch (IncorrectCredentialsException e) {
// 密码不存在
model.addAttribute("msg","密码错误");
return "login";
}
}
进行测试:
输入账户或密码错误均输出一行红色字样“用户名错误”或“密码错误”
14.5.4、整合Mybatis
加入依赖,pom.xml
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.16</version> </dependency>
增加application.yml,使用11章中的Druid的配置
application.properties参照12章的mybatis配置
mybatis.type-aliases-package=com.zhou.pojo
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
增加MySQL数据源,数据库调取mybatis
其余MySQL内容参考12章
只在mapper上增加一个方法User queryUserByName(String name);
对应的xml映射
<select id="queryUserByName" parameterType="String" resultType="com.zhou.pojo.User"> select * from mybatis.user where name = #{name}; </select>
对应的service层接口
public interface UserService { User queryUserByName(String name); }
实现类
@Service public class UserServiceImpl implements UserService{ @Autowired UserMapper userMapper; @Override public User queryUserByName(String name) { return userMapper.queryUserByName(name); } }
此时可以将Realm的认证部分进行改造了,连接数据库
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行了=>认证doGetAuthenticationInfo"); UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken; // 连接真实数据库 User user = userService.queryUserByName(usernamePasswordToken.getUsername()); if (user == null){ // 没有这个人,抛出异常:UnknownAccountException return null; } // 密码认证由shiro自主完成 // 可以加密:MD5、MD5盐值加密…… return new SimpleAuthenticationInfo("",user.getPwd(),""); }
可以通过数据库里的信息进行验证。
14.5.5、实现请求授权
ShiroConfig增加perms规则和不符合规则跳转的页面
@Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 设置安全管理器 bean.setSecurityManager(defaultWebSecurityManager); // 添加shiro的内置过滤器 /* anon:无需认证就可以访问 authc:必须认证了才能访问 user:必须拥有“记住我”功能才能用 perms:拥有对某个资源的权限才能访问 role:拥有某个角色权限才能访问 */ Map<String,String> filterMap = new HashMap<>(); // 拦截,正常情况下,没有授权会跳转到未授权页面 filterMap.put("/user/add","perms[user:add]"); filterMap.put("/user/update","perms[user:update]"); // 支持通配符* filterMap.put("/user/*","authc"); bean.setFilterChainDefinitionMap(filterMap); // 设置登录的请求 bean.setLoginUrl("/toLogin"); // 未授权页面 bean.setUnauthorizedUrl("/noauth"); return bean; }
controller层增加未授权页面的跳转
@RequestMapping("/noauth")
@ResponseBody
public String unauthorized(){
return "未经授权无法访问此页面";
}
数据库加入perms字段varchar100,添加权限

User的pojo类增加perms字段
@Data @AllArgsConstructor @NoArgsConstructor public class User { private int id; private String name; private String pwd; private String perms; }
UserRealm加入授权功能,从数据库中拿到权限信息getPerms()
@Autowired UserService userService; /** * 授权 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了=>授权doGetAuthorizationInfo"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 拿到当前登录的这个Subject Subject subject = SecurityUtils.getSubject(); // 拿到User User currentUser = (User) subject.getPrincipal(); // 设置用户权限 info.addStringPermission(currentUser.getPerms()); return info; }
测试:
登录不同的对应账号能进入不同的页面。
没有权限的返回未授权界面
14.5.6、整合Thymeleaf
增加一个依赖
<dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
ShiroConfig增加ShiroDialect
/** * 整合ShiroDialect,用来整合shiro-Thymeleaf * @return */ @Bean public ShiroDialect getShiroDialect(){ return new ShiroDialect(); }
index.xml进行改造,加入th标签和shiro标签
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <h1>首页</h1> <div shiro:notAuthenticated=""> <a th:href="@{/toLogin}">登录</a> </div> <p th:text="${msg}"></p> <hr> <div shiro:hasPermission="user:add"> <a th:href="@{/user/add}">add</a> </div> <div shiro:hasPermission="user:update"> <a th:href="@{/user/update}">update</a> </div> </body> </html>
确保登录后不会重复出现“登录”字样,并只显示对应权限的字段链接
15、Swagger
15.1、前后端分离
Vue+SpringBoot
后端时代:前端只用管理静态页面html,后端通过模板引擎(JSP),后端是主力
前后端分离时代
-
后端:后端控制层、服务层、数据访问层【后端团队】
-
前端:前端控制层、视图层【前端团队】
-
伪造后端数据:json,不需要后端,前端工程也能够跑起来
-
-
前端后端如何交互?===>API
-
前后端相对独立,松耦合
-
前后端可以部署在不同的服务器上
产生一个问题:前后端集成联调:前端人员和后端人员无法做到及时协商,遇到问题尽早解决,最终导致问题集中爆发
解决方案:
-
指定schema(计划提纲),实时更新最新API,降低继承的风险
-
早些年:指定word计划文档
-
前后端分离:
-
前端测试后端接口:postman工具,自行用url发get/post测试
-
后端提供接口,需要实时更新最新的消息及改动
-
-
15.2、Swagger
流行API框架
RestFul API 文档在线自动生成工具 => API文档与API定义同步更新
在项目中使用Swagger需要Springfox
-
Swagger2
-
UI
15.3、SpringBoot继承Swagger
新建一个SpringBoot项目,加入web组件
导入依赖:
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>
编写一个HelloWorld
@RestController public class HelloController { @RequestMapping(value = "/hello") public String Hello(){ return "Hello World!"; } }
配置Swagger==>Config,加上@EnableSwagger2注解
浏览:http://localhost:8080/swagger-ui.html打开管理界面

15.4、配置Swagger基本信息
Swagger的bean实例Docket
package com.zhou.swagger.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import java.util.ArrayList; /** * @Name SwaggerConfig * @Description @EnableSwagger2开启 * @Author 88534 * @Date 2021/10/19 17:31 */ @Configuration @EnableSwagger2 public class SwaggerConfig { /** * 配置了Swagger的Docket实例 * @return */ @Bean public Docket docket(){ return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()); } /** * 配置Swagger信息ApiInfo * @return */ private ApiInfo apiInfo(){ Contact contact = new Contact("zhou", "http://www.baidu.com/", "123456@qq.com"); return new ApiInfo("Api Documentation", "Description", "1.0", "http://www.baidu.com/", contact, "Apache 2.0", "http://www.apache.org/license/LICENSE-2.0", new ArrayList<>()); } }

15.5、配置扫描接口及开关
可以采用链式配置
@Bean public Docket docket(){ return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) // enable 是否启动Swagger,如果为false,则Swagger不能在浏览器中访问 .enable(true) .select() // RequestHandlerSelectors 配置要扫描接口的方式 // .basePackage 指定要扫描的包 // .any 扫描全部 // .none 均不扫描 // .withClassAnnotation 扫描类上的注解 参数为类的注解的反射.class,注解的target不同 // .withMethodAnnotation 扫描方法上的注解 参数为方法的注解的反射.class .apis(RequestHandlerSelectors.basePackage("com.zhou.controller.HelloController")) // paths 过滤掉的路径 // .regex 正则表达式 .paths(PathSelectors.ant("/zhou/**")) .build(); }
如果只希望Swagger在生产环境dev中使用,在发布pro的时候不使用
-
判断是不是生产环境 Profile
-
注入enable() flag
@Bean public Docket docket(Environment environment){ // 设置要显示的swagger环境 Profiles profiles = Profiles.of("dev"); // 通过acceptsProfiles判断是否在设定环境中(监听) boolean flag = environment.acceptsProfiles(profiles); return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) // enable 是否启动Swagger,如果为false,则Swagger不能在浏览器中访问 .enable(flag) .select() // RequestHandlerSelectors 配置要扫描接口的方式 // .basePackage 指定要扫描的包 // .any 扫描全部 // .none 均不扫描 // .withClassAnnotation 扫描类上的注解 参数为类的注解的反射.class,注解的target不同 // .withMethodAnnotation 扫描方法上的注解 参数为方法的注解的反射.class .apis(RequestHandlerSelectors.basePackage("com.zhou.controller.HelloController")) // paths 过滤掉的路径 // .regex 正则表达式 .paths(PathSelectors.ant("/zhou/**")) .build(); }
新增application-dev和application-pro.properties,分别设置server.port为8081和8082
访问http://localhost:8081/swagger-ui.html#/成功
访问http://localhost:8082/swagger-ui.html#/失败
15.6、分组和注释
15.6.1、配置API文档的分组
上方链式编程加一段.groupName("xxx"),将在swagger右上角select a spec找到xxx
如何配置多个分组?
bean配置多个Socket实例
@Bean public Docket docket1(){ return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).groupName("A"); } @Bean public Docket docket2(){ return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).groupName("B"); } @Bean public Docket docket3(){ return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).groupName("C"); }
可在spec上找到A、B、C实现各个分组独立扫描独立维护
15.6.2、注释
使用@Api注解
@ApiModel标记类,也可以用tags:@Api(tags = "用户相关")
@ApiModel("用户实体类")
public class User {
@ApiModelProperty("用户名")
public String username;
@ApiModelProperty("密码")
public String password;
}
Swagger UI显示:
上述User添加构造方法和getter、setter方法
controller增加一个post测试
@ApiOperation("Post测试类")
@PostMapping("/post")
public User hello2(@ApiParam("用户") User user){
return user;
}
可以右上角try it out手动输入user信息进行测试


输出结果:

实现了自行测试
还可以显示各种报错
15.7、扩展:皮肤
可以导入不同的包实现不同的皮肤定义:
1、默认的 访问 http://localhost:8080/swagger-ui.html
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>

2、bootstrap-ui 访问 http://localhost:8080/doc.html
<!-- 引入swagger-bootstrap-ui包 /doc.html--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> <version>1.9.1</version> </dependency>

3、Layui-ui 访问 http://localhost:8080/docs.html
<!-- 引入swagger-ui-layer包 /docs.html--> <dependency> <groupId>com.github.caspar-chen</groupId> <artifactId>swagger-ui-layer</artifactId> <version>1.1.3</version> </dependency>

4、mg-ui 访问 http://localhost:8080/document.html
<!-- 引入swagger-ui-layer包 /document.html--> <dependency> <groupId>com.zyplayer</groupId> <artifactId>swagger-mg-ui</artifactId> <version>1.0.6</version> </dependency>

15.8、总结
我们可以通过Swagger给一些比较难理解的属性或者接口增加注释信息
接口文档是实时更新实时测试的,可以在线测试,不同的人使用不同的分组
Swagger是一个优秀的前后端交互工具,几乎所有大公司都有使用。
注意点:处于安全考虑和节省运行内存,在正式发布时关闭Swagger!!!
16、任务
异步任务
定时发送
邮件发送
16.1、异步任务Async
新建一个SpringBoot项目,使用web组件
建立一个测试Service
package com.zhou.text.service; import org.springframework.stereotype.Service; /** * @Name AsyncService * @Description * @Author 88534 * @Date 2021/10/20 9:02 */ @Service public class AsyncService { //告诉Spring是一个异步的方法:添加注解@Async @Async public void hello(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("数据正在处理……"); } }
建立一个controller测试
package com.zhou.text.controller; import com.zhou.text.service.AsyncService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @Name AsyncController * @Description * @Author 88534 * @Date 2021/10/20 9:05 */ @RestController public class AsyncController { @Autowired AsyncService asyncService; @RequestMapping("/hello") public String hello(){ // 停止三秒,等待响应 asyncService.hello(); return "OK"; } }
在主启动类上也增加@EnableAsync注解启动异步服务
package com.zhou.text; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; @EnableAsync //开启异步注解功能 @SpringBootApplication public class SpringBoot09TextApplication { public static void main(String[] args) { SpringApplication.run(SpringBoot09TextApplication.class, args); } }
测试:如果没有添加@Async注解等异步操作,浏览器将会等待3秒后再加载,此时控制台输出,前端与后端同步。
添加@Async后,前端与后端分离,前端快速刷出页面(先执行),后端进行3秒等待后加载并返回数据,实现了异步操作
16.2、邮件业务
16.3、定时执行任务
16.3.1、cron表达式
cron:计划任务,是任务在约定的时间执行已经计划好的工作。在Linux中,经常用到cron服务器完成。cron服务器可以根据配置文件约定的时间来执行特定的任务
星期字段按照标准日历的格式对应值(不是1和周一对应!)
| 日 | 一 | 二 | 三 | 四 | 五 | 六 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
Quartz使用类似于Linux下的Cron表达式定义时间规则,Cron表达式由6或7个由空格分隔的时间字段组成,如表1所示:
表1 Cron表达式时间字段
| 位置 | 时间域名 | 允许值 | 允许的特殊字符 |
|---|---|---|---|
| 1 | 秒 | 0-59 | , - * / |
| 2 | 分钟 | 0-59 | , - * / |
| 3 | 小时 | 0-23 | , - * / |
| 4 | 日期 | 1-31 | , - * ? / L W C |
| 5 | 月份 | 1-12 | , - * / |
| 6 | 星期 | 1-7 | , - * ? / L C # |
| 7 | 年(可选) | 空值1970-2099 | , - * / |
Cron表达式的时间字段除允许设置数值外,还可使用一些特殊的字符,提供列表、范围、通配符等功能,细说如下:
●星号():可用在所有字段中,表示对应时间域的每一个时刻,例如,在分钟字段时,表示“每分钟”;
●问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符,避免一周的第几天和一月的第几天冲突;
●减号(-):表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12;
●逗号(,):表达一个列表值,如在星期字段中使用“MON,WED,FRI”,则表示星期一,星期三和星期五;
●斜杠(/):x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0,15,30和45秒,而5/15在分钟字段中表示5,20,35,50,你也可以使用*/y,它等同于0/y;
●L:该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中意思不同。L在日期字段中,表示这个月份的最后一天,如一月的 31号,非闰年二月的28号;如果L用在星期中,则表示星期六,等同于7。但是,如果L出现在星期字段里,而且在前面有一个数值X,则表示“这个月的最后的一个星期X-1”,例如,6L表示该月的最后一个星期五;
●W:该字符只能出现在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如15W表示离该月15号最近的工作日,如果该月15号是星期六,则匹配14号星期五;如果15日是星期日,则匹配16号星期一;如果15号是星期二,那结果就是15号星期二。但必须注意关联的匹配日期不能够跨月,如你指定1W,如果1号是星期六,结果匹配的是3号星期一,而非上个月最后的那天。W字符串只能指定单一日期,而不能指定日期范围;
●LW组合:在日期字段可以组合使用LW,它的意思是当月的最后一个工作日;
●井号(#):该字符只能在星期字段中使用,表示当月某个工作日。如6#3表示当月的第三个星期五(6表示星期五,#3表示当前的第三个),而4#5表示当月的第五个星期三,假设当月没有第五个星期三,忽略不触发;
● C:该字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如5C在日期字段中就相当于日历5日以后的第一天。1C在星期字段中相当于星期日后的第一天。
Cron表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。
表2下面给出一些完整的Cron表示式的实例:
表2 Cron表示式示例
| 表示式 | 说明 |
|---|---|
| "0 0 12 * * ? " | 每天12点运行 |
| "0 15 10 ? * *" | 每天10:15运行 |
| "0 15 10 * * ?" | 每天10:15运行 |
| "0 15 10 * * ? *" | 每天10:15运行 |
| "0 15 10 * * ? 2008" | 在2008年的每天10:15运行 |
| "0 * 14 * * ?" | 每天14点到15点之间每分钟运行一次,开始于14:00,结束于14:59。 |
| "0 0/5 14 * * ?" | 每天14点到15点每5分钟运行一次,开始于14:00,结束于14:55。 |
| "0 0/5 14,18 * * ?" | 每天14点到15点每5分钟运行一次,此外每天18点到19点每5钟也运行一次。 |
| "0 0-5 14 * * ?" | 每天14:00点到14:05,每分钟运行一次。 |
| "0 10,44 14 ? 3 WED" | 3月每周三的14:10分到14:44,每分钟运行一次。 |
| "0 15 10 ? * MON-FRI" | 每周一,二,三,四,五的10:15分运行。 |
| "0 15 10 15 * ?" | 每月15日10:15分运行。 |
| "0 15 10 L * ?" | 每月最后一天10:15分运行。 |
| "0 15 10 ? * 6L" | 每月最后一个星期五10:15分运行。 |
| "0 15 10 ? * 6L 2007-2009" | 在2007,2008,2009年每个月的最后一个星期五的10:15分运行。 |
| "0 15 10 ? * 6#3" | 每月第三个星期五的10:15分运行。 |
生成器举例:https://cron.qqe2.com/
16.3.2、SpringBoot开启
@EnableScheduling //开启定时功能 @SpringBootApplication public class SpringBoot09TextApplication { public static void main(String[] args) { SpringApplication.run(SpringBoot09TextApplication.class, args); } }
service加入
@Service public class ScheduledService { /** * 在一个特定的时间执行这个方法Timer * cron表达式: * 秒 分 时 日 月 星期(0和7都代表周日) * 30 15 10 * * ? 每天的10点15分30执行一次 * 30 0/5 10,18 * * ? 每天的10点和18点,间隔5分钟执行一次 * 0 15 10 ? * ? 1-6 每个月的周一到周六的10点15分执行一次 * ?只能用在日或星期 */ @Scheduled(cron = "0 * * * * ?") //每天的第0秒执行 public void hello(){ System.out.println("执行!"); } }
可在控制台查到到点的打印结果
17、集成Redis
在Redis
18、Dubbo和Zookeeper集成
18.1、分布式理论
分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统。
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
分布式系统(distributed system)是建立在网络之上的软件系统。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。
Dubbo发展历程:

单一应用架构
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。
缺点:
-
性能扩展比较难
-
协同开发问题
-
不利于升级维护
垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。
通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。
缺点:公用模块无法重复利用,开发性的浪费
分布式垂直架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。
流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。
18.2、RPC
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
为什么要用RPC呢?因为无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;
推荐阅读文章:https://www.jianshu.com/p/2accc2840a1b
RPC基本流程:


以左边的Client端为例,Application就是rpc的调用方,Client Stub就是代理对象,其实内部是通过rpc方式来进行远程调用的代理对象,至于Client Run-time Library,则是实现远程调用的工具包,比如jdk的Socket,最后通过底层网络实现实现数据的传输。
这个过程中最重要的就是序列化和反序列化了,因为数据传输的数据包必须是二进制的,直接丢一个Java对象过去,无法识别,必须把Java对象序列化为二进制格式,传给Server端,Server端接收到之后,再反序列化为Java对象。
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
目的:
以某种存储形式使自定义
将对象从一个地方传递到另一个地方。
使程序更具维护性。
步骤:

RPC两个核心模块:通讯,序列化。
18.3、Dubbo
官网:https://dubbo.apache.org/zh/
Apache Dubbo 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
18.3.1、基本概念

服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
监控中心(Monitor):服务消费者和提供者,累计在内存中的调用次数和调用时间,每分钟定时发送一次统计数据到监控中心
调用关系说明
-
服务容器负责启动,加载,运行服务提供者。
-
服务提供者在启动时,向注册中心注册自己提供的服务。
-
服务消费者在启动时,向注册中心订阅自己所需的服务。
-
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
-
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
-
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
18.3.2、ZooKeeper注册中心
安装:https://downloads.apache.org/zookeeper/zookeeper-3.7.0/
安装apache-zookeeper-3.7.0-bin.tar.gz并解压
用notepad++编辑bin/zkServer.cmd文件末尾添加pause,添加"-Dzookeeper.audit.enable=true"

将conf文件夹下面的zoo_sample.cfg复制一份改名为zoo.cfg
dataDir=./ 临时数据存储的目录(可写相对路径),路径可以手动添加
clientPort=2181 zookeeper的端口号


来到bin目录下双击zkServer.cmd打开服务器,然后双击zkCl.cmd打开客户端

出现connected表示连接成功,可以在此窗口下输入一些简单命令
ls /:列出zookeeper根下保存的所有节点
create –e /zhou 123:创建一个zhou节点,值为123(类似于键值对)
get /zhou:获取/zhou节点的值
delete /zhou:删除/zhou节点

18.3.3、dubbo-admin
dubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。
但是为了让用户更好的管理监控众多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin,不过这个监控即使不装也不影响使用。
下载地址 :https://github.com/apache/dubbo-admin/tree/master
在项目目录下打包dubbo-admin,跳过测试,节省时间
mvn clean package -Dmaven.test.skip=true
找到dubbo-admin\target 下的dubbo-admin-0.0.1-SNAPSHOT.jar
开启zookeeper
再用cmd执行java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
访问一下 http://localhost:7001/ , 这时候我们需要输入登录账户和密码,我们都是默认的root-root;
登录成功后,查看界面

环境搭建完成!
dubbo-admin:是一个监控管理后台,可以查看注册哪些业务,哪些业务被消费
zookeeper:注册中心
Dubbo:jar包环境
18.3.4、SpringBoot + Dubbo + zookeeper
创建一个空项目,添加两个SpringBoot组件provider-server和consumer-server分别代表提供者、消费者,选择web依赖
两者的其余依赖相同,pom.xml:
将服务注册到注册中心,需要整合Dubbo和zookeeper
zookeeper及其依赖包,解决日志冲突,还需要剔除日志依赖;
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.3</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-x-discovery</artifactId> <version>5.1.0</version> </dependency> <dependency> <groupId>com.github.sgroschupf</groupId> <artifactId>zkclient</artifactId> <version>0.1</version> </dependency> <!-- 引入zookeeper --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.14</version> <!--排除这个slf4j-log4j12--> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
举例写一个买票的业务
provider提供者
编写接口
public interface TicketService { public String getTicket(); }
编写实现类
在service的实现类中配置服务注解,发布服务
package com.zhou.service; import org.apache.dubbo.config.annotation.Service; import org.springframework.stereotype.Component; /** * @Name TickerServiceImpl * @Description @Service可以被扫描到,在项目一旦启动就自动注册到注册中心 * zookeeper:服务注册与发现 * @Author 88534 * @Date 2021/10/20 20:51 */ @Service // 与web的service区分开 @Component //使用Dubbo后尽量不要使用Service注解,容易混淆 public class TicketServiceImpl implements TicketService { @Override public String getTicket() { return "北京-上海"; } }
在springboot配置文件中配置dubbo相关属性
server.port=8001
# 当前应用名字
dubbo.application.name=provider-server
# 注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
# 哪些服务需要注册
dubbo.scan.base-packages=com.zhou.service
逻辑理解 :应用启动起来,dubbo就会扫描指定的包下带有@component注解的服务,将它发布在指定的注册中心中
consumer消费者
配置参数
#当前应用名字
dubbo.application.name=consumer-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
创建接口
@Service //放到容器中 public interface UserService { /** * 想拿到票 */ void bugTicket(); }
实现类,使用@Reference引用注解
package com.zhou.service; import org.apache.dubbo.config.annotation.Reference; import org.springframework.stereotype.Service; /** * @Name UserServiceImpl * @Description 想拿到provider-server提供的票,要去注册中心拿到服务 * @Author 88534 * @Date 2021/10/20 22:42 */ @Service public class UserServiceImpl implements UserService{ /** * 引用,远程引用指定的服务,会按照全类名进行匹配,看谁给注册中心注册了这个全类名 */ @Reference TicketService ticketService; @Override public void bugTicket(){ String ticket = ticketService.getTicket(); System.out.println("在注册中心买到"+ticket+"的票"); } }
将提供者的接口直接拿过来,只拿接口不拿实现类

编写一个测试类测试
package com.zhou; import com.zhou.service.UserService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @SpringBootTest class ConsumerServerApplicationTests { @Autowired UserService userService; @Test void contextLoads() { userService.bugTicket(); } }
启动测试类
-
开启zookeeper
-
打开dubbo-admin实现监控【可以不用做】
-
开启服务者
-
消费者消费测试
2021-10-20 22:56:51.292 INFO 41116 --- [ main] org.apache.zookeeper.ZooKeeper : Initiating client connection, connectString=127.0.0.1:2181 sessionTimeout=60000 watcher=org.apache.curator.ConnectionState@66f659e6
2021-10-20 22:56:51.551 INFO 41116 --- [127.0.0.1:2181)] org.apache.zookeeper.ClientCnxn : Opening socket connection to server 127.0.0.1/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error)
2021-10-20 22:56:51.552 INFO 41116 --- [127.0.0.1:2181)] org.apache.zookeeper.ClientCnxn : Socket connection established to 127.0.0.1/127.0.0.1:2181, initiating session
2021-10-20 22:56:51.558 INFO 41116 --- [127.0.0.1:2181)] org.apache.zookeeper.ClientCnxn : Session establishment complete on server 127.0.0.1/127.0.0.1:2181, sessionid = 0x10025cc2a8e0003, negotiated timeout = 40000
2021-10-20 22:56:51.566 INFO 41116 --- [ain-EventThread] o.a.c.f.state.ConnectionStateManager : State change: CONNECTED
2021-10-20 22:56:53.262 INFO 41116 --- [ main] com.zhou.ConsumerServerApplicationTests : Started ConsumerServerApplicationTests in 3.479 seconds (JVM running for 4.507)
在注册中心买到北京-上海的票
在dubbo-admin界面可以看到提供者(在测试类中消费者时间较短,该界面只显示很短的时间)

至此,实现了简单跨服务的接口调用(后续可以使用Feign)
总结步骤:
前提:zookeeper服务已开始
提供者提供服务
导入依赖
配置注册中心的地址,以及服务发现名,和要扫描的包
在想要被注册的服务上面增加一个@Service注解
消费者如何消费
导入依赖
配置注册中心的地址,配置自己的服务名
从远程注入服务
19、现在和未来
三层架构 + MVC
开发框架Spring IOC+AOP
IOC:控制反转(以前都是一步一步操作,现在交给容器直接从里边拿(类似于房屋中介))
AOP:切面(本质,动态代理)
不影响业务本来的情况下,实现动态增加功能,大量应用在日志、事务等等
Spring是一个轻量级的Java开源框架、容器
目的:解决企业开发的复杂性问题
SpringBoot是新一代JavaEE的开发标准,开箱即用,Spring的升级版
自动配置许多东西,拿来即用
特性:约定大于配置
随着公司体系越来越大,用户越来越多
升级到微服务架构--->新架构
模块化、功能化
服务过多,一台服务器解决不了,再横向增加服务器
服务器占用比率不高时,使用负载均衡
微服务架构问题
-
这么多服务,客户端如何访问?网关
-
这么多服务,服务之间如何进行通信?RPC
-
这么多服务,如何管理?注册中心、统一服务管理平台zookeeper
-
服务挂了,怎么办?熔断机制
解决方案:
SpringCloud,是一套生态,解决以上分布式架构4个问题
基于SpringBoot,一站式解决方案
Spring Cloud NetFlix
-
访问:Api网关,zuul组件
-
通信:Feign ---> HttpClient ---> HTTP的通信方式,同步并阻塞
-
服务注册与发现:Euyuka
-
熔断机制:Hystrix
2018年年底,NetFlix宣布无限期停止维护,生态不再维护,就会脱节
Apache Dubbo zookeeper
-
访问:Api没有,需要找第三方组件或自己实现
-
通信:Dubbo,高性能RPC框架
-
服务注册与发现:zooKeeper,管理Hadoop、Hive……
-
熔断机制借助了Hystrix
不完善,属于组装
Spring Cloud Alibaba 一站式解决方案
全体起立!
目前又提出了新方案:服务网格service mesh,下一代微服务标准
代表解决方案:istio(未来)
万变不离其宗,一通百通
API网关,服务路由
HTTO、RPC框架,异步调用
服务注册与发现,高可用性
熔断机制,服务降级
基于这四个问题开发的解决方案,也叫SpringCloud
为什么要解决这个问题?本质:网络是不可靠的
不要停止学习的脚步!
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>









浙公网安备 33010602011771号