微服务开发与实战Day04

作者 : admin 本文共22339个字,预计阅读时间需要56分钟 发布时间: 2024-06-10 共3人阅读

一、网关路由

网关:就是网络的关口,负责请求的路由、转发、身份校验。

微服务开发与实战Day04插图

在SpringCloud中网关的实现包括两种:

微服务开发与实战Day04插图(1)

1. 快速入门

Spring Cloud Gateway

微服务开发与实战Day04插图(2)

步骤:

①新建hm-gateway模块

微服务开发与实战Day04插图(3)

②引入依赖pom.xml(hm-gateway)



    
        hmall
        com.heima
        1.0.0
    
    4.0.0

    hm-gateway

    
        11
        11
        UTF-8
    

    
        
        
            com.heima
            hm-common
            1.0.0
        
        
        
            org.springframework.cloud
            spring-cloud-starter-gateway
        
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
        
        
        
            org.springframework.cloud
            spring-cloud-starter-loadbalancer
        
    
    
        ${project.artifactId}
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

③新建启动类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个服务

微服务开发与实战Day04插图(4)

测试

微服务开发与实战Day04插图(5)

微服务开发与实战Day04插图(6)

微服务开发与实战Day04插图(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}
StripPrefix去除请求路径中的N段前缀StripPrefix=1,则路径/a/b转发时只保留/b
… …

二、网关登录校验

1. 如何在网关转发之前做登录校验?

2. 网关如何将用户信息传递给微服务

3. 如何在微服务之间传递用户信息?

微服务开发与实战Day04插图(8)

网关请求处理流程

微服务开发与实战Day04插图(9)

1. 自定义过滤器

网关过滤器有两种,分别是:

  • GatewayFilter:路由过滤器,作用于任意指定的路由;默认不生效,要配置到路由后生效。
  • GlobalFilter:全局过滤器,作用范围是所有路由;声明后自动生效。

两种过滤器的过滤方法签名完全一致:

微服务开发与实战Day04插图(10)

1.1 自定义过滤器 GlobalFilter

自定义GlobalFilter比较简单,直接实现GlobalFilter接口即可

微服务开发与实战Day04插图(11)

步骤: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 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

微服务开发与实战Day04插图(12)

1.2 自定义过滤器 GatewayFilter

自定义GatewayFilter不是直接实现GatewayFilter,而是实现AbstractGatewayFilterFactory,示例如下:

微服务开发与实战Day04插图(13)

