嵌入式Jruby

0.嵌入式Jruby

能够在Ruby中使用Java是JRuby最广为人知的特性,那么,反过来在Java中使用Ruby也同样是允许的,你可以在Java中执行一个完整的Ruby脚本、或者单独调用其中的一个Ruby方法,甚至使用Ruby实现Java接口。想要做到这一点,可以通过多种不同的方式。我们把这些技术统称作为“嵌入式”技术。本文内容将介绍如何在Java工程中嵌入JRuby。

1.JRuby Embed ( 原称 Red Bridge )

Jruby一直以来就拥有一套 私有的 嵌入式API(原始API)。这套API与运行时内部紧密地耦合,在Jruby快速进化的过程中,这套API同时也频繁的发生变更。从1.4版本以后,Jruby提供了更加稳定的公共API,最初被称为 Reb bridge ,现如今称为 JRuby Embed。虽然,旧的原始私有API在遗留程序中可能还能继续工作,但是,对于新项目而言,强烈推荐使用新的公开API。不过,需要注意的是,使用 JRuby Embed 尽管可以完全胜任“ruby脚本执行”、“在Java与Ruby之间共享变量”之类的工作,但是如果存在更高级别的需求,比如在java里控制创建Jruby实例,那么就只能求助于最原始的API了。

1.1 Features of Red Bridge

Red Bridge 可以从以下两个层面进行使用:

  • 底层的Embed Core
  • 基于Embed Core提供的JSR223和BSF(Bean Scripting Framework)实现

Embed Core是Jruby特有的,可以充分发挥JRuby的各种能力。JSR223与BSF是针对各种不同脚本语言的通用接口。

上述API形式该如何选择呢?对于只是用到Ruby脚本的项目,推荐使用Embed Core。原因如下:

  1. 使用Embed Core你可以在一个JVM中创建多个Ruby环境,并且分别进行单独的配置。JSR223和BSF下,你只能通过System属性进行全局的设置。
  2. Embed Core提供了许多简化便捷操作,比如从java.io.InputStream中加载脚本,或者从Ruby代码中返回一个Java友好的对象。这些功能,使得你可以略过许多繁琐的模板式的设置。

而对于需要处理多种类型脚本语言的项目,JSR223会更合适。虽然它是中立于语言的,但是JRuby关于JSR223的实现允许你去设置一些Ruby特有的选项。尤其是,你可以控制脚本引擎的线程模型、本地变量的生命周期、编译模式以及行号的显示方式。

突出重点地说,Red Bridge的优势主要在于以下两点:

1.1.1 Context Type

上下文类型(单例[singleton]、线程安全[thread-safe]、单线程[single-threaded]、并发[concurrent])决定了JRuby如何维护它内部的Ruby状态。关于以上类型的错误选择,将会导致不可预知的严重错误。

简单的应用场景下你可能只需要一个Ruby runtime,而且你也不需要在多线程环境下来访问它,那么这时应该选用 singleton 类型。如果你要在多线程环境下运行Ruby脚本,你应该选择 thread-safe 类型。作为新引入的类型, concurrent 类型是一种混合了 singletonthread-safe的类型。在 concurrent 类型下,将会拥有一个Ruby runtime单例和多个本地的variable map。

1.1.2 Variables

“变量共享”是Red bridge提供的附加功能。借助于此,你可以为Ruby设置一些全局变量,然后再运行脚本之后,再通过全局变量来获取执行结果。

其实,通用的脚本API也可以通过 blunt instrument 来功效全局变量。不过,RedBridge提供了更强的功能,不仅限于共享全局变量,同样也可以共享本地变量与实例变量。JRuby使用了称为 variable map 的机制来追踪Java与Ruby之间的变量名值对。我们可以通过“变量选项”(various options)来控制variable map如何在不同的调用之间进行复用。

同样,你也可以通过方法参数传递数据,然后获取返回值。这些数据值在java端都是普通java对象。

自JRuby 1.4.0RC1版本以后,JRuby Embed (Red Bridge)的二进制版本被包含在JRuby的官方下载中。在1.5版本之后,Red Bridge源码被包含在JRuby core中。

