springboot

Spring Boot学习第五天

一、Spring Boot 异常处理

Object
Throwable
可以捕获并处理的异常 Exception
运行时异常 RunTimeException
非运行时异常 IOException
不可处理的异常 Error

1 Spring Boot中捕获全局异常

  • 在Spring Boot中处理全局异常,两个注解起了关键作用
    • @ControllerAdvice:控制器增强,配合@ExceptionHandler来增所有的控制方法
    • @ExceptionHandler:用来捕获@RequestMapping抛出的异常;
  • 首先定义一个处理异常的类,在类上加入@ControllerAdvice注解,标识此类为异常处理类
  • 再自定义一个异常处理方法并加上@ExceptionHandler指定要处理的异常
/**
 * 使用注解开发异常处理器,声明该类是一个Controller的通知类,声明后该类就会被加载成异常处理器
 */
@Component
@ControllerAdvice
@Slf4j
public class ExceptionAdvice {

    /**
     * 类中定义的方法携带@ExceptionHandler注解的会被作为异常处理器,后面添加实际处理的异常类型
     * @param ex 异常对象
     * @return
     */
    @ExceptionHandler(NullPointerException.class)
    @ResponseBody
    public String doNullException(Exception ex) {
        log.error("空指针异常");
        return  "空指针异常";
    }

    @ExceptionHandler(ArithmeticException.class)
    @ResponseBody
    public String doArithmeticException(Exception ex) {
        return "ArithmeticException";
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public String doException(Exception ex) {
        return "all";
    }
}
  • 在Controller方法自定义一个异常进行测试,通过debug方法也可以看到自定义的异常被捕获
#Swagger返回结果:
Code	200	
Response body 空指针异常

#控制台结果:
2021-04-14 10:51:57.156 ERROR 14696 --- [nio-8082-exec-1] com.example.exception.ExceptionAdvice    : 空指针异常
2021-04-14 10:51:57.263 ERROR 14696 --- [nio-8082-exec-1] o.s.web.servlet.HandlerExecutionChain    : HandlerInterceptor.afterCompletion threw exception

java.lang.NullPointerException: null

2. 优化全局异常捕获

以上就基本的完成的异常处理,但是还不完善;需要改善的地方:

  • 异常返回应该有一个统一的异常业务类,和异常常量类来封装异常错误信息;

实现步骤:

  • 自定义一个业务异常类 BusinessException,用于处理业务异常
/**
 * 自定义异常继承RuntimeException,覆盖父类所有的构造方法
 * @author 沈晨曦
 */
public class BusinessException extends RuntimeException {

    public BusinessException(String message) {
        super(message);
    }

    public BusinessException(String message, Throwable cause) {
        super(message, cause);
    }

    public BusinessException(Throwable cause) {
        super(cause);
    }

    public BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

}
  • 定义一个SystemException用于处理系统异常
/**
 * 自定义异常继承RuntimeException,覆盖父类所有的构造方法
 * @author 沈晨曦
 */
public class SystemException extends RuntimeException {
    public SystemException() {
    }

    public SystemException(String message) {
        super(message);
    }

    public SystemException(String message, Throwable cause) {
        super(message, cause);
    }

    public SystemException(Throwable cause) {
        super(cause);
    }

    public SystemException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}
  • 定义一个枚举类来封装异常状态码和错误提示信息
/**
 * 异常枚举类
 * @author 沈晨曦
 * @version 2021/4/14 11:04:29
 */
public enum ExceptionHttpCodeEnum {
    /**
     * 用于封装空指针异常
     */
    NULLPOINTEXCEPTION(201, "空指针异常");


    int code;
    String errorMessage;

    ExceptionHttpCodeEnum(int code, String errorMessage) {
        this.code = code;
        this.errorMessage = errorMessage;
    }

    public int getCode() {
        return code;
    }

