Hystrix初试牛刀(使用切面编程/拦截器踩坑了)

Hystrix服务降级

首先配置Hystrix时需要使用到@HystrixCommand注解,将该注解标记在所需要限制或服务降级的代码块上(目前只在controller层的方法上使用过),其中降级的目标方法,方法入参需要与被降级的方法保持一致,最多添加一个Throwable类型参数,保证ignoreExceptions能够使用,具体配置如下:


@Slf4j
@RestController
public class InternationalGMController {
   
    @Autowired
    private IInternationalGMjiBiz internationalGMjiBiz;
    @Autowired
    private RestTemplate restTemplate;
    @Value("${fallbackServiceUrl}")
    private String fallBackServiceUrl;
    @Value("${isManualFallback}")
    private String isManualFallback;
    @Value("${rocketmq.producer.topic}")
    private String defaultTopic;

    @SneakyThrows
    @PostMapping(value = "/internationalGMWaybill")
    @HystrixCommand( fallbackMethod = "fallbackService",
             /* 忽略不需要使服务降级的异常,如非法数据校验需要传递response */
            ignoreExceptions = {InternationalGMException.class, ApiException.class, UnknownHostException.class},
            /* 线程超时时间,如果超过时间则算异常处理*/
            commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500")},
            threadPoolProperties = {/* 核心线程或信号量限制*/
                    @HystrixProperty(name = "coreSize", value = "10"),
                    @HystrixProperty(name = "maxQueueSize", value = "20"),
                    @HystrixProperty(name = "keepAliveTimeMinutes", value = "0"),
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
                    @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
                    @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1440")
            })
   public WaybillResponse<String> internationalGMWaybillServlet(HttpServletRequest request, @RequestParam("data_digest") String data_digest, @RequestParam("logistics_interface") String logistics_interface) {
        log.info("internationalGMWaybillServlet data_digest:{},logistics_interface:{}", data_digest, logistics_interface);
        //验证项目中拦截器线程和controller线程是否一个线程
        System.out.println(Thread.currentThread().getId()+Thread.currentThread().getName()+"controller");
        // do something
        return new WaybillResponse<>(true, result, ResponseEnums.SUCCESS_OPTION);
        } else {
            // 通过配置中心设置手动抛出异常,进行手动降级,手法比较低劣哈
            throw new ManualFallbackException("应用异常,手动降级");
        }
    }

    //降级方法将会被调用
    public WaybillResponse<String> fallbackService(HttpServletRequest request, @RequestParam("data_digest") String data_digest, @RequestParam("logistics_interface") String logistics_interface, Throwable e) {
        log.error("调用降级服务 url={} data_digest:{},logistics_interface:{}", fallBackServiceUrl, data_digest, logistics_interface);
        MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
        paramMap.add("data_digest", data_digest);
        paramMap.add("logistics_interface", logistics_interface);
        return new WaybillResponse<>(false, restTemplate.postForObject(fallBackServiceUrl, paramMap, String.class), ResponseEnums.SERVICE_FALLBACK);
    }
}

忽略异常和数据校验

在这里有个比较隐蔽的东西,我想分享下。
场景:在代码中我有对反序列化(json或者字节数组转实体)进行校验,校验方式使用的JSR-303规范进行校验

扩展阅读
  • JSR-303简单介绍
    JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation,官方参考实现是Hibernate Validator,与Hibernate ORM(
    对象关系映射Object Relational Mapping)没有什么关系,JSR-303可以再controller控制器层对数据进行简单方便校验。
  • JSR-303用到的JAR库
    validation-api-1.0.0.GA.jar:JDK的接口;
    hibernate-validator-4.2.0.Final.jar是对上述接口的实现(反正我看包名是这个地方)
    log4j、slf4j、slf4j-log4j
  • 简单校验规则
    1. 判空检查
    @Null // 验证对象是否为null 
    @NotNull // 验证对象是否不为null, 无法查检长度为0的字符串 
    @NotBlank // 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格. 
    @NotEmpty // 检查约束元素是否为NULL或者是EMPTY.
    
    1. 长度检查
    @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内 
    @Length(min=, max=) 验证字符串长度是否在最大最小值之间.
    
    1. 日期检查
    @Past 验证 Date 和 Calendar 对象是否在当前时间之前,验证成立的话被注释的元素一定是一个过去的日期 
    @Future 验证 Date 和 Calendar 对象是否在当前时间之后 ,验证成立的话被注释的元素一定是一个将来的日期 
    @Pattern 验证 String 对象是否符合正则表达式的规则,被注释的元素符合制定的正则表达式,regexp:正则表达式 flags: 指定 Pattern.Flag 的数组,表示正则表达式的相关选项。
    
    1. 数字区间检查
    1. 其他检查
    @Valid // 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值  >部分进行校验.(是否进行递归验证) 
    @CreditCardNumber // 信用卡验证 
    @Email // 验证是否是邮件地址,如果为null,不进行验证,算通过验证。 
    @ScriptAssert(lang= ,script=, alias=) 
    @URL(protocol=,host=, port=,regexp=, flags=)
    