JSR223与BSF是稳定的API。Embed Core API在某种程度上是稳定的,但在每个release版本之间不排除会有细小的变更。# JRuby Embed (originally known as Red Bridge)

2.Hello World 样例

不同形式API所依赖的JAR包。

  • Embed Core
    • jruby-complete.jar or jruby.jar
  • JSR223
    • ruby-complete.jar or jruby.jar
    • script-api.jar (if you are using JDK1.5)
  • BSF
    • jruby-complete.jar or jruby.jar
    • bsf.jar
    • commons-logging-[version].jar

2.1 Embed Core

package vanilla;

import org.jruby.embed.ScriptingContainer;

public class HelloWorld {

    private HelloWorld() {
        ScriptingContainer container = new ScriptingContainer();
        container.runScriptlet("puts 'Hello World!'");
    }

    public static void main(String[] args) {
        new HelloWorld();
    }
}

2.2 JSR223

package redbridge;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class Jsr223HelloWorld {

    private Jsr223HelloWorld() throws ScriptException {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("jruby");
        engine.eval("puts 'Hello World!'");
    }

    public static void main(String[] args) throws ScriptException {
        new Jsr223HelloWorld();
    }
}

2.3 BSF

package azuki;

import org.apache.bsf.BSFException;
import org.apache.bsf.BSFManager;

public class BsfHelloWorld {
    private BsfHelloWorld() throws BSFException {
        BSFManager.registerScriptingEngine("jruby", "org.jruby.embed.bsf.JRubyEngine", new String[] {"rb"});
        BSFManager manager = new BSFManager();
        manager.exec("jruby", "<script>", 0, 0, "puts 'Hello World!'");
    }

    public static void main(String[] args) throws BSFException {
        new BsfHelloWorld();
    }

}

3.配置

使用 JRuby Embed (RedBridge) 可以允许进行一些环境和执行方面的配置,大多数情况下默认的配置就是够用的。

使用Embed Core,各种配置主要是通过构造函数或者方法参数来设置;而对于JSR223,则主要是通过系统属性(System properties)来进行配置。

RedBridge支持对于每个“container”或者“evaluation”级别上的设置。当使用系统属性来进行“container”级别的配置时,必须在初始化“container”之前进行设置。当使用Embed Core提供的方法来进行设置时,那么必须在执行“container”的put/parse/runScriptlet方法之前进行设置,这些方法会基于container的设置来初始化Ruby runtime。一旦“Ruby runtime”初始化完毕,那么container的配置将不会再被引用。“evaluation”级别的配置应该在运行ruby代码之前进行设定。

  • Per-container configurations:
    • JRuby Home
    • Classpath
    • Context Instance Type
    • Local Variable Behavior
    • Compile Mode
    • Ruby Version
  • Per-evaluation configurations:
    • Disabling Sharing Variables
    • Line Number

3.1 JRuby Home

Scope: Per Container System Propety Name: jruby.home

JRuby Home 决定了JRuby到哪里去寻找内建库。JRuby Embed确定该路径的内部规则为:

  • 检查JRUBY_HOMEOS环境变量
  • 检查jruby.homeJVM系统属性
  • 检查ruby-complete.jar中的设置

使用OS环境变量或者JVM系统属性都将起到全局的效果。而使用Embed Core则可以对每个脚本容器实例进行不同的配置:

ScriptingContainer container = new ScriptingContainer();
// JRuby 1.5.x
container.setHomeDirectory("/Users/yoko/Tools/jruby-1.5.6");
// JRuby 1.4.0
//container.getProvider().getRubyInstanceConfig().setJRubyHome("/Users/yoko/Tools/jruby-1.3.1");

3.2 Class Path (Load Path)

Scope: Per Container System Propety Name: org.jruby.embed.class.path (or java.class.path)

当加载Ruby脚本或者Java Class时,将会用到Classpaths配置。Embed Core 和 BSF都有设置classpath的方法,而JSR223没有相关方法只能通过系统属性来进行设定。org.jruby.embed.class.path 或者 java.class.path都可以用来进行此项配置。以下是一些关于优先级顺序等发面的详细说明。