    public String getErrorMessage() {
        return errorMessage;
    }
}
  • 在捕获到异常时,根据自己需求返回到业务异常还是系统异常
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public BusinessException doNullException(Exception ex) {
    log.error("空指针异常");
    return  new BusinessException(ExceptionHttpCodeEnum.NULLPOINTEXCEPTION.toString());
  	//return   ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR);
}
  • Swagger在线测试,查看返回结果
{
  "cause": null,
  "stackTrace": [
    {
      "methodName": "doNullException",
      "fileName": "ExceptionAdvice.java",
      "lineNumber": 29,
      "className": "com.example.exception.ExceptionAdvice",
      "nativeMethod": false
    },....省略中间
  ],
  "localizedMessage": "空指针异常",
  "message": "空指针异常",
  "suppressed": []
}

3. 再次优化

可以看到上面的异常捕获大致完善,但是还能持续优化,响应结果过于长篇大幅,相对信息比较全,但是有更好的解决方案;

  • 封装一个统一的返回类,所有结果通过此类返回
/**
 * 通用的结果返回类
 * @param <T>
 */
public class ResponseResult<T> implements Serializable {

    private String host;

    private Integer code;

    private String errorMessage;

    private T data;

    public static ResponseResult errorResult(ExceptionHttpCodeEnum enums){
        return setExceptionHttpCodeEnum(enums,enums.getErrorMessage());
    }
    private static ResponseResult setExceptionHttpCodeEnum(ExceptionHttpCodeEnum enums, String errorMessage){
        return okResult(enums.getCode(),errorMessage);
    }
    public static ResponseResult okResult(int code, String msg) {
        ResponseResult result = new ResponseResult();
        return result.ok(code, null, msg);
    }

    public static ResponseResult okResult(Object data) {
        ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getErrorMessage());
        if(data!=null) {
            result.setData(data);
        }
        return result;
    }
}
  • 测试
{
  "host": null,
  "code": 201,
  "errorMessage": "空指针异常",
  "data": null
}

4. 对Controller进行优化测试

  • ResponseResult返回类新增一个构造方法
/** 成功响应方法
 * @param o 响应对象
 * @param success 成功响应码和message的封装
 * @return 通用返回结果
 */
public static ResponseResult okResult(Object o, AppHttpCodeEnum success) {
        ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getErrorMessage());
        return result.ok(success.getCode(),o,success.getErrorMessage());
    }
  • 测试:
{
  "host": null,
  "code": 200,
  "errorMessage": "操作成功",
  "data": [
    {
      "id": 1,
      "name": "韩雪",
      "age": 18
    },
    {
      "id": 3,
      "name": "一栗小莎子",
      "age": 24
    },
    {
      "id": 4,
      "name": "史蒂夫",
      "age": 18
    }
  ]
}

二、任务调度

1. Java中常见的定时任务比较

  • Timer:jdk自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让程序按照某一个频度执行,但不能在指定时间运行,一般很少使用,主要用于非Spring项目简单的任务调度。
  • Spring Task:Spring3.0以后自带的Task,可以将它看成一个轻量级的Quartz,使用起来比Quartz简单很多,在Spring应用中,直接使用@Scheduled注解即可,但对于集群项目比较麻烦,需要避免集群环境下任务被多次调用的情况,而且不能动态维护,任务启动以后不能修改、暂停等。
  • Quartz:好用的第三方任务调度工具,可谓是企业级应用系统任务调度工具的老大。可以方便的在集群下使用、可以动态增加、删除、暂停等维护任务,动态定时任务更加灵活。而且,和Spring Boot集成非常方便。

2. Spring boot中集成task

  • pom
 	<!-- Spring Boot启动器父类 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- Spring Boot web启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
  • 配置属性:无

  • 编码

    • 启动类:只需要加上@EnableScheduling注解开启task
    • 自定义一个task类作为任务调度类,在类上加入@Component注解,再自定义一个任务调度方法,并加上@Scheduling注解
    • @Scheduling:此注解可以通过属性指定每隔一定时间执行此方法。以下代码是任务执行完5秒后继续执行此任务
    @Scheduled (fixedDelay= 5000 )
    public  void  doSomething() {    
         // 做些什么
    }
    
    • fixedDelay:固定延迟。基于上一次任务结束的时间节点,计数达到指定时间重新执行;
    • fixedRete:固定速率。以上一个任务开始的时间节点为基准,每隔指定时间重新执行;
    • initialDelay:延迟初始化;指定在第一次调用之前的等待时间;
    • cron= "*/5 * * * * MON-FRI":只会在工作日执行

