دليل واجهة برمجة تطبيقات COM: دمج مكتبة JACOB مع Spring Boot في Java

دقائق القراءة: 16

في هذا المقال، سنستعرض خطوة بخطوة كيفية دمج مكتبة JACOB القوية ضمن تطبيق Spring Boot الخاص بك المكتوب بلغة Java. سيمكنك هذا الدمج من استدعاء واجهات برمجة تطبيقات COM interface API عبر مكتبات DLL في تطبيقات الويب الخاصة بك. ولتوضيح المفهوم بشكل عملي، سنقدم وصفًا لواجهة برمجة تطبيقات COM API افتراضية يمكنك البناء عليها لتطوير تطبيقك.

يمكنك العثور على جميع مقتطفات التعليمات البرمجية المستخدمة في هذا الشرح في مستودع GitHub المخصص.

ملاحظة سريعة: في C the Signs، قمنا بنشر حل مماثل أتاح لنا التكامل مع نظام EMIS Health، وهو نظام سجلات المرضى الإلكترونية المستخدم في الرعاية الأولية بالمملكة المتحدة. استخدمنا مكتبة DLL التي قدموها لهذا التكامل. النهج الذي سنعرضه هنا (مع تنقيحه لتجنب تسريب أي معلومات حساسة) تم نشره في بيئة الإنتاج منذ أكثر من عامين، وقد أثبت متانته وفعاليته. نظرًا لأننا اعتمدنا مؤخرًا نهجًا جديدًا تمامًا للتكامل مع EMIS، فسيتم إيقاف النظام القديم في غضون شهر أو شهرين. لذا، يُعد هذا الدليل بمثابة “أغنية البجعة” لهذا النظام، وداعًا أيها الأمير الصغير.

ما هي واجهة برمجة تطبيقات DLL؟

لنبدأ بوصف واضح لمكتبة DLL (مكتبة الربط الديناميكي). لهذا الغرض، أعددت نموذجًا مبسطًا للوثائق التقنية الأصلية. دعنا نلقي نظرة على الأساليب الثلاثة الرئيسية لواجهة COM:

1. طريقة InitialiseWithID

تُعد هذه الطريقة ميزة أمان مطلوبة في الموقع، وتسمح لنا بالحصول على اتصال بخادم API الذي نرغب في دمجه مع المكتبة. تتطلب هذه الطريقة AccountID (معرف فريد عالمي GUID) لمستخدم API الحالي (للوصول إلى الخادم) وبعض وسائط التهيئة الأخرى المذكورة أدناه. تدعم هذه الدالة أيضًا ميزة تسجيل الدخول التلقائي (auto-login).

إذا كان العميل يمتلك نسخة مسجلة الدخول من النظام قيد التشغيل (المكتبة جزء من هذا النظام) ويستدعي الطريقة على نفس المضيف، فستقوم واجهة API بإكمال تسجيل الدخول تلقائيًا تحت حساب هذا المستخدم. بعد ذلك، ستُرجع SessionID للمكالمات اللاحقة لواجهة API. بخلاف ذلك، يحتاج العميل إلى المتابعة باستخدام دالة Logon (انظر الجزء التالي) باستخدام LoginID المُعاد.

لاستدعاء هذه الدالة، استخدم الاسم InitialiseWithID مع الوسائط التالية:

الاسم (Name) إدخال/إخراج (In/out) النوع (Type) الوصف (Description)
address In String عنوان IP لخادم التكامل المقدم
AccountID In String سلسلة GUID فريدة مقدمة
LoginID Out String سلسلة GUID تُستخدم لاستدعاء API الخاص بتسجيل الدخول (Logon)
Error Out String وصف الخطأ
Outcome Out Integer
  • -1 = ارجع إلى الخطأ
  • 1 = تهيئة ناجحة بانتظار تسجيل الدخول
  • 2 = تعذر الاتصال بالخادم بسبب غياب الخادم أو تفاصيل غير صحيحة
  • 3 = AccountID غير مطابق
  • 4 = تسجيل الدخول التلقائي ناجح
