springCloud特色知识记录(基于黑马教程2024年)

目录

[Nacos 简介](#Nacos 简介)

[Nacos 的特点](#Nacos 的特点)

[Nacos 的使用步骤可以查看黑马教程文档:‍‌​‌​⁠​⁠​​​​​‬​​​​‍‌‬⁠​​‬​​​​‍​⁠​​​⁠​​‬​⁠​​day03-微服务01 - 飞书云文档 (feishu.cn)](#Nacos 的使用步骤可以查看黑马教程文档:‍‌‌⁠⁠‬‍‌‬⁠‬‍⁠⁠‬⁠day03-微服务01 - 飞书云文档 (feishu.cn))

[OpenFeign 的使用步骤](#OpenFeign 的使用步骤)

[Feign Logger.Level 的选项](#Feign Logger.Level 的选项)

网关

自定义过滤器(拦截器)

网关到微服务的用户传递

META-INF是什么?

微服务之间的用户传递

配置管理

如何进行配置管理?

如果进行热部署更新?

更新路由

微服务保护和分布式管理

雪崩问题

sentinel是什么?

如何使用sentinel?

熔断的具体实现步骤

主要逻辑

如何解决雪崩问题

分布式事务(多个服务或数据库之间数据一致性的问题)

​编辑

什么事seata?

XA模式

AT模式


Nacos 简介

Nacos 是阿里巴巴开源的一个动态服务发现、配置管理和服务管理平台。它在微服务架构中常被用作服务注册中心和配置中心。

Nacos 的特点
  1. 服务发现与注册:支持 DNS 和 HTTP 协议的服务发现。
  2. 动态配置管理:支持实时动态配置刷新。
Nacos 的使用步骤可以查看黑马教程文档: ‍‌​‌​⁠​⁠​​​​​‬​​​​‍‌‬⁠​​‬​​​​‍​⁠​​​⁠​​‬​⁠​​day03-微服务01 - 飞书云文档 (feishu.cn)

通常Nacos会和OpenFeign结合使用 更加方便

OpenFeign的产生就是为了让远程调用像本地方法调用一样简单。

使用步骤:

1.引入服务注册和服务发现的依赖

XML 复制代码
<!--nacos 服务注册发现-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos 服务注册发现-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  1. 配置Nacos地址
cpp 复制代码
spring:
  cloud:
    nacos:
      server-addr: 自己的虚拟机Ip:8848

OpenFeign 的使用步骤

  1. 引入依赖

    XML 复制代码
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  2. 开启 Feign 功能 在主类上添加 @EnableFeignClients 注解。

    java 复制代码
    @SpringBootApplication
    @EnableFeignClients
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
  3. 定义 Feign 客户端 使用 @FeignClient 注解标注接口,指定调用的服务名称。

    java 复制代码
    @FeignClient(name = "service-name")
    public interface MyFeignClient {
        @GetMapping("/api/endpoint")
        String getEndpoint();
    }
  4. 调用 Feign 接口 注入定义的 Feign 接口并调用方法。

    java 复制代码
    @RestController
    public class MyController {
        @Autowired
        private MyFeignClient myFeignClient;
    
        @GetMapping("/call")
        public String callService() {
            return myFeignClient.getEndpoint();
        }
    }

    如果将这个代理的专门使用一个包提取出来,那么我们需要在启动类上面添加扫描包的注解

java 复制代码
package com.hmall.pay;

import com.hmall.api.config.DefaultFeignConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@MapperScan("com.hmall.pay.mapper")
@SpringBootApplication
@EnableFeignClients(basePackages =  "com.hmall.api.client" ,defaultConfiguration = DefaultFeignConfig.class)
public class PayApplication
{
    public static void main(String[] args) {SpringApplication.run(PayApplication.class, args);}
}

在 Spring Boot 的微服务项目中,上述代码配置了 defaultConfiguration 属性来指定一个默认的配置类 DefaultFeignConfig,它用于 Feign 客户端的默认行为设置。具体来说,这是为了增强日志打印功能,方便开发者在调用微服务时追踪和调试。

以下是DefaultFeignConfig的内容:

java 复制代码
package com.hmall.api.config;

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

public class DefaultFeignConfig
{
    @Bean
    public Logger.Level feignLoggerLevel()
    {
        return Logger.Level.FULL;
    }
}
Feign Logger.Level 的选项
  • NONE:不记录任何日志(默认值)。
  • BASIC:记录请求方法、URL、响应状态码以及执行时间。
  • HEADERS:在 BASIC 的基础上,记录请求和响应的头信息。
  • FULL:记录请求和响应的所有信息,包括头、体、元数据。

网关

服务拆分之后面临两大问题:

1.服务地址过多,前端不知道如何请求谁

  1. 每个服务都可以需要登录信息

自定义过滤器(拦截器)

NettyRoutingFilter 是 Spring Cloud Gateway 中的一个核心过滤器,它负责将请求真正路由到目标服务,并处理响应的回传。简单来说,这是网关在处理请求时,实际完成路由和转发的核心组件

网关到微服务的用户传递

网关层的请求拦截: 在微服务架构中,为了提高安全性和请求处理的一致性,需要在网关层对所有请求进行统一拦截。具体要求如下:

  • 放行无需登录的请求:对于公共接口或资源,不进行Token校验,直接放行。
  • 拦截需要登录的请求 :对于需要登录的请求,检查Request中是否包含有效的Token。
    • 如果Token不存在或无效,返回HTTP状态码401 (UNAUTHORIZED),表示用户未登录,并终止请求的后续处理(调用response.setComplete())。
    • 如果Token有效,解析Token获取用户的唯一标识(如用户ID),将该用户ID通过ServerWebExchange注入到请求头中,然后继续放行请求。
java 复制代码
 String id = userId.toString();
        ServerWebExchange swe = exchange.mutate().
                request(build -> build.header("user-info", id)).build();

微服务中如何获取用户ID: 通常情况下,微服务可以通过读取请求头中的user-info字段获取用户ID。然而,如果每个微服务都单独实现一个拦截器,会导致代码重复、不便于维护。

为了解决这个问题,可以将通用的拦截器逻辑抽取到common模块。所有微服务都引入common模块,直接复用其中的拦截器。这种方式可以避免重复开发,提高代码的复用性和一致性。但是这个时候springBoot无法扫描到这个拦截器的包,因此拦截器不会失效。所以要配置spring.factories 文件

基于Spring Boot的拦截器配置: 微服务通常基于Spring Boot,而不是传统的Spring MVC。如果直接使用Spring MVC的拦截器,在Spring Boot环境下可能会报错。

总结:为了解决上述问题,可以通过在common模块中为拦截器配置META-INF文件,同时在拦截器类上添加@ConditionalOnClass(DispatcherServlet.class)注解。这样可以确保拦截器仅在支持Spring MVC的环境中加载,从而避免不兼容问题。

META-INF是什么?

META-INF 是Java程序的一个特殊目录,用于存放配置文件和元数据信息。它通常位于JAR文件的根目录下,主要包含以下类型的内容:

  1. MANIFEST.MF 文件

    • Java的清单文件,用于描述JAR包的信息。
    • 包括主类、版本号、依赖项等。
  2. Spring Boot相关配置

    • 例如,spring.factories 文件,用于注册自动配置类。
    • 格式为键值对,可以定义拦截器、过滤器等组件的自动加载逻辑。
  3. 服务注册文件

    • 用于SPI(服务提供接口)机制,例如META-INF/services目录下的文件定义了特定接口的实现类。

示例:在common模块中的spring.factories

bash 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.hmall.common.config.MyBatisConfig,\
  com.hmall.common.config.JsonConfig,\
   com.hmall.common.config.MvcConfig

微服务之间的用户传递

如果是微服务之间的用户Id传递的话,其实获取到的用户id的值是null,因为微服务之间的网络请求不是通过网关的,所以网关无法在请求头中设置userId,其他服务也不可以获取到userId的值了

微服务之间是通过openFeign进行网络请求转发的,在转发之前我们是可以获得用户Id的,所以我们可以在请求发送之前设置拦截器,如果我们在每一个微服务都加一个拦截器太麻烦了。所以我们选择加在专门写FeignClient的模块中(hm-api)

那么我们应该如何改造转发的请求呢?在转发之前我们是可以获取到用户id的,所以我们可以借助Feign中提供的一个拦截器接口:feign.RequestInterceptor

java 复制代码
public interface RequestInterceptor {

  /**
   * Called for every request. 
   * Add data using methods on the supplied {@link RequestTemplate}.
   */
  void apply(RequestTemplate template);
}

我们只需要实现这个接口,然后实现apply方法,利用RequestTemplate类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。

java 复制代码
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 requestTemplate) {
                // 获取用户Id
                Long user = UserContext.getUser();
               if(user == null) {
                   // 如果为空则直接跳过
                   return;
               }
                // 存入请求头当中
                requestTemplate.header("user-info",user.toString());
            }
        };
    }

}
  • 网关部分:用户身份校验,并将用户信息存入请求头。
  • 微服务部分 :使用拦截器提取用户信息存入 ThreadLocal,微服务间通过 OpenFeign 的 RequestInterceptor 传递用户信息。

配置管理

在微服务架构中,服务数量众多且配置管理复杂,常常导致重复代码和维护成本增加。通过 Nacos 的配置管理功能,我们能够实现配置的统一管理和动态更新,提升开发效率并减少运维压力。

在微服务架构中,每个服务都需要独立管理自己的配置文件,例如数据库连接(JDBC)、日志配置、Swagger 接口文档配置等。这些配置中往往存在大量重复内容,既增加了维护成本,也容易引发配置不一致问题。

Nacos 是一款集服务注册与配置管理于一体的工具,通过将公共配置提取到 Nacos 中并实现动态加载,可以有效减少代码冗余、降低维护难度并支持配置实时更新。

如何进行配置管理?

引入 Maven 依赖

在 Spring Boot 项目中,添加以下依赖:

XML 复制代码
  <!--nacos配置管理-->
  <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  </dependency>
  <!--读取bootstrap文件-->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-bootstrap</artifactId>
  </dependency>
  1. 在Nacos网址上面添加配置,分别提取jdbc-mysql配置,swagger配置,日志的配置

jdbc共享配置:

bash 复制代码
spring:
  datasource:
    url: jdbc:mysql://${hm.db.host:192.168.88.130}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8&useSSL=false&allowPublicKeyRetrieval=true&autoReconnect=true&failOverReadOnly=false
    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

日志的共享配置:

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

swagger的共享配置:

bash 复制代码
knife4j:
  enable: true
  openapi:
    title: ${hm.swagger.title:"黑马商城购物车接口文档"}
    description: ${hm.swagger.desc:"黑马商城购物车接口文档"}
    email: [email protected]
    concat: 虎哥
    url: https://www.itcast.cn
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - com.hmall.cart.controller
          - ${hm.swagger.package}
  1. 创建bootstrap.yaml 公共配置是动态的,所以我们需要在 bootstrap.yaml进行配置

必须配置其 spring: application: name,profiles: active: ,cloud:nacos:config:extension

bash 复制代码
spring:
  application:
    name: cart-service # 服务名称
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 192.168.88.130 # nacos地址
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 共享配置
          - dataId: shared-jdbc.yml # 共享mybatis配置
          - dataId: shared-log.yml # 共享日志配置
          - dataId: shared-swagger.yml # 共享日志配置

注意这里的shared-configs的dataId的配置名一定要和Nacos里面的一模一样 ,如果 dataId 与 Nacos 中的配置名称不一致,服务将无法加载到对应的共享配置,从而导致启动失败或无法正确读取配置

  1. 剩余部分就需要在application.yaml里面配置
bash 复制代码
server:
  port: 8082
feign:
  okhttp:
    enabled: true # 开启OKHttp连接池支持
hm:
  swagger:
    title: 购物车服务接口文档
    package: com.hmall.cart.controller
  db:
    host: 192.168.88.130 # 修改为你自己的虚拟机IP地址
    database: hm-cart
如果进行热部署更新?

进入Nacos网站http://192.168.88.130:8848/nacos 开始添加配置

使用默认的 应用名称.yaml(例如,cart-service.yaml),这样无需显式在 bootstrap.yaml 中指定 dataId

什么是默认的?

应用名称作为默认 Data ID

Spring Boot 项目的 spring.application.name 属性会作为 Nacos 配置的默认 Data ID

例如: 如果你的 bootstrap.yaml 配置如下:

bash 复制代码
spring:
  application:
    name: cart-service
  cloud:
    nacos:
      server-addr: 192.168.88.130:8848

那么 Nacos 会自动尝试加载以下文件作为配置:

bash 复制代码
Data ID: cart-service.yaml
Group: DEFAULT_GROUP
  • 优先加载的文件格式

    默认情况下,Nacos 会加载和 spring.cloud.nacos.config.file-extension 对应的文件格式(默认为 yaml)。

  • 无需显式指定 Data ID

    只要 Nacos 配置文件的 Data IDspring.application.name 一致(并以 .yaml 作为后缀),就会被自动加载,而无需手动在 bootstrap.yaml 中声明 shared-configs 或特定 dataId

配置项前缀匹配prefix 指定的值必须在配置文件中明确存在。例如,hm.cart 对应的配置项必须是以下形式之一:

  • 在对应的yml 中,配置如下,

    bash 复制代码
    hm:
      cart:
        maxAmount: 10

此时对应的配置类的注解@ConfigurationProperties(prefix = "hm.cart")的prefix必须是hm.cart

java 复制代码
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 maxAmount;
}

