SpringBoot整合篇

SpringBoot整合第三方技术

1、整合缓存


何为缓存?

  • 缓存是一种介于数据永久存储介质与数据应用之间的数据临时存储介质
  • 使用缓存可以有效的减少低速数据读取过程的次数(例如磁盘IO),提高系统性能
  • 缓存不仅可以用于提高永久性存储介质的数据读取效率,还可以提供临时的数据存储空间

Spring缓存抽象

Spring 从3.1开始定义了 org.springframework.cache.Cacheorg.springframework.cache.CacheManager 接口来统一不同的缓存技术;并支持使用JCache注解简化开发

注解说明
Cache缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等
CacheManager缓存管理器,管理各种缓存组件Cache
@Cacheable主要针对方法配置,能够根据方法的请求参数对其结果进行缓存(查询)
@CacheEvict清空缓存(删除)
@CachePut保证方法被调用,又希望结果被缓存(修改)
@EnableCaching开启基于注解的缓存
keyGenerator缓存数据时key生成策略
serialize缓存数据时value序列化策略

使用缓存

将方法的返回值进行缓存,以后如果需要相同的数据,直接从缓存中获取,不再调用方法,从而提高系统的性能!

  • 引入 spring-boot-starter-cache 模块
  • @EnableCaching 开启缓存
  • 使用缓存注解(@Cacheable@CachePut@CacheEvict
  • (切换为其他缓存产品)
@Cacheable属性
  • cacheNames/value:指定缓存组件的名字,将方法的返回结果放在哪个缓存中,可以指定多个缓存
  • key:缓存数据使用的key值,可以直接指定,默认是使用方法参数的值,可以编写 SpEL
  • keyGenerator:key的生成器,key和keyGenerator,二选一使用
  • cacheManager:指定缓存管理器,或者cacheResolver指定获取解析器,二选一使用
  • condition:指定符合条件的情况下才缓存
  • unless:当unless指定的条件为true,方法的返回值就不会被缓存
  • sync:是否使用异步模式,默认方法执行完,将结果以同步的方式存入缓存中
@CachePut

修改了数据库的某个数据,同时更新缓存,运行时先调用目标方法,将目标方法的结果(返回值)缓存起来

注:更新函数的缓存 Key 值如果和查询函数的 Key 值不一致,那么更新后查询函数查询的结果依然是旧的数据

@CacheEvict

缓存清除

  • key:指定要清除的数据
  • allEntries:默认false,如果为true,表示清除这个缓存中所有数据
  • beforeInvocation:缓存的清除是否在方法之前,默认false,即缓存清除在方法执行后进行,如果方法出现异常缓存就不会清除。如果设为true,则表示清除缓存在方法运行之前,无论方法是否出现异常,缓存都会被清除

整合 Redis

默认使用的是 ConcurrentMapCacheManagerConcurrentMapCache ,但在实际开发中通常会使用 Redis、memcached、Ehcache

  • 导入 spring-boot-starter-data-redis
  • 配置文件配置 redis 的连接地址等信息

RedisTemplate & StringRedisTemplate

RedisAutoConfiguration 中自动配置了 RedisTemplateStringRedisTemplate,可以很方便的来操作Redis!

  • StringRedisTemplate:存取的数据都是字符串时使用
  • RedisTemplate:存取的数据是对象时使用

Redis常见的五大数据类型和RedisTemplate的相关API

  • redisTemplate.opsForValue() String
  • redisTemplate.opsForList() List
  • redisTemplate.opsForSet() Set
  • redisTemplate.opsForHash() Hash
  • redisTemplate.opsForZSet() ZSet
序列化

RedisTemplate 默认使用的序列化类是 JdkSerializationRedisSerializer,而 StringRedisTemplate 使用的是 StringRedisSerializer

如果需要更改序列化规则,如将数据以 JSON 形式保存,可以自定义 RedisTemplate 设置序列化规则并注入容器中

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<Object, Object> jsonRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        // 默认的Key序列化器为:JdkSerializationRedisSerializer
        template.setDefaultSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
        return template;
    }
}
自定义CacheManager

引入 Redis 的 starter 后,容器中注册的但是 RedisCacheManager,默认的SimpleCacheManager 失效。RedisManager 帮助我们创建 RedisCache 来作为缓存组件,RedisCache 通过操作 redis 来缓存数据

整合 Elasticsearch

数据检索

开源的 ElasticSearch 是目前全文搜索引擎的首选。它可以快速的存储、搜索和分析海量数据。Spring Boot通过整合Spring Data ElasticSearch为我们提供了非常便捷的检索功能支持

