У цій статті я покажу вам, як вбудувати бібліотеку JACOB у програму Spring Boot. Це допоможе вам викликати API інтерфейсу COM через бібліотеку DLL у вашому веб-додатку.
Крім того, для ілюстративних цілей я надам опис COM API, щоб ви змогли побудувати свою програму поверх неї. Ви можете знайти всі фрагменти коду в цьому репозиторії GitHub.
Але спочатку коротке зауваження: на C Signs ми застосували це рішення, яке дозволило нам інтегруватися з EMIS Health. Це електронна система обліку пацієнтів, яка використовується у первинній медичній допомозі у Великобританії. Для інтеграції ми використовували надану ними бібліотеку DLL.
Підхід, який я вам покажу тут (дезінфікований, щоб уникнути витоку будь-якої конфіденційної інформації), впроваджений у виробництво більше двох років тому, і з тих пір довів свою довговічність.
Оскільки нещодавно ми застосували абсолютно новий підхід до інтеграції з EMIS, стару систему буде закрито через місяць-два. Тож цей підручник - це його пісня лебідь. Спи, мій маленький принце.
Що таке DLL API?
Спочатку почнемо з чіткого опису бібліотеки DLL. Для цього я підготував короткий макет оригінальної технічної документації.
Давайте розглянемо його, щоб побачити, що таке три методи інтерфейсу COM.
Метод InitialiseWithID
Цей метод - це функція безпеки, необхідна на місці, яка дозволяє нам отримати зв’язок із сервером API, який ми хочемо інтегрувати з бібліотекою.
Для цього потрібен AccountID
(GUID) поточного користувача API (для доступу до сервера) та деякі інші аргументи ініціалізації, перелічені нижче.
Ця функція також підтримує функцію автоматичного входу. Якщо клієнт має зареєстровану версію запущеної системи (бібліотека є частиною цієї системи) і викликає метод на тому самому хості, API автоматично завершить вхід під обліковим записом цього користувача. Потім він поверне SessionID
для наступних викликів API.
В іншому випадку клієнту потрібно продовжити Logon
функцію (див. Наступну частину), використовуючи повернене LoginID
.
Для виклику функції використовуйте ім'я InitialiseWithID
з такими аргументами:
Ім'я | In out | Тип | Опис |
---|---|---|---|
адресу | В | Рядок | наданий IP-сервер інтеграції |
Номер рахунку | В | Рядок | надано унікальний рядок GUID |
Ім'я користувача | Вийшов | Рядок | Рядок GUID, який використовується для виклику API входу |
Помилка | Вийшов | Рядок | Опис помилки |
Результат | Вийшов | Ціле число | -1 = Посилання на помилку 1 = Успішна ініціалізація в очікуванні входу 2 = Неможливо підключитися до сервера через відсутність сервера або неправильні дані 3 = Невідповідний ідентифікатор облікового запису 4 = Автовхід успішний |
Ідентифікатор сеансу | Вийшов | Рядок | GUID, що використовується для подальших взаємодій (якщо авторизація успішна) |
Метод входу
Цей метод визначає повноваження користувача. Ім'я користувача - це ідентифікатор, який використовується для входу в систему. Пароль - це пароль API, встановлений для цього імені користувача.
У сценарії успіху виклик повертає SessionID
рядок (GUID), який повинен бути переданий іншим наступним викликам для їх автентифікації.
Для виклику функції використовуйте ім'я Logon
з такими аргументами:
Ім'я | In out | Тип | Опис |
---|---|---|---|
Ім'я користувача | В | Рядок | Ідентифікатор входу, повернутий методом ініціалізації Initialise with ID |
ім'я користувача | В | Рядок | надане ім’я користувача API |
пароль | В | Рядок | наданий пароль API |
Ідентифікатор сеансу | Вийшов | Рядок | GUID, що використовується для подальших взаємодій (якщо вхід успішний) |
Помилка | Вийшов | Рядок | Опис помилки |
Результат | Вийшов | Ціле число | -1 = Технічна помилка 1 = Успішно 2 = Термін дії закінчився 3 = Невдало 4 = Недійсний ідентифікатор для входу або ідентифікатор для входу не має доступу до цього продукту |
Метод getMatchedUsers
Цей дзвінок дозволяє знаходити записи даних користувачів, які відповідають певним критеріям. Термін пошуку може посилатися лише на одне поле одночасно, наприклад, прізвище, ім’я чи дату народження.
Успішний виклик повертає рядок XML із даними в ньому.
Для виклику функції використовуйте ім'я getMatchedUsers
з такими аргументами:
Ім'я | In out | Тип | Опис |
---|---|---|---|
Ідентифікатор сеансу | В | Рядок | Ідентифікатор сеансу, повернутий методом входу |
MatchTerm | В | Рядок | Пошуковий термін |
MatchedList | Вийшов | Рядок | XML, що відповідає наданій відповідній схемі XSD |
Ідентифікатор сеансу | Вийшов | Рядок | GUID, що використовується для подальших взаємодій (якщо вхід успішний) |
Помилка | Вийшов | Рядок | Опис помилки |
Результат | Вийшов | Ціле число | -1 = Технічна помилка 1 = Користувачі знайдені 2 = Доступ заборонено 3 = Немає користувачів |
Потік застосунків бібліотеки DLL
Щоб було легше зрозуміти те, що ми хочемо реалізувати, я вирішив створити просту схему.
Він описує покроковий сценарій того, як веб-клієнт може взаємодіяти з нашим серверним додатком за допомогою його API. Він інкапсулює взаємодію з бібліотекою DLL і дозволяє отримувати гіпотетичних користувачів із наданим терміном відповідності (критерії пошуку):