更新路由

微服务保护和分布式管理

雪崩问题
sentinel是什么?

Sentinel 是阿里巴巴开源的一款用于分布式系统的流量控制和熔断降级的中间件。它主要用于保护微服务的稳定性,帮助应对高并发场景下的流量突增或服务调用异常。

Sentinel 提供了灵活的流量控制规则和强大的监控能力,是 Spring Cloud Alibaba 技术栈中的核心组件之一,用于服务治理和高可用保障。

如何使用sentinel?
  1. 引入依赖
XML 复制代码
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2021.0.3.0</version>
</dependency>
  1. 连接sentinel-dashboard控制台,需要配置控制台,修改application.yaml文件,添加下面内容:
bash 复制代码
spring:
  cloud: 
    sentinel:
      transport:
        dashboard: localhost:8090
  1. 由于我们的SpringMVC接口是按照Restful风格设计,因此购物车的查询、删除、修改等接口全部都是/carts路径,所以我们需要在application.yaml里面添加 http-method-specify: true # 开启请求方式前缀
bash 复制代码
spring:
  cloud:
    sentinel:
      transport:
        dashboard: 127.0.0.1:8090
      http-method-specify: true # 开启请求方式前缀

‌‍‬‍‌​​​​​​⁠​​​⁠​‍‍​​⁠‍​​‍​​​​⁠⁠‬​⁠​​‬​‌‌​‍​‬​‬详细请点击黑马教程:day05-服务保护和分布式事务 - 飞书云文档 (feishu.cn)