3. Quartz

3.1 quart介绍
  • 定义:Quartz是Job scheduling(作业调度)领域的一个开源项目,Quartz既可以单独使用也可以跟spring框架整合使用,在实际开发中一般会使用后者。使用Quartz可以开发一个或者多个定时任务,每个定时任务可以单独指定执行的时间,例如每隔1小时执行一次、每个月第一天上午10点执行一次、每个月最后一天下午5点执行一次等。
  • 使用场景:定时清理垃圾资源、定时扫描、监控需要被处理的资源等;
  • Quartz有3个核心要素:调度器(Scheduler)、任务(Job)、触发器(Trigger)。
  • Job(任务):是一个接口,有一个方法void execute(),可以通过实现该接口来定义需要执行的任务(具体的逻辑代码)。
  • JobDetail:Quartz每次执行job时,都重新创建一个Job实例,会接收一个Job实现类,以便运行的时候通过newInstance()的反射调用机制去实例化Job.JobDetail是用来描述Job实现类以及相关静态信息,比如任务在scheduler中的组名等信息。
  • Trigger(触发器):描述触发Job执行的时间触发规则实现类SimpleTrigger和CronTrigger可以通过crom表达式定义出各种复杂的调度方案。
  • Calendar:是一些日历特定时间的集合。一个Trigger可以和多个 calendar关联,比如每周一早上10:00执行任务,法定假日不执行,则可以通过calendar进行定点排除。
  • Scheduler(调度器):代表一个Quartz的独立运行容器。Trigger和JobDetail可以注册到Scheduler中。Scheduler可以将Trigger绑定到某一JobDetail上,这样当Trigger被触发时,对应的Job就会执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job.
3.2 quart 使用
  • pom
<!--spring boot集成quartz-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
  • 编码

    • 自定义一个job类,继承QuartzJobBean类,并重写executeInternal方法,此方法内部写代码逻辑
    public class DateTimeJob extends QuartzJobBean {
     
    @Override
    public void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            //获取JobDetail中关联的数据
    	String msg = (String) jobExecutionContext.getJobDetail().getJobDataMap().get("msg");
        System.out.println("current time :"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "---" + msg);
        }
    }
    
    • 配置属性:
      • 指定要执行的job类
    @Configuration
    public class QuartzConfig {
        @Bean
        public JobDetail printTimeJobDetail() {
            /*
                newJob:指定job类,这里指定我们自定义的job类
                withIdentity:取名
                usingJobData:存入键值对
             */
            return JobBuilder.newJob(DateTimeJob.class)
                    .withIdentity("DateTimeJob")
                    .usingJobData("msg", "Hello Quartz")
                    .storeDurably()
                    .build();
        }
    
        @Bean
        public Trigger printTimeJobTrigger() {
            CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/1 * * * * ?");
            /*
            forJob:指定job
            withIdentity:给任务调取取名
             */
            return TriggerBuilder.newTrigger()
                    .forJob(printTimeJobDetail())
                    .withIdentity("quartzTaskService")
                    .withSchedule(cronScheduleBuilder)
                    .build();
        }
    }
    
    

4. corn表达式

cron表达式分为七个域,之间使用空格分隔。其中最后一个域(年)可以为空。每个域都有自己允许的值和一些特殊字符构成。使用这些特殊字符可以使我们定义的表达式更加灵活。

下面是对这些特殊字符的介绍:

逗号(,):指定一个值列表,例如使用在月域上1,4,5,7表示1月、4月、5月和7月

横杠(-):指定一个范围,例如在时域上3-6表示3点到6点(即3点、4点、5点、6点)

星号(*):表示这个域上包含所有合法的值。例如,在月份域上使用星号意味着每个月都会触发

斜线(/):表示递增,例如使用在秒域上0/15表示每15秒

问号(?):只能用在日和周域上,但是不能在这两个域上同时使用。表示不指定