Реєстрація COM
Тепер давайте дізнаємось, як ми можемо отримати доступ до бібліотеки DLL. Щоб мати можливість взаємодіяти зі стороннім інтерфейсом COM, його потрібно додати до реєстру.
Ось що сказано в документах:
Реєстр - це системна база даних, яка містить інформацію про конфігурацію системного обладнання та програмного забезпечення, а також про користувачів системи. Будь-яка програма на базі Windows може додавати інформацію до реєстру та читати інформацію назад із реєстру. Клієнти шукають у реєстрі цікаві компоненти для використання.Реєстр зберігає інформацію про всі COM-об'єкти, встановлені в системі. Кожного разу, коли програма створює екземпляр компонента COM, реєструється консультація, щоб вирішити або CLSID, або ProgID компонента в імені шляху DLL сервера або EXE, що його містить.
Визначивши сервер компонента, Windows або завантажує сервер у робочий простір клієнтської програми (компоненти, що працюють в процесі), або запускає сервер у власному просторі процесів (локальний та віддалений сервери).
Сервер створює екземпляр компонента і повертає клієнту посилання на один з інтерфейсів компонента.
Щоб дізнатись, як це зробити, офіційна документація Microsoft говорить:
Ви можете запустити інструмент командного рядка, який називається інструментом реєстрації збірки (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
З'явиться повідомлення про те, чи файл успішно зареєстровано. Тепер ми готові до наступного кроку.
Визначення основи програми
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 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 } }
Як ви могли помітити, усі методи відображають визначення інтерфейсу COM, описаного вище, за винятком initialiseWithID
функції.
Я вирішив опустити address
змінну в підписі (IP сервера інтеграції) і ввести її як змінну середовища, яку ми будемо реалізовувати.
Пояснення сесії
Щоб отримати будь-які дані за допомогою бібліотеки, спочатку нам потрібно отримати файл 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 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 фасад
Наступним кроком є розробка нашого API веб-додатків. Він повинен представляти та інкапсулювати нашу взаємодію з API інтерфейсу COM:
@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); } }
Окрім Router
класу, давайте визначимо реалізацію його обробника з логікою для отримання ідентифікатора Session і даних користувача.
Для другого сценарію, щоб мати змогу здійснити getMatchedUsers
виклик API 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 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)); } } }
Сутність рахунку
Як ви могли помітити, ми імпортували AccountRepo
в обробник маршрутизатора, щоб знайти сутність у базі даних за наданим 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 :
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
Це дозволить вам легко додати залежність, як зазвичай:
net.sf.jacob-project jacob 1.19
Імпорт бібліотеки завершено. Тепер давайте підготуємо його до використання.
Для взаємодії з компонентом COM JACOB надає обгортку, яка називається ActiveXComponent
класом (як я вже згадував раніше).
У нього є метод, що називається, invoke(String function, Variant... args)
що дозволяє нам робити саме те, що ми хочемо.
Взагалі кажучи, наша бібліотека створена для створення ActiveXComponent
компонента, щоб ми могли використовувати його де завгодно в додатку (і ми хочемо його в реалізації DllApiService
).
Тож давайте визначимо окрему весну @Configuration
з усіма основними препаратами:
@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"); } }
Варто згадати, що, окрім визначення компонента, ми ініціалізуємо компоненти бібліотеки на основі ISA хост-машини (архітектура набору інструкцій).
Крім того, ми дотримуємося деяких загальних рекомендацій щодо копіювання файлу відповідної бібліотеки. Це дозволяє уникнути будь-якого потенційного пошкодження оригінального файлу під час роботи. Нам також потрібно очистити всі виділені ресурси, коли програми припиняються.
Тепер бібліотека налаштована та готова до використання. Нарешті, ми можемо реалізувати наш останній головний компонент , який допомагає нам взаємодіяти з DLL API: DllApiServiceImpl
.
Як реалізувати службу API бібліотеки DLL
Оскільки всі виклики COM API будуть готуватися з використанням загального підходу, давайте реалізуємо InitialiseWithID
спочатку. Після цього всі інші методи можна легко реалізувати подібним чином.
Як я вже згадував раніше, для взаємодії з COM-інтерфейсом JACOB надає нам ActiveXComponent
клас, що має invoke(String function, Variant... args)
метод.
Якщо ви хочете дізнатись більше про Variant
клас, у документації JACOB сказано наступне (ви можете знайти це в архіві або /libs/jacob-1.19
в проекті):
Це означає, що всі аргументи, визначені в InitialiseWithID
підписі, повинні бути обгорнуті new Variant(java.lang.Object in)
та передані invoke
методу. Використовуйте той самий порядок, що вказаний в описі інтерфейсу на початку цієї статті.
Єдине інше важливе, чого ми ще не торкалися, - це як розрізнити in
та out
ввести аргументи.
З цією метою Variant
надає конструктор, який приймає об’єкт даних та інформацію про те, чи є це посиланням чи ні. Це означає, що після invoke
виклику після виклику можна отримати доступ до всіх варіантів, які були ініціалізовані як посилання. Тож ми можемо витягти результати з out
аргументів.
Щоб зробити це, просто передати додаткову логічну змінну конструктору в якості другого параметра: new Variant(java.lang.Object pValueObject, boolean fByRef)
.
Ініціалізація Variant
об’єкта як посилання ставить додаткову вимогу до клієнта вирішувати, коли звільняти значення (щоб воно могло бути скасовано збирачем сміття).
Для цього у вас є safeRelease()
метод, який передбачається викликати, коли значення береться з відповідного Variant
об’єкта.
Поєднання всіх частин дає нам наступну реалізацію послуги:
@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; }
Два інших методи, Logon
і getMatchedUsers
, реалізовані відповідно. Ви можете звернутися до мого репозитарію GitHub для отримання повної версії послуги, якщо ви хочете перевірити її.
Вітаємо - ти мало чого навчився
Ми пройшли покроковий сценарій, який показав нам, як гіпотетичний COM API можна розповсюджувати та викликати на Java.
Ми також дізналися, як бібліотеку JACOB можна налаштувати та ефективно використовувати для взаємодії з бібліотекою DDL у вашому додатку Spring Boot 2.
Невеликим вдосконаленням було б кешування отриманого SessionID, що може покращити загальний потік. Але це трохи виходить за рамки цієї статті.
Якщо ви хочете дослідити далі, ви можете знайти це на GitHub, де це реалізовано за допомогою механізму кешування Spring.
Сподіваюся, вам сподобалось пережити все зі мною та знайшов цей підручник корисним!