Powerful web applications with old and new ABAP systems
2023-10-12 21:26:29 Author: blogs.sap.com(查看原文) 阅读量:7 收藏

ABAP RFC protocol is the fastest way of data exchange between ABAP systems or ABAP system and non-ABAP application. The protocol is available in all on-premise ABAP systems, old and new and in cloud-based ABAP systems, like SAP S/4HANA Cloud or Steampunk. It is enabled in SAP Cloud Connector for Java runtime and for certain scenarios in NodeJS and Python run-times.

Over decades tons of ABAP business logic is encapsulated in RFC-enabled ABAP Function Modules and BAPIs, many of which already exposed via ODATA services added on top. Nevertheless, RFC is still used in scenarios when larger data volumes shall be moved between ABAP systems, because of unmatched performance and availability “everywhere”.

Web applications with ABAP RFM API

In this blog we cover one less common scenario, using RFC protocol to expose ABAP business logic and direct consume it by on-premise or cloud web applications, built with web framework of choice (configurable by developer).

In this direct mix of “plain ABAP” and modern web, one the same ABAP data model is exposed and used at all application layers, from ABAP backend to express and React for example. It accelerates the development and troubleshooting and results in apps with by factors less code and unique capabilities, impossible to achieve using other technologies. Apps performance is like clickable demo while working with real ABAP systems and no hourglass has been used yet.

ABAP development is done like the frontend will be built with ABAP and no additional efforts or web skills are required for ABAP RFM API development. The same is with web development, no special SAP/ABAP skills are required for web app development with ABAP RFM backend. RFM stands for Remote Enabled ABAP Function Module.

When ABAP data model is exposed in frontend, what is missing are metadata or annotations, like  data type, length, field texts etc., so that the frontend can handle it accordingly. This gap is solved in design-time, using command line tool abap, part of SAP/fundamental-tools open source.

Using abap utility, HTML/JS snippets of annotated ui elements with ABAP bindings are generated for ABAP RFM API fields, like <ui-input>, <ui-checkbox>, <ui-date-picker> etc.. App developer can copy/paste/adapt these ui elements into pages and layouts and frontend logic, per application requirements.

Annotated HTML/JS snippers shall represent real ui elements, capable to “understand” and handle annotations. One and only set of real ui elements implemented so far, is built with Aurelia web framework and easy portable to other web frameworks.

This solution design is used in prototyping of powerful cloud business applications with ABAP backend systems and limits of functional/visual scalability are not reached yet.

Application components are shown on deployment diagrams below and how to build them is illustrated with one real-life application example.

Deployment options

Web applications consuming ABAP RFMs can be deployed on-premise or in the cloud. Connection parameters in the cloud are different then on-premise and cloud services, like audit log for example, are not available on premise. The application code is otherwise the same.

RFC connectivity from on-premise web application to ABAP system requires platform specific RFC connector: Java SAP JCo, Microsoft SAP NCo, Python SAP/PyRFC  or Node.JS: SAP/node-rfc.

The diagram below shows de-facto standard web application, with View-Model, View and Server, and ABAP RFM API.

Deployment%20on-premise

Deployment on-premise

RFC connectivity from cloud Java applications is supported by SAP Cloud Connector. The same is for Node.JS and Python enabled only for certain usage scenarios, supported by SAP development. The usage scenario shall be therefore aligned with SAP development or custom buildpack can be used for prototyping for example.

Deployment%20in%20cloud

Deployment in cloud

The custom buildpack includes SAP NW RFC SDK binaries, added to LD_LIBRARY_PATH. The application can use WebSocket RFC connection parameters, to reach ABAP on-premise system with WebSocket RFC support, outside company firewall. If WebSocket RFC protocol is not supported by ABAP system, or connection outside firewall not wanted, the SAP Business Connector shall be installed on-premise, inside the firewall. For more info check SAP blogs

Let now build app components and put them together following above mentioned approach: ABAP API, App Server, View Model and View.

ABAP RFM API

Before opening ABAP editor, a good idea is to clarify requirements, thus let start with that first 🙂

Requirements

