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