非SpringCloud如何调用SpringCloud服务

前言

转载自http://blog.csdn.net/tornadojava/article/details/54580436

SpringCloud 其良好的背景以及社区非常高的活跃度,使其发展迅速,成为微服务实施的首选框架。 
如果是新的业务考虑使用SpringCloud来进行实现,面临的一个比较严峻的问题就是老的应用如何访问SpringCloud微服务,因为目前可见的SpringCloud客户端无论是Ribbon还是Feign都必须在SpringCloud中使用,但是老应用的架构什么样的都有,因此实现一个简单的通过API访问SpringCloud的应用是当务之急。

设计目标

1:可以动态更新Eureka的服务信息,和Eureka的信息基本同步。 
2:可以发现服务是否可用。 
3:可以发现服务实例是否可用。 
4:客户端可以实现简单负载均衡(类似Ribbon,随机路由,哈希路由) 
5:客户端和服务器端规范通接口通信。 
6:客户端动态代理实现接口调用。

研究阶段

如何同Eureka进行通信是关键,可以通过Eureka获得服务URL,这样就可以发送请求。 
关键接口EurekaClient。这个接口的实例过程会自动加载EurekaClient配置文件。会主动同Eureka建立连接,定时任务启动,去和Eureka同步信息。有了这个其他的都好办了。 
问我如何找到这个EurekaClient的,那就去http://bbs.springcloud.cn 去和大家交流,一定会有帮助的。 
找到这个,那就得看如何获取实例,目前获取实例的方法有些土,等有时间再详细研究更科学方法。

//最关键的代码,加载配置文件,向Eureka发送请求,获取服务列表。
DiscoveryManager.getInstance().initComponent(new MyDataCenterInstanceConfig(), new DefaultEurekaClientConfig());
ApplicationInfoManager.getInstance().setInstanceStatus(InstanceStatus.UP);
EurekaClient client = DiscoveryManager.getInstance().getEurekaClient();

//获取从Eureka获取的全部的应用列表
Applications apps = client.getApplications();
//根据应用的名称获取已经可用的应用对象,可能是注册了多个。
Application app=apps.getRegisteredApplications(serviceName);
String reqUrl=null;
if(app!=null)
{       
    List<InstanceInfo> instances = app.getInstances();      
    if(instances.size()>0)
    {
        //获取其中一个应用实例,这里可以添加路由算法
        InstanceInfo instance = instances.get(0);               
        //获取公开的地址和端口           
        reqUrl="http://"+instance.getIPAddr()+":"+instance.getPort();                       
    }
}

基本上前期的工作就完成了,启动后,可以看到客户端定时去Eureka进行更新。

完整代码:TestClientMain.java

public class TestClientMain
{
    public static void main(String[] args)
    {
        //这个名称需要用你的服务的名称替换
        String serviceName="TESTSERVICE";
        //最关键的代码,加载配置文件,向Eureka发送请求,获取服务列表。
        DiscoveryManager.getInstance().initComponent(new MyDataCenterInstanceConfig(), new DefaultEurekaClientConfig());
        ApplicationInfoManager.getInstance().setInstanceStatus(InstanceStatus.UP);
        EurekaClient client = DiscoveryManager.getInstance().getEurekaClient();

        //获取从Eureka获取的全部的应用列表
        Applications apps = client.getApplications();
        //根据应用的名称获取已经可用的应用对象,可能是注册了多个。
        Application app=apps.getRegisteredApplications(serviceName);
        String reqUrl=null;
        if(app!=null)
        {       
            List<InstanceInfo> instances = app.getInstances();      
            if(instances.size()>0)
            {
                //获取其中一个应用实例,这里可以添加路由算法
                InstanceInfo instance = instances.get(0);               
                //获取公开的地址和端口
                reqUrl="http://"+instance.getIPAddr()+":"+instance.getPort();                       
            }
        }
        System.out.println("输出URL="+reqUrl);
    }
}
  • eureka-client.properties