Our app shall provide an alternative to ABAP Dynpro transactions IE03 and IE02, offering SAP Business Object Equipment read and edit, with three customer specific requirements:

  • Read and edit grouped and ungrouped Classifications and Characteristics associated with Equipment instance
  • Display Mean Time to Repair (MTTR) and Mean Time Between Failure (MTBF) statistics, relevant in certain business scenarios
  • Attach (and remove) documents like PDF, AutoCAD drawings etc. to Equipment instance, using the Document Management System (DMS)

SAP business object Equipment represents physical assets like the office furniture, devices, machineries etc. Using Classifications and Characteristics, name/value pairs can be added to business object instance, further describing the object. Similar characteristics (name/value pairs) can be grouped under the same group name and several such groups plus ungrouped characteristics may be maintained for Equipment object instance.

Screenshots below show ABAP Dynpro screen and corresponding web application, as per above requirements. The app can be tested from Walldorf VPN only: coevi76/plm3

Dynpro%20screen

Dynpro screen

Web%20application%20screen

Web application screen

Based app requirements, the business logic is localised mostly in already existing BAPIs, then wrapped into RFMs, to provide more flexibility and capabilities like reading MTTTR/MTBF times. Here are some basic rules how to expose ABAP business logic via ABAP RFM API, as per experience from real-life projects,

ABAP RFM names are for now only saved in local file, so that ui elements can be generated later on, in step 4 (View).

config/api.equipment.yaml

api.equipment:
  - /COE/RBP_PAM_EQUIPMENT_GETL # Selection of Equipment List
  - /COE/RBP_PAM_EQUIPMENT_GET # Read Equipment
  - /COE/RBP_PAM_EQUIPMENT_CHANGE # Change Equipment
  - /COE/RBP_PAM_EQUIPMENT_INSTALL # Install Equipment (Functional Location, Superior Equipment)
  - /COE/RBP_PAM_EQUIPMENT_DISMTLE # Dismantle Equipment (Functional Location, Superior Equipment)
  - /COE/RBP_PAM_EQUIP_HIER_GET # Read Equipment installation hierarchy
  - BAPI_EQUI_CREATE # Create Equipment

App Server

ABAP RFM API data are exposed via app server routes. App server pattern depends on ABAP API and app requirements. In this case no additional logic is required on server and ABAP data are just passed forth and back, without orchestration.

The example below is implemented with Python Flask server and looks almost identical in Node.JS express or Java. Data mappings work the same way as well:

  • Python variables represent ABAP variables
  • Python dictionary (like plain Node.JS or Java object) represents ABAP structures
  • Arrays of Python dictionaries represent ABAP tables

ABAP to Python/Node/Java data conversions are done automatically, inside RFC connector.

Here the app server code snippet for Equipment routes and here full source code server/server.py#L48

# Equipment
@app.route('/equipment/<path:path>', methods=['POST'])
def equipment(path):
    try:
        to_abap = json.loads(request.data)

        if path == 'get':
            from_abap = abap_client.call('/COE/RBP_PAM_EQUIPMENT_GET', to_abap)
        elif path == 'getlist':
            from_abap = abap_client.call('/COE/RBP_PAM_EQUIPMENT_GETL', to_abap)
        elif path == 'change':
            from_abap = abap_client.call('/COE/RBP_PAM_EQUIPMENT_CHANGE', to_abap)
        elif path == 'install':
            from_abap = abap_client.call('/COE/RBP_PAM_EQUIPMENT_INSTALL', to_abap)
        elif path == 'dismantle':
            from_abap = abap_client.call('/COE/RBP_PAM_EQUIPMENT_DISMTLE', to_abap)
        else:
            raise Exception ('not implemented: %s' % path)

        return to_json(from_abap)

    except Exception, e:
        return serverError(e), 50

Let trace one full HTTP request/response cycle, using equipment/get route, to read Equipment data from ABAP system.

The frontend works with ABAP RFM data in JavaScript format and sends the equipment number as ABAP API IV_EQUIID parameter in HTTP POST request:

POST coevi76/plm3/api/equipment/get
Content-Type: application/json

{"IV_EQUIID": 4711}

Using json.loads(), the HTTP request data are transformed to Python dictionary {“IV_EQUIID”: 4711} and sent to ABAP RFM PAM_EQUIPMENT_GET, to read Equipment data.