The org.jruby.embed.class.path system property is avaiable to use in Embed Core and BSF, too. In case of Embed Core and JSR223, a value assigned to org.jruby.embed.class.path is looked up first, then java.class.path. This means that only org.jruby.embed.class.path is used if exists. As for BSF, after java.class.path is looked up, org.jruby.embed.class.path is added to if exists. The format of the paths is the same as Java's class path syntax, :(colon) separated path string on Unix and OS X or ;(semi-colon) separated one on Windows. Be sure to set classpaths before you instantiate javax.script.ScriptEngineManager or register engine to org.apache.bsf.BSFManager.

另外还有一篇专门关于ClasspathAndLoadPath的主题。

Samples below run testMath.rb, which is included in JRuby’s source archive. The script, testMath.rb, needs minirunit.rb, which should be loaded from the classpath.

下方代码示例的场景为:运行testMath.rb,且该脚本中引用了minirunit.rb脚本。

为了能够在运行testMath.rb时正确地从classpath(loadpath)中加载minirunit.rb,不同的API代码样例如下。

3.2.1 Core

package vanilla;

import java.util.ArrayList;
import java.util.List;
import org.jruby.embed.PathType;
import org.jruby.embed.ScriptingContainer;

public class LoadPathSample {
    private final static String jrubyhome = "/Users/yoko/Tools/jruby-1.3.1";
    private final String filename = jrubyhome + "/test/testMath.rb";

    private LoadPathSample() {
        ScriptingContainer container = new ScriptingContainer();
        List<String> loadPaths = new ArrayList();
        loadPaths.add(jrubyhome);
        // JRuby 1.5.x
        container.setLoadPaths(loadPaths);
        // JRuby 1.4.0
        //container.getProvider().setLoadPaths(loadPaths);
        container.runScriptlet(PathType.ABSOLUTE, filename);
    }

    public static void main(String[] args) {
        new LoadPathSample();
    }
}

3.3.2 JSR223

package redbridge;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.Reader;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class Jsr223LoadPathSample {
    private final static String jrubyhome = "/Users/yoko/Tools/jruby-1.3.1";
    private final String filename = jrubyhome + "/test/testMath.rb";

    private Jsr223LoadPathSample() throws ScriptException, FileNotFoundException {
        System.setProperty("org.jruby.embed.class.path", jrubyhome);
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("jruby");
        Reader reader = new FileReader(filename);
        engine.eval(reader);
    }

    public static void main(String[] args) throws ScriptException, FileNotFoundException {
        new Jsr223LoadPathSample();
    }
}

3.3.3 BSF

package azuki;

import java.io.FileNotFoundException;
import org.apache.bsf.BSFException;
import org.apache.bsf.BSFManager;
import org.jruby.embed.PathType;

public class BsfLoadPathSample {
    private final static String jrubyhome = "/Users/yoko/Tools/jruby-1.3.1";
    private final String filename = jrubyhome + "/test/testMath.rb";
    
    private BsfLoadPathSample() throws BSFException, FileNotFoundException {
        BSFManager.registerScriptingEngine("jruby", "org.jruby.embed.bsf.JRubyEngine", new String[] {"rb"});
        BSFManager manager = new BSFManager();
        manager.setClassPath(jrubyhome);
        manager.exec("jruby", filename, 0, 0, PathType.ABSOLUTE);
    }

    public static void main(String[] args) throws BSFException, FileNotFoundException {
        new BsfLoadPathSample();
    }
}

3.4 Context Instance Type

Scope: Per Container Property Name: org.jruby.embed.localcontext.scope Value:

  • singleton(default in JRuby 1.6.x/1.5.x/1.4.0 and JRuby trunk; rev. 9e557a2 and later)
  • theadsafe (default in JRuby 1.4.0RC1, RC2, RC3)
  • singlethread
  • concurrent (since JRuby 1.6.0)

本地上下文中维护了以下内容:

  • Ruby runtime
  • Java与Ruby共享的“键值对”变量
  • 缺省 I/O streams (reader/writer/error writer)
  • 其它属性

3.4.1 Singleton