Elasticsearch是一个分布式搜索服务,提供Restful API,底层基于Lucene,采用多shard(分片)的方式保证数据安全,并且提供自动resharding的功能,Stack Overflow等大型的站点也是采用了ElasticSearch作为其搜索服务

ES 简介

Elasticsearch 是面向文档的,意味着它存储整个对象或文档。ES 不仅存储文档,而且索引每个文档的内容,使之可以被检索。在 ES 中,我们对文档进行索引、检索、排序和过滤,而不是对行列数据

Elasticsearch 使用 JavaScript Object Notation(或者 JSON)作为文档的序列化格式。JSON 序列化为大多数编程语言所支持,并且已经成为 NoSQL 领域的标准格式。 它简单、简洁、易于阅读

存储数据到 Elasticsearch 的行为叫做索引,但在索引一个文档之前,需要确定将文档存储在哪里。一个 Elasticsearch 集群可以包含多个索引 ,相应的每个索引可以包含多个类型。这些不同的类型存储着多个文档,每个文档又有多个属性

  • Index(索引):数据库
  • Document 文档:数据库表中的一条记录
  • Field 字段:数据库中的字段

在这里插入图片描述

注:ES 里的 Index 可以看做一个库,而 Type 相当于表,Documents 则相当于表的行(记录)

这里 Types 的概念已经被逐渐弱化,Elasticsearch 6.X 中,一个 index 下已经只能包含一个 type,Elasticsearch 7.X 中,Type 的概念已经被删除了,type都为_doc

安装 ES
  • 拉取镜像:docker pull elasticsearch:7.8.0

  • 运行容器

    # -d 后台运行
    # -v 绑定一个数据卷
    # -p(小写) 指定要映射的IP和端口	hostPort:containerPort
    # -e "discovery.type=single-node"   单节点集群
    # -e ES_JAVA_OPTS="-Xms512m -Xmx512m"  制定运行参数,不然如果机器内存太小,启动后会非常卡顿
    # --name 起个别名
    docker run -p 9200:9200 -p 9300:9300 --name=es-7.8.0 \
    -e "discovery.type=single-node" \
    -e ES_JAVA_OPTS="-Xms512m -Xmx512m" \
    -d elasticsearch:7.8.0
    

ES官方文档

安装Kibana
  • 拉取镜像:docker pull kibana:7.8.0

  • 运行容器

    # kibana版本必须和elasticsearch版本保持一致
    # 启动容器
    # YOUR_ELASTICSEARCH_CONTAINER_NAME_OR_ID 正在运行的ES容器ID或name
    # docker run --link YOUR_ELASTICSEARCH_CONTAINER_NAME_OR_ID:elasticsearch -p 5601:5601 {docker-repo}:{version}
    docker run --link es-7.8.0:elasticsearch -p 5601:5601 -d --name=kibana-7.8 kibana:7.8.0
    
整合 ES

导入整合依赖包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
实体类映射操作
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "product")
public class Product implements Serializable {
    private static final long serialVersionUID = 1L;

    // 必须有 id,这里的 id 是全局唯一的标识,等同于 es 中的"_id"
    @Id
    private Long id;            // 商品唯一标识
    /**
     * type : 字段数据类型
     * analyzer : 分词器类型
     * index : 是否索引(默认:true)
     * Keyword : 短语,不进行分词
     */
    @Field(type = FieldType.Text)
    private String title;       // 商品名称

    @Field(type = FieldType.Keyword)
    private String category;    // 分类名称

    @Field(type = FieldType.Double)
    private Double price;       // 商品价格

    @Field(type = FieldType.Keyword, index = false)
    private String images;      // 图片地址
}
DAO 数据访问对象
public interface ProductRepository extends ElasticsearchRepository<Product,Long> {
}
CRUD 操作
@SpringBootTest
class ElasticsearchApplicationTests {
    @Autowired
    private ElasticsearchRestTemplate restTemplate;

    @Autowired
    private ProductRepository productRepository;

    // ****** 索引操作 ******
    @Test
    public void testCreateIndex(){
        // 创建索引并增加映射配置
        // 创建索引,系统初始化会自动创建索引
        System.out.println("创建索引...");
    }

    @Test
    public void testDelIndex(){
        // 删除索引
        boolean flag = restTemplate.indexOps(Product.class).delete();
        System.out.println("flag = " + flag);
    }