先贴上我校验的实体代码,这是一个很简单的JavaBean,然而我却想把这些字段都做校验

package org.tennyson.international;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
/**
 * author:Tennyson time:2020/5/8
 */
@Data
public class MDto {
    @NotBlank(message = "wNo must not empty")
    private String wNo;
    @NotBlank(message = "cCode must not empty")
    @Length(max = 20,message="cCode length too long")
    private String cCode;
    private String orderLogisticsCode;
    @NotBlank(message = "bCode must not empty")
    private String bCode;
    @NotBlank(message ="bName" )
    private String bName;

}

前面controller接收的是一个string字符串,所以我不能再controller层进行校验,因此我放在了序列化之后使用校验器进行校验。

/* 创建一个全局变量,通过验证器工程获取一个验证器 */
private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

@SneakyThrows /* 这个东西是悄悄抛异常,反正代码没有try-catch要干净多了 */
@Override
public String sendMessage(String logisticOrder) {
    MDto wOrg = JSON.parseObject(logisticOrder, MDto.class); // 反序列化
    Set<ConstraintViolation<MDto>> set = validator.validate(wOrg, Default.class);  // 开始验证序列化后的实体
    if (!set.isEmpty()) {
        // 一旦存在验证不通过,就获取验证不通过的原因,并抛异常error输出日志
        String reason = set.iterator().next().getMessage();
        log.error("Data validation failure,Original data:{} Failure reason:{}", logisticOrder, reason);
        // 就是这个异常非常重要,我需要把这个异常信息通过controller的response返回给客户端
        throw new InternationalGMException(ResponseEnums.BAD_REQUEST, reason);
    }
   /**
     * do someting
    */
    return logisticOrder; // 图方便,我这肯定不是这么写的,自己改改哈
}

在上面验证了实体的数据,不规范的数据直接抛弃,但现在存在一个问题了,我这里抛出来的异常,会被Hystrix捕获然后,它居然给我服务降级了!!!我要把这个异常信息返回给客户端哇,因此就出现了咱们所要使用的忽略异常,也就是@HystrixCommandignoreExceptions属性,我随即设置了这个属性,但是没生效,后来查阅资料,发觉别人降级服务的方法中多了一个入参。多了一个Throwable e 然而什么也没有做,只是多了这么个东西,异常信息就抛出去了(emmm……我这里做了相对友好的全局异常处理,所以我感放心大胆的抛异常)


public WaybillResponse<String> fallbackService(@RequestParam("data_digest") String data_digest, @RequestParam("logistics_interface") String logistics_interface, Throwable e) {
    log.error("调用降级服务 url={} data_digest:{},logistics_interface:{}", fallBackServiceUrl, data_digest, logistics_interface);
    return new WaybillResponse<>(false, "我在这里降级调接口,备胎上位", ResponseEnums.SERVICE_FALLBACK);
}

咱们继续扒一扒源码,我看了看源码大概是在com.netflix.hystrix.contrib.javanica.command.AbstractHystrixCommand这里头有判断这个异常是否被忽略的异常

/**
 * Check whether triggered exception is ignorable.
 *
 * @param throwable the exception occurred during a command execution
 * @return true if exception is ignorable, otherwise - false
 */
