Groovy 使 Spring 更出色,第 1 部分: 集成的基础知识

来源:developerWorks 中国 作者:Scott Leberknight
  
Spring Framework 为 Web 和企业应用程序提供了坚实的基础。通过支持 Groovy 等动态语言,Spring 添加了一些功能,从而使应用程序架构更加灵活、更具动态性。在包含 2 部分的系列文章 的第一部分中,您将学习将 Groovy 集成到 Spring 应用程序的基础知识。

Spring 2.0 支持将动态语言集成到基于 Spring 的应用程序中。Spring 开箱即用地支持 Groovy、JRuby 和 BeanShell。以 Groovy、JRuby 或任何受支持的语言(当然包括 Java™ 语言)编写的应用程序部分可以无缝地集成到 Spring 应用程序中。应用程序其他部分的代码不需要知道或关心单个 Spring bean 的实现语言。

Spring 支持动态语言意味着应用程序可以获得灵活性和动态性,并且没有任何附加条件。在本系列的第 1 部分中,您将看到如何将 Spring 和 Groovy 一起使用,以及这个强大集成如何为应用程序增加有趣的功能。例如,您可能需要频繁地更改小块的业务逻辑、应用程序发出的 e-mail 消息中包含的文本、应用程序生成的 PDF 格式和布局等。为了进行更改,传统的应用程序架构可能需要完全重新部署应用程序。Spring 支持 Groovy 之后,您可以这样更改一个已部署的应用程序,并使这些更改立即生效。我将讨论这一功能为应用程序所带来的好处,以及可能引发的问题。本文中所有例子的完整的源代码(参见 下载)都可以下载。

Spring 的动态语言支持

动态语言支持将 Spring 从一个以 Java 为中心的应用程序框架改变成一个以 JVM 为中心的应用程序框架。现在,Spring 不再只是让 Java 开发变得更容易。它还允许将以静态和动态语言编写的代码轻松地插入到 Spring 支持的分层架构方法中,从而使 JVM 的开发也变得更加容易。如果您已经熟悉 Spring,那么您会感到很舒服:可以利用 Spring 已经提供的所有特性 — 控制反转(IoC)和依赖项注入、面向方面编程(AOP)、声明式事务划分、Web 和数据访问框架集成、远程调用等 — 同时又可以使用灵活动态的语言,比如 Groovy。

Spring 通过 ScriptFactory 和 ScriptSource 接口支持动态语言集成。ScriptFactory 接口定义用于创建和配置脚本 Spring bean 的机制。理论上,所有在 JVM 上运行语言都受支持,因此可以选择特定的语言来创建自己的实现。ScriptSource 定义 Spring 如何访问实际的脚本源代码;例如,通过文件系统或 URL。Groovy 语言集成通过 ScriptFactory 的 GroovyScriptFactory 实现得到支持。

为什么是 Groovy?

根据官方的 Groovy 站点,Groovy 是 “用于 Java 虚拟机的一种敏捷的动态语言”,它 “以 Java 的强大功能为基础,同时又包含由 Python、Ruby 和 Smalltalk 等语言带来的强大附加功能”,例如动态类型转换、闭包和元编程(metaprogramming)支持(参见 参考资料)。它是一种成熟的面向对象编程语言,既可以用于面向对象编程,又可以用作纯粹的脚本语言。我喜欢将它看作是没有讨厌代码,但又具有闭包和动态语言中的其他特性的 Java 语言。

Groovy 特别适合与 Spring 的动态语言支持一起使用,因为它是专门为 JVM 设计的,设计时充分考虑了 Java 集成,这使 Groovy 与 Java 代码的互操作很容易。它的类 Java 语法对于 Java 开发人员来说也很自然。

接下来,看看如何将 Groovy 代码集成到基于 Spring 的应用程序中。





更巧妙的 Spring bean

在 Spring 应用程序中使用 Groovy bean 很容易,就像使用 Java bean 一样。(但是,在后面可以看到,对于如何配置它们,则有很多选项)。首先,需要定义一个接口作为 Groovy bean 必须遵从的约定。虽然不是非得定义接口不可,但是大多数 Spring 应用程序会通过接口(而不是具体实现类)来定义应用程序组件之间的交互和依赖项,以促进松散耦合并为测试提供便利。