eureka.region=default
eureka.name=sampleEurekaClient
eureka.preferSameZone=true
eureka.shouldUseDns=false
eureka.serviceUrl.default=http://localhost:1111/eureka/

可以发现这条日志

DEBUG org.apache.http.wire - >> “GET /eureka/apps/ HTTP/1.1[\r][\n]”

也可以发现类似

输出URL=http://...:3333

获得了Application 的Instance的URL,就可以发送REST请求了。OK基于这思路开始我们的实现过程。

实现阶段

包括了5个不同的maven工程。

1:MyRibbon 
2:MyRibbonInterface 
3:MyRibbonService 
4:EurekaServer 
5:MyRibbonClient

MyRibbon这个工程是我们实现的公共API,提供了对SpringCloud引用的调用。 
MyRibbonInterface这个工程师定义了服务接口API,面向接口进行编程,具体服务提供者(MyRibbonService)需要实现这些接口。 
MyRibbonService具体的服务实现,注册在Eureka,为客户端提供调用。 
EurekaServer一个Eureka的服务器 
MyRibbonClient这个应用就是我们测试的客户端应用。

MyRibbon工程

pom.xml

<modelVersion>4.0.0</modelVersion>
<groupId>org.lipengbin.spring</groupId>
<artifactId>MyRibbon</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.5.RELEASE</version>
        <relativePath />
</parent>
<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mortbay.jetty</groupId>
            <artifactId>jetty</artifactId>
            <version>6.1.26</version>
        </dependency>
</dependencies>
<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Brixton.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
</dependencyManagement>

定义两个注解 
MyService,MyMethod

MyService.java

package org.lipengbin.myribbon.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 定义接口服务的时候,需要指定这个接口服务对应在Eureka上对应的服务名称。
 * 需要根据这个服务的名称获取对应的Application。
 * @author Administrator
 *
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyService
{
    String value() default "";

}

MyMethod.java

package org.lipengbin.myribbon.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 在服务接口的方法上使用。
 * 主要是为了拼装URL和参数
 * 
 * http://ip:port/MyMethod?a=10&b=20
 * 
 * @author Administrator
 *
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyMethod
{
    String value() default "";
}

MyEurekaClient.java

package org.lipengbin.myribbon.eureka;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.client.RestTemplate;

import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.MyDataCenterInstanceConfig;
import com.netflix.appinfo.InstanceInfo.InstanceStatus;
import com.netflix.discovery.DefaultEurekaClientConfig;
import com.netflix.discovery.DiscoveryManager;
import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.shared.Application;
import com.netflix.discovery.shared.Applications;

/**
 * 同EurekaServer建立连接
 * 负责定时更新
 * 负责获取指定的Service
 * 外部不需要调用这个类
 * 这个类是个单例
 * @author Administrator
 *
 */
@SuppressWarnings("deprecation")
class MyEurekaClient
{
    private static final Logger logger = LoggerFactory.getLogger(MyEurekaClient.class);

    private EurekaClient client;

    @Autowired
    private  RestTemplate restTemplate;

    protected MyEurekaClient()
    {       
        if(restTemplate==null)
        {
            restTemplate=new RestTemplate();
        }
        init();
    }

    protected void init()
    {
        DiscoveryManager.getInstance().initComponent(new MyDataCenterInstanceConfig(), new DefaultEurekaClientConfig());
        ApplicationInfoManager.getInstance().setInstanceStatus(InstanceStatus.UP);
        client = DiscoveryManager.getInstance().getEurekaClient();
    }

