WebObjects/Web 服务/Web 服务提供程序
WebObjects 既支持作为生产者也支持作为消费者使用 Web 服务,并且一旦您弄清楚如何正确配置,它实际上运行得非常好。希望此演练可以帮助您启动此过程。
以下是使用 WebObjects 和 Eclipse/WOLips 设置 Web 服务生产者的基本步骤
- 创建一个新的 WOApplication 项目
- 编辑项目的构建路径,然后转到“库”选项卡
- 从 /Library/WebObjects/Extensions 添加以下外部 jar 文件。
- axis.jar
- commons-logging.jar
- commons-discovery.jar
- wsdl4j.jar
- saaj.jar
- jaxrpc.jar
- 编辑 WO Frameworks 集合,并从系统框架中添加 JavaWebServicesSupport 框架
- 从 /Library/WebObjects/Extensions 添加以下外部 jar 文件。
- 创建一个类来保存您的 Web 服务方法。这些方法不需要是静态的,并且可以同时将复杂类型作为参数并返回复杂类型作为返回值。目前,只需返回基本类型和/或字符串。
- 编辑您的应用程序类,并添加 WOWebServiceRegistrar.registerWebService("PublishedNameOfYourWebService", NameOfTheClassYouJustMade.class, true);
就是这样。现在,当您启动应用程序时,您可以请求 http://yourserver.com/cgi-bin/WebObjects/YourApp.woa/ws/PublishedNameOfYourWebService?wsdl,它将返回您可以与任意数量的 Web 服务客户端一起使用的自动生成的 WSDL 文档,以与您的服务器交互。
现在的问题是复杂类型。返回复杂类型是可以的,但是您必须为引用的每个复杂类型注册序列化程序和反序列化程序类。如果不这样做,服务器将尝试使用 ArraySerializer 序列化您的对象(您将在服务器上看到此异常),并且客户端将抱怨与 SYSTEMID 相关的无意义错误(必须喜欢糟糕的错误处理!)。解决方法是对您的每个复杂类型,在您的应用程序构造函数中调用以下方法
WOWebServiceRegistrar.registerFactoriesForClassWithQName(new BeanSerializerFactory(_class, _qName), new BeanDeserializerFactory(_class, _qName), _class, _qName);
其中 _class 是表示复杂类型的 Class 对象,_qName 是类在 WSDL 文档中显示的 QName(完全限定名称)。例如,如果您创建了一个名为 Person 的复杂返回类型,并且它位于 com.yourserver.service 包中,则 _class 将为 com.yourserver.service.Person.class,_qName 将为 new QName("http://service.yourserver.com", "Person")。请注意,命名空间是您包名称的反向。您需要为引用的每个参数和返回类型调用此方法。
如实说,我不知道为什么要手动执行此步骤 - WSDL 是自动生成的,因此它知道类及其 QName WSDL 映射,但我无法在没有此步骤的情况下使事情正常工作。如果有人知道原因或解决方法,请更新本文。
通过这些注册,您现在应该能够使用任何标准 Web 服务客户端(Axis、.NET 等)与 WO 通信。
您可能已注意到,在 Web 服务方法中,您没有传递 WOContext、WORequest、WOSession 和朋友。不用担心。WebServiceRequestHandler 会使用 Axis 的 MessageContext 类来处理此部门的挂钩。您可以使用以下代码获取您的 WOSession
WOContext context = (WOContext)MessageContext.getCurrentContext().getProperty("com.webobjects.appserver.WOContext"); WOSession session = context.session();
或快捷方式
WOSession session = WOWebServiceUtilities.currentWOContext().session();
以下其他键可通过 MessageContext 访问
- "com.webobjects.appserver.WOContext" = 此请求的 WOContext
- "transport.url" = 我/相信/这包含直到查询字符串的完整请求 URL
- org.apache.axis.transport.http.HTTPConstants.MC_HTTP_SERVLETPATHINFO = 包含请求的请求处理程序路径
- "Authorization" = 包含授权标头,以防您需要处理 Kerberos/SPNEGO 等内容。
- "remoteaddr" = 包含请求的远程地址
如果您使用 Axis 使用 WO Web 服务,请注意存在一个未解决的错误(至少从 2003 年开始),Axis 默认不支持向服务器传递多个 Cookie。WO 发送 woinst 和 wosid,因此您在返回服务器的行程中丢失了客户端的会话 ID。这可以通过将 http://issues.apache.org/jira/browse/AXIS-1059 中的补丁应用到客户端的 axis.jar 来修复。Axis 1.1 已在 Apache 中存档,但您可以从 http://archive.apache.org/dist/ws/axis/1_1/ 下载源代码。补丁没有完美应用。有两个被拒绝的块,但修复拒绝应该非常明显(补丁有两个 System.out.printlns,它声称在原始源代码中但实际上没有)。修复此问题后,您可以设置服务器的 WOSession 上的 setStoreSessionIdInCookies(true) 并设置客户端的 ServiceLocator 上的 setMaintainSessions(true),然后您就可以开始了。
此 Axis 错误似乎在最新版本的 Axis(包括 1.4 版)中已修复。尝试升级 WO Web Services 服务器上的 Axis 版本不太可能是一个愉快的体验(并且升级 Direct To Web Services 客户端中的 Axis 也可能不是 - 尽管我还没有尝试过)。但是,似乎可以在打算使用 WSDL2Java 生成的类连接到远程 Web 服务服务器的 WebObjects 应用程序的类路径上使用更高版本的 Axis jar 文件 - 假设 WSDL 中不包含任何 WebObjects 类。在这种情况下,重要的是您使用匹配版本的 WSDL2Java。
使用 WebServicesCore 与 WebObjects 结合时,存在一些复杂情况,所有这些都源于 WSMakeStubs 生成的代码。使用 WSMakeStubs 生成的代码后,您将遇到以下需要在其代码中修复的问题
Apple 提供了一个名为 WSMakeStubs 的程序,它类似于 Axis 中的 WSDL2Java,只是它很糟糕。但是,它至少可以为您构建 Web 服务客户端代码提供一个起点,并且通过以下概述的更改,您可以最终获得不错的客户端 API。
运行 WSMakeStubs 非常简单
/Developer/Tools/WSMakeStubs -x ObjC -name NameOfServiceClass -url http://yourserver.com/cgi-bin/WebObjects/YourWOA.woa/ws/YourService?wsdl
这将生成您可以用来调用 Web 服务的 Objective-C 代码。与 Axis 相反,WSMakeStubs 为您的服务生成无状态代码(即没有会话跟踪或 Cookie 支持 - 只有 Web 服务每个方法的静态方法)。您需要调用的所有方法都出现在 NameOfServiceClass.m 的末尾。WSMakeStubs 还生成 WSGeneratedObj.m,其中包含更低级别的 Web 服务核心调用。
WSMakeStubs 中的另一个错误与没有返回值的方法有关。对于 void 方法,WSMakeStubs 实际上从未调用过这些方法。如果您查看 returnValue 方法的代码,您会发现它从未调用 [super getResultDictionary]。问题在于 [super getResultDictionary] 是实际执行 Web 服务方法的代码。只需将 void 方法的定义更改为
- (id) resultValue { return [self getResultDictionary]; }
一切都会按计划进行。
WSGeneratedObj 基本上没有错误。但是,需要进行一些更改才能修复它生成的内存泄漏(来自 cocoadev.com)
在 getResultDictionary 的末尾,添加
if (fRef) { // new code WSMethodInvocationSetCallBack(fRef, NULL, NULL); // new code } // new code return fResult; // original code
现在显示使用的 NSURL 被释放了两次,可以通过从 createInvocationRef 中删除一行来修复
NSURL* url = [NSURL URLWithString: endpoint]; if (url == NULL) { [self handleError: @"NSURL URLWithString failed in createInvocationRef" errorString:NULL errorDomain:kCFStreamErrorDomainMacOSStatus errorNumber:paramErr]; } else { ref = WSMethodInvocationCreate((CFURLRef) url, (CFStringRef)methodName, (CFStringRef) protocol); // [url release]; remove this line ....
我想要在生成的代码中进行的另一个更改是从调用服务的代码中删除硬编码的服务 URL 并将其传递进来(类似于 Axis 的方式)。这应该是一个相当简单的更改,但我想记下如何进行。您通常希望使用相同的代码与开发服务器和生产服务器通信,因此,您希望该变量可以参数化。
WSMakeStubs 没有提供直接支持来传递复杂类型 - 您只能获得 NSDictionary,并且您只能发送回 NSDictionary,而没有关于这些字典中究竟是什么的说明。
要将复杂类型发送回 WO,您必须在字典中设置以下键
[dictionary setObject:@"http://extranet.mdtask.mdimension.com" forKey:(NSString *)kWSRecordNamespaceURI]; [dictionary setObject:@"WSCompany" forKey:(NSString *)kWSRecordType];
其中 kWSRecordNamespaceURI 的值为正在传递的复杂对象的类型的 XML 命名空间,kWSRecordType 的值为类型的名称。在 WO 端,命名空间将是类型类名的反向,记录类型将是类名。例如,在上面的示例中,服务器上的实际类名为 com.mdimension.mdtask.extranet.WSCompany。
字典的其余部分包含 attribute=>value 映射。例如,上面示例中的 WSCompany 具有“name”属性,因此字典还包含一个映射到相应值的“name”键。
从 Cocoa 发送 NSDictionary 实例时,WO 会触发 WOGlobalIDDeserializer,它不会正确解析 nsdictionary 或 nsarray,看来 WO 端没有这些类的默认反序列化器。
一种解决方案是在
@implementation NSObject (NSObject_WOXML) - (NSString*)xmlPlist { NSString* error; NSData* data = [NSPropertyListSerialization dataFromPropertyList:self format:NSPropertyListXMLFormat_v1_0 errorDescription:&error]; return [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]; } @end
Cocoa 端添加,然后在编译 WSMethodInvocationRef 的参数时调用它
然后在 WO 端使用 NSPropertyListSerialization.propertyListFromString(xmlPlist) 重新创建对象。
WSMakeStubs 的另一个问题是它不会生成用于检索 WO Web 服务返回值的有效标识符。在生成的代码中,您会看到类似以下内容
- (id) resultValue { return [[super getResultDictionary] objectForKey: @"getBillableCompaniesReturn"]; }
但是,实际的返回值名称需要包含其命名空间。该例程的修正版本如下所示
- (id) resultValue { return [[super getResultDictionary] objectForKey: @"ns1:getBillableCompaniesReturn"]; }
注意键以“ns1:”开头。此值应与 WSDL 中显示的值匹配。
这是一个我根据上面的 WSCompany 示例使用的示例类型包装器。在 WSMakeStubs 创建的包装 Web 服务方法的静态方法中,我只是使用结果字典从 Web 服务中初始化此类型,并返回 WSCompany 的实例,而不是字典。当我发送这些对象中的一个时,我只是在包装器方法中发送 [wsCompany dictionary]。
@interface WSCompany : NSObject { NSMutableDictionary *myDictionary; } -(id)initWithDictionary:(NSDictionary *)_dictionary; -(NSDictionary *)dictionary; -(NSString *)name; -(NSString *)companyID; @end
@implementation WSCompany -(id)initWithDictionary:(NSDictionary *)_dictionary { self = [super init]; myDictionary = [[_dictionary mutableCopy] retain]; [myDictionary setObject:@"http://extranet.mdtask.mdimension.com" forKey:(NSString *)kWSRecordNamespaceURI]; [myDictionary setObject:@"WSCompany" forKey:(NSString *)kWSRecordType]; return self; } -(void)dealloc { [myDictionary release]; [super dealloc]; } -(NSDictionary *)dictionary { return myDictionary; } -(NSString *)name { return [myDictionary objectForKey:@"name"]; } -(NSString *)companyID { return [myDictionary objectForKey:@"companyID"]; } @end
WSMakeStubs 没有正确处理错误,但它在字典中。在 +resultForInvocation: 中,我添加了几行代码来检查并返回错误
+ (id) resultForInvocation:(WSGeneratedObj*)invocation; { result = [[invocation resultValue] retain]; // Added check if a fault occurred and return the fault string if so if([invocation isComplete]) { if([invocation isFault]) { result = [[invocation getResultDictionary] valueForKey:@"/FaultString"]; } } // [invocation release]; return result; }
以下是启用 Cookie 支持和 WSMakeStubs 生成的文件的有状态会话所需的代码。此代码还包含更改,以便在 init 方法中提供基本 Web 服务 URL,并允许指定超时值(我将其默认为 30 秒)。对于 WSGeneratedObj.h,添加三个新的成员变量
@interface WSGeneratedObj : NSObject { WSMethodInvocationRef fRef; NSDictionary* fResult; NSDictionary* fCookies; NSString fURLString; int fTimeout; id fAsyncTarget; SEL fAsyncSelector; };
以下是添加到 WSGeneratedObject.m 的新方法
- (id) initWithWebServicesURLString:(NSString*)urlString { if (self = [super init]) { fURLString = [urlString copy]; } return self; } - (NSString*) getWebServicesURLString { return fURLString; } - (NSURL*) getWebServicesURL { return [NSURL URLWithString: [self getWebServicesURLString]]; } - (NSArray*) getReturnedCookies { NSDictionary *results = [self getResultDictionary]; if (nil == results) return nil; CFHTTPMessageRef msgRef = (CFHTTPMessageRef)[results objectForKey: (id)kWSHTTPResponseMessage]; NSDictionary *headers = (NSDictionary*)CFHTTPMessageCopyAllHeaderFields(msgRef); [headers autorelease]; //parse the cookies NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields: headers forURL: [self getWebServicesURL]]; return cookies; } - (void) setCookies:(NSArray*)cookies { [fCookies release]; fCookies = [[NSHTTPCookie requestHeaderFieldsWithCookies: cookies] retain]; WSMethodInvocationSetProperty([self getRef], kWSHTTPExtraHeaders, fCookies); }
- (int)timeoutValue { return fTimeout; } - (void)setTimeout:(int)t { if (t >= 0 && t < 600) fTimeout = 30; }
您需要修改 -dealloc 以释放 fCookies 和 fURLString。以下是我的修改版 getCreateInvocationRef。它被修改为使用上述新的访问器方法获取 URL,从类名获取方法名(这比在每个子类中将其硬编码为类名更有意义),并设置超时。之后是一个通用的 resultValues 方法,以便您的生成子类可以删除其 -resultValues 和 -getCreateInvocationRef 方法 - 它们唯一需要的方法是设置参数。还有一个注释掉的代码行,您可以取消注释它以在结果字典中包含调试信息。在尝试调试复杂对象的传输时,这非常有用。
- (WSMethodInvocationRef) genCreateInvocationRef { WSMethodInvocationRef invRef = [self createInvocationRef /*endpoint*/: [self getWebServicesURLString] methodName: NSStringFromClass([self class]) protocol: (NSString*) kWSSOAP2001Protocol style: (NSString*) kWSSOAPStyleRPC soapAction: @"" methodNamespace: @"http://DefaultNamespace"]; //set a time-out value if (fTimeout > 0) { WSMethodInvocationSetProperty(invRef, kWSMethodInvocationTimeoutValue, (CFTypeRef)[NSNumber numberWithInt: fTimeout]); // WSMethodInvocationSetProperty(invRef, kWSDebugIncomingBody, (CFTypeRef)kCFBooleanTrue); } return invRef; } - (id) resultValue { NSString *key = [NSString stringWithFormat: @"ns1:%@Return", NSStringFromClass([self class])]; return [[self getResultDictionary] objectForKey: key]; }
要使用有状态服务,请在第一个请求后调用 getReturnedCookies 并存储 Cookie 字典。然后在所有后续 Web 服务调用中使用该字典调用 setCookies:。根据您使用的 Cookie,您可能希望在每次请求后保存 Cookie 字典的新副本。