The RFC connector, encapsulated in abap_client.call(), automatically transforms Python request data to ABAP, invokes PLM_EQUIPMENT_GET and returns ABAP Equipment data to Python variable result. The result is sent in HTTP response to web browser, using to_json(result).

In this scenario

  • ABAP data go all the way up to app server and frontend, thus all application layers work with the same ABAP data model. App server works with ABAP data in Python, frontend with ABAP data in JavaScript and ABAP works with ABAP 🙂
  • It considerably simplifies the development, troubleshooting and communication among ABAP and Web developers
  • The programming model is “standard” ABAP, like the frontend is built in ABAP. It is a nice opportunity for ABAP developers to leverage ABAP knowledge and implement some server logic in Java, Node.JS or Python app server.
  • Application is ABAP stateful by default (configuration), thus COMMIT BAPI shall be invoked after CHANGE BAPI for example. In this example BAPIs are wrapped into application specific RFMs, including COMMITs but COMMIT can be also explicit, depending on scenario.
  • ABAP API adaptations, extensions, choreography, orchestration, caching etc. can be added at server level, if needed, to cover industry or customer specific requirements
  • The server logic sometimes need access to ABAP data structures at field level, in which case abap call template can help.

View Model (JS)

Via server routes, ABAP data, like Equipment instance, reach the frontend View-Model. The programming language is now JavaScript and the orchestration can be done also at this level, the ABAP way. Calling BAPI COMMIT after BAPI CHANGE for example is here still possible, via server routes now.

The snippet below shows the frontend logic of Equipment class get() request, copied from the full source code: client/src/plm/equipment/model.js

get(id = null) {
  if (id) this.selection.EQUIID = id;
  return this.httpService
    .backend("/equipment/get", {
      IV_EQUIID: this.selection.EQUIID,
      IV_CHARACTERISTICS: "X",
      IV_DOCUMENT: "X",
      IV_WITH_TIMESTATS: "X",
    })
    .then((FROM_ABAP) => {
      // header
      this.ES_HEADER = FROM_ABAP.ES_HEADER;
      this.ES_SPECIFIC = FROM_ABAP.ES_SPECIFIC;

      // clone structures for X-fields change detection
      this.IS_HEADER = UIUtils.abapStructClone(this.ES_HEADER);
      this.IS_SPECIFIC = UIUtils.abapStructClone(this.ES_SPECIFIC);

      // user status
      this.ET_STATUS = FROM_ABAP.ET_USER_STATUS;

      // DMS Attachments
      this.Attachments = FROM_ABAP.ET_DOCUMENT;

      //// get the image
      //if (image) {
      //  this.httpService.backend(`/document/download/${image.FILE_ID}?mime_type=${image.MIMETYPE}`)
      //    .then(IMAGE => {
      //      //this.Image = {
      //      //  imageMimeType: mimeType,
      //      this.image.content = IMAGE.EV_CONTENT;
      //      //}
      //    })
      //    .catch(error => {
      //      this.app.toastError(error);
      //    });
      //}

      // its url
      this.href = this.app.webGuiUrl("IE03", false, this.selection.EQUIID);
      this.hrefText =
        this.IS_HEADER.DESCRIPT + " (" + this.selection.EQUIID + ")";

      // Characteristics todo: optimize
      this.Characteristics = [];

      // add grouped characteristics
      for (let chGroup of FROM_ABAP.ET_CHARACT_GROUP) {
        this.addCharactGroup(
          FROM_ABAP.ET_CHARACTERISTICS,
          chGroup.CHARACT_GROUP,
          chGroup.CHARACT_GROUP_NAME
        );
      }

      // add ungrouped characteristics
      this.addCharactGroup(FROM_ABAP.ET_CHARACTERISTICS, "", "Ungrouped");

      // let the View update
      // this.app.toastSuccess(`Equipment found: ${this.selection.EQUIID}`);

      // mttr/mtbf kpi
      this.KPI = [];
      if (this.IS_HEADER.START_FROM) {
        if (FROM_ABAP.ET_RESULT.length) {
          this.KPI = [
            {
              id: "BMON",
              name: "No. of breakdowns reported in month",
              value: "",
            },
            { id: "TBR", name: "Time between repairs", value: "" },
            { id: "BACT", name: "No. of actual breakdowns", value: "" },
            { id: "MTBR", name: "Mean time between repairs ", value: "" },
            {
              id: "TLDM",
              name: "Total length of downtime in month",
              value: "",
            },
            { id: "MTTRM", name: "Mean Time To Repair in Month", value: "" },
          ];
          let result = FROM_ABAP.ET_RESULT[0];
          for (let line of this.KPI) {
            if (line.id === "BMON") line.value = result.NBDEFF;
            else if (line.id === "TBR") line.value = result.STBRHRS;
            else if (line.id === "BACT") line.value = result.SNBDREP;
            else if (line.id === "TLDM") line.value = result.STTRHRS;
          }
          for (let line of this.KPI) {
            if (line.id === "MTBR")
              if (result.SNBDREP) line.value = result.STBRHRS / result.NBDEFF;
            if (line.id === "TLDM")
              if (result.SNBDREP) line.value = result.STBRHRS / result.SNBDREP;
            if (line.id === "MTTRM")
              if (result.NBDEFF) line.value = result.STTRHRS / result.NBDEFF;
            if (line.id !== "BMON" && line.id !== "BACT") {
              line.value += " H";
            }
          }
        }
      }
    })

    .catch((error) => {
      this.reset();
      this.app.toastError(error);
    });
}

