SpringCloud简明教程

本文所有代码:

https://github.com/tianhaowang-6/cloudstudy.git

1基本概念:

1.1单一架构

一个工程对应一个war包,订单服务、库存服务、仓储服务、积分服务都在一个服务中,运行在一个tomcat上,all in one 的策略。单机模式
image.png

1.2演进过程

将单一架构进行水平拆分,垂直拆分。
水平拆分,不同层次负责不同功能。例如数据库层,请求处理层,控制跳转层
垂直拆分,不同模块负责不同功能。例如订单服务、库存服务、仓储服务、积分服务。

1.3分布式架构

分布式与集群的不同:
分布式:每个应用提供不同的功能。
集群:每个提供相同的功能。

2springcloud

springcloud微服务架构,利用它的最基本的五个组件Eureka、Ribbon、Feign、Hystrix、Zuul实现快速的构建分布式系统。

2.1基本组件简介

注册中心:Eureka

c/s架构,我们创建的Eureka应用,一般作为服务端,其他应用作为客户端
Eureka Server是一个注册中心,里面有一个注册表,保存了各服务所在的机器和端口号
主要用于注册管理springcloud微服务。

负载均衡:Ribbon

管理同一功能的应用,实现负载均衡。

声明式远程调用方法:Feign

用于处理远程调用。

熔断降级监控:Hystrix

用于处理熔断,降级

网关:Zuul

统一访问路径。

3准备工作

3.1父工程主要进行管理版本

导入依赖:

<dependencyManagement>
  <dependencies>
    <!-- 导入 SpringCloud 需要使用的依赖信息 -->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>Greenwich.SR2</version>
      <type>pom</type>
      <!-- import 依赖范围表示将 spring-cloud-dependencies 包中的依赖信息导入 -->
      <scope>import</scope>
    </dependency>
    <!-- 导入 SpringBoot 需要使用的依赖信息 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>2.1.6.RELEASE</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

3.2创建通用子工程

创建通用工程的目的是管理后续代码中使用的公用代码。
pom.xml编写

    <parent>
        <artifactId>cloudparent</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudcommon</artifactId>

创建一个employee类,后面会使用到,作为数据传输的

public class Employee {
    private int id;
    private String name;
    private double salary;

    public Employee() {
    }

    public Employee(int id, String name, double salary) {
        this.id = id;
        this.name = name;
        this.salary = salary;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Employee{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", salary=" + salary +
            '}';
    }
}

4注册中心Eureka

4.1创建一个eureka子工程

创建也给Eureka注册中心,监听client请求,用于注册应用。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

创建主启动类:注意添加@EnableEurekaServer标识为主启动类。

@EnableEurekaServer // 标记为EurefaServer
@SpringBootApplication
public class CloudMainType {
    public static void main(String[] args) {
        SpringApplication.run(CloudMainType.class,args);
    }
}

添加application.yml配置我们需要的信息

server:
	port: 5000
spring:
  application:
    name: cloud-consumer
eureka:
	instance:
		hostname: localhost
	  client: #关闭很多功能,这里是eureka服务端
      registerWithEureka: false # 由于自己就是注册中心所以不用注册
      fetchRegistry: false # 自己就是注册中心,不用给出注册信息。
      serviceUrl:			#客户端访问eureka的地址
        defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

再被注册应用上添加依赖,和配置信息,实现成功注册:

4.2创建一个服务提供子工程

image.png
为provider工程添加依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--这里就会用到上面提供的cloudcommon工程  -->
<dependency>
    <groupId>org.example</groupId>
    <artifactId>cloudcommon</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

创建主启动类:

@SpringBootApplication
    public class CloudMainType {
        public static void main(String[] args) {
            SpringApplication.run(CloudMainType.class,args);
        }
    }

配置application.yml

server:
	port: 1000
spring:
  application:
    name: cloud-consumer

编写一个handler处理请求。

@RestController
public class EmpHandler {
    @RequestMapping("/provider/get/emp/remote")
    public Employee getEmpRemote(HttpServletRequest request){
        int serverPort = request.getServerPort();
        return new Employee(555,"tom333"+serverPort,123.123);
    }
}

4.3创建一个消费工程

image.png
用于向provider发起请求。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.example</groupId>
    <artifactId>cloudcommon</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

创建主启动类:

@SpringBootApplication
    public class CloudMainType {
        public static void main(String[] args) {
            SpringApplication.run(CloudMainType.class,args);
        }
    }

创建配置类提供RestTemplate

@Configuration
public class CloudConfig {
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

配置application.yml

server:
	port: 4000	

创建handler方法。请求处理请求。

@RestController
public class HumanResourceHandler {
    @Autowired
    private RestTemplate restTemplate;
    @RequestMapping("/consume/get/emp")
    public Employee getEmployeeRemote(){
        // 远程方法调用的ip与主机
        String host="http://localhost:1000";
        // 请求的路径
        String url="/provider/get/emp/remote";
        return restTemplate.getForObject(host+url,Employee.class);
    }
}

实际操作:

  • 运行eureka服务,运行provider服务,运行consumer服务
  • 请求到consumer服务 localhost:4000/consume/get/emp
  • 请求会到provider中。

这里是eureka的界面,里面有两个服务(application),就是我们注册进去的。
image.png

这样做还是有很多问题,例如consumer写死了请求路径,既然eureka都知道服务的ip和端口,为服务指定一个名称,这样不就好标识了吗。

5ribbon

然后使用eureka+ribbon转发请求,通过微服务名代替ip+端口号的方式。降低了耦合度。
使用ribbon不用单独创立一个子工程,只需要导入依赖,使用就可。
导入依赖

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

上方在创建provider和consumer的时候就对工程进行取名了。

spring:
  application:
    name: cloud-xxx #这里进行取名

在consumer的CloudConfig类的获取RestTemplate方法上添加一个注解。

@Bean
    @LoadBalanced // 这里需要配置一下
    public RestTemplate getRestTemplate() {
    return new RestTemplate();
}

开启该注解后,就能解析服务名和主机地址之间的映射关系。重新编写一个consumer的方法。

    @RequestMapping("/consume/get/emp")
    public Employee getEmployeeRemote(){
//        String host="http://localhost:1000";
        // eureka+ribbon 可以通过微服务名代替ip+端口号
        String host="http://cloud-provider";
        String url="/provider/get/emp/remote";
        return restTemplate.getForObject(host+url,Employee.class);
    }

ribbon通过服务名转换为实际ip:port的过程,eureka有服务名到ip:port的映射。
image.png
然而我们的服务往往不是单一的,我们同一个provider服务可能部署在多个服务器上,我们需要以集群的方式启动这些服务器。管理这些服务器。

5.1创建provider集群提供服务

复制cloudprovider子工程 创建3个provider。ip+port表示为一个微服务。
image.png
三个服务名都是cloud-provider
provider 使用端口与之前相同,1000端口
provider1使用端口是,2000端口
provider2使用端口是,3000端口
为了表示我们确实调用了不同服务,在provider中使用request.getServerPort()来获取端口信息.

@RestController
public class EmpHandler {

    @RequestMapping("/provider/get/emp/remote")
    public Employee getEmpRemote(HttpServletRequest request){
        int serverPort = request.getServerPort();// 获取服务端口信息。
        return new Employee(555,"tom333"+serverPort,123.123);
    }
}

浏览器发送请求。三个provider轮询被访问。
image.png

6feign

6.1基本思想

consumer调用provider方法的操作就是远程调用。中间通过common进行了映射。
image.png

6.2使用feign实现远程方法调用

创建common工程
提供了一个common,通常在项目中命名为api工程,我们这里为了简单,就直接使用common工程作为api管理接口。

  • 导入依赖
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 创建一个远程调用的服务接口:
@FeignClient("cloud-provider")
 public interface EmployeeRemoteService {
 @RequestMapping("/provider/get/emp/remote")
    public Employee getEmployeeRemote();
 }   

创建一个feign-consumer工程:与之前的consumer不同了

  • 导入依赖
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.example</groupId>
    <artifactId>cloudcommon</artifactId>
    <version>1.0-SNAPSHOT</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  </dependency>
</dependencies>
  • 创建一个feign客户端
// 启用 Feign 客户端功能
@EnableFeignClients
    @SpringBootApplication
    public class CloudMainType {
        public static void main(String[] args) {
            SpringApplication.run(CloudMainType.class, args);
        }
    }
  • 添加配置信息
server:
  port: 7000
spring:
    application:
      name: cloud-feign-consumer
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:5000/eureka/
feign:
  hystrix:
    enabled: true
  • 创建一个handler发送请求。
@RestController
public class EmpFeignHandler {
    // 看似进行本地调用,实际上调用了common程序的service,然后通过feign调用了
    @Autowired
    private EmployeeRemoteService employeeRemoteService;

    @RequestMapping("/feign/consumer/get/emp")
    public Employee getEmployeeRemote() {
        return employeeRemoteService.getEmployeeRemote();
    }
}

我们这里没有进行参数传递演示。其实和使用springmvc传递参数流程差不多。下面有个例子看看吧
consumer
image.png
commonimage.png
providerimage.png

7Hystrix

7.1介绍

最开始看不懂这些概念不重要,这里就是就理解为处理服务异常的就行了。
正常三个服务正常工作
image.png
但是现在service3由于流量大导致崩坏,导致我们三个服务寄了,实际情况更加复杂。
image.png
现在我们期望service3崩坏后,不影响我们其他服务的正常使用。于是hystrix处理了这种情况。一个服务崩坏后,导致其他服务相继死掉的现象可以理解为雪崩。
下方这些概念可以不用管。
Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多
依赖不可避免的会调用失败,比如超时、异常等,Hystrix 能够保证在一个依赖出问题的
情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故
障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应
(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服
务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延, 乃至雪崩。
Hytrix 能够提供服务降级、服务熔断、服务限流、接近实时的监控等方面的功能。

重点关注
服务熔断:

7.2服务熔断机制:

当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该
节点微服务的调用,快速响应错误信息。当检测到该节点微服务调用响应正常后恢复调用链
路。在 SpringCloud 框架里熔断机制通过 Hystrix 实现。Hystrix 会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是 5 秒内 20 次调用失败就会启动熔断机制。熔断机制的注解是@HystrixCommand。
处理provider模块的错误信息。

  • 添加依赖
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
  • 主启动类添加注解
@EnableCircuitBreaker // 启动断路器功能
@SpringBootApplication
public class CloudMainType {
    public static void main(String[] args) {
        SpringApplication.run(CloudMainType.class,args);
    }
}

common添加一个类,作为请求处理返回对象。

public class ResultEntity<T> {
    public static final String SUCCESS = "SUCCESS";
    public static final String FAILED = "FAILED";
    public static final String NO_MESSAGE = "NO_MESSAGE";
    public static final String NO_DATA = "NO_DATA";
    /**
* 操作成功,不需要返回数据
* @return
*/
    public static ResultEntity<String> successWithoutData() {
        return new ResultEntity<String>(SUCCESS, NO_MESSAGE, NO_DATA);
    }
    /**
* 操作成功,需要返回数据
* @param data * @return
*/
    public static <E> ResultEntity<E> successWithData(E data) {
        return new ResultEntity<>(SUCCESS, NO_MESSAGE, data);
    }
    /**
* 操作失败,返回错误消息
* @param message * @return
*/
    public static <E> ResultEntity<E> failed(String message) {
        return new ResultEntity<>(FAILED, message, null);
    }
    private String result;
    private String message;
    private T data;
    public ResultEntity() {
        // TODO Auto-generated constructor stub
    }
    public ResultEntity(String result, String message, T data) {
        super();
        this.result = result;
        this.message = message;
        this.data = data;
    }
    @Override
    public String toString() {
        return "ResultEntity [result=" + result + ", message=" + message + ", data=" + data + "]";
    }
    public String getResult() {
        return result;
    }
    public void setResult(String result) {
        this.result = result;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

provider方法,添加处理方法。
@HystrixCommand(fallbackMethod = “getEmpBackup”)注解进行处理异常,跳转到指定方法。

// 第一个方法主要用于处理请求。
// 通过 fallbackMethod 属性指定断路情况下要调用的备份方法
@HystrixCommand(fallbackMethod = "getEmpBackup")
    @RequestMapping("/provider/circuit/breaker/get/emp")
    public ResultEntity<Employee> getEmp(@RequestParam("signal") String signal) {
        System.out.println("getEmp!!!!!!!!!!");
        System.out.println("signal="+signal+"bang".equals(signal));
        if("bang".equals(signal)) {
            // 抛出异常,会被@HystrixCommand(fallbackMethod = "getEmpBackup")捕获进行处理。
            throw new RuntimeException();
        }
        return ResultEntity.successWithData(new Employee(666, "sam666", 666.66));
    }

// 异常出现后的处理方法。
    public ResultEntity<Employee> getEmpBackup(@RequestParam("signal") String signal) {
        return ResultEntity.failed("circuit break workded,with signal="+signal);
    }

7.3服务降级

服务降级处理是在客户端(Consumer 端)实现完成的,与服务端(Provider 端)没有关系。
当某个 Consumer 访问一个 Provider 却迟迟得不到响应时执行预先设定好的一个解决方案,
而不是一直等待。
在common添加依赖

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

添加一个FallbackFactory

// 请注意自动扫描包的规则
// 比如:feign-consumer 工程需要使用 MyFallBackFactory,那么 MyFallBackFactory 应该在feign-consumer 工程的主启动类所在包或它的子包下
    // 简单来说:哪个工程用这个类,哪个工程必须想办法扫描到这个类
    @Component
@Component
public class MyFallBackFactory implements FallbackFactory<EmployeeRemoteService> {
    // cause 对象是失败原因对应的异常对象
    @Override
    public EmployeeRemoteService create(Throwable cause) {
        return new EmployeeRemoteService() {
            @RequestMapping("/provider/save/emp")
            public Employee saveEmp(@RequestBody Employee employee) {
                return null;
            }
            @RequestMapping("/provider/get/emp/by/id")
            public Employee getEmployeeById(@RequestParam("empId") Integer empId) {
                return null;
            }

            @RequestMapping("/provider/circuit/breaker/get/emp")
            public ResultEntity<Employee> getEmp(@RequestParam("signal") String signal){
                System.out.println("getEmp");
                return new ResultEntity<>();
            }

            @RequestMapping("/provider/get/emp/remote")
            public Employee getEmployeeRemote() {
                System.out.println("MyFallBackFactory+getEmployeeRemote");
                return new Employee(444, "call provider failed,fall back here, reason is "+cause.getClass().getName()+" "+cause.getMessage(), 444.444);
            }
        };
    }
}

然后再common的feign接口(就是远程调用的接口)中添加fallbackFactory属性,指定consumer请求provider失败的的处理规则。

// 在@FeignClient 注解中增加 fallbackFactory 属性 
// 指定 consumer 调用 provider 时如果失败所采取的备用方案
// fallbackFactory 指定 FallbackFactory 类型的类,保证备用方案返回相同类型的数据
//@FeignClient("cloud-provider")
@FeignClient(value="cloud-provider", fallbackFactory= MyFallBackFactory.class)
public interface EmployeeRemoteService {
    @RequestMapping("/provider/get/employee/by/id")
    public Employee getEmployeeById(@RequestParam("empId") Integer empId);
    @RequestMapping("/provider/get/emp/remote")
    public Employee getEmployeeRemote();
    @RequestMapping("/provider/circuit/breaker/get/emp")
    public ResultEntity<Employee> getEmp(@RequestParam("signal") String signal);
}

在consumer的application中开启hystrix:

feign:
  hystrix:
  	enabled: true

测试:前提手动将所有的provider停止:
然后发送请求得到效果
image.png

7.4监控provider工程

provider中导入依赖,被监控。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

配置 application.yml

management:
  endpoints:
    web:
      exposure:
        include: hystrix.stream

添加监控工程:cloudhystrixdashboard
添加依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
    </dependencies>

开启仪表盘功能:

// 启用 Hystrix 仪表盘功能
@EnableHystrixDashboard
@SpringBootApplication
public class CloudMainType {
    public static void main(String[] args) {
        SpringApplication.run(CloudMainType.class, args);
    }
}

配置application.yml

server:
  port: 8000
spring:
  application:
    name: cloud-dashboard

使用步骤

  • 访问表盘工程首页 http://localhost:8000/hystrix
  • 填写参数,这里使用最简单的方式:http://localhost:1000/actuator/hystrix.stream

image.png

注意要访问有熔断功能的方法。
例如这种:
image.png
直接查看监控数据本身

  • http://localhost:1000/actuator/hystrix.stream
  • 说明 1:http://localhost:1000 访问的是被监控的 provider 工程
  • 说明 2:/actuator/hystrix.stream 是固定格式
  • 说明 3:如果从 provider 启动开始它的方法没有被访问过,那么显示的数
  • 据只有“ping:”,要实际访问一个带熔断功能的方法才会有实际数据。

image.png

8Zuul网关:

Zuul 和 Eureka 进行整合,将 Zuul 自身注册为 Eureka 服务治理下的应用,同时从 Eureka 中获得其他微服务的信息,也即以后的访问微服务都是通过 Zuul 跳转后获得。
zuul提供了代理,路由,过滤功能。
配置Zuul后的访问规则

8.1基本原理

image.png

8.2创建zuul工程

导入依赖:

<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
  </dependency>
</dependencies>

配置信息:

server:
	port: 9000
spring:
  application:
  	name: zuul-gateway
eureka:
	client:
  	serviceUrl:
			defaultZone: http://localhost:5000/eureka/

开启Zuul功能:

// 启用 Zuul 代理功能
@EnableZuulProxy
@SpringBootApplication
public class CloudMainType {
    public static void main(String[] args) {
        SpringApplication.run(CloudMainType.class, args);
    }
}

测试:http://localhost:9000/cloud-feign-consumer/feign/consumer/get/emp
路径解析:
image.png
cloud-feign-consumer这样的服务名不够优雅。

8.3修改配置

zuul:
  routes:
    emp: # 这个是配置名称,底层使用map的key
      serviceId: cloud-feign-consumer # 服务名
      path: /consumer/** #新的路径,**表示多层路径匹配,不加就不能进行多层路径匹配

http://localhost:9000/cloud-feign-consumer/feign/consumer/get/emp

http://localhost:9000/consumer/feign/consumer/get/emp
都能进行访问

由于更改了路径以前的访问就不希望被访问

zuul:
	ignored-services: # 忽略指定微服务名称,让用户不能通过微服务名称访问
	- cloud-feign-consumer

http://localhost:9000/cloud-feign-consumer/feign/consumer/get/emp

http://localhost:9000/consumer/feign/consumer/get/emp
上方不能访问了

更多配置

zuul:
# ignored-services: 忽略指定微服务名称,让用户不能通过微服务名称访问
# - cloud-feign-consumer
	ignored-services: '*' # 忽略所有微服务名称
	prefix: /maomi # 给访问路径添加统一前缀
	routes:
  employee: # 自定义路由规则的名称,在底层的数据结构中是 Map 的键
 	 serviceId: cloud-feign-consumer # 目标微服务名称,ZuulRoute 类型的一个属性
 	 path: /zuul-emp/** # 用来代替目标微服务名称的路径,ZuulRoute 类型的一个属性
  # /**表示匹配多层路径,如果没有加/**则不能匹配后续的多层路径了

现在的访问路径:http://localhost:9000/maomi/consume/feign/consumer/get/emp
多了个前缀maomi

8.4ZuulFilter

用于过滤请求,使用规则和filter差不多。

@Component
public class MyZuulFilter extends ZuulFilter {
    @Override
    public String filterType() {
        // 这个方法返回过滤器类型,
        // 可选项,pre,route , post ,static
        return "pre";
    }
    @Override
    public int filterOrder() {
        return 0;
    }
    @Override
    public boolean shouldFilter() {
        // 1.获取当前 RequestContext 对象
        RequestContext context = RequestContext.getCurrentContext();
        // 2.获取当前请求对象
        HttpServletRequest request = context.getRequest();
        // 3.获取当前请求要访问的目标地址
        String servletPath = request.getServletPath();
        // 4.打印
        System.err.println("servletPath="+servletPath);
        System.out.println("servletPath+ " +servletPath);

        // true : 过滤,执行run方法
        // false :不过滤,直接放行
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        System.out.println("run()........");// 执行具体过滤逻辑
        return null; // 官方文档说:当前实现会忽略这个返回值,所以返回 null 即可
    }
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值