接下来详细介绍一下熔断的解决办法

熔断的具体实现步骤

步骤 1:配置 Sentinel 的熔断规则

Sentinel 可视化控制台 中配置熔断规则,基于:

  • 异常比例(当调用失败率超过设定值时触发)。
  • 异常数(当调用失败总数超过设定值时触发)。
  • 响应时间(当接口响应时间超过设定值时触发)。

示例:

  • 熔断条件:某接口失败率 >50% 且调用量 >10,在 1 分钟内触发熔断。
  • 熔断恢复:在熔断 5 秒后恢复尝试。

步骤 2:编写 FallbackFactory

当服务触发熔断时,FallbackFactory 提供降级逻辑。正常会去调用ItemClient接口,但是当ItemClinet接口对应的Controller的处理出现异常或者请求被限流了,就会走这个fallback"备选方案",它也会创建一个ItemClient,去返回一些提示信息。以 ItemClientFactoryFallback 为例:

主要逻辑
  1. 实现 FallbackFactory 接口。
  2. create 方法中捕获异常信息 Throwable
  3. 定义服务降级逻辑:
    • 非关键业务返回默认值(如返回空集合)。
    • 关键业务抛出业务异常,触发事务回滚,确保数据一致性。

代码解读

java 复制代码
package com.hmall.api.fallback;

