• 服务网关GateWay基础


    1. 网关基础介绍

    1.1 网关是什么

    ​ 在微服务架构中,由于系统和服务的细分,导致系统结构变得非常复杂, 为了跨平台、统一集中管理api,同时不暴露后置服务。甚至有时候需要对请求进行一些安全、负载均衡、限流、熔断、灰度等中间操作,基于此类种种的客观需求一个类似综合前置的系统就产生了,这就是API网关(API Gateway)。(网关)是一种在微服务架构中使用的服务器端组件,用于管理请求的转发和路由。它充当了系统的入口点,接收来自客户端的请求,并将其转发到相应的微服务实例上。

    1.2 为啥要用网关

    项目使用了微服务架构后,没有统一的网关服务,客户端直接与具体的微服务交互

    1. 安全隐患:服务端直接暴露,服务器易受攻击。
    2. 日志、监控、鉴权等统一操作 需要在不同微服务上重复处理
    3. 服务拆分或者合并,重构困难。

    随着微服务架构在落地的过程中,如何统一入口,统一安全、高性能等问题成为新的挑战,所以微服务网关的重要性就体现出来了。

    网关的核心功能

    网关提供了统一的请求入口

    • 统一入口:可以进行日志、监控、限流等统一化的需求,屏蔽服务协议细节。
    • 权限控制:统一入口处可以进行用户的认证授权、请求的过滤、验证。
    • 负载均衡:根据规则将请求路由到不同的后端服务。

    以下是 Gateway 的详解:

    1. 请求路由:Gateway 提供了灵活且强大的路由功能,可以根据请求的 URL、请求头、请求方法等条件将请求路由到不同的后端微服务。通过配置路由规则,可以实现 URL 重写、请求过滤、请求转发等功能。
    2. 负载均衡:Gateway 集成了负载均衡的能力,可以以均衡的方式将请求分发给多个后端微服务的实例。通过与负载均衡器(如 Ribbon、Spring Cloud LoadBalancer)配合使用,可以提高系统的可扩展性和容错性。
    3. 安全认证与授权:Gateway 可以作为安全层,处理身份验证和授权的问题。它可以集成认证服务器(如 Spring Security、OAuth2),对请求进行身份验证,并根据配置的权限规则控制访问授权。
    4. 请求过滤与增强:Gateway 具备强大的过滤器功能,可以对请求进行修改、验证、限流等操作。通过编写自定义的过滤器,可以实现请求日志记录、请求转换、请求重试等功能。
    5. 动态路由:Gateway 支持动态路由的功能,可以根据配置的规则动态地将请求路由到目标微服务。这使得系统能够动态调整路由规则,而无需重新部署或重启网关。
    6. 监控与统计:Gateway 提供了丰富的监控和统计功能,可以通过集成监控平台(如 Spring Cloud Sleuth、Prometheus)来收集和展示关于请求流量、性能指标等方面的数据。

    总结来说,Gateway 是微服务架构中的入口组件,它通过灵活的路由、负载均衡、安全认证、请求过滤等功能,为系统提供了高效、可靠的请求处理机制。它可以帮助实现微服务的解耦、横向扩展、安全性和可维护性。在使用 Gateway 时,需要考虑系统的架构需求、性能要求和安全要求,并根据实际情况进行配置和定制化开发。

    1.3 常见的网关组件

    对比这几种网关可以从多个方面进行分析,包括性能、扩展性、生态支持、功能丰富程度等。下面是对这几种网关的详细分析:

    Nginx

    优点:

    1. 性能优越: Nginx以其高性能而闻名,适用于高并发和大规模负载的场景。
    2. 反向代理和负载均衡: 提供强大的反向代理和负载均衡功能。
    3. 灵活性: 可以通过Lua脚本进行二次开发,扩展功能。

    缺点:

    1. 微服务集成: 不直接支持微服务架构,需要额外的开发和配置。
    2. 功能相对简单: 相较于专门的微服务网关,功能相对简单,需要额外的配置和开发来实现一些高级功能。
    Netflix Zuul

    优点:

    1. 微服务集成: 与Spring Cloud整合,天然支持微服务架构。
    2. 易于使用: 使用简单,适合初学者。

    缺点:

    1. 性能问题: 在大规模微服务架构中,性能可能会成为瓶颈。
    2. 更新慢: 由于Netflix停止维护,可能会缺乏新功能和及时的安全更新。
    Spring Cloud Gateway

    优点:

    1. WebFlux: 基于WebFlux,支持异步和非阻塞式的请求处理。
    2. 灵活性: 支持Groovy脚本,可以灵活扩展功能。
    3. 微服务集成: 与Spring Cloud生态系统无缝集成,天然支持微服务。

    缺点:

    1. 相对年轻: 相对较新,可能在一些功能上不如成熟的解决方案。
    Kong

    优点:

    1. 高可用性: 基于Nginx和Lua,提供高可用性和易扩展性。
    2. 插件系统: 支持丰富的插件系统,提供了许多现成的扩展。
    3. 易扩展: 易于扩展和定制,适合大型复杂场景。

    缺点:

    1. 相对庞大: 相对于简单的网关,Kong可能显得庞大一些。
    APISIX

    优点:

    1. 云原生: 基于云原生技术,支持高性能和可扩展性。
    2. 插件系统: 提供丰富的插件系统,易于定制和扩展。
    3. 易于使用: 设计简单,易于使用。

    缺点:

    1. 相对较新: 与其他解决方案相比,APISIX相对较新,可能在一些方面不如成熟的解决方案。
    综合比较
    • 性能: Nginx在性能上有优势,但其他网关也在不同程度上提供了高性能。
    • 微服务集成: Spring Cloud Gateway和Netflix Zuul天然支持微服务架构。
    • 灵活性: Kong和APISIX提供了更丰富的插件系统,具有更高的灵活性和可扩展性。
    • 生态支持: Spring Cloud Gateway和Zuul是Spring Cloud生态的一部分,与其他Spring Cloud组件天然集成。
    • 社区活跃度: Nginx、Spring Cloud Gateway、Kong都有活跃的社区支持,而Netflix Zuul的社区支持相对较少。
    • 成熟度: Nginx和Netflix Zuul相对来说更成熟,而Spring Cloud Gateway、Kong和APISIX相对较新。

    选择适合自己需求的网关取决于具体的业务场景、性能要求、团队技术栈等因素。

    2. gateWay的使用

    2.1 springCloud整合gateway
    • maven依赖
     
     <dependency>
         <groupId>org.springframework.cloudgroupId>
         <artifactId>spring-cloud-starter-gatewayartifactId>
     dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 添加配置
    spring:
      cloud:
        gateway:
          routes:
            - id: example-route   # 路由的Id,没有固定规则但要求唯一,建议配合服务名
              uri: http://com.xiu.com  # 匹配后提供服务的路由根路径地址
              predicates:                
                - Path=/api-gateway/xiu/**         # 断言 路径相匹配的进行路由
              filters:
                - StripPrefix=1        #去掉请求的第一个路径元素
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    上述配置意思:当请求路径匹配 /api-gateway/xiu/** 时,会将请求转发到 http://com.xiu.com,并去除掉请求路径的第一个路径元素。

    比如访问http://ip:port/api-gateway/xiu/sayHello 则会通过路由访问真实的请求地址为http://com.xiu.com/sayHello

    路由主要有四个配置:

    • 路由id(id): 没有固定规则但要求唯一,建议配合服务名
    • 路由目标(uri): 匹配后提供服务的路由地址
    • 路由断言(predicates):判断路由的规则 - Path=/xiu/** 需要匹配的路由
    • 路由过滤器(filters):对请求或响应做处理
    • 开启网关

    应用程序主类上添加 @EnableGateway 注解或者 spring.cloud.gateway.enabled=true启用 Spring Cloud Gateway。

    其实默认网关是开启的,所以不加上述配置也行也可以

    2.2 GateWay的相关用法

    下面是GateWay的请求流程概述

    在这里插入图片描述

    一个请求进入网关,首先请求会通过GateWay Handler Mapping 进行路由匹配,匹配成功后请求会交由GateWay Web Handler。请求会经过一系列的过滤器链(责任链模式),进行每个filer的前置和后置处理。

    2.3 GateWay路由使用示例
    基本用法

    ​ Route 路由 网关的基本组成部分。它由一个ID、一个目标URI、一组谓词predicates和一组过滤器filters组成

    • 配置predicates
    spring:
      cloud:
        gateway:
          routes:
          - id: after_route
            uri: https://example.org
            predicates:
            #简写 
            - Cookie=mycookie,regexp
            #完整写法
            - name: Cookie
              args:
                name: mycookie2
                regexp: regexp
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    上述配置的predicates 标识 请求中必须要包含Cookie参数 且cookie的name 为mycookie,value为regex 正则表达式匹配的值。

    • 配置filters
    spring:
      cloud:
        gateway:
          routes:
          - id: add_request_header_route
            uri: https://example.org
            filters:
            #简写
            - AddRequestHeader=token, 111
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如上,会给匹配的请求添加 请求头为token=111的header信息。

    转发/重定向

    转发是GateWay最普通的功能,我们大部分的路由规则也都是进行转发处理。

    spring:
      cloud:
        gateway:
          routes:
            # 转发路由配置 路径匹配/forward/** 请求路由到http://www.baidu.com上
            - id: forward-route
              uri: http://www.baidu.com
              predicates:
                - Path=/forward/**
              filters:
                - StripPrefix=1
            # 重定向路由配置  路径匹配/redirect/** 请求路由到http://www.jd.com上,原来的uri不起作用了
            - id: redirect-route
              uri: https://www.baidu.com
              predicates:
                - Path=/redirect/**
              filters:
                - RedirectTo=302,https://www.jd.com
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    负载请求

    ​ 在实际工作场景中,大部分需要路由转发的微服务都是多个实例,并部署到不同的服务机器上(不同的ip或者不同的域名),所以这里的uri 直接写死ip不合适。此处可以将网关服务作为微服务注册到注册中心,其可以从注册中心获取到所有服务实例同时路由配置中uri 使用lb://,这样,客户端请求网关,路径匹配成功后,网关会将请求负载到多个实例中的其中一个,这样服务变更不用频繁修改路由配置。

    • 引入注册中心依赖
     <dependency>
         <groupId>com.alibaba.cloudgroupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
     dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 开启服务注册

    ​ 主类中添加@EnableDiscoveryClient

    • yml配置
    spring:
      cloud:
        nacos:
          discovery:
            server-addr: ip:port
            namespace: my-namespace
        gateway:
          routes:  
            - id: customer
              #此处uri没有写死,而是使用了(load blance) lb://${service_name} 负载均衡策略到某个具体实例
              uri: lb://customer-server
              predicates:
                - Path=/api-gateway/customer/**
              filters:
                - StripPrefix=2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    动态路由

    上述服务路由配置都是硬编码配置这种配置方式,在启动网关服务后,将无法修改路由配置,若有新服务上线的话,则需要重新部署网关服务。如果使用动态路由则能很好的解决上述问题。

    • 添加pom依赖
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-actuatorartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 暴露网关服务端点
    management:
      endpoints:
        web:
          exposure:
            include: '*'
            exclude: env
      endpoint:
        health:
          show-details: always
         
    #通过如下两个url进行访问(端口根据以及ip根据自己设定进行调整)     
    #http://localhost:9001/actuator
    #http://localhost:9001/actuator/gateway/routes      
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 实现动态路由相关功能
    @Service
    public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
    
        @Resource
        private RouteDefinitionWriter routeDefinitionWriter;
    
        //事件发布组件 用于网关运行中 发布RefreshRoutesEvent事件刷新内存中路由配置
        @Resource
        private ApplicationEventPublisher publisher;
    
        @Override
        public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
            this.publisher = applicationEventPublisher;
        }
    
        //新增路由
        public void add(RouteDefinition routeDefinition){
            /**
             * 新增的Actuator Endpoint,刷新路由的时候,先加载路由配置到内存中,
             * 然后再使用RefreshRoutesEvent事件刷新内存中路由配置。
             */
            routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
        }
    
        // 删除路由
        public void delete(String id){
            routeDefinitionWriter.delete(Mono.just(id)).subscribe();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 添加路由入口

      可以通过暴露API接口、或者容器启动的时候添加

      @Service
      public class RouterConfig  implements InitializingBean {
      
          private final static Logger log = LoggerFactory.getLogger(RouterConfig.class);
          @Resource
          private DynamicRouteServiceImpl dynamicRouteService;
      
          @Override
          public void afterPropertiesSet() throws Exception {
              //模拟容器启动过程中 添加动态路由
              //路由数据可以在服务启动过程中从存储层获取
              RouteDefinition route = new RouteDefinition();
              route.setId("example-route");
              route.setUri(new URI("lb://customer-server"));
              //添加 predicates
              List<PredicateDefinition>  predicates = Lists.newArrayList();
              //path 断言
              PredicateDefinition path = new PredicateDefinition();
              path.setName("Path");
              Map<String,String> args = Maps.newHashMap();
              args.put("pattern","/api-gateway/customer/**");
              path.setArgs(args);
              predicates.add(path);
              route.setPredicates(predicates);
              //添加filter
              List<FilterDefinition> filters = Lists.newArrayList();
              FilterDefinition filter = new FilterDefinition();
              filter.setName("StripPrefix");
              Map<String,String> filterArgs = Maps.newHashMap();
              filterArgs.put("_genkey_0","2");
              filter.setArgs(filterArgs);
              filters.add(filter);
              route.setFilters(filters);
      
              String routJson = JSON.toJSONString(route);
              log.info("添加的路由信息:{}",routJson);
      
              dynamicRouteService.add(route);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40

      等价于

            routes:  
              - id: customer
                #此处uri没有写死,而是使用了(load blance) lb://${service_name} 负载均衡策略到某个具体实例
                uri: lb://customer-server
                predicates:
                  - Path=/api-gateway/customer/**
                filters:
                  - StripPrefix=2
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

      访问http://ip:port/api-gateway/customer/v1/customer/sayHello 会转发到customer模块的某个实例节点http://ip:port/v1/customer/sayHello。

    2.5 断言(Predicate)

    断言就是说: 在 什么条件下 才能进行路由转发。 gateWay提供了如下多种断言
    在这里插入图片描述

    自定义断言

    设置传入的age参数在指定范围区间才能正常访问。

    /**
     * 自定义的断言工厂
    * 1.名称必须是 ${配置}+RoutePredicateFactory
    * 2.必须继承AbstractRoutePredicateFactory<配置类> */
    @Component public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> { public AgeRoutePredicateFactory() { super(AgeRoutePredicateFactory.Config.class); } //用于从配置文件中获取参数值赋值到配置类中的属性上 @Override public List<String> shortcutFieldOrder() { //这里的顺序要跟配置文件中的参数顺序一致 return Arrays.asList("minAge", "maxAge"); } //断言 @Override public Predicate<ServerWebExchange> apply(Config config) { return new Predicate<ServerWebExchange>() { @Override public boolean test(ServerWebExchange serverWebExchange) { //从serverWebExchange获取传入的参数 String ageStr = serverWebExchange.getRequest().getQueryParams().getFirst("age"); if (StringUtils.isNotEmpty(ageStr)) { int age = Integer.parseInt(ageStr); return age > config.getMinAge() && age < config.getMaxAge(); } return true; } }; } //配置类 @Data public static class Config{ private int minAge; private int maxAge; } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    上面的代码创建了一个AgeRoutePredicateFactory 自定义的断言组件。自定义的断言类名是"Age"+"RoutePredicateFactory"的形式。

    所以在断言语句中可以配置Age。

    配置 指定参数在年龄范围的请求才会被路由到对应的。

        gateway:
          routes:
            #负载均衡
            - id: customer
              # 此处uri没有写死,而是使用了(load balance) 
              # lb://${service_name} 负载均衡策略到某个具体实例
              uri: lb://customer-server
              predicates:
                - Path=/api-gateway/customer/**
                - Age=24,35 # 限制年龄只有在24到35岁之间的人才能找到工作
              filters:
                - StripPrefix=2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    请求

    http://127.0.0.1:8804/api-gateway/customer/v1/customer/sayHello?age=30 #请求正常转发
    http://127.0.0.1:8804/api-gateway/customer/v1/customer/sayHello?age=15 #请求不会转发
    #注意转发到的真实请求地址需要接收GET请求的参数age。
    
    • 1
    • 2
    • 3
    2.6 过滤器(Filter )

    在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器。如下

    过滤器工厂作用参数
    AddRequestHeader为原始请求添加HeaderHeader的名称及值
    AddRequestParameter为原始请求添加请求参数参数名称及值
    AddResponseHeader为原始响应添加HeaderHeader的名称及值
    DedupeResponseHeader剔除响应头中重复的值需要去重的Header名称及去重策略
    Hystrix为路由引入Hystrix的断路器保护HystrixCommand的名称
    FallbackHeaders为fallbackUri的请求头中添加具体的异常信息Header的名称
    PrefixPath为原始请求路径添加前缀前缀路径
    PreserveHostHeader为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host
    RequestRateLimiter用于对请求限流,限流算法为令牌桶keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus
    RedirectTo将原始请求重定向到指定的URLhttp状态码及重定向的url
    RemoveHopByHopHeadersFilter为原始请求删除IETF组织规定的一系列Header默认就会启用,可以通过配置指定仅删除哪些Header
    RemoveRequestHeader为原始请求删除某个HeaderHeader名称
    RemoveResponseHeader为原始响应删除某个HeaderHeader名称
    RewritePath重写原始的请求路径原始路径正则表达式以及重写后路径的正则表达式
    RewriteResponseHeader重写原始响应中的某个HeaderHeader名称,值的正则表达式,重写后的值
    SaveSession在转发请求之前,强制执行WebSession::save操作
    secureHeaders为原始响应添加一系列起安全作用的响应头无,支持修改这些安全响应头的值
    SetPath修改原始的请求路径修改后的路径
    SetResponseHeader修改原始响应中某个Header的值Header名称,修改后的值
    SetStatus修改原始响应的状态码HTTP 状态码,可以是数字,也可以是字符串
    StripPrefix用于截断原始请求的路径使用数字表示要截断的路径的数量
    Retry针对不同的响应进行重试retries、statuses、methods、series
    RequestSize设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回 413 Payload Too Large请求包大小,单位为字节,默认值为5M
    ModifyRequestBody在转发请求之前修改原始请求体内容修改后的请求体内容
    ModifyResponseBody修改原始响应体的内容修改后的响应体内容
    Default为所有路由添加过滤器过滤器工厂名称及值
    自定义局部Filter
    /**
     * 自定义的局部过滤器工厂
    * 1.名称必须是 ${配置}+GatewayFilterFactory
    * 2.必须继承AbstractGatewayFilterFactory<配置类> */
    @Component public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> { public LogGatewayFilterFactory() { super(Config.class); } @Override public List<String> shortcutFieldOrder() { return Arrays.asList("consoleLog"); } @Override public GatewayFilter apply(Config config) { return new GatewayFilter() { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { if (config.isConsoleLog()) { System.out.println("consoleLog已经开启了...."); } return chain.filter(exchange); } }; } @Data public static class Config{ private boolean consoleLog; } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    配置

        gateway:
          routes:
           - id: customer
              #此处uri没有写死,而是使用了(load balance) lb://${service_name} 负载均衡策略到某个具体实例
              uri: lb://customer-server
              predicates:
                - Path=/api-gateway/customer/**
                - Age=24,35 # 限制年龄只有在24到35岁之间的人才能找到工作
              filters:
                - StripPrefix=2
                - Log=true #开启日志打印
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    自定义全局Filter
    /**
     * 自定义的局部过滤器工厂
    * 需要实现GlobalFilter、Ordered
    */
    @Component public class MyGlobalGateWayFactory implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { System.out.println("全局过滤器前置处理"); return chain.filter(exchange) .then(Mono.fromRunnable( () -> System.out.println("全局过滤器后置处理") )); } @Override public int getOrder() { return 0; } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    使用@Component直接会生效。

  • 相关阅读:
    C语言 —— 初步入门知识(选择与循环语句、函数、数组、操作符、关键字和#define)
    有向图计数优化版原理及C++实现
    redis bitmap数据结构之java对等操作
    OPPO Reno7/Reno7pro/Reno8/Reno8Pro刷机后需要账号激活,如何解锁删除欢太账号绑定
    JAVA--类的继承
    若依的环境的部署以及系统的运行
    数据结构与算法 | 第二章:线性表
    stable diffusion中的negative prompt是如何工作的
    300. 最长递增子序列
    浅写一个3D旋转相册
  • 原文地址:https://blog.csdn.net/liushangzaibeijing/article/details/138026557