Урок за API на интерфейса COM: Java Spring Boot + JACOB Library

В тази статия ще ви покажа как да вградите библиотеката 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със следните аргументи:

ИмеВход / изходТипОписание
адресВСтрунапредоставен IP за сървър за интеграция
AccountIDВСтрунапредоставен уникален GUID низ
Самоличност за влизанеВънСтрунаGUID низ, използван за извикване на API за влизане
ГрешкаВънСтрунаОписание на грешката
РезултатВънЦяло число-1 = Вижте грешка

1 = Успешна инициализация в очакване на влизане

2 = Не може да се свърже със сървър поради липсващ сървър или неправилни подробности

3 = Несравним AccountID

4 = Автологизацията е успешна

Сесиен идентификаторВънСтрунаGUID, използван за последващи взаимодействия (ако автоматичното влизане е успешно)

Метод за влизане

Този метод определя правомощията на потребителя. Потребителското име тук е идентификаторът, използван за влизане в системата. Паролата е API паролата, зададена за това потребителско име.

В сценария за успех повикването връща SessionIDниз (GUID), който трябва да бъде предаден на други следващи повиквания, за да ги удостовери.

За да извикате функцията, използвайте името Logonсъс следните аргументи:

ИмеВход / изходТипОписание
Самоличност за влизанеВСтрунаИдентификационният номер за вход, върнат от метода за инициализация Initialise with ID
потребителско имеВСтрунапредоставено потребителско име за API
паролаВСтрунапредоставена парола за API
Сесиен идентификаторВънСтрунаGUID, използван за последващи взаимодействия (ако влизането е успешно)
ГрешкаВънСтрунаОписание на грешката
РезултатВънЦяло число-1 = Техническа грешка

1 = Успешно

2 = Изтекъл

3 = Неуспешно

4 = Невалиден идентификационен номер за вход или идентификационен номер за вход няма достъп до този продукт

метод getMatchedUsers

Това обаждане ви позволява да намерите записи на потребителски данни, които отговарят на определени критерии. Терминът за търсене може да се отнася само до едно поле в даден момент, като фамилно име, име или дата на раждане.

Успешното повикване връща XML низ с данните в него.

За да извикате функцията, използвайте името getMatchedUsersсъс следните аргументи:

ИмеВход / изходТипОписание
Сесиен идентификаторВСтрунаИдентификаторът на сесията, върнат от метода за влизане
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класа, нека дефинираме изпълнение на неговия манипулатор с логика за извличане на SessionID и потребителските записи на данни.

За втория сценарий, за да можем да осъществим извикване на getMatchedUsersAPI на 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 и да използваме и трите в извикването на DLL LogonAPI.

За да получите по-ясна картина, нека дефинираме и управлявания 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).

Така че нека дефинираме отделен Spring @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:   DllApiServiceImpl.

Как да внедрим услуга за API на DLL библиотека

Тъй като всички извиквания на COM API ще бъдат приготвени с помощта на общ подход, нека InitialiseWithIDпърво да внедрим. След това всички други методи могат да бъдат внедрени лесно по подобен начин.

Както споменах преди, за да си взаимодействаме с COM интерфейса, JACOB ни предоставя ActiveXComponentкласа, който има invoke(String function, Variant... args)метода.

Ако искате да научите повече за Variantкласа, документацията на JACOB казва следното (можете да го намерите в архива или /libs/jacob-1.19в проекта):

Многоформатният тип данни, използван за всички обратни обаждания и повечето комуникации между Java и COM. Той осигурява един клас, който може да обработва всички типове данни.

Това означава, че всички аргументи, дефинирани в 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.

Надявам се, че ви е харесало да преживеете всичко с мен и сте намерили този урок за полезен!