步骤: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 {
    @Override
    public GatewayFilter apply(Object config) {
        return new OrderedGatewayFilter(new GatewayFilter() {
            @Override
            public Mono 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

微服务开发与实战Day04插图(14)

微服务开发与实战Day04插图(15)

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 {
    @Override
    public GatewayFilter apply(Config config) {
        return new OrderedGatewayFilter(new GatewayFilter() {
            @Override
            public Mono 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 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

微服务开发与实战Day04插图(16)

2. 实现登录校验

需求:在网关中基于过滤器实现登录校验功能

提示:黑马商城是基于JWT实现的登录校验,目前相关功能在hm-service模块。我们可以将其中的JWT工具拷贝到gateway模块,然后基于GlobalFilter来实现登录校验。

步骤:hm-gateway模块

①从hm-service拷贝以下登录校验相关的文件到hm-gateway模块

微服务开发与实战Day04插图(17)

②在AuthProperties里加上component注解

微服务开发与实战Day04插图(18)

③复制相关配置到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 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 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. 网关传递用户

微服务开发与实战Day04插图(19)

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 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 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 queryMyCarts(@RequestHeader(value="user-info", required = false)String userInfo){
   System.out.println("userInfo = " + userInfo);
   return cartService.queryMyCarts();
}

③重启CartApplication和GatewayApplication

微服务开发与实战Day04插图(20)

3.2 在hm-common中编写SpringMVC拦截器,获取登录用户

需求:由于每个微服务都可能有获取登录用户的需求,因此我们直接在hm-common模块定义拦截器,这样微服务只需要引入依赖即可生效,无需重复编写。

①在hm-common模块新增一个UserInfoInterceptor

微服务开发与实战Day04插图(21)

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 queryMyCarts() {
        // 1.查询我的购物车列表
        List carts = lambdaQuery().eq(Cart::getUserId, UserContext.getUser()).list();
        if (CollUtils.isEmpty(carts)) {
            return CollUtils.emptyList();
        }

        // 2.转换VO
        List 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

微服务开发与实战Day04插图(22)

4. OpenFeign传递用户

微服务项目中的很多业务要多个微服务共同合作完成,而这个过程中也需要传递登录用户信息,例如:

微服务开发与实战Day04插图(23)

OpenFeign中提供了一个拦截器接口,所有由OpenFeign发起的请求都会先调用拦截器处理请求:

微服务开发与实战Day04插图(24)

其中的RequestTemplate类中提供了一些方法可以让我们修改请求头:

微服务开发与实战Day04插图(25)

微服务开发与实战Day04插图(26)

步骤:hm-api模块

①pom.xml



  com.heima
  hm-common
  1.0.0

②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());
                }
            }
        };
    }
}

三、配置管理

  • 微服务重复配置过多,维护成本高
  • 业务配置经常变动,每次修改都要重启服务
  • 网关路由配置写死,如果变更要重启网关

微服务开发与实战Day04插图(27)

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=

微服务开发与实战Day04插图(28)

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

微服务开发与实战Day04插图(29)

logging:
  level:
    com.hmall: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"

微服务开发与实战Day04插图(30)

knife4j:
  enable: true
  openapi:
    title: ${hm.swagger.title:黑马商城接口文档}
    description: ${hm.swagger.desc:黑马商城接口文档}
    email: zhanghuyi@itcast.cn
    concat: 虎哥
    url: http://www.itcast.cn
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - ${hm.swagger.package}

1.2 拉取共享配置

基于NacosConfig拉取共享配置代替微服务的本地配置

微服务开发与实战Day04插图(31)

步骤:cart-service模块

①引入依赖 pom.xml(cart-service)

  
  
      com.alibaba.cloud
      spring-cloud-starter-alibaba-nacos-config
  
  
  
      org.springframework.cloud
      spring-cloud-starter-bootstrap
  

②新建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

微服务开发与实战Day04插图(32)微服务开发与实战Day04插图(33)

注:其他模块也可以进行同样的配置

2. 配置热更新

配置热更新:当修改配置文件中的配置时,微服务无需重启即可使配置生效

前提条件:

①nacos中要有一个与微服务名有关的配置文件

微服务开发与实战Day04插图(34)

②微服务中要以特定方式读取需要热更新的配置属性

微服务开发与实战Day04插图(35)

2.1 实现购物车添加商品上限的配置热更新

需求:购物车的限定数量目前是写死在业务中的,将其改为读取配置文件属性,并将配置交给Nacos管理,实现热更新。

微服务开发与实战Day04插图(36)

步骤:

①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;

/**
 * 

* 订单详情表 服务实现类 *

* * @author 虎哥 * @since 2023-05-05 */ @Service @RequiredArgsConstructor // 必备参数的构造函数 public class CartServiceImpl extends ServiceImpl 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())); } } // ... ... }

③新增配置管理

微服务开发与实战Day04插图(37)

hm:
  cart:
    maxItems: 1

④重启CartApplication

微服务开发与实战Day04插图(38)

⑤maxItems修改为10 (无需重启,即刻生效)

微服务开发与实战Day04插图(39)

微服务开发与实战Day04插图(40)

3. 动态路由

要实现动态路由首先要将路由配置保存到Nacos,当Nacos中的路由配置变更时,推送最新配置到网关,实时更新网关中的路由信息。

我们需要完成两件事情:

①监听Nacos配置变更的消息

②当配置变更时,将最新的路由信息更新到网关路由表

3.1 监听Nacos配置

监听Nacos配置变更可以参考官方文档:Java SDK

微服务开发与实战Day04插图(41)

步骤:hm-gateway模块

①在pom.xml(hm-gateway)



  com.alibaba.cloud
  spring-cloud-starter-alibaba-nacos-config



  org.springframework.cloud
  spring-cloud-starter-bootstrap

②新增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来更新路由表

微服务开发与实战Day04插图(42)

3.3 路由配置语法

为了方便解析从Nacos读取道德路由配置,推荐使用json格式的路由配置,模板如下:

微服务开发与实战Day04插图(43)

步骤: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 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 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());
        }

    }
}

②动态添加路由信息

微服务开发与实战Day04插图(44)

[
    {
        "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测试

微服务开发与实战Day04插图(45)

本站无任何商业行为
个人在线分享 » 微服务开发与实战Day04
E-->