SessionID Out String GUID يُستخدم للتفاعلات اللاحقة (إذا كان تسجيل الدخول التلقائي ناجحًا)

2. طريقة Logon

تحدد هذه الطريقة صلاحية المستخدم. اسم المستخدم هنا هو المعرف المستخدم لتسجيل الدخول إلى النظام، وكلمة المرور هي كلمة مرور API المحددة لذلك المستخدم. في حالة النجاح، تُرجع المكالمة سلسلة SessionID (معرف فريد عالمي GUID) يجب تمريرها إلى المكالمات اللاحقة للمصادقة عليها.

لاستدعاء هذه الدالة، استخدم الاسم Logon مع الوسائط التالية:

الاسم (Name) إدخال/إخراج (In/out) النوع (Type) الوصف (Description)
LoginID In String معرف تسجيل الدخول المُعاد بواسطة طريقة التهيئة InitialiseWithID
username In String اسم مستخدم API المقدم
password In String كلمة مرور API المقدمة
SessionID Out String GUID يُستخدم للتفاعلات اللاحقة (إذا كان تسجيل الدخول ناجحًا)
Error Out String وصف الخطأ
Outcome Out Integer
  • -1 = خطأ تقني
  • 1 = ناجح
  • 2 = منتهي الصلاحية
  • 3 = غير ناجح
  • 4 = معرف تسجيل دخول غير صالح أو لا يملك صلاحية الوصول لهذا المنتج

3. طريقة getMatchedUsers

تتيح لك هذه المكالمة العثور على سجلات بيانات المستخدم التي تتطابق مع معايير محددة. يمكن أن يشير مصطلح البحث إلى حقل واحد فقط في كل مرة، مثل اسم العائلة أو الاسم الأول أو تاريخ الميلاد. تُرجع المكالمة الناجحة سلسلة XML تحتوي على البيانات.

لاستدعاء هذه الدالة، استخدم الاسم getMatchedUsers مع الوسائط التالية:

الاسم (Name) إدخال/إخراج (In/out) النوع (Type) الوصف (Description)
SessionID In String معرف الجلسة المُعاد بواسطة طريقة تسجيل الدخول (Logon)
MatchTerm In String مصطلح البحث
MatchedList Out String سلسلة XML متوافقة مع مخطط XSD المقابل المقدم
SessionID Out String GUID يُستخدم للتفاعلات اللاحقة (إذا كان تسجيل الدخول ناجحًا)
Error Out String وصف الخطأ
Outcome Out Integer
  • -1 = خطأ تقني
  • 1 = تم العثور على مستخدمين
  • 2 = تم رفض الوصول
  • 3 = لا يوجد مستخدمون

سير عمل تطبيق مكتبة DLL

لتسهيل فهم ما نهدف إلى تنفيذه، قمنا بإنشاء مخطط سير عمل بسيط. يصف هذا المخطط سيناريو خطوة بخطوة لكيفية تفاعل عميل الويب مع تطبيقنا القائم على الخادم باستخدام واجهة برمجة التطبيقات الخاصة به. إنه يغلف التفاعل مع مكتبة DLL ويسمح لنا بالحصول على مستخدمين افتراضيين بمعيار البحث المحدد:

مخطط سير عمل تطبيق يوضح تفاعل عميل الويب مع خادم التطبيق ومكتبة DLL لاسترداد بيانات المستخدمين.

مخطط سير عمل التطبيق

تسجيل مكونات COM

الآن، دعنا نتعرف على كيفية الوصول إلى مكتبة DLL. لكي نتمكن من التفاعل مع واجهة COM خارجية، يجب إضافتها إلى سجل النظام (registry). إليك ما تقوله الوثائق بهذا الشأن:

