Spring WebFlux 实践

WebFlux 学习之路

本文借鉴于 Spring WebFlux 官方文档 ,对 Spring WebFlux进行学习、练习它的功能。涉及到 Reactive 的核心Api操作,操作数据库、Redis、Http请求等方面。

本文的代码仓库:
https://gitee.com/fengsoshuai/webflux-demo

以下说明只粘贴了部分代码,全部代码,需要童鞋去仓库下载哦。

1 、WebFlux 简介

Spring WebFlux 是Spring 框架在 5.0版本之后推出的,异步响应式框架。可以不需要 Servlet Api,也就是能够不使用 Spring Mvc就能对外暴露接口。通过 Reactor 项目,实现了相关的流式操作。

一般有两种写法。

一种是使用 Reactor 提供的流式Api,同时使用 Spring Mvc 的相关注解,使用上和 Spring Mvc 差异性很小。主要在于它返回的数据类型变成了 WebFlux 中的 MonoFlux 了。具体见本项目中对应的模块《webflux-hello-web-demo》。

另一种写法是使用配置的方式,使用 @Confuguration ,并在该配置类中配置 Bean,返回RouterFunction对象。该对象可以有多个,存在优先级时可以使用 @Order 注解。该对象的作用是配置了路由,请求参数的格式等。具体见本项目中对应的《webflux-hello-demo》模块。

另外,Spring WebFlux 大量的使用了 流式API,如果对 Java 8 之后的 Stream 操作不熟悉的同学,可以先去了解 Stream 操作。可以参考一下 这篇文章

2、WebFlux 的数据库操作

本文只探讨对mysql的操作。不同于常规的jdbc的方式,当使用响应式编程对数据库操作时,也有对应的驱动,mysql 的驱动叫 r2dbc。

它的主要特点是,从对数据库的连接,就开始返回了 Publisher 的数据类型。
一般需要引入包:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-r2dbc</artifactId>
        </dependency>
        <dependency>
            <groupId>dev.miku</groupId>
            <artifactId>r2dbc-mysql</artifactId>
            <version>0.8.2.RELEASE</version>
        </dependency>

WebFlux 实践内容

项目结构如下:
在这里插入图片描述

1 、入门案例

在Gitee 仓库中,对应的 webflux-hello-demo模块。完整代码请从代码仓库拉取。

主要是使用 RouterFunction 和 处理器,来替代原先的 SpringMVC的映射界面路由的功能。

1.1 RouterConfiguration

package org.feng.config;

import lombok.extern.slf4j.Slf4j;
import org.feng.handler.RouterHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

/**
 * WebFlux 路由配置
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年06月07日 21时03分
 */
@Slf4j
@Configuration
public class RouterConfiguration {
    @Bean
    public RouterFunction<ServerResponse> routerMapper(RouterHandler handler) {
        log.info("注册路由...");

        // 这里在写完整后,可以考虑使用 import static 来简化代码;虽然牺牲了一部分可读性,但是一些无用代码确实能大面积减少
        return RouterFunctions
                // 根路径
                .nest(RequestPredicates.path("/webflux"),
                        RouterFunctions
                                .route(RequestPredicates.GET("/hello").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::hello)
                                // 追加其他路由
                                .andRoute(RequestPredicates.GET("/getEmpById").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::getEmpById)
                );
    }
}


1.2 RouterHandler

package org.feng.handler;

import lombok.extern.slf4j.Slf4j;
import org.feng.entity.Employee;
import org.feng.entity.dto.EmployeeDTO;
import org.feng.entity.dto.GetEmpRequestDTO;
import org.feng.service.EmployeeService;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;

/**
 * 路由处理器
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年06月07日 21时05分
 */
@Slf4j
@Component
public class RouterHandler {

    @Resource
    private EmployeeService employeeService;

    /**
     * GET 请求 http://localhost/webflux/hello 时调用此方法;
     *
     * @param request 请求体
     * @return 响应
     */
    public Mono<ServerResponse> hello(ServerRequest request) {
        log.info("Hello Webflux 的请求体 {}", request);
        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue("Hello Webflux");
    }

    /**
     * 通过 HTTP 请求的入参id,获得想要的员工的数据
     *
     * @param request 请求体
     * @return 响应
     */
    public Mono<ServerResponse> getEmpById(ServerRequest request) {
        log.info("getEmpById 的请求体 {}", request);

        // 将接口传入的参数,转换为 Mono 对象
        Mono<GetEmpRequestDTO> requestParam = request.bodyToMono(GetEmpRequestDTO.class);
        // 消费请求参数,从 业务实现中查找数据,并消费转换获得最终响应
        Mono<EmployeeDTO> response = requestParam.map(dto -> {
            Mono<Employee> empById = employeeService.getEmpById(dto.getId());

            EmployeeDTO responseEmp = new EmployeeDTO();
            if (!Mono.empty().equals(empById)) {
                // 消费数据
                empById.subscribe(resp -> {
                    responseEmp.setName(resp.getName());
                    responseEmp.setAge(resp.getAge());
                    responseEmp.setBirthday(resp.getBirthday());
                    responseEmp.setJobNo(resp.getJobNo());
                    responseEmp.setHireDay(resp.getHireDay());
                });
            }
            return responseEmp;
        });
        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(response, EmployeeDTO.class);
    }
}


2、操作Mysql

这一部分主要是实现了 mysql 动态数据源的切换。
并且整合了nacos做配置中心,主要是配置了mysql的连接信息。

另外,练习了 Webclient 的常见使用。
对应的代码模块是 webflux-hello-mysql-demo

2.1 动态数据源配置

package org.feng.config;

import dev.miku.r2dbc.mysql.MySqlConnectionConfiguration;
import dev.miku.r2dbc.mysql.MySqlConnectionFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.r2dbc.connection.lookup.AbstractRoutingConnectionFactory;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Mysql 动态数据源
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年06月14日 22时32分
 */
@Slf4j
@Configuration
public class MySqlDynamicDatasourceConfig extends AbstractRoutingConnectionFactory {
    /**
     * 默认租户ID
     */
    private static final String DEFAULT_MERCHANT_ID = "master";
    /**
     * 租户Key,切换数据源时,Context对象设置的key
     */
    public static final String MERCHANT_KEY = "merchantKey";
    /**
     * 数据源连接工厂映射
     */
    private static final Map<Object, Object> CONNECTION_FACTORY_CACHE_MAP = new HashMap<>(8);

    @Resource
    private MysqlDatasourceProperties mysqlDatasourceProperties;

    @Resource
    private MysqlDatasourcePropertiesV2 mysqlDatasourcePropertiesV2;

    @PostConstruct
    private void init() {
        final List<Property> propertyList = mysqlDatasourcePropertiesV2.getPROPERTY_LIST();
        for (Property property : propertyList) {
            MySqlConnectionFactory connectionFactory = MySqlConnectionFactory.from(MySqlConnectionConfiguration.builder()
                    .host(property.getHost())
                    .port(property.getPort())
                    .username(property.getUsername())
                    .password(property.getPassword())
                    .database(property.getDatabase())
                    .connectTimeout(Duration.ofSeconds(property.getConnectTimeout()))
                    .build());

            log.info("注册数据源 租户:{}  数据库名:{}", property.getMerchantKey(), property.getDatabase());
            CONNECTION_FACTORY_CACHE_MAP.put(property.getMerchantKey(), connectionFactory);
        }
        setTargetConnectionFactories(CONNECTION_FACTORY_CACHE_MAP);
        setDefaultTargetConnectionFactory(CONNECTION_FACTORY_CACHE_MAP.get(DEFAULT_MERCHANT_ID));
    }

    @NonNull
    @Override
    protected Mono<Object> determineCurrentLookupKey() {
        return Mono.deferContextual(Mono::just).handle((context, sink) -> {
            Object merchantKey = context.getOrDefault(MERCHANT_KEY, DEFAULT_MERCHANT_ID);
            log.info("使用数据源 {} HashCode {}", merchantKey, CONNECTION_FACTORY_CACHE_MAP.get(merchantKey).hashCode());
            assert merchantKey != null;
            sink.next(merchantKey);
        });
    }
}

2.2 事务配置

package org.feng.config;

import io.r2dbc.spi.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.r2dbc.connection.R2dbcTransactionManager;
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.reactive.TransactionalOperator;

/**
 * 事务配置
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年06月15日 20时44分
 */
@Configuration
public class R2dbcTransactionConfiguration {
    @Bean
    public ReactiveTransactionManager transactionManager(ConnectionFactory connectionFactory) {
        return (new R2dbcTransactionManager(connectionFactory));
    }

    @Bean
    public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) {
        return TransactionalOperator.create(transactionManager);
    }
}

2.3 DAO 操作

package org.feng.repository;

import org.feng.entity.Student;
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * 学生表-数据库操作
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年06月11日 11时01分
 */
@Repository
public interface StudentRepository extends ReactiveCrudRepository<Student, Integer> {
    /**
     * 按照学生ID查找
     *
     * @param id 学生ID
     * @return 对应ID的学生信息
     */
    Mono<Student> getStudentById(Integer id);

    /**
     * 通过学生姓名,模糊查询
     *
     * @param name 学生姓名的一部分
     * @return 学生列表
     */
    @Query("select id, name, age, weight, height from student where name like :name")
    Flux<Student> getStudentsByName(String name);
}

2.4 Service操作

这一步使用了两种操作数据库的方式。


package org.feng.service;

import org.feng.config.MySqlDynamicDatasourceConfig;
import org.feng.entity.Student;
import org.feng.repository.StudentRepository;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.util.List;

/**
 * 学生业务实现
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年06月11日 11时11分
 */
@Service
public class StudentServiceImpl implements StudentService {
    @Resource
    private StudentRepository studentRepository;

    @Resource
    private R2dbcEntityTemplate r2dbcEntityTemplate;

    @Override
    public Mono<Student> getById(Integer id) {
        return studentRepository.getStudentById(id);
    }

    @Override
    public Flux<Student> getStudentsByName(String merchantKey, String name) {
        return studentRepository.getStudentsByName("%" + name + "%")
                .contextWrite(context -> context.put(MySqlDynamicDatasourceConfig.MERCHANT_KEY, merchantKey));
    }

    @Transactional(rollbackFor = RuntimeException.class)
    @Override
    public Mono<Student> saveStudent(Student student) {
        return studentRepository.save(student);
    }

    @Transactional(rollbackFor = RuntimeException.class)
    @Override
    public Flux<Student> saveStudent(List<Student> studentList) {
        return studentRepository.saveAll(studentList);
    }

    @Override
    @Transactional(rollbackFor = RuntimeException.class)
    public Mono<Void> deleteStudent(Integer id) {
        return studentRepository.deleteById(id);
    }

    @Override
    public Flux<Student> selectStudent(Integer currentPage, String subName) {
        // 设置每页显示2条数据
        return r2dbcEntityTemplate.select(
                Query.query(Criteria.where("name").like("%" + subName + "%")).with(PageRequest.of(currentPage - 1, 2)),
                Student.class);
    }
}


3、WebClient的操作

3.1 配置

package org.feng.config;

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.client.reactive.ReactorResourceFactory;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import reactor.netty.resources.LoopResources;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.function.Function;

/**
 * WebClient 配置,参考官方文档,https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-client
 *
 * <br>参考:
 * <br> https://www.freesion.com/article/7381889590/
 * <br> https://blog.csdn.net/weixin_44266223/article/details/122967933
 *
 * @version V1.0
 * @author: fengjinsong
 * @date: 2022年06月16日 15时41分
 */
@Configuration
public class WebClientConfig {

    /**
     * 默认情况下,HttpClient参与持有的全局 Reactor Netty 资源 reactor.netty.http.HttpResources,包括事件循环线程和连接池<br>
     * 但是,这里选择不使用全局资源
     *
     * @return 资源工厂
     */
    private ReactorResourceFactory reactorResourceFactory() {
        ReactorResourceFactory factory = new ReactorResourceFactory();
        factory.setUseGlobalResources(false);
        // 设置一个loop进行http线程管理
        factory.setLoopResources(LoopResources.create("tcp-connect-loop", 30, true));
        // 配置固定大小连接池
        factory.setConnectionProvider(connectionProvider());
        return factory;
    }

    private ConnectionProvider connectionProvider() {
        return ConnectionProvider
                .builder("tcp-connect-pool")
                // 等待超时时间
                .pendingAcquireTimeout(Duration.ofSeconds(6))
                // 最大连接数
                .maxConnections(30)
                // 等待队列大小
                .pendingAcquireMaxCount(300)
                .maxIdleTime(Duration.ofSeconds(200))
                .maxLifeTime(Duration.ofSeconds(200))
                .build();
    }

    @Bean
    public WebClient webClient() {
        Function<HttpClient, HttpClient> mapper = client -> {
            // 连接超时时间
            client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
                    // 连接后的读、写超时
                    .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(10))
                            .addHandlerLast(new WriteTimeoutHandler(10)))
                    // 设置响应超时时间
                    .responseTimeout(Duration.of(6, ChronoUnit.SECONDS))
            ;
            return client;
        };

        ClientHttpConnector connector = new ReactorClientHttpConnector(reactorResourceFactory(), mapper);

        return WebClient.builder()
                // 编解码器对在内存中缓冲数据大小修改
                .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(4 * 1024 * 1024))
                .clientConnector(connector)
                .build();
    }
}

3.2 具体操作

package org.feng.controller;

import org.feng.entity.Student;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 学习使用 webClient
 *
 * @version V1.0
 * @author: fengjinsong
 * @date: 2022年06月17日 09时03分
 */
@RequestMapping("/webclient")
@RestController
public class StudentWebClientController {
    @Resource
    private WebClient webClient;
    private final String BASE_URL = "http://localhost/student";

    @GetMapping("/{id}")
    public Mono<Student> getOneByWebClient(@PathVariable("id") Integer id) {
        return webClient.get().uri(BASE_URL + "/{id}", id).accept(MediaType.APPLICATION_JSON)
                .retrieve().bodyToMono(Student.class);
    }

    @GetMapping("/doPostSingle")
    public Mono<Student> doPostSingle() {
        Student student = new Student();
        student.setName("doPostSingle");

        return webClient.post().uri(BASE_URL + "/postSingle")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(student)
                .retrieve()
                .bodyToMono(Student.class);
    }

    @GetMapping("/doPostForm")
    public Mono<Student> doPostForm() {
        return webClient.post().uri(BASE_URL + "/postForm?studentName={studentName}&namePrefix={namePrefix}", "小明", LocalDate.now().toString())
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .retrieve()
                .bodyToMono(Student.class);
    }

