Intro

We recently received a requirement to integrate with a Java API. I’ve worked with Java before and natch, the requirement came my way.

Considerations

  • This is a .NET shop and Maven / Gradle / Spring / IntelliJ are foreign concepts (the company firewall wouldn’t let me trough to the Maven repo, nor was there an internal repo that I could find). Thus, in order to ensure the solution is maintainable by anyone in the team, most of the logic needed to reside in the .NET space and the least amount of Java had to be written.

Requirements

  • The interaction with Java should be seamless.
  • There should be minimal to no coupling between the Java API and the .NET code. This meant no jni4net that would require intricate knowledge of the API’s internals and in addition it will require the Java API to emit to our .NET application, and vice versa. (The implementation of jni4net in the Java API was not an option anyway as it would require additional support from the writers of the API for my specific situation.)
  • The Java API needs to be hosted regardless of any user being logged in, it needs to run as a background process (like a Daemon in a Unix environment).1

The result of ruminating on the constraints

Design

  • Seamless, loosely coupled, highly cohesive integration between the .NET application and the Java API by wrapping the Java API in a web service that can then just be consumed like any other SOAP based web service.

  • Write as little Java as possible by exposing the required Java API functionality as vanilla as possible in the web service. The code on the .NET side is then automatically created through the service proxy class generation tools in Visual Studio OR Svcutil.exe. Any orchestration logic such as polling should then reside in the .NET application code.

  • Run the Web Service (that wraps the Java API2) as a background application by wrapping it in a Windows Service. The windows service hosts the web service as a background operation which means it is not tied to any user session. A windows service has the additional capability of being configured to starting automatically on the occasional server reboot as well as run under elevated credentials specific to the operation.

Pre-requisites

  1. Java SDK
  2. Maven (optionally Gradle)
  3. MS Visual Studio 2013
  4. Git

Paths

Either start with the Getting started section below…

OR

clone this Java.NETWindowsService repo with the pre-compiled Spring Java SOAP service (that wraps the test Java API3) as well as the test .NET console application4 that consumes the SOAP based web service . Then install and start the compiled service and run the .NET console application executable5.

Getting started

For the purposes of this post I used a pre-existing Spring SOAP web service that I borrowed from the spring getting started guide: “Producing a SOAP web service” which is at this GitHub repo.

  • I forked the “Producing a SOAP web service” GitHub repo > here and,
    • …added additional logging (for demonstration purposes that shows the Endpoint called and the Country parameter in the service request).
    • …added a run.bat batch file to run the compiled sample jar as a stand alone service.
  1. Clone the “Producing a SOAP web service” GitHub repo altered by me.

  2. Open the project in IntelliJ and run a mvn clean install to produce a jar file (gs-producing-web-service-0.1.0.jar) which will serve as both, the Java API being wrapped, and the web service that will be exposed from the Windows Service. intelliJ_build_maven

  3. Execute the run.bat file to run the Spring web service6 which is hosted at http://localhost:8080/ws. A console should appear as below with Spring telling us what’s going on. Spring Test

  4. Test the service once it is up and running. I used SoapUI by SMARTBEAR7. In the previous screen shot, you will see the additional logging come into play when a few service calls are made. I called the service 4 times, three times with “Spain” as the request country and once with “France”. Spring Test SOAP UI

Hosting the Java API as a Windows Service

Running the run.bat file in the console is essentially starting up the JVM and running our test web service (that wraps the Java API) that I want to consume from a .NET application. When the Windows Service executes a Process it is doing exactly that, only as a background process devoid of any dependency on a logged in user. (The windows service still needs to run under a user account though and defaults to local system account.)

This project replicates the command line execution of a Spring web service host as a background process, but managed from the Windows Service. The start and stop operations of the service are mapped to the start and stop of the hosted process. The project also ensures output is redirected from the hosted process into a logging mechanism to prevent the loss of feedback from the process that is hosted in the Windows Service.

The logging mechanism employed in this project is: NLog, with a file target configured (alternatively you could log to the EventLog under the Windows Service name).

Solution overview