boolean isIgnorable(Throwable throwable) {
    if (ignoreExceptions == null || ignoreExceptions.isEmpty()) { // ignoreExceptions被初始化时很重要   
        return false;
    }
    for (Class<? extends Throwable> ignoreException : ignoreExceptions) {
        if (ignoreException.isAssignableFrom(throwable.getClass())) {
            return true;
        }
    }
    return false;
}

/**
 * Executes an action. If an action has failed and an exception is ignorable then propagate it as HystrixBadRequestException
 * otherwise propagate original exception to trigger fallback method.
 * Note: If an exception occurred in a command directly extends {@link java.lang.Throwable} then this exception cannot be re-thrown
 * as original exception because HystrixCommand.run() allows throw subclasses of {@link java.lang.Exception}.
 * Thus we need to wrap cause in RuntimeException, anyway in this case the fallback logic will be triggered.
 *
 * @param action the action
 * @return result of command action execution
 */
Object process(Action action) throws Exception {
    Object result;
    try {
        result = action.execute();
        flushCache();
    } catch (CommandActionExecutionException throwable) {
        Throwable cause = throwable.getCause();
        if (isIgnorable(cause)) {
             // 如果是忽略的异常就抛出一个HystrixBadRequest异常,又来了一次封装
            throw new HystrixBadRequestException(cause.getMessage(), cause);
        }
        if (cause instanceof RuntimeException) {
            throw (RuntimeException) cause;
        } else if (cause instanceof Exception) {
            throw (Exception) cause;
        } else {
            // instance of Throwable
            throw new CommandActionExecutionException(cause);
        }
    }
    return result;
}

后面的因为我断点时间太长了,服务又被降级了,害~下载再深入了解下吧

流量监控设置

hystrix.stream属于静态资源,因此在加载过程前,虽然


/**
 * author:Tennyson time:2020/5/9
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private RestTemplateBuilder builder;

    // 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例
    @Bean
    public RestTemplate restTemplate() {
        return builder.build();
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/statics/**")
                .addResourceLocations("classpath:/statics/");
        // 解决 SWAGGER 404报错
        registry.addResourceHandler("/swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
        // 解决Hystrix静态资源访问404问题,作用与xml中配置静态资源访问一致
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/META-INF/resources/")
                .addResourceLocations("classpath:/resources/")
                .addResourceLocations("classpath:/static/")
                .addResourceLocations("classpath:/public/");
    }

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {

    }
    /**
     **添加Hystrix Dashboard 的servlet 能够通过httprequest进行访问
     **/
    @Bean
    public ServletRegistrationBean<HystrixMetricsStreamServlet> getServlet() {
        HystrixMetricsStreamServlet hystrixMetricsStreamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean<HystrixMetricsStreamServlet> servletRegistrationBean = new ServletRegistrationBean();
        servletRegistrationBean.setServlet(hystrixMetricsStreamServlet);
        servletRegistrationBean.setLoadOnStartup(1);
        servletRegistrationBean.addUrlMappings("/hystrix.stream");
        servletRegistrationBean.setName("HystrixMetricsStreamServlet");
        return servletRegistrationBean;
    }

}

SpringBoot启动类使用@EnableCircuitBreaker@EnableHystrixDashboard开启熔断器和hystrix仪表盘


/**
 * author:Tennyson time:2020/5/7
 */
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class, SessionAutoConfiguration.class, DruidDataSourceAutoConfigure.class}) //关闭springboot的自动配置
@EnableHystrixDashboard // 启用hystrix监控仪表盘
@EnableCircuitBreaker // 启用hystrix熔断器
@EnableSwagger2 //启用swagger2 接口文档
@EnableAspectJAutoProxy //启用AspectJ进行AOP
@ImportResource(
        locations = {
                "classpath*:spring-*.xml",
                "classpath*:*/spring-*.xml"
        })