    /**
     * 请求体+路径参数
     */
    @GetMapping("/doPostRequestBody")
    public Mono<Student> doRequestBody() {
        Student student = new Student();
        student.setName("doPostRequestBody");

        int age = ThreadLocalRandom.current().nextInt(20, 30);
        return webClient.post().uri(BASE_URL + "/postRequestBody/{age}", age)
                .contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(student))
                .retrieve()
                .bodyToMono(Student.class);
    }

    /**
     * 请求体+URL参数
     */
    @GetMapping("/doRequestBodyWithParam")
    public Mono<Student> doRequestBodyWithParam() {
        Student student = new Student();
        student.setName("doRequestBodyWithParam");
        int id = ThreadLocalRandom.current().nextInt(20, 30);
        return webClient.post().uri(BASE_URL + "/postRequestBodyWithParam?id={id}", id)
                .contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(student))
                .retrieve()
                .bodyToMono(Student.class);
    }

    @GetMapping("/doFormRequest1")
    public Mono<Student> doFormRequest() {
        Map<String, Object> paramMap = new HashMap<>(4);
        paramMap.put("id", ThreadLocalRandom.current().nextInt(20, 30));
        paramMap.put("name", "小明");

        return webClient.post().uri(BASE_URL + "/formRequest1?name={name}&id={id}", paramMap)
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .retrieve()
                .bodyToMono(Student.class);
    }

    @GetMapping("/doFormRequest2")
    public Mono<Student> testFormParam() {
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add("name", "value1");
        formData.add("id", "23");

        return webClient.post()
                .uri(BASE_URL + "/formRequest2")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .body(BodyInserters.fromFormData(formData))
                .retrieve()
                .onStatus(HttpStatus::is4xxClientError, res -> Mono.error(new RuntimeException(res.statusCode() + "-自定义的")))
                .onStatus(HttpStatus::is5xxServerError, res -> Mono.error(new RuntimeException(res.statusCode() + "自定义的")))
                .bodyToMono(Student.class);
    }
}

注意事项

在练习数据库操作时,需要连接数据库。
我这里使用了Nacos做配置。
项目中的 bootstrap.yml文件如下:

spring:
  profiles:
    active: ${profiles.active:dev}
  application:
    # 应用名称
    name: webflux-demo
  cloud:
    nacos:
      config:
        # 启用 nacos 做配置
        enabled: true
        # 配置文件后缀
        file-extension: yml
        #  配置所在组
        group: fjs
        extension-configs:
          # nacos 中组为 fjs,data-id 是 datasource.yml
          - data-id: datasource.yml
            group: fjs
            # 配置热更新
            refresh: true
debug: false
# 端口
server:
  port: 80
---
spring:
  cloud:
    nacos:
      config:
        # nacos 用户名
        username: nacos
        # nacos 密码
        password: nacos
        # nacos 所在地址
        server-addr: localhost:80
        # 命名空间ID
        namespace: fjs
  config:
    activate:
      on-profile: dev



在这里插入图片描述
在这里插入图片描述
Nacos中的配置如下:

dynamic:
  mysql:
    datasource:
      master:
        merchantKey: master
        host: localhost
        port: 3306
        username: root
        password: 123456
        database: dynamic_master1
        connectTimeout: 30
      slave:
        merchantKey: slave
        host: localhost
        port: 3306
        username: root
        password: 123456
        database: dynamic_slave1
        connectTimeout: 30

        
    map:
      master:
        - merchantKey=master
        - host=localhost
        - port=3306
        - username=root
        - password=123456
        - database=dynamic_master1
        - connectTimeout=30
      slave:
        - merchantKey=slave
        - host=localhost
        - port=3306
        - username=root
        - password=123456
        - database=dynamic_slave1
        - connectTimeout=30



学生表结构:

/*
 Navicat Premium Data Transfer

 Source Server         : 阿里云
 Source Server Type    : MySQL
 Source Server Version : 80020
 Source Schema         : test

 Target Server Type    : MySQL
 Target Server Version : 80020
 File Encoding         : 65001

 Date: 11/06/2022 11:21:25
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student`  (
  `id` int(0) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `age` int(0) NULL DEFAULT NULL,
  `weight` double NULL DEFAULT NULL,
  `height` double NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (1, '小明', 23, 33.2, 172.23);
INSERT INTO `student` VALUES (2, '小明 79.1669237551011', 68, 79.1669237551011, 206.7008080419343);
INSERT INTO `student` VALUES (3, '小明 74.1299037849883', 63, 74.1299037849883, 185.21078230412152);

SET FOREIGN_KEY_CHECKS = 1;


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你家宝宝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值