例如,假设有一个定义如何从 Invoice 对象生成 PDF 的接口。如清单 1 所示:


清单 1. PdfGenerator 接口
				
public interface PdfGenerator {
    byte[] pdfFor(Invoice invoice);
}

PdfGenerator 接口被用作 Groovy 实现类必须遵从的约定。这很容易,因为 Groovy 类可以像 Java 类那样实现接口。清单 2 显示了 PdfGenerator 的 Groovy 实现,它使用 iText 库(参见 参考资料)完成实际的 PDF 生成;它返回一个包含 PDF 内容的字节数组:


清单 2. GroovyPdfGenerator
				
class GroovyPdfGenerator implements PdfGenerator {

    String companyName

    public byte[] pdfFor(Invoice invoice) {
        Document document = new Document(PageSize.LETTER)
        ByteArrayOutputStream output = new ByteArrayOutputStream()
        PdfWriter.getInstance(document, output)
        document.open()
        Font headerFont = new Font(family: Font.HELVETICA, size: 24.0, style: Font.ITALIC)
        document.add(new Paragraph("$companyName", headerFont))
        document.add(new Paragraph("Invoice $invoice.orderNumber"))
        document.add(new Paragraph("Total amount: \$ ${invoice.total}"))
        document.close()
        output.toByteArray()
    }
}

GroovyPdfGenerator 已准备就绪。它定义了一个名为 companyName 的 string 属性,该属性在生成的 PDF 发票上与订单号和总额一起使用。此时,可以将 GroovyPdfGenerator 集成到 Spring 应用程序中。使用 Java 语言编写的 bean 必须编译成 .class 文件,但是在使用基于 Groovy 的 bean 时,则有几种选择:

  • 将 Groovy 类编译成普通的 Java 类文件
  • 在一个 .groovy 文件中定义 Groovy 类或脚本
  • 在 Spring 配置文件中以内联方式编写 Groovy 脚本

可以选择不同的方法在 Spring 应用程序上下文中定义和配置 Groovy bean,这取决于 Groovy bean 采用的选项。接下来,我们将探讨每一种配置选项。





Groovy bean 配置

通常,可以使用 XML 配置用 Java 代码编写的 Spring bean,或者 — 从 Spring 2.5(参见 参考资料)开始 — 使用注释进行配置,后者可以显著减少 XML 配置。当配置 Groovy bean 时,可用的选项取决于是使用编译的 Groovy 类还是 .groovy 文件中定义的 Groovy 类。需要记住的是,您可以使用 Groovy 实现 bean,然后可以像 Java 编程那样编译它们;或者在 .groovy 文件中以类似脚本的形式实现它们,然后由 Spring 负责在创建应用程序上下文时编译它们。

如果选择在 .groovy 文件中实现 bean,那么您不必 自己编译它们。相反,Spring 读取文件,获得脚本源代码并在运行时编译它们,使它们可用于应用程序上下文。这比直接编译更灵活性,因为不一定必须将 .groovy 文件部署在应用程序的 JAR 或 WAR 文件中,它们还可以来自文件系统的某个地方或 URL。

接下来介绍各种不同的配置选项的应用。要记住在构建过程中自己编译的 Groovy 类中定义的 bean 与在 .groovy 脚本中定义的 bean 之间的区别。

配置编译的 Groovy 类

配置已经编译成 .class 文件的 Groovy bean,这与配置基于 Java 的 bean 完全一样。假设您已经使用 groovyc 编译器编译了 GroovyPdfGenerator,那么可以使用常规的 Spring XML 配置定义 bean,如清单 3 所示:


清单 3. 使用 XML 配置预编译的 GroovyPdfGenerator
				
<bean id="pdfGenerator" class="groovierspring.GroovyPdfGenerator">
    <property name="companyName" value="Groovy Bookstore"/>
</bean>

Groovy bean 上没有基于构造函数的注入

不幸的是,目前还不能使用构造函数注入在 Groovy bean 上设置属性(或任何其他动态语言 bean,比如 JRuby bean)。原因之一是脚本可以定义多个实现类和逻辑,以根据运行时环境或其他因素选择不同的实现。换句话说,实际的构造是由脚本完成的,而不是 Spring。Spring 使用 setter 注入在返回的 bean 上设置属性。清单 7 显示了一个例子。