@ComponentScan(basePackages = {"org.tennyson" })
public class InternationalGMProducerApp {
    public static void main(String[] args) {
        SpringApplication.run(InternationalGMProducerApp.class, args);
    }
}

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.tennyson.international</groupId>
        <artifactId>international</artifactId>
        <version>alpha</version>

    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>international</artifactId>
    <version>alpha</version>
    <packaging>jar</packaging>
    

    <dependencies>
        <!--apollo配置中心-->
        <dependency>
            <groupId>com.ctrip.framework.apollo</groupId>
            <artifactId>apollo-client</artifactId>
        </dependency>
        <!--内涵springboot启动器和springMVC依赖包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--熔断器依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.7.0</version>
        </dependency>
        <!--日志依赖包-->
        <!--<dependency>
            <groupId>net.logstash.logback</groupId>
            <artifactId>logstash-logback-encoder</artifactId>
        </dependency>-->
    </dependencies>
    <build>
        <finalName>${artifactId}-${version}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

线程替换

在输出日志过程中,我有尝试使用MDC将部分key-value追加到日志中,日志输出到ELK日志中心,使用的com.github.danielwegener.logback.kafka.KafkaAppender进行日志输出至kafka,其中编码器使用的net.logstash.logback.encoder.LogstashEncoder
使用MDC追加时发现追加不进去,kafkaAppender输出的日志根本就没有我追加的key-value,因此查看了MDC的使用姿势和它的暂存方式。
MDC简单封装成一个工具,只需要进行调用即可,此处涉及到java8新特性中的static方法

java8中为接口新增了一项功能:定义一个或者更多个静态方法。用法和普通的static方法一样。实现接口的类或者子接口不会继承接口中的静态方法

扩展阅读:
JAVA8 新特性详解

  1. Lambda表达式和函数式接口(也称为闭包),简单来说就是方法也成为了第一公民
  2. 接口的默认方法和静态方法
  3. 方法引用
  4. 重复注解
  5. 泛型类,泛型方法
  6. 注解应用场景拓宽
package cn.yto.international.utils;

import lombok.val;
import org.slf4j.MDC;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * author:admin date:2020/6/4
 **/
public interface MDCUtils{

    /**
     * 设置请求中对于日志查询和统计具有意义的字段
     */
    static void putRequestFields(HttpServletRequest request) {
        // 此处为测试拦截器中线程是否与MDCUtil的线程一致
        System.out.println(Thread.currentThread().getId()+"mdcutil");
        val IS_DEBUG = request.getHeader("IS_DEBUG");
        val DEVICE_ID = request.getHeader("DEVICE_ID");
        MDC.put("req_is_debug", StringUtils.isEmpty(IS_DEBUG) ? "false" : "true");
        MDC.put("req_device_id", StringUtils.isEmpty(DEVICE_ID) ? "anonymity_device_id" : DEVICE_ID);
        MDC.put("req_user_agent", request.getHeader("User-Agent"));
        // MDC.put("req_request_url", request.getRequestURL());
        // 获取请求对应的交易名称
        MDC.put("req_request_uri", request.getRequestURI());
        // 获取请求对应的交易对应的方法(如:POST)
        MDC.put("req_request_method", request.getMethod());
        // 返回请求体内容的长度,不包含url query string,字节为单位
        MDC.put("req_content_length", String.valueOf(request.getContentLength()));
        // 获取发出请求的客户端的IP地址
        MDC.put("req_remote_addr", request.getRemoteAddr());
        // 获取发出请求的客户端的端口号
        // MDC.put("req_remote_port", String.valueOf(request.getRemotePort()));
        // 如果用户已经过认证,则返回发出请求的用户登录信息
        val userid = request.getRemoteUser();
        MDC.put("req_remote_user", StringUtils.isEmpty(userid) ? "anonymity_user" : userid);
        // qr放在这里设置不生效,目前调整到控制器的拦截器进行设置
        // MDC.put("req_query_string", request.getQueryString()));
    }

    /**
     * 放置spring mvc拦截器触发时机才能获取到的字段
     */
    static void putSpringMVCInterceptorFields(HttpServletRequest request) {
        MDC.put("req_query_string", String.valueOf(request.getQueryString()));
    }

    /**
     * 放置响应结果中有意义的字段
     *
     * @param response
     */
    static void putResponseFields(HttpServletResponse response) {
        MDC.put("resp_status", String.valueOf(response.getStatus()));
    }
}

接下来就是通过实现HandlerInterceptor 创建拦截器了,不过在spring boot项目中,创建完拦截器后,还需要使用InterceptorRegistry 也就是配置WebMvcConfigurer 将拦截器注册(添加)到拦截器栈中,这是重点!!!

