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 字典的新副本。