深圳微商城网站设计价格,网站没有备案会怎么样,企业关键词排名优化网址,wordpress加载慢目录 1.网关路由转发 1.1 网关 1.2 快速入门 1.2.1 创建项目 1.2.2 引入依赖 1.2.3 启动类 1.2.4 配置路由 1.2.5 测试 1.3 路由过滤 2.网关登录校验 2.1 鉴权思路分析 2.2 网关过滤器 2.3 自定义过滤器 2.3.1 自定义GatewayFilter 2.3.2 自定义GlobalFilter 2.4 登录校验 2.4.… 目录 1.网关路由转发 1.1 网关 1.2 快速入门 1.2.1 创建项目 1.2.2 引入依赖 1.2.3 启动类 1.2.4 配置路由 1.2.5 测试 1.3 路由过滤 2.网关登录校验 2.1 鉴权思路分析 2.2 网关过滤器 2.3 自定义过滤器 2.3.1 自定义GatewayFilter 2.3.2 自定义GlobalFilter 2.4 登录校验 2.4.1 JWT工具 2.4.2 登录校验过滤器 2.5 网关传递用户数据 2.5.1 保存用户到请求头 2.5.2 拦截器获取用户 2.5.3 恢复购物车代码 2.5.4 测试 2.6 OpenFeign实现微服务传递用户数据 2.6.1 实现 2.6.2 测试 3.配置管理 3.1 配置共享 3.1.1.添加共享配置 3.1.2 拉取共享配置 3.2 配置热更新 3.2.1 添加配置到Nacos 3.2.2 配置热更新读取配置 3.3 动态路由 3.3.1 监听Nacos配置变更 3.3.2 更新路由 3.3.3 实现动态路由 前置 将黑马商城拆分为5个微服务 用户服务 商品服务 购物车服务 交易服务 支付服务 问题 每个微服务都有不同的地址或端口入口不同相信大家在与前端联调的时候发现了一些问题 后续部署在Linux微服务的地址以及端口可能随时会改变 前端无法调用nacos无法实时更新服务列表 思维导图
传递用户信息 重点
1、利用网关是网络的入口实现登录校验简化了微服务重复出现的校验逻辑。
2、微服务只需编写拦截器拦截网关的请求获取请求头的用户数据并保存到ThreadLocal。因为每一个微服务都要拦截器因此把拦截器定义在common包下。
3、OpenFeign中的拦截器可以实现微服务传递用户数据调用到OpenFeign服务就会被拦截并保存用户数据到请求头在发送请求时携带。其他微服务的拦截器会拦截这次请求取出用户数据保存到ThreadLocal。 1.网关路由转发 1.1 网关 什么是网关
网关就是网络的关口。数据在网络间传输从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全的校验。 前端请求不能直接访问微服务而是要请求网关 网关可以做安全控制也就是登录身份校验校验通过才放行 通过认证后网关再根据请求判断应该访问哪个微服务将请求转发到对应的微服务 注意事项网关其实本身也是一个微服务可以去注册中心拉取服务进行路由转发以及负载均衡处理。 在SpringCloud当中提供了两种网关实现方案 Netflix Zuul早期实现目前已经淘汰 SpringCloudGateway基于Spring的WebFlux技术完全支持响应式编程吞吐能力更强 1.2 快速入门 接下来如何利用网关实现请求路由。由于网关本身也是一个独立的微服务因此也需要创建一个模块开发功能。大概步骤如下 创建网关微服务独立的模块 引入SpringCloudGateway、NacosDiscovery依赖 编写启动类 配置网关路由 1.2.1 创建项目 首先我们要在hmall下创建一个新的module命名为hm-gateway作为网关微服务 1.2.2 引入依赖 在hm-gateway模块的pom.xml文件中引入依赖
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdparentartifactIdhmall/artifactIdgroupIdcom.heima/groupIdversion1.0.0/version/parentmodelVersion4.0.0/modelVersionartifactIdhm-gateway/artifactIdpropertiesmaven.compiler.source11/maven.compiler.sourcemaven.compiler.target11/maven.compiler.target/propertiesdependencies!--common--dependencygroupIdcom.heima/groupIdartifactIdhm-common/artifactIdversion1.0.0/version/dependency!--网关--dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-gateway/artifactId/dependency!--nacos discovery--dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId/dependency!--负载均衡--dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-loadbalancer/artifactId/dependency/dependenciesbuildfinalName${project.artifactId}/finalNamepluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build
/project 1.2.3 启动类 在hm-gateway模块的com.hmall.gateway包下新建一个启动类 代码如下
SpringBootApplication
public class GatewayApplication {public static void main(String[] args) {SpringApplication.run(GatewayApplication.class, args);}
}1.2.4 配置路由 接下来在hm-gateway模块的resources目录新建一个application.yaml文件内容如下
server:port: 8080
spring:application:name: gatewaycloud:nacos:server-addr: 192.168.150.101:8848gateway:routes:- id: item # 路由规则id自定义唯一一般用服务名称uri: lb://item-service # 路由的目标服务lb代表负载均衡会从注册中心拉取服务列表predicates: # 路由断言判断当前请求是否符合当前规则符合则路由到目标服务- Path/items/**,/search/** # 这里是以请求路径作为判断规则- id: carturi: lb://cart-servicepredicates:- Path/carts/**- id: useruri: lb://user-servicepredicates:- Path/users/**,/addresses/**- id: tradeuri: lb://trade-servicepredicates:- Path/orders/**- id: payuri: lb://pay-servicepredicates:- Path/pay-orders/**注意事项
1、网关服务的端口一般都是8080因为它是入口。
2、后续我们不用访问微服务地址只需要访问8080端口对应的服务网关会进行路由转发到对应的微服务。
访问nacos注册中心http://192.168.85.144:8848/nacos 1.2.5 测试 启动ItemApplication然后启动GatewayApplication以 http://localhost:8080 拼接微服务接口路径来测试。例如
http://localhost:8080/items/page?pageNo1pageSize1 此时启动UserApplication、CartApplication然后打开前端页面发现相关功能都可以正常访问了。 1.3 路由过滤 路由规则的定义语法如下
spring:cloud:gateway:routes:- id: itemuri: lb://item-servicepredicates:- Path/items/**,/search/** 其中routes对应的类型如下 是一个集合也就是说可以定义很多路由规则。
集合中的RouteDefinition就是具体的路由规则定义其中常见的属性如下 四个属性含义如下 id路由的唯一标示 predicates路由断言其实就是匹配条件。满足条件才会到路由目标地址。 filters路由过滤条件 uri路由目标地址lb://代表负载均衡从注册中心获取目标微服务的实例列表并且负载均衡选择一个访问。 这里重点关注predicates也就是路由断言。SpringCloudGateway中支持的断言类型有很多 名称 说明 示例 After 是某个时间点后的请求 - After2037-01-20T17:42:47.789-07:00[America/Denver] Before 是某个时间点之前的请求 - Before2031-04-13T15:14:47.43308:00[Asia/Shanghai] Between 是某两个时间点之前的请求 - Between2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] Cookie 请求必须包含某些cookie - Cookiechocolate, ch.p Header 请求必须包含某些header - HeaderX-Request-Id, \d Host 请求必须是访问某个host域名 - Host**.somehost.org,**.anotherhost.org Method 请求方式必须是指定方式 - MethodGET,POST Path 请求路径必须符合指定规则 - Path/red/{segment},/blue/** Query 请求参数必须包含指定参数 - Queryname, Jack或者- Queryname RemoteAddr 请求者的ip必须是指定范围 - RemoteAddr192.168.1.1/24 weight 权重处理 2.网关登录校验 单体架构时我们只需要完成一次用户登录、身份校验就可以在所有业务中获取到用户信息。而微服务拆分后每个微服务都独立部署不再共享数据。也就意味着每个微服务都需要做登录校验这显然不可取。 2.1 鉴权思路分析 既然网关是所有微服务的入口一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做这样之前说的问题就解决了 只需要在网关保存秘钥 只需要在网关开发登录校验功能 此时登录校验的流程如图 不过这里存在几个问题 网关路由是配置的请求转发是Gateway内部代码我们如何在转发之前做登录校验 自定义网关过滤器校验用户信息并且在NettyRoutingFilter请求转发到微服务过滤器前执行 网关校验JWT之后如何将用户信息传递给微服务 通过手段让网关发请求时携带用户数据到请求头。微服务定义拦截器来拦截请求取出用户数据保存到ThreadLocal。 微服务之间也会相互调用这种调用不经过网关又该如何传递用户信息 OpenFeign发起的请求自动携带登录用户信息传递数据给其他微服务。由于其他微服务有定义拦截器就会去请求头取出用户数据。 2.2 网关过滤器 而网关的请求转发是Gateway内部代码实现的要想在请求转发之前做登录校验就必须了解Gateway内部工作的基本原理。 如图所示 客户端请求进入网关后由HandlerMapping对请求做判断找到与当前请求匹配的路由规则Route然后将请求交给WebHandler去处理。 WebHandler则会加载当前路由下需要执行的过滤器链Filter chain然后按照顺序逐一执行过滤器后面称为Filter。 图中Filter被虚线分为左右两部分是因为Filter内部的逻辑分为pre和post两部分分别会在请求路由到微服务之前和之后被执行。 只有所有Filter的pre逻辑都依次顺序执行通过后请求才会被路由到微服务。 微服务返回结果后再倒序执行Filter的post逻辑。 最终把响应结果返回。 如图中所示最终请求转发是有一个名为NettyRoutingFilter的过滤器来执行的而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器在其中实现登录校验逻辑并且将过滤器执行顺序定义到NettyRoutingFilter之前这就符合我们的需求了 该如何实现一个网关过滤器呢
网关过滤器链中的过滤器有两种 GatewayFilter路由过滤器作用范围比较灵活可以是任意指定的路由Route。 GlobalFilter全局过滤器作用范围是所有路由不可配置。 其实GatewayFilter和GlobalFilter这两种过滤器的方法签名完全一致
/*** 处理请求并将其传递给下一个过滤器* param exchange 当前请求的上下文其中包含request、response等各种数据* param chain 过滤器链基于它向下传递请求* return 根据返回值标记当前请求是否被完成或拦截chain.filter(exchange)就放行了。*/
MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain);
FilteringWebHandler在处理请求时会将GlobalFilter装饰为GatewayFilter然后放到同一个过滤器链中排序以后依次执行。 Gateway内置的GatewayFilter过滤器使用起来非常简单无需编码只要在yaml文件中简单配置即可。而且其作用范围也很灵活配置在哪个Route下就作用于哪个Route。
例如有一个过滤器叫做AddRequestHeaderGatewayFilterFacotry顾明思议就是添加请求头的过滤器可以给请求添加一个请求头并传递到下游微服务。
使用的使用只需要在application.yaml中这样配置
spring:cloud:gateway:routes:- id: test_routeuri: lb://test-servicepredicates:-Path/test/**filters:- AddRequestHeaderkey, value # 逗号之前是请求头的key逗号之后是value 如果想要让过滤器作用于所有的路由则可以这样配置跟routes配置同级
spring:cloud:gateway:default-filters: # default-filters下的过滤器可以作用于所有路由- AddRequestHeaderkey, valueroutes:- id: test_routeuri: lb://test-servicepredicates:-Path/test/** 2.3 自定义过滤器 无论是GatewayFilter还是GlobalFilter都支持自定义只不过编码方式、使用方式略有差别。 2.3.1 自定义GatewayFilter 自定义GatewayFilter不是直接实现GatewayFilter而是实现AbstractGatewayFilterFactory。最简单的方式是这样的
Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactoryObject {Overridepublic GatewayFilter apply(Object config) {return new GatewayFilter() {Overridepublic MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取请求ServerHttpRequest request exchange.getRequest();// 编写过滤器逻辑System.out.println(过滤器执行了);// 放行return chain.filter(exchange);}};}
} 注意事项该类的名称一定要以GatewayFilterFactory为后缀 2.3.2 自定义GlobalFilter 自定义GlobalFilter则简单很多直接实现GlobalFilter即可而且也无法设置动态参数
Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {Overridepublic MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 编写过滤器逻辑System.out.println(未登录无法访问);// 放行// return chain.filter(exchange);// 拦截ServerHttpResponse response exchange.getResponse();response.setRawStatusCode(401);return response.setComplete();}Overridepublic int getOrder() {// 过滤器执行顺序值越小优先级越高return 0;}
} 注意事项getOrder方法返回值只需要比int最大值小就可以实现在NettyRoutingFilter过滤器前执行。 2.4 登录校验 接下来我们就利用自定义GlobalFilter来完成登录校验。 2.4.1 JWT工具 登录校验需要用到JWT而且JWT的加密需要秘钥和加密工具。这些在hm-service中已经有了我们直接拷贝过来 具体作用如下 AuthProperties配置登录校验需要拦截的路径因为不是所有的路径都需要登录才能访问 JwtProperties定义与JWT工具有关的属性比如秘钥文件位置 SecurityConfig工具的自动装配 JwtToolJWT工具其中包含了校验和解析token的功能 hmall.jks秘钥文件 其中AuthProperties和JwtProperties所需的属性要在application.yaml中配置
hm:jwt:location: classpath:hmall.jks # 秘钥地址alias: hmall # 秘钥别名password: hmall123 # 秘钥文件密码tokenTTL: 30m # 登录有效期auth:excludePaths: # 无需登录校验的路径- /search/**- /users/login- /items/** 2.4.2 登录校验过滤器 接下来我们定义一个登录校验的过滤器 代码如下
package com.hmall.gateway.filter;import java.util.List;Component
RequiredArgsConstructor
EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {private final JwtTool jwtTool;private final AuthProperties authProperties;private final AntPathMatcher antPathMatcher new AntPathMatcher();Overridepublic MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.获取RequestServerHttpRequest request exchange.getRequest();// 2.判断是否不需要拦截if(isExclude(request.getPath().toString())){// 无需拦截直接放行return chain.filter(exchange);}// 3.获取请求头中的tokenString token null;ListString headers request.getHeaders().get(authorization);if (!CollUtils.isEmpty(headers)) {token headers.get(0);}// 4.校验并解析tokenLong userId null;try {userId jwtTool.parseToken(token);} catch (UnauthorizedException e) {// 如果无效拦截ServerHttpResponse response exchange.getResponse();response.setRawStatusCode(401);return response.setComplete();}// TODO 5.如果有效传递用户信息System.out.println(userId userId);// 6.放行return chain.filter(exchange);}private boolean isExclude(String antPath) {for (String pathPattern : authProperties.getExcludePaths()) {if(antPathMatcher.match(pathPattern, antPath)){return true;}}return false;}Overridepublic int getOrder() {return 0;}
} 登录完成访问其他接口打印用户信息 注意事项
1、该过滤器还需要实现Ordered接口并且重写getOrder方法并且返回一个整数类型。
这个值越小过滤器的优先级越高。
2、getOrder方法返回值只需要比int最大值小就可以实现在NettyRoutingFilter过滤器前执行。 重启测试会发现访问/items开头的路径未登录状态下不会被拦截 访问其他路径则比如http://localhost:8080/carts未登录状态下请求会被拦截并且返回401状态码 2.5 网关传递用户数据 现在网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时微服务又该如何获取用户身份呢
由于网关发送请求到微服务依然采用的是Http请求因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取并存入ThreadLocal方便后续使用。 据图流程图如下 因此接下来我们要做的事情有 改造网关过滤器在获取用户信息后保存到请求头转发到下游微服务 编写微服务拦截器拦截请求获取用户信息保存到ThreadLocal后放行 2.5.1 保存用户到请求头 首先我们修改登录校验拦截器的处理逻辑保存用户信息到请求头中 注意事项网关向微服务发送数据也是通过HTTP请求只不过底层帮我们自动转发请求。 2.5.2 拦截器获取用户 在hm-common中已经有一个用于保存登录用户的ThreadLocal工具 其中已经提供了保存和获取用户的方法 接下来只需要编写拦截器获取用户信息并保存到UserContext然后放行即可。
由于每个微服务都有获取登录用户的需求因此拦截器我们直接写在hm-common中并写好自动装配。这样微服务只需要引入hm-common就可以直接具备拦截器功能无需重复编写。 我们在hm-common模块下定义一个拦截器 具体代码如下
package com.hmall.common.interceptor;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 {Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的用户信息String userInfo request.getHeader(user-info);// 2.判断是否为空if (StrUtil.isNotBlank(userInfo)) {// 不为空保存到ThreadLocalUserContext.setUser(Long.valueOf(userInfo));}// 3.放行return true;}Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserContext.removeUser();}
} 注意事项该拦截器作用是获取请求头的用户信息然后存入ThreadLocal中并不拦截请求。 接着在hm-common模块下编写SpringMVC的配置类配置登录拦截器 具体代码如下
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
//当类路径下存在DispatcherServlet.class这个类时被标注的配置或组件才会被加载。
//当ConditionalOnClass(DispatcherServlet.class)出现时通常意味着这个配置或组件是与 Spring MVC 相关的。
//网关微服务不会注册该组件,其他微服务会被注册
ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new UserInfoInterceptor());}
} 注意事项
1、这个配置类默认是不会生效的因为它所在的包是com.hmall.common.config与其它微服务的扫描包不一致无法被扫描到因此无法生效。
2、基于SpringBoot的自动装配原理我们要将其添加到resources目录下的META-INF/spring.factories文件中 内容如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration\com.hmall.common.config.MyBatisConfig,\com.hmall.common.config.MvcConfig
实现自动装配之后后续其他服务引入该模块就可以直接使用。 2.5.3 恢复购物车代码 之前我们无法获取登录用户所以把购物车服务的登录用户写死了现在需要恢复到原来的样子。
找到cart-service模块的com.hmall.cart.service.impl.CartServiceImpl 修改其中的queryMyCarts方法 2.5.4 测试 访问http://localhost:18080/cart.html可以看到根据用户查询购物车信息功能实现。 2.6 OpenFeign实现微服务传递用户数据 2.6.1 实现 前端发起的请求都会经过网关再到微服务由于我们之前编写的过滤器和拦截器功能微服务可以轻松获取登录用户信息。 应用场景但有些业务是比较复杂的请求到达微服务后还需要调用其它多个微服务。
比如下单业务流程如下 下单的过程中需要调用商品服务扣减库存调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是订单服务调用购物车时并没有传递用户信息购物车服务无法知道当前用户是谁 由于微服务获取用户信息是通过拦截器在请求头中读取因此要想实现微服务之间的用户信息传递就必须在微服务发起调用时把用户信息存入请求头。 微服务之间调用是基于OpenFeign来实现的并不是我们自己发送的请求。 那如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢
这里要借助Feign中提供的一个拦截器接口feign.RequestInterceptor
public interface RequestInterceptor {/*** Called for every request. * Add data using methods on the supplied {link RequestTemplate}.*/void apply(RequestTemplate template);
}只需要实现这个接口然后实现apply方法利用RequestTemplate类来添加请求头将用户信息保存到请求头中。这样以来每次通过OpenFeign发起请求的时候都会调用该方法传递用户信息。 由于FeignClient全部都是在hm-api模块因此我们在hm-api模块的com.hmall.api.config.DefaultFeignConfig中编写这个拦截器 在com.hmall.api.config.DefaultFeignConfig中添加一个Bean
Bean
public RequestInterceptor userInfoRequestInterceptor(){return new RequestInterceptor() {Overridepublic void apply(RequestTemplate template) {// 获取登录用户Long userId UserContext.getUser();if(userId null) {// 如果为空则直接跳过return;}// 如果不为空则放入请求头中传递给下游微服务template.header(user-info, userId.toString());}};
} 注意事项这个拦截器的配置类需要在服务发现的启动类中声明。
EnableFeignClients(basePackages com.hmall.api.client,defaultConfiguration DefaultFeignConfig.class) 好了现在微服务之间通过OpenFeign调用时也会传递登录用户信息了。 2.6.2 测试 点击提交订单
1、创建订单表以及详情表
2、清理用户购物车数据
3、扣减商品库存 TradeApplication CartApplication
清理用户购物车数据 ItemApplication 注意事项
这里需要加载OpenFeign的拦截器不然拦截器不生效不能进行微服务之间的用户信息传递。 3.配置管理 微服务共享的配置可以统一交给Nacos保存和管理在Nacos控制台修改配置后Nacos会将配置变更推送给相关的微服务并且无需重启即可生效实现配置热更新。 到目前为止我们已经解决了微服务相关的几个问题 微服务远程调用实现微服务之间的通信 微服务注册、发现微服务的集群注册 微服务请求路由、负载均衡OpenFeign动态代理机制 微服务登录用户信息传递浏览器 - 网关 - 微服务 - 微服务与微服务 不过现在依然还有几个问题需要解决 网关路由在配置文件中写死了如果变更必须重启微服务 某些业务配置在配置文件中写死了每次修改都要重启服务 每个微服务都有很多重复的配置维护成本高像配置MYSQLMybatis配置 实现配置共享 这些问题都可以通过统一的配置管理器服务解决。
而Nacos不仅仅具备注册中心功能也具备配置管理的功能 1、微服务共享的配置可以统一交给Nacos保存和管理在Nacos控制台修改配置后Nacos会将配置变更推送给相关的微服务并且无需重启即可生效实现配置热更新。
2、网关的路由同样是配置因此同样可以基于这个功能实现动态路由功能无需重启网关即可修改路由配置。 3.1 配置共享 我们可以把微服务共享的配置抽取到Nacos中统一管理这样就不需要每个微服务都重复配置了。分为两步 在Nacos中添加共享配置 微服务拉取配置 3.1.1.添加共享配置 以cart-service为例我们看看有哪些配置是重复的可以抽取的
首先是jdbc相关配置 然后是日志配置 然后是swagger以及OpenFeign的配置 我们在nacos控制台分别添加这些配置。
首先是jdbc相关配置在配置管理-配置列表中点击新建一个配置 在弹出的表单中填写信息 其中详细的配置如下
spring:datasource:url: jdbc:mysql://${hm.db.host:192.168.150.101}:${hm.db.port:3306}/${hm.db.database}?useUnicodetruecharacterEncodingUTF-8autoReconnecttrueserverTimezoneAsia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driverusername: ${hm.db.un:root}password: ${hm.db.pw:123}
mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandlerglobal-config:db-config:update-strategy: not_nullid-type: auto 注意这里的jdbc的相关参数并没有写死例如 数据库ip通过${hm.db.host:192.168.150.101}配置了默认值为192.168.150.101同时允许通过${hm.db.host}来覆盖默认值 数据库端口通过${hm.db.port:3306}配置了默认值为3306同时允许通过${hm.db.port}来覆盖默认值 数据库database可以通过${hm.db.database}来设定无默认值 然后是统一的日志配置命名为shared-log.yaml配置内容如下
logging:level:com.hmall: debugpattern:dateformat: HH:mm:ss:SSSfile:path: logs/${spring.application.name} 然后是统一的swagger配置命名为shared-swagger.yaml配置内容如下
knife4j:enable: trueopenapi:title: ${hm.swagger.title:黑马商城接口文档}description: ${hm.swagger.description:黑马商城接口文档}email: ${hm.swagger.email:zhanghuyiitcast.cn}concat: ${hm.swagger.concat:虎哥}url: https://www.itcast.cnversion: v1.0.0group:default:group-name: defaultapi-rule: packageapi-rule-resources:- ${hm.swagger.package} 注意这里的swagger相关配置我们没有写死例如 title接口文档标题我们用了${hm.swagger.title}来代替将来可以有用户手动指定 email联系人邮箱我们用了${hm.swagger.email:zhanghuyiitcast.cn}默认值是zhanghuyiitcast.cn同时允许用户利用${hm.swagger.email}来覆盖。 3.1.2 拉取共享配置 将拉取到的共享配置与本地的application.yaml配置合并完成项目上下文的初始化。
不过需要注意的是读取Nacos配置是SpringCloud上下文ApplicationContext初始化时处理的发生在项目的引导阶段。然后才会初始化SpringBoot上下文去读取application.yaml。 问题也就是说引导阶段application.yaml文件尚未读取根本不知道nacos 地址该如何去加载nacos中的配置文件呢 答案SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml(或者bootstrap.properties)的文件如果我们将nacos地址配置到bootstrap.yaml中那么在项目引导阶段就可以读取nacos中的配置了。 因此微服务整合Nacos配置管理的步骤如下
1引入依赖
在cart-service模块引入依赖 !--nacos配置管理--dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-config/artifactId/dependency!--读取bootstrap文件--dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-bootstrap/artifactId/dependency 2新建bootstrap.yaml
在cart-service中的resources目录新建一个bootstrap.yaml文件 内容如下
spring:application:name: cart-service # 服务名称profiles:active: devcloud:nacos:server-addr: 192.168.150.101 # nacos地址config:file-extension: yaml # 文件后缀名shared-configs: # 共享配置- dataId: shared-jdbc.yaml # 共享mybatis配置- dataId: shared-log.yaml # 共享日志配置- dataId: shared-swagger.yaml # 共享日志配置
内容包含服务名称环境nacos地址以及共享的配置文件坐标。 3修改application.yaml
由于一些配置挪到了bootstrap.yaml因此application.yaml需要修改为
server:port: 8082
feign:okhttp:enabled: true # 开启OKHttp连接池支持
hm:swagger:title: 购物车服务接口文档package: com.hmall.cart.controllerdb:database: hm-cart 重启服务发现所有配置都生效了。 测试效果 CartApplication控制台 可以访问swagger接口 拿得到用户的购物车数据数据库起作用 3.2 配置热更新 有很多的业务相关参数将来可能会根据实际情况临时调整。例如购物车业务购物车数量有一个上限默认是10对应代码如下 现在这里购物车是写死的固定值我们应该将其配置在配置文件中方便后期修改。 但现在的问题是即便写在配置文件中修改了配置还是需要重新打包、重启服务才能生效。能不能不用重启直接生效呢 这就要用到Nacos的配置热更新能力了分为两步 在Nacos中添加配置 在微服务读取配置 3.2.1 添加配置到Nacos 首先我们在nacos中添加一个配置文件将购物车的上限数量添加到配置中 注意文件的dataId格式
[服务名]-[spring.active.profile].[后缀名]
文件名称由三部分组成 服务名我们是购物车服务所以是cart-service spring.active.profile就是spring boot中的spring.active.profile可以省略则所有profile共享该配置 后缀名例如yaml 这里我们直接使用cart-service.yaml这个名称则不管是dev还是local环境都可以共享该配置。
配置内容如下
hm:cart:maxAmount: 1 # 购物车商品数量上限 提交配置在控制台能看到新添加的配置 3.2.2 配置热更新读取配置 接着我们在微服务中读取配置实现配置热更新。
在cart-service中新建一个属性读取类 代码如下
Data
Component
ConfigurationProperties(prefix hm.cart)
public class CartProperties {private Integer maxAmount;
} 接着在业务中使用该属性加载类 测试向购物车中添加多个商品 我们在nacos控制台将购物车上限配置为5 无需重启再次测试购物车功能 加入成功
无需重启服务配置热更新就生效了 3.3 动态路由 网关的路由配置全部是在项目启动时由org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator在项目启动的时候加载并且一经加载就会缓存到内存中的路由表内一个Map不会改变。也不会监听路由变更所以我们无法利用配置热更新来实现路由更新。 因此我们必须监听Nacos的配置变更然后手动把最新的路由更新到路由表中。这里有两个难点 如何监听Nacos配置变更 如何把路由信息更新到路由表 3.3.1 监听Nacos配置变更 在Nacos官网中给出了手动监听Nacos配置变更的SDK 如果希望 Nacos 推送配置变更可以使用 Nacos 动态监听配置接口来实现。
public void addListener(String dataId, String group, Listener listener) 请求参数说明 参数名 参数类型 描述 dataId string 配置 ID保证全局唯一性只允许英文字符和 4 种特殊字符.、:、-、_。不超过 256 字节。 group string 配置分组一般是默认的DEFAULT_GROUP。 listener Listener 监听器配置变更进入监听器的回调函数。 示例代码
String serverAddr {serverAddr};
String dataId {dataId};
String group {group};
// 1.创建ConfigService连接Nacos
Properties properties new Properties();
properties.put(serverAddr, serverAddr);
ConfigService configService NacosFactory.createConfigService(properties);
// 2.读取配置
String content configService.getConfig(dataId, group, 5000);
// 3.添加配置监听器
configService.addListener(dataId, group, new Listener() {Overridepublic void receiveConfigInfo(String configInfo) {// 配置变更的通知处理System.out.println(recieve1: configInfo);}Overridepublic Executor getExecutor() {return null;}
}); 这里核心的步骤有2步 创建ConfigService目的是连接到Nacos 添加配置监听器编写配置变更的通知处理逻辑 第一步
由于我们采用了spring-cloud-starter-alibaba-nacos-config自动装配因此ConfigService已经在com.alibaba.cloud.nacos.NacosConfigAutoConfiguration中自动创建好了 NacosConfigManager中是负责管理Nacos的ConfigService的具体代码如下 因此只要我们拿到NacosConfigManager就等于拿到了ConfigService第一步就实现了。 第二步
编写监听器。虽然官方提供的SDK是ConfigService中的addListener不过项目第一次启动时不仅仅需要添加监听器也需要读取配置因此建议使用的API是这个
String getConfigAndSignListener(String dataId, // 配置文件idString group, // 配置组走默认long timeoutMs, // 读取配置的超时时间Listener listener // 监听器
) throws NacosException;
既可以配置监听器并且会根据dataId和group读取配置并返回。我们就可以在项目启动时先更新一次路由后续随着配置变更通知到监听器完成路由更新。 3.3.2 更新路由 更新路由要用到org.springframework.cloud.gateway.route.RouteDefinitionWriter这个接口
package org.springframework.cloud.gateway.route;import reactor.core.publisher.Mono;/*** author Spencer Gibb*/
public interface RouteDefinitionWriter {/*** 更新路由到路由表如果路由id重复则会覆盖旧的路由*/MonoVoid save(MonoRouteDefinition route);/*** 根据路由id删除某个路由*/MonoVoid delete(MonoString routeId);}这里更新的路由也就是RouteDefinition之前我们见过包含下列常见字段 id路由id predicates路由匹配规则 filters路由过滤器 uri路由目的地
将来我们保存到Nacos的配置也要符合这个对象结构将来我们以JSON来保存格式如下
{id: item,predicates: [{name: Path,args: {_genkey_0:/items/**, _genkey_1:/search/**}}],filters: [],uri: lb://item-service
} 注意事项这里用JSON格式是方便后续类型的转换。 以上JSON配置就等同于
spring:cloud:gateway:routes:- id: itemuri: lb://item-servicepredicates:- Path/items/**,/search/** 3.3.3 实现动态路由 首先 我们在网关gateway引入依赖
!--统一配置管理--
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-config/artifactId
/dependency
!--加载bootstrap--
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-bootstrap/artifactId
/dependency 然后在网关gateway的resources目录创建bootstrap.yaml文件内容如下
spring:application:name: gatewaycloud:nacos:server-addr: 192.168.150.101config:file-extension: yamlshared-configs:- dataId: shared-log.yaml # 共享日志配置 接着修改gateway的resources目录下的application.yml把之前的路由移除最终内容如下
server:port: 8080 # 端口
hm:jwt:location: classpath:hmall.jks # 秘钥地址alias: hmall # 秘钥别名password: hmall123 # 秘钥文件密码tokenTTL: 30m # 登录有效期auth:excludePaths: # 无需登录校验的路径- /search/**- /users/login- /items/** 然后在gateway中定义配置监听器 其代码如下
Slf4j
Component
RequiredArgsConstructor
public class DynamicRouteLoader {private final RouteDefinitionWriter writer;private final NacosConfigManager nacosConfigManager;// 路由配置文件的id和分组private final String dataId gateway-routes.json;private final String group DEFAULT_GROUP;// 保存更新过的路由idprivate final SetString routeIds new HashSet();PostConstructpublic void initRouteConfigListener() throws NacosException {// 1.注册监听器并首次拉取配置String configInfo nacosConfigManager.getConfigService().getConfigAndSignListener(dataId, group, 5000, new Listener() {Overridepublic Executor getExecutor() {return null;}Overridepublic void receiveConfigInfo(String configInfo) {updateConfigInfo(configInfo);}});// 2.首次启动时更新一次配置updateConfigInfo(configInfo);}private void updateConfigInfo(String configInfo) {log.debug(监听到路由配置变更{}, configInfo);// 1.反序列化ListRouteDefinition routeDefinitions JSONUtil.toList(configInfo, RouteDefinition.class);// 2.更新前先清空旧路由// 2.1.清除旧路由for (String routeId : routeIds) {writer.delete(Mono.just(routeId)).subscribe();}routeIds.clear();// 2.2.判断是否有新的路由要更新if (CollUtils.isEmpty(routeDefinitions)) {// 无新路由配置直接结束return;}// 3.更新路由routeDefinitions.forEach(routeDefinition - {// 3.1.更新路由writer.save(Mono.just(routeDefinition)).subscribe();// 3.2.记录路由id方便将来删除routeIds.add(routeDefinition.getId());});}
}重启网关任意访问一个接口比如 http://localhost:8080/search/list?pageNo1pageSize1 发现是404无法访问。 接下来我们直接在Nacos控制台添加路由路由文件名为gateway-routes.json类型为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}
] 无需重启网关稍等几秒钟后再次访问刚才的地址 网关路由成功了