拦截器栈应该也是一种责任链设计模式,这个在设计模式模块会进行简单实现


package org.tennyson.international.intercepter;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * author:admin date:2020/6/4
 **/
@Component
public class HttpServiceIntercepter implements HandlerInterceptor  {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        MDCUtils.putRequestFields(request);
        // 此处依然是验证拦截器线程是否与controller线程,一致
        System.out.println(Thread.currentThread().getId()+Thread.currentThread().getName()+"intercepter");

        System.out.println("Pre Handle method is Calling");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("Request and Response is completed");
    }
}

拦截器注册配置

package org.tennyson.international.config;

import cn.yto.international.intercepter.HttpServiceIntercepter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * author:tennyson date:2020/6/4
 **/
@Configuration
public class ProductServiceInterceptorAppConfig implements WebMvcConfigurer {
    @Autowired
    HttpServiceIntercepter productServiceInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(productServiceInterceptor);
    }
}

现在基本的都准备完毕了,

  • 拦截器准备完毕
  • controller准备完毕
  • Hystrix在controller层进行服务降级或限流,准备完毕
  • SpringBoot启动类将所需要的模块都已开启,准备完毕
# 先用命令打个包,可视化界面都可以的,只不过我喜欢装X,哈哈哈哈哈
mvn clean package -Dskip.test=true -Pdev # 跳过测试,并只打包dev环境

# 进入target目录下,就是你jar包输出的目录,准备启动
mvn springboot:run 
# 或者是使用jvm的参数,指定配置文件时要是配置文件在jar包内就不用这个参数了,-D都是jvm的参数嗷
java -jar -Dspring.profiles.active=dev -Dspring.config.location=<你指定的application.properties路径> international-alpha.jar

项目启动了,在浏览器中打开项目的swagger接口文档http://localhost:8049/swagger-ui.html,添加参数开始发包
在这里插入图片描述
由上图可知intercepter的线程是容器的线程,而进入controller代码中的线程为hystrix的线程,所以咱们在拦截器中调用MDCUtil的线程与controller的线程不属于一个线程,断点发现,request资源在hystrix线程当中,这也是为什么MDCUtil在拦截器中获取request资源了,我的解决方法就是取消Hystrix的使用(因为在我这个项目中对于Hystrix的使用并没有特别强的需求),当然也可以尝试将hystrix线程中的资源拷贝一份到http-nio线程中。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
可以在@HystrixCommand注解中添加fallbackMethod属性和ignoreExceptions属性,将需要忽略的异常类型添加到ignoreExceptions中,然后在fallbackMethod方法中将异常抛出,这样在切面中就可以获取到方法的真实异常了。示例代码如下: ``` @HystrixCommand(fallbackMethod = "fallback", ignoreExceptions = {SomeException.class}) public void someMethod() throws SomeException { // do something } public void fallback() throws SomeException { throw new SomeException(); } ``` 在切面中可以通过JoinPoint对象获取到抛出的异常,然后进行相应的处理。示例代码如下: ``` @Around("execution(public * com.example..*.*(..)) && @annotation(hystrixCommand)") public Object around(ProceedingJoinPoint joinPoint, HystrixCommand hystrixCommand) { try { return joinPoint.proceed(); } catch (Throwable throwable) { if (throwable instanceof HystrixRuntimeException) { HystrixRuntimeException hystrixRuntimeException = (HystrixRuntimeException) throwable; if (hystrixRuntimeException.getFailureType() == FailureType.SHORTCIRCUIT) { // 熔断器打开 } else if (hystrixRuntimeException.getFailureType() == FailureType.TIMEOUT) { // 超时 } else if (hystrixRuntimeException.getFailureType() == FailureType.REJECTED_THREAD_EXECUTION) { // 线程池拒绝 } else { // fallbackMethod方法中抛出的真实异常 Throwable cause = hystrixRuntimeException.getCause(); // 处理真实异常 } } else { // 其他异常 // 处理异常 } } } ``` 需要注意的是,@HystrixCommand注解中的fallbackMethod方法必须要与原方法在同一个类中。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

喜马拉雅以南

奶茶,干杯?!

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

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

打赏作者

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

抵扣说明:

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

余额充值