井号(#):只能使用在周域上,用于指定月份中的第几周的哪一天,例如6#3,意思是某月的第三个周五 (6=星期五,3意味着月份中的第三周)

L:某域上允许的最后一个值。只能使用在日和周域上。当用在日域上,表示的是在月域上指定的月份的最后一天。用于周域上时,表示周的最后一天,就是星期六

W:W 字符代表着工作日 (星期一到星期五),只能用在日域上,它用来指定离指定日的最近的一个工作日

  • 案例:

“30 * * * * ?”:每半分钟触发任务;

“30 10 * * * ?”:每小时的10分30秒触发任务;

“30 10 1 * * ?”“:每天1点10分30秒触发任务;

“30 10 1 20 * ?”:每月20号1点10分30秒触发任务

“30 10 1 20 10 ? *”:每年10月20号1点10分30秒触发任务;

“30 10 1 20 10 ? 2011”:2011年10月20号1点10分30秒触发任务;

“30 10 1 ? 10 * 2011”:2011年10月每天1点10分30秒触发任务;

“30 10 1 ? 10 SUN 2011”:2011年10月每周日1点10分30秒触发任务;

“15,30,45 * * * * ?”:每15秒,30秒,45秒时触发任务;

“15-45 * * * * ?”:15到45秒内,每秒都触发任务;

“15/5 * * * * ?”:每分钟的每15秒开始触发,每隔5秒触发一次;

“15-30/5 * * * * ?”:每分钟的15秒到30秒之间开始触发,每隔5秒触发一次;

“0 0/3 * * * ?”:每小时的第0分0秒开始,每三分钟触发一次;

“0 15 10 ? * MON-FRI”:星期一到星期五的10点15分0秒触发任务;

“0 15 10 L * ?”:每个月最后一天的10点15分0秒触发任务;

“0 15 10 LW * ?”:每个月最后一个工作日的10点15分0秒触发任务;

“0 15 10 ? * 5L”:每个月最后一个星期四的10点15分0秒触发任务;

“0 15 10 ? * 5#3”:每个月第三周的星期四的10点15分0秒触发任务;

三、IOC涉及到的设计模式

1 策略模式

  • 定义:策略模式也叫政策模式,是一种行为型设计模式,是一种比较简单的设计模式。策略模式采用了面向对象的继承和多态机制;

  • 场景:

    • 多个类只有在算法或行为上稍有不同的场景。
    • 算法需要自由切换的场景。
    • 需要屏蔽算法规则的场景;
  • 注意:策略类不要太多,如果一个策略家族的具体策略数量超过4个,则需要考虑混合模式,解决策略类膨胀和对外暴露问题。在实际项目中,一般通过工厂方法模式来实现策略类的声明。

  • 优点:

    • 算法可以自由切换。
    • 避免使用多重条件判断。
    • 扩展性良好。
  • 缺点:

    • 策略类会增多。
    • 所有策略类都需要对外暴露。
  • **在spring中的应用:**例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略,如:

    • 当ApplicationContext指向 PathXmlApplicationContext子类时:
    ApplicationContext ctx = new Class PathXmlApplicationContext("bean.xml");
    Resource res = ctx.getResource("book.xml");
    
    
    • 当ApplicationContext指向FileSystemXmlApplicationContext子类时
    ApplicationContext ctx = new Class FileSystemXmlApplicationContext("bean.xml");
    
    
  • 在项目中使用:

    • 先定义一个接口
    public interface Driveable {
    
        void drive();
    }
    
    
    • 定义接口的实现类,每一个实现就是一种策略
    @Slf4j
    public class Car implements Driveable {
        @Override
        public void drive() {
            log.info("开着汽车出去浪~");
        }
    }
    
    
    @Slf4j
    public class Ship implements Driveable {
        @Override
        public void drive() {
            log.info("坐船出去浪~");
        }
    }
    
    
    • 测试
    // 策略1
    Driveable car = new Car();
    new Person(car).hangOut();
    
    // 策略2
    Driveable ship = new Ship();
    new Person(ship).hangOut();
    
    
    • 结果
    15:56:35.944 [main] INFO com.example.demo.ioc.Car - 开着汽车出去浪~
    15:56:35.948 [main] INFO com.example.demo.ioc.Ship - 坐船出去浪~
    
    

2 单例模式

  • 定义:单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
  • 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
  • 注意:
    • 单例类只能有一个实例。
    • 单例类必须自己创建自己的唯一实例。
    • 单例类必须给所有其他对象提供这一实例。
  • spring中:bean默认为单例模式,又分为饿汉和懒汉
    • 饿汉:一开始就加载实例,但是会造成性能浪费;
    • 懒汉:延迟加载;bean默认就是延迟加载
  • 单例bean的优势
    • 减少了新生成实例的消耗:
      • spring会通过反射会cglib来生成bean
      • 给对象分配内存
    • 减少了JVM垃圾回收
    • bean复用,能够快速取到bean,在单例模式下,bean除了第一次生成,其余都是从缓存取
  • 劣势:
    • 单例bean有线程安全问题,所有请求都共享一个bean实例;
  • 单例模式三要素:
    • 构造方法私有化
    • 静态属性指向实例
    • public static的getInstance方法,返回第二部的静态属性;
  • spring实现单例模式,使用@Scope属性指定为singleton
  • Spring 通过 ConcurrentHashMap 实现单例注册表的特殊方式实现单例模式。Spring 实现单例的核心代码:
// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(beanName, "'beanName' must not be null");
        synchronized (this.singletonObjects) {
            // 检查缓存中是否存在实例  
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
                //...省略
                try {
                    singletonObject = singletonFactory.getObject();
                }
                //...省略
                // 如果实例对象在不存在,我们注册到单例注册表中。
                addSingleton(beanName, singletonObject);
            }
            return (singletonObject != NULL_OBJECT ? singletonObject : null);
        }
    }
    //将对象添加到单例注册表
    protected void addSingleton(String beanName, Object singletonObject) {
            synchronized (this.singletonObjects) {
                this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

            }
        }
}