addCharactGroup(ET_CHARACTERISTICS, groupId, groupName) {
  let chlist = [];

  // add group
  for (let ch of ET_CHARACTERISTICS) {
    if (ch.CHARACT_GROUP === groupId) {
      ch.NUMBER_DIGITS -= ch.NUMBER_DECIMALS; // adapt for MNUMBER :-)
      chlist.push(ch);
    }
  }

  if (chlist.length) {
    this.Characteristics.push({
      groupId: groupId ? groupId : "GROUPID",
      groupDesc: groupName,
      CHARLIST: chlist,
    });
  }
}

save() {
  // structures are identical, set X-fields for header and specific
  let header = [
    "MANFACTURE",
    "MANMODEL",
    "MANPARNO",
    "MANSERNO",
    "MAINTPLANT",
    "OBJECTTYPE",
    "ABCINDIC",
    "WORK_CTR",
  ];
  let specific = ["EQUICATGRY", "READ_FLOC"];
  this.IS_HEADER_X = UIUtils.abapStructDiff(
    this.ES_HEADER,
    this.IS_HEADER,
    header
  );
  this.IS_SPECIFIC_X = UIUtils.abapStructDiff(
    this.ES_SPECIFIC,
    this.IS_SPECIFIC,
    specific
  );
  // characteristics
  let itChar = [];
  for (let chGroup of this.Characteristics) {
    for (let ch of chGroup.CHARLIST) {
      itChar.push({
        CLASS: ch.CLASS,
        CHARACT: ch.NAME_CHAR,
        VALUE: ch.VALUE,
        VALUE_FROM: ch.VALUE_FROM,
        VALUE_TO: ch.VALUE_TO,
      });
    }
  }

  return this.httpService
    .backend("/equipment/change", {
      IV_EQUIID: this.selection.EQUIID,
      IS_HEADER: this.ES_HEADER, // changed data in ES_
      IS_HEADER_X: this.IS_HEADER_X,
      IS_DATA_SPECIFIC: this.ES_SPECIFIC, // changed data in ES_
      IS_DATA_SPECIFIC_X: this.IS_SPECIFIC_X,
      IT_CHARACTERISTICS: itChar,
    })
    .then((FROM_ABAP) => {
      this.app.toastSuccess({
        type: "success",
        message: `Equipment saved: ${this.selection.EQUIID}`,
      });
    })
    .catch((error) => {
      this.app.toastError(error);
    });
}

Inspecting view model source code probably the first thing to notice is it is less than 200 lines long, despite not quite simple requirements. Here are few more observations and experience from projects:

  • The code comprises almost exclusively of application logic, no traces of frameworks specific or technical components, hindering developer’s view of application. Each web frameworks brings own “artefacts” more or less but developers are happier and more productive with less “verbose” frameworks. Aurelia is used here because of the minimum of such overhead.
  • The same ABAP RFM API data are used in frontend and ABAP developers recognise well known “X fields” in save() method for example. ABAP developers feels therefore also here “at home” and can do the View Model development. It is just like ABAP development, same data structures, only in JavaScript/TypeScript and no HTML/CSS knowledge mandatory
  • Just like app server, the view-model deals with ABAP data on detail/field level only when necessary, keeping the code short and readable. ABAP RFM API can operate with data structures with hundreds of fields. They are all there but exposed in code only when necessary, which simplifies the development and troubleshooting and improves maintainability.