import com.hmall.api.client.ItemClient;
import com.hmall.api.dto.ItemDTO;
import com.hmall.api.dto.OrderDetailDTO;
import com.hmall.common.exception.BizIllegalException;
import com.hmall.common.utils.CollUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;

import java.util.Collection;
import java.util.List;

@Slf4j
public class ItemClientFactoryFallback implements FallbackFactory<ItemClient> {
    @Override
    public ItemClient create(Throwable cause) {
        return new ItemClient() {
            @Override
            public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
                log.error("远程调用ItemClient#queryItemByIds方法出现异常,参数:{}", ids, cause);
                // 查询购物车允许失败,查询失败,返回空集合
                return CollUtils.emptyList();
            }

            @Override
            public void deductStock(List<OrderDetailDTO> items) {
                // 库存扣减业务需要触发事务回滚,查询失败,抛出异常
                throw new BizIllegalException(cause);
            }
        };
    }
}

步骤 3:在配置类中注册为 Bean

原因

  • 默认情况下,Spring Boot 不会扫描非显式标注为 @Component 的类。
  • 需要显式将 FallbackFactory 注册为 Bean,才能生效。

代码解读

DefaultFeignConfig 配置类中:

  1. 注册 ItemClientFactoryFallback
java 复制代码
package com.hmall.api.config;