    /**
     * 根据Service名称和请求的,获取返回内容!
     * @param serviceName
     * @param url
     * @return
     */
    public <T> T request(String serviceName,String url,Class<T> returnClaz)
    {
        Applications apps = client.getApplications();
        Application app=apps.getRegisteredApplications(serviceName);
        if(app!=null)
        {
            List<InstanceInfo> instances = app.getInstances();      
            if(instances.size()>0)
            {
                try
                {
                    InstanceInfo instance = instances.get(0);               
                    String reqUrl="http://"+instance.getIPAddr()+":"+instance.getPort()+"/"+url;                
                    return restTemplate.getForEntity(reqUrl, returnClaz).getBody();
                }
                catch(Exception e)
                {
                    logger.error("request is error。["+serviceName+"]",e);
                    return null;
                }
            }
            else
            {
                logger.error("Application instance not exist。["+serviceName+"]");
                return null;
            }
        }
        else
        {
            logger.error("Target Application not exist。["+serviceName+"]");
            return null;
        }

    }
}

MyEurekaHandler.java

package org.lipengbin.myribbon.eureka;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;

import org.lipengbin.myribbon.annotation.MyMethod;
import org.lipengbin.myribbon.annotation.MyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;


/**
 * 利用动态代理封装了接口的远程调用。
 * @author Administrator
 *
 */
public class MyEurekaHandler implements InvocationHandler
{

    private static final Logger logger = LoggerFactory.getLogger(MyEurekaHandler.class);

    /**
     * 代理的目标接口类
     */
    private Class<?> target;

    /**
     * Eureka中定义的Service的名称
     */
    private String serviceName;

    private MyEurekaClient client;

    MyEurekaHandler(MyEurekaClient client)
    {
        this.client=client;
    }

    @SuppressWarnings("unchecked")
    public <T> T create(Class<T> target)
    {
        this.target=target;
        MyService s=target.getAnnotation(MyService.class);
        if(s!=null)
        {
            serviceName=s.value().toUpperCase();
            return (T)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class<?>[]{target}, this);
        }
        else
        {
            logger.error(target.getName()+",没有定义 @MyService!");
            return null;
        }               
    }

    /**
     * 获取服务的名称
     * @return
     */
    public String getService()
    {
        return this.serviceName;
    }

    /**
     * 函数的实现内容,可以使用用其他的方式实现,例如通信等。
     * 
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
    {       
        MyMethod m=method.getAnnotation(MyMethod.class);
        Class<?> returnClaz=method.getReturnType();
        if(m==null)
        {
            logger.error(target.getName()+"."+method.getName()+",没有定义 @MyMethod!");
            return null;
        }
        else
        {
            //拼接URL
            StringBuilder builder=new StringBuilder(m.value());

            String url=parseBySpring(method, args);
            if(url!=null)
            {
                builder.append("?");
                builder.append(url);
            }

            return client.request(serviceName, builder.toString(), returnClaz);
        }
    }
    /**
     * Java8是可以通过类文件获取参数的名称,但是需要在编译的时候进行参数设置。
     * @param method
     * @param args
     * @return
     */
    protected String parseByJava8(Method method, Object[] args)
    {
        StringBuilder builder=new StringBuilder();
        Parameter[] params=method.getParameters();          
        int length=params.length;           
        for(int i=0;i<length;i++)
        {               
            Parameter p=params[i];
            Object value=args[i];
            if(i>0)
            {
                builder.append("&");
            }               

            builder.append(p.getName());
            builder.append("=");
            builder.append(value);
        }
        return builder.toString();
    }

    /**
     * 
     * @param method
     * @param args
     * @return
     */
    protected String parseBySpring(Method method, Object[] args)
    {
        StringBuilder builder=new StringBuilder();
        //以下这种获取参数名称的解析,需要在编译的时候设置一下。见图
        ParameterNameDiscoverer parameterNameDiscoverer =new DefaultParameterNameDiscoverer();
        // new LocalVariableTableParameterNameDiscoverer(); 这个不知道为什么不好使?ASM5以上。设置都是OK的 
        try 
        {  
            System.out.println(method.getName());

            String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
            for(int i=0;i<args.length;i++)
            {
                if(i>0)
                {
                    builder.append("&");
                }               

                builder.append(parameterNames[i]);
                builder.append("=");
                builder.append(args[i]);
            }
            return builder.toString();

        }
        catch(Exception e)
        {
            e.printStackTrace();
            return null;
        }
    }
}

