1. SpringCloud

SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud.
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验:
在这里插入图片描述
SpringCloud基于SpringBoot实现了微服务组件的自动装配,从而提供了良好的开箱即用体验。但对于SpringBoot的版本也有要求:
在这里插入图片描述

2. 拆分服务

2.1 拆分目标

微服务拆分时粒度要小,这其实是拆分的目标。具体可以从两个角度来分析:

  • 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
  • 低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。

2.2 拆分方式

明确了拆分目标,接下来就是拆分方式了。我们在做服务拆分时一般有两种方式:

  • 纵向拆分:按照项目的功能模块来拆分
  • 横向拆分:抽取公共的业务部分,将其作为通用服务,提高复用性

2.3 项目结构

一般微服务项目有两种不同的工程结构:

  • 独立Project:每一个微服务都创建为一个独立的工程,甚至可以使用不同的开发语言来开发,项目完全解耦。
    • 优点:服务之间耦合度低
    • 缺点:每个项目都有自己的独立仓库,管理起来比较麻烦
  • Maven聚合:整个项目为一个Project,然后每个微服务是其中的一个Module
    • 优点:项目代码集中,管理和运维方便(授课也方便)
    • 缺点:服务之间耦合,编译时间较长

2.4 远程调用——RestTemplate

  • 注入RestTemplate到Spring容器
    在这里插入图片描述

  • 发起远程调用
    在这里插入图片描述

3. 服务治理

3.1 注册中心原理

在这里插入图片描述

3.2 Nacos注册中心

参考文档:https://b11et3un53m.feishu.cn/wiki/R4Sdwvo8Si4kilkSKfscgQX0niB

3.3 服务注册

  • 引入nacos discovery依赖

    <!--nacos 服务注册发现-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    
  • 配置Nacos地址

    spring:
      application:
        name: item-service # 服务名称
      cloud:
        nacos:
          server-addr: 192.168.74.130:8848 # nacos地址
    
  • 重启
    访问 http://192.168.74.130:8848/nacos

3.4 服务发现

  • 引入nacos discovery依赖

  • 配置nacos地址

  • 服务发现
    服务发现需要用到一个工具,DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用:
    在这里插入图片描述

    // 1.获取商品id
    Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
    // 2.查询商品
    // 2.1 根据服务名称获取服务的实例列表
    List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
    if(CollUtil.isEmpty(instances)){
        return;
    }
    // 2.2 手写负载均衡,从实例列表中挑选一个实例
    ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
    // 2.3 利用RestTemplate发送http请求,得到http响应
    ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
            instance.getUri()+"/items/list?ids={ids}",
            HttpMethod.GET,
            null,
            new ParameterizedTypeReference<List<ItemDTO>>() {
            },
            Map.of("ids", CollUtil.join(itemIds, ","))
    );
    //解析响应
    if(!response.getStatusCode().is2xxSuccessful()){
        //查询失败,直接结束
        return;
    }
    List<ItemDTO> items = response.getBody();
    

4. OpenFeign

4.1 快速入门

OpenFeign是一个声明式的http客户端,是SpringCloud在Eureka公司开源的Feign基础上改造而来。
官方地址:https://github.com/openFeign/feign
其作用就是基于SpringMVC的常见注解,帮我们优雅的实现http请求的发送

4.1.1 引入依赖

  <!--openFeign-->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
  </dependency>
  <!--负载均衡器-->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-loadbalancer</artifactId>
  </dependency>

4.1.2 启用OpenFeign功能

@EnableFeignClients注解
在这里插入图片描述

4.1.3 编写OpenFeign客户端

@FeignClient("item-service")
public interface ItemClient {

    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}

4.1.4 使用FeignClient

private final Itemclient itemclient

List<ItemDTo>items = itemClient.queryItemByIds(itemIds);

4.2 连接池

Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:

  • HttpURLConnection:默认实现,不支持连接池
  • Apache HttpClient :支持连接池
  • OKHttp:支持连接池

因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.

4.2.1.引入依赖

在cart-service的pom.xml中引入依赖:

<!--OK http 的依赖 -->
<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-okhttp</artifactId>
</dependency>

4.2.2.开启连接池