سجل النظام هو قاعدة بيانات تحتوي على معلومات حول تكوين أجهزة وبرامج النظام، بالإضافة إلى معلومات حول مستخدمي النظام. يمكن لأي برنامج يعمل بنظام Windows إضافة معلومات إلى السجل وقراءة المعلومات منه. يبحث العملاء في السجل عن المكونات التي يرغبون في استخدامها. يحتفظ السجل بمعلومات حول جميع كائنات COM المثبتة في النظام. عندما يقوم تطبيق بإنشاء مثيل لمكون COM، يتم استشارة السجل لحل CLSID أو ProgID للمكون إلى مسار ملف DLL أو EXE الخاص بالخادم الذي يحتويه. بعد تحديد خادم المكون، يقوم Windows إما بتحميل الخادم في مساحة عملية تطبيق العميل (مكونات داخل العملية) أو يبدأ الخادم في مساحة عمليته الخاصة (الخوادم المحلية والبعيدة). يقوم الخادم بإنشاء مثيل للمكون وإرجاع مرجع إلى إحدى واجهات المكون للعميل.

لتعلم كيفية القيام بذلك، تشير وثائق Microsoft الرسمية إلى:

يمكنك تشغيل أداة سطر الأوامر تسمى أداة تسجيل التجميع (Regasm.exe) لتسجيل أو إلغاء تسجيل تجميع لاستخدامه مع COM. يضيف Regasm.exe معلومات حول الفئة إلى سجل النظام حتى يتمكن عملاء COM من استخدام فئة .NET Framework بشفافية. توفر فئة RegistrationServices وظائف مكافئة. يجب تسجيل مكون مُدار في سجل Windows قبل أن يتم تنشيطه من عميل COM.

تأكد من أن جهازك المضيف قد قام بتثبيت مكونات .NET Framework المطلوبة. بعد ذلك، يمكنك تنفيذ أمر سطر الأوامر التالي:

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

ستظهر رسالة تشير إلى ما إذا كان الملف قد تم تسجيله بنجاح. الآن نحن جاهزون للخطوة التالية.

تحديد البنية الأساسية للتطبيق

خدمة DllApiService

أولاً وقبل كل شيء، دعنا نحدد الواجهة التي تصف مكتبة DLL الخاصة بنا كما هي:

 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<Tuple3<Integer, String, String>> 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<Tuple3<Integer, String, String>> 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<Tuple3<Integer, String, String>> getMatchedUsers(String sessionId, String matchTerm);

    enum COM_API_Method {
        InitialiseWithID,
        Logon,
        getMatchedUsers
    }
 }

كما قد لاحظت، تتطابق جميع الطرق مع تعريف واجهة COM Interface الموصوفة أعلاه، باستثناء دالة initialiseWithID. لقد قررت حذف المتغير address من التوقيع (عنوان IP لخادم التكامل) وإدخاله كمتغير بيئة (environment variable) سنقوم بتنفيذه لاحقًا.

شرح خدمة SessionIDService

لكي نتمكن من استرداد أي بيانات باستخدام المكتبة، نحتاج أولاً إلى الحصول على SessionID. وفقًا لمخطط سير العمل أعلاه، يتضمن ذلك استدعاء طريقة initialiseWithID أولاً. بعد ذلك، وبناءً على النتيجة، سنحصل إما على SessionID أو LoginID لاستخدامه في مكالمات Logon اللاحقة. لذا، هذه في الأساس عملية من خطوتين خلف الكواليس.

الآن، دعنا ننشئ الواجهة، وبعد ذلك، التنفيذ:

 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<Tuple3<Boolean, String, HttpStatus>> getSessionId(String accountId, String username, String password);
 }
 @Service
 @RequiredArgsConstructor
 public class SessionIDServiceImpl implements SessionIDService {

    private final DllApiService dll;

    @Override
    public Mono<Tuple3<Boolean, String, HttpStatus>> 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)

الخطوة التالية هي تصميم واجهة برمجة تطبيقات تطبيق الويب الخاص بنا. يجب أن تمثل وتغلف تفاعلنا مع واجهة برمجة تطبيقات COM Interface API:

 @Configuration
 public class DllApiRouter {

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

بالإضافة إلى فئة Router، دعنا نحدد تنفيذًا لمعالجها (handler) مع المنطق الخاص باسترداد SessionID وبيانات سجلات المستخدم. للسيناريو الثاني، لكي نتمكن من إجراء استدعاء getMatchedUsers الخاص بمكتبة DLL وفقًا للتصميم، دعنا نستخدم العنوان الإلزامي 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<ServerResponse> 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<ServerResponse> 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<String> 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<ServerResponse> handleT3 (Tuple3<Integer, String, String> 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<ServerResponse> 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<String> errors;

        private Response (Set<String> errors) {
            this.errors = errors;
        }

        public static Response error (String error) {
            return new Response(singleton(error));
        }
    }
 }

