18.3.2.5. 調用BAPI
SAP系統提供的BAPI的參數結構有個特點:一般會將類似的字段放在同一個結構中,同時,還會存在一個與該結構名類似(后面以X結尾)標識結構,該標識結構中的字段名與賦值的結構中的字段名一致,但是其字段類型只是一個長度為1的字符,用於標識某個字段的數據是否需要通過BAPI來變更
18.3.2.5.1. BAPI事務處理
根據事務的ACID原則,一個獨立的BAPI實現必須具有事務性,同時,BAPI事務模型還必須允許開發者在調用多個BAPI時可以將它們綁定到同一個LUW上。因此,如果同時調用幾個BAPI,開發者需要在程序中進行的事務控制,決定何時執行數據庫提交或回滾操作;而BAPI內部則通常不包含COMMIT WORK和ROLLBACK WORK命令
操作多個BAPI時必須遵循以下原則:
l 如果有更新操作的BAPI,如創建、修改或刪除一個業務對象實例,則對該實例進行另外的讀取操作的BAPI只能訪問上一個COMMIT WORK執行后的最新數據
l 在同一個LUW中,不能對同一個業務對象實例時行超過一次的更新操作。例如,不允許在一個LUW中創建一個新實例,隨后就修改它。但可以創建多個相同的類型的不同實例
在BAPI內部,數據庫更新操作必須通過同步或異步的更新過程實現(需使用 CALL FUNCTION update_function IN UPDATE TASK的方式來更新數據庫),因為否則可能出現不必要的數據庫提交過程,從而破壞了BAPI調用的ACID原則。同樣原因,BAPI內部也不能觸發新的LUW,因而其內部程序代碼中不能包含以下命令:
l CALL TRANSACTION
l SUBMIT REPORT AND RETURN
l COMMIT WORK
l ROLLBACK WORK
因此,BAPI事務中的數據庫提交和回滾必須在主調程序中通過調用SAP標准業務對象BapiService(業務對象類型為SAP0001)的BAPI方法BapiService.TransactionCommit(此BAPI方法實際上還是通過調用BAPI函數BAPI_TRANSACTION_COMMIT來實現的)和BapiService.TransactionRollback(此BAPI方法實際上還是通過調用BAPI函數BAPI_TRANSACTION_ROLLBACK來實現的)來完成
外部程序直接到調用BapiService.TransactionCommit方法,才會觸發BAPI方法中的數據庫提交
對於BAPI的操作都要用BAPI_TRANSACTION_COMMIT來提交的,在提交前,要根據BAPI函數的執行返回參數RETURN來判斷函數是否執行成功(RETURN中是否有E類消息),如果有錯誤消息則要用BAPI_TRANSACTION_ROLLBACK取消所做的操作,而不是COMMIT WORK,如:
CALL FUNCTION 'BAPI_FIXEDASSET_CHANGE'
...
IMPORTING
return = return.
IF return-type <> 'S'.
CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
ELSE.
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
EXPORTING
wait = 'X'.
ENDIF.
另外建議在調用BAPI_TRANSACTION_COMMIT函數進行提交BAPI操作時,加上wait參數,這樣直到BAPI函數中的數據庫操作提交數據庫后,才去執行其后面的語句,這樣后面程序依賴於此提交的數據時就不會出問題:
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
EXPORTING
wait = 'X'.
WAIT為X時,會執行COMMIT WORK AND WAIT語句,否則執行COMMIT WORK語句
18.3.2.5.2. 外部系統(Java)調用BAPI函數
18.3.2.5.2.1. 直連、連接池
import java.io.File;
import java.io.FileOutputStream;
import java.util.Properties;
import com.sap.conn.jco.JCoDestination;?dest??ne??n
import com.sap.conn.jco.JCoDestinationManager;
import com.sap.conn.jco.JCoException;
import com.sap.conn.jco.ext.DestinationDataProvider;
publicclass ConnectNoPool {// 直連方式,非連接池
// 連接屬性配置文件名,名稱可以隨便取
static String ABAP_AS = "ABAP_AS_WITHOUT_POOL";
static {
Properties connectProperties = new Properties();
connectProperties.setProperty(DestinationDataProvider.JCO_ASHOST,
"192.168.111.137");
connectProperties.setProperty(DestinationDataProvider.JCO_SYSNR, "00");
connectProperties
.setProperty(DestinationDataProvider.JCO_CLIENT, "800");
connectProperties.setProperty(DestinationDataProvider.JCO_USER,
"SAPECC");
// 注:密碼是區分大小寫的,要注意大小寫
connectProperties.setProperty(DestinationDataProvider.JCO_PASSWD,
"sapecc60");
connectProperties.setProperty(DestinationDataProvider.JCO_LANG, "en");
// *********連接池方式與直接不同的是設置了下面兩個連接屬性
// JCO_PEAK_LIMIT - 同時可創建的最大活動連接數,0表示無限制,默認為JCO_POOL_CAPACITY的值
// 如果小於JCO_POOL_CAPACITY的值,則自動設置為該值,在沒有設置JCO_POOL_CAPACITY的情況下為0
connectProperties.setProperty(DestinationDataProvider.JCO_PEAK_LIMIT,
"10");
// JCO_POOL_CAPACITY - 空閑連接數,如果為0,則沒有連接池效果,默認為1
connectProperties.setProperty(
DestinationDataProvider.JCO_POOL_CAPACITY, "3");
// 需要將屬性配置保存屬性文件,該文件的文件名為 ABAP_AS_WITHOUT_POOL.jcoDestination,
// JCoDestinationManager.getDestination()調用時會需要該連接配置文件,后綴名需要為jcoDestination
createDataFile(ABAP_AS, "jcoDestination", connectProperties);
}
// 基於上面設定的屬性生成連接配置文件
staticvoid createDataFile(String name, String suffix, Properties properties) {
File cfg = new File(name + "." + suffix);
if (!cfg.exists()) {
try {
FileOutputStream fos = new FileOutputStream(cfg, false);
properties.store(fos, "for tests only !");
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
publicstaticvoid connectWithoutPool() throws JCoException {
// 到當前類所在目錄中搜索 ABAP_AS_WITHOUT_POOL.jcoDestination
// 屬性連接配置文件,並根據文件中的配置信息來創建連接
JCoDestination destination = JCoDestinationManager
.getDestination(ABAP_AS);// 只需指定文件名(不能帶擴展名jcoDestination名,會自動加上)
System.out.println("Attributes:");
// 調用destination屬性時就會發起連接,一直等待遠程響應
System.out.println(destination.getAttributes());
}
publicstaticvoid main(String[] args) throws JCoException {
connectWithoutPool();
}
}
18.3.2.5.2.2. 訪問結構
JCoDestination destination = JCoDestinationManager
.getDestination(ABAP_AS);
JCoFunction function = destination.getRepository().getFunction(
"RFC_SYSTEM_INFO");//從對象倉庫中獲取 RFM 函數
function.execute(destination);
JCoStructure exportStructure = function.getExportParameterList()
.getStructure("RFCSI_EXPORT");
for (int i = 0; i < exportStructure.getMetaData().getFieldCount(); i++) {
System.out.println(exportStructure.getMetaData().getName(i) + ":\t"
+ exportStructure.getString(i));
}
System.out.println();
// 也可以使用下面的方式來遍歷
for (JCoField field : exportStructure) {
System.out.println(field.getName() + ":\t" + field.getString());
}
//*********也可直接通過結構中的字段名或字段所在的索引位置來讀取某個字段的值
System.out.println(exportStructure.getString(0));
System.out.println(exportStructure.getString("RFCPROTO"));
18.3.2.5.2.3. 訪問表 (Table)
JCoDestination destination = JCoDestinationManager
.getDestination(ABAP_AS);
JCoFunction function = destination.getRepository().getFunction(
"BAPI_COMPANYCODE_GETLIST");//從對象倉庫中獲取 RFM 函數:獲取公司列表
function.execute(destination);
JCoStructure returnStructure = function.getExportParameterList()
.getStructure("RETURN");
//判斷讀取是否成功
if (!(returnStructure.getString("TYPE").equals("") || returnStructure
.getString("TYPE").equals("S"))) {
throw new RuntimeException(returnStructure.getString("MESSAGE"));
}
//獲取Table參數:COMPANYCODE_LIST
JCoTable codes = function.getTableParameterList().getTable(
"COMPANYCODE_LIST");
for (int i = 0; i < codes.getNumRows(); i++) {//遍歷Table
codes.setRow(i);//將行指針指向特定的索引行
System.out.println(codes.getString("COMP_CODE") + '\t'
+ codes.getString("COMP_NAME"));
}
// move the table cursor to first row
codes.firstRow();//從首行開始重新遍歷 codes.nextRow():如果有下一行,下移一行並返回True
for (int i = 0; i < codes.getNumRows(); i++, codes.nextRow()) {
//進一步獲取公司詳細信息
function = destination.getRepository().getFunction(
"BAPI_COMPANYCODE_GETDETAIL");
function.getImportParameterList().setValue("COMPANYCODEID",
codes.getString("COMP_CODE"));
// We do not need the addresses, so set the corresponding parameter
// to inactive.
// Inactive parameters will be either not generated or at least
// converted. 不需要返回COMPANYCODE_ADDRESS參數(但服務器端應該還是組織了此數據,只是未經過網絡傳送?)
function.getExportParameterList().setActive("COMPANYCODE_ADDRESS",
false);
function.execute(destination);
returnStructure = function.getExportParameterList().getStructure(
"RETURN");
if (!(returnStructure.getString("TYPE").equals("")
|| returnStructure.getString("TYPE").equals("S") || returnStructure
.getString("TYPE").equals("W"))) {
throw new RuntimeException(returnStructure.getString("MESSAGE"));
}
JCoStructure detail = function.getExportParameterList()
.getStructure("COMPANYCODE_DETAIL");
System.out.println(detail.getString("COMP_CODE") + '\t'
+ detail.getString("COUNTRY") + '\t'
+ detail.getString("CITY"));
}// for
18.3.2.5.2.4. Java多線程調用有/無狀態RFM
有狀態調用:指多次調用某個程序(如多次調用某個RFC函數、調用某個函數組中的多個不同的RFC函數、及BAPI函數——因為BAPI函數也是一種特殊的具有RFC功能的函數,它也有自己的函數組)時,在這多次調用過程中,程序運行時的內存狀態(即全局變量的值)可以在每次調用后保留下來,供下一次繼續使用,而不是每次調用后,程序所在的內存狀態被清除。這種調用適用於那些使用到函數組中的全局變量的RFC函數的調用
無狀態調用:每次的調用都是獨立的一次調用(上一次調用與當前以及下一次調用之間不會共享任何全局變量),調用后不會保留內存狀態,這種調用適用於那些沒有使用到函數組中的全局變量的RFC函數調用
如果主調程序為Java,有狀態調用的前提是:
l 多次調用RFC函數時,Java端要確保每次調用所使用的連接與上次是同一個(應該不需要是同一物理連接,只需要確保是同一遠程會話,從下面演示程序來看,用的是連接池,但同一任務執行時並未去特意使用同一物理連接去發送遠程調用,而只是要求是同一遠程會話)
l ABAP端需要在每次調用后,保留每一次被調用后函數組的內存狀態,直到最后一次調用完成止,這需要Java與ABAP配合來完成(Java在第一次調用時,調用JCoContext.begin、JCoContext.end這兩個方法,告訴SAP這一調用過程將是有狀態調用,需要保留內存狀態,然后SAP端就會自動保留內存狀態)
如果主調程序是ABAP(即ABAP程序調用ABAP函數),此種情況下沒有特殊的要求,直接調用就即可,只要是在同一程序的同一運行會話其間(會話相當於Java中的同一線程吧),不管是多次調用同一個函數、還是調用同一函數組中的不同函數,則都會自動保留內存狀態,直到程序運行結束,這是系統自己完成的。一個函數組好比一個類,函數組中不同的函數就相當於類中不同的方法、全局變量就相當於類中的屬性,所以只要是在同一程序的同一運行會話期間,調用的同一函數所在的函數組中的全局變量都是共享的,就好比調用一類的某個方法時,該方法設置了某個類的屬性,再去調用該類的其它方法時,該屬性值還是保留了以前其它方法修改后的狀態值。
狀態調用只要保證同一Java線程中多次遠程方法調用采用的都是同一會話即可
18.3.2.5.3. ABAP訪問Java服務
18.3.2.5.4. ABAP創建遠程目標
SAP通過JCO反向調用JAVA的RFC服務其實也是相對簡單的,只是在JAVA端需要使用JCO創建一個RFC服務,然后在SAP端注冊這個服務程序。
首先,JCo服務器程序需在網關中進行注冊,在SM59中,定義一個連接類型為T的遠程目標
RFC目標系統:是ABAP RFC調用Java時,需指定的目標系統名。
Program ID:是JAVA程序中使用的
Gateway Host與Gateway service值來自以下界面(Tcode:SMGW):
TCP服務sapgw是固定的,后面的00就是系統編號
所有配置好且Java服務器代碼跑起來后,點擊“Connection Test”按鈕,如不出現紅色文本,則表示鏈接成功(注:此時需要ServerDataProvider.JCO_PROGID設置的Program ID要與SM59中設置的相同,否則測試不成功。另要注意的是:即使Java服務器設置的Program ID亂設置,Java服務端還是能啟起來,但ABAP測試連接時會不成功,也就代表ABAP不能調用Java)
18.3.2.5.5. 連接異常registrationnot allowed
Java服務啟動時,如出現以下異常,則需在SAP中修改網關參數:
com.sap.conn.jco.JCoException: (113) JCO_ERROR_REGISTRATION_DENIED: CPIC-CALL: SAP_CMACCPTP3 on convId:
LOCATION SAP-Gateway on host LRP-ERP / sapgw00
ERROR registration of tp JCOTEST from host JIANGZHENGJUN not allowed
……
通過事務碼SMGW修改參數:
18.3.2.5.6. 帶狀態訪問
// 如果是某任務LUW中第一次調用時,則jcoServerCtx服務上下文為非狀態,需設置為狀態調用
if (!jcoServerCtx.isStatefulSession()) {
// 設置為狀態調用,這樣在有狀態調用的情況下,上下文中會攜帶會話ID
jcoServerCtx.setStateful(true);
cachedSession = new SessionContext();// 新建會話
// 將會話存儲在映射表中,以便某個任務里每次遠程調用都可以拿到同一會話
statefulSessions.put(jcoServerCtx.getSessionID(),
cachedSession);
} else {// 非第一次調用
cachedSession = statefulSessions.get(jcoServerCtx
.getSessionID());
}