41.SpringBoot实用篇—开发(下册)

目录

一、SpringBoot实用篇—开发。

(1)整合第三方技术。

(1.1)缓存。

(1.1.1)自定义缓存。

(1.1.2)使用springboot提供的缓存技术(simple缓存)。

(1.1.2.1) 默认缓存的使用。

(1.1.2.2) 缓存使用案例—手机验证码案例。

(1.1.3)ehcache缓存技术。

(1.1.4)redis缓存技术。

(1.1.5)memcached缓存技术。

(1.1.6)jetcache缓存技术。

(1.1.6.1)使用缓存对象方式。

(1.1.6.2)使用方法注解方式。

(1.1.7)j2cache缓存整合框架。 

(1.1.8)总结。

 (1.1.9)补充知识:数据淘汰策略。

(1.2)(定时)任务。

(1.2.1)整合quartz。 

(1.2.2)springboot中的task。

(1.3)邮件。

(1.3.1)简单邮件。

(1.3.2)复杂邮件。

(1.4)消息(MQ,即消息队列)。

(1.4.1)消息的了解。

(1.4.2)消息案例。

(1.4.3)activeMQ。

(1.4.4)RabbitMQ。

(1.4.4.1)RabbitMQ下载与启动。

(1.4.4.2)SpringBoot整合RabbitMQ直连交换机模式。

(1.4.4.3)SpringBoot整合RabbitMQ主题交换机模式。

(1.4.5)RocketMQ。

(1.4.5.1)RocketMQ下载与启动。

(1.4.5.2)SpringBoot整合RocketMQ。

(1.4.6)Kafka。

(1.4.6.1)kafka下载与启动。

(1.4.6.2)SpringBoot整合Kafka。

(2)监控。

(2.1)监控的意义与实施方式。

(2.2)可视化监控平台。 

(2.2.1)服务端。

(2.2.2)客户端(即需要被监控的应用程序)。

(2.3)监控原理。

(2.4)自定义info端点信息。

(2.5)自定义health端点信息。 

(2.6)自定义性能指标。 

(2.7)自定义端点。

(2.8)总结。


一、SpringBoot实用篇—开发。

以下是一些springboot版本的字母代表的意思。 

(1)整合第三方技术。

(1.1)缓存。

注意:虽然下面很多缓存技术,但是接口是统一的,即导入坐标、配置好配置文件属性就行,注解不需要改动。 

(1.1.1)自定义缓存。

如:使用HashMap集合当作缓存使用。 

@Service
public class MsgServiceImpl implements MsgService {
    private HashMap<String,String> cache = new HashMap<>();
    @Override
    public String get(String tele) {
        String code = tele.substring(tele.length() - 6);
        cache.put(tele,code);
        return code;
    }
    @Override
    public boolean check(String tele, String code) {
        String queryCode = cache.get(tele);
        return code.equals(queryCode);
    }
}
@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;
    private HashMap<Integer,Book> cache = new HashMap<>();
    @Override
    public Book getById(Integer id) {
        //如果当前缓冲中没有本次要查询的数据,则进行查询,否则直接从缓存中获取数据返回
        Book book = cache.get(id);
        if (book == null){
            book = bookDao.selectById(id);
            cache.put(book.getId(),book);
        }
        return book;
    }
}

(1.1.2)使用springboot提供的缓存技术(simple缓存)。

@EnableCaching 注解是 Spring Framework 提供的一个通用注解,用于启用方法级别的缓存功能。无论你使用哪种缓存实现,包括 Ehcache、Redis、Caffeine 等,都可以通过 @EnableCaching 注解来开启 Spring 的缓存功能。 (不包含不被springboot整合的)

(1.1.2.1) 默认缓存的使用。

解析:当调用这个方法时,key = "#id" 获取参数id的值,然后从名为cacheSpace的缓存中缓存key=“#id”的值,如果有,则直接返回得到的值(即return bookDao.selectById(id);语句不执行);若是没有,则就会执行return bookDao.selectById(id);语句返回,并且将该方法的参数id为键,查询结果为值,存到缓存中去。