ProjectStructure

Windows Service

  • I created a Windows Service using the vanilla Windows Service C# project template from MS Visual Studio 2013. Create Windows Service

  • The [JavaWindowsService.cs] is the main class in the solution. It is instantiated by [Program.cs] when the service starts up. It inherits from [ServiceBase] and overrides two methods, the OnStart() and OnStop(), where the logic required to start and stop the Java API process needs to be incorporated.

  • The OnStart() method retrieves the start arguments from the app.config AppSettings section.

protected override void OnStart(string[] args)
{
    var processStartArguments = GetArgsFromConfig();
    _logger.Info("Starting with args: {0}", processStartArguments);
    try
    {
        var javaProcess = new JavaProcess(processStartArguments);
        javaProcess.OnMessage += OnMessage;
        javaProcess.OnExit += OnExited;
        _javaProcessId = javaProcess.Start();
    }
    catch (Exception exception)
    {
        _logger.Error(exception);
        throw;
    }
}
  • A [JavaProcess] is instantiated and event handlers are assigned to the events that the process emits. The process is started and the process ID is returned to keep track of the process. If an exception occurs it is logged and then re-thrown again — throw;, because I do not want the service to start up in a faulted state or lose the stack trace.

  • Config args: All the app settings are used as arguments in order - regardless of key name. So first entry is first argument. arg0 then arg1 then charlie123 etc…

<?XML version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <appSettings>
    <add key="arg0" value="-jar" />
    <add key="arg1" value="gs-producing-web-service-0.1.0.jar" />
  </appSettings>
  <system.web>
    <membership defaultProvider="ClientAuthenticationMembershipProvider">
      <providers>
        <add name="ClientAuthenticationMembershipProvider"
             type="System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
             serviceUri="" />
      </providers>
    </membership>
    <roleManager defaultProvider="ClientRoleProvider" enabled="true">
      <providers>
        <add name="ClientRoleProvider" type="System.Web.ClientServices.Providers.ClientRoleProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
             serviceUri="" cacheTimeout="86400" />
      </providers>
    </roleManager>
  </system.web>
</configuration>
  • The OnMessage() and OnExited() event handlers simply log messages with the appropriate descriptive prefix.
private void OnMessage(string message)
{
	_logger.Info("JVM: {0}", message);
}

private void OnExited(int exitCode)
{
	_logger.Error("Java process stopped, exit code:" + exitCode);
}
  • The OnStop() method delegates to the StopProcess() method which employs the static [processById] field to retrieve a reference to the process. The reference is retrieved in order to issue Kill() commands against.
protected override void OnStop()
{
    _logger.Info("Stop");
    try
    {
        StopProcess();
    }
    catch (Exception exception)
    {
        _logger.Error(exception);
    }
}
  • The WaitForExit() method prevents the service from reporting the stopped state prematurely, and waits for the [JavaProcess] to shut down before entering a stopped state.
private void StopProcess()
{
    var processById = Process.GetProcessById(_javaProcessId);
    processById.Kill();
    processById.WaitForExit();
}

.NET [JavaProcess.cs]

  • There are two public events that emit output messages, OnMessage and OnExit, pretty self explanatory, but the code will follow.

  • The [JavaProcess] has a constructor for passing the start arguments to the underlying JVM Java.exe process.

    • The [JavaProcess] relies on the %JAVA_HOME% environment variable in order to locate the Java installation on the machine.
      • The ProcessStartInfo parameters:
        • UseShellExecute = false allows redirection of the input, output, and error streams from the process.
        • CreateNoWindow = true prevents another console form opening.
        • RedirectStandardError = true redirect error messages to ErrorDataReceived event subscribers.
        • RedirectStandardOutput = true redirect error messages to OutputDataReceived event subscribers.
        • RedirectStandardInput = true don’t accept input from other than the StandardInput property.(Not employed by this solution.)
public class JavaProcess
{
    private readonly ProcessStartInfo _startInfo;
    
    public event Action<string> OnMessage;
    public event Action<int> OnExit;
    