这里需要说一下两个参数Discoverer。 
DefaultParameterNameDiscoverer:这个使用的Java8新提供的Parameter获取参数名称。 
这种方式和parseByJava8基本上是一样的。 
需要在Eclipse中进行设置。

这里写图片描述

LocalVariableTableParameterNameDiscoverer:这个是使用ASM获取参数名称。 
需要在Eclipse进行设置。 
这里写图片描述

MyEurekaFactory.java 
这个类是为外界提供调用的,使用方式很简单,后面会写一个完整的demo例子.

package org.lipengbin.myribbon.eureka;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * 这个工厂类管理了当前应用需要调用或者已经调用的接口实例。
 * 因为通过动态代理对服务进行了封装。
 * 因此对于一个应用来讲,是需要对服务实例进行管理的,否则每次都会进行创建。
 * 
 * @author Administrator
 *
 */

public class MyEurekaFactory
{
    private static final Logger logger = LoggerFactory.getLogger(MyEurekaFactory.class);
    /**
     * 当前应用对应的服务Map
     */
    public Map<Class<?>,Object> serviceMap=new HashMap<>();

    private static MyEurekaFactory factory;

    private static ReentrantLock lock=new ReentrantLock();

    private MyEurekaClient client;

    /**
     * 这个是当前的启动入口
     */
    private MyEurekaFactory()
    {
        client=new MyEurekaClient();
    }
    /**
     * 单例模式创建对象,当然可以通过注解的形式进行创建
     * @return
     */
    public static MyEurekaFactory gi()
    {
        if(factory==null)
        {
            lock.lock();
            if(factory==null)
            {
                factory=new MyEurekaFactory();
            }
            lock.unlock();
        }
        return factory;
    }

    @SuppressWarnings("unchecked")
    public <T> T createService(Class<T> serviceInterface)
    {
        if(serviceMap.containsKey(serviceInterface))
        {
            logger.debug("Service存在" +serviceInterface);
            return (T)serviceMap.get(serviceInterface);
        }
        else
        {
            logger.debug("Service不存在" +serviceInterface);
            return add(serviceInterface);
        }
    }   
    /**
     * 此处未做同步,因为创建多个实例会被覆盖,不会出现问题!
     * 只会影响首次的创建效率
     * @param serviceInterface
     * @return
     */
    private <T> T add(Class<T> serviceInterface)
    {
        MyEurekaHandler handler=new MyEurekaHandler(client);
        T t=handler.create(serviceInterface);
        serviceMap.put(serviceInterface, t);
        return t;
    }
}

MyRibbonInterface工程

pom.xml

 <modelVersion>4.0.0</modelVersion>
  <groupId>org.lipengbin.spring</groupId>
  <artifactId>MyRibbonInterface</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <dependencies>
    <dependency>
        <groupId>org.lipengbin.spring</groupId>
        <artifactId>MyRibbon</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
  </dependencies>

MyValue.java 
简单的返回对象

package org.lipengbin.test;

public class MyValue
{
    private String name;

    private int value;

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    public int getValue()
    {
        return value;
    }

    public void setValue(int value)
    {
        this.value = value;
    }
}

定义的服务接口

package org.lipengbin.test;

import org.lipengbin.myribbon.annotation.MyMethod;
import org.lipengbin.myribbon.annotation.MyService;

@MyService("TestService")
public interface TestService
{
    @MyMethod("execute")
    public MyValue execute(String name,Integer value);
}
  • 1

MyRibbonService工程

pom.xml

<modelVersion>4.0.0</modelVersion>
<groupId>org.lipengbin.spring</groupId>
<artifactId>MyRibbonService</artifactId>
<version>0.0.1-SNAPSHOT</version>

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.5.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
                <groupId>org.lipengbin.spring</groupId>
                <artifactId>MyRibbon</artifactId>
                <version>0.0.1-SNAPSHOT</version>
                <type>jar</type>
            </dependency>
            <dependency>
                <groupId>org.lipengbin.spring</groupId>
                <artifactId>MyRibbonInterface</artifactId>
                <version>0.0.1-SNAPSHOT</version>
                <type>jar</type>
            </dependency>