清单 3 中的配置是一个简单的旧的 Spring bean 定义。它是用 Groovy 实现的,但这一点不重要。在包含 pdfGenerator bean 的 Spring 应用程序中,任何其他组件都可以使用它,而不必知道或关心它的实现细节或语言。还可以像往常一样使用 <property> 元素在 bean 上设置属性。(Spring 2.0 引入了 p 名称空间,以便更简练地定义属性,但是我坚持使用 <property> 元素,因为我发现它们可读性更好 — 这完全是个人的喜好)。

另外,如果使用 Spring 2.5 或更高版本,还可以使用基于注释的 GroovyPdfGenerator 的配置。在此情况下,不必在 XML 应用程序上下文中实际定义 bean;相反,可以用 @Component 构造型注释来注释类,如清单 4 所示:


清单 4. 用 @Component 注释 GroovyPdfGenerator
				
@Component("pdfGenerator")
class GroovyPdfGenerator implements PdfGenerator {
    ...
}

然后,在 Spring 应用程序上下文 XML 配置中启用注释配置和组件扫描,如清单 5 所示:


清单 5. 启用 Spring 注释配置和组件扫描
				
<context:annotation-config/>
<context:component-scan base-package="groovierspring"/>

不管使用 XML 还是注释来配置编译后的 Groovy bean,这种配置与普通的基于 Java bean 的配置是一样的。

配置来自 Groovy 脚本的 bean

配置来自 .groovy 脚本的 Groovy bean 与配置编译后的 Groovy bean 大不相同。在这里,事情开始变得更加有趣。将 Groovy 脚本转换为 bean 的机制包括读取并编译 Groovy 脚本,然后使之可以在 Spring 应用程序上下文中作为 bean 使用。第一步是定义一个 bean,它的类型可以认为是 GroovyScriptFactory,并且指向 Groovy 脚本的位置,如清单 6 所示:


清单 6. 定义 GroovyScriptFactory bean
				
<bean id="pdfGenerator"
      class="org.springframework.scripting.groovy.GroovyScriptFactory">
    <constructor-arg value="classpath:groovierspring/GroovyPdfGenerator.groovy"/>
    <property name="companyName" value="Groovier Bookstore"/>
</bean>

在这个清单中,pdfGenerator bean 被定义为 GroovyScriptFactory。<constructor-arg> 元素定义要配置的 Groovy 脚本的位置。特别要注意,这指向一个 Groovy 脚本,而不是一个已编译的 Groovy 类。可以使用定义 Spring bean 的语法设置用脚本编写的对象的属性。正如您预期的那样,清单 6 中的 <property> 元素设置 companyName 属性。

GroovyPdfGenerator.groovy 脚本 必须包含至少一个实现接口的类。通常,最好的做法是遵从标准 Java 实现,每个 .groovy 文件定义一个 Groovy 类。但是,您可能想在脚本中实现用于确定创建哪种类型的 bean 的逻辑。例如,可以在 GroovyPdfGenerator.groovy 中定义 PdfGenerator 接口的两种不同的实现,并直接在脚本中执行确定应该返回哪种实现的逻辑。清单 7 定义两种不同的 PdfGenerator 实现,并根据系统的属性选择使用一种实现:


清单 7. Groovy 脚本中的多个类定义
				
class SimpleGroovyPdfGenerator implements PdfGenerator {
    ...
}

class ComplexGroovyPdfGenerator implements PdfGenerator {
    ...
}

def type = System.properties['generatorType']
if (type == 'simple')
    return new SimpleGroovyPdfGenerator()
}
else {
    return new ComplexGroovyPdfGenerator()
}

如这段代码所示,可以通过用脚本编写的 bean 根据系统属性选择不同的实现。当 generatorType 系统属性为 simple 时,该脚本创建并返回一个 SimpleGroovyPdfGenerator;否则,它返回一个 ComplexGroovyPdfGenerator。由于简单和复杂的实现都实现了 PdfGenerator 接口,因此 Spring 应用程序中使用 pdfGenerator bean 的代码不必知道也不必关心实际的实现是什么。

注意,仍然可以像 清单 6 那样在从脚本返回的 bean 上设置属性。所以,如果脚本返回一个 ComplexGroovyPdfGenerator,则设置该 bean 上的 companyName 属性。如果不需要定义多个实现,那么可以在 Groovy 脚本文件中仅定义一个类,如清单 8 所示。在这种情况下,Spring 发现并实例化这个惟一的类。