3 工厂模式

  • 定义:工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

  • 在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

  • 优点:

    • 一个调用者想创建一个对象,只要知道其名称就可以了。
    • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
    • 屏蔽产品的具体实现,调用者只关心产品的接口。
  • **缺点:**每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。

  • 使用场景:

    • 日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。
    • 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。
    • 设计一个连接服务器的框架,需要三个协议,“POP3”、“IMAP”、“HTTP”,可以把这三个作为产品类,共同实现一个接口。
  • **注意事项:**作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。

  • spring中:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象

    • BeanFactory:延迟注入(使用到某个 bean 的时候才会注入)。相比于ApplicationContext 来说会占用更少的内存,程序启动速度更快。
    • ApplicationContext:容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。
  • 使用:

    • 先定义一个接口:
    public interface Shape {
       void draw();
    }
    
    
    • 定义该接口的实现
    public class Rectangle implements Shape {
     
       @Override
       public void draw() {
          System.out.println("Inside Rectangle::draw() method.");
       }
    }
    
    
    public class Square implements Shape {
     
       @Override
       public void draw() {
          System.out.println("Inside Square::draw() method.");
       }
    }
    
    
    public class Circle implements Shape {
     
       @Override
       public void draw() {
          System.out.println("Inside Circle::draw() method.");
       }
    }
    
    
    • 创建工厂生成对应实体类对象
    public class ShapeFactory {
        
       //使用 getShape 方法获取形状类型的对象
       public Shape getShape(String shapeType){
          if(shapeType == null){
             return null;
          }        
          if(shapeType.equalsIgnoreCase("CIRCLE")){
             return new Circle();
          } else if(shapeType.equalsIgnoreCase("RECTANGLE")){
             return new Rectangle();
          } else if(shapeType.equalsIgnoreCase("SQUARE")){
             return new Square();
          }
          return null;
       }
    }
    
    
    • 测试
    public class FactoryPatternDemo {
     
       public static void main(String[] args) {
          ShapeFactory shapeFactory = new ShapeFactory();
     
          //获取 Circle 的对象,并调用它的 draw 方法
          Shape circle = shapeFactory.getShape("CIRCLE");
          circle.draw();
     
          //获取 Rectangle 的对象,并调用它的 draw 方法
          Shape rectangle = shapeFactory.getShape("RECTANGLE");
          rectangle.draw();
     
          //获取 square 的对象,并调用它的 draw 方法
          Shape square = shapeFactory.getShape("SQUARE");
          square.draw();
       }
    }
    
    
    • 结果
    Inside Circle::draw() method.
    Inside Rectangle::draw() method.
    Inside Square::draw() method.
    
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值