Grails是一个插件架构,这一点我们已经在前面体会到了,最典型的就是GORM一节中,我们明明没有定义crud操作,但在运行时却可以使用它,造成这一结果的“元凶”就是我们预先安装的Hibernate插件。在这一节里,我们将对这背后的机制进行一番探究,对插件有一个基本的了解。
Plugin是Grails的主要扩展点,其工程跟普通Grails工程并无区别,只是多了一个描述文件。描述文件位于位于工程根目录,是以GrailsPlugin结尾的Groovy文件。同时,工程中缺省没有URL Mappings,因此Controller无法马上工作,如果需要则必须手工添加该文件。
插件相关的命令如下:
  • grails create-plugin
  • grails run-app
  • grails package-plugin
  • grails install-plugin url(或路径)
  • grails list-plugins:列出插件仓库中插件
  • grails plugin-info 插件名:列出插件信息
  • grails release-plugin
插件的描述文件定义了插件的相关信息,主要的属性有:
  • version:版本
  • title:短描述
  • author:作者
  • authorEmail:作者的Email
  • description:长描述
  • documentation:文档URL
插件完成之后,需要进行打包发布,在打包时需要注意,以下内容会被排除:
  • grails-app/conf/DataSource.groovy
  • grails-app/conf/UrlMappings.groovy
  • build.xml
  • /web-app/WEB-INF目录中所有内容
  • 插件文件中pluginExcludes定义的内容,如:
    def pluginExcludes = [
             "grails-app/views/error.gsp"
        ]
虽然UrlMappings.groovy会被排除,但是xxxUrlMappings.groovy并不在此列。同时,如果需要,你还可以修改工程的_Install.groovy脚本改变打包行为。
插件最终的发布位置是“仓库”。缺省仓库是:http://plugins.grails.org,Grails也支持配置多个仓库。在Grails中,自定义仓库的主要目的有2个:发现和发布,前者相对于插件的使用者,后者相对于插件的创作者。配置文件是grails-app/conf/BuildConfig.groovy或USER_HOME/.grails/settings.groovy,前者针对于单个项目,后者则是多项目间共享。配置内容:
grails.plugin.repos.discovery.myRepository=
              "http://svn.codehaus.org/grails/trunk/grails-test-plugin-repo" 
grails.plugin.repos.distribution.myRepository=
              "https://svn.codehaus.org/grails/trunk/grails-test-plugin-repo"
如果仓库设有用户名/口令,URL格式:PROTOCOL://USERNAME:PASSWORD@SERVER……
Grails的命令一般是自动解析仓库位置,也可以在命令中指定使用哪个仓库:
  • 在指定仓库中查找,grails list-plugins -repository=myRepository
  • 在指定仓库中发布,grails release-plugin -repository=myRepository
命令缺省的仓库搜索顺序为:default、core、自定义仓库。改变顺序则在配置文件中书写:
grails.plugin.repos.resolveOrder=['myRepository','default','core']
如果想只解析指定仓库:
grails.plugin.repos.resolveOrder=['myRepository']
安装插件时,需要注意其对使用工程的影响。它会影响使用工程的目录结构:
  • 原Plugin工程的grails-app目录内容移至plugins/plugin-version/grails-app。
  • 原Plugin工程中web-app的静态内容移至web-app/plugins/plugin-version。
  • 原Plugin工程的Java和Groovy内容编译到web-app/classes
鉴于以上目录改变,在Plugin工程中使用pluginContextPath引用目录。
<g:createLinkTo dir="${pluginContextPath}/js" file="mycode.js" />
Plugin工程中的一些部件的创建方法和普通Grails工程一样:Script、Controller、Tag Lib、Server、DomainClass等。其中view的查找顺序是:首先查找所安装App中的view,再查看插件中的。如果使用的template是插件的,那么写成:
<g:render template="fooTemplate" plugin="amazon"/>
每个Plugin工程都有一个application变量,它是GrailsApplication接口实例。每个GrailsApplication接口提供了计算项目内部惯例的方法,内部使用GrailsClass保存引用。GrailsClass代表一个物理的Grails资源,如一个Controller或一个标签库。application主要的动态方法有:
  • *Classes,获得某artefact的所有类:application.controllerClasses
  • get*Class,获得某artefact的指定类:application.getControllerClass("ExampleController")
  • is*Class,判断指定类是否是某artefact:application.isControllerClass(ExampleController.class)
