本教程将指导你创建一个XWiki组件,这是一种方式来扩展或定制XWiki platform。事实上,XWiki platform是由组件组成,可以通过你的实现来替换默认的实现。也可以添加新的组件来扩展platform,例如通过实现新的渲染宏。
组件目前已经替换旧的插件。
你应该先阅读XWiki组件的参考文档。
在下面的教程中,我们将引导你写一个简单的组件,帮助你快速学习XWiki组件,并解释它是如何工作的。
正如你阅读XWiki组件参考文档,编写一个组件是由三个步骤构成(组件接口,组件实现,注册组件)。
为了让你更容易上手,我们已经创建了一个Maven原型来帮助创建一个简单的组件模块。
在你安装Maven之后,打开一个命令提示符并键入:mvn archetype:generate。
这将列出Maven Central所有可用的原型。相反,如果你想直接使用XWiki组件原型,可以直接输入(版本改为你想要使用的版本):
mvn archetype:generate \ -DarchetypeArtifactId=xwiki-commons-component-archetype \ -DarchetypeGroupId=org.xwiki.commons \ -DarchetypeVersion=5.4.4
然后按照说明进行操作。例如:
[INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building Maven Stub Project (No POM) 1 [INFO] ------------------------------------------------------------------------ [INFO] [INFO] >>> maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom >>> [INFO] [INFO] <<< maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom <<< [INFO] [INFO] --- maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom --- [INFO] Generating project in Interactive mode [INFO] Archetype repository missing. Using the one from [org.xwiki.commons:xwiki-commons-component-archetype:6.1-milestone-1] found in catalog remote Define value for property 'groupId': : com.acme Define value for property 'artifactId': : example Define value for property 'version': 1.0-SNAPSHOT: : Define value for property 'package': com.acme: : Confirm properties configuration: groupId: com.acme artifactId: example version: 1.0-SNAPSHOT package: com.acme Y: : Y [INFO] ---------------------------------------------------------------------------- [INFO] Using following parameters for creating project from Archetype: xwiki-commons-component-archetype:5.4.4 [INFO] ---------------------------------------------------------------------------- [INFO] Parameter: groupId, Value: com.acme [INFO] Parameter: artifactId, Value: example [INFO] Parameter: version, Value: 1.0-SNAPSHOT [INFO] Parameter: package, Value: com.acme [INFO] Parameter: packageInPathFormat, Value: com/acme [INFO] Parameter: package, Value: com.acme [INFO] Parameter: version, Value: 1.0-SNAPSHOT [INFO] Parameter: groupId, Value: com.acme [INFO] Parameter: artifactId, Value: example [INFO] project created from Archetype in dir: /private/tmp/example [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 25.019s [INFO] Finished at: Thu May 29 18:49:46 CEST 2014 [INFO] Final Memory: 11M/26M [INFO] ------------------------------------------------------------------------
然后到创建的目录(在上面的例子中的example)下,运行mvn install编译你的组件。
假设,下面的说明,你所使用的包是com.acme
在组件项目下浏览文件夹,你会看到下面标准的Maven项目结构:
pom.xml src/main/java/com/acme/HelloWorld.java src/main/java/com/acme/internal/DefaultHelloWorld.java src/main/java/com/acme/internal/HelloWorldScriptService.java src/main/resources/META-INF/components.txt src/test/java/com/acme/HelloWorldTest.java
默认创建的文件:HelloWorld为接口(又名component role),它的实现DefaultHelloWorld (component implementation),一个对应组件的测试类HelloWorldTest,组件定义文件components.txt,Maven项目pom.xml文件。HelloWorldScriptService文件用于解释在wiki页面让如何调用组件API。
如果你看下pom.xml文件你会注意到以下依赖:
<dependencies> <dependency> <groupId>org.xwiki.commons</groupId> <artifactId>xwiki-commons-component-api</artifactId> <version>
上面的代码定义了xwiki-core-component-api的依赖,这就是XWiki组件概念的定义。还依赖了xwiki-core-shared-tests,提供辅助类来轻松测试组件。
接口文件(HelloWorld.java)包含一个普通的Java接口的定义,如下所示:
@Role /* annotation used for declaring the service our component provides */ public interface HelloWorld { String sayHello(); }
请记住这个接口指定了其他组件可以在你的组件上使用的API。在我们的例子中,我们将构建一个组件调用sayHello()。
然后我们实现接口,DefaultHelloWorld类。
@Component /* annotation used for declaring a component implementation */ @Singleton /* annotation used for defining the component as a singleton */ public class DefaultHelloWorld implements HelloWorld
请注意,有一个@Named注解来指定一个组件hint。这是有用的,尤其是当我们要区分相同类型组件的若干实现。假想一下,我们有一个特殊的Hello World实现从数据库获取问候信息:
@Component @Named("database") public class DatabaseHelloWorld implements HelloWorld
然后DefaultHelloWorld的sayHello方法(为了简单起见,直接返回Hello world!):
/** * Says hello by returning a greeting to the caller. * * @return A greeting. */ public String sayHello() { return "Hello world!"; }
在components.txt文件注册。
com.acme.internal.DefaultHelloWorld
为了从其他组件访问你的组件,我们使用components engine,指定依赖关系,通过组件管理器(component manager)来处理组件实例化和注入。
为了使用HelloWorld组件,你需要一个组件的引用来使用它。为此,你需要使用一个成员变量,例如,一个Socializer组件中引用:
@Component @Singleton public class DefaultSocializer implements Socializer { [...] /** Will be injected by the component manager */ @Inject private HelloWorld helloWorld; /** Will be injected by the component manager */ @Inject @Named("database") private HelloWorld databaseWorld; [...] }
注意@Inject注解,指示组件管理器在需要的时候注入所需的组件。
这样,你现在就可以在DefaultSocializer类使用helloWorld成员变量,当使用Socializer类时,它由组件管理器分配,在运行时提供HelloWorld组件。如:
public class DefaultSocializer implements Socializer { [...] public void startConversation() { this.helloWorld.sayHello(); [...] } [...] }
注意,所有两个组件之间的调用过程中,我们从来没有看到组件实现,所有是通过roles和interfaces完成:一个服务的实现完全隐藏于外部组件的任何代码。
对于这种用法,因为我们不能使用基于组件的架构优势和组件管理器(component manager)的“神奇”,在XWiki团队已经创建了一个辅助方法就像在组件代码和非组件代码之间的桥梁,com.xpn.xwiki.web.Utils.getComponent(String role, String hint)从组件管理器获取指定组件实例,并返回它。如在前面的章节中看到的,hint是一个可选标识符,附加role,用于区分相同的接口的实现:roles识别服务,而hints有助于实现区分实现。getComponent函数也有一个没有带hint参数,使用的是默认的hint。
要使用我们上面提供的组件,如下调用:
HelloWorld greeter = Utils.getComponent(HelloWorld.class); greeter.sayHello(); HelloWorld databaseGreeter = Utils.getComponent(HelloWorld.class, "database"); greeter.sayHello();
即使该函数返回的对象是DefaultHelloWorld一个实例,你也永远不应该声明你实现类型的对象,也不应该转换为实现。
一个组件是由它的接口表示,对于这样一个服务的实现可以通过任何代码、任何类提供,所以依靠实现类型既不是很好的做法,也是不安全的。在未来,一个Maven enforcer插件将在构建生命周期的设置,因此,任何引用的组件实现(位于“internal”子包)会导致编译错误。
当你需要从非组件化的代码访问的组件时,使用Utils.getComponent()函数是强烈不推荐的。对于组件化的代码,你应该在“编译时”依赖声明(如之前注解所示),如果你需要在运行时解决组件的依赖,使用的ComponentManager,你可以通过实现组合接口访问,如组件模块参考里描述。
组件可以通过编写ScriptService实现wiki页面访问。他们可以使用任何提供的脚本语言访问(velocity, groovy, python, ruby, php等)。
让我们访问sayHello方法:
@Component @Named("hello") @Singleton public class HelloWorldScriptService implements ScriptService { @Inject private HelloWorld helloWorld; public String greet() { return this.helloWorld.sayHello(); } }
注意:我们也可以注入了命名组件,"database",如下
// Or inject a Named Component @Inject @Named("database") private HelloWorld databaseWorld;
使用组件hint(在@Component中的hello),即@Named("hello"),可以从脚本语言访问通过这个名字访问脚本服务。
例如,通过Velocity访问,可以这样写:$services.hello.greet()
通过Groovy:services.hello.greet()
为了让我们的脚本服务可以使用,我们需要把它添加到META-INF/components.txt文件,注册为一个组件:
... com.acme.internal.HelloWorldScriptService
我们还需要让脚本服务在我们classpath可用,需要在pom.xml文件添加以下内容:
<dependency> <groupId>org.xwiki.commons</groupId> <artifactId>xwiki-commons-script</artifactId> <version>${commons.version}</version> </dependency>
遗留意思是旧的XWiki代码还没迁移为组件。
由于XWiki的数据模型(文档,对象,附件等)存在于又大又老的xwiki-core模块,因为我们不希望添加全部core和所有它的依赖为一个简单的轻量级组件的依赖(这将最终导致循环依赖,maven对这是不允许的),当前的策略,使用一个新的组件架构和old xwiki-core之间的bridge,直到数据模型完成变成一个组件。
总之,这工作方式是基于这样的事实,对于一个组件实现不必在同一个.jar作为接口,不依赖组件接口到实际实现。所以,我们做了几个简单的组件,提供给XWiki文档基本的访问,并在xwiki-core声明类作为这些组件的默认实现。
如果你的组件需要访问XWiki的数据模型,需要使用xwiki-platform-bridge模块。请注意,这些接口是相当小的,所以你不能对旧模型做所有的事情。如果你需要bridge添加一些方法,随时在邮件列表提出。
例如:
@Component @Singleton public class DefaultHelloWorld implements HelloWorld { /** Provides access to documents. Injected by the Component Manager. */ @Inject private DocumentAccessBridge documentAccessBridge; [...] private String getConfiguredGreeting() { return documentAccessBridge.getProperty("XWiki.XWikiPreferences", "greeting_text"); }
查询可以通过使用QueryManager,如以下代码:
QueryManager queryManager = (QueryManager) componentManager.getInstance(QueryManager.class); Query query = queryManager.createQuery(xwqlstatement,Query.HQL); List<Object> results = query.execute();
可以通过注解引用一个ComponentManager,请参考组件模块扩展页面。
注意XWiki context已经废弃。这个一种老的方式跟踪当前请求,需要方法到方法传递,就像个枷锁一样遍布每个代码。
在组件的世界里,当前请求信息是在保持Execution Context。这实际上比旧的XWikiContext更加强大,因为它是一个通用的执行环境,持有一个ThreadLocal变量,你可以在随时随地需要的时候创建。
你不需要通过方法调用来手动传递,通过Execution组件管理执行上下文,就像其他XWiki组件一样。
总之,如果你想获得执行上下文(包含了上下文信息),你必须声明的Execution组件(位于xwiki-commons-context模块)并注入,你可以这样写:
/** Provides access to the request context. Injected by the Component Manager. */ @Inject private Execution execution; ... private void workWithTheContext() { ExecutionContext context = execution.getContext(); // Do something with the execution context }
所有这一切说,我们仍然处于一个过渡阶段,大量的信息仍然只能通过旧的XWikiContext还未转移到执行上下文。因此,你可能仍然需要访问旧的XWiki Context。你可以从Execution Context获得它的引用。如果可以,应该尽量不将其转换为一个XWikiContext,把xwiki-platform-oldcore作为一个依赖,作为一个Map。这样做,你将无法访问的所有属性,如当前用户的名称或URL factory,但是你可以访问任何在XWikiContext map里面的内容。
private void workWithTheContext() { ExecutionContext context = execution.getContext(); Map<Object, Object> xwikiContext = (Map<Object, Object>) context.getProperty("xwikicontext"); // Do something with the XWiki context }
如果你需要访问typed信息,那么最简单的就是使用一个Provider如下:
@Inject private Provider<XWikiContext> xwikiContextProvider; ... XWikiContext xcontext = this.xwikiContextProvider.get();
这会提供一个可用XWikiContext(如果没有,当前的ExecutionContext会创建一个)。
从7.2开始,如果在已经有一个情况下你只想获得一个(即你不需要自动创建一个新的),可以使用"readonly"关键字:
@Inject @Named("readonly") private Provider<XWikiContext> xwikiContextProvider; ... XWikiContext xcontext = this.xwikiContextProvider.get(); if (xcontext != null) { ... }
如果你想不只是使用执行上下文,而是使每一个执行上下文提供的东西,你可以创建ExecutionContextInitializer组件的实现,并填充新创建的执行环境,就像velocity上下文。
你可以使用任何其他Maven模块中的外部库,只要在你的模块的pom.xml声明正确的依赖关系。
作为一般规则,你不应该使用任何非组件化XWiki代码,因为这样的旧代码的设计会导致最终依赖整个xwiki-core模块,我们需要避免。如果你正在编写的组件由其它模块所需的,那么这将有可能引起最终循环依赖,这将打破整个编译。
如果你需要old core的一些功能,首先考虑的是重写那部分代码并作为一个新的组件,然后在你的代码使用新的组件。你应该在开发者邮件列表提问,这样我们就可以协同设计和实施。
如果需要的工作量太大,你可以尝试创建一个bridge组件,在一个新的模块只需编写接口,让core类从这些接口的默认实现。然后,因为最终xwiki-core,bridge组件和你的组件将在同一类路径,plexus将耦合正确的类。编写这样的bridge时要小心,因为它们是短暂的(因为最终所有的旧代码将通过适当的组件来代替),如果未来真正的组件将具有不同的接口,那么你将不得不重写你的代码适应新的方法名称,或者更糟的是,新组件的逻辑。
现在我们已经有一个功能组件,让我们编译、部署它到一个XWiki实例。这里有2种方式。
比起手动的方式,使用Extension Manager你不需要重启XWiki实例,这样的话你就不用等待启动的时间。
如果你想重新部署一个扩展程序而已经安装过相同版本,Extension Manager是不会让你这样做的。因此,你需要通过Extension Manager先卸载已有的扩展程序。你还需要使用Extension Tweak移除内存中的元数据。
你的组件现在已经准备好提供服务。