什么是网关
背景
单体
项目中,前端只用访问指定的一个端口8080,就可以得到任何想要的数据
微服务项目中,ip是不断变化的,端口是多个的
解决方案:网关
网关:就是网络的关口,负责请求的
路由、转发、身份校验
。
前段还是访问之前的端口8080即可
后端对于前端来说是透明的
网关的两种实现
网关是一种开发规范,实际的实现有两种:
- 官方
- 网飞公司
步骤
maven坐标
在hm-gateway模块的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>hmall</artifactId>
<groupId>com.heima</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>hm-gateway</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!--common-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
启动类
在hm-gateway模块的com.hmall.gateway包下新建一个启动类:
package com.hmall.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
测试
启动
GatewayApplication
,以 http://localhost:8080 拼接微服务接口路径来测试。例如:
http://localhost:8080/items/page?pageNo=1&pageSize=1
路由属性
网关路由对应的Java类型是RouteDefinition,其中常见的属性有:
- id:路由唯一标识
- uri:路由目标地址
predicates
:路由断言
,判断请求是否符合当前路由。filters
:路由过滤器,对请求或响应做特殊处理。
网关中提供了33种路由过滤器,每种过滤器都有独特的作用。
设置请求头的filter
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.88.130:8848
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
filters: # 过滤器,可以对请求进行一些处理,比如添加请求头,添加请求参数等
- AddRequestHeader=X-Request-red, blue # 添加请求头 X-Request-red,值为 blue
@ApiOperation("分页查询商品")
@GetMapping("/page")
public PageDTO<ItemDTO> queryItemByPage(PageQuery query,
@RequestHeader(value = "X-Request-red", required = false) String red
) {
// 打印一下请求头
System.out.println("X-Request-red: " + red);
// 1.分页查询
Page<Item> result = itemService.page(query.toMpPage("update_time", false));
// 2.封装并返回
return PageDTO.of(result, ItemDTO.class);
}
默认路由是全局生效的,对所有的路由都会生效
网关的登录校验功能
架构图
- 校验和授权,JWT校验可以在网关中进行,将相关数据向后传递
网关请求流程
网关过滤器
全局过滤器GlobalFilter(更常见)
- 第一个参数:存储的是所有共享的数据
- 第二个参数:过滤器链,前一个过滤器执行完毕之后,就会紧接着执行下一个过滤器
不知道如何实现自定义的过滤器,我们可以参考
NettyRoutingFilter
- 不仅实现了过滤器的类,还是实现了一个Ordered的接口
在网关中定义一个 过滤器
- 实现接口GlobalFilter是必须得
- 实现接口Ordered 是为了排序
- 自定义的这个过滤器要在NettyRoutingFilter之前执行
package com.hmall.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
public static final String TOKEN = "token";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// TODO: 模拟登录校验
// 1. 从请求头中获取token
ServerHttpRequest request = exchange.getRequest();
String token = request.getHeaders().getFirst(TOKEN);
System.out.println("token: " + token);
return chain.filter(exchange);
}
@Override
public int getOrder() {
// 这个值是小所有filter的值的,会优先执行
return 0;
}
}
网关过滤器GatewayFilter
不用于全局过滤器,不是实现接口,而是继承一个抽象工厂
路由过滤器的代码
package com.hmall.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
GatewayFilter gatewayFilter = new GatewayFilter() {
// 这个方法是真正执行过滤逻辑的方法 这个实现全局过滤器简直一模一样
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("print any filter running");
System.out.println("config: " + config);
return chain.filter(exchange);
}
};
// 这个是给过滤器设置优先级的,值越小优先级越高
// 可有可无,不设置默认是0
return new OrderedGatewayFilter(gatewayFilter, 1);
}
}
因为路由过滤器不是作用于全局的,需要针对路由进行配置之后才会生效
- 配置PrintAny 的路由过滤器
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.88.130:8848
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
filters: # 过滤器,可以对请求进行一些处理,比如添加请求头,添加请求参数等
- AddRequestHeader=X-Request-red, blue # 添加请求头 X-Request-red,值为 blue
- PrintAny # 自定义过滤器,打印请求信息 没有任何参数信息
带属性的路由过滤器
package com.hmall.gateway.filter;
import lombok.Data;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
GatewayFilter gatewayFilter = new GatewayFilter() {
// 这个方法是真正执行过滤逻辑的方法 这个实现全局过滤器简直一模一样
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("print any filter running");
System.out.println("config.a = " + config.a);
System.out.println("config.b = " + config.b);
System.out.println("config.c = " + config.c);
return chain.filter(exchange);
}
};
// 这个是给过滤器设置优先级的,值越小优先级越高
// 可有可无,不设置默认是0
return new OrderedGatewayFilter(gatewayFilter, 1);
}
PrintAnyGatewayFilterFactory() {
super(Config.class);
}
@Data
public static class Config {
private String a;
private String b;
private String c;
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("a", "b", "c");
}
}
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.88.130:8848
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
filters: # 过滤器,可以对请求进行一些处理,比如添加请求头,添加请求参数等
- AddRequestHeader=X-Request-red, blue # 添加请求头 X-Request-red,值为 blue
- PrintAny=1,2,3 # 自定义过滤器,打印请求参数
实现登录校验
两个配置文件
- yaml配置文件
- 密钥文件
配置类,实现自动装配
安全配置类
工具类
- 生成token
- 解析token
将配置类转移到gateway服务下
把jwt工具包也转移过来,报错的话,给AuthProperties加上component注解
把秘钥相关配置也配置到yaml中
jwt密钥文件
package com.hmall.gateway.filter;
import cn.hutool.core.collection.CollectionUtil;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.utils.JwtTool;
import lombok.RequiredArgsConstructor;
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.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.List;
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
public static final String AUTHORIZATION = "authorization";
private final AuthProperties authProperties;
private final JwtTool jwtTool;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1,获取请求对象
ServerHttpRequest request = exchange.getRequest();
// 2,判断请求是不是需要拦截
if(isExcludePath(request.getURI().getPath())){
return chain.filter(exchange);
}
// 3. 获取token
List<String> tokenList = request.getHeaders().get(AUTHORIZATION);
String token = null;
if(CollectionUtil.isNotEmpty(tokenList)){
token = tokenList.get(0);
}
// 4. 校验并且解析token
Long userId;
try {
userId = jwtTool.parseToken(token);
} catch (Exception e) {
// 4.1 如果解析失败,返回401
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 到此为止,结束请求
return response.setComplete();
}
// 5. 传递用户信息
System.out.println("传递用户信息"+userId);
// 6. 放行
return chain.filter(exchange);
}
/**
* 判断请求是否需要拦截
* @param path 请求路径
* @return 是否需要拦截
*/
private boolean isExcludePath(String path) {
return authProperties.getExcludePaths().stream().anyMatch(path::startsWith);
}
@Override
public int getOrder() {
return 0;
}
}
没有登陆就会直接跳转过来
网关传递用户信息到微服务
步骤
package com.hmall.gateway.filter;
import cn.hutool.core.collection.CollectionUtil;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.utils.JwtTool;
import lombok.RequiredArgsConstructor;
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.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.List;
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
public static final String AUTHORIZATION = "authorization";
public static final String HEADER_NAME = "userInfo";
private final AuthProperties authProperties;
private final JwtTool jwtTool;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1,获取请求对象
ServerHttpRequest request = exchange.getRequest();
// 2,判断请求是不是需要拦截
if(isExcludePath(request.getURI().getPath())){
return chain.filter(exchange);
}
// 3. 获取token
List<String> tokenList = request.getHeaders().get(AUTHORIZATION);
String token = null;
if(CollectionUtil.isNotEmpty(tokenList)){
token = tokenList.get(0);
}
// 4. 校验并且解析token
Long userId;
try {
userId = jwtTool.parseToken(token);
} catch (Exception e) {
// 4.1 如果解析失败,返回401
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 到此为止,结束请求
return response.setComplete();
}
// 5. 传递用户信息
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header(HEADER_NAME, userId.toString()))
.build();
// 6. 放行
return chain.filter(swe);
}
/**
* 判断请求是否需要拦截
* @param path 请求路径
* @return 是否需要拦截
*/
private boolean isExcludePath(String path) {
return authProperties.getExcludePaths().stream().anyMatch(path::startsWith);
}
@Override
public int getOrder() {
return 0;
}
}
改写一下cart-service中controller中的方法,看看请求中有没有userinfo
public static final String USER_INFO = "user-info";
@ApiOperation("查询购物车列表")
@GetMapping
public List<CartVO> queryMyCarts(
@RequestHeader(value = USER_INFO, required = false) String userInfo){
System.out.println("user-info: " + userInfo);
return cartService.queryMyCarts();
}
优化一下获取用户信息的方式
这个用户信息,以后肯定很多地方都会用到,不能每次都在controller中写吧。这么多怎么写的过来
user拦截器
package com.hmall.common.interceptor;
import cn.hutool.core.util.ObjUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从请求头中获取user-info
Long userInfo = Long.valueOf(request.getHeader("user-info"));
// 将user-info放入UserContext中
if (ObjUtil.isNotEmpty(userInfo)) {
UserContext.setUser(userInfo);
}
// 放行
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 请求处理完毕后,清空UserContext
UserContext.removeUser();
}
}
mvc拦截器
package com.hmall.common.config;
import com.hmall.common.interceptor.UserInfoInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 可以不设置拦截路径,因为在默认就是拦截所有路径
// 而且此拦截器的作用是将请求头中的user-info放入UserContext中,而不是认证
registry.addInterceptor(new UserInfoInterceptor());
// .addPathPatterns("/**");
}
}
但是mvc是在common模块下,但是微服务不再这个包,而且路径名称也不一样
- 那就一定扫描不到这个config配置类
想要扫描到,需要借助配置文件
spring.factories
配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig,\
com.hmall.common.config.JsonConfig
此时会报错FileNotFoundException
因为gateway的底层不是基于SpringMVC,但是WebMvcConfigurer是springmvc的包类
给此类的加载配置条件注解
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 可以不设置拦截路径,因为在默认就是拦截所有路径
// 而且此拦截器的作用是将请求头中的user-info放入UserContext中,而不是认证
registry.addInterceptor(new UserInfoInterceptor());
// .addPathPatterns("/**");
}
}
修改cart的购物车查询
@Override
public List<CartVO> queryMyCarts() {
// 1.查询我的购物车列表
List<Cart> carts = lambdaQuery().eq(Cart::getUserId, UserContext.getUser()).list();
if (CollUtils.isEmpty(carts)) {
return CollUtils.emptyList();
}
// 2.转换VO
List<CartVO> vos = BeanUtils.copyList(carts, CartVO.class);
// 3.处理VO中的商品信息
handleCartItems(vos);
// 4.返回
return vos;
}
如果登录的时候被拦截了,可以在拦截器中放行一下登录路径
微服务之间的用户信息传递
- 前端与微服务之间的用户信息,可以使用全局过滤器传递
- 微服务之间的用户信息,需要使用
openfeign配置类
来实现
这个配置是给所有的openfeign转发的请求使用的
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public RequestInterceptor userInfoRequestInterceptor() {
return requestTemplate -> {
// 传递用户信息
// 从UserContext中获取用户信息
// 传递给下游服务
Long userInfo = UserContext.getUser();
if (ObjUtil.isNotEmpty(userInfo)) {
requestTemplate.header("user-info", userInfo.toString());
}
};
}
}
在启动类上加上,openfeign使用这个配置类
@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)
@MapperScan("com.hmall.trade.mapper")
@SpringBootApplication
public class TradeApplication {
public static void main(String[] args) {
SpringApplication.run(TradeApplication.class, args);
}
}
登录解决方案小结
配置管理
定义
微服务将服务重新拆分,导致很多配置要重写
其实nacos不仅是
注册中心
,还可以是配置中心
配置管理-共享配置
配置jdbc共享配置
打开nacos的地址:http://192.168.88.130:8848/nacos
- 打开配置列表新建配置id:shared-jdbc.yaml
只保留jdbc相关的配置
- 变动配置是可以 进行变量设置的:ip、端口、数据库实例、用户名、密码
spring:
datasource:
url: jdbc:mysql://${hm.db.host}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${hm.db.un:root}
password: ${hm.db.pw:123}
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
global-config:
db-config:
update-strategy: not_null
id-type: auto
logging:
level:
com.hmall: debug
pattern:
dateformat: HH:mm:ss:SSS
file:
path: "logs/${spring.application.name}"
同理还有swagger的配置
knife4j:
enable: true
openapi:
title: ${hm.swagger.title:黑马商城接口文档}
description: ${hm.swagger.doc:黑马商城购物车接口文档}
email: zhanghuyi@itcast.cn
concat: 虎哥
url: https://www.itcast.cn
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- ${hm.swagger.package}
拉取nacos中的共享配置
完整步骤
上述步骤不能自己做,spring已经做好了相关的配置
相关文件
application.yaml
server:
port: 8082 # 服务端口 8081 不能和其他服务端口重复
# feign配置
feign:
okhttp:
enabled: true # 开启OKHttp功能
hm:
db:
database: hm-cart
swagger:
title: "购物车服务接口文档"
package: "com.hmall.cart.controller"
bootstrap.yaml
spring:
application:
name: cart-service # 服务名称
cloud:
nacos:
server-addr: 192.168.88.130:8848 # Nacos服务地址
discovery:
server-addr: 192.168.88.130:8848 # Nacos服务地址
config:
file-extension: yaml
shared-configs:
- dataId: shared-jdbc.yaml
- dataId: shared-log.yaml
- dataId: shared-swagger.yaml
pom.xml
<!--nacos配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
效果
重启之后我们会看到拉去了nacos共享配置
配置热更新
配置热更新:当修改配置文件中的配置时,微服务
无需重启
即可使配置生效。
步骤
或者实用这种方式进行配置
案例
步骤
新增属性配置类 CartProperties
package com.hmall.cart.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
private Integer maxItems ;
}
更改CartServiceImpl中的调用逻辑
// 注入CartProperties
private final CartProperties cartProperties;
private void checkCartsFull(Long userId) {
int count = lambdaQuery().eq(Cart::getUserId, userId).count();
if (count >= cartProperties.getMaxItems()) {
throw new BizIllegalException(StrUtil.format("用户购物车课程不能超过{}", cartProperties.getMaxItems()));
}
}
在nacos配置中心。新增配置文件
yaml配置文件冒号的前后都必须要有空格
hm:
cart:
maxItems : 10
尝试将最大数量改成2
nacos的应用-动态路由
要实现
动态路由
首先要将路由配置保存到Nacos,当Nacos中的路由配置变更时,推送最新配置到网关,实时更新网关中的路由信息。
官方文档中是这么写的
步骤
- 复制cart-service中关于nacos相关的maven坐标到gateway模块中
<!--nacos配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
- 复制cart-service中bootStrap.yaml中的配置文件到gateway中
spring:
application:
name: gateway# 服务名称
cloud:
nacos:
server-addr: 192.168.88.130:8848 # Nacos服务地址
discovery:
server-addr: 192.168.88.130:8848 # Nacos服务地址
config:
file-extension: yaml
shared-configs:
- dataId: shared-log.yaml
- 去除掉application.yaml中关于路由的配置
server:
port: 8080
# jwt配置
hm:
jwt:
location: classpath:hmall.jks
alias: hmall
password: hmall123
tokenTTL: 30m
auth:
excludePaths:
- /search/**
- /users/login
- /items/**
- /hi
- 新增动态路由的配置类
package com.hmall.gateway.routers;
import cn.hutool.json.JSONUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
/**
* 动态路由加载器
*/
@Slf4j
@Component
@@RequiredArgsConstructor
public class DynamicRouteLoader {
private final NacosConfigManager nacosConfigManager;
private final RouteDefinitionWriter writer;
// 路由配置的DataId
private final String dataId = "gateway-routes.json";
// 路由配置的Group
private final String group = "DEFAULT_GROUP";
// 超时时间
private final long timeout = 5000;
// 存储路由id的set
private final Set<String> routeIds = new HashSet<>();
/**
* 初始化路由配置监听器:此方法会在Bean初始化完成后执行
*/
@PostConstruct
public void initRouteConfigListener() throws NacosException {
// 1. 项目启动时,加载一次路由配置
String configInfo = nacosConfigManager.getConfigService()
.getConfigAndSignListener(
dataId,
group,
timeout,
new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
// 2. 监听到路由变化时,需要更新路由配置表
updateRouteConfig(configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
}
);
// 3. 第一次加载路由配置的时候,也需要更新路由配置表
updateRouteConfig(configInfo);
}
/**
* 更新路由配置表
*
* @param configInfo 路由配置信息
*/
private void updateRouteConfig(String configInfo) {
log.info("更新路由配置表开始:{}", configInfo);
// 1. 解析路由配置信息,转换为RouteDefinition对象
List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
// 2. 删除已有的路由
routeIds.forEach(routeId -> {
// delete方法是异步的,需要订阅才能生效
writer.delete(Mono.just(routeId)).subscribe();
});
// 清空路由id
routeIds.clear();
// 3. 更新路由配置表
routeDefinitions.forEach(routeDefinition -> {
// 3.1 更新路由
// save方法是异步的,需要订阅才能生效
// save不能直接传入RouteDefinition对象,需要使用Mono.just()包装一下
writer.save(Mono.just(routeDefinition)).subscribe();
// 3.2 记录路由id 方便第二次更新时删除
routeIds.add(routeDefinition.getId());
});
log.info("更新路由配置表结束:{}", configInfo);
}
}
- 在nacos注册中惊醒gateway-routes.json的配置
[
{
"id": "item",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
}],
"filters": [],
"uri": "lb://item-service"
},
{
"id": "cart",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/carts/**"}
}],
"filters": [],
"uri": "lb://cart-service"
},
{
"id": "user",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/users/**", "_genkey_1":"/addresses/**"}
}],
"filters": [],
"uri": "lb://user-service"
},
{
"id": "trade",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/orders/**"}
}],
"filters": [],
"uri": "lb://trade-service"
},
{
"id": "pay",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/pay-orders/**"}
}],
"filters": [],
"uri": "lb://pay-service"
}
]
上述配置发布成功了以后,重启所有服务,如果都好使,那么就成功了
参考链接:https://b11et3un53m.feishu.cn/wiki/UMgpwmmQKisWBIkaABbcwAPonVf