كيان الحساب (Account Entity)

كما لاحظت، قمنا باستيراد AccountRepo في معالج الموجه (router's handler) للعثور على الكيان في قاعدة البيانات بواسطة accountId المقدم. يتيح لنا هذا الحصول على بيانات اعتماد مستخدم API المقابلة واستخدامها جميعًا في استدعاء Logon API الخاص بمكتبة DLL. للحصول على صورة أوضح، دعنا نحدد كيان Account المُدار أيضًا:

 @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;
 }

إعداد مكتبة JACOB

جميع أجزاء تطبيقنا جاهزة الآن باستثناء الجزء الأساسي – تهيئة واستخدام مكتبة JACOB. لنبدأ بإعداد المكتبة. يتم توزيع المكتبة عبر sourceforge.net. لم أجدها متاحة في مستودع Central Maven Repo أو أي مستودعات أخرى عبر الإنترنت. لذلك، قررت استيرادها يدويًا إلى مشروعنا كحزمة محلية. للقيام بذلك، قمت بتنزيلها ووضعها في المجلد الجذر تحت المسار /libs/jacob-1.19.

بعد ذلك، ضع تهيئة maven-install-plugin التالية في ملف pom.xml. سيؤدي هذا إلى إضافة المكتبة إلى المستودع المحلي خلال مرحلة بناء install في Maven:

 <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-install-plugin</artifactId>
    <executions>
        <execution>
            <id>install-jacob</id>
            <phase>validate</phase>
            <configuration>
                <file>${basedir}/libs/jacob-1.19/jacob.jar</file>
                <repositoryLayout>default</repositoryLayout>
                <groupId>net.sf.jacob-project</groupId>
                <artifactId>jacob</artifactId>
                <version>1.19</version>
                <packaging>jar</packaging>
                <generatePom>true</generatePom>
            </configuration>
            <goals>
                <goal>install-file</goal>
            </goals>
        </execution>
    </executions>
 </plugin>

سيسمح لك ذلك بإضافة التبعية (dependency) بسهولة كالمعتاد:

 <dependency>
    <groupId>net.sf.jacob-project</groupId>
    <artifactId>jacob</artifactId>
    <version>1.19</version>
 </dependency>

اكتمل استيراد المكتبة. الآن دعنا نجهزها للاستخدام. للتفاعل مع مكون COM، توفر JACOB غلافًا يسمى فئة ActiveXComponent. تحتوي هذه الفئة على طريقة تسمى invoke(String function, Variant... args) تتيح لنا تنفيذ ما نريده بالضبط.

بشكل عام، تم إعداد مكتبتنا لإنشاء bean من ActiveXComponent حتى نتمكن من استخدامه في أي مكان في التطبيق (ونحن نريده في تنفيذ DllApiService). لذا، دعنا نحدد @Configuration منفصلة في Spring مع جميع التحضيرات الأساسية:

 @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");
    }
 }

تجدر الإشارة إلى أنه بالإضافة إلى تحديد bean، نقوم بتهيئة مكونات المكتبة بناءً على بنية مجموعة التعليمات (ISA) للجهاز المضيف. كما نتبع بعض التوصيات الشائعة لإنشاء نسخة من ملف المكتبة المقابل. هذا يتجنب أي تلف محتمل للملف الأصلي أثناء وقت التشغيل. نحتاج أيضًا إلى تنظيف جميع الموارد المخصصة عند إنهاء التطبيق.

الآن تم إعداد المكتبة وجاهزة للاستخدام. أخيرًا، يمكننا تنفيذ المكون الرئيسي الأخير الذي يساعدنا في التفاعل مع DLL API: DllApiServiceImpl.

كيفية تنفيذ خدمة API لمكتبة DLL

