微服务概述
使用场景小结
上次课讲解了企业级应用和互联网应用的特征和基本区别
下面进行一个结论
- 企业级应用使用单体项目的情况较多
- 互联网应用使用微服务项目的情况较多
什么是微服务
微服务的概念是由Martin Fowler(马丁·福勒)在2014年提出的
微服务是由以单一应用程序构成的小服务,自己拥有自己的行程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用 HTTP API 通信。同时服务会使用最小的规模的集中管理能力,服务可以用不同的编程语言与数据库等组件实现。
简单来说,微服务就是将一个大型项目的各个业务拆分成多个互不干扰的小项目的结构,而这些小项目专心完成自己的功能,而且可以调用别的微服务的方法,最终实现项目整体功能
京东\淘宝等大型互联网应用程序在基本业务的每个流程都是一个微服务项目在支持
为什么使用微服务
单体项目成本低,但是不能实现高并发,高可用,高性能的程序
适合对性能要求不是很高的项目
微服务成本高,但是可以实现高并发,高可用,高性能的程序
适合互联网应用这类的对性能要求高的项目
怎么实现搭建微服务项目
在2014年之前,每个需要微服务功能的厂商都有自己的解决方案
但是技术和使用的标准不统一,Martin Fowler(马丁·福勒)提出了微服务的标准之后,很多企业都开始支持这个标准,也导致大家微服务项目的思路统一
但是我们也不可能去编写微服务底层的结构,肯定是使用别人写好的软件或框架来搭建微服务项目
一般情况下,我们会使用SpringCloud来实现微服务架构项目的开发
什么是SpringCloud
SpringCloud就是由Spring提供的一套能够快速搭建微服务架构项目的框架集
SpringCloud不是一个框架,而是一系列框架或软件的统称
SpringCloud其中的内容都是为了搭建微服务框架而出现的
有人将SpringCloud称之为"Spring全家桶",广义上指Spring提供的所有框架
SpringCloud中都包含什么内容呢?
从内容的提供者角度来说
- Spring自己编写的组件
- netflix:奈非
- alibaba:阿里巴巴
国内越来越多的公司开始使用阿里巴巴的SpringCloud组件
从功能上来说
- 微服务的注册
- 微服务的网关
- 微服务的调用
- 微服务的安全和权限管理
- .......
Nacos注册中心
什么是Nacos
Nacos是Spring Cloud Alibaba提供的一个软件
这个软件的功能是将所有微服务项目的信息收集注册和管理
也就是"注册中心"
Nacos是一个开发好的软件,我们需要先启动它,它就可以为我们的微服务项目提供注册的服务了
下面我们就要学习怎么安装\启动Nacos
首先Nacos是java开发的,它的运行需要系统支持java的环境变量
java环境变量的配置
windows系统环境变量配置过程如下:
1."计算机\此电脑"图标上单击右键->属性
点击高级系统设置
点击环境变量
检查是不是包含JAVA_HOME的配置,并且配置的路径确实包含jdk文件夹
如果没有配置需要点击新建按钮,按上图配置即可
没有安装java的同学先安装java再配置
安装启动Nacos
我们下载了nacos的软件(下载路径在笔记末尾)
将压缩包解压后,打开解压内容中的bin目录,看到如下内容
startup.cmd是windows系统启动nacos的命令
shutdown.cmd是windows系统停止nacos的命令
.sh结尾的命令是linux和mac系统使用的
启动nacos不能直接双击文件
需要打开dos命令行来执行
Win+R 运行输入cmd打开dos
启动命令中standalone指本次运行是单机模式,如果不指定,默认是集群模式
startup.cmd -m standalone
启动成功之后访问
localhost:8848/nacos
如果访问这个路径长时间没有响应,可以将dos窗口关闭之后再重新启动尝试
登录系统
用户名和密码默认都是nacos
到此为止 nacos的安装和启动就完成了
注册项目到Nacos
我们使用knows-resource项目
将它注册到Nacos中
其实注册的过程就是一些固定的配置,我们需要时直接套用即可
首先我们需要将父项目的pom文件进行比较多的修改,让父项目支持微服务类型的子项目
配置内容较多,直接发给大家, 我们也不需要去记住
父项目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.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>knows</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>knows</name>
<description>Demo project for Spring Boot</description>
<packaging>pom</packaging>
<modules>
<module>knows-resource</module>
</modules>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version>
<mybatis.plus.version>3.3.1</mybatis.plus.version>
<pagehelper.starter.version>1.3.0</pagehelper.starter.version>
<knows.commons.version>0.0.1-SNAPSHOT</knows.commons.version>
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
<spring-security-jwt.version>1.0.10.RELEASE</spring-security-jwt.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>cn.tedu</groupId>
<artifactId>knows-commons</artifactId>
<version>${knows.commons.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.starter.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>${spring-security-jwt.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
下面开始在knows-resource项目中配置Nacos的注册
步骤1:
knows-resource的pom.xml文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringCloudAlibaba 注册到Nacos的依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
步骤2:
knows-resource的application.properties文件
# 定义微服务项目的名称,当前项目会以这个名称注册到nacos
spring.application.name=resource-server
# 定义当前正在运行的nacos的位置
# 其实如果不配置下面的内容,会启用nacos的默认位置,localhost:8848
# 但是实际开发时,注册中心还在这个位置的几率较小,一定会配置
spring.cloud.nacos.discovery.server-addr=localhost:8848
步骤3:
最后在knows-resource项目的SpringBoot启动类中添加注解
表示这个项目启动时要注册到注册中心
@SpringBootApplication
// 下面的注解标记之后,项目启动时会注册到注册中心,成为一个微服务项目
@EnableDiscoveryClient //EDC
public class KnowsResourceApplication {
public static void main(String[] args) {
SpringApplication.run(KnowsResourceApplication.class, args);
}
}
测试注册是否能够完成
1.先必须保证Nacos正在执行,nacos的dos界面不能关闭,一关闭就停止了
2.再运行knows-resource项目
3.在nacos界面中的服务管理->服务列表中,看到我们设定的微服务名称:resource-server既表示成功!
小结:
一个项目如果要注册到nacos需要进行如下3个位置的配置
1.子项目的pom.xml文件
2.application.properties添加配置
3.SpringBoot启动类添加注解
今后还有很多类似的配置都是修改这3个位置
我们为了方便大家记忆,将这个配置过程称之为"三板斧"
Spring Gateway 网关
什么是网关
所谓网关,就是请求到当前项目时,本项目为访问者提供的一个统一的入口
在这个入口处,所有请求都会被记录和检查,或者验证是否有访问权限
网关就像公司中雇佣的保安一样,监控记录访客的进出
网关在实际的项目中运行功能如下
- 监视和监控和日志的记录
- 身份验证与权限的控制
- 根据请求路径动态路由到不同的微服务
路由的近义词就是"分配"
网关在项目中的位置和作用如下
Spring Gateway 使用
我们当前达内知道项目中就使用SpringGatway当做微服务的网关组件
SpringGatway是Spring自己的开发,属于SpringCloud框架集中的内容
它和nacos不同,nacos是个软件,SpringGateway是一个依赖,更类似一个框架,是半成品,我们需要创建项目添加这个依赖,编写配置,运行项目才能让网关生效
创建子项目gateway
父子相认和三板斧配置
父项目pom文件
<modules>
<module>knows-resource</module>
<module>gateway</module>
</modules>
gateway子项目的pom
<?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>cn.tedu</groupId>
<artifactId>knows</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
</project>
三板斧的配置第二部是application.properties
但是这里我们要学习一个新的配置文件application.yml
它能使用更简单的写法表示application.properties文件中的配置,在实际开发中,编写较复杂的配置文件中,一般都会优先使用yml文件
在gateway项目的resources文件夹在创建一个application.yml的文件
编写配置内容如下
server:
port: 9000
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
# 启动网关功能,允许路由到指定路径
enabled: true
# 允许服务器名称小写
lower-case-service-id: true
routes: # 开始配置路由
- id: gateway-resource #设置路由名称,和其他任何配置无关
# 配置路由目标服务器名称:resource-server,和nacos中注册的名称一致
# lb是Load Balance的缩写,意思是负载均衡
uri: lb://resource-server
# 路由路径的配置
# 下面的配置表示当访问localhost:9000/image/a.jpg时
# 相当于访问了resource-server服务器的相同路径
# 既访问了: localhost:8899/image/a.jpg
predicates:
- Path=/image/**
步骤3是配置SpringBoot启动类
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
启动gateway项目
在nacos正在运行的前提下,到nacos服务列表中观察是否包含gateway的服务器信息
通过网关访问静态资源
静态资源服务器就是为了让我们访问那些图片的
我们现在有了网关路由设置,要实现通过访问网关的9000端口,显示静态资源服务器中图片的功能
到knows-resource项目中的application.properties文件中添加一个配置即可
# 在配置下面内容之前
# 我们访问静态资源 图片的路径为 http://localhost:8899/a.jpg
# 添加这个配置之后, 图片的路径变为 http://localhost:8899/image/a.jpg
# 配置的原因是因为我们的网关路由配置中设置了/image开头的路径访问静态资源服务器
# 配置之后 localhost:9000/image/a.jpg
# 就能转换 localhost:8899/image/a.jpg 来显示图片了
server.servlet.context-path=/image
重启knows-resource项目 保证nacos和网关也都在启动状态
输入路径测试
http://localhost:9000/image/a.jpg
如果能显示静态资源服务器的图片,表示一切正常(路径要指定真实存在的文件)
英文
startup:启动
shutdown:停止\关闭
enable:启用
discovery:探索
client:客户端
随笔
application.properties和application.yml区别
application.properties是中规中矩的配置格式
例如
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/knows?characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
yml可以将它简化为:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/knows?characterEncoding=utf8
username: root
password: root
我们看到yml文件冗余的配置信息较少
但是要遵循更多的格式要求
在现在企业中,为了更简洁的配置设计,很多配置文件采用了yml的格式
SpringBoot项目启动时
会解析application.properties和application.yml
负载均衡
什么是负载均衡
所谓的负载均衡就是一个能够将当前微服务所有项目的任务尽量平均分配的算法
当一个业务有多个服务器处理时
肯定有服务器比较忙,有服务器比较闲
负载均衡的算法能够实现请求到比较闲的服务器,来处理请求
nacos下载路径
国外网站,下载较慢多试几次
续 通过网关访问静态资源服务器
上次课完成了网关访问静态资源服务器的配置
下面有一些补充
Spring Gateway的依赖和SpringMvc的依赖时冲突的,不能同时存在于一个项目中
SpringMvc自带一个服务器软件Tomcat,
Spring Gateway自带一个服务器软件Netty
前后端分离
什么是前后端分离
字面理解前后端分离,就是前端和后端是不同的项目
前端:html\css\js\静态资源图片等资源文件,是用户看到的界面
后端:就是我们编写java代码的部分
所谓分离就是前端和后端独立开发,互不影响
当今企业开发的项目,大多数都会采用前后端分离的开发方式
成熟的前端项目一般会使用Vue+nodeJs技术来搭建,因为我们没有学习这个技术,所以在现在学习中我们还是暂时使用SpringBoot项目来创建一个前端项目
前后端分离的好处:
- 方便程序的扩展,能够接受各种前端访问
- 前端是单独项目,可以访问任何编程语言编写的服务器
- 后端项目专注业务,在网关项目后为各种前端项目做出响应
前后端分离项目结构图
创建前端项目
创建一个子项目knows-client
父子相认
<module>knows-client</module>
knows-client的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>cn.tedu</groupId>
<artifactId>knows</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>knows-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>knows-client</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
它既然是静态资源服务器,就要把portal项目中所有的静态资源
也就是static文件夹中的所有内容复制到当前项目中
然后启动client项目,能够访问页面才算正常,但是不能进行实际操作页面也不显示内容,因为没有后端操作
使用Idea启动nacos
使用dos启动nacos过于麻烦
我们推荐大家利用idea给的功能,启动nacos
能够大幅减少启动难度
步骤如下
最好重启一下Idea确保Idea读取了最新的环境变量
然后就可以在idea中启动和停止nacos了
创建用户管理模块项目
我们已经完成了微服务项目的基本搭建
注册中心,网关和前端分离的项目
我们最终的目标是将knows-portal项目中所有功能按用户管理和问答管理拆分成两个模块
形成微服务的业务架构
首先我们以注册功能为目标,来创建用户管理模块项目
创建knows-sys项目
什么都不勾选直接下一步创建项目
父子相认
<module>knows-sys</module>
子项目sys模块的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>cn.tedu</groupId>
<artifactId>knows</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>knows-sys</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>knows-sys</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>cn.tedu</groupId>-->
<!-- <artifactId>knows-commons</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- nacos配置中心的依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 验证框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
</project>
配置中心
什么是配置中心
上面章节,我们学习了创建用户管理模块,而且完成了pom文件的依赖添加
下面应该编写knows-sys模块的application.properties文件的配置
这里我们使用配置中心来保存knows-sys模块的配置
配置中心的定义:将项目需要的配置信息保存起来,并可以让项目来读取内容的组件
好处是如果当前微服务多个项目都将配置信息保存在配置中心,那么修改配置时只需要在配置中心中修改即可,无需再找到对应的项目去修改,可以提高项目的维护性
Nacos实际上除了作为注册中心,还可以作为配置中心使用
Nacos支持多种格式的配置文件
properties\yaml(yml)\txt\json\xml....
Nacos配置中心结构
上图所示
一个Naocs可以创建多个Namespace(命名空间)
一个Namespace可以创建多个Group(分组)
一个Group可以保存多个配置信息文件(DataId)
所以在Nacos中想定位一个配置文件
Namespace->Group->DataId
Nacos提供了一个默认的Namespace:public,它不能被删除
将sys项目的配置保存到Nacos
Nacos首页->配置管理->配置列表->添加配置(右侧的大"+"号)
添加在public命名空间中
点击发布就能够将配置信息保存在Nacos中了
SpringBoot加载配置文件的顺序
我们都知道SpringBoot项目启动时会加载一系列配置文件
我们使用过的配置文件有application.properties和application.yml
他们两个都会被加载,而且是由先后顺序的
- 先加载yml内容
- 后加载properties内容
- 如果两个配置文件都由相同信息,后加载的覆盖先加载的
实际上SpringCloud环境下,会多出一组配置文件
bootstrap.yml和bootstrap.properties
这组配置文件运行的时机先于application那一组
而且必须等待bootstrap这组配置完全加载完毕后,application这组才运行
只有SpringCloud框架依赖下才有上面的加载顺序
左侧bootstrap所有信息加载完毕之后,才会开始加载右侧application的信息
所以我们将读取配置中心信息的配置编写在bootstrap中
以保证在程序运行时,配置中心的配置已经加载到当前项目
读取配置中心的配置信息
创建bootstrap.yml文件来读取配置中心的配置信息
knows-sys项目中的resources文件夹下创建bootstrap.yml文件
server:
port: 8001
spring:
application:
name: sys-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
# 指定配置中心的位置
server-addr: localhost:8848
# 指定配置所在的组名
# 我们使用的命名空间是默认的public,无需配置
group: DEFAULT_GROUP
# 指定配置文件的后缀名\类型
# 指定后缀名后,会自动到配置中心搜索名称为 [项目名称].[后缀名]的DataId
# 当前项目结合这个后缀名,会到配置中心寻找的DataId为: sys-service.properties
file-extension: properties
到此为止,配置信息成功加载到sys项目
sys项目的配置三板斧还有最后一个步骤
SpringBoot启动类添加注解
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("cn.tedu.knows.sys.mapper")
public class KnowsSysApplication {
public static void main(String[] args) {
SpringApplication.run(KnowsSysApplication.class, args);
}
}
别忘了@MapperScan的设置!
启动sys项目(Nacos要先启动)
启动不报错,表示读取了配置中心的内容
迁移注册功能
我们上面已经完成了knows-sys项目的创建和基本信息的配置
下面我们要将之前编写好的portal项目中的注册功能迁移到knows-sys项目中
我们计划的迁移步骤为:
- Mapper(数据访问层\持久层)
- Service(业务逻辑层:接口\实现类)
- Controller(控制层)
迁移Mapper
第一次开始迁移代码
我们可能需要导入很多包,为了快速导入确定的类的包
我们可以开启Idea自动倒包的功能
勾选java列表中Add unambiguous ..... 这个选择
这样的话就能自动导入能够确定的类的包了
我们在赋值Mapper时会发现Mapper中需要实体类的支持,否则是报错的
为了减少代码冗余,避免出现每个微服务项目都编写实体类的情况,我们创建一个新的通用资源项目,在这个项目中保存实体类和其他需要使用的类型,这样的话,哪个项目需要使用实体类直接添加通用资源项目的依赖即可
创建通用项目
创建knows-commons
通用资源项目就是一个提供通用类的项目,甚至都不需要启动,所以什么都不用勾选
父子相认
<module>knows-commons</module>
commons项目的pom文件
<?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>cn.tedu</groupId>
<artifactId>knows</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>knows-commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>knows-commons</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
</dependency>
</dependencies>
</project>
knows-commons项目要删除一些内容
删除SpringBoot启动类
删除test测试文件夹
最终内容如下图
这个项目一旦将需要有的资源复制完毕
其他项目就可以添加这个项目的依赖了
打开knows-sys项目的pom.xml文件
解除对knows-commons依赖的注释,并刷新maven
然后Mapper接口中的实体类就可以导入了
有些实体类需要我们手动导入(UserMapper中的三个实体类)
测试Mapper
为了验证我们从portal项目中迁移的Mapper可用
最好在测试类中编写测试代码运行验证
@Resource
UserMapper userMapper;
@Test
void contextLoads() {
User user=userMapper.findUserByUsername("st2");
System.out.println(user);
}
保证Nacos启动
如果能够输出学生信息,表示当前所有配置都在正常工作
创建控制层类测试
knows-sys模块创建controller包
包中创建一个类AuthController,代码如下
@RestController
@RequestMapping("/v1/auth")
public class AuthController {
// /v1/auth/demo
@GetMapping("/demo")
public String demo(){
return "controller demo";
}
}
重启服务测试这个路径是否能运行
http://localhost:8001/v1/auth/demo
我们访问时发现,Spring-Security正在工作,必须登录之后才能访问这个控制器
我们sys模块不需要这样的验证,创建一个security包,包中定义配置类,设置全部放行即可
编写Spring-Security放行设置
knows-sys项目
创建一个security包,包中创建SecurityConfig类
代码如下
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 配置全部放行
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 禁用防跨域攻击
.authorizeRequests() //开始设置请求授权
// 任何请求,全部放行
.anyRequest().permitAll();
}
}
重启服务
再次访问http://localhost:8001/v1/auth/demo,就能实现直接显示字符串结果不需要我们登录了
网关路由配置
我们之前学习了网关,了解了网关是当前我们项目正式接收外界请求的唯一入口
任何从前端发送到项目的请求,都要通过localhost:9000来访问
下面我们就通过配置gateway模块的信息,实现网关路由到knows-sys模块
转到gateway模块
修改application.yml文件
routes: # 开始配置路由
- id: gateway-sys
uri: lb://sys-service
predicates:
- Path=/v1/**
- id: gateway-resource #设置路由名称,和其他任何配置无关
# 配置路由目标服务器名称:resource-server,和nacos中注册的名称一致
# lb是Load Balance的缩写,意思是负载均衡
uri: lb://resource-server
# 路由路径的配置
# 下面的配置表示当访问localhost:9000/image/a.jpg时
# 相当于访问了resource-server服务器的相同路径
# 既访问了: localhost:8899/image/a.jpg
predicates:
- Path=/image/**
启动gateway项目
同时保证Nacos和Sys项目正在启动状态
http://localhost:9000/v1/auth/demo查看是否能显示同样的字符串结果
迁移注册的业务逻辑层
从portal项目中复制相关的接口和实现类
IUserService接口迁移过来需要少量的手动导包
UserServiceImpl实现类不是单纯的导包能解决问题的情况
需要将暂时无法调用的功能从代码中删除,保证编译通过
最终代码:
@Override
public UserVO getUserVO(String username) {
// 根据用户名获得用户信息
User user=userMapper.findUserByUsername(username);
// 根据用户id获得用户的问题数
// (作业)根据用户id获得用户的收藏数
// 实例化UserVo对象 赋值 最后返回
UserVO userVO=new UserVO()
.setId(user.getId())
.setUsername(user.getUsername())
.setNickname(user.getNickname());
// 千万别忘了返回userVO
return userVO;
}
迁移控制层代码
将上图所示两个类复制到controller包
通过导包解决编译错误
注册的控制层代码并不在我们迁移的内容中,而是在portal项目的SystemController类中
我们要复制SystemController类中注册代码,粘贴到sys项目的UserController中
可能需要在类上添加@Slf4j注解支持log对象
到此为止,sys项目中才有注册方法,路径是localhost:9000/v1/users/register
要执行真正的注册,要通过前端发送post请求实现
我们需要在现在的knows-client项目中异步注册的axios请求路径进行修改
前端项目发送注册请求
转到knows-client项目
js文件夹中打开register.js文件
修改异步注册的axios请求路径
axios({
method:'post',
url:'http://localhost:9000/v1/users/register',
data:form
})
重启knows-sys并且启动(或重启)knows-client
保证Nacos和gateway正在运行状态
访问前端的注册页面执行注册操作,会发生下面的错误
这个错误是因为跨域请求失败造成的,是一定会发生的错误
但是观察数据库,注册的用户数据是新增成功的!
什么是跨域
浏览器为了保证用户在访问网络资源时的安全,都是遵守"同源策略"的
同源策略,它是由Netscape提出的一个著名的安全策略。现在所有支持JavaScript 的浏览器都会使用这个策略。所谓同源是指,域名,协议,端口相同。目的是出于安全考虑,防止js脚本随意调用其他网站的资源。
"同源"的示例
我们前端项目注册页面的路径为:http://localhost:8080/register.html
我们注册要访问的axios路径为: http://localhost:9000/v1/users/register
上面的两个路径端口不同,所以不满足同源的判定要求,被视为非同源访问,受同源策略限制
只要受到同源策略限制,客户端发出的请求虽然可以到达服务器,但是客户端不能接受到服务器的响应
如果我们在不同源的情况下,还想要正常的请求和响应效果,就需要使用"跨域"技术
明确当今网站等各种前端,大多数是前后端分离的,跨域实际上已经是基本操作
实现跨域的方式:
- 编写前端代码可以实现跨域操作
- 编写后端代码可以实现跨域操作
我们主要学习后端代码实现跨域的解决方案
就后端来讲,java实现跨域的方式也很多
- 过滤器
- 拦截器
- SpringMvc跨域配置
- 控制器添加跨域注解
- 其他跨域方式.......
实现跨域注册
当前我们通过SpringMvc配置实现跨域请求效果
转到knows-sys模块
security包中创建一个配置SpringMvc框架的类WebConfig
设置跨域配置如下
//SpringMVC的配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 配置当前项目的所有路径跨域请求
registry.addMapping("/**") // 表示所有路径
.allowedOrigins("*") // 允许任何访问源
.allowedMethods("*") // 允许所有请求方法(get\post)
.allowedHeaders("*"); // 允许任何请求头信息
}
}
重启sys模块
再次执行注册,就应该能够得到正常的响应了!
创建问答模块
上面章节完成了knows-sys模块的注册功能迁移
下面我们开始着手完成达内知道问答模块的功能:显示首页标签列表
创建knows-faq项目
这个项目的功能主要是完成问答流程和用户首页
什么都不需要勾选
父子相认
<module>knows-faq</module>
knows-faq模块的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>cn.tedu</groupId>
<artifactId>knows</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>knows-faq</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>knows-faq</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>cn.tedu</groupId>
<artifactId>knows-commons</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- nacos配置中心的依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 验证框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
application.properties配置文件添加内容
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/knows?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
spring.datasource.username=root
spring.datasource.password=root
# 修改日志级别,这个操作也叫"修改日志输出门槛"
logging.level.cn.tedu.knows.faq=debug
server.port=8002
spring.application.name=faq-service
spring.cloud.nacos.discovery.server-addr=localhost:8848
英文
origins:源头
续 创建问答模块项目
上次课我们创建了knows-faq项目作为问答模块项目
完成了pom.xml的依赖个application.properties的编写
下面开始继续配置
SpringBoot启动类
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("cn.tedu.knows.faq.mapper")
public class KnowsFaqApplication {
public static void main(String[] args) {
SpringApplication.run(KnowsFaqApplication.class, args);
}
}
faq模块的创建和基本配置就完毕了
faq是问答模块,功能很多,我们先实现首页的标签列表
迁移显示所有标签的列表
迁移数据访问层
复制上图中portal项目中的4个mapper接口到faq项目
除了TagMapper需要手动导包只外,其他的都是自动导包即可
和sys模块一样,faq模块的mapper也推荐大家测试
@Resource
TagMapper tagMapper;
@Test
void contextLoads() {
List<Tag> tags=tagMapper.selectList(null);
for(Tag t:tags){
System.out.println(t);
}
}
迁移业务逻辑层
数据访问层迁移测试完成后
开始迁移业务逻辑层
通过导包就可以解决编译错误
迁移控制层
注意,除了正常复制和导包之外
我们要将控制器路径由之前的v1开头修改为v2开头,以应对后面的路由配置
@RequestMapping("/v2/tags")
下面就可以发同步测试功能了
保证先启动Nacos
localhost:8002/v2/tags
观察浏览器是否能显示所有标签(这次访问也是需要登录的!)
设置Spring-Security放行
和sys模块一样faq模块也是可以设置全部放行的
而且可以顺便完成跨域设置
既可以直接赋值sys模块中的security包到faq模块
直接复制过来就可以使用,不报错
重启faq模块
再次访问localhost:8002/v2/tags
就不需要登录直接显示所有标签信息了!
配置网关路由
转到gateway项目
添加faq模块的路由配置信息
application.yml添加配置如下
- id: gateway-faq
uri: lb://faq-service
predicates:
- Path=/v2/**
启动网关项目 gateway
Nacos和faq是启动状态
输入路径
localhost:9000/v2/tags
观察是否能够显示所有标签
前端项目修改请求路径
转到knows-client项目
要想在页面上显示所有标签的列表
最后要在前端项目的js文件请求路径上做修改
tags_nav.js文件的axios请求路径修改为
axios({
url:'http://localhost:9000/v2/tags',
method:'GET'
})
启动knows-client
Nacos\网关\faq都已经启动
访问学生首页,如果显示了所有标签表示一切顺利
可以受到之前缓的干扰,尝试清空缓存的刷新!
Redis 概述
Redis下载
什么是Redis
Redis就是一个缓存数据库
是一个能够将数据保存在内存中并且对数据进行高效读写的数据库软件
Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。目前Redis的开发由Redis Labs赞助。根据月度排行网站DB-Engines.com的数据,Redis是最流行的键值对存储数据库。
和mysql\mariaDB类似,Redis也是一个软件,可以安装,然后启动才能使用
Redis特征
-
Redis是一个缓存数据库,数据保存在内存中,处理数据速度快
经过测试,Redis可以支持每秒10万次读写
-
虽然Redis是内存数据库,但是它支持将内存中的数据(备份\保存)到硬盘上
如果遇到突发情况(例如断电),导致内存中的数据丢失,我们可以在开机时将硬盘上的数据恢复到Reids中,(Redis将数据保存在硬盘上有两种策略分别是AOF和RDB,它们可以同时开启,具体原理和区别同学们可以自己查阅资料)
-
Redis使用Key-Value的数据结构保存数据,可以将它看做是一个Map类型对象
业界中还有一些这样使用Key-Value保存数据的数据库,
这样的数据库我们称之为:"非关系型数据库"英文:no-sql
-
Redis支持保存的Value数据是各种类型的:
string,list,set,zset,hash
-
Redis支持分布式部署,以达到高并发,高可用,高性能的目的
-
Redis的竞品软件:memcached
为什么需要Redis
之前我们使用java代码自己定义集合实现了缓存效果
那为什么还需要使用Redis呢?
当我们的faq项目并发较高时,可能需要组成一个集群,也就是多台服务器共同完成业务时,每个服务器都要保存一份缓存,缓存的内容是一致的,内存信息就冗余了,造成了内存的浪费
我们希望在Redis中保存一份缓存数据,哪个服务器需要,哪个服务器来读取,这样就不需要没每个服务器都保存缓存数据了,减少了内存的浪费
Redis的解压和运行
将我们下载的压缩包解压后,得到内容如图
redis-server.exe\redis-start.bat这两个文件双击其中任何一个都可以达到启动Redis的效果
但是这个打开的界面一旦关闭Redis就停止了
在Redis开启的状态下,双击redis-cli.exe来启动客户端访问redis服务器
输入"info"指定测试当前Redis是都正常运行
上面的启动方式每次开机都要运行,而且打开的窗口如果不小心关掉了Redis就停止了
比较麻烦,我们希望Redis能够像mysql一样,每次开机自动启动
- service-installing.bat 双击能够将Redis进行安装,到当前系统中
- service-start.bat 双击能够启动当前Redis的服务,默认每次开机自动启动
- service-stop.bat 停止服务
- service-uninstalling.bat 卸载程序
我们只需要运行1,2两个文件
就可以实现每次开机自动启动Redis的效果
仍然可以启动redis-cli.exe进行测试
Redis默认端口号:6379
Redis的基本使用
课程中,我们给大家介绍Redis的基本操作指令
而我们操作的数据是由多种类型的
我们实际使用Redis的情况中基本只会使用string类型
string可以用json格式字符串表示对象等信息
下面我们就演示一下string类型的基本操作
运行redis-cli.exe文件,在界面中输入:
127.0.0.1:6379> set mystr "hello world"
OK
127.0.0.1:6379> get mystr
"hello world"
Redis适合将标签\分类\秒杀这样频繁被访问的信息缓存起来,提高网站性能
Redis底层操作数据的线程只有一条,即时有并发的请求,也不会有线程安全问题
因为足够快,所以不会造成明显的延迟
Redis除了直接保存字符串数据之外
还经常需要保存可以变化的数字
例如短视频app的点赞数,浏览数,评论数,收藏数等
电商网站定义一个库存总数,每销售一个库存数减一也是redis操作
Redis提供了常见的加一和减一操作的命令
127.0.0.1:6379> set num "2"
OK
127.0.0.1:6379> get num
"2"
127.0.0.1:6379> incr num
(integer) 3
127.0.0.1:6379> get num
"3"
127.0.0.1:6379> decr num
(integer) 2
其他类型的操作在笔记末尾有文档,同学们可以自行测试
SpringBoot中操作Redis
添加依赖
转到knows-faq模块,在pom.xml文件中添加依赖如下
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
Redis也是数据库,和java操作mysql有很多类似的地方
java是通过jdbc来操作mysql数据库的
java是通过jedis来操作Redis数据库的
jedis和jdbc操作对应数据库的代码是比较繁琐的
所以我们使用Mybatis简化mysql的操作
我们连接Redis也有简化工具:SpringData Redis
我们的目标是将当前达内知道项目的标签列表保存到Redis中
还是faq模块,application.properties配置文件中设置Redis的位置
# 配置Redis的ip地址和端口号信息,以便SpringDataRedis连接
spring.redis.host=localhost
spring.redis.port=6379
基本操作测试
knows-faq模块的测试类中,可以编写对Redis基本操作的测试
// 下面的对象是Spring容器中有SpringDataRedis提供的
// RedisTemplate<[key类型],[value类型]>
@Resource
RedisTemplate<String,String> redisTemplate;
@Test
public void redis(){
// 向redis中新增一个数据
redisTemplate.opsForValue().set("myname","诸葛亮");
System.out.println("ok");
}
@Test
public void getValue(){
// 从Redis中获得指定key的value
String name=redisTemplate.opsForValue().get("myname");
System.out.println(name);
}
Redis缓存标签列表
knows-faq模块中
TagServiceImpl类中,我们使用了List<Tag>和Map<String,Tag>类型的属性分别做了缓存,保存了所有标签对象
经过上面的讲解,大家知道了使用Redis作为缓存更好,所以我们将这个类中的缓存的标签保存在Redis中进行优化
TagServiceImpl类要进行大范围的修改
最终修改结果如下
@Service
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements ITagService {
@Resource
private RedisTemplate<String,List<Tag>> redisTemplate;
@Autowired
private TagMapper tagMapper;
@Override
public List<Tag> getTags() {
// 先从Redis中尝试获得标签集合
List<Tag> tags=redisTemplate.opsForValue().get("tags");
// 判断获得的集合是不是null
if(tags==null) {
// 如果是null表示Redis中没有所有标签集合的缓存,需要连接数据保存到Redis中
tags=tagMapper.selectList(null);
redisTemplate.opsForValue().set("tags",tags);
System.out.println("Redis加载标签完毕");
}
return tags;
}
@Override
public Map<String, Tag> getTagMap() {
// 实例化一个Map对象
Map<String,Tag> tagMap=new HashMap<>();
// 调用上面获得List<Tag>的方法,遍历它为Map对象赋值
for(Tag t:getTags()){
tagMap.put(t.getName(),t);
}
return tagMap;
}
}
重启faq项目
再访问学生首页,第一次访问时控制台会输出"Redis加载标签完毕",但是之后的刷新就不会再输出这个信息了,甚至重启faq模块之后访问学生首页也不会输出这个信息了,原因是Redis中保存着这个信息,只要Redis不重启,就不会出现这个信息!
Ribbon实现微服务间调用
什么是Ribbon
Ribbon也是SpringCloud提供的以组件
它的功能是实现微服务之间的相互调用
因为一个微服务项目几乎都会和其他微服务有调用关系,也就是调用操作是非常普遍存在的
所以Ribbon的依赖已经被集成在spring-cloud-starter-alibaba-nacos-discovery中了
也就是说Ribbon不需要添加额外依赖就可以使用了
Ribbon使用示例
在使用Ribbon之前
我们需要先明确在指定的业务流程中,Ribbon作用的多个微服务项目中哪个是调用的发起者,哪个是被调用的
被调用的一方称之为服务的提供者,也叫"生产者"
发起调用的一方称之为服务的调用者,也叫"消费者"
Ribbon可以调用什么样的方法呢?
Ribbon本质上就是向目标服务器发送了一次请求,它能调用到的方法就是目标服务器编写的控制器的方法,调用的依据就是控制器方法的url
我们将用与Ribbon调用的控制器方法称之为"Rest接口"
1.定义生产者方法(Controller的方法)
2.添加Ribbon调用支持(一次性配置)
3.消费者发起调用(一般是在业务逻辑层中发起调用)
步骤1:
定义生产者
任何已经编写好的控制器方法,都可以是Ribbon调用的目标
我们本次调用使用knows-sys模块中的AuthController类中的demo方法作为调用模板
访问的路径为: /v1/auth/demo
步骤2:
添加Ribbon调用支持
我们需要在发起调用的一方的Spring容器中保存一个能够执行Ribbon请求的对象
当前测试时在knows-faq项目中发起对knows-sys的Ribbon请求
所以在knows-faq的SpringBoot启动类中添加如下配置
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("cn.tedu.knows.faq.mapper")
public class KnowsFaqApplication {
public static void main(String[] args) {
SpringApplication.run(KnowsFaqApplication.class, args);
}
// @Bean表示会将下面方法的返回值保存到Spring容器中
@Bean
// LoadBalanced是负载均衡的意思
// 微服务间调用不经过网关,所以网关中设置的负载均衡无效
// 所以要设置Ribbon的负载均衡
@LoadBalanced
// RestTemplate就是能够实现微服务间调用的对象
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
RestTemplate就是能够执行Ribbon请求的对象
这个步骤只需要配置一次,如果在其他业务中knows-faq项目又需要调用其他项目的方法,就不需要再次配置了
步骤3:
发起调用
我们应该已经在发起调用的一方中配置了RestTemplate对象
实际开发中,都是在业务逻辑层中发起Ribbon调用
现在我们是测试,写在测试类中即可
调用目标:/v1/auth/demo
测试代码如下
// 必须添加下面的注解!!!!!!!
@SpringBootTest
public class TestRibbon {
@Resource
RestTemplate restTemplate;
@Test
public void ribbon(){
// 发送ribbon请求需要先定义url
// sys-service是服务的生产者注册到Nacos的名称
// /v1/auth/demo就是要调用的控制方法的路径,既要调用的Rest接口
String url="http://sys-service/v1/auth/demo";
// restTemplate.getForObject就是在发起Ribbon请求
// 参数url就是上面定义好的字符串
// String.class就是这个Rest接口返回值类型的反射
String str=restTemplate.getForObject(url,String.class);
System.out.println(str);
}
}
测试必须在Nacos和sys启动的情况下运行
faq模块会在测试运行时启动
Ribbon调用关系图
Ribbon调用用户信息
上面的测试时简单的调用,实际开发中返回值可能不是简单的字符串并且调用可能需要参数
下面我们提高一些难度,实现一个贴合我们实际开发需求的Ribbon调用
在我们的项目中经常会根据用户名获得用户对象信息
faq模块是没有这个功能的,有这个功能的模块是sys
所以我们实现一个faq模块调用sys模块中"根据用户名获得用户对象"的Ribbon请求
sys模块仍然是生产者
faq模块仍然是消费者
根据上面章节的学习,我们知道sys模块应该先定义一个能够根据用户名返回用户对象的控制器方法,然后才能由faq调用
但是我们sys模块先在没有这个功能的控制层方法,没有就去编写
从业务逻辑层开始编写
knows-sys模块业务逻辑层IUserService接口添加方法如下
// 根据用户名获得用户对象的方法
User getUserByUsername(String username);
UserServiceImpl实现类代码
@Override
public User getUserByUsername(String username) {
return userMapper.findUserByUsername(username);
}
AuthController类中添加上面业务逻辑层方法的调用
@Resource
private IUserService userService;
// 根据用户名获得用户对象的控制器方法(Rest接口)
// 所有支持Ribbon请求的方法都是GetMapping
@GetMapping("/user")
public User getUser(String username){
return userService.getUserByUsername(username);
}
sys模块准备工作完成
转到knows-faq模块
继续在测试类中编写调用上面sys模块控制器方法的代码
// Ribbon调用sys模块根据用户名获得用对象的Rest接口
@Test
public void getUser(){
// url路径中?之后的内容就是Ribbon请求的参数设置
// 参数名称必须和控制器方法声明的参数名称一致
// 参数值不用直接赋值,使用{1}来占位,需要更多参数时使用{2},{3}....
String url="http://sys-service/v1/auth/user?username={1}";
// 调用时,前两个参数意义不变,从第三个参数开始向{1}中传值
User user=restTemplate.getForObject(
url,User.class,"st2");
System.out.println(user);
}
别忘了重启sys模块在运行测试类代码
如果能输出我们给定用户名的用户信息,表示一切顺利
随笔
Spring-Security登录\权限管理框架 shiro 权限管理框架
Redis memcached
Redis资料:
操作其他类型数据的命令
List 列表
常用命令: lpush,rpush,lpop,rpop,lrange等
Redis的list在底层实现上并不是数组而是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
lists的常用操作包括LPUSH、RPUSH、LRANGE、RPOP等。可以用LPUSH在lists的左侧插入一个新元素,用RPUSH在lists的右侧插入一个新元素,用LRANGE命令从lists中指定一个范围来提取元素,RPOP从右侧弹出数据。来看几个例子::
//新建一个list叫做mylist,并在列表头部插入元素"Tom"
127.0.0.1:6379> lpush mylist "Tom"
//返回当前mylist中的元素个数
(integer) 1
//在mylist右侧插入元素"Jerry"
127.0.0.1:6379> rpush mylist "Jerry"
(integer) 2
//在mylist左侧插入元素"Andy"
127.0.0.1:6379> lpush mylist "Andy"
(integer) 3
//列出mylist中从编号0到编号1的元素
127.0.0.1:6379> lrange mylist 0 1
1) "Andy"
2) "Tom"
//列出mylist中从编号0到倒数第一个元素
127.0.0.1:6379> lrange mylist 0 -1
1) "Andy"
2) "Tom"
3) "Jerry"
//从右侧取出最后一个数据
127.0.0.1:6379> rpop mylist
"Jerry"
//再次列出mylist中从编号0到倒数第一个元素
127.0.0.1:6379> lrange mylist 0 -1
1) "Andy"
2) "Tom"
Set 集合
常用命令: sadd,smembers,sunion 等
set 是无序不重复集合,list是有序可以重复集合,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要功能,这个也是list所不能提供的。
可以基于 set 轻易实现交集、并集、差集的操作。比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能,也就是求交集的过程。set具体命令如下:
//向集合myset中加入一个新元素"Tom"
127.0.0.1:6379> sadd myset "Tom"
(integer) 1
127.0.0.1:6379> sadd myset "Jerry"
(integer) 1
//列出集合myset中的所有元素
127.0.0.1:6379> smembers myset
1) "Jerry"
2) "Tom"
//判断元素Tom是否在集合myset中,返回1表示存在
127.0.0.1:6379> sismember myset "Tom"
(integer) 1
//判断元素3是否在集合myset中,返回0表示不存在
127.0.0.1:6379> sismember myset "Andy"
(integer) 0
//新建一个新的集合yourset
127.0.0.1:6379> sadd yourset "Tom"
(integer) 1
127.0.0.1:6379> sadd yourset "John"
(integer) 1
127.0.0.1:6379> smembers yourset
1) "Tom"
2) "John"
//对两个集合求并集
127.0.0.1:6379> sunion myset yourset
1) "Tom"
2) "Jerry"
3) "John"
Sorted Set 有序集合
常用命令: zadd,zrange,zrem,zcard等
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。
很多时候,我们都将redis中的有序集合叫做zsets,这是因为在redis中,有序集合相关的操作指令都是以z开头的,比如zrange、zadd、zrevrange、zrangebyscore等等
来看几个生动的例子:
//新增一个有序集合hostset,加入一个元素baidu.com,给它赋予score:1
127.0.0.1:6379> zadd hostset 1 baidu.com
(integer) 1
//向hostset中新增一个元素bing.com,赋予它的score是30
127.0.0.1:6379> zadd hostset 3 bing.com
(integer) 1
//向hostset中新增一个元素google.com,赋予它的score是22
127.0.0.1:6379> zadd hostset 22 google.com
(integer) 1
//列出hostset的所有元素,同时列出其score,可以看出myzset已经是有序的了。
127.0.0.1:6379> zrange hostset 0 -1 with scores
1) "baidu.com"
2) "1"
3) "google.com"
4) "22"
5) "bing.com"
6) "30"
//只列出hostset的元素
127.0.0.1:6379> zrange hostset 0 -1
1) "baidu.com"
2) "google.com"
3) "bing.com"
Hash
常用命令: hget,hset,hgetall 等。
Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息:
//建立哈希,并赋值
127.0.0.1:6379> HMSET user:001 username antirez password P1pp0 age 34
OK
//列出哈希的内容
127.0.0.1:6379> HGETALL user:001
1) "username"
2) "antirez"
3) "password"
4) "P1pp0"
5) "age"
6) "34"
//更改哈希中的某一个值
127.0.0.1:6379> HSET user:001 password 12345
(integer) 0
//再次列出哈希的内容
127.0.0.1:6379> HGETALL user:001
1) "username"
2) "antirez"
3) "password"
4) "12345"
5) "age"
6) "34"
微服务的会话保持
会话和会话保持
会话就是Session,指多次请求和响应的过程,浏览器只要不关闭,不超时,就是同一次会话
登录时将用户信息保存在session中,在同一次会话中就可以随时从session中获得当前登录用户信息,这就是会话保持
而我们portal项目使用的会话保持方式是利用Spring-Security提供的各种方法,实际上Spring-Security底层依靠的也是session实现会话保持
单体项目只需要一个服务器来保存当前登录用户信息,就可以实现会话保持
但是微服务项目是由多个服务器构成的,我们登录之后如何让所有微服务都知道我们的身份就成为了问题
微服务中的会话保持问题
因为微服务项目是由多个不同的服务器构成的
每个服务器都有自己的内存,它们的内存不会自动共享
一个服务器用户登录后,只会在当前服务器中保存登录用户信息,不会将登录用户信息发送给其它服务器,这样用户在访问其它服务器时又需要登录,那么会话保持就失败了
所有微服务项目在会话保持时都会有上面的问题
那么想要在微服务项目中实现会话保持就需要特殊的解决方案
这个方案的名称叫"单点登录"
单点登录的实现思路
单点登录现在有比较流行的两种解决方案
1.Session共享
2.Token(令牌)
方案一:Session共享
核心思想就是将登录服务器获得的用户信息共享给其它微服务模块
实现思路
用户在登录服务器(sys)模块登录成功时,同时将登录成功的用户信息发送给Redis
用户访问其它模块时,其它模块到Redis中寻找当前访问用户的信息,如果能找到,就能获得用户信息
优点:
- 很多框架直接支持Session共享技术,直接进行相关配置就能实现功能
- 结构相对简单,不需要专门的服务器进行共享工作,成本较低
缺点:
- 因为用户信息要共享到所有服务器,而且Redis还保存一次,所以内存冗余量比较大,严重情况下会影响服务器性能
- 只能在当前微服务项目范围内共享,跨项目的情况下比较难以实现
方案二:Token令牌
登录成功时,由登录服务器向客户端响应一个Token,客户端保存这个Token
这个Token是一个加密字符串,其中包含着当前登录用户的信息
当前项目的所有服务器都可以解析这个Token
效果为只要客户端持有这个Token,就可以在整个项目中表名自己身份了
这样的话服务器就不需要使用内存来保存用户的信息,提高运行效率
优点:
- 解放内存,服务器不需要再使用内存保存用户信息,运行效率更高
- 客户端保存令牌,只要是可以解析这个令牌的项目都可以知道用户身份,方便登录和功能的扩展
缺点:
- 需要架设额外服务器,开发成本高,代码比较多
- 以为加密解密Token需要CPU的运算,所以消耗了额外的算力,CPU运行效率可能较低
达内知道项目使用Token令牌的方式实现微服务的单点登录
Oauth2概述
什么是Oauth2
实现Token进行单点登录的解决方案业务业界是由明确标准的
我们需要按照它的标准进行实现,实际上这个标准的实现是非常复杂的,我们需要使用现成的框架和规范,才能快速的实现标准的Token单点登录
Oauth(Open Auth:开发授权)是一个单点登录\授权的业界标准
现在大多数企业都在使用Oauth2作为授权或登录的标准
Oauth2.0是Oauth1.0的升级,Oauth1.0是不太完善的标准,几乎没人使用
一个项目支持Oauth2标准后,就能获得多种登录和授权的方法,可以使授权安全的在多个项目中运行甚至开放第三方软件登录的功能
Oauth2支持的部分授权模式:
- 扫码登录(专业说法:授权码登录)
- 用户名密码登录
- 客户端登录
- .....
Spring Cloud Security
Spring Cloud Security就是微服务版的Spring-Security
其中添加了微服务模式下登录和用户权限管理的支持
我们最终使用的框架是SpringCloudSecurity和Oauth2结合实现微服务架构下的登录\授权\权限管理功能
最终的实现结构如下图
微服务项目结构从功能上分为两大类
- 授权服务器:接收用户名和密码,验证登录,返回令牌
- 资源服务器:当用户请求当前服务器时,解析令牌,获得用户信息
Oauth2标准主要是提供授权服务器的各种功能
我们会添加支持Oauth2的依赖,这个依赖中会包含很多功能
尤其是包含了一些控制器方法,也就是说,我们创建的auth授权服务器项目,不需要编写控制器,也不需要编写控制器路径,是由Oauth提供的,我们直接调用即可
创建授权服务器项目
创建knows-auth项目
父子相认
<module>knows-auth</module>
knows-auth的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>cn.tedu</groupId>
<artifactId>knows</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>knows-auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>knows-auth</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>cn.tedu</groupId>
<artifactId>knows-commons</artifactId>
</dependency>
<!-- Spring Cloud Security结合Oauth2会在数据库中
保存一些临时信息,需要jdbc依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
</dependencies>
</project>
auth模块的application.properties配置如下
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/knows?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
spring.datasource.username=root
spring.datasource.password=root
# 修改日志级别,这个操作也叫"修改日志输出门槛"
logging.level.cn.tedu.knows.auth=debug
server.port=8010
spring.application.name=auth-service
spring.cloud.nacos.discovery.server-addr=localhost:8848
# 下面的配置含义是允许Spring容器中已经定义好的对象被覆盖
# 意思就是当两个相同id的对象保存到Spring容器时会不会报错
# 设置完true之后,相同id的后一个出现的对象会将之前的对象覆盖掉
# 在我们当前auth项目的配置中,需要这个操作
spring.main.allow-bean-definition-overriding=true
SpringBoot启动类修改
@SpringBootApplication
@EnableDiscoveryClient
public class KnowsAuthApplication {
public static void main(String[] args) {
SpringApplication.run(KnowsAuthApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
授权服务器配置准备
Auth会有大量配置
我们尽量将这些配置分散
我们先编写Spring-Security的放行配置和相关的准备工作
创建包security,包中创建SecurityConfig
代码如下
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 设置全部放行,因为我们将登录和验证都交给了Oauth和Token
// SpringSecurity不再负责验证登录的任务,所以直接放行
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 防跨域攻击
.authorizeRequests() // 访问权限设置
.anyRequest().permitAll() // 全部放行
.and().formLogin(); //支持表单登录
}
// 我们的令牌和一些信息需要进行加密操作
// 向Spring容器中保存一个加密对象供框架使用
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// Spring Cloud Security 需要授权管理器对象
// 当前类的父类中有这个类型对象
// 我们要将它保存到Spring容器中,Oauth2后面的配置需要使用到它
@Bean
public AuthenticationManager authenticationManagerBean()
throws Exception {
return super.authenticationManagerBean();
}
}
在授权过程中,令牌的保存也是一个问题
我们设计最终的效果是保存到客户端
但是我们开发过程中将令牌的生产和保存分步骤实现
初期先将令牌生成后保存在服务器内存
我们创建一个TokenConfig类,来设置和编写如何保存令牌的信息
// 这个注解必须要添加,才能编写配置,否则编写的配置不会生效
@Configuration
public class TokenConfig {
// 向Spring容器中保存一个令牌的生成策略
// 策略指:1.保存在内存中 2.保存在客户端
// 先保存在内存中
@Bean
public TokenStore tokenStore(){
return new InMemoryTokenStore();
}
}
登录过程中,我们仍然使用Spring-Security的登录思路,也就是要实现根据用户名获得用户详情(UserDetails)的方法
这个方法中,我们需要sys模块的3个功能,
所以我们需要sys模块声明这3个功能的控制器方法做rest接口
在auth模块中使用Ribbon先后调用这个3个方法,实现登录过程
这3个方法是:
- 根据用户名获得用户对象信息
- 根据用户id获得所有权限
- 根据用户id获得所有角色
转到knows-sys模块
将上面需要的3个方法全部在控制器方法中实现
其中根据用户名获得用户对象信息的方法已经完成
还差另外两个
编写业务逻辑层,IUserService接口添加方法
// 根据用户id获得所有权限
List<Permission> getPermissionsById(Integer id);
// 根据用户id获得所有角色
List<Role> getRolesById(Integer id);
UserServiceImpl实现类
@Override
public List<Permission> getPermissionsById(Integer id) {
return userMapper.findUserPermissionsById(id);
}
@Override
public List<Role> getRolesById(Integer id) {
return userMapper.findUserRolesById(id);
}
控制层AuthController类添加调用方法
@GetMapping("/permissions")
public List<Permission> permissions(Integer id){
return userService.getPermissionsById(id);
}
@GetMapping("/role")
public List<Role> roles(Integer id){
return userService.getRolesById(id);
}
到此为止,sys模块准备好了auth模块需要的Rest接口
转到knows-auth模块
我们可以在auth项目中创建一个service包
包中创建UserDetailsServiceImpl类
这个类中编写的代码参数和返回值以及业务逻辑思路和portal单体项目的登录是一致的
只是请求的数据需要通过发送Ribbon请求到sys模块来获得
具体代码如下
// 将当前这个类保存到Spring容器
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private RestTemplate restTemplate;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名获得用户对象
String url="http://sys-service/v1/auth/user?username={1}";
User user=restTemplate.getForObject(
url , User.class , username);
// 判断用户对象是否为空
if(user == null){
throw new UsernameNotFoundException("用户名不存在!");
}
// 获得当前用户所有权限
url="http://sys-service/v1/auth/permissions?id={1}";
// 当Ribbon请求的目标方法返回值是List类型时
// 要使用该List泛型类型的数组来接收这个返回值
Permission[] permissions=restTemplate.getForObject(
url, Permission[].class ,user.getId());
// 获得当前用户所有角色
url="http://sys-service/v1/auth/roles?id={1}";
Role[] roles=restTemplate.getForObject(
url, Role[].class, user.getId());
// 将权限和角色保存在数组中
String[] auth=new String[permissions.length+roles.length];
int i=0;
for(Permission p: permissions){
auth[i++]=p.getName();
}
for(Role r:roles){
auth[i++]=r.getName();
}
// 获得UserDetails对象并返回
UserDetails details= org.springframework.security.core
.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(auth)
.accountLocked(user.getLocked()==1)
.disabled(user.getEnabled()==0)
.build();
// 千万别忘了返回details
return details;
}
}
需要在测试类中测试一下才能安心
auth模块测试类代码编写
@Resource
UserDetailsServiceImpl userDetailsService;
@Test
void contextLoads() {
UserDetails user=userDetailsService.loadUserByUsername("st2");
System.out.println(user);
}
要保证Nacos启动并且sys模块启动
运行能输出用户信息表示一切正常
授权服务器核心配置
上面所有编写的类和代码都是为了核心配置做准备的
核心配置就是再创建一个类,这个类要继承一个父类
重写这个父类的三个方法,这三个方法的作用分别是
1.配置授权服务器的各种参数
2.配置客户端对应的各种权限
3.配置客户端允许使用的功能
security包中创建该类
AuthorizationServer(授权服务器)
@Configuration
// 这个注解表示当前类是Oauth2标准下
// 实现授权服务器的配置类,启动授权服务器相关功能
@EnableAuthorizationServer
public class AuthorizationServer extends
AuthorizationServerConfigurerAdapter {
// 添加依赖注入对象
// 授权管理器
@Resource
private AuthenticationManager authenticationManager;
// 登录用用户详情类
@Resource
private UserDetailsServiceImpl userDetailsService;
// 配置授权服务器的各种参数
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// Oauth2标准提供了很多控制器方法用于授权或验证
// 这个方法参数endpoints翻译为"端点",这里指的就是Oauth2提供的控制器方法
// 这个方法要做的就是配置端点,以便授权和验证等操作顺利执行
endpoints
// 配置授权管理器对象
.authenticationManager(authenticationManager)
// 配置登录时调用的获得用户详情的对象(我们自己编写的并以及测试通过)
.userDetailsService(userDetailsService)
// 配置登录信息提交的方法只能是Post
.allowedTokenEndpointRequestMethods(HttpMethod.POST)
// 配置生产令牌的对象
.tokenServices(tokenService());
}
// 添加保存令牌配置的对象
@Resource
private TokenStore tokenStore;
// 添加客户端详情对象(系统自动提供的)
@Resource
private ClientDetailsService clientDetailsService;
// 配置生成令牌和保存令牌的方法
@Bean
public AuthorizationServerTokenServices tokenService() {
// 创建生产令牌的对象
DefaultTokenServices services=new DefaultTokenServices();
// 设置令牌如何保存
services.setTokenStore(tokenStore);
// 设置令牌有效期(单位是秒 3600既1小时)
services.setAccessTokenValiditySeconds(3600);
// 配置这个令牌为哪个客户端生成
services.setClientDetailsService(clientDetailsService);
// 别忘了返回services对象
return services;
}
// 获得加密对象
@Resource
private PasswordEncoder passwordEncoder;
// 重写方法2配置客户端对应的各种权限
// 这里的客户端指所有依赖当前授权服务器进行授权和登录的项目
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 当前项目只支持达内知道进行授权
// "所有客户端"只包含达内知道项目
clients.inMemory() // 将客户端信息保存在内存中
.withClient("knows") //开始配置达内知道(knows)项目的权限
// 配置达内知道客户端登录时的密码(加密的)
.secret(passwordEncoder.encode("123456"))
.scopes("main") //配置当前客户端的权限 main只是个名字
// 设置客户端登录的方式
// password表示用户名密码登录的方式
// 这个字符串是框架识别的关键字,不能写错
.authorizedGrantTypes("password");
}
// 重写方法3配置客户端允许使用的功能
// 这个配置设置资源服务器的安全
// 规定哪些资源可以被客户端访问
// 我们的项目所有资源都对所有客户端开放,所以配置比较简单
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// 允许哪些客户端访问生产令牌的端点(permitAll():全部允许)
.tokenKeyAccess("permitAll()")
// 允许哪些客户端访问验证令牌的端点
.checkTokenAccess("permitAll()")
// 允许通过验证的客户端获得令牌
.allowFormAuthenticationForClients();
}
}
测试核心配置效果
首先保证要启动的项目
Nacos
knows-sys
knows-auth
启动postman来测试
别忘了把要登录的用户的密码列前面的{bcrypt}删除
删除后密码为
10$ELGiEhKyLlO9r3.WVOkHDe16JTCKCErcABhElD5CF7ZwQ.Hm6sVRW
下载一下Postman软件