在cart-service的application.yml配置文件中开启Feign的连接池功能:

feign:
  okhttp:
    enabled: true # 开启OKHttp功能

重启服务,连接池就生效了。

4.3 最佳实践

  • 方案一
    适合各功能是一个单独的项目,耦合度低
    在这里插入图片描述
  • 方案二
    项目是聚合结构,各功能在一个项目下面
    在这里插入图片描述

4.4 日志

OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
    Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。

4.4.1.定义日志级别

在hm-api模块下新建一个配置类,定义Feign的日志级别:

package com.hmall.api.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;

public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.FULL;
    }
}

4.4.2.配置
接下来,要让日志级别生效,还需要配置这个类。有两种方式:

  • 局部生效:在某个FeignClient中配置,只对当前FeignClient生效

    @FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
    
  • 全局生效:在@EnableFeignClients中配置,针对所有FeignClient生效。

    @EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
    

5. 网关

网关:就是网络的关口,负责请求的路由、转发、身份校验。
在这里插入图片描述
在这里插入图片描述

5.1 路由转发

5.1.1 快速入门

  • 创建新模块

  • 引入网关依赖

    <!--网关-->
    <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>
    
  • 编写启动类

  • 配置路由规则

    server:
      port: 8080
    spring:
      application:
        name: gateway
      cloud:
        nacos:
          server-addr: 192.168.150.101:8848
        gateway:
          routes:
            - id: item # 路由规则id,自定义,唯一
              uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
              predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
                - Path=/items/**,/search/** # 这里是以请求路径作为判断规则
            - id: cart
              uri: lb://cart-service
              predicates:
                - Path=/carts/**
    

5.1.2 路由属性

网关路由对应的Java类型是RouteDefinition,其中常见的属性有:

  • id:路由的唯一标示
  • predicates:路由断言,其实就是匹配条件
  • filters:路由过滤条件
  • uri:路由目标地址,lb://代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。
路由断言

Spring提供了12中基本的RoutePredicateFactory实现
在这里插入图片描述

路由过滤器

网关中提供了33种路由过滤器,每种过滤器都有独特的作用
在这里插入图片描述

5.2 网关登录校验

在这里插入图片描述
在这里插入图片描述

5.2.1 自定义过滤器

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

  • GatewayFilter:路由过滤器,作用于任意指定的路由;默认不生效,要配置到路由后生效。
  • GlobalFilter:全局过滤器,作用范围是所有路由;声明后自动生效。
    两种过滤器的过滤方法签名完全一致
自定义过滤器GlobalFilter
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 编写过滤器逻辑
        System.out.println("未登录,无法访问");
        // 放行
        // return chain.filter(exchange);

        // 拦截
        ServerHttpResponse response = exchange.getResponse();
        response.setRawStatusCode(401);
        return response.setComplete();
    }

    @Override
    public int getOrder() {
        // 过滤器执行顺序,值越小,优先级越高
        return 0;
    }
}
自定义GateWayFilter

自定义GatewayFilter不是直接实现GatewayFilter,而是实现AbstractGatewayFilterFactory。
注意:该类的名称一定要以GatewayFilterFactory为后缀!

  • 无参过滤器

    @Component
    public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
        @Override
        public GatewayFilter apply(Object config) {
            return new GatewayFilter() {
                @Override
                public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                    // 获取请求
                    ServerHttpRequest request = exchange.getRequest();
                    // 编写过滤器逻辑
                    System.out.println("过滤器执行了");
                    // 放行
                    return chain.filter(exchange);
                }
            };
        }
    }
    
    spring:
      cloud:
        gateway:
          default-filters:
                - PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器
    
  • 带参过滤器

    @Component
    public class PrintAnyGatewayFilterFactory // 父类泛型是内部类的Config类型
                    extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
    
        @Override
        public GatewayFilter apply(Config config) {
            // OrderedGatewayFilter是GatewayFilter的子类,包含两个参数:
            // - GatewayFilter:过滤器
            // - int order值:值越小,过滤器执行优先级越高
            return new OrderedGatewayFilter(new GatewayFilter() {
                @Override
                public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                    // 获取config值
                    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);
                    // 放行
                    return chain.filter(exchange);
                }
            }, 100);
        }
    
        // 自定义配置属性,成员变量名称很重要,下面会用到
        @Data
        static class Config{
            private String a;
            private String b;
            private String c;
        }
        // 将变量名称依次返回,顺序很重要,将来读取参数时需要按顺序获取
        @Override
        public List<String> shortcutFieldOrder() {
            return List.of("a", "b", "c");
        }
            // 返回当前配置类的类型,也就是内部的Config
        @Override
        public Class<Config> getConfigClass() {
            return Config.class;
        }
    
    }
    
    spring:
      cloud:
        gateway:
          default-filters:
                - PrintAny=1,2,3 # 注意,这里多个参数以","隔开,将来会按照shortcutFieldOrder()方法返回的参数顺序依次复制
    

5.2.2 实现登录校验

https://b11et3un53m.feishu.cn/wiki/UMgpwmmQKisWBIkaABbcwAPonVf

5.2.3 网关传递用户

利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。
在这里插入图片描述

  • AuthGlobalFilter

    //5.传递用户信息
    String userInfo = userId.toString();
    ServerWebExchange swe = exchange.mutate()
            .request(builder -> builder.header("user-info", userInfo))
            .build();
    
  • 拦截器

    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.判断是否获取了用户,如果有,存入ThreadLocal
            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();
        }
    }
    
    @Configuration
    @ConditionalOnClass(DispatcherServlet.class)
    public class MvcConfig implements WebMvcConfigurer {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new UserInfoInterceptor());
        }
    }
    

5.2.4 OpenFeign传递用户

OpenFeign中提供了一个拦截器接口,所有由0penFeign发起的请求都会先调用拦截器处理请求:
这里要借助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发起请求的时候都会调用该方法,传递用户信息。

  • hm.api模块
    DefaultFeignConfig中编写内部类

    @Bean
        public RequestInterceptor requestInterceptor(){
            return new RequestInterceptor() {
                @Override
                public void apply(RequestTemplate requestTemplate) {
                    Long userId = UserContext.getUser();
                    if(userId!=null){
                        requestTemplate.header("user-info",userId.toString());
                    }
    
                }
            };
        }
    

在这里插入图片描述

6. 配置管理

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

6.1 配置共享

6.1.1.添加共享配置

我们在nacos控制台分别添加配置。
首先是jdbc相关配置,在配置管理->配置列表中点击+新建一个配置:
[图片]

在弹出的表单中填写信息:
[图片]

其中详细的配置如下:

spring:
  datasource:
    url: jdbc:mysql://${hm.db.host:192.168.150.101}:${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

注意这里的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: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"

然后是统一的swagger配置,命名为shared-swagger.yaml,配置内容如下:

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

注意,这里的swagger相关配置我们没有写死,例如:

  • title:接口文档标题,我们用了${hm.swagger.title}来代替,将来可以有用户手动指定
  • email:联系人邮箱,我们用了${hm.swagger.email:zhanghuyi@itcast.cn},默认值是zhanghuyi@itcast.cn,同时允许用户利用${hm.swagger.email}来覆盖

6.1.2 拉取共享配置

在这里插入图片描述
基于NacosConfig拉取共享配置代替微服务的本地配置
SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml(或者bootstrap.properties)的文件,如果我们将nacos地址配置到bootstrap.yaml中,那么在项目引导阶段就可以读取nacos中的配置了。

  • 引入依赖

      <!--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>
    
  • 新建bootsrap.yaml

      spring:
      application:
        name: cart-service # 服务名称
      profiles:
        active: dev
      cloud:
        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 # 共享日志配置
    

6.2 配置热更新

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

  • nacos中要有一个与微服务名有关的配置文件
    在这里插入图片描述
    如:cart-service.yaml
    在这里插入图片描述

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

    @Data
    @Component
    @ConfigurationProperties(prefix = "hm.cart")
    public class CartProperties {
        private Integer maxItems;
    }
    
    @Data
    @Component
    //@ConfigurationProperties(prefix = "hm.cart")
    public class CartProperties {
        @Value("${hm.cart.maxItems}")
        private Integer maxItems;
    }
    
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