    // ****** 文档操作 ******
    @Test
    public void testAdd() {
        Product product = new Product(1001L, "华为mate50 Pro", "手机", 2699.00, "https://img11.360buyimg.com/n1/s450x450_jfs/t1/100178/24/22008/134447/63808261E7e8fdf63/bfc4ff46e04450b6.jpg.avif");
        productRepository.save(product);
    }

    @Test
    public void testUpdate() {
        Product product = new Product(1001L, "Redmi K50", "手机", 2599.00, "https://img12.360buyimg.com/n1/s450x450_jfs/t1/211467/21/27724/73497/63842044E32d227a6/232dbbcc378d8742.jpg.avif");
        productRepository.save(product);
    }

    @Test
    public void testGetById() {
        Optional<Product> product = productRepository.findById(1001L);
        System.out.println("product = " + product);
    }

    @Test
    public void testGetAll() {
        Iterable<Product> products = productRepository.findAll();
        products.forEach(System.out::println);
    }

    @Test
    public void testDel() {
        productRepository.deleteById(1001L);
    }

    @Test
    public void testBatchAdd() {
        List<Product> productList = new ArrayList<>();
        Product p1 = new Product(1003L, "OPPO Reno8 系列", "手机", 2099.00, "https://img13.360buyimg.com/n1/s450x450_jfs/t1/166774/34/31501/126697/636cd9dfE2329529e/bce5ef0bc351e516.jpg.avif");
        Product p2 = new Product(1004L, "Apple iPhone 14", "手机", 6899.00, "https://img14.360buyimg.com/n1/s450x450_jfs/t1/79861/16/22966/34933/6381b0d7Ece7f68c2/6dd5f2a3f892add8.jpg.avif");
        Product p3 = new Product(1005L, "奥克斯(AUX)大3匹空调", "大家电", 5999.00, "https://img12.360buyimg.com/n1/jfs/t1/109793/27/35299/101988/637aeaf7Ede5dd640/22e59a42744809c2.jpg.avif");
        productList.add(p1);
        productList.add(p2);
        productList.add(p3);

        productRepository.saveAll(productList);
    }

    @Test
    public void testBatchDel() {
        List<Long> ids = Arrays.asList(1003L, 1004L);
        productRepository.deleteAllById(ids);
    }

    @Test
    public void testFindByPage() {
        Sort sortRule = Sort.by(Sort.Direction.DESC, "price");
        int currentPage = 0;    // 当前页,第一页从 0 开始,1 表示第二页
        int pageSize = 5;       // 每页显示多少条
        PageRequest pageRequest = PageRequest.of(currentPage, pageSize, sortRule);
        Iterable<Product> products = productRepository.findAll(pageRequest);
        products.forEach(System.out::println);
    }
}

2、整合任务与邮件


异步任务

在Java应用中,绝大多数情况下都是通过同步的方式来实现交互处理的;但是在处理与第三方系统交互的时候,容易造成响应迟缓的情况,之前大部分都是使用多线程来完成此类任务,其实,在Spring 3.x之后,就已经内置了@Async来完美解决这个问题

使用方法是在主启动类上用 @EnableAsync 开启异步支持,将 @Async 标注在指定的异步方法上

// 告诉Spring这是一个异步方法
@Async
public void hello()  {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("处理数据中...");
}

定时任务

项目开发中经常需要执行一些定时任务,比如需要在每天凌晨时候,分析一次前一天的日志信息

市面上流行的定时任务技术

  • Quartz
  • Spring Task

相关概念

  • 工作(Job):用于定义具体执行的工作
  • 工作明细(JobDetail):用于描述定时工作相关的信息
  • 触发器(Trigger):用于描述触发工作的规则,通常使用cron表达式定义调度规则
  • 调度器(Scheduler):描述了工作明细与触发器的对应关系
Quartz

导入SpringBoot整合quartz的坐标

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

定义具体要执行的任务,继承QuartzJobBean,并定义工作明细与触发器,并绑定对应关系

@Configuration
public class MyQuartz extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        System.out.println("生成年度报表...");
    }

    @Bean
    public JobDetail jobDetail() {
        // 绑定具体的工作
        return JobBuilder.newJob(MyQuartz.class).storeDurably().build();
    }

    @Bean
    public Trigger trigger(){
        // 绑定对应的工作明细
        CronScheduleBuilder csb = CronScheduleBuilder.cronSchedule("0 59 23 L 12 ? 2022-2060");
        return TriggerBuilder.newTrigger().withSchedule(csb).forJob(jobDetail()).build();
    }
}
Spring Task

在主启动类上添加@EnableScheduling注解开启定时任务功能,然后在需要定时的函数上标注 @Schedule 注解,并设置 cron 表达式