@Cacheable 注解中的 cacheNamesvalue 参数是等效的,它们用于指定缓存区域的名称。 

    @Override
    //value属性指定了要使用的缓存空间(Cache Space)的名称。就是缓存的名称
    @Cacheable(value = "cacheSpace",key = "#id")
    public Book getById(Integer id) {
        return bookDao.selectById(id);
    }

  • @Cacheable:在方法上加上该注解后,下次调用时如果缓存存在,则直接从缓存中获取,否则会执行该方法并将结果存入缓存。
  • @CachePut:在方法上加上该注解后,每次都会执行该方法并将结果存入缓存中。
  • @CacheEvict:在方法上加上该注解后,会移除指定的缓存。

(1.1.2.2) 缓存使用案例—手机验证码案例。

注意:如果在这个类中直接调用get(tele)方法,那么只是普通调用,没有通过bean调用该方法,则该注解不起作用(因为spring没有参与),要用注入的对象调用(例如@Autowired)该方法,则get方法的注解就会被spring处理。(下面的代码不是全部都是在一个类的)

    @Autowired
    private CodeUtils codeUtils;
  
    @Override
//    @Cacheable(value = "smsCode",key = "#tele")
    @CachePut(value = "smsCode",key = "#tele")
    public String sendCodeToSMS(String tele) {
        String code = codeUtils.generator(tele);
        return code;
    }
----------------------------------------------------------------------------------------
  @Cacheable(value = "smsCode",key="#tele")
    public String get(String tele){
        return null;
    }

(1.1.3)ehcache缓存技术。

 注意:我使用的是springboot3.1.0版本,配置这里没有ehcache,所以没有完成这些步骤。

接下来就是跟springboot的简单缓存技术一样的使用方法

@Cacheable 注解中的 cacheNamesvalue 参数是等效的,它们用于指定缓存区域的名称。

@Service
public class MyService {
    @Cacheable(cacheNames = "exampleCache")
    public String getFromCache(String key) {
        // 从数据库或其他数据源获取数据
        // ...
        return data;
    }
    @CachePut(cacheNames = "exampleCache", key = "#key")
    public void saveToCache(String key, String value) {
        // 存储数据到数据库或其他数据源

    }
    // 其他业务方法...
}

(1.1.4)redis缓存技术。

提示:基本上属性名没变,可能前面多了个名字,如spring.redis变成spring.data.redis等等。

spring:
  cache:
    type: redis
    redis:
      cache-null-values: true
      key-prefix: false
      use-key-prefix: true
      time-to-live: 10s
  data:
    redis:
      host: localhost
      port: 6379

 接下来就是跟springboot的简单缓存技术一样的使用方法

@Service
public class MyService {
    @Cacheable(cacheNames = "exampleCache")
    public String getFromCache(String key) {
        // 从数据库或其他数据源获取数据
        // ...
        return data;
    }
    @CachePut(cacheNames = "exampleCache", key = "#key")
    public void saveToCache(String key, String value) {
        // 存储数据到数据库或其他数据源

    }
    // 其他业务方法...
}

(1.1.5)memcached缓存技术。

(1.1.6)jetcache缓存技术。

(1.1.6.1)使用缓存对象方式。

这里注意fastjson,因为版本不同,发生了变化变成fastjson2。 

jetcache:
  statIntervalMinutes: 1
  local:
    default:
      type: linkedhashmap
      keyConvertor: fastjson2 # 如果key为对象,则需要指定转换为字符串的技术
    sms:
      type: linkedhashmap
      keyConvertor: fastjson2

  remote:
    default:
      type: redis
      host: localhost
      port: 6379
      keyConvertor: fastjson2
      valueEncode: java
      valueDecode: java
      poolConfig: # 至少写一条,不然报错
        maxTotal: 50
    sms:
      type: redis
      host: localhost
      port: 6379
      poolConfig:
        maxTotal: 50

springboot是3.1.1版本,使用的是2.7.3(这里整整浪费了我很多时间)。

        <dependency>
            <groupId>com.alicp.jetcache</groupId>
            <artifactId>jetcache-starter-redis</artifactId>
            <version>2.7.3</version>
        </dependency>

