Grails提供了许多扩展点来满足你的扩展,包括从命令行接口到运行时配置引擎。以下章节详细说明了该如何着手来做这些扩展。
创建一个Grails插件,只需要运行如下命令即可:
grails create-plugin [PLUGIN NAME]
根据你输入的名字将产生一插件工程。比如你输入 grails create-plugin example
. 系统将创建一个名为 example
的插件工程.
除了插件的根目录有一个所谓的“插件描述”的Groovy文件外,其他的跟一般的Grails工程结构完全一样.
将插件作为一个常规的Grails工程是有好处的,比如你可以马上用以下命令来测试你的插件:
grails run-app
由于你创建插件默认是没有 URL 映射的,因此控制器并不会马上有效.如果你的插件需要控制器,那要创建grails-app/conf/MyUrlMappings.groovy
文件,并且在起始位置增加缺省的映射"/$controller/$action?/$id?"()
.
插件描述文件本身需要符合以 GrailsPlugin
结尾的惯例并且将位于插件工程的根目录中。比如:
class ExampleGrailsPlugin { def version = 0.1… }
所有插件的根目录下边都必须有此类并且还要有效,此类中定义了插件的版本和其他各式各样的可选的插件扩展点的钩子(hooks)--即插件预留的可以扩展的接口.
通过以下特殊的属性,你还可以提供插件的一些额外的信息:
title
- 用一句话来简单描述你的插件 author
- 插件的作者 authorEmail
- 插件作者的电子邮箱 description
- 插件的完整特性描述 documentation
- 插件文档的URL 以 Quartz Grails plugin为例:
class QuartzGrailsPlugin { def version = "0.1" def author = "Sergey Nebolsin" def authorEmail = "[email protected]" def title = "This plugin adds Quartz job scheduling features to Grails application." def description = ''' Quartz plugin allows your Grails application to schedule jobs to be executed using a specified interval or cron expression. The underlying system uses the Quartz Enterprise Job Scheduler configured via Spring, but is made simpler by the coding by convention paradigm. ''' def documentation = "http://grails.org/Quartz+plugin"… }
要发布插件,你需要一个命令行窗口,并且进入到插件的根目录,输入:
grails package-plugin
这将创建一个 grails-
+插件名称+版本的zip文件. 以先前的example插件为例,这个文件名是 grails-example-0.1.zip
. package-plugin
命令还将生成 plugin.xml
f在此文件中包含机器可读的插件信息,比如插件的名称、版本、作者等等。
产生了可以发布的插件文件以后(zip文件),进入到你自己的Grails工程的根目录,输入:
grails install-plugin /path/to/plugin/grails-example-0.1.zip
如果你的插件放在远程的Http服务器上,你也可以这样:
grails install-plugin http://myserver.com/plugins/grails-example-0.1.zip
尽管 create-plugin 命令为您创建某些文件,以便插件能做为Grails应用运行,但是当打包插件的时候不是所有的文件都会在含在里面. 以下是通过package-plugin创建时,不包含的文件和目录:
grails-app/conf/DataSource.groovy
grails-app/conf/UrlMappings.groovy
grails-app/conf/DataSource.groovy
build.xml
/web-app/WEB-INF
如果你希望创建包含 WEB-INF
目录的组建,那么建议你使用 _Install.groovy
脚本文件 (covered later),这个脚本文件之后会解释;当安装一个插件提供这些组件时,这个脚本文件会被执行。 此外,除了用 UrlMappings.groovy
之外,也允许你使用包括 UrlMappings
名字来定义不同的名称,例如 FooUrlMappings.groovy
更好的发布插件的方式是将其发布到Grails插件的存储仓库. 这样通过 list-plugins 命令就可以看到你的插件了:
grails list-plugins
此命令将列出Grails插件存储库的所有插件,当然了也可以用 plugin-info 来查看指定插件的信息:
grails plugin-info [plugin-name]
这将输出更多的详细信息,这些信息都是维护在插件描述文件中的。
如果你创建了一个Grails插件,你可以访问 创建插件,这里详细说明了如何在容器中发布你的插件。
当你有访问Grails插件仓库的权限时,要发行你的插件,只需要简单执行 release-plugin 即可:
grails release-plugin
这将自动地将改动提交到SVN和创建标签(svn的tagging),并且通过 list-plugins 命令你可以看到这些改动.
默认情况下,您使用的 list-plugins, install-plugin and release-plugin 命令都指向 http://plugins.grails.org。
然而, 要配置多个插件仓库,您可以使用grails-app/conf/BuildSettings.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"
Repositories are split into those used for discovery over HTTP and those used for distribution, typically over HTTPS. 如果你想在多个项目中使用相同的设置,你可以把这些配置到 USER_HOME/.grails/settings.groovy
。
一旦使用了 list-plugins, install-plugin and plugin-info 命令将会自动处理最新配置的插件库。如果你只想把插件库中的插件列表列出来,你可以使用别名:
grails list-plugins -repository=myRepository
此外,如果你想和配置好的插件包一起发布插件,你可以用 release-plugin 命令:
grails release-plugin -repository=myRepository
如前所提到的,一个插件除了包含一个插件描述文件外,几乎就是一个常规的Grails应用。尽管如此,当安装以后,插件的结构还是有些许的差别。比如一个插件目录的结构如下:
+ grails-app + controllers + domain + taglib etc. + lib + src + java + groovy + web-app + js + css
从本质上讲,当一个插件被安装到Grails工程以后, grails-app
下边的内容将被拷贝到以 plugins/example-1.0/grails-app
(以example为例)目录中. 这些内容 不会 被拷贝到工程的源文件主目录,即插件永远不会跟工程的主目录树有任何接口上的关系。.
然而,那些在特定插件目录中 web-app
目录下的静态资源将会被拷贝到主工程的 plugins
目录下. 比如 web-app/plugins/example-1.0/js
.
因此,要从正确的地方引用这些静态资源也就成为插件的责任。比如,你要在GSP中引用一个JavaScript文件,你可以这样:
<g:createLinkTo dir="/plugins/example/js" file="mycode.js" />
这样做当然可以,但是当你开发插件并且单独运行插件的时候,将产生相对链接(link)的问题.
为了应对这种变化即不管插件是单独运行还是在Grails应用中运行,特地新增一个特别的 pluginContextPath
变量,用法如下:
<g:createLinkTo dir="${pluginContextPath}/js" file="mycode.js" />
这样在运行期间 pluginContextPath
变量将会等价于/ 或 /plugins/example
这取决于插件是单独运行还是被安装在Grails应用中
在lib和 src/java
以及 src/groovy
下的Java、Groovy代码将被编译到当前工程的 web-app/WEB-INF/classes
下边,因此在运行时也不会出现类找不到的问题.
在插件的scripts目录下可以增加新的Gant相关的脚本:
+ MyPlugin.groovy + scripts <-- additional scripts here + grails-app + controllers + services + etc. + lib
在 grails-app
相关的目录树下,可以增加新的控制器、标签库、服务等,不过要注意:当插件被安装后将从其被安装的地方加载,而不是被拷贝到当前主应用工程的相应目录。.
+ ExamplePlugin.groovy + scripts + grails-app + controllers <-- additional controllers here + services <-- additional services here + etc. <-- additional XXX here + lib
提供控制器的插件也会提供默认的视图。通过插件模块化您的应用是个很好的途径。Grails视图处理机制的工作原理是首先查看应用中被安装的视图,如果失败将视图查找插件中的视图。
比如有一个 AmazonGrailsPlugin
插件提供一个叫 BookController
的控制器,如果执行了 list
将会首先查找 grails-app/views/book/list.gsp
这个视图,如果失败,将会在插件里查找相同名称的视图。
但是,如果视图使用了模板,同时插件也提供了这个视图,那么必须使用以下的语法:
<g:render template="fooTemplate" contextPath="${pluginContextPath}"/>
注意 pluginContextPath
变量做为 contextPath
属性值的用法。如果没有指定这个属性,Grails将在应用中的模板中查找。
默认的,when packaging a plug-in,当打包一个插件时,Grails 的插件包中将不包含以下文件:
如果你的插件需要 web-app/WEB-INF
目录下的文件,那么建议你修改插件的 scripts/_Install.groovy
Gant 脚本文件把项目的目标目录安装到插件包中。
此外, UrlMappings.groovy
文件默认不会避免命名冲突,你可以使用在默认名字前加增加 前缀。比如叫做 grails-app/conf/BlogUrlMappings.groovy
。
在得以继续查看基于规约所能提供的运行时配置以前,有必要了解一下怎样来评估插件的这些基本规约。本质上,每一个插件都有一个隐含的 GrailsApplication接口的实例变量:application
。
GrailsApplication
提供了在工程内评估这些规约的方法并且保存着所有类的相互引用,这些类都实现了 GrailsClass 接口.
一个 GrailsClass
代表着一个物理的Grails资源,比如一个控制器或者一个标签库。如果要获取所有 GrailsClass
实例,你可以这样:
application.allClasses.each { println it.name }
在 GrailsApplication
实例中有一些特殊的属性可以方便的操作你感兴趣的人工制品(artefact)类型,比如你要获取所有控制器的类,可以如此:
application.controllerClasses.each { println it.name }
这些动态方法的规约如下:
*Classes
- 获取特定人工制品名称的所有类,比如 application.controllerClasses
. get*Class
- 获取特定人工制品的特定类,比如 application.getControllerClass("ExampleController")
is*Class
- 如果给定的类是指定的人工制品类型,那么返回true, 比如 application.isControllerClass(ExampleController.class)
add*Class
- 为给定的人工制品类型新增一个类并且返回新增的 GrailsClass
实例-比如:application.addControllerClass(ExampleController.class)
GrailsClass
接口本身也提供了很多有用的方法以允许你进一步的评估和了解这些规约,他们包括:
getPropertyValue
- 获取给定属性的初始值 hasProperty
- 如果类含有指定的属性,那么返回true newInstance
- 创建一个类的新实例 getName
- 如果可以的话,返回应用类的逻辑名称,此名称不含后缀部分 getShortName
- 返回类的简称,不包含包前缀 getFullName
- 返回应用类的完整名称,包含后缀部分和包的名称 getPropertyName
- 将类的名称返回为属性名称 getLogicalPropertyName
- 如果可以的话,返回应用类的逻辑属性名称,此名称不包含后缀部分 getNaturalName
- 返回属性名称的自然语言的术语(比如将'lastName' 变为 'Last Name') getPackageName
- 返回包的名称 完整的索引请参考 javadoc API.
Grails插件可以在安装完后进行配置并且可以参与应用的升级过程(通过 upgrade命令),这是由scripts目录下两个特定名称的脚本来完成的: - _Install.groovy
和 _Upgrade.groovy
.
_Install.groovy
是在插件安装完成后被执行的,而 _Upgrade.groovy
是用户每次通过 upgrade 命令来升级他的应用时被执行的.
这些是一个普通的 Gant 脚本,因此你完全可以使用Gant的强大特性。另外 pluginBasedir
被加入到Gant的标准变量中,其指向安装插件的根目录。
以下的 _Install.groovy
示例脚本将在 grails-app
目录下创建一个新的目录,并且安装一个配置模板,如下:
Ant.mkdir(dir:"${basedir}/grails-app/jobs") Ant.copy(file:"${pluginBasedir}/src/samples/SamplePluginConfiguration.groovy", todir:"${basedir}/grails-app/conf")// To access Grails home you can use following code: // Ant.property(environment:"env") // grailsHome = Ant.antProject.properties."env.GRAILS_HOME"
将插件和命令行的脚本事件关联起来还是有可能的,这些事件在执行Grails的任务和插件事件的时候被触发。
比如你希望在更新的时候,显示更新状态(如"Tests passed", "Server running"),并且创建文件或者人工制品。
一个插件只能通过 Events.groovy
脚本来监听那些必要的事件。更多详细信息请参考 Hooking into Events.
Grails提供了很多的钩子函数来处理系统的不同部分,并且通过惯例的形式来执行运行时配置。
首先你可以使用 doWithSpring
闭包来跟Grails运行时的配置进行交互,例如下面的代码片段是取自于Grails核心插件 i18n的一部分:
import org.springframework.web.servlet.i18n.CookieLocaleResolver; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.context.support.ReloadableResourceBundleMessageSource;class I18nGrailsPlugin {
def version = 0.1
def doWithSpring = { messageSource(ReloadableResourceBundleMessageSource) { basename = "WEB-INF/grails-app/i18n/messages" } localeChangeInterceptor(LocaleChangeInterceptor) { paramName = "lang" } localeResolver(CookieLocaleResolver) } }
这个插件建立起了Grails messageSource
bean和一对其他beans以管理Locale解释和更改。它使用 Spring Bean Builder 语法。
Grails是在加载的时候生成 WEB-INF/web.xml
文件,因此插件不能直接修改此文件,但他们可以参与此文件的生成。 本质上一个插件可以通过 doWithWebDescriptor
闭包来完成此功能,此闭包的参数是 web.xml
是作为 XmlSlurper
GPathResult
类型传入的.
考虑如下来自 ControllersPlugin
的示例:
def doWithWebDescriptor = { webXml -> def mappingElement = webXml.'servlet-mapping' mappingElement + { 'servlet-mapping' { 'servlet-name'("grails") 'url-pattern'("*.dispatch") } } }
此处插件得到最后一个 <servlet-mapping>
元素的引用,并且在其后添加Grails' servlet,这得益于XmlSlurper可以通过闭包以编程的方式修改XML的能力。
有时候在Spring的 ApplicationContext 被创建以后做一些运行时配置是有意义的,这种情况下,你可以定义 doWithApplicationContext
闭包,如下例:
class SimplePlugin { def name="simple" def version = 1.1def doWithApplicationContext = { appCtx -> SessionFactory sf = appCtx.getBean("sessionFactory") // do something here with session factory } }
Grails插件允许你在运行时注册Grails管辖类或者其他类的动态方法,但新的方法只能通过 doWithDynamicMethods
闭包来增加。
对Grails管辖类来说,比如controllers、tag libraries等等,你可以增加方法,构造函数等,这是通过 ExpandoMetaClass 机制做到的,比如访问每个控制器的 MetaClass的代码如下所示:
class ExamplePlugin {
def doWithDynamicMethods = { applicationContext ->
application.controllerClasses.each { controllerClass ->
controllerClass.metaClass.myNewMethod = {-> println "hello world" }
}
}
}
此处我们通过隐含的application对象来获取所有控制器类的MetaClass实例,并且为每一个控制器增加一个 myNewMethod
的方法。或者,你已经知道要处理的类的类型了,那你只需要在此类的 metaClass
属性上增加一个方法即可,代码如下:
class ExamplePlugin {def doWithDynamicMethods = { applicationContext -> String.metaClass.swapCase = {-> def sb = new StringBuffer() delegate.each { sb << (Character.isUpperCase(it as char) ? Character.toLowerCase(it as char) : Character.toUpperCase(it as char)) } sb.toString() }
assert "UpAndDown" == "uPaNDdOWN".swapCase() } }
此例中,我们直接在 java.lang.String
的 metaClass
上增加一个新的 swapCase
方法.
doWithDynamicMethods
闭包的参数是Spring的 ApplicationContext
实例,这点非常有用,因为这允许你和该应用上下文实例中的对象进行交互。比如你打算实现一个跟Hibernate交互的方法,那你可以联合着 HibernateTemplate
来使用SessionFactory
例,代码如下:
import org.springframework.orm.hibernate3.HibernateTemplateclass ExampleHibernatePlugin {
def doWithDynamicMethods = { applicationContext ->
application.domainClasses.each { domainClass ->
domainClass.metaClass.static.load = { Long id-> def sf = applicationContext.sessionFactory def template = new HibernateTemplate(sf) template.load(delegate, id) } } } }
另外因为Spring容器具有自动装配和依赖注入的能力,你可以在运行时实现更强大的动态构造器,此构造器使用applicationContext来装配你的对象及其依赖:
class MyConstructorPlugin {def doWithDynamicMethods = { applicationContext -> application.domainClasses.each { domainClass -> domainClass.metaClass.constructor = {-> return applicationContext.getBean(domainClass.name) } }
} }
这里我们实际做的是通过查找Spring的原型beans(prototyped beans)来替代缺省的构造器。
通常来讲,当资源发生改变的时候,监控并且重新加载这些变化是非常有意义的。这也是Grails为什么要在运行时实现复杂的应用程序重新加载。查看如下Grails的 ServicesPlugin
的一段简单的代码片段:
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)) } } } }
首先定义了 watchedResources
集合,此集合可能是String或者String的List,包含着要监控的资源的引用或者模式。如果要监控的资源是Groovy文件,那当它被改变的时候,此文件将会自动被重新加载,而且被传给 onChange
闭包的参数 event
.
event
对象定义了一些有益的属性:
event.source
- The source of the event which is either the reloaded class or a Spring Resource event.ctx
- The Spring ApplicationContext
instance event.plugin
- The plugin object that manages the resource (Usually this) event.application
- The GrailsApplication
instance 通过这些对象,你可以评估这些惯例,而且基于这些惯例你可以将这些变化适当的应用到 ApplicationContext
中, 在上述的"Services"示例中,当一个service类变化时,一个新的service类被重新注册到 ApplicationContext
中.
当一个插件变化时,插件不但要有相应地反应,而且有时还会“影响”另外的插件。
以Services 和 Controllers插件为例. 当一个service被重新加载的时候,除非你也重新加载controllers,否则你将加载过的service自动装配到旧的controller类的时候,将会发生问题。.
为了避免这种情况发生,你可以指定将要受到“影响”的另外一个插件,这意味着当一个插件监测到改变的时候,它将先重新加载自身,然后重新加载它所影响到的所有插件。看 ServicesGrailsPlugin
的代码片段:
def influences = ['controllers']
如果你想观察一个特殊的插件的变化但又不需要监视插件的资源,那你可以使用"observe"属性:
def observe = ["hibernate"]
在此示例中,当一个Hibernate的领域类变化的时候,你将收到从hibernate插件传递过来的事件。你也可以使用一个通配符查看所有加载的插件:
def observe = ["*"]
Logging plugin不仅如此,当应用运行时它都能添加 log
属性到 任何 插件库。
插件经常依赖于其他已经存在的插件,并且也能调整这种依赖. 为了做到这点,一个插件可以定义两个属性,首先是 dependsOn
.让我们看看Grails Hibernate插件的代码片段:
class HibernateGrailsPlugin { def version = 1.0 def dependsOn = [dataSource:1.0, domainClass:1.0, i18n:1.0, core: 1.0]}
如上述示例所演示的,Hibernate插件依赖于4个插件: dataSource
, domainClass
, i18n
和 core
.
根本上讲,这些被依赖的插件将先被加载,接着才是Hibernate插件,如果这些被依赖的插件没有加载,那么Hibernate也不会加载。
dependsOn
属性也支持一个小型的表达语言指定版本范围。以下是一些简单的语法例子:
def dependsOn = [foo:"* > 1.0"] def dependsOn = [foo:"1.0 > 1.1"] def dependsOn = [foo:"1.0 > *"]
当使用*通配符的时候,它表示"任何"版本。 The expression syntax also excludes any suffixes such as -BETA, -ALPHA etc. so for example the expression "1.0 > 1.1" would match any of the following versions:
如果所依赖的插件不能被解析的话,则依赖于此的插件将被放弃并且不会被加载,这就是所谓的“强”依赖。然而我们可以通过使用 loadAfter来定义一个“弱”依赖,示例如下:
def loadAfter = ['controllers']
此处如果 controllers
插件存在的话,插件将在controllers之后被加载,否则的话将被单独加载. 插件也可以适应于其他已存在的插件,以Hibernate插件的 doWithSpring
闭包代码为例:
if(manager?.hasGrailsPlugin("controllers")) { openSessionInViewInterceptor(OpenSessionInViewInterceptor) { flushMode = HibernateAccessor.FLUSH_MANUAL sessionFactory = sessionFactory } grailsUrlHandlerMapping.interceptors << openSessionInViewInterceptor }
这里,controllers
插件如果被加载的话,Hibernate插件仅仅注册一个OpenSessionInViewInterceptor
变量manager是 GrailsPluginManager interface 接口的一个实例,并且提供同其他插件交互的方法,而且 GrailsPluginManager
本身存在与任何一个插件中。