@Component
public class MyTask {
    @Scheduled(cron = "0/6 * * * * ?")      // 每隔6秒执行一次
    public void recordLog(){
        System.out.println("记录日志...");
    }
}

在线Cron表达式生成器

邮件任务

  • SMTP(Simple Mail Transfer Protocol):简单邮件传输协议,用于发送电子邮件的传输协议
  • POP3(Post Office Protocol - Version 3):用于接收电子邮件的标准协议
  • IMAP(Internet Mail Access Protocol):互联网消息协议,是POP3的替代协议

导入SpringBoot整合JavaMail的坐标

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

配置JavaMail

spring:
  mail:
    host: smtp.qq.com
    username: ********@qq.com
    password: ********

发送邮箱必备四要素

@Autowired
private JavaMailSender mailSender;

@Value(value = "${from}")
private String from;
@Value(value = "${to}")
private String to;
@Value(value = "${subject}")
private String subject;
@Value(value = "${content}")
private String content;

@Test
public void testSendSimpleEmail() {
    SimpleMailMessage msg = new SimpleMailMessage();
    // 邮件设置
    // 发件人
    msg.setFrom(from);
    // 收件人
    msg.setTo(to);
    // 标题
    msg.setSubject(subject);
    // 正文
    msg.setText(content);
    // 发送简单邮箱
    mailSender.send(msg);
}

附件与HTML文本支持

@Test
public void testSendHardMail() {
    try {
        // 创建一个复杂的消息邮箱
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
        // 邮箱设置
        helper.setFrom(from);
        helper.setTo(to);
        helper.setSubject("测试发送复杂邮箱");
        helper.setText("<p><a href='https:www.baidu.com'>点开有惊喜</a></p>", true);

        // 添加附件
        File img1 = new File("D:\\DesktopBackground\\mobile\\73.png");
        File img2 = new File("D:\\DesktopBackground\\mobile\\68.png");
        helper.addAttachment(img1.getName(), img1);
        helper.addAttachment(img2.getName(), img2);
        mailSender.send(mimeMessage);
    } catch (MessagingException e) {
        throw new RuntimeException(e);
    }
}

3、整合消息队列


  • 消息发送方

    • 生产者
  • 消息接收方

    • 消费者
  • MQTT

应用场景

  • 异步处理
  • 应用解耦
  • 流量消峰

JMS

JMS(Java Message Service)JAVA 消息服务:一个规范,等同于JDBC规范,提供了与消息服务相关的API接口

JMS消息模型

  • point to point:点对点模型(队列:queue),消息发送到一个队列中,队列保存消息。队列的消息只能被一个消费者消费,或超时
  • publish-subscribe:发布订阅模型(主题:topic),消息可以被多个消费者消费,生产者和消费者完全独立,不需要感知对方的存在

JMS消息种类

  • TextMessage
  • MapMessage
  • BytesMessage
  • StreamMessage
  • ObjectMessage
  • Message (只有消息头和属性)

JMS实现:ActiveMQ、Redis、HornetMQ、RabbitMQ、RocketMQ(没有完全遵守JMS规范)

AMQP

AMQP(advanced message queuing protocol):一种协议(高级消息队列协议,也是消息代理规范),规范了网络交换的数据格式,兼容JMS

优点:具有跨平台性,服务器供应商,生产者,消费者可以使用不同的语言来实现

AMQP消息模型

  • direct exchange
  • fanout exchange
  • topic exchange
  • headers exchange
  • system exchange

AMQP消息种类:byte[]

AMQP实现:RabbitMQ、StormMQ、RocketMQ

MQTT

MQTT(Message Queueing Telemetry Transport)消息队列遥测传输,专为小设备设计,是物联网(IOT)生态系统中主要成分之一

Spring 支持

  • spring-jms提供了对JMS的支持
  • spring-rabbit提供了对AMQP的支持
  • 通过ConnectionFactory的实现来连接消息代理
  • 提供JmsTemplate、RedisTemplate来发送消息
  • @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息代理发布的消息
  • @EnableJms、@EnableRabbit开启支持

SpringBoot 自动配置

  • JmsAutoConfiguration
  • RabbitAutoConfiguration

Docker 安装 RabbitMQ

  • 拉取镜像:docker pull rabbitmq:3-management
  • 运行容器:docker run -d -e RABBITMQ_DEFAULT_USER=root -e RABBITMQ_DEFAULT_PASS=root -p 5672:5672 -p 15672:15672 --name=rabbitmq-3 rabbitmq:3-management

SpringBoot 整合 RabbitMQ

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit-test</artifactId>
    <scope>test</scope>