(1.1.6.2)使用方法注解方式。

这种方式也是需要配置的,配置使用上面的。(name其实是缓存名,key是键)

@CacheInvalidate:这个注解是先删除数据库在删除缓存。

@CacheUpdate :这个注解是先修改数据库在修改缓存。

  • keyConvertor 表示缓存键的转换器。在将对象用作缓存键时,需要将其序列化为字符串表示。

  • valueEncode 表示缓存值的编码方式。当将对象存储到缓存中时,需要将对象序列化为字节数组进行存储。

  • valueDecode 表示缓存值的解码方式。当从缓存中获取对象时,需要将存储的字节数组反序列化为真正的对象。

(1.1.7)j2cache缓存整合框架。 

注意:因为org.slf4j出现错误,那么可以试着排除看一下。 

        <dependency>
            <groupId>net.oschina.j2cache</groupId>
            <artifactId>j2cache-core</artifactId>
            <version>2.8.5-release</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-simple</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

(1.1.8)总结。

 (1.1.9)补充知识:数据淘汰策略。

(1.2)(定时)任务。

(1.2.1)整合quartz。 

  1. JobBuilder.newJob(QuartzTaskBean.class):创建一个新的 JobBuilder 实例,并指定要执行的任务类(QuartzTaskBean.class)。这里的 QuartzTaskBean.class 是你自定义的实现了 Quartz Job 接口的任务类。
  2. storeDurably():此方法用于设置任务的持久化标志。当任务被设为持久化时,即使没有与该任务相关联的触发器,该任务仍然存在于调度器中。这样可以避免任务在没有触发器的情况下被自动删除。
  3. build():此方法用于构建最终的 JobDetail 对象,该对象包含了设置的任务类和其他属性。

(1.2.2)springboot中的task。

@Scheduled 注解有以下常用的参数:

  1. fixedRate:固定频率执行任务,以毫秒为单位。例如,@Scheduled(fixedRate = 5000) 表示每隔 5 秒执行一次任务。

  2. fixedDelay:固定延迟执行任务,以毫秒为单位。例如,@Scheduled(fixedDelay = 5000) 表示当任务执行完成后,延迟 5 秒再次执行。

  3. initialDelay:初始延迟时间,以毫秒为单位。例如,@Scheduled(initialDelay = 10000, fixedRate = 5000) 表示首次执行任务前延迟 10 秒,然后以 5 秒的间隔重复执行。

  4. cron:使用 Cron 表达式指定执行时间。例如,@Scheduled(cron = "0 0 12 * * ?") 表示在每天中午 12 点执行任务。

  5. zone:指定时区,用于计算 Cron 表达式。

Cron 表达式是一种用于指定定时任务执行时间的字符串表达式,它由六个或七个字段组成,每个字段代表一个时间单位。Cron 表达式的格式如下:

second minute hour dayOfMonth month dayOfWeek [year]

其中各个字段的含义如下:

  1. second(秒):可选值为 0-59 的整数。

  2. minute(分钟):可选值为 0-59 的整数。

  3. hour(小时):可选值为 0-23 的整数。

  4. dayOfMonth(某月的第几天):可选值为 1-31 的整数。

  5. month(月份):可选值为 1-12 或 JAN-DEC 的字符串。

  6. dayOfWeek(星期几):可选值为 0-7 或 SUN-SAT 的字符串,其中星期日可以用 0 或 7 表示,星期一到星期六分别用 1-6 表示。

  7. year(年份):可选值为四位的整数。

上述字段可以使用以下特殊字符来指定特定的时间:

  • 星号 (*):匹配任意值。例如,*month 字段表示每个月,而在 dayOfWeek 字段表示每个星期。

  • 问号 (?):只适用于 dayOfMonthdayOfWeek 字段,表示不指定具体的值。例如,?dayOfMonth 字段表示不指定具体日期,而在 dayOfWeek 字段表示任意星期。

  • 斜线 (/):用于指定增量。例如,0/15second 字段表示每隔 15 秒,而 3/20minute 字段表示从第 3 分钟开始,每隔 20 分钟。

  • 逗号 (,):用于指定多个值。例如,MON,WED,FRIdayOfWeek 字段表示周一、周三和周五。

  • 连接符 (-):用于指定范围。例如,10-20dayOfMonth 字段表示从第 10 天到第 20 天。

