构建简单的 C++ 服务组件,第 1 部分: 服务组件体系结构 C++ API 简介
Ed Slattery (slattery@uk.ibm.com), 软件工程师, IBM UK
Pete Robbins (slattery@uk.ibm.com), 软件工程师, IBM UK
Andrew Borley (borley@uk.ibm.com), 软件工程师, IBM UK
2006 年 12 月 12 日
- 内容
构建并连接简单的 C++ 服务组件
关于 Tuscany
Apache Tuscany 是正处于孵化期的 Apache Software Foundation 项目。此项目的目标之一是获得实现以下服务组件体系结构(Service Component Architecture,SCA)规范的 C++ 运行时(有关更多信息,请参见参考资料):
- SCA 组装模型 (SCA Assembly Model)
- SCA C++ 客户机和实现 (SCA C++ Client and Implementation)
在本文中,我们将详细说明采用 C++ 针对 Apache Tuscany C++ 运行时开发和部署服务组件的步骤。
介绍
Tuscany C++ 服务组件体系结构 (SCA) 运行时允许使用标准 C++ 代码构建 SCA 组件,并将其部署到 SCA 运行时可以查找和加载的位置。为了实现此组件动态加载功能,运行时需要一系列描述文件,这些构件以及您自己的头文件一起用于生成代理和包装,以允许从其他组件或客户机代码采用与处理本地 C++ 对象类似的方式调用您的组件。
我们将首先创建一个简单的 SCA 组件,然后创建第二个组件,并将二者连接到一起。
我们使用 Microsoft Visual Studio 作为开发环境,但也可以使用命令行编译器和文本编辑器。您将了解如何设置 Studio 项目和开发应用程序。
注意:Tuscany SCA 依赖于 Tuscany SDO 项目和 Apache Axis2/C 项目。在开始工作前,必须确保在您的 PATH 环境变量中设置了 Tuscany SCA/SDO 库和 Apache Axis 库。有关更多信息,请参见项目下载说明。
Tuscany SCA C++ 运行时将需要知道模块和组件所部署到的位置。部署根目录使用环境变量 TUSCANY_SCACPP_SYSTEM_ROOT 进行标识。我们将马上设置此变量,以便能够从 Visual Studio 内运行我们的测试程序。如果您使用的是命令行,则在运行前都不需要设置这些内容。
TUSCANY_SCACPP_SYSTEM_ROOT 指定运行时将用于寻找已部署模块和子系统的路径,我们将在稍后对此予以说明。根目录必须具有两个子目录,分别名为“modules”和“subsystems”。
使用控制面板设置:TUSCANY_SCACPP_SYSTEM_ROOT=c:\mybasicsample。
转到“控制面板”、“系统”,然后选择“高级”选项卡,然后单击“环境变量”按钮。单击“新建”按钮,并将“变量名”设置为 TUSCANY_SCACPP_SYSTEM_ROOT,将“变量值”设置为 c:\mybasicsample。然后单击“确定”,以设置此环境变量。
创建名为 mybasicsample 的目录,其中包含两个子目录,分别名为 modules 和 subsystems。
现在已经准备好,可以进行部署了。我们可能应该编写一些能够部署的东西。
简单回顾一下 SCA 规范(您已经读过了此规范——对吗?),就会记得 SCA 系统包含一个或多个子系统。每个子系统包含模块组件的列表。每个模块组件实际上是由模块实现的。在 C++ 中,存在一组描述性 XML 文件,用于在编译时生成服务代理和包装以及在运行时查找提供的服务。在开始进行开发前,有必要对这些文件进行一下了解。描述子系统的文件必须命名为 sca.subsystem,且必须保存在自己的子目录中,其位于根目录中的 subsystems 目录下。sca.subsystem 文件描述子系统中涉及哪些模块组件。模块组件可以视为子系统的简单部件,模块组件具有名称,且同时指示实现模块组件行为的模块:
清单 1. 模块组件
<subsystem xmlns="http://www.osoa.org/xmlns/sca/0.9" name="MyServiceSubsystem">
<moduleComponent name="MyModuleComponent" module="FloatConverter" />
</subsystem>
清单 1 告知 SCA 运行时,模块组件“MyModuleComponent”是由名为“FloatConverter”的模块实现的,因此我们必须构建此模块。
sca.subsystem 文件实际上是运行时构件,编译时没有用处。其他文件(组件类型文件和 sca.module 文件)对“FloatConverter”模块进行描述,以便能在运行时找到它。这些文件还帮助代码生成器为服务构建包装和代理。我们将在下面的开发过程中更为详细地讨论这些文件。
现在让我们回到这一过程的开头。我们希望将 C++ 类作为服务部署,并将服务放入名为“FloatConverter”的模块中。以下步骤将说明如何完成此任务。
首先,尽管可能有存在冗余的风险(很多人肯定之前已经进行过此工作了),我们将创建一个示例 C++ 应用程序。
注意:在开始开发过程前,您必须下载 SCA/SDO 代码并对其进行构建,或下载二进制版本,以便稍后告知项目在何处查找 SCA 运行时。设置两个环境变量,分别名为 TUSCANY_SCACPP 和 TUSCANY_SDOCPP,指向 SCA 和 SDO 项目的部署目录,在这两个目录下存在相应的 bin、lib 和 include 目录。
首先,我要创建一个抽象基类,用于表示我们要公开的服务。这与定义 Java 接口等价。我们在此处创建的头文件将由客户机应用程序用于解释可用的服务接口。
以下就是这个类,位于名为“Example.h”的头文件中:
清单 2. 类示例
class Example
{
public:
// we will get a float from a string
virtual float toFloat(const char* input) = 0; // we will convert a float to a string
virtual char* toString(float value) = 0;
};
我们需要告知代码生成器,哪个组件将公开此抽象行为,因此我们创建了一个组件类型文件。组件类型文件通过命名约定将实现类的名称与抽象行为联系起来。此文件根据实现类命名,包含对抽象类的引用,因此,在本例中,该文件将被称为“ExampleImpl.componentType”:
清单 3. 组件类型文件
<?xml version="1.0" encoding="ASCII"?>
<componentType xmlns="http://www.osoa.org/xmlns/sca/0.9"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<service name="ExampleService">
<interface.cpp header="Example.h">
</interface.cpp>
</service>
</componentType>
清单 3 告知代码生成器,一个名为“ExampleService”的组件将公开头文件“Example.h”中的行为。它还告知运行时,头文件“ExampleImpl.h”实现的服务是 ExampleService。
因此,在 Visual Studio 中,我们创建了一个 win32 dll 项目,并在其中插入了上面的头文件。我们将此项目命名为“TheExampleProject”,因此我们预期它缺省生成名为“TheExampleProject.dll”的 dll。现在为服务创建实现,相应的文件自然就命名为 ExampleImpl.cpp 和 ExampleImpl.h:
清单 4. 服务实现
#include "Example.h" class ExampleImpl : public Example
{
public:
ExampleImpl();
virtual ~ExampleImpl(); // Example interface
virtual float toFloat(const char* input); virtual char* toString(float value); }; #include "ExampleImpl.h"
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h> ExampleImpl::ExampleImpl()
{
} ExampleImpl::~ExampleImpl()
{
} // Example interface float ExampleImpl::toFloat(const char* input)
{
if (input != 0)
{
float f = (float)atof(input);
return f;
}
return (float)0;
} char* ExampleImpl::toString(float value)
{
char * r = new char[100];
sprintf(r,"The float is : %5.5f", value);
return r;
}
此时将编译和链接 DLL,因此可以编写测试程序来使用 Example API 并对其进行测试。请跳过这一步骤,因为这里面实际包含的都是标准内容。
现在我们将要介绍 jigsaw 剩下的各个部分,以将“floatConverter”模块链接到我们所编写的代码。
SCA 运行时需要知道服务所在的位置。使用运行时来在客户机代码中查找模块上下文,并调用“locateService”。运行时可以采用两种方式确定缺省模块上下文:
- 通过环境变量 TUSCANY_SCACPP_DEFAULT_MODULE=<subsystemname>/<modulecomponentname>,因此在本例中,此变量将为:TUSCANY_SCACPP_DEFAULT_MODULE=MyServiceSubsystem/MyModuleComponent。
- 客户机代码可以使用 TuscanyRuntime 类指定缺省模块。这是我们在本示例中使用的方法。
现在运行时只需要在模块中查找服务,并找到相关 DLL 的名称即可。这将通过读取“sca.module”文件来完成,如清单 5 中所示。
清单 5. 读取 sca.module 文件
<?xml version="1.0" encoding="ASCII"?>
<module xmlns="http://www.osoa.org/xmlns/sca/0.9"
xmlns:v="http://www.osoa.org/xmlns/sca/values/0.9"
name="FloatConverter">
<component name="ExampleService"> <implementation.cpp dll="TheExampleProject.dll" header="ExampleImpl.h">
</implementation.cpp>
<properties>
</properties>
<references>
</references>
</component>
</module>
我们现在知道 FloatConverter 模块包含名为“ExampleService”的组件。ExampleService 在 TheExampleProject.dll 中实现,其方法在头文件“ExampleImpl.h”中描述。
我们现在应该将这些 sca.module 和 sca.subsystem 文件添加到我们的 Studio 项目中,以便对其进行跟踪。
我们的 SCA 运行时现在具有了查找服务和调用方法所需的所有信息。不过,不能编写直接调用服务的客户机,否则就会在编译时产生对 dll 的依赖关系,因此需要可调用的代理,而库需要对服务进行包装的包装类。这些将由我们的 scagen 实用工具生成。scagen 将获取 sca.module 文件中定义的头文件,随后构建所需代码。您只需要运行它,告知在何处查找相应的文件以及将输出写入到何处。让我们假定 sca.module 文件和项目的源文件位于 c:\mybasicsample\Example 中。在目录 c:\mybasicsample 中键入 scagen -dir Example -output Example。
注意:首先,当然您将需要获得 scagen,并需要知道 scagen 是一个 Java 应用程序。如果下载二进制代码,则 scagen 已经位于“bin”目录中,否则就需要对其进行构建。要构建 scagen,只需要安装 Java JDK(1.4.2 或更高版本),并安装 apache ant 即可。转到 sca 项目中的 tools/scagen 目录,并键入“ant”。将会在 bin 目录中构建 scagen 实用工具。
将按照清单 6 中所示的命名约定生成四个文件。
清单 6. scagen 实用工具
<headername>_<servicename>_Proxy.cpp/.h and
<headername>_servicename>_Wrapper.cpp/.h
将这些新文件添加到您的 Visual Studio 项目中。
现在您的项目依赖于 sca,在告知在何处查找 sca 头文件和库之前不会进行构建。将 $(TUSCANY_SCACPP)/include 和 $(TUSCANY_SDO)/include 添加到所包括的头文件(project、settings、C++、preprocessor 及其他 include 目录)的路径中。另将 tuscany_sdo.lib 和 tuscany_sca.lib 添加到 libraries (link/input/Object/library modules)。还要将 $(TUSCANY_SCACPP)\lib,$(TUSCANY_SDOCPP)\lib 添加为额外库路径。
现在要再次编译您的项目,接下来就可以开始编写客户机了。
客户机程序将位于新的控制台可执行项目中,因此可以直接创建一个此类项目。环境依赖于 sca 运行时,但显然一定不依赖于 dll 项目。不过,客户机并不需要知道提供了哪些方法和服务,因此,最终我们要使用在开始时定义的抽象基类。应该将 Example.h 头文件添加到客户机项目中。客户机项目依赖于 SCA 运行时,因此我们还必须像处理 dll 项目时一样添加所有的库和头文件。
现在创建客户机 cpp 文件:
清单 7. 客户机 cpp 文件
#include "..\MyServiceProject\Example.h"
#include "osoa/sca/sca.h"
#include <iostream>
#include <stdlib.h>
#include <tuscany/sca/core/TuscanyRuntime.h> using namespace osoa::sca;
using namespace std; int main(int argc, char* argv[])
{ if (argc != 2)
{
cout << "MyClient.exe: Would you cast me adrift without my float?" << endl;
return 0;
} // Set the default moduleComponent
TuscanyRuntime runtime("MyServiceSubsystem/MyModuleComponent"); try {
runtime.start(); // bootstrap the Tuscany Runtime // Get the current module context - (which is the default)
ModuleContext myContext = ModuleContext::getCurrent();
// Get an example service
Example *theService = (Example*) myContext.locateService("ExampleService");
if (theService == 0) {
cout << "MyClient.exe: Unable to find MyFloatService" << endl;
}
else {
try {
float result = theService->toFloat(argv[1]);
cout << "The float returned is " << result << endl;
char *str = theService->toString(result + 1);
cout << "The string came back as " << str << endl;
}
catch (char* x) {
cout << "MyClient.exe: exception caught: " << x << endl;
}
}
}
catch (ServiceRuntimeException& ex) {
cout << "MyClient.exe: runtime exception caught: " << ex << endl;
}
runtime.stop(); // stop the Tuscany Runtime
return 0;
}
这样就完成了创建过程。您的第一个 SCA 应用程序会从客户机调用单个服务。要部署它,必须将运行时构件放入部署目录中,如下所示:
对于 TUSCANY_SCACPP_SYSTEM_ROOT/modules/Example 目录:
- * Example.h 头文件
- * ExampleImpl.h 头文件
- * TheExampleProject.dll
- * ExampleImpl.componentType
- * sca.module
对于 TUSCANY_SCACPP_SYSTEM_ROOT/subsystems/Example 目录:
- * sca.subsystem
现在 tuscany_sca.dll、tuscany_sdo.dll 和 axis2 dll 都已位于路径上的某个位置,且已设置了 TUSCANY_SCACPP_SYSTEM_ROOT 环境变量,您的应用程序将能够运行了。
您已构建了一个组件,但单个组件并没有很大的用处。您不会买了一个电阻,然后说“哦,快看,电流减小了”。您希望做一台收音机,那么让我们讨论一下如何进行连接吧。
服务可能调用其他服务,这些服务可以连接到一起,以便让运行时按照与处理客户机调用相同的方式解决服务使用的问题。其中的主要概念就是组件上下文。我们已经了解到运行时具有模块上下文,可通过其找到服务。而在服务中,还存在组件上下文,运行时可通过其查找当前服务所依赖的服务。
在本系列的后续文章中,您将了解到这些服务可以作为 Web 服务公开,同样,我们的 sca 服务也可以依赖于传入 Web 服务,但目前我们只是要将两个在相同应用程序空间内运行的服务连接到一起。
首先,我们需要创建第二个服务。并没有必要详细说明如何完成此工作。我们只需要创建一个返回字符串的简单服务:
清单 8. 简单服务示例
class StringThing
{
public: // we will get a string
virtual char* getString() = 0;
}; #include "StringThing.h" class StringThingImpl : public StringThing
{
public:
StringThingImpl();
virtual ~StringThingImpl(); // interface virtual char* getString(); }; #include "StringThingImpl.h"
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h> StringThingImpl::StringThingImpl()
{
} StringThingImpl::~StringThingImpl()
{
} // interface char* StringThingImpl::getString()
{
char * r = new char[100];
sprintf(r,"The string from stringthing");
return r;
}
现在,再次使用 scagen 前,我们需要为新组件创建新组件类型文件,还需要将新组件添加到 sca.module 文件中。最后,我们还需要告知系统,我们的第一个组件类型将引用第二个组件类型。这是通过更改 ExampleImpl.componentType 文件来实现的。
以下是新组件类型 (StringThingImpl.componentType):
清单 9. StringThingImpl.componentType
<?xml version="1.0" encoding="ASCII"?>
<componentType xmlns="http://www.osoa.org/xmlns/sca/0.9"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<service name="StringService">
<interface.cpp header="StringThing.h">
</interface.cpp>
</service>
</componentType>
以下是更改后的 sca.module 文件:
清单 10. 更改后的 sca.module 文件
>
<?xml version="1.0" encoding="ASCII"?>
<module xmlns="http://www.osoa.org/xmlns/sca/0.9"
xmlns:v="http://www.osoa.org/xmlns/sca/values/0.9"
name="FloatConverter">
<component name="ExampleComponent">
<implementation.cpp dll="TheExampleProject.dll" header="ExampleImpl.h">
</implementation.cpp>
<properties>
</properties>
<references>
<stringService>StringThing/StringService</stringService>
</references>
</component>
<component name="StringThing">
<implementation.cpp dll="TheExampleProject.dll" header="StringThingImpl.h">
</implementation.cpp>
<properties>
</properties>
<references>
</references>
</component>
</module>
以下是更改后的 ExampleImpl.componentType 文件:
清单 11. 更改后的 ExampleImpl.componentType
<?xml version="1.0" encoding="ASCII"?>
<componentType xmlns="http://www.osoa.org/xmlns/sca/0.9"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<service name="ExampleService">
<interface.cpp header="Example.h">
</interface.cpp>
</service> <reference name="stringService">
<interface.cpp header="StringThing.h">
</interface.cpp>
</reference> </componentType>
请注意,我们使用了小写的“stringService”来引用大写的服务“StringService”。再次运行 scagen 时,会发现创建了两个额外的文件。这些是从 ExampleImpl 调用时“stringService”引用的代理头文件和 cpp 文件。您将需要把这两个新文件添加到项目中。
最后,以下是 Exampleservice 的新代码,其将使用组件上下文解析新服务:
清单 12. Exampleservice
char* ExampleImpl::toString(float value)
{
char * r = new char[100];
// now make a service call to stringthing...
try {
ComponentContext myContext = ComponentContext::getCurrent();
StringThing* stringService = (StringThing*)myContext.getService("stringService"); if (stringService == 0)
{
cout << "unable to find string thing service" << endl;
}
else
{
char* chars = stringService->getString();
if (chars != 0)
{
sprintf(r,"%s and the float is %5.5f",chars,value);
delete chars;
return r;
}
}
}
catch (ServiceRuntimeException& e)
{
cout << "Errror from service: " << e << endl;
// .. just carry on
} sprintf(r,"The float is : %5.5f", value);
return r;
}
请记住,由于我们有了一个新服务,因此必须部署新运行时文件:
- StringThingImpl.h
- StringThing.h
- StringThingImpl.componentType
当然,dll 和模块文件都发生了变化,因此也需要复制这些文件。
结束语
到此为止,我们的第一个连接好且正常工作的 SCA 子系统已经完成。图 1 将帮助您回顾解析服务名称的过程。我们希望这个示例能让您对 SCA 的强大功能有所了解。作为练习,您可以向组件添加一些属性,或尝试重新将 StringThing 打包为其他 dll。
在下一篇文章中,我们将讨论入站和出站 Web 服务以及与 Axis2 的关系。
图 1. 服务布局
参考资料
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
- Apache Tuscany
- SCA Assembly Model
- SCA C++ Client and Implementation