    public JavaProcess(string processStartArguments)
    {
        var javaPath = GetJavaPath(Environment.GetEnvironmentVariable("JAVA_HOME"));
        
        _startInfo = new ProcessStartInfo(javaPath, processStartArguments)
        {
            UseShellExecute = false,
            CreateNoWindow = true,
            RedirectStandardError = true,
            RedirectStandardOutput = true,
            RedirectStandardInput = true
        };
    }
    ...
}
  • The only place a [JavaException] is thrown is in the GetJavaPath(…) method. If you encounter a [JavaException], and you have installed the JAVA runtime, either you don’t have the environment variable set or the environment variable path is incorrectly set.
private string GetJavaPath(string javaHomePathEnvironmentVariable)
{
    var javaPath = string.Format(@"{0}\bin\java.exe", javaHomePathEnvironmentVariable);
    
    if (string.IsNullOrWhiteSpace(javaHomePathEnvironmentVariable))
    {
        throw new JavaException(
        	"Error: JAVA_HOME is set to an invalid directory. JAVA_HOME = '%JAVA_HOME%' Please set the JAVA_HOME variable in your environment to match the location of your Java installation.");
    }
    if (!File.Exists(javaPath))
    {
        throw new JavaException(
        	"Error: Please confirm you have a valid Java installation. And please set the JAVA_HOME = '%JAVA_HOME%' variable in your environment to match the location of your Java installation. ");
    }
    
    return javaPath;
}
  • The OutputDataReceived and ErrorDataReceived events are subscribed to with the RaiseMessageEvent event handler. In this case no distinction should be made between the two types of messages, both are reported the same8.
private void RaiseMessageEvent(object sender, DataReceivedEventArgs e)
{
    if (OnMessage != null) OnMessage(e.Data);
}		
  • The Exited event is subscribed to by the exposed OnExit event handler to inform the service when the process has stopped. At present this is just logged in the Windows Service but some fault tolerance, a restart or some recovery mechanism could be incorporated here.

  • From the OnStart() method the Windows Service the Start() command is issued to the [JavaProcess]. It instantiates a new Process with the ProcessStartInfo created in the constructor as the _startInfo.

public int Start()
{
    var process = new Process { StartInfo = _startInfo };
    
    process.OutputDataReceived += RaiseMessageEvent;
    process.ErrorDataReceived += RaiseMessageEvent;
    process.Exited += (sender, e) =>
    {
        if (OnExit != null) OnExit(process.ExitCode);
    };
    
    process.Start();
    
    process.BeginOutputReadLine();
    process.BeginErrorReadLine();
    
    return process.Id;
}
  • Once the process is started the BeginOutputReadLine() and BeginErrorReadLine() statements are required to start redirecting the output and error streams to the subscribed event handlers.

Consuming the API through the web service

Create a console application

  • Create a new vanilla C# console application. New Console Application Template

Now add a service reference to the .NET application in order to consume the web service that wraps the Java API2.

  • Run the run.bat file from the altered “Producing a SOAP web service” project - to get the service up and running. Spring WS Reference

  • Right click on the Project > Add > Service Reference… — Add a service reference to the console application using the end point: http://localhost:8080/ws/countries.wsdl. Add Service Reference Add Service Reference Console

  • Replace the Program Class (in the [Program.cs] file) code with the below snippet.

using System;
using ConsoleApplication.JavaAPIEndPoint;

namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Please enter a country");
            while (true)
            {
                var readLine = Console.ReadLine();
                if (readLine != null && readLine.ToLower() == "exit") break;
    
                using (CountriesPortClient client = new CountriesPortClient())
                {
                    try
                    {
                        var getCountryResponse = client.getCountry(new getCountryRequest() { name = readLine });
                        Console.WriteLine(getCountryResponse.country.capital);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                }
            }
        }
    }
}

Test

  • Run the console application and test the Java API by typing in a country like Spain. .NET Console Application consuming Java API

  • Stop the run.bat process and close the .NET Console Application.

Installing/Uninstalling the service