下面是一些 Cron 表达式的示例:

  • 0 0 7 * * ?:表示每天早上 7 点触发。

  • 0 30 23 ? * MON-FRI:表示每个工作日的晚上 11:30 触发。

  • 0 0 12 1 1/3 ?:表示每年的 1 月 1 日和 4 月 1 日的中午 12 点触发。

  • "0/5" 表示每隔 5 秒执行一次任务。其中 "0" 表示起始秒数为 0,"/5" 表示间隔为 5 秒(0该位置只有第一次执行的效果,代表分钟内的第几秒,如果还没够5秒就已经到了分钟内的第0秒,则执行,然后再每隔五秒执行。否则就是先够5秒,然后分钟内的第0秒还没到,则执行,则0已经失效,每隔5秒就执行)(举例:项目启动后是第5秒,则0肯定不起作用了;如果项目启动后是第58秒,过2秒后就是第0秒,则执行一次(0执行后就失效了),然后每过5秒再循环执行)

  • "5" 表示在每分钟的第 5 秒执行任务,即每分钟的第 5 秒时触发任务

请注意,在 year 字段是可选项,可以省略。


不需要导入坐标就能使用,如果不需要配置,则可以不写配置。 

(1.3)邮件。

(1.3.1)简单邮件。

SimpleMailMessage是JavaMail API提供的简单邮件消息类。它提供了一种简化的方式来创建和发送基本的文本邮件。使用SimpleMailMessage,你可以设置发送者、接收者、主题、内容等基本邮件属性。但是,它不支持复杂的邮件格式或附件。

(1.3.2)复杂邮件。

MimeMessage是JavaMail API提供的更灵活和功能更强大的邮件消息类。它允许你创建复杂的邮件,包括HTML内容、附件、多媒体资源等。

(1.4)消息(MQ,即消息队列)。

(1.4.1)消息的了解。

(1.4.2)消息案例。

@Service
public class MessageServiceImpl implements MessageService {
    
    private ArrayList<String> msgList = new ArrayList<>();
    
    @Override
    public void sendMessage(String id) {
        System.out.println("待发送短信的订单已纳入处理队列,id:"+id);
        msgList.add(id);
    }
    @Override
    public String doMessage() {
        String id = msgList.remove(0);
        System.out.println("已完成短信发送业务,id:"+id);
        return id;
    }
}

(1.4.3)activeMQ。

(1.4.4)RabbitMQ。

(1.4.4.1)RabbitMQ下载与启动。

注意:这个是需要安装的,安装后找到安装路径的文件夹,进入命令行输入命令启动。

(1.4.4.2)SpringBoot整合RabbitMQ连交换机模式。

将队列绑定的交换机(不能反过来),交换机名称与路由键的组合需要唯一。 

@Configuration
public class RabbitConfigDirect {
    @Bean
    public Queue directQueue(){
        return new Queue("direct_queue");
    }
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("directExchange");
    }
//将名为"directQueue"的队列通过绑定关系与名为"directExchange"的交换机绑定起来,
//并且指定绑定的路由键为"direct"。(通过路由键+交换机名称  =》得到队列) 
    @Bean
    public Binding bindingDirect(){
        return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct");
    }
}

下面的方法可以参考得到队列的过程(需要提供交换机名称+路由键)。 

@Service
public class MessageServiceRabbitmqDirectImpl implements MessageService {

    @Autowired
    private AmqpTemplate amqpTemplate;

    @Override
    public void sendMessage(String id) {
        System.out.println("待发送短信的订单已纳入处理列表(rabbitmq direct),id:"+id);
        amqpTemplate.convertAndSend("directExchange","direct",id);
    }
}

(1.4.4.3)SpringBoot整合RabbitMQ主题交换机模式。