View

HTML or JS View comprises of annotated ui elements, bound to View-Model data, thus ABAP data in JavaScript. When View Model data are updated, per equipment/get route response for example, the ui elements are automatically updated, When ui element updated by user input, the underlying View Model data are updated automatically, thanks to bi-directional binding.

View

View

Now is the time to use the local file created in first step and generate ui elements’ for ABAP RFM API, using abap CLI get and make commands:

npm install -g abap-api-tools

# run after ABAP RFM API changed, conection to ABAP system required
abap get MME -c config/api.equipment 

# no ABAP system connection needed
abap make aurelia -c config/api.equipment

It creates HTML/JS snippets of Aurelia ui elements, with ABAP RFM data bindings and annotations:

  • Data type, length
  • Texts (label, caption)
  • Unit of measure
  • Value Input Help
    • Field Domain Values
    • Check Tables and custom lookups
    • Elementary and complex F4 search helps
  • Default value (ABAP SU3 SET/GET parameter)
  • Conversion Exit

The output are plain lists of ui elements, for each field and table of ABAP RFM API, like pam_equipment_get.html and pam_equipment_get.js. HTML file contains HTML snippets of annotated ui element and JS file contains ABAP RFM API data structures initialisers, helpful in certain scenarios.

Few remarks also here:

  • Using abap utility is not mandatory. Ui elements bound to ABAP RFM API data can be manually implemented and generated ui elements can be adapted as needed. Attributes like SU3 defaults, value input helps etc., can be arbitrary added or removed from any ui element, technical format can be changed etc
  • With basic HTML/CSS skills, ABAP developers can build also this layer, when no more then copy.paste/group of ui elements needed.
  • HTML and JS snippets expose “expanded” ABAP API structures, with all fields and no frontend application needs all fields. They cover all scenarios for all industries, customer segments etc. The fields relevant for front-end depend on business scenario and usually defined by SAP functional expert. Only these fields` HTML snippets are used in particular app front-end view.
  • The generated HTML UI elements impose no restrictions on view layout and styling, they are fully independent

Low-code by design

Fully functional app is implemented with ca. 400 lines of code, which looks surprising little onsidering capabilities:

  • ABAP: the pure ABAP business logic exposed, without technical overhead
  • App server: ca. 20 lines of Python
  • View-Model: ca. 190 lines of JavaScript
  • Front-end view: ca. 150 lines of HTML

The low-code is here achieved by solution design simplification (until nothing left to remove …) and using the same data model at each level: frontend/server/backend. Standard web best-practices are combined with classical ABAP best practices, without “mediators” in-between and play surprisingly well together.

Comparisons with similar applications using other technologies show around 40-50 times less code required at ABAP level, because of using plain ABAP business logic, without overhead. Around 10-20 times less code is required at Web level, by straightforward ABAP data consumption, with ui components enriched in design-time by ABAP metadata.

The simplicity of this approach makes the app development fun because results come up quickly and functional/visual variations are easy to do.

The implementation is under full developer’s control, without any “magic” added by abap CLI and without run-time dependencies of this toolkit, except for inevitable Value Helps.

Super-Apps

Alhough already powerful, the Equipment maintenance app is far from limits of what this approach can handle.

When developed this way, more apps can be assembled into so called super-apps, without noticable impact on performance or maintainability.

The example below is probably unique case implemented with SAP technologies, a cockpit super-app, assembled from Service Notifications, Service Orders and Equipment Maintenance “elementary” apps, all without iframes: client/src/plm/super-app/app.html

Summary

Hope you enjoyed reading and stay tuned for more updates.

Contributions to SAP/fundamental-tools project are welcome and please feel free to share your thoughts, questions and suggestions.


文章来源: https://blogs.sap.com/2023/10/12/powerful-web-applications-with-old-and-new-abap-systems/
如有侵权请联系:admin#unsafe.sh