该模型应用了“单例模式”。无论创建了多少个ScriptingConatiners (or ScriptEngines (JSR223)),都只会有一个Ruby runtime和variable map,在该模式下线程安全由用户来负责。

+------------------------------------------------------------+
|                       Variable Map                         |
+------------------------------------------------------------+
+------------------------------------------------------------+
|                       Ruby runtime                         |
+------------------------------------------------------------+
+------------------+ +------------------+ +------------------+
|ScriptingContainer| |ScriptingContainer| |ScriptingContainer|
+------------------+ +------------------+ +------------------+
+------------------------------------------------------------+
|                         JVM                                |
+------------------------------------------------------------+

                Singleton Local Context Type
          (Thread safety is users' responsibility)

Core

ScriptingContainer instance = new ScriptingContainer(LocalContextScope.SINGLETON);

JSR223/BSF

System.setProperty("org.jruby.embed.localcontext.scope", "singleton");

3.4.2 ThreadSafe

在该模式下,脚本的parse(解析)和evaluation(执行)都可以在一个多线程的环境下安全执行。一个ScriptingContainer将会创建线程本地的Ruby runtimes 和 variable maps。

+------------------+ +------------------+ +------------------+
|   Variable Map   | |   Variable Map   | |   Variable Map   |
+------------------+ +------------------+ +------------------+
+------------------+ +------------------+ +------------------+
|   Ruby runtime   | |   Ruby runtime   | |   Ruby runtime   |
+------------------+ +------------------+ +------------------+
+------------------------------------------------------------+
|                     ScriptingContainer                     |
+------------------------------------------------------------+
+------------------+ +------------------+ +------------------+
|   Java Thread    | |   Java Thread    | |   Java Thread    |
+------------------+ +------------------+ +------------------+
+------------------------------------------------------------+
|                         JVM                                |
+------------------------------------------------------------+

                Threadsafe Local Context Type
       (Per Thread Isolated Variable Map and Runtime)

Core

ScriptingContainer instance = new ScriptingContainer(LocalContextScope.THREADSAFE);

JSR223/BSF

System.setProperty("org.jruby.embed.localcontext.scope", "threadsafe");

3.4.3 Concurrent

该模式中隔离了variable maps,但是共享了Ruby runtime。一个ScriptingContainer将会创建一个runtime和多个线程本地的variable maps。这种情况下全局变量和常量都不是线程安全的,但是跟java端绑定的变量或者常量都是线程本地的。

+------------------+ +------------------+ +------------------+
|   Variable Map   | |   Variable Map   | |   Variable Map   |
+------------------+ +------------------+ +------------------+
+------------------------------------------------------------+
|                        Ruby runtime                        |
+------------------------------------------------------------+
+------------------------------------------------------------+
|                     ScriptingContainer                     |
+------------------------------------------------------------+
+------------------+ +------------------+ +------------------+
|   Java Thread    | |   Java Thread    | |   Java Thread    |
+------------------+ +------------------+ +------------------+
+------------------------------------------------------------+
|                         JVM                                |
+------------------------------------------------------------+

                Concurrent Local Context Type
    (Per Thread Isolated Variable Map and Singleton Runtime)

Core

ScriptingContainer instance = new ScriptingContainer(LocalContextScope.CONCURRENT);

JSR223/BSF

System.setProperty("org.jruby.embed.localcontext.scope", "concurrent");

3.4.4 SingleThread

这个模型真的没什么用。

This model pretends as if there is only one thread in the world, and does not mind race condition at all. This naive model is used in many other JSR223 ScriptEngines. Users are responsible to thread safety.

+------------------+ +------------------+ +------------------+
|   Variable Map   | |   Variable Map   | |   Variable Map   |
+------------------+ +------------------+ +------------------+
+------------------+ +------------------+ +------------------+
|   Ruby runtime   | |   Ruby runtime   | |   Ruby runtime   |
+------------------+ +------------------+ +------------------+
+------------------+ +------------------+ +------------------+
|ScriptingContainer| |ScriptingContainer| |ScriptingContainer|
+------------------+ +------------------+ +------------------+
+------------------------------------------------------------+
|                         JVM                                |
+------------------------------------------------------------+

                Singlethread Local Context Type
          (Thread safety is users' responsibility)

Core

ScriptingContainer instance = new ScriptingContainer(LocalContextScope.SINGLETHREAD);

JSR223/BSF

System.setProperty("org.jruby.embed.localcontext.scope", "singlethread");

3.5 Local Variable Behavior Options

Scope: Per Container Property Name: org.jruby.embed.localvariable.behavior Value:

  • transient (default for core)
  • persistent
  • global (default for JSR223)
  • bsf (for BSF)

JRuby Embed 可以在java与ruby之间共享ruby的本地变量、实例变量、全局变量和常量。

3.5.1 Transient Local Variable Behavior

Embed Core的缺省设置。

  • 本地变量将在执行后被销毁,而不会再多次执行之间传递
  • 实例变量、全局变量和常量将会跟随ruby runtime共存。

以上基本遵从了ruby原本的语义。

Core

ScriptingContainer instance = new ScriptingContainer();

或者

ScriptingContainer instance = new ScriptingContainer(LocalVariableBehavior.TRANSIENT)

或者

ScriptingContainer instance = new ScriptingContainer(LocalContextScope.SINGLETHREAD, LocalVariableBehavior.TRANSIENT);

JSR223

System.setProperty("org.jruby.embed.localvariable.behavior", "transient");
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("jruby");

BSF

BSF can choose only BSF type.

3.5.2 Persistent Local Variable Behavior

当选择该种类型时,JRuby Embed会在多次执行中维持本地变量。

Core

ScriptingContainer instance = new ScriptingContainer(LocalVariableBehavior.PERSISTENT);

或者

ScriptingContainer instance = new ScriptingContainer(LocalContextScope.SINGLETHREAD, LocalVariableBehavior

JSR223

System.setProperty("org.jruby.embed.localvariable.behavior", "persistent");
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("jruby");

BSF

BSF can choose only BSF type.

3.5.3 Global Local Variable Behavior

Default for JSR223. This behavior might be convenient to users who have used JSR223 reference implementation released at scripging.dev.java.net and don't want change any code at all. With names like Ruby's local variable name, variables are mapped to Ruby's global variables. Only global variables can be shared between Ruby and Java, when this behavior is chosen. The values of global variables of this type are not kept over the evaluations.

Core

ScriptingContainer instance = new ScriptingContainer(LocalVariableBehavior.GLOBAL);

JSR223

System.setProperty("org.jruby.embed.localvariable.behavior", "global");
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("jruby");

BSF

BSF can choose only BSF type.

3.5.3 BSF Local Variable Behavior

Default for BSF. Local and global variables are available to share between Java and Ruby. Variable names doesn’t start with “$” no matter what the variable types are. Since BSF has a method defined for sharing local variables, it doesn’t confuse. However, core and JSR223 will confuse variables, so don’t use this type for them.

BSF

BSFManager.registerScriptingEngine("jruby", "org.jruby.embed.bsf.JRubyEngine", new String[] {"rb"});
BSFManager manager = new BSFManager();

或者

System.setProperty("org.jruby.embed.localvariable.behavior", "bsf");
BSFManager.registerScriptingEngine("jruby", "org.jruby.embed.bsf.JRubyEngine", new String[] {"rb"});
BSFManager manager = new BSFManager();

3.6 Disabling Sharing Variables

Scope: Per Evaluation Systeom Property Name: org.jruby.embed.sharing.variables Value: true (default), or false

该选项控制变量共享机制是否启用,默认是启用状态。如果置为关闭,那么将会带来些许性能方面的改善。

Core

container.setAttribute(AttributeName.SHARING_VARIABLES, false);

JSR223

System.setProperty("org.jruby.embed.sharing.variables", "false");

3.7 CompileMode

Scope: Per Container Systeme Property Name: org.jruby.embed.compilemode Value: off (default), jit, or force

JRuby has jit and force options to compile Ruby scripts. This configuration provides users a way of setting a compile mode. When jit or force is specified, only a global variable can be shared between Ruby and Java.

This option is not for pre-compiled Ruby scripts. Ruby2java http://kenai.com/projects/ruby2java generated Java classes are not executable on current JRuby Embed.

Core

ScriptingContainer container = new ScriptingContainer();
container.setCompileMode(CompileMode.JIT);

JSR223/BSF

System.setProperty("org.jruby.embed.compilemode", "jit");

3.8 Ruby Version

Scope: Per Container Systemo Property Name: org.jruby.embed.compat.version Value: jruby19, JRuby1_9, ruby1.9….matches j?ruby1[\\._]?9 (case insensitive) 默认版本1.8。如果要用别的版本,那么需要进行设置。如果系统变量值或者注册的名称匹配正则表达式 [jJ]?(r|R)(u|U)(b|B)(y|Y)1[\\\\._]?9,那么将会使用1.9版本来执行ruby脚本。

Core

ScriptingContainer container = new ScriptingContainer();
// JRuby 1.5.x
container.setCompatVersion(CompatVersion.RUBY1_9);
// JRuby 1.4.0
//container.getProvider().getRubyInstanceConfig().setCompatVersion(CompatVersion.RUBY1_9);

JSR223

System.setProperty("org.jruby.embed.compat.version", "JRuby1.9");
JRubyScriptEngineManager manager = new JRubyScriptEngineManager();
JRubyEngine engine = (JRubyEngine) manager.getEngineByName("jruby");

BSF

BSFManager.registerScriptingEngine("jruby19", "org.jruby.embed.bsf.JRubyEngine", new String[] {"rb"});
BSFManager manager = new BSFManager();
manager.exec("jruby19", "ruby/block-param-scope.rb", 0, 0, PathType.CLASSPATH);

3.9 Line Number

Scope: Per Evaluation Context Attribute Name: org.jruby.embed.linenumber Value: 1, 2, 3,,,, (integer)

Embed Core 和 BSF 可以允许通过方法参数来设置显示行号,以追踪解析错误。JSR223的此项设置必须通过上下文属性来进行。

该行号的作用在于设定所解析执行脚本的起始行号。

core

private final String script =
            "puts \"Hello World.\"\n" +
            "puts \"Error is here.";
ScriptingContainer container = new ScriptingContainer();
EvalUnit unit = container.parse(script, 1);
Object ret = unit.run();

JSR223

import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import org.jruby.embed.jsr223.JRubyScriptEngineManager;

public class LineNumberSample {
    private final String script =
            "puts \"Hello World.\"\n" +
            "puts \"Error is here.";

    private LineNumberSample() throws ScriptException {
        JRubyScriptEngineManager manager = new JRubyScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("jruby");
        try {
            engine.eval(script);    // Since no line number is given, 0 is applied to.
        } catch (Exception e) {
            ;
        }
        try {
            engine.getContext().setAttribute("org.jruby.embed.linenumber", 1, ScriptContext.ENGINE_SCOPE);
            engine.eval(script);
        } catch (Exception e) {
            ;
        }
        try {
            engine.getContext().setAttribute("org.jruby.embed.linenumber", 2, ScriptContext.ENGINE_SCOPE);
            engine.eval(script);
        } catch (Exception e) {
            ;
        }
    }

    public static void main(String[] args)
            throws ScriptException {
        new LineNumberSample();
    }
}

BSF

private final String script =
            "puts \"Hello World.\"\n" +
            "puts \"Error is here.";
BSFManager.registerScriptingEngine("jruby", "org.jruby.embed.bsf.JRubyEngine", new String[] {"rb"});
BSFManager manager = new BSFManager();
manager.exec("jruby", “<script>”, 1, 0, script);

3.10 Other Ruby Runtime Configurations

关于Ruby runtime更多更细致的控制,可以通过Embed Core中 org.jruby.RubyInstanceConfig的更多的方法来进行设定。操作举例如下:

ScriptingContainer container = new ScriptingContainer();
RubyInstanceConfig config = container.getProvider().getRubyInstanceConfig();

# First, make the configuration changes you need.
config.someConfigMethod(...);

# Now, you can set variables or run Ruby code.
container.runScriptlet(...);

转载于:https://my.oschina.net/yumg/blog/466252

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值