区别: 可以将消息发送到多个队列当中。(只要是匹配上了绑定键,就发送)

注意:匹配规则只能用在绑定键的命名中(发送消息到队列的方法中用的绑定键名不会起作用的)

@Configuration
public class RabbitConfigTopit {
    @Bean
    public Queue topicQueue(){
        return new Queue("topic_queue");
    }
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange("topicExchange");
    }
    @Bean
    public Binding bindingTopic() {
//        return BindingBuilder.bind(topicQueue()).to(topicExchange()).with("topic.order.id");
        //模糊匹配该规则
        return BindingBuilder.bind(topicQueue()).to(topicExchange()).with("topic.*.id");
    }
}
@Service
public class MessageServiceRabbitmqTopicImpl implements MessageService {
    @Autowired
    private AmqpTemplate amqpTemplate;
    @Override
    public void sendMessage(String id) {
        System.out.println("待发送短信的订单已纳入处理列表(rabbitmq direct),id:"+id);
        amqpTemplate.convertAndSend("topicExchange","topic.order.id",id);
    }
}

(1.4.5)RocketMQ。

(1.4.5.1)RocketMQ下载与启动。

注意:rocketmq使用的JAVA_HOME里面不能有空格,只能使用java8版本。 


(1.4.5.2)SpringBoot整合RocketMQ。

建议:还是使用jdk8比较好,因为我使用高版本的jdk,导致各种问题出现,而且监听器信息消费也没有起作用(不知道为什么,可能版本问题)。

启动类启动失败提示:(原因就是template没有被bean管理,因为spring没有维护rocket)

Action:

Consider defining a bean of type 'org.apache.rocketmq.spring.core.RocketMQTemplate' in your configuration.

@SpringBootApplication
public class Springboot24MqApplication {

    @Bean //这里我懒得再写一个配置类,所以直接在这里写了
    public RocketMQTemplate rocketMQTemplate(){
        return new RocketMQTemplate();
    }
    public static void main(String[] args) {
        SpringApplication.run(Springboot24MqApplication.class, args);
    }

}

出现这个异常,我通过@Bean配置producer对象,解决了这个异常。 

@SpringBootApplication
public class Springboot24MqApplication {
    @Bean
    public RocketMQTemplate rocketMQTemplate(){
        return new RocketMQTemplate();
    }
    @Bean(initMethod = "start", destroyMethod = "shutdown")//这注解里面的参数很关键,注释后就报错(最低要求要有 initMethod = "start")
    public DefaultMQProducer defaultMQProducer() {
        DefaultMQProducer producer = new DefaultMQProducer("group_rocketmq");
        producer.setNamesrvAddr("127.0.0.1:9876");
        return producer;
    }
    public static void main(String[] args) {
        SpringApplication.run(Springboot24MqApplication.class, args);
    }
}
@Service
public class MessageServiceRocketmqImpl implements MessageService {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @Autowired
    private DefaultMQProducer producer;

    @Override
    public void sendMessage(String id) throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
        //第一种方式:
//        Message message = new Message("order_id", "my_tag", id.getBytes(StandardCharsets.UTF_8));
//        producer.send(message);
        //第二种方式:
        //使用同步
//        rocketMQTemplate.setProducer(producer);
//        rocketMQTemplate.convertAndSend("order_id",id);
        //使用异步
        rocketMQTemplate.setProducer(producer);
        SendCallback callback = new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println("message successful");
            }

            @Override
            public void onException(Throwable throwable) {
                System.out.println("message fail");
            }
        };
        rocketMQTemplate.asyncSend("order_id",id,callback);
        System.out.println("待发送短信的订单已纳入处理列表(rocketmq),id:"+id);
    }
    @Override
    public String doMessage() {
        return null;
    }
}

提示:下面的消费消息不起作用(不清楚具体原因)。 

测试了一下:如果使用jdk8与springboot版本2.5.4的时候,可以正常使用,只需要配置文件(而且还不需要改变其他的东西就能使用,所以使用rocketmq的时候建议使用jdk8与2.5.4):
rocketmq:
  name-server: localhost:9876
  producer:
    group: group_rocketmq