GrailsClass的主要接口:
  • getPropertyValue
  • hasProperty
  • newInstance
  • getName
  • getShortName
  • getFullName
  • getPropertyName
  • getLogicalPropertyName
  • getNaturalName
  • getPackageName
插件工程中的脚本:
  • scripts/_Install.groovy,plugin被安装后触发
  • scripts/ _Update.groovy,update命令触发
以下的内容将说明,插件的一些“神迹”,如上面Domain Class中自动出现的CRUD,实现的原因。所有这些都要归因于插件参与了动态配置。在描述文件中定义以下闭包,将定义插件在动态配置时的行为:
  • doWithSpring:Spring配置,使用Spring Bean Builder。
  • doWithWebDescriptor:web.xml,使用XmlSlurper。
  • doWithApplicationContext:ApplicationContext构建之后。
  • doWithDynamicMethods:动态增加动态方法
导致DomainClass中凭空多出的CRUD方法的正是doWithDynamicMethods,使用例子:
class ExamplePlugin {
  def doWithDynamicMethods = { applicationContext ->
    application.controllerClasses.metaClass.each { metaClass ->
       metaClass.myNewMethod = {-> println "hello world" }
    }
  }
}
以上代码将给所有Controller都添加一个myNewMethod,其作用就是打印一行“hello world”。建议下载Grails的源码,然后搜一搜插件的描述文件,读一读,或许你会有其他的收获。
Grails也为插件自动重载提供了支持。描述文件中以下几个属性(或闭包)涉及Plugin的自动重载:
  • watchedResources属性:监测的资源。
  • onChange闭包:监视资源发生变化时调用,传入event对象,它的主要属性:event.source,事件源,重载的类或Spring资源;event.ctx,Spring的ApplicationContext实例;event.plugin,管理资源的plugin对象(通常是this);event.application,GrailsApplication实例
  • influences属性:定义影响的plugin,在重载时会一并载入。方向:由此及彼。
    def influences = ['controllers']
  • observe属性:定义观察的plugin,当那个插件变化时,获得变化通知事件。方向:由彼及此。
    def observe = ["hibernate"]
        def observe =["*"]
看看例子:
class ServicesGrailsPlugin {
     def watchedResources = "file:./grails-app/services/*Service.groovy"
     def onChange = { event ->
           if(event.source) {
                def serviceClass = application.addServiceClass(event.source)
                def serviceName = "${serviceClass.propertyName}"
                def beans = beans {
                       "$serviceName"(serviceClass.getClazz()) { bean ->
                              bean.autowire =  true
                }
           }
           if(event.ctx) {
               event.ctx.registerBeanDefinition(serviceName,
                    beans.getBeanDefinition(serviceName))
      } }}}
插件之间可以有依赖关系,因此这就引出了跟插件加载顺序相关的话题。这其中涉及两个属性:dependsOn和loadAfter。
dependsOn:定义了强依赖,如果依赖没有得到满足,Plugin不会加载。
  • 例1: def dependsOn = [dataSource:1.0, core: 1.0]
  • 例2: def dependsOn = [foo:"1.0 > 1.1"]。该表达式表示1.0~1.1版,包括1.0和1.1。可以使用*代表任意版本,如[foo:"* > 1.0"]、[foo:"1.0 > *"] 。
loadAfter:定义了弱依赖,如依赖没有满足,也会加载。但是Plugin可以在程序内部查看依赖是否满足(使用GrailsPluginManager),以决定是否提供高级操作。
以上就是插件的基本情况,除了这些,如今Grails社区已经就一点达成共识:使用插件作为一种模块化的机制,通过给主程序安装不同插件,逐步达成应用需求。