Install the Windows Service

  • I used the sc.exe9 tool. Below is a simple version I employed to test this service (run from the ../bin/debug path). Important: The space after the = sign is required!
    • sc create testJavaWindowsService binpath= “Java.NETWindowsService.exe”
      • The installed but stopped service should look like below. Installed Windows Service

Uninstalling the Windows Service

  • It is not required to uninstall the Windows Service with each change you would like to deploy locally, however, it is advisable to stop the service, uninstall it, replace the altered assembly and re-install the service.
    1. sc stop testJavaWindowsService
    2. sc delete testJavaWindowsService
    3. replace the altered assembly
    4. sc create testJavaWindowsService binpath= “Java.NETWindowsService.exe”

Testing it all together

Start the service

  • You can start the service by pressing the [Play] button (red rectangle in the Install the Windows Service screen shot).

OR

  • Start the service via another simple sc.exe command.
    • sc start testJavaWindowsService

Test

  • Start the .NET Console Application and retest, much like the previous Test section, but this time - testing that the Java API is correctly consumed when running the Spring web service (Java API wrapper) as a Windows Service.
    • Monitoring the hosted process ([JavaProcess]) passes along the messages, previously logged to the console, to the Windows Service that then employs NLog.
      • I used BareTail to monitor the log files produced by NLog. The location & name of the log file is specified in the NLog.config file: ${basedir}/logs/${shortdate}.log.

BareTail

Additional parameters

  • start= [auto] — sets the service to start automatically on OS boot up.
  • obj= [useraccountname] — sets the User Account under which the service should run - in certain cases elevated privileges might be required.
  • password= [useraccountpassword] — sets the password for the User Account under which the service should run.
  • depend= [servicename] — specifies a service that this one depends on,the API might need to wait for another service to startup first - like a database server that also runs as a service.
  • displayname= [pretty name] — a more descriptive name for the windows service.

Improvements

  • The Windows Service can host only one [JavaProcess] - a static field is used to keep reference to the running process ID.
  • Logging seems pretty vanilla and introducing PostSharp or another AOP Framework to implement this cross-cutting concern could be beneficial - especially as the project grows. (The logging Advice could still point to NLog, I’ll post this some other time.)
  • Fault tolerance / some sort of auto recovery should the [JavaProcess] stop could also be introduced.

Alternatives

  • I did some research and found an awesome library in Tanuki Software’s Java Service Wrapper. It looks like an awesome tool with a lot of features and will be able to provide the same functionality as this project with a few added benefits.
  • If you have the ability to alter both the Java and .NET code - jni4net is an option.

Moral:

So what can we take from Phil’s infinite wisdom?

  • “When life gives you lemonade, make lemons. Life’ll be all like ‘what?!’” — Phil Dunphy [Phil’s-osophy]

    Shock and awe life10…, and there are ways of making .NET and Java work together with vanilla mechanisms at your disposal, whilst still remaining loosely coupled and highly cohesive.

Quick Guide:

  1. Clone Java.NETWindowsService repo.
  2. Compile the solution.
  3. Install and Start the compiled service. see the Installing/Uninstalling the service section on how to install the service.
  4. Run the ConsoleApplication.exe executable located in the .NETApp folder of the cloned repo.

##Footnotes

  1. java.exe -jar on a console was thus not an option. 

  2. ..in the house that Jack built.  2

  3. Repo location for Java API and batch file to run. 

  4. Repo location for .Net Console Application 

  5. see the Quick Guide section at the end. 

  6. Why don’t I just do this? - Because as soon as I close the console the process stops and our service disappears. Hence, the Windows Service. 

  7. Adding the service to the project - Right click on Projects > New SOAP Project and in the Initial WSDL field paste: http://localhost:8080/ws/countries.wsdl 

  8. An internal error to the process being hosted should not necessarily be interpreted as a fatal exception and should not affect the hosting process. 

  9. Installing a Windows Service can be done either through the installutil.exe that ships with the .NET framework or the sc.exe utility that is native to, and naturally occurs in, the Windows habitat (from Vista and up). 

  10. Shock and awe