</dependencies>
<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Brixton.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
</dependencyManagement>

TestServiceImpl.java

package org.lipengbin.spring.test;

import org.lipengbin.test.MyValue;
import org.lipengbin.test.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestServiceImpl implements TestService
{
    @Autowired
    private DiscoveryClient client;

    @RequestMapping(value = "/execute" ,method = RequestMethod.GET) 
    public MyValue execute(@RequestParam  String name,@RequestParam  Integer value)
    {
        System.out.println("------------------>");
        ServiceInstance instance = client.getLocalServiceInstance();
        MyValue myvalue=new MyValue();
        myvalue.setName("name="+name);
        myvalue.setValue(100+value);
        return myvalue;
    }

}

TestServiceApplication.java

package org.lipengbin.spring.test;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class TestServiceApplication
{
    public static void main(String[] args)
    {
        new SpringApplicationBuilder(TestServiceApplication.class).web(true).run(args);
    }
}

application.properties

spring.application.name=TestService
server.port=3333
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/

EurekaServer工程

pom.xml

<modelVersion>4.0.0</modelVersion>
<groupId>org.lipengbin</groupId>
<artifactId>Eureka-Server</artifactId>
<version>0.0.1-SNAPSHOT</version>


<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.5.RELEASE</version>
        <relativePath />
</parent>
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mortbay.jetty</groupId>
            <artifactId>jetty</artifactId>
            <version>6.1.26</version>
        </dependency>
</dependencies>
<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Brixton.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
</dependencyManagement>

EurekaServer.java

package org.lipengbin;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaServer
{

    public static void main(String[] args)
    {
        new SpringApplicationBuilder(EurekaServer.class).web(true).run(args);
    }

}

MyRibbonClient工程

pom.xml

<modelVersion>4.0.0</modelVersion>
<groupId>org.lipengbin.spring</groupId>
<artifactId>MyRibbonClient</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
        <dependency>
            <groupId>org.lipengbin.spring</groupId>
            <artifactId>MyRibbon</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.lipengbin.spring</groupId>
            <artifactId>MyRibbonInterface</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
</dependencies>

eureka-client.properties

eureka.region=default
eureka.name=sampleEurekaClient
eureka.preferSameZone=true
eureka.shouldUseDns=false
eureka.serviceUrl.default=http://localhost:1111/eureka/

ClientMain.java

package org.lipengbin.spring.test.client;

import org.lipengbin.myribbon.eureka.MyEurekaFactory;
import org.lipengbin.test.MyValue;
import org.lipengbin.test.TestService;

public class ClientMain
{

    public static void main(String[] args)
    {       
        TestService service=MyEurekaFactory.gi().createService(TestService.class);
        MyValue value=service.execute("joy", 8);
        System.out.println(value.getName()+","+value.getValue());

    }
}

执行过程

1:将MyRibbon执行mvn install安装到本地仓库 
2:将MyRibbonInterface 执行mvn install安装到本地仓库 
3:关闭MyRibbon,和MyRibbonInterface,强制让其他工程引入jar包。 
4:启动EurekaServer 
5:启动MyRibbonService 
6:运行客户端MyRibbonClient

源码github的地址:(还没有传上去)

利用MyRibbon,你的非SpringCloud工程就可以访问SpringCloud工程了。 
这个工程花了两天完成,有很多需要优化的地方,后续会不断完善。 
有不足的地方,欢迎指正。

部分参考

http://bbs.springcloud.cn/ 
http://blog.didispace.com/ 
http://blog.csdn.net/wwwwenl/article/details/53427039 
http://blog.csdn.net/wwwihpccn/article/details/30496089 
http://m.blog.csdn.net/article/details?id=51314001 
https://github.com/Netflix/eureka/wiki/Understanding-eureka-client-server-communication

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值