Groovy-拾遗

Groovy拾遗

美妙的操作符

许多以前使用 C++ 的开发人员会怀念操作符重载,例如 +-。虽然它们很方便,但是被覆盖的操作符的多态实质会造成混淆,所以操作符重载在 Java 语言中被取消了。这个限制的好处是清晰:Java 开发人员不必猜想两个对象上的 + 是把它们加在一起还是把一个对象附加到另一个对象上。不好的地方则是丧失了一个有价值的简写形式。

现在,期望放任自由的 Groovy 把这个简写形式带回来!以下将介绍 Groovy 对操作符即时多态(也称为操作符重载)的支持。正如 C++ 开发人员会告诉您的,这个东西既方便又有趣,虽然必须小心谨慎才能接近。

算术类操作符

Groovy 支持以下算术类操作符的重载:

表 1. Groovy 的算术类操作符
操作符方法
a + ba.plus(b)
a - ba.minus(b)
a * ba.multiply(b)
a / ba.divide(b)
a++ or ++aa.next()
a-- or --aa.previous()
a << ba.leftShift(b)

您可能已经注意到 Groovy 中的 + 操作符已经在几个不同的领域重载了,特别是在用于集合的时候。

在表 1 中有一个可以重载的算术类操作符 <<,它恰好也为 Groovy 的集合重载。在集合的情况下,<< 覆盖后的作用像普通的 Java add() 方法一样,把值添加到集合的尾部(这与 Ruby 也很相似)。

数组类操作符

Groovy 支持重载标准的 Java 数组存取语法 [],如表 4 所示:

表 2. 数组操作符
操作符方法
a[b]a.getAt(b)
a[b] = ca.putAt(b, c)

可以看到,操作符的即时多态,或操作符重载,对于我们来说,如果小心使用和记录,会非常强大。但是,要当心不要滥用这个特性。如果决定覆盖一个操作符去做一些非常规的事情,请一定要清楚地记录下您的工作。对 Groovy 类进行改进,支持重载非常简单。小心应对并记录所做的工作,对于由此而来的方便的简写形式来说,代价非常公道。

Groovy在Spring中的简单使用

1. 首先 编写java的业务接口类

package com.springandgroovy;


public interface HelloWorldService {

	String sayHello();

}

2. 编写groovy类实现这个接口(注意:该文件名是HelloWorldServiceImpl.groovy)

package com.springandgroovy;

public class HelloWorldServiceImpl  implements HelloWorldService{

	String name;

	String sayHello(){

		return "Hello $name!!!. Welcome to Scripting in Groovy.";

	}

}

3.比较关键的是spring配置文件,在文件的头部需要lang的名字空间,以便识别 <lang:groovy ...

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:lang="http://www.springframework.org/schema/lang" xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/lang
            http://www.springframework.org/schema/lang/spring-lang-2.5.xsd">
  
  <!-- 设置默认的延时刷新时间 -->
  <lang:defaults refresh-check-delay="60000" />
  
  <lang:groovy id="helloWorldService" script-source="classpath:com/springandgroovy/HelloWorldServiceImpl.groovy">
    <lang:property name="name" value="meera"/>
  </lang:groovy>
</beans>

4. 还可以将HelloWorldServiceImpl写在spring的配置文件中,如下所示:(不提倡使用此方法)

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:lang="http://www.springframework.org/schema/lang" xsi:schemaLocation="http://www.springframework.org/schema/beans
             http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
             http://www.springframework.org/schema/lang
             http://www.springframework.org/schema/lang/spring-lang-2.5.xsd">
  <!--   
      <lang:defaults refresh-check-delay="60000" />

      <lang:groovy id="helloWorldService"
        script-source="classpath:com/springandgroovy/HelloWorldServiceImpl.groovy">
        <lang:property name="name" value="meera">
      </lang:groovy>
  -->
  
  <lang:groovy id="helloWorldService">
    <lang:inline-script>
             <![CDATA[
                package com.springandgroovy;

                public class HelloWorldServiceImpl  implements HelloWorldService{
                    String name;

                    String sayHello(){
                      return "Hello $name. Welcome to Scripting in Groovy.";
                    }
               }
           ]]>
       </lang:inline-script>
    <lang:property name="name" value="meera"></lang:property>
  </lang:groovy>
</beans>

给大文件计算SHA1哈希值

import java.security.MessageDigest

int KB = 1024
int MB = 1024*KB

File f = new File(args[0])
if (!f.exists() || !f.isFile()) {
  println "Invalid file $f provided"
  println "Usage: groovy sha1.groovy <file_to_hash>"
}

def messageDigest = MessageDigest.getInstance("SHA1")

long start = System.currentTimeMillis()

f.eachByte(MB) { byte[] buf, int bytesRead ->
  messageDigest.update(buf, 0, bytesRead);
}

def sha1Hex = new BigInteger(1, messageDigest.digest()).toString(16).padLeft( 40, '0' )
long delta = System.currentTimeMillis()-start

println "$sha1Hex took $delta ms to calculate"

ConfigSlurper

ConfigSlurper是Groovy中的一个实用程序类,用于编写属性文件,例如用于执行配置的脚本。 与常规Java属性文件不同,ConfigSlurper脚本支持本机Java类型,并且结构类似于树。 下面是如何使用ConfigSlurper脚本配置Log4j的示例:

log4j.appender.stdout = "org.apache.log4j.ConsoleAppender"
log4j.appender."stdout.layout"="org.apache.log4j.PatternLayout"
log4j.rootLogger="error,stdout"
log4j.logger.org.springframework="info,stdout"
log4j.additivity.org.springframework=false

要将其加载到可读配置中,您可以执行以下操作:

def config = new ConfigSlurper().parse(new File('myconfig.groovy').toURL())

assert "info,stdout" == config.log4j.logger.org.springframework
assert false == config.log4j.additivity.org.springframework

从上面的示例中可以看出,您可以使用点表示法导航配置,返回值是Java类型,如字符串和布尔 值。

转换为Java属性文件和从Java属性文件转换

您可以将ConfigSlurper配置转换为Java属性文件或从Java属性文件转换。 例如:

java.util.Properties props = // load from somewhere

def config = new ConfigSlurper().parse(props)

props = config.toProperties()

合并配置

您可以合并配置对象,这样如果您有多个配置文件并想要创建一个中央配置对象,您可以执行以下操作:

def config1 = new ConfigSlurper().parse(..)
def config2 = new ConfigSlurper().parse(..)

config1 = config1.merge(config2)

将配置序列化到磁盘

您可以将配置对象序列化到磁盘。 每个配置对象都实现了groovy.lang.Writable接口,允许您将配置写入任何java.io.Writer

def config = new ConfigSlurper().parse(..)

new File("..").withWriter { writer ->
     config.writeTo(writer)
}

特殊的“环境(environments)”配置

ConfigSlurper类具有除默认构造函数之外的特殊构造函数,该构造函数采用“environment”参数。 此特殊构造函数与称为环境的属性设置协同工作。 这允许属性文件中存在默认设置,该设置可以通过适当的环境闭包中的设置取代。 这允许多个相关配置存储在同一文件中。

Sample.groovy这个groovy属性文件:

sample {
  foo = "default_foo"
  bar = "default_bar"
}

environments {
  development {
    sample {
      foo = "dev_foo"
    }
  }
  test {
    sample {
      bar = "test_bar"
    }
  }
}

以下是演示此配置的演示代码:

def config = new ConfigSlurper("development").parse(new File('Sample.groovy').toURL())

assert config.sample.foo == "dev_foo"
assert config.sample.bar == "default_bar"

config = new ConfigSlurper("test").parse(new File('Sample.groovy').toURL())

assert config.sample.foo == "default_foo"
assert config.sample.bar == "test_bar"

注意:环境闭包不能直接解析。 不使用特殊环境构造函数,将忽略闭包。

转换SQL ResultXML

import groovy.sql.Sql
import groovy.xml.MarkupBuilder
def schema = "PROD"
def sql = Sql.newInstance("jdbc:oracle:thin:@hostname:1526:${schema}", "scott", "tiger", "oracle.jdbc.driver.OracleDriver")

/* Request */
def req = """
SELECT id,  name, givenname, unit FROM ${schema}.people
WHERE
in_unit=1
AND visible=0
"""

def out = new File('out.xml')
def writer = new FileWriter( out )
def xml = new MarkupBuilder( writer )

xml.agents {
    sql.eachRow( req as String  ) { row ->
        /* For each row output detail */
            xml.agent(id:row.id) {
                name( row.name )
                givenname( row.givenname )
                unit( row.unit )
            }
    }
}

输出:

<agents>                               <!-- xml.agents {                  -->
  <agent id='870872'>                  <!--    agent(id:row.id) {         -->
    <name>ABTI</name>                  <!--       name( row.nom )         -->
    <givenname>Jean</givenname>        <!--       givenname( row.prenom ) -->
    <unit>Sales</unit>                 <!--       unit( row.unite )       -->
  </agent>                             <!--    }                          -->
...
</agents>

HTTP

Making an HTTP GET Request

def page = new URL('http://www.aboutgroovy.com' ).text
  
new URL('http://www.aboutgroovy.com').eachLine { line ->
   println line
}
  
'http://www.aboutgroovy.com'.toURL().text
 
//Processing a Request Based on the HTTP Response Code
def url = new URL('http://www.aboutgroovy.com')
def connection = url.openConnection()
if(connection.responseCode == 200) {
   println connection.content.text
} else {
   println 'An error occurred:'
   println connection.responseCode
   println connection.responseMessage
}

// Get content of URL with parameters.
def url = "http://www.mrhaki.com/url.html".toURL()

// Simple Integer enhancement to make
// 10.seconds be 10 * 1000 ms.
Integer.metaClass.getSeconds = { ->
    delegate * 1000
}

def content = url.getText(connectTimeout: 10.seconds, readTimeout: 10.seconds,
                          useCaches: true, allowUserInteraction: false,
                          requestProperties: ['User-Agent': 'Groovy Sample Script'])
                          
url.newReader(connectTimeout: 10.seconds, useCaches: true).withReader { reader ->
    assert reader.readLine() == 'Simple test document'
}

Making an HTTP POST Request

def url = new URL('http://search.yahoo.com/search')
def connection = url.openConnection()

//switch the method to POST (GET is the default)
connection.setRequestMethod('POST')

//write the data
def queryString = 'n=20&vf=pdf&p=groovy+grails'
connection.doOutput = true
Writer writer = new OutputStreamWriter(connection.outputStream)
writer.write(queryString)
writer.flush()
writer.close()
connection.connect()

//print the results
println connection.content.text
//PUT 和 DELETE 操作形式上同上。

//RESTful POST Requests Using XML
def xml = """
<person id="100" >
  <firstname>Jane</firstname>
  <lastname>Doe</lastname>
  <address type="home" >
    <street>123 Main St</street>
    <city>Denver</city>
    <state>CO</state>
    <zip>80020</zip>
  </address>
</person>
 """
 ...
 connection.setRequestProperty('Content-Type', 'application/xml')
 ...
 writer.write(xml)
 ...

Padding Strings

Groovy使用几种填充方法扩展了String类。 这些方法允许我们定义String值必须占用的固定宽度。 如果String本身小于固定宽度,则用空格或我们定义的任何其他字符或字符串填充空格。 我们可以填充字符串的左侧或右侧或左右两侧,并将字符串放在中心。

当我们创建在控制台上运行的Groovy脚本并且我们想要格式化某些输出时,这些方法特别有用。

assert '   Groovy   ' == 'Groovy'.center(12)
assert 'Groovy      ' == "Groovy".padRight(12)
assert '      Groovy' == /Groovy/.padLeft(12)

assert '---Groovy---' == "Groovy".center(12, '-')
assert 'Groovy * * *' == "Groovy".padRight(12, ' *')
assert 'Groovy Groovy Groovy' == 'Groovy'.padLeft(20, 'Groovy ')

def createOutput = {
	def table = [
	// Page,    Response time, Size
	['page1.html',        200, 1201],
	['page2.html',         42, 8853],
	['page3.html',         98, 3432],
	['page4.html',        432, 9081]
	]

	def total = { data, index ->
		data.inject(0) { result, row -> result += row[index] }
	}
	def totalTime = total.curry(table, 1)
	def totalSize = total.curry(table, 2)

	def out = new StringBuffer()
	out << ' Summary '.center(15, "*") << '\n\n'
	out << 'Total pages:'.padRight(25)
	out << table.size().toString().padLeft(6) << '\n'
	out << 'Total response time (ms):'.padRight(25)
	out << totalTime().toString().padLeft(6) << '\n'
	out << 'Total size (KB):'.padRight(25)
	out << totalSize().toString().padLeft(6) << '\n\n'

	out << ' Details '.center(15, "*") << '\n\n'
	table.each {
		out << it[0].padRight(14)
		out << it[1].toString().padLeft(5)
		out << it[2].toString().padLeft(8)
		out << '\n'
	}
	out.toString()
}

assert '''\
*** Summary ***

Total pages:                  4
Total response time (ms):   772
Total size (KB):          22567

*** Details ***

page1.html      200    1201
page2.html       42    8853
page3.html       98    3432
page4.html      432    9081
''' == createOutput()

With

Groovy有一个with方法,我们可以用它来对方法调用和属性访问对象进行分组。 with方法接受闭包,闭包中的每个方法调用或属性访问都适用于对象(如果适用)。 该方法是Groovy对java.lang.Object类的扩展的一部分。 让我们看一个例子:

class Sample {
    String username
    String email
    List<String> labels = []
    def speakUp() { "I am $username" }
    def addLabel(value) { labels << value }
}

def sample = new Sample()
sample.with {
    username = 'mrhaki'
    email = 'email@host.com'
    println speakUp()  // Output: I am mrhaki
    addLabel 'Groovy' 
    addLabel 'Java'    
}
assert 2 == sample.labels.size()
assert 'Groovy' == sample.labels[0]
assert 'Java' == sample.labels[1]
assert 'mrhaki' == sample.username
assert 'email@host.com' == sample.email

def sb = new StringBuilder()
sb.with {
    append 'Just another way to add '
    append 'strings to the StringBuilder '
    append 'object.'    
}

assert 'Just another way to add strings to the StringBuilder object.' == sb.toString()

// Another example as seen at 
// http://javajeff.blogspot.com/2008/11/getting-groovy-with-with.html
def cal = Calendar.instance
cal.with {
    clear()
    set(YEAR, 2009)
    set MONTH, SEPTEMBER
    set DATE, 4    
    add DATE, 2
}
assert'September 6, 2009' == cal.time.format('MMMM d, yyyy')

数组Array

Groovy支持数组,就像在Java中一样。 由于添加到阵列的GDK扩展,我们只获得了更多的方法。 我们唯一需要考虑的是我们初始化数组的方式。 在Java中,我们可以使用以下代码定义和填充数组:String [] s = new String [] {"a","b"} ;,但在Groovy中我们不能使用此语法。 在Groovy中,前面的语句将变为String [] s = ["a","b"] as String[]

def strArray = new String[3]
assert strArray instanceof String[]
strArray[0] = 'mrhaki'
strArray.putAt(1, 'Groovy')  // New syntax.
strArray[2] = 'Java'

assert 'mrhaki' == strArray.getAt(0)  // Just another way to get a value.
assert 'Groovy' == strArray[1]
assert 'Java' == strArray[-1]  // Negative indeces allowed.
assert ['mrhaki', 'Groovy'] == strArray[0..1]  // We can use ranges.
assert ['mrhaki', 'Java'] == strArray[0, 2]

assert 3 == strArray.length  // Normal length property for arrays.
assert 3 == strArray.size()  // Groovy adds size() method as well.

// We can use min() and max() methods.
assert 42 == [102,301,42,83].min()
assert 301 == [102,301,42,83].max()
assert 'Java' == strArray.min { it.size() }
assert 'mrhaki' == strArray.max { it[0] as char }

// We can even use the Collection GDK methods on an array.
strArray.eachWithIndex { value, idx -> assert value == strArray[idx] }
assert ['ikahrm', 'yvoorG', 'avaJ'] == strArray.collect { it.reverse() }
assert 'Groovy' == strArray.find { it =~ /Groovy/ }

// We can remove values with the '-' operator.
assert ['Groovy', 'Java'] == strArray - 'mrhaki'

// Other useful methods for arrays.
assert ['Java', 'Groovy', 'mrhaki'] == strArray.reverse()
assert ['Groovy', 'Java', 'mrhaki'] == strArray.sort()
assert 1 == strArray.count('mrhaki')

// Convert to ArrayList.
def strList = strArray.toList()
assert 'java.util.ArrayList' == strList.class.name

// Convert ArrayList to array object.
def otherArray = strList as String[]
assert otherArray instanceof String[]

GroupBy

我们可以在Groovy中使用groupBy()方法将ListMap中的元素分组。

import static java.util.Calendar.*

class User {
    String name
    String city
    Date birthDate
    public String toString() { name }
}

def users = [
    new User(name:'mrhaki', city: 'Tilburg',   birthDate: Date.parse('yyyy-MM-dd', '1973-9-7')),
    new User(name:'bob',    city: 'New York',  birthDate: Date.parse('yyyy-MM-dd', '1963-3-30')),
    new User(name:'britt',  city: 'Amsterdam', birthDate: Date.parse('yyyy-MM-dd', '1980-5-12')),
    new User(name:'kim',    city: 'Amsterdam', birthDate: Date.parse('yyyy-MM-dd', '1983-3-30')),
    new User(name:'liam',   city: 'Tilburg',   birthDate: Date.parse('yyyy-MM-dd', '2009-3-6'))
]

def result = users.groupBy({it.city}, {it.birthDate.format('MMM')})

assert result.toMapString() == 
    '[Tilburg:[Sep:[mrhaki], Mar:[liam]], New York:[Mar:[bob]], Amsterdam:[May:[britt], Mar:[kim]]]'

assert result.Amsterdam.size() == 2
assert result.Tilburg.Mar.name == ['liam']

result = users.groupBy({it.name[0]}, {it.city})
assert result.toMapString() ==
    '[m:[Tilburg:[mrhaki]], b:[New York:[bob], Amsterdam:[britt]], k:[Amsterdam:[kim]], l:[Tilburg:[liam]]]'
assert result.k.Amsterdam.name == ['kim']  

// groupBy with multiple closues also works on Map
def usersByCityMap = users.groupBy({it.city})
def resultMap = usersByCityMap.groupBy({it.value.size()}, { k,v -> k.contains('i') })
assert resultMap.toMapString() ==
    '[2:[true:[Tilburg:[mrhaki, liam]], false:[Amsterdam:[britt, kim]]], 1:[false:[New York:[bob]]]]'
assert resultMap[1].size() == 1
assert resultMap[2].size() == 2
assert resultMap[2][true].Tilburg.name.join(',') == 'mrhaki,liam'

CliBuilder来解析命令行

import java.text.*

def showdate(args) {
    def cli = new CliBuilder(usage: 'showdate.groovy -[chflms] [date] [prefix]')
    // Create the list of options.
    cli.with {
        h longOpt: 'help', 'Show usage information'
        c longOpt: 'format-custom', args: 1, argName: 'format', 'Format date with custom format defined by "format"'
        f longOpt: 'format-full',   'Use DateFormat#FULL format'
        l longOpt: 'format-long',   'Use DateFormat#LONG format'
        m longOpt: 'format-medium', 'Use DateFormat#MEDIUM format (default)'
        s longOpt: 'format-short',  'Use DateFormat#SHORT format'
    }
    
    def options = cli.parse(args)
    if (!options) {
        return
    }
    // Show usage text when -h or --help option is used.
    if (options.h) {
        cli.usage()
        // Will output:
        // usage: showdate.groovy -[chflms] [date] [prefix]
        //  -c,--format-custom <format>   Format date with custom format defined by "format"
        //  -f,--format-full              Use DateFormat#FULL format   
        //  -h,--help                     Show usage information   
        //  -l,--format-long              Use DateFormat#LONG format   
        //  -m,--format-medium            Use DateFormat#MEDIUM format   
        //  -s,--format-short             Use DateFormat#SHORT format   
        return
    }
    
    // Determine formatter.
    def df = DateFormat.getDateInstance(DateFormat.MEDIUM)  // Defeault.
    if (options.f) {  // Using short option.
        df = DateFormat.getDateInstance(DateFormat.FULL) 
    } else if (options.'format-long') {  // Using long option.
        df = DateFormat.getDateInstance(DateFormat.LONG) 
    } else if (options.'format-medium') {
        df = DateFormat.getDateInstance(DateFormat.MEDIUM) 
    } else if (options.s) {
        df = DateFormat.getDateInstance(DateFormat.SHORT) 
    } else if (options.'format-custom') {
        df = new SimpleDateFormat(options.c)
    }

    // Handle all non-option arguments.
    def prefix = ''  // Default is empty prefix.
    def date = new Date()  // Default is current date.
    def extraArguments = options.arguments()
    if (extraArguments) {
        date = new Date().parse(extraArguments[0])
        // The rest of the arguments belong to the prefix.
        if (extraArguments.size() > 1) {
            prefix = extraArguments[1..-1].join(' ')
        }
    }
    
    "$prefix${df.format(date)}"
}

// Set locale for assertions.
Locale.setDefault(Locale.US)
assert '12/1/09' == showdate(['--format-short', '2009/12/1'])
assert '12/1/09' == showdate(['-s', '2009/12/1'])
assert 'Dec 1, 2009' == showdate(['2009/12/1'])
assert 'Dec 1, 2009' == showdate(['--format-medium', '2009/12/1'])
assert 'Dec 1, 2009' == showdate(['-m', '2009/12/1'])
assert 'December 1, 2009' == showdate(['--format-long', '2009/12/1'])
assert 'December 1, 2009' == showdate(['-l', '2009/12/1'])
assert 'Tuesday, December 1, 2009' == showdate(['--format-full', '2009/12/1'])
assert 'Tuesday, December 1, 2009' == showdate(['-f', '2009/12/1'])
assert 'Default date format: Dec 1, 2009' == showdate(['2009/12/1', 'Default', 'date', 'format: '])
assert 'Important date: Dec 1, 2009' == showdate(['-m', '2009/12/1', 'Important date: '])
assert 'week 49 of the year 2009 AD' == showdate(['-c', "'week' w 'of the year' yyyy G", '2009/12/1'])
assert '2009/12/01' == showdate(['--format-custom', 'yyyy/MM/dd', '2009/12/01'])
assert '2009' == showdate(['-cyyyy', '2009/12/1'])
assert new Date().format('yyyy/MM/dd') == showdate(['--format-custom', 'yyyy/MM/dd'])

println showdate(args) 

正则表达式

在Groovy中,我们使用=~运算符(查找运算符)来创建新的匹配器(Matcher)对象。
我们可以使用第二个运算符==~(匹配运算符)来进行精确匹配。 使用此运算符,将在Matcher对象上调用matches()方法。 结果是一个布尔值。

def finder = ('groovy' =~ /gr.*/)
assert finder instanceof java.util.regex.Matcher

def matcher = ('groovy' ==~ /gr.*/)
assert matcher instanceof Boolean

assert 'Groovy rocks!' =~ /Groovy/  // =~ in conditional context returns boolean.
assert !('Groovy rocks!' ==~ /Groovy/)  // ==~ looks for an exact match.
assert 'Groovy rocks!' ==~ /Groovy.*/

def cool = /gr\w{4}/  // Start with gr followed by 4 characters.
def findCool = ('groovy, java and grails rock!' =~ /$cool/)
assert 2 == findCool.count
assert 2 == findCool.size()  // Groovy adds size() method.
assert 'groovy' == findCool[0]  // Array-like access to match results.
assert 'grails' == findCool.getAt(1)

// With grouping we get a multidimensional array.
def group = ('groovy and grails, ruby and rails' =~ /(\w+) and (\w+)/)
assert group.hasGroup()
assert 2 == group.size()
assert ['groovy and grails', 'groovy', 'grails'] == group[0]
assert 'rails' == group[1][2]

// Use matcher methods.
assert 'Hi world' == ('Hello world' =~ /Hello/).replaceFirst('Hi')

// Groovy matcher syntax can be used in other methods.
assert ['abc'] == ['def', 'abc', '123'].findAll { it =~ /abc/ }
assert [false, false, true] == ['def', 'abc', '123'].collect { it ==~ /\d{3}/ }

转载于:https://my.oschina.net/wstone/blog/2872774

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值