</dependency>
  • ConnectionFactory:连接工厂
  • RabbitProperties:封装了RabbitMQ的配置
  • RabbitTemplate:给RabbitMQ发送和接收消息
  • AmqpAdmin:RabbitMQ系统管理功能组件
序列化机制

RabbitTemplate 默认使用 SimpleConverter (即JDK反序列规则),要将序列化规则设置为 JSON 形式,则可以使用如下方法:

@Configuration
public class RabbitMQConfig {
    public static final String EXCHANGE_NAME = "boot_topic_exchange";
    public static final String QUEUE_NAME = "boot_queue";
    // #:匹配一个或多个词	item.# = item.insert.abc.xxx.../item.insert
    // *:只能匹配一个次	item.* = item.insert
    public static final String ROUTING_KEY = "boot.#";

    // 1.交换机
    @Bean
    public Exchange exchange() {
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).build();
    }

    // 2.队列
    @Bean
    public Queue queue() {
        return QueueBuilder.durable(QUEUE_NAME).build();
    }

    // 3.队列与交换机的绑定
    /* 1.知道那个队列
       2.知道那个交换机
       3.routing key
    */
    @Bean
    public Binding bindingQueueExchange(Queue queue, Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY).noargs();
    }

    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

发送数据

@Test
public void testTopicModel() {
    // 只需要传入要发送的对象,自动序列化发送到 RabbitMQ
    HashMap<String, Object> map = new HashMap<>();
    map.put("name", "小白");
    map.put("data", Arrays.asList("苹果", "甜橙", "葡萄"));
    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.rabbit.hello", map);
}

接收数据

@Test
public void testReceive() {
    Object o = rabbitTemplate.receiveAndConvert(RabbitMQConfig.QUEUE_NAME);
    System.out.println(o.getClass());
    System.out.println(o);
}
监听队列

在消费者应用中,通常需要监听队列,以获取消息。在 Spring Boot 中如何要监听队列,需要在主启动类上开启 @EnableRabbit,然后在指定方法上使用 @RabbitListener 进行监听

@Service
public class UserServiceImpl {
    @RabbitListener(queues = "boot_queue")
    public void receive(User u) {
        System.out.println("收到消息:" + u);
    }

    @RabbitListener(queues = "boot_queue")
    public void receiveMsg(Message message){
        System.out.println(message.getBody());
        System.out.println(message.getMessageProperties());
    }
}
AmqpAdmin

AmqpAdmin 可以帮助我们创建和删除 Queue,Exchange,Binding规则

@Test
public void testAmqpAdmin() {
    // 创建交换机
    amqpAdmin.declareExchange(new DirectExchange("amqpAdmin.exchange"));
    // 创建队列
    amqpAdmin.declareQueue(new Queue("amqpAdmin.queue", true));
    // 绑定关系
    amqpAdmin.declareBinding(new Binding("amqpAdmin.queue",
            Binding.DestinationType.QUEUE,
            "amqpAdmin.exchange",
            "amqp.hello", null));
}

4、SpringBoot与安全


Spring Security

在 SpringBoot 中常用的安全框架有 Spring SecurityShiro

Spring Security 是针对 Spring 项目的安全框架,也是 Spring Boot 底层安全模块默认的技术选型。它可以实现强大的 web 安全控制。对于安全控制,只需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
登录 & 认证 & 授权 & 注销 & 记住我
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 定制请求的授权规则
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("VIP1")
                .antMatchers("/level2/**").hasRole("VIP2")
                .antMatchers("/level3/**").hasRole("VIP3");
        // 开启自动配置的登录功能,/login来到登录页,登录失败重定向到/login?error,支持更多详细定制
        http.formLogin().usernameParameter("uname").passwordParameter("upwd").loginPage("/userLogin");
        // 开启自动配置的注销功能,/logout发起注销请求,并清空session,注销成功默认会返回 /login?logout页面
        http.logout().logoutSuccessUrl("/userLogin");
        // 开启记住我功能(登录成功之后,将cookie发送给浏览器进行保存,下次访问页面的时候带上这个cookie,只要通过检查就会自动登录,点击注销也会删除该cookie)
        http.rememberMe();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 定义认证规则
        // 从spring security 5.X开始,需要使用密码编码器,也就是需要对你的明文密码进行加密
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin").password(new BCryptPasswordEncoder().encode("admin")).roles("VIP1", "VIP2", "VIP3")
                .and()
                .withUser("xiaobai").password(new BCryptPasswordEncoder().encode("666666")).roles("VIP1", "VIP2");
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值