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 框架集合,并从系统框架中添加 JavaWebServicesSupport 框架
- 从 /Library/WebObjects/Extensions 添加以下外部 jar 文件。
- 创建一个类来保存您的 Web 服务方法。这些方法不需要是静态的,既可以将复杂类型作为参数传递,也可以将复杂类型作为返回值返回。现在,只返回基本类型或字符串。
- 编辑您的 Application 类,并添加 WOWebServiceRegistrar.registerWebService("PublishedNameOfYourWebService", NameOfTheClassYouJustMade.class, true);
就是这样。现在,当您启动应用程序时,您可以请求 http://yourserver.com/cgi-bin/WebObjects/YourApp.woa/ws/PublishedNameOfYourWebService?wsdl,它将返回您可以与任何数量的 Web 服务客户端一起使用的自动生成的 WSDL 文档,以与您的服务器交互。
现在是关于复杂类型的问题。返回复杂类型很好,但是您必须为引用的每个复杂类型注册序列化程序和反序列化程序类。如果您没有这样做,服务器将尝试使用 ArraySerializer 对您的对象进行序列化(您将在服务器上看到此异常),并且客户端将抱怨关于带有 SYSTEMID 的毫无意义的错误(必须喜欢可怕的错误处理!)。解决此问题的办法是,对于您的每个复杂类型,在您的 Application 构造函数中调用以下方法
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 服务服务器中升级 Axis 版本可能不会是一个愉快的体验(直接到 Web 服务客户端中升级 Axis 也可能不会,虽然我还没有尝试过)。但是,使用更高版本的 Axis jar 文件作为打算使用由 WSDL2Java 生成的类连接到远程 Web 服务服务器的 WebObjects 应用程序的类路径似乎是可行的,假设 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。
字典的其余部分包含属性 => 值映射。例如,上面的示例中的 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 服务中将此类型初始化为 initWithDictionary,并返回 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; }
以下是通过 WSMakeStubs 生成的文件启用 cookie 支持和有状态会话所需的代码。此代码还包含更改,以便在 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 字典的新副本。