区分Spring、SpringMVC、SpringBoot、SpringCloud
Spring
- 广义: 指的全家桶(桶里装的是
Spring顶级项目
) - 侠义: 指的是
Spring FrameWork(IOC、DI、AOP)
SpringMVC
Spring
中Web层
框架,底层基于Servlet
SpringBoot
- 核心: 起步依赖(原理: maven依赖传递)
自动装配
-
SpringBoot
项目启动时会加载spring-boot-autoconfigure
包下的MTATA-INF
下的spring.factories
文件import + selector选择器
-
spring.factories
文件配置了自动装配类的路径 -
不是所有的自动装配类都生效,是否生效取决于类似的方法上的条件注解
@ConditionalOnClass(.class)
-
条件注解是否生效又取决于
pom
文件是否导入了响应的starter
SpringCloud
微服务中服务治理一整套解决方案,全家桶(
erueka注册中心
、ribbon负载均衡
、feign远程调用
、gateway网关
)
认识微服务
单体架构
将业务的所有功能集中在一个项目中开发,打包成一个包部署
优点
- 架构简单
- 部署成本低
缺点
- 耦合度高
分布式架构
根据业务功能对系统进行拆分,每个业务模块作为单独项目开发,称为一个服务
优点
- 降低服务耦合
- 有利于服务升级扩展
微服务
微服务是一种经过良好架构设计的分布式架构方案
微服务架构特征
- 单一职责: 微服务拆分力度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
- 面向服务: 微服务对外暴露业务接口
- 自治: 团队独立、技术独立、数据独立、部署独立
微服务结构
- 服务集群
- 注册中心: 拉取或注册服务信息
- 配置中心: 拉取配置信息
- 服务网关: 请求路由负载均衡
常见微服务技术对比
- Dubbo
- 注册中心: zookeeper、Redis
- 服务远程调用: Dubbo协议
- 配置中心: 无
- 服务网关: 无
- 服务监控和保护: dubbo-admin,服务弱
- SpringCloud
- 注册中心: Eureka、Consul
- 服务远程调用: Feign(http协议)
- 配置中心: SpringCloudConfig
- 服务网关: SpringCloudGateway、Zuul
- 服务监控和保护: Hystix
- SpringCloudAlibaba
- 注册中心: Nacos、Eureka
- 服务远程调用: Dubbo、Feign
- 配置中心: SpringCloudConfig、Nacos
- 服务网关: SpringCloudGateway、Zuul
- 服务监控和保护: Sentinel
微服务常见使用组合
- SpringCloud + Feign: 使用
SpringCLoud
技术栈、服务接口采用Resuful
风格、服务调用采用Feign
方式 - SpringCLoudAlibaba + Feign: 使用
SpringCloudAlibaba
技术栈、服务接口采用Restful
风格、服务调用采用Feign
方式 - SpringCloudAlibaba + Dubbo: 使用
SpringCloudAlibaba
技术栈、服务接口采用Dubbo
协议标准、服务调用采用Dubbo
方式 - Dubbo原始模式: 基于
Dubbo
老旧技术体系、服务接口采用Dubbo
协议标准、服务调用采用Dubbo
方式
微服务技术
- Dubbo: 侧重于
远程调用
,rpc框架
,注册中心可以采用zookeeper
、redis
,服务治理相关功能不全
- SpringCloud: 微服务中服务治理一整套解决方案,全家桶(
erueka注册中心
、ribbon负载均衡
、feign远程调用
、gateway网关
) - SpringCloudAlibaba: 阿里针对微服务开源一整套解决方案`
SpringCloud
微服务中服务治理一整套解决方案,全家桶(
erueka注册中心
、ribbon负载均衡
、feign远程调用
、gateway网关
),SpringCloud
依赖于SpringBoot
SpringCloud
集合了各种微服务功能组件,并基于SpringBoot
实现了这些组件的自动装配,从而提供了良好的开箱即用体验
服务拆分
注意事项
- 单一职责: 不同微服务,不要重复开发相同业务
- 数据独立: 不要访问其他微服务的数据库
- 面向服务: 将自己的业务暴露为接口,供其他微服务调用
消费者与提供者
- 消费者: 一次业务中,被其他微服务调用的服务(提供接口给其他微服务)
- 提供者: 一次业务中,调用其他微服务的服务(调用其他微服务提供的接口)
- 一个服务既可以是消费者、也可以是提供者
RestTemplate
可以发送HTTP请求
- getForObject: 能从该
url
中获取一个对象 - postForObject: 能把一个对象转换为json发送给指定
url
- exchange(url, HttpMethod, HttpEntity, Boolean): 发送一次Http请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); // 表示这是一个application/x-www-form-urlencoded格式数据
MultiValueMap body = new LinkedMultiValueMap();
// 放入数据
multiValueMap.add();
...
multiValueMap.add();
HttpEntity httpEntity = new HttpEntity(body, headers);
ResponseEntity<Boolean> exchange = new restTemplate.exchange(url, HttpMethod.POST, httpEntity, Boolean.class);
Eureka注册中心
作用
- 消费者如何获取服务提供者具体信息?
- 服务提供者启动时向
eureka
注册自己的信息 eureka
保存这些信息- 消费者根据服务名称向
eureka
拉取提供者的信息
- 服务提供者启动时向
- 有多个服务提供者时,消费者该怎么选择?
- 服务消费者利用负载均衡算法,从服务列表中挑选一个
- 消费者怎么感知服务提供者健康状态?
- 服务提供者会每隔
30秒
向EurekaServer
发送心跳请求,报告健康状态 eureka
会更新记录服务列表信息,心跳不正常会被剔除- 消费者就可以拉取到最新的信息
- 服务提供者会每隔
Eureka架构中的微服务角色
- EurekaServer: 服务端,注册中心
- 记录服务信息
- 心跳监控
- EurekClient: 客户端
- Provider: 服务提供者
- 注册自己的信息到
EurekaServer
- 每隔
30秒
向EurekaServer
发送心跳
- 注册自己的信息到
- Consumer: 服务消费者
- 根据服务名称从
EurekaServer
拉取服务列表 - 基于服务列表做负载均衡,选中一个微服务后发起远程调用
- 根据服务名称从
- Provider: 服务提供者
搭建EurekaServer
- 创建项目,引入
spring-cloud-starter-netflix-eureka-server
的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
- 编写启动类,添加
@EnableEurekaServer
注解 - 添加
application.yml
文件,编写以下配置
server:
port: 10086
spring:
application:
name: eurekaserver
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka/
Eureka服务注册
- 引入
eureka-client
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 在
application.yml
中配置eureka
地址
spring:
application:
name: userservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka/
服务发现
- 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 修改
url
路径,用服务名代替ip、端口
- 在启动类上添加负载均衡注解
@LoadBalanced
Ribbon负载均衡
工作流程
- 发起带有服务的请求
- 拉取对应服务
- 返回该服务的列表
- 轮询到合适接口
负载均衡策略
Ribbon的负载均衡规则是一个叫做
IRule
的接口来定义的,每一个子接口都是一种规则
- 方式1: 代码方式,在启动类中定义一个新的
IRule
@Bean
public IRule randomRule() {
return new RandomRule();
}
- 方式2: 配置文件方式
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
Ribbon和Nginx的区别
Eureka
是客户端负载均衡器: 消费者从Eureka
拉取提供者的实例信息,经过Ribbon
的负载均衡直接发起调用,消费者是知道调用了那个提供者的Nginx
是服务端负载均衡器: 负载均衡能力是在服务端完成的,消费者不知道调用了哪个提供者
饥饿加载
Ribbon默认采用懒加载,第一次访问的时候才会去创建LoadBalanceClient,请求时间会很长。
饥饿加载会在项目启动时就加载,降低第一次访问的耗时
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: userservice # 指定对userservice这个服务饥饿加载
Nacos
Nacos注册中心
既可以作为注册中心、也可由作为配置中心
使用步骤
- 导入依赖
<!-- 父工程 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 子工程 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
- 配置类
spring:
cloud:
nacos:
server-addr: localhost:8848
Nacos服务分级存储模型
服务 -> 集群 -> 实例
使用步骤(在注册中心的基础上)
- 修改配置,添加所处集群
# service1
spring:
cloud:
nacos:
discovery:
cluster-name: HN # 集群1 HN
# service2
spring:
cloud:
nacos:
discovery:
cluster-name: HZ # 集群2 HZ
- 测试
此时还是轮询 - 如果需要优先同集群访问
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡策略,优先选择同级群
根据权重负载均衡
- 设置实例的权重(0~1),权重越小,访问的几率越小
- 同级群内的多个实例,权重越高被访问的频率越高
- 权重设置为0则完全不会被访问
环境隔离 - namespace
Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离
namespace -> group -> service/data
- namespace用来做环境隔离
- 每隔namespace都有
唯一id
- 不同namespace下的服务不可见
使用方法
spring:
cloud:
nacos:
discovery:
namespace: 2e32b562-a44c-4750-a2c6-e6539b06438d # 命名空间id
Nacos和Eureka区别
- 共同点
- 都支持服务注册和服务拉取
- 都支持服务提供者心跳方式做健康检测
- 不同点
- Nacos支持服务端主动检测提供者状态,临时实例采用心跳模式,非临时实例采用主动检测模式a
- 临时实例心跳不正常会被剔除,非临时实例不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用
AP方式
,当集群中存在非临时实例时,采用CP方式
;Eureka采用AP方式
Nacos注册中心细节分析
- 设置临时实例
spring:
cloud:
nacos:
discovery:
ephemeral: false # 是否是临时实例
Nacos配置管理
Nacos统一配置管理步骤
- 在
Nacos
中添加配置文件
# DataId完整格式为${prefix}-${spring.profiles.active}.${file-extension}
# prefix默认为spring.application.name的值,也可以通过配置项spring.cloud.nacos.confg.prefix来配置
# spring.profiles.active为当前环境对应的profile
# file-extension为配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file.extension来配置,目前只支持properties和yum类型
- 从
Nacos
中拉取配置 - 引入
nacos-config
依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- 添加
bootstrap.yaml
(引入bootstrap.yml是在引用application.yml之前 )
spring:
application:
name: userservice # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
discovery:
cluster-name: HN
config:
file-extension: yaml # 文件后缀名
prefix: aaa
profiles:
active: dev # 开发环境
实现热更新
- 方式一: 在使用远程配置的类上加
@RefreshScope
注解 - 方式二: 使用
@ConfigurationProperties
注解代替@Value
注解(新建一个类,将远程配置的变量作为成员变量,使用getter
方法取出)
应用场景
- 应用级别配置与业务相关的配置
- 系统级别配置例如端口号
- 应用名称等不适合被
nacos
管理
多环境配置共享
配置了服务名和
profiles.active
时,可以同时读取到服务名.ymal
和服务名-profile.ymal
多环境配置的优先级
服务名-profile.ymal > 服务名称.ymal > 本地配置
Nacos集群
配置步骤
- 初始化数据库: 官方推荐的最佳实践是使用带有主从的高可用数据库集群(手头服务器不够,暂时不作此配置)
- 下载Nacos
- 配置Nacos
- 修改
conf
目录下cluster.conf.example
名为cluster.conf
cluster.conf
添加内容
# 集群地址和端口,手头没有多于服务器,先放在本地,本地有虚拟机的可以使用192.168.126.1:端口号
127.0.0.1:8845
127.0.0.1:8846
127.0.0.1:8847
- 修改
application.properties
文件,添加数据库配置
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123
- 启动
- nginx反向代理,修改
conf/nginx.conf
文件
upstream nacos-cluster {
server 127.0.0.1:8845;
server 127.0.0.1:8846;
server 127.0.0.1:8847;
}
server {
listen 80;
server_name localhost;
location /nacos {
proxy_pass http://nacos-cluster;
}
}
- 而后在浏览器访问:http://localhost/nacos即可。
Feign
Feign是一个声明式的http客户端,作用是帮我们发送http请求,底层可以理解为RestTemplate+Ribbon
使用步骤
- 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 在启动类上添加注解
@EnableFeignClients(clients = UserClient.class)
// 如果Feign接口包路径和引导类包路径不相同,需要单独指定
@EnableFeignClients(basePackages = "com.example.feignapi") // 目的是创建feign接口代理对象并放入spring容器
- 编写Feign的客户端
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
主要基于SpringMVC
的注解来声明远程调用的信息,比如:
- 服务名称: userservice
- 请求方式: GET
- 请求路径: /user/{id}
- 请求参数: Long id
- 返回值类型: User
- 添加Feign的超时时间
feign:
client:
config:
default:
#不设置connectTimeout会导致readTimeout设置不生效
connectTimeout: 3000
readTimeout: 6000
- 替换原先http远程调用代码
自定义配置
feign日志
- 基于配置文件修改feign的日志级别可以针对单个服务
feign:
client:
config:
userservice: # 针对某个微服务的配置
loggerLevel: FULL # 日志级别
- 针对所有服务
feign:
client:
config:
default: # 针对全局配置
loggerLevel: FULL # 日志级别
- 使用Java代码生成日志
# 声明一个类
public class DefaultFeignConfiguration {
@Bean
public Logger.Level.BASIC; // 日志级别为BASIC
}
- 如果要全局生效将其放到启动类的
@EnableFeignClients
这个注解中
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
- 如果是局部生效将其放到对应的
@FeignClient
注解中
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)
日志级别
- NONE: 不记录任何日志信息,这是默认值
- BASIC: 仅记录请求的方法,URL以及响应码和执行时间
- HEADERS: 在BASIC的基础上,额外记录了请求和响应的头信息
- FULL: 记录所有请求和响应的明细,包括头信息、请求体、元数据
自定义Feign的配置
Feign的性能优化
Feign底层发起http请求,依赖于其他的框架
Feign底层的客户端实现
- URLConnection: 默认实现,不支持连接池
- Apache HttpClient: 支持连接池
- OKHttp: 支持连接池
Feign添加HttpClient支持步骤
- 引入依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
- 配置连接池
feign:
client:
config:
default: # 全局配置
loggerLevel: BASIC # 日志级别,BASIC
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
Feign的最佳实践
- 方式一(继承): 给消费者的FeignClient和提供者的Controller定义同意的父接口作为标准
- 方式二(抽取): 将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
方式2的实现步骤
- 创建一个module,命名为
feign-api
,引入feign
的starter依赖 - 将order-service(发起Http远程调用的服务)中编写的
Client
、实体类
、日志配置类
都复制到feign-api
中 - 在order-service中引入
feign-api
的依赖 - 修改order-service中的所有与上述三个组件有关的
import
部分,改成导入feign-api
中的包 - 重启测试
- 当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法启动,此时有两种解决方案:
- 指定FeignClient所在包
@EnableFeignClients(basePackages = "com.example.feign.clients")
- 指定FeignClient字节码
@EnableFeignClients(clients = {UserClient.class})
Gateway网关
核心功能
- 身份认证和权限校验
- 服务路由和负载均衡
- 请求限流
SpringCloud中的网关实现有两种
- SpringCloudGateway: 基于
Spring5
中提供的WebFlux
,属于响应式编程的实现,具备更好的性能 - zuul: 基于
Servlet实现
,属于阻塞式编程
Gateway使用步骤
- 创建gateway服务,引入依赖
<!-- 网关依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- nacos服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 编写SpringBoot启动类
- 编写路由配置及nacos地址
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost: 8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址,不建议这样写
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
filters:
StripPrefix=1 # 去除1个前缀
default-filters: # 默认过滤器
- AddRequestHeader=authorization,2 # 所有的请求头都带有该信息
路由断言工厂Route Predicate Factory
我们在配置文件中写的断言规则只是字符串,这些字符串会被
Predicate Factory
读取并处理,转变为路由判断的条件
网关路由可以配置的内容包括:
- 路由id: 路由的唯一标识
- uri: 路由目的地,支持
lb
和http
两种 - predicates: 路由断言,判断请求是否符合要求,符合则转发到路由目的地
- fileters: 路由过滤器,处理请求或响应
Spring提供的11种基本的Predicate工厂
路由过滤器GatewayFilter
GatewayFilter是网关种提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理
Spring提供了31种GatewayFilter工厂
- 默认过滤器,对所有服务生效:
defaultFilters
全局过滤器GlobalFilter
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样
和GatewayFilter的区别
GatewayFilter
通过配置定义,处理逻辑是固定的,GlobalFilter
的逻辑是自己写代码实现的
实现步骤
- 自定义类
- 实现
GlobalFilter接口
- 添加
@Order
注解(也可以实现Ordered接口
)
@Order(-1) // 数值越小优先级越高
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取请求参数
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
// 2. 获取authorization参数
String authorication = params.getFirst("authorization");
// 3. 校验
if (java.util.Objects.equals("admin", authorication)) {
// 放行
return chain.filter(exchange);
}
// 4. 拦截
// 4.1 禁止访问,设置状态码
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// 4.2 结束处理
return exchange.getResponse().setComplete();
}
}
过滤器执行顺序
请求进入网关会碰到三类过滤器: 当前路由的过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)种,排序后依次执行每个过滤器
- 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
- GloableFilter通过实现
Ordered接口
,或者添加@Order
注解来指定order值,由我们自己决定 - 路由过滤器和defaultFilter的order由
Spring指定
,默认是按照声明顺序从1递增 - 当过滤器的order值一样时,会按照defaultFilter > 路由过滤器 > GloableFilter的顺序执行
跨域问题
域: 协议、域名、端口
三样有任意一样不一样就是跨域
- 跨域: 值浏览器针对
js
,js
从一个域加载到浏览器,但是发起ajax异步请求请求另一个域的后台,就叫跨域,浏览器默认不允许,有同源策略,为了安全,如果跨域,会报错
跨域问题解决
- 修改网关配置
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
- 通过Nginx
让orgin原来的域和异步请求要访问的域保持一致,不算跨域,然后让Nginx做代理
- 后端处理
- 前端处理