Dalam artikel ini, saya akan menunjukkan kepada anda cara memasukkan pustaka JACOB ke dalam aplikasi Spring Boot anda. Ini akan membantu anda memanggil API antara muka COM melalui pustaka DLL di aplikasi web anda.
Juga, untuk tujuan ilustrasi, saya akan memberikan penerangan tentang API COM supaya anda dapat membina aplikasi anda di atasnya. Anda boleh mendapatkan semua coretan kod di repo GitHub ini.
Tetapi pertama, catatan ringkas: di C the Signs kami menggunakan penyelesaian ini yang membolehkan kami berintegrasi dengan EMIS Health. Ini adalah sistem rekod pesakit elektronik yang digunakan dalam perawatan primer di United Kingdom. Untuk penyatuan kami menggunakan perpustakaan DLL yang disediakan.
Pendekatan yang akan saya tunjukkan di sini (dibersihkan untuk mengelakkan kebocoran maklumat sensitif) dilancarkan ke produksi lebih dari dua tahun lalu, dan sejak itu membuktikan ketahanannya.
Oleh kerana kami baru-baru ini menggunakan pendekatan baru untuk berintegrasi dengan EMIS, sistem lama akan ditutup dalam satu atau dua bulan. Jadi tutorial ini adalah lagu angsa. Tidurlah, putera kecil saya.
Apa itu API DLL?
Pertama, mari kita mulakan dengan penerangan yang jelas mengenai perpustakaan DLL. Untuk melakukan ini, saya telah menyiapkan dokumentasi teknikal asal.
Mari kita perhatikan untuk melihat apakah tiga kaedah antara muka COM.
Kaedah InitialiseWithID
Kaedah ini adalah ciri keselamatan yang diperlukan di laman web yang memungkinkan kita mendapatkan sambungan ke pelayan API yang ingin kita gabungkan dengan perpustakaan.
Ia memerlukan AccountID
(GUID) pengguna API semasa (untuk mengakses pelayan) dan beberapa argumen permulaan lain yang disenaraikan di bawah.
Fungsi ini juga menyokong ciri log masuk automatik. Sekiranya pelanggan mempunyai versi sistem berjalan yang dilog masuk (perpustakaan adalah bahagian dari sistem itu) dan memanggil kaedah tersebut pada host yang sama, API akan secara automatik melengkapkan log masuk di bawah akaun pengguna tersebut. Kemudian ia akan mengembalikan SessionID
panggilan API berikutnya.
Jika tidak, klien perlu meneruskan Logon
fungsi (lihat bahagian seterusnya) menggunakan dikembalikan LoginID
.
Untuk memanggil fungsi, gunakan nama InitialiseWithID
dengan argumen berikut:
Nama | Dalam keluar | Jenis | Penerangan |
---|---|---|---|
alamat | Dalam | Tali | disediakan IP pelayan integrasi |
ID Akaun | Dalam | Tali | disediakan rentetan GUID yang unik |
ID masuk | Keluar | Tali | Rentetan GUID digunakan untuk panggilan API Logon |
Ralat | Keluar | Tali | Huraian ralat |
Hasil | Keluar | Bilangan bulat | -1 = Rujuk kesilapan 1 = Permulaan yang berjaya menunggu log masuk 2 = Tidak dapat menyambung ke pelayan kerana pelayan tidak ada, atau perincian yang salah 3 = Akaun Akaun yang tidak dapat ditandingi 4 = Autologon berjaya |
SesiID | Keluar | Tali | GUID digunakan untuk interaksi seterusnya (sekiranya log masuk automatik berjaya) |
Kaedah Log masuk
Kaedah ini menentukan kewibawaan pengguna. Nama pengguna di sini adalah ID yang digunakan untuk log masuk ke sistem. Kata laluan adalah kata laluan API yang ditetapkan untuk nama pengguna tersebut.
Dalam senario kejayaan, panggilan mengembalikan SessionID
rentetan (GUID) yang mesti diteruskan ke panggilan berikutnya yang lain untuk mengesahkannya.
Untuk memanggil fungsi, gunakan nama Logon
dengan argumen berikut:
Nama | Dalam keluar | Jenis | Penerangan |
---|---|---|---|
ID masuk | Dalam | Tali | Id log masuk dikembalikan dengan kaedah inisialisasi Permulaan dengan ID |
nama pengguna | Dalam | Tali | nama pengguna API yang disediakan |
kata laluan | Dalam | Tali | kata laluan API yang disediakan |
SesiID | Keluar | Tali | GUID digunakan untuk interaksi seterusnya (jika log masuk berjaya) |
Ralat | Keluar | Tali | Huraian ralat |
Hasil | Keluar | Bilangan bulat | -1 = Kesalahan teknikal 1 = Berjaya 2 = Tamat tempoh 3 = Tidak berjaya 4 = ID log masuk atau ID masuk yang tidak sah tidak mempunyai akses ke produk ini |
Kaedah getMatchedUsers
Panggilan ini membolehkan anda mencari rekod data pengguna yang sesuai dengan kriteria tertentu. Istilah carian hanya boleh merujuk kepada satu bidang pada satu masa seperti nama belakang, nama depan, atau tarikh lahir.
Panggilan yang berjaya mengembalikan rentetan XML dengan data di dalamnya.
Untuk memanggil fungsi, gunakan nama getMatchedUsers
dengan argumen berikut:
Nama | Dalam keluar | Jenis | Penerangan |
---|---|---|---|
SesiID | Dalam | Tali | Id sesi dikembalikan dengan kaedah log masuk |
Syarat Padan | Dalam | Tali | Istilah carian |
Senarai Padan | Keluar | Tali | XML sesuai dengan skema XSD yang sesuai |
SesiID | Keluar | Tali | GUID digunakan untuk interaksi seterusnya (jika log masuk berjaya) |
Ralat | Keluar | Tali | Huraian ralat |
Hasil | Keluar | Bilangan bulat | -1 = Kesalahan teknikal 1 = Pengguna dijumpai 2 = Akses ditolak 3 = Tiada pengguna |
Aliran Aplikasi Perpustakaan DLL
Untuk lebih mudah memahami apa yang ingin kami laksanakan, saya memutuskan untuk membuat gambarajah aliran sederhana.
Ini menerangkan senario langkah demi langkah bagaimana klien web dapat berinteraksi dengan aplikasi berasaskan pelayan kami menggunakan API-nya. Ini merangkumi interaksi dengan Perpustakaan DLL dan membolehkan kita mendapatkan pengguna hipotesis dengan istilah padanan yang disediakan (kriteria carian):

