微服务笔记:第一章_微服务简介|Eureka注册中心|Nacos注册中心|Nacos配置管理|Feign|Gateway服务网关
说明:本内容整理自B站黑马程序员SpringCloud微服务课程视频及文档 >>黑马程序员SpringCloud微服务课程视频
1. 微服务简介
1.1 服务架构演变
服务架构演变: 单体架构——> 分布式架构
1、单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署
优点:
·架构简单
·部署成本低
缺点:
·耦合度高
特点:
·简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统
2、分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务
优点:
·降低服务耦合
·有利于服务升级拓展
特点:
·松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:京东、淘宝
单体架构 分布式架构
3、分布式架构的要考虑的问题——服务治理:
·服务拆诉分粒度如何?
·服务集群地址如何维护?
·服务之间如何实现远程调用?
·服务健康状态如何感知?
4、微服务是一种经过良好架构设计的分布式架构方案
·优点:
拆分粒度更小、服务更独立、耦合度更低
·缺点:
架构非常复杂,运维、监控、部署难度提高
微服务结构
5、微服务架构特征:
·单一职责: 微服务拆分粒度更小,每一个服务都对应唯一的业务能力,
做到单一职责,避免重复业务开发
·面向服务: 微服务对外暴露业务接口
·自治: 团队独立、技术独立、数据独立、部署独立
·隔离性强: 服务调用做好隔离、容错、降级,避免出现级联问题
微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。
在国内最知名的就是SpringCloud和阿里巴巴的Dubbo
微服务技术对比:
. Dubbo Springcloud SpringcloudAlibaba 注册中心 zookeeper、Redis Eureka、consul Nacos、Eureka 服务远程调用 Dubbo协议 Feign ( http协议) Dubbo、Feign 配置中心 无 SpringCloudConfig SpringcloudConfig、Nacos 服务网关 无 SpringCloudGateway、Zuul SpringcloudGateway、Zuul 服务监控和保护 dubbo-admin,功能弱 Hystrix Sentinel
从以上表格信息可以看出:
SpringcloudAlibaba兼容Dubbo架构和Springcloud架构,Nacos注册中心支持Dubbo和Feign两种远程调用
常见微服务实现场景
1.2 SpringCloud
1、SpringCloud是目前国内使用最广泛的微服务框架。
官网地址: https://spring.io/projects/spring-cloud。
2、SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,
从而提供了良好的开箱即用体验:
SpringCloud集成了各种微服务功能组件
SpringCloud与SpringBoot的版本兼容关系如下:
Release Train Boot Version 2020.0.x aka llford 2.4.x Hoxton 2.2.x,2.3.x (Starting with SR5) Greenwich 2.1.x Finchley 2.0.x Edgware 1.5.x Dalston 1.5.x
本课堂学习的版本是Hoxton.SR10,因此对应的SpringBoot版本是2.3.x版本
1.3 服务拆分
1.3.1 服务拆分原则
1、服务拆分注意事项
1.不同微服务,不要重复开发相同业务
2.做到微服务数据独立,不要访问其它微服务的数据库
3.微服务可以将自己的业务暴露为接口,供其它微服务调用
服务拆分
1.3.2 服务拆分案例
1.3.2.1 项目文件 & 数据库表
服务拆分案例——将项目拆为order-service和user-service两个单体项目
1.导入课前资料提供的工程:cloud-demo
cloud-demo.zip
2.项目结构
cloud-demo
|—— order-service(根据id查询订单)
|—— user-service(根据id查询用户)
3.将课前资料准备的sql导入数据库中:
1.创建两个DATABASE模拟两个不同的数据库
CREATE DATABASE cloud_order;
CREATE DATABASE cloud-user;
2.导入如下两个sql文件到数据库中
cloud-user.sql
cloud-order.sql
cloud-user.sql文件:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收件人',
`address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES (1, '柳岩', '湖南省衡阳市');
INSERT INTO `tb_user` VALUES (2, '文二狗', '陕西省西安市');
INSERT INTO `tb_user` VALUES (3, '华沉鱼', '湖北省十堰市');
INSERT INTO `tb_user` VALUES (4, '张必沉', '天津市');
INSERT INTO `tb_user` VALUES (5, '郑政', '辽宁省沈阳市大东区');
INSERT INTO `tb_user` VALUES (6, '范兵兵', '山东省青岛市');
SET FOREIGN_KEY_CHECKS = 1;
cloud-order.sql文件:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for tb_order
-- ----------------------------
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品名称',
`price` bigint(20) NOT NULL COMMENT '商品价格',
`num` int(10) NULL DEFAULT 0 COMMENT '商品数量',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of tb_order
-- ----------------------------
INSERT INTO `tb_order` VALUES (101, 1, 'Apple 苹果 iPhone 12 ', 699900, 1);
INSERT INTO `tb_order` VALUES (102, 2, '雅迪 yadea 新国标电动车', 209900, 1);
INSERT INTO `tb_order` VALUES (103, 3, '骆驼(CAMEL)休闲运动鞋女', 43900, 1);
INSERT INTO `tb_order` VALUES (104, 4, '小米10 双模5G 骁龙865', 359900, 1);
INSERT INTO `tb_order` VALUES (105, 5, 'OPPO Reno3 Pro 双模5G 视频双防抖', 299900, 1);
INSERT INTO `tb_order` VALUES (106, 6, '美的(Midea) 新能效 冷静星II ', 544900, 1);
INSERT INTO `tb_order` VALUES (107, 2, '西昊/SIHOO 人体工学电脑椅子', 79900, 1);
INSERT INTO `tb_order` VALUES (108, 3, '梵班(FAMDBANN)休闲男鞋', 31900, 1);
SET FOREIGN_KEY_CHECKS = 1;
1.3.2.2 项目结构 & 详细代码
4.cloud-demo项目结构 & 详细代码
cloud-demo
|—— order-service(根据id查询订单)
|—— user-service(根据id查询用户)
order-service目录结构 user-service目录结构
1.父模块cloud-demo的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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itcast.demo</groupId>
<artifactId>cloud-demo</artifactId>
<version>1.0</version>
<modules>
<module>user-service</module>
<module>order-service</module>
</modules>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.9.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR10</spring-cloud.version>
<mysql.version>5.1.47</mysql.version>
<mybatis.version>2.1.1</mybatis.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- springCloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
2.order-service模块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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>cn.itcast.demo</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>order-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.order-service模块OrderMapper类:
package cn.itcast.order.mapper;
import cn.itcast.order.pojo.Order;
import org.apache.ibatis.annotations.Select;
public interface OrderMapper {
@Select("select * from tb_order where id = #{id}")
Order findById(Long id);
}
4.order-service模块实体类Order:
package cn.itcast.order.pojo;
import lombok.Data;
@Data
public class Order {
private Long id;
private Long price;
private String name;
private Integer num;
private Long userId;
private User user;
}
5.order-service模块实体类User:
package cn.itcast.order.pojo;
import lombok.Data;
@Data
public class User {
private Long id;
private String username;
private String address;
}
6.order-service模块OrderService类:
package cn.itcast.order.service;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 4.返回
return order;
}
}
7.order-service模块OrderController类:
package cn.itcast.order.web;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
// 根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
}
8.order-service模块启动类OrderApplication:
package cn.itcast.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
9.order-service模块配置文件application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 135
driver-class-name: com.mysql.jdbc.Driver
mybatis:
type-aliases-package: cn.itcast.user.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
10.user-service模块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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>cn.itcast.demo</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
11.user-service模块UserMapper类:
package cn.itcast.user.mapper;
import cn.itcast.user.pojo.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
public interface UserMapper {
@Select("select * from tb_user where id = #{id}")
User findById(@Param("id") Long id);
}
12.user-service模块User类:
package cn.itcast.user.pojo;
import lombok.Data;
@Data
public class User {
private Long id;
private String username;
private String address;
}
13.user-service模块UserService类:
package cn.itcast.user.service;
import cn.itcast.user.mapper.UserMapper;
import cn.itcast.user.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id) {
return userMapper.findById(id);
}
}
14.user-service模块UserController类:
package cn.itcast.user.web;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 路径: /user/110
* @param id 用户id
* @return 用户
*/
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
}
15.user-service模块启动类:
package cn.itcast.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.mybatis.spring.annotation.MapperScan;
@MapperScan("cn.itcast.user.mapper")
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
16.user-service模块配置文件application.yml
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
username: root
password: 135
driver-class-name: com.mysql.jdbc.Driver
mybatis:
type-aliases-package: cn.itcast.user.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
以上两个单体项目order-service和user-service相互独立
两个单体项目之间没有关联
测试——访问以下地址查询数据:
http://localhost:8081/user/1
{
id: 1,
username: "柳岩",
address: "湖南省衡阳市"
}
http://localhost:8080/order/101
{
id: 101,
price: 699900,
name: "Apple 苹果 iPhone 12 ",
num: 1,
userId: 1,
user: null
}
1.4 RestTemplate远程调用
1.4.1 RestTemplate接口调用
1.微服务调用方式
·基于RestTemplate发起的http请求实现远程调用
·http请求做远程调用是与语言无关的调用,只要知道对方的ip、端口、接口路径、请求参数即可
2.案例:根据订单id查询订单完整信息
·访问http://localhost:8080/order/101可以发现返回的Order对象中user字段为null:
{
id: 101,
price: 699900,
name: "Apple 苹果 iPhone 12 ",
num: 1,
userId: 1,
user: null
}
3.需求:根据订单id查询订单的同时,把订单所属的用户信息一起返回
4.实现:
1.思路:订单模块调用用户模块获取信息——订单模块向用户模块发送http请求
2.步骤一:注册RestTemplate
在order-service的OrderApplication中注册RestTemplate
3.步骤二:服务远程调用RestTemplate
修改order-service中的OrderService的queryOrderById方法
订单模块调用用户模块 订单模块向用户模块发送http请求
改造一:在order-service的OrderApplication中注入RestTemplate对象:
package cn.itcast.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
/* 在order-service的OrderApplication中注册RestTemplate
* 创建RestTemplate并注入spring容器
*/
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
改造二:修改order-service中的OrderService的queryOrderById方法:
package cn.itcast.order.service;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.利用RestTemplate发起http请求,查询用户
// 2.1 url路径
String url = "http://localhost:8081/user/" + order.getUserId();
// 2.2 发送http请求,实现远程调用
User user = restTemplate.getForObject(url, User.class);
// 3.封装user到order
order.setUser(user);
// 4.返回
return order;
}
}
重新访问order-service服务http://localhost:8080/order/101 , user字段不为null
{
id: 101,
price: 699900,
name: "Apple 苹果 iPhone 12 ",
num: 1,
userId: 1,
user: {
id: 1,
username: "柳岩",
address: "湖南省衡阳市"
}
}
1.4.2 提供者与消费者
1.服务调用关系
·服务提供者:暴露接口给其它微服务调用
·服务消费者:调用其它微服务提供的接口
2.思考:服务A调用服务B,服务B调用服务C,那么服务B是什么角色?
· 对于A调用B的业务而言:A是服务消费者,B是服务提供者
· 对于B调用C的业务而言:B是服务消费者,C是服务提供者
· 因此,服务B既可以是服务提供者,也可以是服务消费者。
· 提供者与消费者角色其实是相对的
· 一个服务可以同时是服务提供者和服务消费者
2. Eureka注册中心
2.1 Eureka简介 & 原理
1. 服务调用出现的问题:
假如我们的服务提供者user-service部署了多个实例,如图:
user-service部署了多个实例
2.上述场景引出的服务调用问题:
·服务消费者该如何获取服务提供者的地址信息?
·如果有多个服务提供者,消费者该如何选择?
·消费者如何得知服务提供者的健康状态?
3.Eureka对以上问题给出的解决方案:
1.消费者该如何获取服务提供者具体信息?
·服务提供者启动时向eureka注册自己的信息,这叫作服务注册
·eureka-server保存服务名称到服务实例地址列表的映射关系
·消费者根据服务名称向eureka拉取提供者信息,这叫作服务发现或服务拉取
2.如果有多个服务提供者,消费者该如何选择?
·服务消费者利用负载均衡算法,从服务列表中挑选一个
3.消费者如何感知服务提供者健康状态?
·服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
·eureka会更新记录服务列表信息,心跳不正常会被剔除
·消费者可以拉取到最新的信息
注意:
一个微服务,既可以是服务提供者,又可以是服务消费者,因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端
Eureka注册中心原理
4.在Eureka架构中,微服务角色有两类:
1.EurekaServer: 服务端,注册中心
·记录服务信息
·心跳监控
2.EurekaClient: 客户端
·Provider: 服务提供者,例如案例中的user-service
·注册自己的信息到EurekaServer
·每隔 30秒向EurekaServer发送心跳
·consumer:服务消费者,例如案例中的order-service
·根据服务名称从EurekaServer拉取服务列表
·基于服务列表做负载均衡,选中一个微服务后发起远程调用
5.Eureka改造上述微服务调用:
接下来动手实践的步骤包括:
Eureka改造上述微服务调用的步骤
2.2 搭建EurekaServer
搭建EurekaServer服务步骤如下:
1.创建eureka-server微服务项目:
创建eureka-server微服务项目
2.引入spring-cloud-starter-netflix-eureka-server的依赖
<!--eureka服务端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<!--version版本信息已经在父工程中引入-->
</dependency>
3.编写启动类,添加@EnableEurekaServer注解:
package cn.itcaet.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
4.添加application.yml文件,编写下面的配置:
server:
port: 10086 #服务端口
spring:
application:
name: eurekaserver #eureka的服务信息
#配置作用:将euteka服务注册到euteka上
#euteka在启动时会将自己也注册到注册中心
eureka:
client:
service-url: #eureka的地址信息
defaultZone: http://127.0.0.1:10086/eureka
5.启动eureka-server微服务,然后在浏览器访问:http://127.0.0.1:10086
看到下面结果应该是成功了:
eureka-server微服务注册、启动成功
2.3 Eureka服务注册
接下来要将user-service、order-service都注册到eureka
2.3.1 注册user-service
将user-service服务注册到EurekaServer步骤如下:
1.在user-service项目引入spring-cloud-starter-netflix-eureka-client的依赖
<!--eureka客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.在application.yml文件,编写(添加)下面的配置:
spring:
application:
name: userservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
3.另外可以将user-service多次启动, 模拟多实例部署,但为了避免端口冲突,需要修改端口设置
端口设置: -Dserver.port=端口号
模拟多实例部署 eureka注册中心显示实例
2.3.2 注册order-service
order-service虽然是消费者,但与user-service一样都是eureka的client端,同样可以实现服务注册:
1.在order-service项目引入spring-cloud-starter-netflix-eureka-client的依赖
<!--eureka客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.在application.yml文件,编写(添加)下面的配置:
spring:
application:
name: orderservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
服务注册总结:
1.服务注册步骤:
1.引入eureka-client依赖
2.在application.yml中配置eureka地址
2.无论是消费者还是提供者,引入eureka-client依赖,知道eureka地址后,都可以完成服务注册
2.4 eureka服务发现(服务拉取)
2.4.1 消费者order-service的配置
之前说过,服务发现、服务注册统一都封装在eureka-client依赖,因此这一步与服务注册时一致
服务发现与服务注册步骤一致(如果消费端已经完成服务注册,则下列步骤跳过):
1.引入eureka-client依赖
2.在application.yml中配置eureka地址
order-service服务发现步骤:
1.在order-service项目引入spring-cloud-starter-netflix-eureka-client的依赖
<!--eureka客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.在application.yml文件,编写(添加)下面的配置:
spring:
application:
name: orderservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
2.4.2 在order-service完成服务拉取和负载均衡
最后,要去eureka-server中拉取user-service服务的实例列表,并且实现负载均衡
服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡
1.修改OrderService的代码,修改访问的url路径,用服务名代替ip、端口:
String urL = "http://userservice/user/" + order. getUserId();
完整代码:
package cn.itcast.order.service;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.利用RestTemplate发起http请求,查询用户
// 2.1 url路径
String urL = "http://userservice/user/" + order. getUserId();
// 2.2 发送http请求,实现远程调用
User user = restTemplate.getForObject(urL, User.class);
// 3.封装user到order
order.setUser(user);
// 4.返回
return order;
}
}
2.在order-service项 目的启动类OrderApplication中的RestTemplate添加负载均衡注解:
@Bean
@LoadBaLanced
public RestTemplate restTemplate() {
return new RestTempLate();
}
完整代码:
package cn.itcast.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
2.4.2 Eureka注册中心总结
Eureka注册中心使用步骤:
1.搭建EurekaServer
引入eureka-server依赖
添加@EnableEurekaServer注解
在application.yml中配置eureka地址
2.服务注册
引入eureka-client依赖
在application.yml中配置eureka地址
3.服务发现
引入eureka-client依赖
在application.yml中配置eureka地址
2.5 Ribbon负载均衡
2.5.1 负载均衡流程
1.上一节中添加了@LoadBalanced注解,即可实现负载均衡功能,这是什么原理呢?
SpringCloud底层其实是利用了一个名为Ribbon的组件,来实现负载均衡功能的
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
被@LoadBalanced标记的RestTemplate发起的请求要被Ribbon拦截和处理
2.那么发出的请求明明是http://userservice/user/1,怎么变成了http://localhost:8081?
负载均衡流程
2.5.2 源码跟踪 & 负载均衡原理
为什么只输入service名称就可以访问了呢?之前还要获取ip和端口。
1.LoadBalancerInterceptor根据service的名称,获取到了服务实例的ip和端口。
2.LoadBalancerInterceptor会对RestTemplate的请求进行拦截,然后从Eureka根据服务id
获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务id
源码跟踪:
访问http://userservice/user/1,进行源码跟踪:
1. LoadBalancerIntercepor
LoadBalancerIntercepor
可以看到这里的intercept方法,拦截了用户的HttpRequest请求,然后做了几件事:
·request.getURI():获取请求uri,本例中就是 http://user-service/user/8
·originalUri.getHost():获取uri路径的主机名,其实就是服务id,user-service
·this.loadBalancer.execute():处理服务id,和用户请求
这里的this.loadBalancer是LoadBalancerClient类型,我们继续跟入
2. LoadBalancerClient
继续跟入LoadBalancerClient.execute方法:
LoadBalancerClient的execute方法
LoadBalancerClient的execute方法代码是这样的:
·getLoadBalancer(serviceId):
根据服务id获取ILoadBalancer,而ILoadBalancer会拿着服务id去eureka中获取服务列表并保存起来
·getServer(loadBalancer):
利用内置的负载均衡算法,从服务列表中选择一个。本例中,可以看到获取了8082端口的服务
放行后,再次访问并跟踪,发现获取的是8081,果然实现了负载均衡:
execute方法
在刚才的代码中,可以看到获取服务使通过一个getServer方法来做负载均衡,继续跟入:
getServer方法
继续跟踪源码chooseServer方法,发现这么一段代码:
chooseServer方法
看看这个rule是谁:
这里的IRule rule默认值是一个RoundRobinRule,看类的介绍:
这不就是轮询的意思嘛。到这里,整个负载均衡的流程就清楚了.
这个IRule决定了负载均衡的策略,后面会详细讲解
2.5.3 Ribbon负载均衡 流程&原理 总结
SpringCloudRibbon的底层采用了一个拦截器,拦截了RestTemplate发出的请求,对地址做了修改
基本流程如下:
1.拦截RestTemplate请求http://userservice/user/1
2.RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service
3.DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表
4.eureka返回列表,localhost:8081、localhost:8082
5.IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081
6.RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求
用一幅图来总结一下:
Ribbon负载均衡原理
2.6 负载均衡策略IRule
2.6.1 IRule接口 & 负载均衡策略
1.IRule接口决定了负载均衡的策略
2.IRule的接口来定义了Ribbon的负载均衡规则
3.IRule的每一个子接口都是一种规则
IRule的每一个子接口都是一种规则
负载均衡策略
内置负载均衡规则类 规则描述 RoundRobinRule 简单轮询 服务列表来选择服务器。它是Ribbon默认的负载均衡规则 AvailabilityFilteringRule 对以下两种服务器进行忽略:(1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加(2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的<clientName>.<clientConfigNameSpace>.ActiveConnect ionsLimit属性进行配置 WeightedResponseTimeRule 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择 ZoneAvoidanceRule 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询 BestAvailableRule 忽略那些短路的服务器,并选择并发数较低的服务器 RandomRule 随机选择一个可用的服务器 RetryRule 重试机制的选择逻辑
2.6.2 自定义负载均衡策略
通过定义IRule实现可以修改负载均衡规则,有两种方式:
1.代码方式: (此种方式是“全局的”,访问任何服务都生效)
在order-service中的OrderApplication类中,定义一个新的IRule:
@Bean
public IRule randomRule(){
return new RandomRule();
//注入RandomRule()作为负载均衡规则
}
注意:
此种方式是“全局的”,通过@Bean注入特定的IRule规则后,
不管是访问userservice服务还是xxx-service服务都遵循 @Bean注入的规则
完整代码:
package cn.itcast.order;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
@Bean
public IRule randomRule(){
return new RandomRule();
}
}
多次访问http://localhost:8080/order/101 (102、103、104...)
发现 localhost:userservice:8081 , localhost:userservice:8082 两个userservice实例被随机调用
2.配置文件方式: (此种方式是针对某个微服务而言)
在order-service的application.yml文件中, 添加新的配置也可以修改规则:
将访问userservice实例的负载均衡规则改为随机:
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
2.6.3 Ribbon饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时, 通过下面配置开启饥饿加载:
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients: userservice #指定对userservice这个服务进行饥饿加载
2.6.4 Ribbon负载均衡总结
1. Ribbon负载均衡规则
·规则接口是IRule
·默认实现是ZoneAvoidanceRule,根据zone选择服务列表,然后轮询
2.负载均衡自定义方式
·代码方式:配置灵活,但修改时需要重新打包发布
·配置方式:直观,方便,无需重新打包发布,但是无法做全局配置
3.饥饿加载
·开启饥饿加载
·指定饥饿加载的微服务名称
3. Nacos注册中心
3.1 Nacos简介 & Nacos安装
1.Nacos简介
·Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件
·相比Eureka而言功能更加丰富,在国内受欢迎程度较高
·Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台
Nacos安装指南
1.Windows安装(开发阶段采用单机安装即可)
1.1.下载安装包(在Nacos的GitHub页面可以下载编译好的Nacos服务端或者源代码)
·GitHub主页:https://github.com/alibaba/nacos
·GitHub的Release下载页:https://github.com/alibaba/nacos/releases
本课程采用1.4.1.版本的Nacos,课前资料已经准备了安装包
windows版本使用nacos-server-1.4.1.zip包即可
1.2.解压
将这个包解压到任意非中文目录下
目录说明:
- bin:启动脚本
- conf:配置文件
1.3.端口配置
Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程
如果无法关闭占用8848端口的进程,也可以进入nacos的conf目录
再修改配置文件application.properties中的端口:修改其中的内容:server.port=8848
1.4.启动
启动非常简单,进入bin目录,
然后在cmd窗口中执行windows命令即可:startup.cmd -m standalone (单机启动)
1.5.访问
在浏览器输入地址:http://127.0.0.1:8848/nacos
默认的账号和密码都是nacos,进入后:
2.Linux安装
Linux或者Mac安装方式与Windows类似
2.1.安装JDK
Nacos依赖于JDK运行,索引Linux上也需要安装JDK才行
上传jdk安装包,上传到某个目录,例如:/usr/local/
然后解压缩:tar -xvf jdk-8u144-linux-x64.tar.gz
然后重命名为java
配置环境变量:
export JAVA_HOME=/usr/local/java
export PATH=$PATH:$JAVA_HOME/bin
设置环境变量:
source /etc/profile
2.2.上传安装包
nacos-server-1.4.1.tar.gz
也可以直接使用课前资料中的tar.gz:
上传到Linux服务器的某个目录,例如/usr/local/src目录下
2.3.解压
命令解压缩安装包:
tar -xvf nacos-server-1.4.1.tar.gz
然后删除安装包:
rm -rf nacos-server-1.4.1.tar.gz
2.4.端口配置(与windows中类似)
2.5.启动
在nacos/bin目录中,输入命令启动Nacos
sh startup.sh -m standalone
3.Nacos的依赖
父工程:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
客户端:
<!-- nacos客户端依赖包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
3.2.Nacos快速入门
1.在cloud-demo父工程中添加spring-cloud-alibaba的管理依赖:
<!--nacos父工程管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
2.注释掉order-service和user-service中原有的eureka依赖
3.添加nacos的客户端依赖:
<!-- nacos客户端依赖包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
4.修改user-service和order-service中的application. yml文件,注释eureka地址,添加nacos地址:
spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务端地址
5.启动服务并测试:
服务已经注册到nacos
访问http://localhost:8080/order/101成功返回数据
说明user-service和order-service服务注册和服务拉取已经实现
6.总结:
6.1. Nacos服务搭建
·下载安装包
·解压
·在bin目录下运行指令: startup.cmd -m standalone
6.2. Nacos服务注册或发现
·引入nacos.discovery依赖
·配置nacos地址spring.cloud.nacos.server-addr
3.3 Nacos服务分级存储模型
3.3.1 Nacos服务分级存储模型
1. Nacos服务分级存储模型
·一级是服务,例如userservice
·二级是集群,例如杭州或上海
·三级是实例,例如杭洲机房的某台部署了userservice的服务器
Nacos服务分级存储模型
3.3.2 服务跨集群调用 & 同集群优先的负载均衡
2.服务跨集群调用问题
·服务调用尽可能选择本地集群的服务,跨集群调用延迟较高
·本地集群不可访问时,再去访问其它集群
服务跨集群调用问题
3.如何设置实例的集群属性(此处给user-service配置集群)
3.1 修改application.yml文件,添加spring.cloud.nacos.discovery.cluster-name属性:
spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务端地址
discovery:
cluster-name: HZ #配置集群名称,也就是机房位置,例如: HZ,杭州
3.2.在Nacos控制台可以看到集群变化
user-service集群变化
4.同集群优先的负载均衡
·默认的ZoneAvoidanceRule并不能实现根据同集群优先来实现负载均衡
·因此Nacos中提供了一个NacosRule的实现,可以优先从同集群中挑选实例
4.1给order-service配置集群信息
修改order-service的application.yml文件,添加集群配置:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称
4.2修改负载均衡规则
修改order-service的application.yml文件,修改负载均衡规则:
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
4.3 测试:
访问http://localhost:8080/order/103
发现只有HZ集群下的userservice被调用
NacosRule负载均衡策略:
·多个服务中优先选择本地集群,本地集群的多个实例则是随机调用
·本地集群没有实例时才会跨集群访问
·优先选择同集群服务实例列表
·本地集群找不到提供者,才去其它集群寻找,并且会报警告
·确定了可用实例列表后,再采用随机负载均衡挑选实例
3.3.3 根据权重负载均衡
实际部署中会出现这样的场景:
·服务器设备性能有差异,部分实例所在机器性能较好,另一些较差
·我们希望性能好的机器承担更多的用户请求
·Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高
1.在Nacos控制台可以设置实例的权重值, 首先选中实例后面的编辑按钮
2.将权重设置为0.1, 测试可以发现8081被访问到的频率大大降低
3.注意:如果权重修改为0,则该实例永远不会被访问
1.实例的权重控制
①Nacos控制台可以设置实例的权重值,0~1之间
②同集群内的多个实例,权重越高被访问的频率越高
③权重设置为0则完全不会被访问
点击实例后的编辑按钮 设置权重的大小
3.4 Nacos环境隔离
1.环境隔离——namespace
1.Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离
2.Nacos提供了namespace来实现环境隔离功能
3.nacos中可以有多个namespace,用来隔离不同环境
4.namespace下可以有group、service等
5.不同namespace之间相互隔离,例如不同namespace的服务互相不可见
2.创建命名空间并将order-service添加到命名空间:
1.在Nacos控制台创建namespace,然后填写一个新的命名空间信息
2.保存后在控制台查看命名空间的id,后续会用到这个id
3.修改order-service的application.yml,添加namespace
spring:
application:
name: orderservice
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ #集群名称
namespace: 01ce67dc-bcc3-4589-ab9b-c52b1d660803 #命名空间,填id
1.创建namespace 2.填写namespace信息 3.查看命名空间的id 4.修改配置
5.重启order-service后,再来查看控制台:
public命名空间 dev命名空间
6.此时访问order-service,因为namespace不同,会导致找不到userservice,控制台报错:
3. Nacos环境隔离总结
1.namespace用来做环境隔离
2.每个namespace都有唯一id
3.不同namespace下的服务不可见
3.5 Nacos原理 & Nacos与eureka对比
1.Nacos的服务实例分为两种类型:
1.临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型
2.非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例
3.nacos将服务提供者分为临时实例和非临时实例
4.临时实例宕机时,会从nacos的服务列表中剔除,而非临时实例则不会
2.配置一个服务实例为永久实例:
spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为非临时实例
3.Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异:
Nacos注册中心原理
4.Nacos与eureka的共同点
1.都支持服务注册和服务拉取
2.都支持服务提供者心跳方式做健康检测
5. Nacos 与Eureka的区别
1.Nacos支持服务端主动检测提供者状态:临时实例采
用心跳模式,非临时实例采用主动检测模式
2.临时实例心跳不正常会被剔除,非临时实例则不会被剔除
3.Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
4.Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式; Eureka采用AP方式
4. Nacos配置管理
Nacos除了可以做注册中心,同样可以做配置管理来使用
4.1 统一配置管理
1.统一配置管理
1.当微服务部署的实例越来越多,逐个修改微服务配置就会繁琐,而且容易出错
2.nacos提供了统一配置管理方案,可以集中管理所有实例的配置
3.Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新
4.注意:
项目的核心配置,需要热更新的配置才有放到nacos管理的必要
基本不会变更的一些配置还是保存在微服务本地比较好
Nacos配置管理
2.微服务从nacos拉取配置
1.微服务要拉取nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动
2.但如果尚未读取application.yml,又如何得知nacos地址呢?
因此spring引入了一种新的配置文件:bootstrap.yaml文件
bootstrap.yaml文件会在application.yml之前被读取,流程如下:
微服务从nacos拉取配置的流程
2.将配置交给Nacos管理的步骤(此处为user-service添加nacos配置):
2.1 在Nacos中添加配置文件
1.在Nacos中添加配置文件
2.填写配置文件(图1)
2.2 在微服务中引入nacos的config依赖(此处在user-service服务中引入nacos-config的客户端依赖):
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
2.3 在微服务中添加bootstrap.yml,配置nacos地址、当前环境、服务名称、文件后缀
这些决定了程序启动时去nacos读取哪个文件,配置的信息要与nacos控制台中的对应(图1)
bootstrap.yml是引导文件,优先级高于application.yml
此处在userservice中的resource目录添加一个bootstrap.yml文件:
spring:
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
作为文件id,来读取配置。本例中,就是去读取userservice-dev.yaml:
根据bootstrap.yml文件确定读取nacos中的哪个配置文件:
bootstrap.yml
2.4 测试:读取nacos控制台userservice-dev.yaml的配置
在user-service中将pattern.dateformat这个属性注入到UserController中做测试:
package cn.itcast.user.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("/user")
public class UserController {
// 注入nacos中的配置属性
@Value("${pattern.dateformat}")
private String dateformat;
// 编写controller,通过日期格式化器来格式化现在时间并返回
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
访问http://localhost:8081/user/now 返回结果2022-10-01 23:13:31
4.2 配置热更新
1.配置热更新:
Nacos中的配置文件变更后,微服务无需重启就可以感知,实现配置自动刷新
2.配置热更新的两种实现方式:
·方式一:在@Value注入的变量所在类上添加注解@RefreshScope
package cn.itcast.user.web;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
·方式二(推荐):使用@ConfigurationProperties注解
1.使用@ConfigurationProperties注解代替@Value注解
2.在user-service服务中,添加一个类,读取patterrn.dateformat属性:
package cn.itcast.user.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
}
3.在UserController中使用PatternProperties这个类代替@Value:
package cn.itcast.user.web;
import cn.itcast.user.config.PatternProperties;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private PatternProperties patternProperties;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
}
}
配置类上方报错:
配置类上方报错
解决报错——添加如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
总结:
Nacos配置更改后,微服务可以实现热更新,方式:
方式一:通过@Value注解注入,结合@RefreshScope来刷新
方式二:通过@ConfigurationProperties注入,自动刷新
注意事项:
·不是所有的配置都适合放到配置中心,维护起来比较麻烦
·建议将一些关键参数,需要运行时调整的参数放到nacos配置中心,一般都是自定义配置
4.3 配置共享
1.微服务启动时会从nacos读取多个配置文件:
第一种文件:[spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml
第二种文件:[spring.application.name].yaml,例如:userservice.yaml
无论profile如何变化,[spring.application.name].yaml这个文件一定会加载
因此多环境共享配置可以写入[spring.application.name].yaml文件
2.读取共享配置文件测试:
2.1 在nacos新建userservice.yaml配置文件
添加userservice.yaml
2.2.在user-service服务中,修改PatternProperties类,读取新添加的属性:
package cn.itcast.user.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
private String envSharedValue;
}
2.3.在user-service服务中,修改UserController,添加一个方法:
package cn.itcast.user.web;
import cn.itcast.user.config.PatternProperties;
import cn.itcast.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private PatternProperties patternProperties;
@GetMapping("prop")
public PatternProperties properties(){
return patternProperties;
}
}
访问http://localhost:8081/user/prop,返回以下结果:
{
dateformat: "yyyy年MM月dd HH:mm:ss",
envSharedValue: "环境共享属性值"
}
可以看到,当前dev环境读到了userservice.yaml配置文件和userservice-dev.yaml配置文件
3.继续测试:
3.1修改UserApplication2这个启动项,改变其profile值:
修改useservice服务配置 改成test环境
3.2访问http://localhost:8082/user/prop,返回以下结果:
{
dateformat: null,
envSharedValue: "环境共享属性值"
}
3.3 可以看不管是dev、还是test环境,都能读取envSharedValue这个属性值
由此证明配置文件userservice.yaml是被不同环境共享的
4.多种配置文件的优先级
1.当nacos、服务本地同时出现相同属性时 ,优先级有高低之分:
优先级
2.验证:
1.在本地application.yml文件中添加属性:
pattern:
name: 本地环境local
2.访问http://localhost:8081/user/prop 返回结果
{
dateformat: "yyyy年MM月dd HH:mm:ss",
envSharedValue: "环境共享属性值",
name: "本地环境local"
}
3.在nacos配置管理的userservice.yaml文件中添加属性:
pattern:
envSharedValue: 环境共享属性值
name: nacos环境共享属性default
4.访问http://localhost:8081/user/prop,返回如下结果:
{
dateformat: "yyyy年MM月dd HH:mm:ss",
envSharedValue: "环境共享属性值",
name: "nacos环境共享属性default"
}
4.4 Nacos集群搭建
Nacos生产环境下一定要部署为集群状态,部署方式参考课前资料中的文档:
集群搭建步骤:
1.搭建MySQL集群并初始化数据库表
2.下载解压nacos
3.修改集群配置(节点信息)、数据库配置
4.分别启动多个nacos节点nginx反向代理
5. http客户端Feign
5.1 Feign替代RestTemplate
1.RestTemplate方式调用存在的问题:
1.以前利用RestTemplate发起远程调用的代码:
String url = "http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
2.存在下面的问题:
·代码可读性差,编程体验不统一
·参数复杂URL难以维护
2.Feign的介绍
Feign是一个声明式的http客户端,官方地址: https://github.com/OpenFeign/feign
官网介绍:Feign makes writing java http clients easier
其作用就是可以优雅的实现http请求的发送,解决上面提到的问题
3.定义和使用Feign客户端,使用Feign的步骤如下:
3.1.引入依赖:
<!--feign客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3.2.在order-service的启动类添加注解@EnableFeignClients开启Feign的功能:
package cn.itcast.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
3.3.编写Feign的客户端
在order-service中新建一个接口,内容如下:
package cn.itcast.order.client;
import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
Feign的声明主要是基于SpringMVC的注解来声明远程调用的信息,比如:
·服务名称:userservice
·请求方式:GET
·请求路径:/user/{id}
·请求参数:Long id
·返回值类型:User
3.4.修改order-service中的OrderService类中的queryOrderById方法
1.使用Feign客户端代替RestTemplate
package cn.itcast.order.service;
import cn.itcast.order.clients.UserClient;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.用Feign远程调用
User user = userClient.findById(order.getUserId());
// 3.封装user到Order
order.setUser(user);
// 4.返回
return order;
}
}
2.user-service的UserService类(无改动,仅作回顾)
package cn.itcast.user.service;
import cn.itcast.user.mapper.UserMapper;
import cn.itcast.user.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id) {
return userMapper.findById(id);
}
}
3.user-service的UserController类(无改动,仅作回顾)
package cn.itcast.user.web;
import cn.itcast.user.config.PatternProperties;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
}
访问http://localhost:8080/order/102,返回如下结果:
{
id: 102,
price: 209900,
name: "雅迪 yadea 新国标电动车",
num: 1,
userId: 2,
user: {
id: 2,
username: "文二狗",
address: "陕西省西安市"
}
}
注:UserClient接口和UserController中的访问接口定义要对应,否则出错(方法名可以不同)
这两个地方声明的请求方式、请求路径、请求参数、返回值等都要一致(5.4中会讲解优化)
feign客户端 UserController
3.5总结:Feign的使用步骤
1.引入依赖
2.添加@EnableFeignClients注解
3.编写FeignClient接口
4.使用FeignClient中定义的方法代替RestTemplate
5.2 Feign的自定义配置
5.2.1 自定义配置类型
Feign可以支持很多的自定义配置,如下表所示:
类型 作用 说明 feign.Logger.Level 修改日志级别 包含四种不同的级别:NONE、BASIC、HEADERS、FULL feign.codec.Decoder 响应结果的解析器 http远程调用的结果做解析,例如解析json字符串为java对象 feign.codec.Encoder 请求参数编码 将请求参数编码,便于通过http请求发送 feign. Contract 支持的注解格式 默认是SpringMVC的注解 feign. Retryer 失败重试机制 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试
一般情况下,默认值就能满足使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可
5.2.2 自定义配置的两种方式
5.2.2.1 前置:Feign的日志级别
1.Feign的自定义配置有两种方式,下面以日志为例来演示如何自定义配置
2.在此先说一下Feign的日志级别,日志的级别分为四种:
1.NONE:不记录任何日志信息,这是默认值
2.BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
3.HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
4.FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据
5.2.2.2 方式一:配置文件方式
1.基于配置文件修改feign的日志级别可以针对单个服务:
feign:
client:
config:
userservice: # 针对某个微服务的配置
loggerLevel: FULL # 日志级别
2.基于配置文件修改feign的日志级别也可以针对所有服务:
feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别
测试:
选择全局生效或局部生效,启动orderservice和userservice,先清空orderservice的日志,
再访问http://localhost:8080/order/104,在orderservice中可以看到如下日志:
orderservice控制台中的日志
5.2.2.3 方式二:Java代码方式
1.基于Java代码来修改日志级别:
在order-service中服务中声明一个类,然后声明一个Logger.Level的对象:
package cn.itcast.order.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfiguration {
@Bean
public Logger.Level logLevel(){
// 日志级别为BASIC
return Logger.Level.BASIC;
}
}
2.如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)
3.如果是局部生效,则把它放到对应的@FeignClient这个注解中:
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class)
完整示例(日志仅对userservice生效):
package cn.itcast.order.clients;
import cn.itcast.order.config.DefaultFeignConfiguration;
import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
测试:
选择全局生效或局部生效,启动orderservice和userservice,先清空orderservice的日志,
再访问http://localhost:8080/order/104,在orderservice中可以看到如下日志:
orderservice控制台中的日志
5.2.2.4 总结
Feign的日志配置:
1.方式一是配置文件:
feign.client.config.xxx.loggerLevel
·如果xxx是default则代表全局
·如果xxx是服务名称,例如userservice则代表某服务
2.方式二是java代码配置Logger.Level这个Bean
·如果在@EnableFeignClients注解声明则代表全局
·如果在@FeignClient注解中声明则代表某服务
5.3 Feign的性能优化
5.3.1 性能优化的手段
1.Feign底层的客户端实现:
1.URLConnection:默认实现,不支持连接池
2.Apache HttpClient:支持连接池
3.OKHttp:支持连接池
4.因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection
2.优化Feign性能的手段主要包括:
1.使用连接池代替默认的URLConnection
2.日志级别,最好用basic或none(上面讲过,此处略过,后面仅演示前一种优化方式)
5.3.2 优化手段一:用连接池代替URLConnection
注:下面用Apache的HttpClient进行演示
1.引入依赖:
<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
2.配置连接池:在order-service的application.yml中添加配置:
feign:
client:
config:
default: # default全局的配置
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 单个请求路径的最大连接数
注:真实案例中要做压测来确定max-connections和max-connections-per-route的具体值
5.4 feign的最佳实践——实际开发中较优雅的写法
所谓最佳实践,就是使用过程中总结的经验,最好的一种使用方式
5.1中的UserClient接口和UserController中的访问接口定义要对应,否则出错(方法名可以不同)
这两个地方声明的请求方式、请求路径、请求参数、返回值等都要一致
feign客户端 UserController
针对5.1中的问题,如何更好地实现这两个地方的统一呢?下面给出两种优化方式:
方式一(继承)∶
1.给消费者的FeignClient和提供者的controller定义统一的父接口作为标准
2.定义一个API接口,利用定义方法,并基于SpringMVC注解做声明
3.Feign客户端和Controller都集成该接口
通过继承来实现统一
方式一优点:
·简单
·实现了代码共享
方式一缺点:
·服务提供方、服务消费方紧耦合
·参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解
方式二(抽取)∶
1.将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中
2.提供给所有消费者使用,消费者(order-service)通过引入依赖实现对提供者的远程调用
3.例如:
将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用
改造前的结构 改造后的结构
Feign的最佳实践:
1.让controller和FeignClient继承同一接口
2.将FeignClient、POJO、Feign的默认配置都定义到一个项目中,供所有消费者使用
方式二抽取FeignClient的步骤如下
1.首先创建一个module,命名为feign-api,然后引入feign的starter依赖
创建新模块 项目结构
<!--引入feign的starter依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
调整后的项目结构:
3.在order-service中引入feign-api的依赖
<!--引入feign的统一api -->
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
4.修改order-service中的所有与上述三个组件有关的import部分,改成导入feign-api中的包
5.重启order-service进行测试
重启测试报错
这是因为UserClient现在在cn.itcast.feign.clients包下,而order-service的@EnableFeignClients注解
是在cn.itcast.order包下,不在同一个包,无法扫描到UserClient
*解决扫描包问题
方式一:
指定Feign应该扫描的包:
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
方式二:
指定需要加载的Client接口:
@EnableFeignClients(clients = {UserClient.class})
解决报错:在orderservice启动类上添加@EnableFeignClients(clients = {UserClient.class})
package cn.itcast.order;
import cn.itcast.feign.clients.UserClient;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = {UserClient.class})
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
6.Gateway服务网关
6.1 简介 & 网关功能
1.Spring Cloud Gateway 是 Spring Cloud 的一个全新项目
2.该项目是基于Spring 5.0,Spring Boot 2.0和Project Reactor等响应式编程和事件流技术开发的网关
3.它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式
4.网关的技术实现,在SpringCloud中网关的实现包括两种:
·gateway
·zuul
1.Zuul是基于Servlet的实现,属于阻塞式编程
2.SpringCloudGateway则是基于Spring5中提供的WebFlux
属于响应式编程的实现,具备更好的性能
5.网关的功能:
1.身份认证和权限校验:
·网关作为微服务入口,需要校验用户是否有请求资格,如果没有则进行拦截
2.服务路由、负载均衡:
·一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,
·这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡
3.请求限流:
·当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大
网关架构图
6.2 gateway快速入门
下面搭建网关服务,在网关里实现请求路由功能 ,搭建步骤:
1.创建新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖:
注:引入nacos依赖原因——网关本身也是微服务,需要注册到nacos
创建服务 添加启动类
<!--网关依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖——网关本身也是微服务,需要注册到nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2.在application.yml中编写路由配置及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就是LoadBalance负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 路径断言,判断路径是否是以/user开头,是则符合
- id: order-service
uri: lb://orderservice # 将请求路由到订单服务
predicates:
- Path=/order/**
3.测试
访问http://localhost:10010/user/2,返回
{
id: 2,
username: "文二狗",
address: "陕西省西安市"
}
访问http://localhost:10010/order/102,返回
{
id: 102,
price: 209900,
name: "雅迪 yadea 新国标电动车",
num: 1,
userId: 2,
user: {
id: 2,
username: "文二狗",
address: "陕西省西安市"
}
}
以上例子的网关路由流程:
网关路由的流程图
总结:
1.网关搭建步骤:
1.创建项目,引入nacos服务发现和gateway依赖
2.配置application.yml,包括服务基本信息、nacos地址、路由
2.路由配置包括:
1.路由id:路由的唯一标示
2.路由目标(uri):
·路由的目标地址,支持lb和http两种,http代表固定地址,lb代表根据服务名负载均衡
3.路由断言( predicates) :判断请求是否符合要求,符合则转发到路由目的地
4.路由过滤器( filters ) :处理请求或响应
6.3 路由断言工厂(Route Predicate Factory)
1.路由断言工厂作用:
·配置文件中断言规则只是字符串,这些字符串会被Predicate Factory(断言工厂)读取
·断言工厂读取到用户配置的断言规则,解析成对应的判断条件,对请求做出判断
2.例如Path=/user/**是按照路径匹配,这个规则是由
org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的
3.SpringCloudGateway中有十几个断言工厂,可以查看官方文档:
·spring cloud Gateway官方文档:
·地址https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#shortcut-configuration
常见断言工厂:
名称 说明 示例 After 是某个时间点后的请求 - After=2037-01-20T17:42:47.789-07:00[America/Denver] Before 是某个时间点之前的请求 - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] Between 是某两个时间点之前的请求 - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] Cookie 请求必须包含某些cookie - Cookie=chocolate, ch.p Header 请求必须包含某些header - Header=X-Request-Id, \d+ Host 请求必须是访问某个host(域名) - Host=.somehost.org, .anotherhost.org Method 请求方式必须是指定方式 - Method=GET,POST Path 请求路径必须符合指定规则 - Path=/red/{segment},/blue/** Query 请求参数必须包含指定参数 - Query=name, Jack或者- Query=name RemoteAddr 请求者的ip必须是指定范围 - RemoteAddr=192.168.1.1/24 Weight 权重处理
6.4 路由过滤器(GatewayFilter)
1.GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:
路由过滤器
2.Spring提供了31种不同的路由过滤器工厂。例如:
名称 说明 AddRequestHeader 给当前请求添加一个请求头 RemoveRequestHeader 移除请求中的一个请求头 AddResponseHeader 给响应结果中添加一个响应头 RemoveResponseHeader 从响应结果中移除有一个响应头 RequestRateLimiter 限制请求的流量
3.路由过滤器案例: 给所有进入userservice的请求添加一个请求头
需求:给所有进入userservice的请求添加一个请求头:Truth,itcast is freaking awesome!
实现方式:在gateway中修改application.yml文件,给userservice的路由添加过滤器
3.1 在gateway模块中添加过滤器配置:
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id
uri: lb://userservice # 路由目标地址
predicates: # 路由断言
- Path=/user/** # 路径断言
filters: # 过滤器
- AddRequestHeader=Truth,itcast is freaking awesome!
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
配置说明:
·针对userservice服务的路由配置了AddRequestHeader
·增加一个键为Truth,值是itcast is freaking awesome!的请求头
3.2 在userservice服务的Controller添加访问路径
package cn.itcast.user.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/truth")
public String getRequestHaeder(@RequestHeader String truth){
return truth;
}
}
3.3 测试:访问http://localhost:10010/user/truth,返回itcast is freaking awesome!
3.4 注:上面的过滤器写在userservice路由下,因此仅仅对访问userservice的请求有效
若希望过滤器对所有路由都生效,则应该把配置改为defaultFilters:
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言
- Path=/user/** # 路径断言
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters:
- AddRequestHeader=Truth,itcast is freaking awesome!
6.5 全局过滤器GlobalFilter
1.上一节学习的过滤器,网关提供了31种,但每一种过滤器的作用都是固定的
2.如果想拦截请求并做自己的业务逻辑,利用上一节中的31种过滤器无法实现
3.全局过滤器的作用也是处理进入网关的请求和微服务响应——与GatewayFilter的作用一样
4.GlobalFilter与GatewayFilter的区别在于:
·GatewayFilter通过配置定义,处理逻辑是固定的
·GlobalFilter的逻辑需要自己写代码实现,定义方式是实现GlobalFilter接口
5.GlobalFilter接口定义如下:
public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
*
* @param exchange 请求上下文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono} 返回标示当前过滤器业务结束
*/
Mono filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
6.GlobalFilter案例:
6.1 案例需求:定义全局过滤器,拦截请求并判断用户身份,判断请求的参数是否满足下面条件:
·参数中是否有authorization
·authorization参数值是否为admin如果同时满足则放行,否则拦截
6.2 案例代码:
package cn.itcast.gateway.common;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
//值越小优先级越高
/*@Order(-1)*/
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();
// 2.获取authorization参数
String auth = params.getFirst("authorization");
// 3.校验
if("admin".equals(auth)){
// 放行
return chain.filter(exchange);
}
// 4.拦截请求
// 4.1 禁止访问,设置状态码——让用户体验更好
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 4.2 结束处理
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return -1;
}
}
6.3 测试:访问http://localhost:10010/user/3?authorization=admin,返回
{
id: 3,
username: "华沉鱼",
address: "湖北省十堰市"
}
7.总结:
1.全局过滤器的作用:对所有路由都生效的过滤器,并且可以自定义处理逻辑
2.实现全局过滤器的步骤:
1.实现GlobalFilter接口
2.添加@Order注解或实现Ordered接口
3.编写处理逻辑
6.6 过滤器执行顺序
1.请求进入网关后会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
2.请求路由后,会将上述三类过滤器都合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
3.路由过滤器、defaultFilter、全局过滤器的执行顺序:
1.每一个过滤器都必须指定一个int类型的order值
2.order值越小,优先级越高,执行顺序越靠前
3.当order值一样时按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行
order值一样时过滤器执行顺序
注:
1.GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值
2.路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
3.详细内容,可以查看下面几个类的源码:
org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()方法:
会先加载defaultFilters,然后再加载某个route的filters,最后合并
org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()方法:
会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链
6.7 跨域问题处理
1.跨域:域名不一致就是跨域,主要包括两种情况:
·情况一:
域名不同: www.taobao.com和www.taobao.org和www.jd.com和miaosha.jd.com
·情况二:
域名相同,端口不同: localhost:8080和localhost8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案:CORS
相关文章:https://www.ruanyifeng.com/blog/2016/04/cors.html
网关处理跨域采用的同样是CORS方案,并且只需要简单配置即可实现
2.模拟跨域问题:
启动gateway服务、userservice服务、orderservice服务。用浏览器打开index.html文件
将index.html放入tomcat或者nginx这样的web服务器中,启动并访问(视频中利用前端工具启index.html文件,端口8090)
index.html文件内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<pre>
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
</pre>
</body>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
axios.get("http://localhost:10010/user/1?authorization=admin")
.then(resp => console.log(resp.data))
.catch(err => console.log(err))
</script>
</html>
访问http://localhost:8090可以在浏览器控制台看到下面的错误:
控制台报错
从http://localhost:8090访问http://localhost:10010/user/1?authorization=admin
端口不同,显然是跨域的请求
3.解决跨域问题
在gateway服务的application.yml文件中,添加下面的配置:
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 # 这次跨域检测的有效期
重启gateway服务,重新访问http://localhost:8090,从控制台可以看到正确返回的信息:
正确的返回结果
总结:
CORS跨域要配置的参数包括:
·允许哪些域名跨域
·允许哪些请求头
·允许哪些请求方式
·是否允许使用cookie
·有效期是多久
说明:本内容整理自B站黑马程序员SpringCloud微服务课程视频及文档 >>黑马程序员SpringCloud微服务课程视频