import com.hmall.api.fallback.ItemClientFactoryFallback;
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 requestTemplate) {
                // 获取用户Id
                Long user = UserContext.getUser();
               if(user == null) {
                   // 如果为空则直接跳过
                   return;
               }
                // 存入请求头当中
                requestTemplate.header("user-info",user.toString());
            }
        };
    }

    // 注册为Bean
    @Bean
    public ItemClientFactoryFallback itemClientFallbackFactory(){
        return new ItemClientFactoryFallback();
    }

}

步骤 4:在服务接口中绑定降级逻辑

使用 @FeignClient 注解绑定服务,并指定:

  • value:目标微服务名称。
  • fallbackFactory:对应的 FallbackFactory 类。
  • configuration:自定义的 Feign 配置类。

代码解读

java 复制代码
@FeignClient(
    value = "item-service",
    fallbackFactory = ItemClientFactoryFallback.class,
    configuration = DefaultFeignConfig.class
)
public interface ItemClient {
    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);

    @PostMapping("/stock/deduct")
    void deductStock(@RequestBody List<OrderDetailDTO> items);
}

如何解决雪崩问题

  1. 快速失败

    • 熔断器在触发后立即返回降级结果,避免资源被耗尽。
  2. 默认降级逻辑

    • 通过 FallbackFactory 提供兜底逻辑,为非关键业务返回默认值,降低错误扩散的影响。
  3. 保护核心业务

    • 关键业务如库存扣减,触发异常并回滚,确保数据一致性。
  4. 监控与恢复

    • Sentinel 自动监控系统恢复状态,当异常比例降低时自动关闭熔断器,恢复正常调用。

分布式事务(多个服务或数据库之间数据一致性的问题)

‍‌‌​​​‍​​​​​​⁠‍​⁠​‬​‍‬⁠​​​​​‬​⁠​‌​‌‍⁠‬​‬⁠​​​‍day05-服务保护和分布式事务 - 飞书云文档 (feishu.cn)

什么事seata?

Seata (Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的一款分布式事务解决方案。它为分布式系统中的事务一致性问题提供了简单、高效的解决方案,支持多种事务模型,帮助开发者轻松实现分布式事务。

在微服务架构中,各服务通常独立部署,拥有独立的数据库。这种设计虽然带来了高可用性和扩展性,但也引入了分布式事务问题(如跨服务的数据一致性)。Seata 专注于保证事务的一致性。

XA模式

首先我们可以在Nacos中的共享shared-seata.yaml配置文件中设置:

复制代码
seata: 
        data-source-proxy-mode: XA

然后我们要利用**@GlobalTransactional标记分布式事务的入口方法即可**

AT模式

RabbitMQ

详细查看请查看RabbitMQ高级篇-CSDN博客

相关推荐
Asthenia04121 小时前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz9651 小时前
ovs patch port 对比 veth pair
后端
Asthenia04121 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom2 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide2 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9652 小时前
qemu 网络使用基础
后端
Asthenia04122 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04122 小时前
Spring 启动流程:比喻表达
后端
Asthenia04123 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua3 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