نظرًا لأن جميع استدعاءات COM API سيتم إعدادها باستخدام نهج مشترك، فلنقم بتنفيذ InitialiseWithID أولاً. بعد ذلك، يمكن تنفيذ جميع الطرق الأخرى بسهولة بطريقة مماثلة.

كما ذكرت سابقًا، للتفاعل مع واجهة COM، توفر لنا JACOB فئة ActiveXComponent التي تحتوي على طريقة invoke(String function, Variant... args). إذا كنت ترغب في معرفة المزيد عن فئة Variant، فإن وثائق JACOB تقول ما يلي (يمكنك العثور عليها في الأرشيف أو تحت /libs/jacob-1.19 في المشروع):

نوع البيانات متعدد التنسيقات المستخدم لجميع استدعاءات رد الاتصال (call backs) ومعظم الاتصالات بين Java و COM. يوفر فئة واحدة يمكنها التعامل مع جميع أنواع البيانات.

هذا يعني أن جميع الوسائط المعرفة في توقيع InitialiseWithID يجب أن تُغلف بـ new Variant(java.lang.Object in) وتُمرر إلى طريقة invoke. استخدم نفس الترتيب المحدد في وصف الواجهة في بداية هذا المقال.

الشيء المهم الآخر الوحيد الذي لم نتطرق إليه بعد هو كيفية التمييز بين وسائط النوع in و out. لهذا الغرض، توفر Variant مُنشئًا يقبل كائن البيانات ومعلومات حول ما إذا كان هذا بالمرجع أم لا. هذا يعني أنه بعد استدعاء invoke، يمكن الوصول إلى جميع المتغيرات التي تم تهيئتها كمرجع بعد الاستدعاء. لذلك يمكننا استخراج النتائج من وسائط out. للقيام بذلك، ما عليك سوى تمرير متغير منطقي إضافي (boolean) إلى المُنشئ كمعامل ثانٍ: new Variant(java.lang.Object pValueObject, boolean fByRef).

تهيئة كائن Variant كمرجع تضع متطلبًا إضافيًا على العميل لتحديد متى يتم تحرير القيمة (حتى يمكن جمعها بواسطة جامع القمامة garbage collector). لهذا الغرض، لديك طريقة safeRelease() التي من المفترض أن تُستدعى عندما يتم أخذ القيمة من كائن Variant المقابل.

بجمع كل القطع معًا، نحصل على تنفيذ الخدمة التالي:

 @RequiredArgsConstructor
 public class DllApiServiceImpl implements DllApiService {

    @Value("${DLL_API_ADDRESS}")
    private String address;

    private final ActiveXComponent dll;

    @Override
    public Mono<Tuple3<Integer, String, String>> initialiseWithID(
            final String accountId) {
        return Mono.just(format("Calling %s(%s, %s, %s, %s, %s, %s)",
                        // InitialiseWithID,
                        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 <T> 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 extends Out> T invoke (COM_API_Method method, Function<Variant[], T> 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;
    }
 }

تم تنفيذ الطريقتين الأخريين، Logon و getMatchedUsers، وفقًا لذلك. يمكنك الرجوع إلى مستودع GitHub الخاص بي للحصول على الإصدار الكامل للخدمة إذا كنت ترغب في التحقق منه.

الخلاصة التقنية

لقد استعرضنا في هذا الدليل سيناريو تفصيليًا يوضح كيفية توزيع واستدعاء واجهة برمجة تطبيقات COM افتراضية في Java. تعلمنا أيضًا كيفية تهيئة مكتبة JACOB واستخدامها بفعالية للتفاعل مع مكتبة DLL ضمن تطبيق Spring Boot 2 الخاص بك. من التحسينات الممكنة التي يمكن إجراؤها هي تخزين SessionID مؤقتًا (caching)، مما قد يحسن سير العمل العام. ومع ذلك، يقع هذا خارج نطاق هذا المقال، ولكن إذا كنت ترغب في استكشاف المزيد، يمكنك العثور على تنفيذه باستخدام آلية التخزين المؤقت في Spring على GitHub. نأمل أن تكون قد استمتعت بهذا الشرح ووجدت هذا الدليل مفيدًا!

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *