Οδηγός API διεπαφής COM: Java Spring Boot + JACOB Library

Σε αυτό το άρθρο, θα σας δείξω πώς να ενσωματώσετε τη βιβλιοθήκη JACOB στην εφαρμογή Spring Boot. Αυτό θα σας βοηθήσει να καλέσετε ένα API διεπαφής COM μέσω της βιβλιοθήκης DLL στην εφαρμογή ιστού σας.

Επίσης, για επεξηγηματικούς σκοπούς, θα δώσω μια περιγραφή ενός COM API ώστε να μπορείτε να δημιουργήσετε την εφαρμογή σας πάνω από αυτό. Μπορείτε να βρείτε όλα τα αποσπάσματα κώδικα σε αυτό το repo GitHub.

Αλλά πρώτα, μια γρήγορη σημείωση: στο C the Signs εφαρμόσαμε αυτήν τη λύση που μας επέτρεψε να ενσωματωθούμε στο EMIS Health. Πρόκειται για ένα ηλεκτρονικό σύστημα καταγραφής ασθενών που χρησιμοποιείται στην πρωτοβάθμια περίθαλψη στο Ηνωμένο Βασίλειο. Για ενσωμάτωση χρησιμοποιήσαμε την παρεχόμενη βιβλιοθήκη DLL.

Η προσέγγιση που θα σας δείξω εδώ (καθαρισμένη για να αποφύγετε τυχόν διαρροές ευαίσθητων πληροφοριών) ξεκίνησε στην παραγωγή πριν από περισσότερα από δύο χρόνια και έκτοτε έχει αποδείξει την ανθεκτικότητά της.

Εφόσον χρησιμοποιήσαμε πρόσφατα μια ολοκαίνουργια προσέγγιση για την ενσωμάτωση στο EMIS, το παλιό σύστημα θα τερματιστεί σε έναν ή δύο μήνες. Αυτό το σεμινάριο είναι το τραγούδι του κύκνου. Κοιμήσου, μικρός μου πρίγκιπας.

Τι είναι το API DLL;

Αρχικά, ας ξεκινήσουμε με μια σαφή περιγραφή της βιβλιοθήκης DLL. Για να το κάνω αυτό, ετοίμασα ένα σύντομο πρότυπο της αρχικής τεχνικής τεκμηρίωσης.

Ας ρίξουμε μια ματιά σε αυτό για να δούμε ποιες είναι οι τρεις μέθοδοι μιας διεπαφής COM.

Μέθοδος InitialiseWithID

Αυτή η μέθοδος είναι μια λειτουργία ασφαλείας που απαιτείται επιτόπου, η οποία μας επιτρέπει να αποκτήσουμε μια σύνδεση με έναν διακομιστή API που θέλουμε να ενσωματώσουμε στη βιβλιοθήκη.

Απαιτεί το AccountID(GUID) του τρέχοντος χρήστη API (για πρόσβαση στον διακομιστή) και ορισμένα άλλα επιχειρήματα προετοιμασίας που παρατίθενται παρακάτω.

Αυτή η λειτουργία υποστηρίζει επίσης μια δυνατότητα αυτόματης σύνδεσης. Εάν ένας πελάτης διαθέτει μια συνδεδεμένη έκδοση του τρέχοντος συστήματος (η βιβλιοθήκη είναι μέρος αυτού του συστήματος) και καλεί τη μέθοδο στον ίδιο κεντρικό υπολογιστή, το API θα ολοκληρώσει αυτόματα την είσοδο στον λογαριασμό του συγκεκριμένου χρήστη. Στη συνέχεια, θα επιστρέψει τις SessionIDεπόμενες κλήσεις API.

Διαφορετικά, ο πελάτης πρέπει να συνεχίσει τη Logonλειτουργία (δείτε το επόμενο μέρος) χρησιμοποιώντας το επιστρεφόμενο LoginID.

Για να καλέσετε τη συνάρτηση, χρησιμοποιήστε το όνομα InitialiseWithIDμε τα ακόλουθα ορίσματα:

Ονομα Μέσα έξω Τύπος Περιγραφή
διεύθυνση Σε Σειρά παρέχεται IP διακομιστή ενοποίησης
Αναγνωριστικό λογαριασμού Σε Σειρά παρείχε μοναδική συμβολοσειρά GUID
Ταυτότητα σύνδεσης Εξω Σειρά Συμβολοσειρά GUID που χρησιμοποιείται για κλήση API σύνδεσης
Λάθος Εξω Σειρά Περιγραφή σφάλματος
Αποτέλεσμα Εξω Ακέραιος αριθμός -1 = Ανατρέξτε στο σφάλμα

1 = Επιτυχής προετοιμασία εν αναμονή σύνδεσης

2 = Δεν είναι δυνατή η σύνδεση με διακομιστή λόγω απουσίας διακομιστή ή εσφαλμένων λεπτομερειών

3 = Μη αντιστοιχισμένος λογαριασμός λογαριασμού

4 = Το Autologon επιτυχές

SessionID Εξω Σειρά Το GUID χρησιμοποιείται για επακόλουθες αλληλεπιδράσεις (εάν η αυτόματη σύνδεση είναι επιτυχής)

Μέθοδος σύνδεσης

Αυτή η μέθοδος καθορίζει την εξουσία του χρήστη. Το όνομα χρήστη εδώ είναι το αναγνωριστικό που χρησιμοποιείται για τη σύνδεση στο σύστημα. Ο κωδικός πρόσβασης είναι ο κωδικός πρόσβασης API που έχει οριστεί για αυτό το όνομα χρήστη.

Στο σενάριο επιτυχίας, η κλήση επιστρέφει μια SessionIDσυμβολοσειρά (GUID) που πρέπει να μεταφερθεί σε άλλες επόμενες κλήσεις για να τις επικυρώσει.

Για να καλέσετε τη συνάρτηση, χρησιμοποιήστε το όνομα Logonμε τα ακόλουθα ορίσματα:

Ονομα Μέσα έξω Τύπος Περιγραφή
Ταυτότητα σύνδεσης Σε Σειρά Το αναγνωριστικό σύνδεσης επέστρεψε με τη μέθοδο αρχικοποίησης Αρχικοποίηση με αναγνωριστικό
όνομα χρήστη Σε Σειρά παρείχε όνομα χρήστη API
Κωδικός πρόσβασης Σε Σειρά παρέχεται κωδικός API
SessionID Εξω Σειρά Το GUID χρησιμοποιείται για επακόλουθες αλληλεπιδράσεις (εάν η σύνδεση είναι επιτυχής)
Λάθος Εξω Σειρά Περιγραφή σφάλματος
Αποτέλεσμα Εξω Ακέραιος αριθμός -1 = Τεχνικό σφάλμα

1 = Επιτυχής

2 = Έληξε

3 = Ανεπιτυχής

4 = Το μη έγκυρο αναγνωριστικό σύνδεσης ή το αναγνωριστικό σύνδεσης δεν έχουν πρόσβαση σε αυτό το προϊόν

μέθοδος getMatchedUsers

Αυτή η κλήση σάς επιτρέπει να βρείτε εγγραφές δεδομένων χρήστη που ταιριάζουν με συγκεκριμένα κριτήρια. Ο όρος αναζήτησης μπορεί να αναφέρεται μόνο σε ένα πεδίο τη φορά, όπως επίθετο, όνομα ή ημερομηνία γέννησης.

Μια επιτυχής κλήση επιστρέφει μια συμβολοσειρά XML με τα δεδομένα σε αυτήν.

Για να καλέσετε τη συνάρτηση, χρησιμοποιήστε το όνομα getMatchedUsersμε τα ακόλουθα ορίσματα:

Ονομα Μέσα έξω Τύπος Περιγραφή
SessionID Σε Σειρά Το αναγνωριστικό περιόδου σύνδεσης επέστρεψε με τη μέθοδο σύνδεσης
Όρος αγώνα Σε Σειρά Ορος αναζήτησης
Λίστα αντιστοίχισης Εξω Σειρά XML συμμόρφωση με το παρεχόμενο αντίστοιχο σχήμα XSD
SessionID Εξω Σειρά Το GUID χρησιμοποιείται για επακόλουθες αλληλεπιδράσεις (εάν η σύνδεση είναι επιτυχής)
Λάθος Εξω Σειρά Περιγραφή σφάλματος
Αποτέλεσμα Εξω Ακέραιος αριθμός -1 = Τεχνικό σφάλμα

1 = Βρέθηκαν χρήστες

2 = Δεν επιτρέπεται η πρόσβαση

3 = Χωρίς χρήστες

Ροή εφαρμογής βιβλιοθήκης DLL

Για να καταλάβω ευκολότερα αυτό που θέλουμε να εφαρμόσουμε, αποφάσισα να δημιουργήσω ένα απλό διάγραμμα ροής.

Περιγράφει ένα βήμα προς βήμα σενάριο για το πώς ένας πελάτης Ιστού μπορεί να αλληλεπιδράσει με την εφαρμογή που βασίζεται σε διακομιστή χρησιμοποιώντας το API του. Περιλαμβάνει αλληλεπίδραση με τη βιβλιοθήκη DLL και μας επιτρέπει να προσελκύσουμε υποθετικούς χρήστες με τον παρεχόμενο όρο αντιστοίχισης (κριτήρια αναζήτησης):

Εγγραφή COM

Τώρα ας μάθουμε πώς μπορούμε να έχουμε πρόσβαση στη βιβλιοθήκη DLL. Για να μπορέσετε να αλληλεπιδράσετε με μια διεπαφή COM τρίτου μέρους, πρέπει να προστεθεί στο μητρώο.

Δείτε τι λένε τα έγγραφα:

Το μητρώο είναι μια βάση δεδομένων συστήματος που περιέχει πληροφορίες σχετικά με τη διαμόρφωση του υλικού και του λογισμικού του συστήματος καθώς και για τους χρήστες του συστήματος. Οποιοδήποτε πρόγραμμα που βασίζεται στα Windows μπορεί να προσθέσει πληροφορίες στο μητρώο και να διαβάσει πληροφορίες από το μητρώο. Οι πελάτες αναζητούν στο μητρώο ενδιαφέροντα στοιχεία για χρήση.

Το μητρώο διατηρεί πληροφορίες σχετικά με όλα τα αντικείμενα COM που είναι εγκατεστημένα στο σύστημα. Κάθε φορά που μια εφαρμογή δημιουργεί μια παρουσία ενός στοιχείου COM, συμβουλεύεται το μητρώο για να επιλύσει είτε το CLSID είτε το ProgID του στοιχείου στο όνομα διαδρομής του διακομιστή DLL ή EXE που το περιέχει.

Μετά τον προσδιορισμό του διακομιστή του στοιχείου, τα Windows είτε φορτώνουν το διακομιστή στο χώρο διεργασίας της εφαρμογής πελάτη (στοιχεία σε διαδικασία) ή εκκινούν το διακομιστή στον δικό τους χώρο επεξεργασίας (τοπικοί και απομακρυσμένοι διακομιστές).

Ο διακομιστής δημιουργεί μια παρουσία του στοιχείου και επιστρέφει στον πελάτη μια αναφορά σε μία από τις διεπαφές του στοιχείου.

Για να μάθετε πώς να το κάνετε αυτό, η επίσημη τεκμηρίωση της Microsoft λέει:

Μπορείτε να εκτελέσετε ένα εργαλείο γραμμής εντολών που ονομάζεται Assembly Registration Tool (Regasm.exe) για να εγγραφείτε ή να καταργήσετε την εγγραφή μιας συγκρότησης για χρήση με COM.

Το Regasm.exe προσθέτει πληροφορίες σχετικά με την κλάση στο μητρώο συστήματος, ώστε οι πελάτες COM να μπορούν να χρησιμοποιούν την κλάση .NET Framework με διαφάνεια.

Η κλάση RegistrationServices παρέχει την αντίστοιχη λειτουργικότητα. Ένα διαχειριζόμενο στοιχείο πρέπει να είναι εγγεγραμμένο στο μητρώο των Windows προτού ενεργοποιηθεί από έναν πελάτη COM

Βεβαιωθείτε ότι ο κεντρικός υπολογιστής σας έχει εγκαταστήσει τα απαιτούμενα .NET Frameworkστοιχεία. Μετά από αυτό, μπορείτε να εκτελέσετε την ακόλουθη εντολή CLI:

C:\Windows\Microsoft.NET\Framework\v2.0.50727\RegAsm.exe {PATH_TO_YOUR_DLL_FILE} /codebase

A message will display indicating whether the file was successfully registered. Now we're ready for the next step.

Defining the Backbone of the Application

DllApiService

First of all, let's define the interface that describes our DLL library as it is:

public interface DllApiService { /** * @param accountId identifier for which we trigger initialisation * @return Tuple3 from values of Outcome, SessionID/LoginID, error * where by the first argument you can understand what is the result of the API call */ Mono
    
      initialiseWithID(String accountId); /** * @param loginId is retrieved before using {@link DllApiService#initialiseWithID(String)} call * @param username * @param password * @return Tuple3 from values of Outcome, SessionID, Error * where by the first argument you can understand what is the result of the API call */ Mono
     
       logon(String loginId, String username, String password); /** * @param sessionId is retrieved before using either * {@link DllApiService#initialiseWithID(String)} or * {@link DllApiService#logon(String, String, String)} calls * @param matchTerm * @return Tuple3 from values of Outcome, MatchedList, Error * where by the first argument you can understand what is the result of the API call */ Mono
      
        getMatchedUsers(String sessionId, String matchTerm); enum COM_API_Method { InitialiseWithID, Logon, getMatchedUsers } }
      
     
    

As you might have noticed, all the methods map with the definition of the COM Interface described above, except for the initialiseWithID function.

I decided to omit the address variable in the signature (the IP of the integration server) and inject it as an environment variable which we will be implementing.

SessionIDService Explained

To be able to retrieve any data using the library, first we need to get the SessionID.

According to the flow diagram above, this involves calling the initialiseWithID method first. After that, depending on the result, we will get either the SessionID or LoginID to use in subsequent Logon calls.

So basically this is a two-step process behind the scenes. Now, let's create the interface, and after that, the implementation:

public interface SessionIDService { /** * @param accountId identifier for which we retrieve SessionID * @param username * @param password * @return Tuple3 containing the following values: * result ( Boolean), sessionId (String) and status (HTTP Status depending on the result) */ Mono
    
      getSessionId(String accountId, String username, String password); }
    
@Service @RequiredArgsConstructor public class SessionIDServiceImpl implements SessionIDService { private final DllApiService dll; @Override public Mono
    
      getSessionId(String accountId, String username, String password) { return dll.initialiseWithID(accountId) .flatMap(t4 -> { switch (t4.getT1()) { case -1: return just(of(false, t4.getT3(), SERVICE_UNAVAILABLE)); case 1: { return dll.logon(t4.getT2(), username, password) .map(t3 -> { switch (t3.getT1()) { case -1: return of(false, t3.getT3(), SERVICE_UNAVAILABLE); case 1: return of(true, t3.getT2(), OK); case 2: case 4: return of(false, t3.getT3(), FORBIDDEN); default: return of(false, t3.getT3(), BAD_REQUEST); } }); } case 4: return just(of(true, t4.getT2(), OK)); default: return just(of(false, t4.getT3(), BAD_REQUEST)); } }); } }
    

API Facade

The next step is to design our web application API. It should represent and encapsulate our interaction with the COM Interface API:

@Configuration public class DllApiRouter { @Bean public RouterFunction dllApiRoute(DllApiRouterHandler handler) { return RouterFunctions.route(GET("/api/sessions/{accountId}"), handler::sessionId) .andRoute(GET("/api/users/{matchTerm}"), handler::matchedUsers); } }

Besides the Router class, let's define an implementation of its handler with logic for retrieving the SessionID and the user records data.

For the second scenario, to be able to make a DLL getMatchedUsers API call according to the design, let's use the mandatory header X-SESSION-ID:

@Slf4j @Component @RequiredArgsConstructor public class DllApiRouterHandler { private static final String SESSION_ID_HDR = "X-SESSION-ID"; private final DllApiService service; private final AccountRepo accountRepo; private final SessionIDService sessionService; public Mono sessionId(ServerRequest request) { final String accountId = request.pathVariable("accountId"); return accountRepo.findById(accountId) .flatMap(acc -> sessionService.getSessionId(accountId, acc.getApiUsername(), acc.getApiPassword())) .doOnEach(logNext(t3 -> { if (t3.getT1()) { log.info(format("SessionId to return %s", t3.getT2())); } else { log.warn(format("Session Id could not be retrieved. Cause: %s", t3.getT2())); } })) .flatMap(t3 -> status(t3.getT3()).contentType(APPLICATION_JSON) .bodyValue(t3.getT1() ? t3.getT2() : Response.error(t3.getT2()))) .switchIfEmpty(Mono.just("Account could not be found with provided ID " + accountId) .doOnEach(logNext(log::info)) .flatMap(msg -> badRequest().bodyValue(Response.error(msg)))); } public Mono matchedUsers(ServerRequest request) { return sessionIdHeader(request).map(sId -> Tuples.of(sId, request.queryParam("matchTerm") .orElseThrow(() -> new IllegalArgumentException( "matchTerm query param should be specified")))) .flatMap(t2 -> service.getMatchedUsers(t2.getT1(), t2.getT2())) .flatMap(this::handleT3) .onErrorResume(IllegalArgumentException.class, this::handleIllegalArgumentException); } private Mono sessionIdHeader(ServerRequest request) { return Mono.justOrEmpty(request.headers() .header(SESSION_ID_HDR) .stream() .findFirst() .orElseThrow(() -> new IllegalArgumentException(SESSION_ID_HDR + " header is mandatory"))); } private Mono handleT3(Tuple3 t3) { switch (t3.getT1()) { case 1: return ok().contentType(APPLICATION_JSON) .bodyValue(t3.getT2()); case 2: return status(FORBIDDEN).contentType(APPLICATION_JSON) .bodyValue(Response.error(t3.getT3())); default: return badRequest().contentType(APPLICATION_JSON) .bodyValue(Response.error(t3.getT3())); } } private Mono handleIllegalArgumentException(IllegalArgumentException e) { return Mono.just(Response.error(e.getMessage())) .doOnEach(logNext(res -> log.info(String.join(",", res.getErrors())))) .flatMap(res -> badRequest().contentType(MediaType.APPLICATION_JSON) .bodyValue(res)); } @Getter @Setter @NoArgsConstructor public static class Response implements Serializable { private String message; private Set errors; private Response(Set errors) { this.errors = errors; } public static Response error(String error) { return new Response(singleton(error)); } } }

Account Entity

As you might have noticed, we've imported AccountRepo in the router's handler to find the entity in a database by the provided accountId. This lets us get the corresponding API user credentials and use all three in the DLL Logon API call.

To get a clearer picture, let's define the managed Account entity as well:

@TypeAlias("Account") @Document(collection = "accounts") public class Account { @Version private Long version; /** * unique account ID for API, provided by supplier * defines restriction for data domain visibility * i.e. data from one account is not visible for another */ @Id private String accountId; /** * COM API username, provided by supplier */ private String apiUsername; /** * COM API password, provided by supplier */ private String apiPassword; @CreatedDate private Date createdAt; @LastModifiedDate private Date updatedOn; }

The JACOB Library Setup

All parts of our application are ready now except the core – the configuration and use of the JACOB library. Let's start with setting up the library.

The library is distributed via sourceforge.net. I did not find it available anywhere on either the Central Maven Repo or any other repositories online. So I decided to import it manually into our project as a local package.

To do that, I downloaded it and put it in the root folder under /libs/jacob-1.19.

After that, put the following maven-install-plugin configuration into pom.xml. This will add the library to the local repository during Maven's install build phase:

 org.apache.maven.plugins maven-install-plugin   install-jacob validate  ${basedir}/libs/jacob-1.19/jacob.jar default net.sf.jacob-project jacob 1.19 jar true   install-file    

That will let you easily add the dependency as usual:

 net.sf.jacob-project jacob 1.19 

The library import is finished. Now let's get it ready to use it.

To interact with the COM component, JACOB provides a wrapper called an ActiveXComponent class (as I mentioned before).

It has a method called invoke(String function, Variant... args) that lets us make exactly what we want.

Generally speaking, our library is set up to create the ActiveXComponent bean so we can use it anywhere we want in the app (and we want it in the implementation of DllApiService).

So let's define a separate Spring @Configuration with all the essential preparations:

@Slf4j @Configuration public class JacobCOMConfiguration { private static final String COM_INTERFACE_NAME = "NAME_OF_COM_INTERFACE_AS_IN_REGISTRY"; private static final String JACOB_LIB_PATH = System.getProperty("user.dir") + "\\libs\\jacob-1.19"; private static final String LIB_FILE = System.getProperty("os.arch") .equals("amd64") ? "\\jacob-1.19-x64.dll" : "\\jacob-1.19-x86.dll"; private File temporaryDll; static { log.info("JACOB lib path: {}", JACOB_LIB_PATH); log.info("JACOB file lib path: {}", JACOB_LIB_PATH + LIB_FILE); System.setProperty("java.library.path", JACOB_LIB_PATH); System.setProperty("com.jacob.debug", "true"); } @PostConstruct public void init() throws IOException { InputStream inputStream = new FileInputStream(JACOB_LIB_PATH + LIB_FILE); temporaryDll = File.createTempFile("jacob", ".dll"); FileOutputStream outputStream = new FileOutputStream(temporaryDll); byte[] array = new byte[8192]; for (int i = inputStream.read(array); i != -1; i = inputStream.read(array)) { outputStream.write(array, 0, i); } outputStream.close(); System.setProperty(LibraryLoader.JACOB_DLL_PATH, temporaryDll.getAbsolutePath()); LibraryLoader.loadJacobLibrary(); log.info("JACOB library is loaded and ready to use"); } @Bean public ActiveXComponent dllAPI() { ActiveXComponent activeXComponent = new ActiveXComponent(COM_INTERFACE_NAME); log.info("API COM interface {} wrapped into ActiveXComponent is created and ready to use", COM_INTERFACE_NAME); return activeXComponent; } @PreDestroy public void clean() { temporaryDll.deleteOnExit(); log.info("Temporary DLL API library is cleaned on exit"); } }

It's worth mentioning that, besides defining the bean, we initialize the library components based on the host machine's ISA (instruction set architecture).

Also, we follow some common recommendations to make a copy of the corresponding library's file. This avoids any potential corruption of the original file during runtime. We also need to cleanup all allocated resources when the applications terminates.

Now the library is set up and ready to use. Finally, we can implement our last main component that helps us interact with the DLL API:  DllApiServiceImpl.

How to Implement a DLL Library API Service

As all COM API calls are going to be cooked using a common approach, let's implement InitialiseWithID first. After that, all other methods can be implemented easily in a similar way.

Όπως ανέφερα προηγουμένως, για να αλληλεπιδράσουμε με τη διεπαφή COM, το JACOB μας παρέχει την ActiveXComponentκλάση που έχει τη invoke(String function, Variant... args)μέθοδο.

Αν θέλετε να μάθετε περισσότερα για την Variantτάξη, η τεκμηρίωση JACOB λέει τα εξής (μπορείτε να τα βρείτε στο αρχείο ή /libs/jacob-1.19στο έργο):

Ο τύπος δεδομένων πολλαπλών μορφών που χρησιμοποιείται για όλες τις επιστροφές κλήσεων και τις περισσότερες επικοινωνίες μεταξύ Java και COM. Παρέχει μία μόνο κατηγορία που μπορεί να χειριστεί όλους τους τύπους δεδομένων.

Αυτό σημαίνει ότι όλα τα ορίσματα που ορίζονται στην InitialiseWithIDυπογραφή θα πρέπει να είναι τυλιγμένα new Variant(java.lang.Object in)και να μεταβιβάζονται στη invokeμέθοδο. Χρησιμοποιήστε την ίδια σειρά όπως ορίζεται στην περιγραφή διεπαφής στην αρχή αυτού του άρθρου.

The only other important thing we haven't touched on yet is how to distinguish in and out type arguments.

For that purpose, Variant provides a constructor that accepts the data object and information about whether this is by reference or not. This means that after invoke is called, all variants that were initialized as references can be accessed after the call. So we can extract the results from out arguments.

To do that, just pass an extra boolean variable to the constructor as the second parameter: new Variant(java.lang.Object pValueObject, boolean fByRef).

Initializing the Variant object as reference puts an additional requirement on the client to decide when to release the value (so it can be scrapped by the garbage collector).

For that purpose, you have the safeRelease() method that is supposed to be called when the value is taken from the corresponding Variant object.

Putting all the pieces together gives us the following service's implementation:

@RequiredArgsConstructor public class DllApiServiceImpl implements DllApiService { @Value("${DLL_API_ADDRESS}") private String address; private final ActiveXComponent dll; @Override public Mono
    
      initialiseWithID(final String accountId) { return Mono.just(format("Calling %s(%s, %s, %s, %s, %s, %s)",// InitialiseWithID, address, accountId, "loginId/out", "error/out", "outcome/out", "sessionId/out")) .doOnEach(logNext(log::info)) //invoke COM interface method and extract the result mapping it onto corresponding *Out inner class .map(msg -> invoke(InitialiseWithID, vars -> InitialiseWithIDOut.builder() .loginId(vars[3].toString()) .error(vars[4].toString()) .outcome(valueOf(vars[5].toString())) .sessionId(vars[6].toString()) .build(), // new Variant(address), new Variant(accountId), initRef(), initRef(), initRef(), initRef())) //Handle the response according to the documentation .map(out -> { final String errorVal; switch (out.outcome) { case 2: errorVal = "InitialiseWithID method call failed. DLL API request outcome (response code from server via DLL) = 2 " +// "(Unable to connect to server due to absent server, or incorrect details)"; break; case 3: errorVal = "InitialiseWithID method call failed. DLL API request outcome (response code from server via DLLe) = 3 (Unmatched AccountID)"; break; default: errorVal = handleOutcome(out.outcome, out.error, InitialiseWithID); } return of(out, errorVal); }) .doOnEach(logNext(t2 -> { InitialiseWithIDOut out = t2.getT1(); log.info("{} API call result:\noutcome: {}\nsessionId: {}\nerror: {}\nloginId: {}",// InitialiseWithID, out.outcome, out.sessionId, t2.getT2(), out.loginId); })) .map(t2 -> { InitialiseWithIDOut out = t2.getT1(); //out.outcome == 4 auto-login successful, SessionID is retrieved return of(out.outcome, out.outcome == 4 ? out.sessionId : out.loginId, t2.getT2()); }); } private static Variant initRef() { return new Variant("", true); } private static String handleOutcome(Integer outcome, String error, COM_API_Method method) { switch (outcome) { case 1: return "no error"; case 2: return format("%s method call failed. DLL API request outcome (response code from server via DLL) = 2 (Access denied)", method); default: return format("%s method call failed. DLL API request outcome (response code from server via DLL) = %s (server technical error). " + // "DLL API is temporary unavailable (server behind is down), %s", method, outcome, error); } } /** * @param method to be called in COM interface * @param returnFunc maps Variants (references) array onto result object that is to be returned by the method * @param vars arguments required for calling COM interface method * @param type of the result object that is to be returned by the method * @return result of the COM API method invocation in defined format */ private T invoke(COM_API_Method method, Function returnFunc, Variant... vars) { dll.invoke(method.name(), vars); T res = returnFunc.apply(vars); asList(vars).forEach(Variant::safeRelease); return res; } @SuperBuilder private static abstract class Out { final Integer outcome; final String error; } @SuperBuilder private static class InitialiseWithIDOut extends Out { final String loginId; final String sessionId; }
    

Two other methods, Logon and getMatchedUsers, are implemented accordingly. You can refer to my GitHub repo for a complete version of the service if you want to check it out.

Congratulations – You've Learned a Few Things

We've gone through a step by step scenario that showed us how a hypothetical COM API could be distributed and called in Java.

We also learned how the JACOB library can be configured and effectively used to interact with a DDL library within your Spring Boot 2 application.

Μια μικρή βελτίωση θα ήταν η προσωρινή αποθήκευση του ανακτημένου SessionID που θα μπορούσε να βελτιώσει τη γενική ροή. Αλλά αυτό είναι λίγο έξω από το πεδίο εφαρμογής αυτού του άρθρου.

Εάν θέλετε να διερευνήσετε περαιτέρω, μπορείτε να το βρείτε στο GitHub όπου εφαρμόζεται χρησιμοποιώντας τον μηχανισμό προσωρινής αποθήκευσης της Spring.

Ελπίζω να απολαύσατε τα πάντα μαζί μου και βρήκατε αυτό το σεμινάριο χρήσιμο!