@Component
@RocketMQMessageListener(topic = "order_id",consumerGroup = "group_rocketmq")//与生产组名称相同,它会去生产组拿
public class MessageListener implements RocketMQListener<String> {

    @Override
    public void onMessage(String id) {
        System.out.println("已完成短信发送业务(rocketmq),id:"+id);
    }
}

注意:这里使用jdk8与springboot版本2.5.4,可以正常使用,而且一模一样(建议使用这一种,毕竟上面的只是创建新的template,给对象配置了一下属性。但是它是被spring管理的(2.5.4),而且还不确定装配的是原本的bean还是我@Bean创建的bean)。 

综上所述:直接使用自动装配使用即可,不需要多此一举。 

(1.4.6)Kafka。

(1.4.6.1)kafka下载与启动。

(1.4.6.2)SpringBoot整合Kafka。
@Component
public class MessageKafkaListener {

    @KafkaListener(topics = "itheima22222")
    public void onMessage(ConsumerRecord<String,String> record){//键与值的类型
        //record.key();
        System.out.println("已完成短信发送业务(rocketmq),id:"+record.value());
    }
}

(2)监控。

(2.1)监控的意义与实施方式。

实施方式:需要监控的(客户端)应用程序在启动后要向服务端上报,服务端是主动拉取监控信息(不延迟,在刷新后就拉取最新信息)。

(2.2)可视化监控平台。 

(2.2.1)服务端。

注意:启动服务端后,访问http://localhost:8080/就能查看监控信息。(根据程序路径访问) 

需要加上web依赖才能一直运行(否则启动类启动就结束) 。

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

(2.2.2)客户端(即需要被监控的应用程序)。

需要加上web依赖才能一直运行(否则启动类启动就结束) 。

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

注意:在应用程序中加上下面的就能被监控,这里的只是被监控需要的依赖和配置,不包含应用程序需要的。

(2.3)监控原理。

management:
  endpoint: # 这里指的是,是否对外开放(如果不开放,那么web与jmx方式访问都没用)
    health:
      enabled: true #默认为true,但是如果enabled-by-default为false的时候,必须配置
      show-details: always #如果不配置这里,就只是健康的信息不会显示而已
  endpoints:
    enabled-by-default: true #这里默认为true,对访问方式不影响,就是对外开放所有端点(改为false会报错,提示至少需要health,给health配置enabled: true)
    web: #这里使用的是web方式访问,当然这里默认就是health,根据需要配置
      exposure:
        include: health,info
#        include: "*"
    jmx: #这里使用的是jmx方式访问,当然这里默认就是*,不用配置(或根据需要配置)
      exposure:
        include: "*"

win+R进入命令行 —(输入回车)jconsole — 弹出一个java的监控平台(jmx方式)。


小结提示:第2点指的是对外开放的端点,第3点说的是web、jmx访问方式端点功能暴露配置。

(2.4)自定义info端点信息。

@Component
public class InfoConfig implements InfoContributor {

    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("runTime",System.currentTimeMillis());
        Map infoMap = new HashMap();
        infoMap.put("buildTime","2006");
        builder.withDetails(infoMap);//支持链式编程
    }
}

(2.5)自定义health端点信息。 

@Component
public class HealthConfig extends AbstractHealthIndicator {//或者实现这个接口implements HealthIndicator
    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        boolean condition = true;
        if (condition){
//            builder.up();//设置上线状态
            builder.status(Status.UP);
            builder.withDetail("runTime",System.currentTimeMillis());
            Map infoMap = new HashMap();
            infoMap.put("buildTime","2006");
            builder.withDetails(infoMap);
        }else {
//            builder.down();
            builder.status(Status.DOWN);
            builder.withDetail("上线了吗?","你做梦");
        }
    }
}

(2.6)自定义性能指标。 

(2.7)自定义端点。

提示:前面的都是在端点中增加一些信息,而这里是增加一个新的端点,虽然增加了,但是只能在映射中找到该端点的uri等信息。(我猜是因为该监控平台不显示该端点信息,毕竟是我新加的)


(2.8)总结。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值