Mendaftar COM
Sekarang mari kita pelajari bagaimana kita boleh mengakses perpustakaan DLL. Untuk dapat berinteraksi dengan antara muka COM pihak ketiga, ia perlu ditambahkan ke pendaftaran.
Inilah yang dikatakan oleh dokumen:
Registri adalah pangkalan data sistem yang mengandungi maklumat mengenai konfigurasi perkakasan dan perisian sistem serta tentang pengguna sistem. Sebarang program berasaskan Windows dapat menambahkan maklumat ke pendaftaran dan membaca maklumat kembali dari pendaftaran. Pelanggan mencari di daftar komponen yang menarik untuk digunakan.Registri menyimpan maklumat mengenai semua objek COM yang dipasang dalam sistem. Setiap kali aplikasi membuat contoh komponen COM, pendaftaran diperiksa untuk menyelesaikan sama ada CLSID atau ProgID komponen ke dalam nama jalan pelayan DLL atau EXE yang mengandunginya.
Setelah menentukan pelayan komponen, Windows memuatkan pelayan ke ruang proses aplikasi klien (komponen dalam proses) atau memulakan pelayan di ruang prosesnya sendiri (pelayan tempatan dan jauh).
Pelayan membuat contoh komponen dan mengembalikan kepada pelanggan rujukan ke salah satu antara muka komponen.
Untuk mengetahui cara melakukannya, dokumentasi Microsoft rasmi mengatakan:
Anda boleh menjalankan alat baris perintah yang disebut Assembly Registration Tool (Regasm.exe) untuk mendaftar atau membatalkan pendaftaran pemasangan untuk digunakan dengan COM.Regasm.exe menambah maklumat mengenai kelas ke sistem pendaftaran sehingga klien COM dapat menggunakan kelas .NET Framework secara telus.
Kelas RegistrationServices menyediakan fungsi yang setara. Komponen yang dikendalikan mesti didaftarkan dalam pendaftaran Windows sebelum dapat diaktifkan dari klien COM
Pastikan mesin hos anda telah memasang .NET Framework
komponen yang diperlukan . Selepas itu, anda boleh melaksanakan perintah CLI berikut:
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.
Seperti yang saya nyatakan sebelumnya, untuk berinteraksi dengan antara muka COM, JACOB memberi kami ActiveXComponent
kelas yang mempunyai invoke(String function, Variant... args)
kaedah.
Sekiranya anda ingin mengetahui lebih lanjut mengenai Variant
kelas, dokumentasi JACOB menyatakan perkara berikut (anda boleh menemuinya di arkib atau di bawah /libs/jacob-1.19
projek):
Ini bermaksud bahawa semua argumen yang ditentukan dalam InitialiseWithID
tandatangan harus dibungkus new Variant(java.lang.Object in)
dan diteruskan ke invoke
kaedah. Gunakan urutan yang sama seperti yang dinyatakan dalam keterangan antara muka pada awal artikel ini.
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.
A small improvement would be to cache the retrieved SessionID which could improve the general flow. But that's a bit outside the scope of this article.
If you want to investigate further, you can find that on GitHub where it's implemented using Spring's caching mechanism.
Hope you enjoyed going through everything with me and found this tutorial helpful!