清单 8. 典型的 Groovy 脚本实现
				
class GroovyPdfGenerator implements PdfGenerator {
    ...
}

至此,您可能想知道为什么 清单 6 将 bean 定义为一个 GroovyScriptFactory。那是因为 Spring 通过一个与 ScriptFactoryPostProcessor bean 结合的 ScriptFactory 实现(在这里是一个 Groovy 工厂)创建脚本对象。ScriptFactoryPostProcessor bean 负责用由工厂创建的实际对象替换工厂 bean。清单 9 显示添加后处理器 bean 的附加配置:


清单 9. 定义 ScriptFactoryPostProcessor bean
				
<bean class="org.springframework.scripting.support.ScriptFactoryPostProcessor"/>

当 Spring 装载应用程序上下文时,它首先创建工厂 bean(例如 GroovyScriptFactory bean)。然后,执行 ScriptFactoryPostProcessor bean,用实际的脚本对象替换所有的工厂 bean。例如,清单 6 和 清单 9 中的配置产生一个名为 pdfGenerator 的 bean,它的类型是 groovierspring.GroovyPdfGenerator。(如果启用 Spring 中的 debug 级日志记录,并观察应用程序上下文的启动,将会看到 Spring 首先创建一个名为 scriptFactory.pdfGenerator 的工厂 bean,然后 ScriptFactoryPostProcessor 从该工厂 bean 创建 pdfGenerator bean)。

现在,您已知道使用 GroovyScriptFactory 和 ScriptFactoryPostProcessor 配置脚本编写的 Groovy bean 的底层细节,接下来我将展示一种更简单、更整洁的方法。这种方法可以得到相同结果。Spring 专门为创建脚本 bean 提供了 lang XML 模式。清单 10 使用 lang 模式定义 pdfGenerator bean:


清单 10. 使用 <lang:groovy> 定义脚本 bean
				
<lang:groovy id="pdfGenerator"
             script-source="classpath:groovierspring/GroovyPdfGenerator.groovy">
    <lang:property name="companyName" value="Really Groovy Bookstore"/>
</lang:groovy>

这段代码产生的 pdfGenerator bean 与 清单 6 和 清单 9 中更冗长的配置产生的 bean 是一样的,但是它更整洁、更简练,而且意图更清晰。<lang:groovy> bean 定义需要 script-source 属性;这告诉 Spring 如何找到 Groovy 脚本源代码。此外,可以使用 <lang:property> 元素为脚本 bean 设置属性。使用 <lang:groovy> 定义基于 Groovy 的 bean 是一种更好的选择。对阅读 Spring 配置的人而言,这种选项也更加清晰。

配置内联 Groovy 脚本

为了实现完整性,我将介绍:Spring 还支持直接在 bean 定义中编写 Groovy 脚本。清单 11 使用一个内联脚本创建 pdfGenerator:


清单 11. 内联定义脚本 bean
				
<lang:groovy id="pdfGenerator">
    <lang:inline-script>
        <![CDATA[
        class GroovyPdfGenerator implements PdfGenerator {
            ...
        }
        ]]>
    </lang:inline-script>
    <lang:property name="companyName" value="Icky Groovy Bookstore"/>
</lang:groovy>

这段代码使用 <lang:groovy> 和 <lang:inline-script> 标记定义 pdfGenerator bean,它包含定义类的 Groovy 脚本。可以像前面一样使用 <lang:property> 设置属性。您可能已经猜到,我不建议在 XML 配置文件中定义脚本 bean(或这一方面的任何类型的代码)。

使用 Grails Bean Builder 配置 bean

Grails Web framework 在幕后依赖于 Spring。Grails 提供了 Bean Builder,这是一个很棒的特性,让您可以使用 Groovy 代码编程式地 定义 Spring bean(参见 参考资料)。编程式地定义 bean 比 XML 配置更灵活,因为可以在 bean 定义脚本中嵌入逻辑,而这在 XML 中是不可能的。通过使用 Bean Builder,可以为已编译 Groovy 类和用脚本编写的 Groovy bean 创建 bean 定义。清单 12 使用已编译的 Groovy 类定义 pdfGenerator bean:


清单 12. 使用 Bean Builder 定义已编译的 Groovy bean
				
def builder = new grails.spring.BeanBuilder()
builder.beans {
    pdfGenerator(GroovyPdfGenerator) {
        companyName = 'Compiled BeanBuilder Bookstore'
    }
}
def appContext = builder.createApplicationContext()
def generator = context.pdfGenerator

清单 12 中的代码首先实例化一个 BeanBuilder,然后通过方法调用创建 bean。每个方法调用和可选的闭包参数定义一个 bean,并设置 bean 属性。例如,pdfGenerator(GroovyPdfGenerator) 定义一个名为 pdfGenerator 的 bean,其类型为 GroovyPdfGenerator,闭包中的代码则设置 companyName 属性。当然,在 beans 闭包中可以定义多个 bean。

通过使用 Bean Builder,还可以从 Groovy 脚本而不是已编译的 Groovy 类创建 bean。但是,Bean Builder 没有 <lang:groovy> 配置中的语法糖(syntactic sugar,即在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用),所以需要将 bean 定义为 GroovyScriptFactory,并创建一个 ScriptFactoryPostProcessor bean。清单 13 是一个例子,展示如何使用 Bean Builder 配置用脚本编写的 Groovy bean:


清单 13. 使用 Bean Builder 定义用脚本编写的 Groovy bean
				
def builder = new grails.spring.BeanBuilder()
builder.beans {
    pdfGenerator(GroovyScriptFactory,
                'classpath:groovierspring/GroovyPdfGenerator.groovy') {
        companyName = 'Scripted BeanBuilder Bookstore'
    }
    scriptFactoryPostProcessor(ScriptFactoryPostProcessor)
}
def appContext = builder.createApplicationContext()
def generator = context.pdfGenerator

清单 13 中的代码在逻辑上等同于 清单 6 和 清单 9 中的 XML 配置。当然,清单 13 是使用 Groovy 代码来定义 bean。为了定义 pdfGenerator bean,清单 13 将类型指定为 GroovyScriptFactory。第二个参数指定脚本源代码的位置,和前面一样,在闭包中设置 companyName 属性。它还定义一个名为 scriptFactoryPostProcessor 的 bean,其类型为 ScriptFactoryPostProcessor,它将用实际的用脚本编写的对象替换工厂 bean。

哪种配置选项最好?

至此,您已经看到配置基于 Groovy 的 bean(无论是已编译的还是用脚本编写的)的几种不同的方式。如果您仅是使用 Groovy 替代 Java 作为应用程序的主要语言,那么配置这些 bean 与配置基于 Java 的 bean 没有区别。对于已编译的 Groovy 类,可以使用 XML 或基于注释的配置进行配置。

对于用脚本编写的 Groovy 对象,虽然可以用几种不同的方式来配置它们,但是 <lang:groovy> 选项却是最简洁的方式,与使用 GroovyScriptFactory 和 ScriptFactoryPostProcessor 或者使用 <lang:inline-script> 进行配置相比,这种选项能够最清晰地表现意图。

您还看到了 Grails Bean Builder,它以完全不同的方式创建大多数 Spring 应用程序使用的 Spring 应用程序上下文。如果要用 Groovy 创建所有的 bean,并且要能够添加逻辑到 bean 构建过程中,Bean Builder 必须很好地符合要求。另一方面,使用 Bean Builder 定义 Groovy bean 时,需要使用 GroovyScriptFactory 和 ScriptFactoryPostProcessor 来定义 bean。





使用 Groovy bean

bean 配置和可用的几个选项是集成 Groovy 和 Spring 的难点(但是如您所见,这并不是很难)。实际上,在 Spring 应用程序中使用 Groovy bean 很容易。Spring 的动态语言支持使得 bean 的使用对于应用程序代码是完全透明的,应用程序代码不需要知道也不需要关心实现细节。您可以像平常开发 Spring 应用程序一样编写应用程序代码,并且可以利用 Spring 提供的所有特性,例如依赖项注入、AOP 和与第三方框架集成。

清单 14 展示了一个简单的 Groovy 脚本,它从 XML 配置文件创建一个 Spring 应用程序上下文,获取 PDF 生成器 bean,并使用它生成一个发票的 PDF 版本:


清单 14. 在脚本中使用 Groovy bean
				
def context = new ClassPathXmlApplicationContext("applicationContext.xml")
def generator = context.getBean("pdfGenerator")

Invoice invoice = new Invoice(orderNumber: "12345", orderDate: new Date())
invoice.lineItems = [
    new LineItem(quantity: 1, description: 'Groovy in Action (ebook)', price: 22.00),
    new LineItem(quantity: 1, description: 'Programming Erlang', price: 45.00),
    new LineItem(quantity: 2, description: 'iText in Action (ebook)', price: 22.00)
]

byte[] invoicePdf = generator.pdfFor(invoice)

FileOutputStream file = new FileOutputStream("Invoice-${invoice.orderNumber}.pdf")
file.withStream {
    file.write(invoicePdf)
}
println "Generated invoice $invoice.orderNumber"

在 清单 14 中,大部分代码用于创建 Spring ApplicationContext,创建发票并将它写出到一个文件。使用 pdfGenerator bean 生成发票仅需一行代码。在通常的 Spring 应用程序中,在应用程序启动时引导一次应用程序上下文,然后,应用程序中的组件只需使用 Spring 为它们提供的依赖项。在 Spring Web 应用程序中,可以配置一个 servlet 上下文侦听器,在应用程序启动时引导 Spring。例如,可以定义一个 PDF 发票生成服务,如清单 15 所示:


清单 15. 使用 PDF 生成器的服务类
				
@Service
public class InvoicePdfServiceImpl implements InvoicePdfService {

    @Autowired
    private PdfGenerator pdfGenerator;

    public byte[] generatePdf(Long invoiceId) {
        Invoice invoice = getInvoiceSomehow(invoiceId);
        return pdfGenerator.pdfFor(invoice);
    }

    // Rest of implementation...

}

清单 15 中的 InvoicePdfServiceImpl 类刚好被实现为一个 Java 类,它依赖于 PdfGenerator。可以很方便地将它实现为 Groovy bean。可以通过任何以编译的或用脚本编写的 bean 配置来使用 GroovyPdfGenerator 实现,而 InvoicePdfServiceImpl 对此一无所知。因此,使用 Groovy(或任何动态语言)对应用程序代码而言是透明的。这样很好,因为实现了组件之间的松散耦合,从而使单元测试更加容易,并且可以使用最适合的实现语言。





结束语

您已经看到了配置 Groovy 语言 bean 的一些不同的方式,以及在基于 Spring 的应用程序中使用它们是多么容易。您可以像使用 Java 类一样使用已编译的 Groovy 类。您还看到了配置用脚本编写的 Groovy 对象的一些不同的方式。应该选择的选项取决于如何在应用程序中使用 Groovy。还可以在同一个应用程序中结合使用已编译的和用脚本编写的 Groovy bean。实际上,如果希望的话,还可以在同一个应用程序中同时使用 Java、Groovy、JRuby 和 BeanShell bean,但我不建议这样做。作为开发人员,必须权衡在同一应用程序中使用多种语言的优点和缺点。

作为一种语言,Groovy 比 Java 更灵活,这使它成为很有吸引力的选择,即使仅选择编译 Groovy 类也是如此。Spring 可以集成用脚本编写的动态语言 bean,这使人们更加喜欢选择 Groovy,因为可以在用脚本编写的 bean 中引入附加的逻辑和灵活性。例如,正如前面看到的那样,可以根据业务逻辑添加确定应用程序启动时应该实例化的 bean 类型的逻辑。或者,可以将用脚本编写的对象部署到 .groovy 文件中,使 Web 应用程序的部署更加灵活。.groovy 文件位于应用程序的 CLASSPATH 中或文件系统中的某个地方,而不是打包在 WAR 文件中。

到目前为止,您看到的所有东西都为 Spring 工具箱增加了灵活性和威力。但是,Spring 动态语言支持中最引人注目的特性可能是在应用程序运行时 监视和检测对动态语言脚本的更改,并在 Spring 应用程序上下文中自动重新装载 更改后的 bean。第 2 部分将深入探索这个功能。包含 bean 的静态配置在运行时不能更改,与之对比,这个功能提供了很大的灵活性。(责任编辑:A6)


时间:2009-02-09 09:10 来源:developerWorks 中国 作者:Scott Leberknight 原文链接

好文,顶一下
(1)
50%
文章真差,踩一下
(1)
50%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量