一、网关路由
网关:就是网络的关口,负责请求的路由、转发、身份校验。
在SpringCloud中网关的实现包括两种:
1. 快速入门
Spring Cloud Gateway
步骤:
①新建hm-gateway模块
②引入依赖pom.xml(hm-gateway)
<?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>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</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>
③新建启动类GatewayApplication
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);
}
}
④添加配置文件application.yaml 以及 从hm-service中拷贝SearchController到item-service
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.126.151:8848
gateway:
routes:
- id: item-service # 路由规则id,自定义,唯一
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
- id: cart-service
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: user-service
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
- id: trade-service
uri: lb://trade-service
predicates:
- Path=/orders/**
- id: pay-service
uri: lb://pay-service
predicates:
- Path=/pay-orders/**
⑤同时启动这7个服务
测试
2. 路由属性
网关路由对应的Java类型是RouteDefinition,其中常见的属性有:
- id:路由唯一标识
- uri:路由目标地址
- predicates:路由断言,判断请求是否符合当前路由
- filters:路由过滤器,对请求或响应做特殊处理
2.1 路由断言
Spring提供了12种基本的RoutePredicateFactory实现:
名称 | 说明 | 示例 |
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 | 权重处理 | - Weight=group1,2 |
XForwarded Remote Addr | 基于请求的来源IP做判断 | - XForwardedRemoteAddr=192.168.1.1/24 |
2.2 路由过滤器
网关中提供了33种路由过滤器,每种过滤器都有独特的作用
名称 | 说明 | 示例 |
AddRequestHeader | 给当前请求添加一个请求头 | AddrequestHeader=headerName,headerValue |
RemoveRequestHeader | 移除请求中一个请求头 | RemoveRequestHeader=headerName |
AddResponseHeader | 给响应结果中添加一个响应头 | AddResponseHeader=headerName,headerValue |
RemoveResponseHeader | 从响应结果中移除一个响应头 | RemoveResponseHeader=headerName |
RewritePath | 请求路径重写 | RewritePath=/red/?(?<segment>.*), /$\{segment} |
StripPrefix | 去除请求路径中的N段前缀 | StripPrefix=1,则路径/a/b转发时只保留/b |
... ... |
二、网关登录校验
1. 如何在网关转发之前做登录校验?
2. 网关如何将用户信息传递给微服务?
3. 如何在微服务之间传递用户信息?
网关请求处理流程
1. 自定义过滤器
网关过滤器有两种,分别是:
- GatewayFilter:路由过滤器,作用于任意指定的路由;默认不生效,要配置到路由后生效。
- GlobalFilter:全局过滤器,作用范围是所有路由;声明后自动生效。
两种过滤器的过滤方法签名完全一致:
1.1 自定义过滤器 GlobalFilter
自定义GlobalFilter比较简单,直接实现GlobalFilter接口即可
步骤:hm-gateway模块
①MyGlobalFilter
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.HttpHeaders;
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 {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// TODO 模拟登录校验逻辑
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
System.out.println("headers = " + headers);
// 放行
return chain.filter(exchange);
}
@Override
public int getOrder () {
return 0;
}
}
②application.yaml
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.126.151:8848
gateway:
routes:
# ... ...
default-filters:
- AddRequestHeader=truth, anyone long-press like button will be rich
1.2 自定义过滤器 GatewayFilter
自定义GatewayFilter不是直接实现GatewayFilter,而是实现AbstractGatewayFilterFactory,示例如下:
步骤:hm-gateway模块
①PrintAnyGatewayFilterFactory
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.http.server.reactive.ServerHttpRequest;
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) {
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("print any filter running");
return chain.filter(exchange);
}
}, 1);
}
}
②application.yaml
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.126.151:8848
gateway:
routes:
# ... ...
default-filters:
- AddRequestHeader=truth, anyone long-press like button will be rich
- PrintAny
PrintAnyGatewayFilterFactory
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.http.server.reactive.ServerHttpRequest;
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) {
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String a = config.getA();
String b = config.getB();
String c = config.getC();
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
System.out.println("print any filter running");
return chain.filter(exchange);
}
}, 1);
}
// 自定义配置属性,成员变量名称很重要,下面会用到
@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");
}
// 将config字节码传递给父类,父类负责帮我们读取yaml配置
public PrintAnyGatewayFilterFactory() {
super(Config.class);
}
}
application.yaml
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.126.151:8848
gateway:
routes:
# ... ...
default-filters:
- AddRequestHeader=truth, anyone long-press like button will be rich
- PrintAny=1,2,3
2. 实现登录校验
需求:在网关中基于过滤器实现登录校验功能
提示:黑马商城是基于JWT实现的登录校验,目前相关功能在hm-service模块。我们可以将其中的JWT工具拷贝到gateway模块,然后基于GlobalFilter来实现登录校验。
步骤:hm-gateway模块
①从hm-service拷贝以下登录校验相关的文件到hm-gateway模块
②在AuthProperties里加上component注解
③复制相关配置到application.yaml
hm:
jwt:
location: classpath:hmall.jks
alias: hmall
password: hmall123
tokenTTL: 30m
auth:
excludePaths:
- /search/**
- /users/login
- /items/**
- /hi
④AuthGlobalFilter
package com.hmall.gateway.filter;
import com.hmall.common.exception.UnauthorizedException;
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.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
@RequiredArgsConstructor
public class AuthGloabalFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final JwtTool jwtTool;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取request对象
ServerHttpRequest request = exchange.getRequest();
// 2. 判断是否需要做登录拦截
if(isExclude(request.getPath().toString())) {
// 放行
return chain.filter(exchange);
}
// 3. 获取token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if(headers != null && !headers.isEmpty()) {
token = headers.get(0);
}
// 4. 校验并解析token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 拦截,设置响应状态码为401
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// TODO 5. 传递用户信息
System.out.println("userId = " + userId);
// 6. 放行
return chain.filter(exchange);
}
private boolean isExclude(String path) {
for (String pathPattern : authProperties.getExcludePaths()) {
if (antPathMatcher.match(pathPattern, path)) {
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
3. 网关传递用户
3.1 在网关的登录校验过滤器中,把获取到的用户写入请求头
需求:修改gateway模块中的登录校验拦截器,在校验成功后保存用户到下游请求的请求头中。
提示:要修改转发到微服务的请求,需要用到ServerWebExchange类提供的API,示例如下:
exchange.mutate() // mutate就是对下游请求做更改
.request(builder -> builder.header("user-info", userInfo))
.build();
①AuthGlobalFilter
package com.hmall.gateway.filter;
import com.hmall.common.exception.UnauthorizedException;
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.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
@RequiredArgsConstructor
public class AuthGloabalFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final JwtTool jwtTool;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取request对象
ServerHttpRequest request = exchange.getRequest();
// 2. 判断是否需要做登录拦截
if(isExclude(request.getPath().toString())) {
// 放行
return chain.filter(exchange);
}
// 3. 获取token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if(headers != null && !headers.isEmpty()) {
token = headers.get(0);
}
// 4. 校验并解析token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 拦截,设置响应状态码为401
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 5. 传递用户信息
String userInfo = userId.toString();
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header("user-info", userInfo))
.build();
// 6. 放行
return chain.filter(swe);
}
// ... ...
}
②cart-service模块中的CartController
@ApiOperation("查询购物车列表")
@GetMapping
public List<CartVO> queryMyCarts(@RequestHeader(value="user-info", required = false)String userInfo){
System.out.println("userInfo = " + userInfo);
return cartService.queryMyCarts();
}
③重启CartApplication和GatewayApplication
3.2 在hm-common中编写SpringMVC拦截器,获取登录用户
需求:由于每个微服务都可能有获取登录用户的需求,因此我们直接在hm-common模块定义拦截器,这样微服务只需要引入依赖即可生效,无需重复编写。
①在hm-common模块新增一个UserInfoInterceptor
package com.hmall.common.interceptors;
import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;
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 {
// 1. 获取登录用户信息
String userInfo = request.getHeader("user-info");
// 2. 判断是否获取了用户,如果有,存入TreadLocal
if(StrUtil.isNotBlank(userInfo)) {
UserContext.setUser(Long.valueOf(userInfo));
}
// 3. 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理用户
UserContext.removeUser();
}
}
②在hm-common模块下新增MvcConfig
package com.hmall.common.config;
import com.hmall.common.interceptors.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
// 只在微服务生效,在网关里不要生效
@ConditionalOnClass(DispatcherServlet.class) // 确保只有在Web环境中(即存在DispatcherServlet的情况下)才会生效
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
③cart-service模块下的CartServiceImpl(获取用户信息)
@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;
}
④在hm-common模块中的spring.factories中添加如下:(SpringBoot的自动装配原理)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig,\
com.hmall.common.config.JsonConfig
⑤重启GatewayApplication和CartApplication进行测试
http://localhost:18080/cart.html
4. OpenFeign传递用户
微服务项目中的很多业务要多个微服务共同合作完成,而这个过程中也需要传递登录用户信息,例如:
OpenFeign中提供了一个拦截器接口,所有由OpenFeign发起的请求都会先调用拦截器处理请求:
其中的RequestTemplate类中提供了一些方法可以让我们修改请求头:
步骤:hm-api模块
①pom.xml
<!--hm-common-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
②DefaultFeignConfig
package com.hmall.api.config;
import com.hmall.common.utils.UserContext;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public RequestInterceptor userInfoRequestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
Long userId = UserContext.getUser();
if(userId != null) {
template.header("user-info", userId.toString());
}
}
};
}
}
三、配置管理
- 微服务重复配置过多,维护成本高
- 业务配置经常变动,每次修改都要重启服务
- 网关路由配置写死,如果变更要重启网关
1. 配置共享
1.1 添加配置到Nacos
添加一些共享配置到Nacos中,包括:jdbc、MyBatisPlus、日志、Swagger、OpenFeign等配置
http://192.168.126.151:8848/nacos/#/configurationManagement?serverId=center&group=&dataId=&namespace=&appName=&pageSize=&pageNo=
①
spring:
datasource:
url: jdbc:mysql://${hm.db.host:192.168.126.151}:${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:123456}
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}"
knife4j:
enable: true
openapi:
title: ${hm.swagger.title:黑马商城接口文档}
description: ${hm.swagger.desc:黑马商城接口文档}
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}
1.2 拉取共享配置
基于NacosConfig拉取共享配置代替微服务的本地配置
步骤:cart-service模块
①引入依赖 pom.xml(cart-service)
<!--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>
②新建boorstrap.yaml 记得更改为自己的nacos地址
spring:
application:
name: cart-service # 服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.126.151 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- data-id: shared-jdbc.yaml # 共享mybatis配置
- data-id: shared-log.yaml # 共享日志配置
- data-id: shared-swagger.yaml # 共享日志配置
③application.yaml
server:
port: 8082
feign:
okhttp:
enabled: true
hm:
db:
database: hm-cart
swagger:
title: "黑马商城购物车服务接口文档"
desc: "黑马商城购物车服务接口文档"
package: com.hmall.cart.controller
④重启CartApplication
注:其他模块也可以进行同样的配置
2. 配置热更新
配置热更新:当修改配置文件中的配置时,微服务无需重启即可使配置生效
前提条件:
①nacos中要有一个与微服务名有关的配置文件
②微服务中要以特定方式读取需要热更新的配置属性
2.1 实现购物车添加商品上限的配置热更新
需求:购物车的限定数量目前是写死在业务中的,将其改为读取配置文件属性,并将配置交给Nacos管理,实现热更新。
步骤:
①cart-service模块下新建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
package com.hmall.cart.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmall.api.client.ItemClient;
import com.hmall.api.dto.ItemDTO;
import com.hmall.cart.config.CartProperties;
import com.hmall.cart.domain.dto.CartFormDTO;
import com.hmall.cart.domain.po.Cart;
import com.hmall.cart.domain.vo.CartVO;
import com.hmall.cart.mapper.CartMapper;
import com.hmall.cart.service.ICartService;
import com.hmall.common.exception.BizIllegalException;
import com.hmall.common.utils.BeanUtils;
import com.hmall.common.utils.CollUtils;
import com.hmall.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* <p>
* 订单详情表 服务实现类
* </p>
*
* @author 虎哥
* @since 2023-05-05
*/
@Service
@RequiredArgsConstructor // 必备参数的构造函数
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
private final ItemClient itemClient;
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()));
}
}
// ... ...
}
③新增配置管理
hm:
cart:
maxItems: 1
④重启CartApplication
⑤maxItems修改为10 (无需重启,即刻生效)
3. 动态路由
要实现动态路由首先要将路由配置保存到Nacos,当Nacos中的路由配置变更时,推送最新配置到网关,实时更新网关中的路由信息。
我们需要完成两件事情:
①监听Nacos配置变更的消息
②当配置变更时,将最新的路由信息更新到网关路由表
3.1 监听Nacos配置
监听Nacos配置变更可以参考官方文档:Java SDK
步骤:hm-gateway模块
①在pom.xml(hm-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>
②新增bootstrap.yaml
spring:
application:
name: gateway # 服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.126.151 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- data-id: shared-log.yaml # 共享日志配置
③修改application.yaml
server:
port: 8080
hm:
jwt:
location: classpath:hmall.jks
alias: hmall
password: hmall123
tokenTTL: 30m
auth:
excludePaths:
- /search/**
- /users/login
- /items/**
- /hi
④新增routers/DynamicRouteLoader
package com.hmall.gateway.routers;
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.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.Executor;
@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouterLoader {
private final NacosConfigManager nacosConfigManager;
private final String dataId = "gateway-routes.json";
private final String group = "DEFAULT_GROUP";
@PostConstruct
public void initRouterConfigListener() throws NacosException {
// 1. 项目启动时,先拉取一次配置,并且添加配置监听器
String configInfo = nacosConfigManager.getConfigService()
.getConfigAndSignListener(dataId, group, 5000, new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
// 2. 监听到配置变更,需要去更新路由表
updateConfigInfo(configInfo);
}
});
// 3. 第一次读取到配置,也需要更新到路由表
updateConfigInfo(configInfo);
}
public void updateConfigInfo(String configInfo) {
// TODO
}
}
3.2 更新路由表
监听到路由信息后,可以利用RouteDefinitionWriter来更新路由表
3.3 路由配置语法
为了方便解析从Nacos读取道德路由配置,推荐使用json格式的路由配置,模板如下:
步骤:hm-gateway
①DynamicRouteLoader
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;
private final String dataId = "gateway-routes.json";
private final String group = "DEFAULT_GROUP";
private final Set<String> routeIds = new HashSet<>();
@PostConstruct
public void initRouterConfigListener() throws NacosException {
// 1. 项目启动时,先拉取一次配置,并且添加配置监听器
String configInfo = nacosConfigManager.getConfigService()
.getConfigAndSignListener(dataId, group, 5000, new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
// 2. 监听到配置变更,需要去更新路由表
updateConfigInfo(configInfo);
}
});
// 3. 第一次读取到配置,也需要更新到路由表
updateConfigInfo(configInfo);
}
public void updateConfigInfo(String configInfo) {
log.debug("监听到路由配置信息:" + configInfo);
// 1. 解析配置信息,转为RouterDefinition
List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
// 2. 删除旧的路由表
for (String routeId : routeIds) {
writer.delete(Mono.just(routeId)).subscribe();
}
routeIds.clear();
// 3. 更新路由表
for (RouteDefinition routeDefinition : routeDefinitions) {
// 3.1 更新路由表
writer.save(Mono.just(routeDefinition)).subscribe();
// 3.2 记录路由id,便于下一次更新时删除
routeIds.add(routeDefinition.getId());
}
}
}
②动态添加路由信息
[
{
"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"
}
]
③重启GateApplication测试