Showing posts with label web services. Show all posts
Showing posts with label web services. Show all posts

Thursday, December 26, 2024

Simple is finally easy: bootstrapping JAX-RS applications in Java SE environments

It has been a while since Jakarta EE 10 was released but the ecosystem is slowly (but steadily!) catching up. The Apache CXF project landed new 4.1.0 release very recently that delivers Jakarta EE 10 compatibility, specifically implementation of the Jakarta RESTful Web Services 3.1 specification (also known as JAX-RS).

One of the most exciting (in my option) features that Jakarta RESTful Web Services 3.1 includes is bootstrapping JAX-RS applications in Java SE environments. From now on, creating the full-fledged RESTful web services on the JVM becomes not only easy, but very straightforward! In today's post, we are going to build a sample RESTful web service and host it inside the Java SE application, with a catch - no boilerplate allowed.

The PeopleRestService, presented in the snippet below, is a minimalistic example of the typical Jakarta RESTful web service: for a sake of keeping things simple, it does not do anything useful besides returning the predefined data back.

import java.util.Collection;
import java.util.List;

import com.example.jakarta.restful.bootstrap.model.Person;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/people")
public class PeopleRestService {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Collection<Person> getPeople() {
        return List.of(new Person("[email protected]", "John", "Smith"));
    }
}

The Person class contains only three fields: email, firstName and lastName.

  
public class Person {
    private String email;
    private String firstName;
    private String lastName;
    // Skipping the getters and setters for brevity
}

Essentially, this is all we need at this point. Now the hardest part, how to expose the PeopleRestService to the outside world? Here is the moment for SeBootstrap to take the stage. Its entire purpose is allowing to startup a JAX-RS application in Java SE environments, without (explicitly) requiring the presence of the web container or application server. How does it look like in practice?

  
import java.util.Set;
import java.util.concurrent.CompletionStage;

import com.example.jakarta.restful.bootstrap.rs.PeopleRestService;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.SeBootstrap;
import jakarta.ws.rs.core.Application;

public class BootstrapRunner {
    @ApplicationPath("/api")
    public static final class JakartaRestfulApplication extends Application {
        @Override
        public Set<Object> getSingletons() {
            return Set.of(new PeopleRestService());
        }
    }

    public static void main(String[] args) {
        final SeBootstrap.Configuration configuration = SeBootstrap.Configuration
            .builder()
            .property(SeBootstrap.Configuration.PROTOCOL, "http")
            .property(SeBootstrap.Configuration.PORT, 10800)
            .property(SeBootstrap.Configuration.ROOT_PATH, "/")
            .build();
    
        SeBootstrap
            .start(new JakartaRestfulApplication(), configuration)
            .toCompletableFuture()
            .join();
    }
}

As simple as that: pass the port (10800), protocol (HTTP) and root path (/) through SeBootstrap.Configuration along with Application subclass (JakartaRestfulApplication) instance to SeBootstrap::start method. To complete the puzzle, here are all the dependencies that are required by our Java SE application (taken from project's Apache Maven pom.xml file).

  
<dependencies>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-frontend-jaxrs</artifactId>
		<version>4.1.0</version>
	</dependency>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-rs-extension-providers</artifactId>
		<version>4.1.0</version>
	</dependency>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-transports-http-jetty</artifactId>
		<version>4.1.0</version>
	</dependency>
	<dependency>
		<groupId>jakarta.json</groupId>
		<artifactId>jakarta.json-api</artifactId>
		<version>2.1.3</version>
	</dependency>
	<dependency>
		<groupId>jakarta.json.bind</groupId>
		<artifactId>jakarta.json.bind-api</artifactId>
		<version>3.0.1</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback</groupId>
		<artifactId>logback-classic</artifactId>
		<version>1.5.15</version>
	</dependency>
	<dependency>
		<groupId>org.eclipse</groupId>
		<artifactId>yasson</artifactId>
		<version>3.0.4</version>
	</dependency>
</dependencies>

Nothing special, except may be Eclipse Yasson, the JSON-B implementation provider. It is time to make sure everything actually works!

$ mvn clean package

...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
...

$ java -jar target/cxf-jakarta-restful-3.1-bootstrap-0.0.1-SNAPSHOT.jar
Dec 24, 2024 2:43:45 P.M. org.apache.cxf.endpoint.ServerImpl initDestination
INFO: Setting the server's publish address to be http://:10800/api
14:43:46.129 [onPool-worker-1] INFO rg.eclipse.jetty.server.Server - jetty-12.0.15; built: 2024-11-05T19:44:57.623Z; git: 8281ae9740d4b4225e8166cc476bad237c70213a; jvm 23.0.1+8-FR
14:43:46.288 [onPool-worker-1] INFO jetty.server.AbstractConnector - Started ServerConnector@7b66a8d{HTTP/1.1, (http/1.1)}{:10800}
14:43:46.302 [onPool-worker-1] INFO rg.eclipse.jetty.server.Server - Started oejs.Server@7683694f{STARTING}[12.0.15,sto=0] @1701ms
14:43:46.349 [onPool-worker-1] INFO .server.handler.ContextHandler - Started oeje10s.ServletContextHandler@4b04a638{ROOT,/,b=null,a=AVAILABLE,h=oeje10s.ServletHandler@23ae88f1{STARTED}}

With the application up and running, we are ready to invoke the http://localhost:10800/api/people HTTP endpoint (the only one our Jakarta RESTful web service exposes).

$ curl http://localhost:10800/api/people -iv
* Host localhost:10800 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:10800...
*   Trying 127.0.0.1:10800...
* Connected to localhost (127.0.0.1) port 10800
* using HTTP/1.x
> GET /api/people HTTP/1.1
> Host: localhost:10800
> User-Agent: curl/8.11.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Server: Jetty(12.0.15)
Server: Jetty(12.0.15)
< Date: Tue, 24 Dec 2024 19:52:10 GMT
Date: Tue, 24 Dec 2024 19:52:10 GMT
< Content-Type: application/json
Content-Type: application/json
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
<

[{"email":"[email protected]","firstName":"John","lastName":"Smith"}]

And here we are, we get the response with our hardcoded list of people, no surprises! I hope you would agree, the bootstrapping process is very simple and easy to follow. Even more, you could integrate SeBootstrap in your test suites as well, thanks to its flexible configuration capabilities, for example:

  
final SeBootstrap.Configuration configuration = SeBootstrap.Configuration
    .builder()
    // Use random free port
    .property(SeBootstrap.Configuration.PORT, SeBootstrap.Configuration.FREE_PORT)
    ...
    .build();

// Start the instance
final Instance instance = SeBootstrap
    .start(new JakartaRestfulApplication(), configuration)
    .toCompletableFuture()
    .join();
    
final SeBootstrap.Configuration actual = instance.configuration();
// Use actual.port(), actual.host(), ...
...

// Stop the instance
instance
    .stop()
    .toCompletableFuture()
    .join();

It is worth to mention that bootstrapping secure Jakarta RESTful Web Services using HTTPS protocol is also supported, for example:

 
final SeBootstrap.Configuration configuration = SeBootstrap.Configuration
    .builder()
    .property(SeBootstrap.Configuration.PROTOCOL, "https")
    .property(SeBootstrap.Configuration.PORT, 10843)
    .property(SeBootstrap.Configuration.ROOT_PATH, "/")
    .sslContext(SSLContext.getDefault()) /* or supply your own */
    .build();

The complete source code of the project is available on Github.

I πŸ‡ΊπŸ‡¦ stand πŸ‡ΊπŸ‡¦ with πŸ‡ΊπŸ‡¦ Ukraine.

Monday, April 30, 2018

Moving With The Times: Towards OpenAPI v3.0.0 adoption in JAX-RS APIs

It is terrifying to see how fast time passes! The OpenAPI specification 3.0.0, a major revamp of so-get-used-to Swagger specification, has been released mostly one year ago but it took awhile for tooling to catch up. However, with the recent official release of the Swagger Core 2.0.0 things are going to accelerate for sure.

To prove the point, Apache CXF, the well-known JAX-RS 2.1 implementation, is one of the first adopters of the OpenAPI 3.0.0 and in today's post we are going to take a look how easy your JAX-RS 2.1 APIs could benefit from it.

As always, to keep things simple we are going to design a people management web APIs with just a handful set of resources to support it, nothing too exciting here.

POST   /api/people
GET    /api/people/{email}
GET    /api/people
DELETE /api/people/{email}

Our model would consist of a single Person class.

public class Person {
    private String email;
    private String firstName;
    private String lastName;
}

To add a bit of magic, we would be using Spring Boot to get us up and running as fast as possible. With that, let us start to fill in the dependencies (assuming we are using Apache Maven for build management).

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-spring-boot-starter-jaxrs</artifactId>
    <version>3.2.4</version>
</dependency>

In the recent 3.2.x releases Apache CXF introduces a new module cxf-rt-rs-service-description-openapi-v3 dedicated to OpenAPI 3.0.0, based on Swagger Core 2.0.0.

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-rs-service-description-openapi-v3</artifactId>
    <version>3.2.4</version>
</dependency>

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>swagger-ui</artifactId>
    <version>3.13.6</version>
</dependency>

The presence of the Swagger UI is not strictly necessary, but this is exceptionally useful and beautiful tool to explore your APIs (and if it is available on classpath, Apache CXF seamlessly integrates it into your applications as we are going to see in a bit).

The prerequisites are in place, let us do some coding! Before we begin, it is worth to note that the Swagger Core 2.0.0 has many ways to populate the OpenAPI 3.0.0 definitions for your services, including property files, annotations or programmatically. Along this post we are going to use annotations only.

@OpenAPIDefinition(
    info = @Info(
        title = "People Management API",
        version = "0.0.1-SNAPSHOT",
        license = @License(
            name = "Apache 2.0 License",
            url = "http://www.apache.org/licenses/LICENSE-2.0.html"
        )
    )
)
@ApplicationPath("api")
public class JaxRsApiApplication extends Application {
}

Looks pretty simple, the @OpenAPIDefinition sets the top-level definition for all our web APIs. Moving on to the PeopleRestService, we just add the @Tag annotation to, well, tag our API.

@Path( "/people" ) 
@Tag(name = "people")
public class PeopleRestService {
    // ...
}

Awesome, nothing complicated so far. The meaty part starts with web API operation definitions, so let us take a look on the first example, the operation to fetch everyone.

@Produces(MediaType.APPLICATION_JSON)
@GET
@Operation(
    description = "List all people", 
    responses = {
        @ApiResponse(
            content = @Content(
                array = @ArraySchema(schema = @Schema(implementation = Person.class))
            ),
            responseCode = "200"
        )
    }
)
public Collection<Person> getPeople() {
    // ...
}

Quite a few annotations but by and large, looks pretty clean and straightforward. Let us take a look on another one, the endpoint to find the person by its e-mail address.

@Produces(MediaType.APPLICATION_JSON)
@Path("/{email}")
@GET
@Operation(
    description = "Find person by e-mail", 
    responses = {
        @ApiResponse(
            content = @Content(schema = @Schema(implementation = Person.class)), 
            responseCode = "200"
        ),
        @ApiResponse(
            responseCode = "404", 
            description = "Person with such e-mail doesn't exists"
        )
    }
)
public Person findPerson(
        @Parameter(description = "E-Mail address to lookup for", required = true) 
        @PathParam("email") final String email) {
    // ...
}

In the same vein, the operation to remove the person by e-mail looks mostly identical.

@Path("/{email}")
@DELETE
@Operation(
    description = "Delete existing person",
    responses = {
        @ApiResponse(
            responseCode = "204",
            description = "Person has been deleted"
        ),
        @ApiResponse(
            responseCode = "404", 
            description = "Person with such e-mail doesn't exists"
        )
     }
)
public Response deletePerson(
        @Parameter(description = "E-Mail address to lookup for", required = true ) 
        @PathParam("email") final String email) {
    // ...
}

Great, let us wrap up by looking into last, arguably the most interesting endpoint which adds a new person (the choice to use the @FormParam is purely to illustrate the different flavor of the APIs).

@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
@POST
@Operation(
    description = "Create new person",
    responses = {
        @ApiResponse(
            content = @Content(
                schema = @Schema(implementation = Person.class), 
                mediaType = MediaType.APPLICATION_JSON
            ),
            headers = @Header(name = "Location"),
            responseCode = "201"
        ),
        @ApiResponse(
            responseCode = "409", 
            description = "Person with such e-mail already exists"
        )
    }
)
public Response addPerson(@Context final UriInfo uriInfo,
        @Parameter(description = "E-Mail", required = true) 
        @FormParam("email") final String email, 
        @Parameter(description = "First Name", required = true) 
        @FormParam("firstName") final String firstName, 
        @Parameter(description = "Last Name", required = true) 
        @FormParam("lastName") final String lastName) {
    // ...
}

If you have an experience with documenting web APIs using older Swagger specifications, you might find the approach quite familiar but more verbose (or better to say, formalized). It is the result of the tremendous work which specification leads and community has done to make it as complete and extensible as possible.

The APIs are defined and documented, it is time to try them out! The missing piece though is the Spring configuration where we would initialize and expose our JAX-RS web services.

@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackageClasses = PeopleRestService.class)
public class AppConfig {
    @Autowired private PeopleRestService peopleRestService;
 
    @Bean(destroyMethod = "destroy")
    public Server jaxRsServer(Bus bus) {
        final JAXRSServerFactoryBean factory = new JAXRSServerFactoryBean();

        factory.setApplication(new JaxRsApiApplication());
        factory.setServiceBean(peopleRestService);
        factory.setProvider(new JacksonJsonProvider());
        factory.setFeatures(Arrays.asList(new OpenApiFeature()));
        factory.setBus(bus);
        factory.setAddress("/");

        return factory.create();
    }

    @Bean
    public ServletRegistrationBean cxfServlet() {
        final ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new CXFServlet(), "/api/*");
        servletRegistrationBean.setLoadOnStartup(1);
        return servletRegistrationBean;
    }
}

The OpenApiFeature is a key ingredient here which takes care of all the integration and introspection. The Spring Boot application is the last touch to finish the picture.

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}

Let us build and run it right away:

mvn clean package 
java -jar target/jax-rs-2.1-openapi-0.0.1-SNAPSHOT.jar

Having the application started, the OpenAPI 3.0.0 specification of our web APIs should be live and available for consumption in the JSON format at:

http://localhost:8080/api/openapi.json

Or in YAML format at:

http://localhost:8080/api/openapi.json

Wouldn't it be great to explore our web APIs and play with it? Since we included Swagger UI dependency, this is no brainer, just navigate to http://localhost:8080/api/api-docs?url=/api/openapi.json:

A special attention to be made to the small icon Swagger UI places alongside your API version, hinting about its conformity to OpenAPI 3.0.0 specification.

Just to note here, there is nothing special in using Spring Boot. In case you are using Apache CXF inside the OSGi container (like Apache Karaf for example), the integration with OpenAPI 3.0.0 is also available (please check out the official documentation and samples if you are interested in the subject).

It all looks easy and simple, but what about migrating to OpenAPI 3.0.0 from older versions of the Swagger specifications? The Apache CXF has an powerful feature to convert older the specifications on the fly but in general the OpenApi.Tools portal is a right place to evaluate your options.

Should you migrate to OpenAPI 3.0.0? I honestly believe you should, at least should try experimenting with it, but please be aware that the tooling is still not mature enough, expect a few roadblocks along the way (which you would be able to overcome by contributing the patches, by the way). But undoubtedly, the future is bright!

The complete project sources are available on Github.

Thursday, February 25, 2016

Your JAX-RS APIs were not born equal: using dynamic features

This time we are going to talk a little bit about JAX-RS 2.0 APIs and touch on one very interesting aspect of the specification: dynamic features and how they are useful.

Traditionally, when JAX-RS 2.0 APIs are configured and deployed (using Application class, bootstrapped from servlet or created through RuntimeDelegate), there is an option to register additional providers and features. The great examples of those could be bean validation (JSR 349) or Java API for JSON processing (JSR-353) support. Those providers and features are going to be applied to all JAX-RS 2.0 resources and in most use cases this is a desired behavior. However, from time to time there is a need to enable a particular provider or feature only for some resources, leaving others unaffected. This is exactly the use case where dynamic features are going to help us a lot.

For this post we are going to use the latest version 3.1.5 of excellent Apache CXF framework but dynamic features are part of the JAX-RS 2.0 specification and are supported by most (if not all) of the implementations.

Let us consider a very simple JAX-RS 2.0 API to manage people, with a single method to handle HTTP GET requests. Let us assume this is a version 1 of the API and although the @Range annotation is specified for the count query parameter, its support was never implemented and it is present in the code for documentation purposes only.

@Path("/v1/people")
public class PeopleRestService {
    @Produces( { MediaType.APPLICATION_JSON } )
    @GET
    public List<Person> getAll(@Range(min = 1, max = 10) @QueryParam("count") int count) {
        return Collections.nCopies(count, new Person("[email protected]", "A", "B"));
    }
}

In this case, passing an invalid value for the count query parameter is going to result in Internal Server Error. Let us make sure this is exactly what is happening:

$ curl -i http://localhost:8080/rest/api/v1/people?count=-1

HTTP/1.1 500 Server Error
Cache-Control: must-revalidate,no-cache,no-store
Content-Type: text/html;charset=iso-8859-1
Content-Length: 377
Connection: close
Server: Jetty(9.3.7.v20160115)

After some time we realized the issues with this API and decided to implement the proper validation mechanism in place, using the Bean Validation 1.1 integration with JAX-RS 2.0. However, we made a decision to create version 2 of the API and to keep version 1 untouched as its clients do not expect any other HTTP status codes except 200 and 500 to be returned (unfortunately, in real life it happens more often than not).

There are couple of different approaches to implement such per-API customization, but probably the most simple one is by introducing a dedicated annotation, for example @EnableBeanValidation, and annotating JAX-RS 2.0 resource class with it:

@Path("/v2/people")
@EnableBeanValidation
public class ValidatingPeopleRestService {
    @Produces( { MediaType.APPLICATION_JSON } )
    @GET
    public @Valid List<Person> getAll(@Range(min = 1, max = 10) @QueryParam("count") int count) {
        return Collections.nCopies(count, new Person("[email protected]", "A", "B"));
    }
}

To enable Bean Validation 1.1 for all the JAX-RS 2.0 APIs annotated with @EnableBeanValidation we are going to create a dynamic feature class, BeanValidationDynamicFeature:

@Provider
public class BeanValidationDynamicFeature implements DynamicFeature {
    private final JAXRSBeanValidationInInterceptor inInterceptor;
    private final JAXRSBeanValidationOutInterceptor outInterceptor;
    
    public BeanValidationDynamicFeature(final BeanValidationProvider provider) {
        this.inInterceptor = new JAXRSBeanValidationInInterceptor();
        this.inInterceptor.setProvider(provider);
        
        this.outInterceptor = new JAXRSBeanValidationOutInterceptor();
        this.outInterceptor.setProvider(provider);
    }
    
    @Override
    public void configure(final ResourceInfo resourceInfo, final FeatureContext context) {
        if (resourceInfo.getResourceClass().getAnnotation(EnableBeanValidation.class) != null) {
            context.register(inInterceptor);
            context.register(outInterceptor);
        }
    }
}
Its job is pretty simple, just register JAXRSBeanValidationInInterceptor and JAXRSBeanValidationOutInterceptor interceptor instances as additional providers for JAX-RS 2.0 APIs in question. One minor but important note though: exception mappers are not supported by dynamic features, at least with respect to Apache CXF implementation, and should be registered as a regular providers (along with dynamic features themselves), for example:

@Bean @DependsOn("cxf")
public Server jaxRsServer() {
    final JAXRSServerFactoryBean factory = 
        RuntimeDelegate.getInstance().createEndpoint( 
            jaxRsApiApplication(), 
            JAXRSServerFactoryBean.class 
        );
        
    factory.setServiceBean(validatingPeopleRestService());
    factory.setServiceBean(peopleRestService());
    factory.setProvider(new JacksonJsonProvider());
    factory.setProvider(new BeanValidationDynamicFeature(new BeanValidationProvider()));
    factory.setProvider(new ValidationExceptionMapper());
        
    return factory.create();
}

@Bean 
public JaxRsApiApplication jaxRsApiApplication() {
    return new JaxRsApiApplication();
}
    
@Bean 
public ValidatingPeopleRestService validatingPeopleRestService() {
    return new ValidatingPeopleRestService();
}
    
@Bean 
public PeopleRestService peopleRestService() {
    return new PeopleRestService();
}

That is basically all we have to do. Once the BeanValidationDynamicFeature is registered (in this case using JAXRSServerFactoryBean), it is going to be applied to all matching service beans. Let us make sure that for version 2 of our people management API the proper out of the box validation is triggered:

$ curl -i http://localhost:8080/rest/api/v2/people?count=-1

HTTP/1.1 400 Bad Request
Content-Length: 0
Server: Jetty(9.3.7.v20160115)

This time the response is different, indicating that invalid input has been submitted by the client (straight result of Bean Validation 1.1 in action): Bad Request.

Hopefully, dynamic features are going to be yet another useful tool in your toolbox. The example we have covered here is somewhat imaginary but it is very easy to use dynamic features with security, tracing, logging, profiling, ... Moreover, dynamic features can be applied even on a particular resource methods, allowing fined-grained control over your APIs.

The complete project source is available on Github.

Saturday, October 29, 2011

Back to soapUI: testing web services and Java RMI services

I already covered soapUI in one of my previous blog posts. As I still use this tool quite often and extremely excited about it, I would like to share more testing scenarios we as a developers can use on day-by-day basis. The ones for today's post would be: testing web services and Java RMI services.

So first thing first: let's assume we are developing application which exposes web services and our goal is to have some integration testing in place. We don't want to hard-code SOAP requests and responses, we want to leverage Groovy to code real test cases. Thanks to soapUI, it's so easy to do by using Groovy test steps. Let me omit the routine and focus on bare bones details. I created a simple test project in soapUI with this structure:

The interesting part is here: Call web service Groovy step. Before we move on, let's copy several JAR files to <soapUI home>\bin\ext folder: Then we need to restart soapUI. Now we are ready to fill in the Groovy step with some code. Thanks to GroovyWS, calling web service from Groovy is very easy:

import groovyx.net.ws.WSClient

def properties = testRunner.testCase.getTestStepByName( "Properties" )
def service = new WSClient( properties.getPropertyValue( "url" ), this.class.classLoader )
service.initialize()

def token = service.login(
 properties.getPropertyValue( "username" ),
 properties.getPropertyValue( "password" )
)

assert token != null, "Login is not successful"
Properties step just contains username, password and url configuration:
And that's it! Now we can call additional web service methods and easily run this test case as load test: just to verify how web service behaves under heavy load. I usually do it overnight to see application heap, GC and whatnot. Cool.

Second use case: testing Java RMI services. This one requires a bit more work to be done. First of all, you need soapUI to be run using RMISecurityManager. Let's do this.

  1. Create file soapui.policy with content below and store it in <soapUI home>\bin:
    grant {
        permission java.security.AllPermission;
    };
    
  2. Change soapUI command line (<soapUI home>\bin\soapui.bat). Find the line set JAVA_OPTS=... and append to it:
    -Djava.security.policy=soapui.policy -Djava.security.manager=java.rmi.RMISecurityManager
    
    So you will have something like this: set JAVA_OPTS=-Xms128m -Xmx1024m -Dsoapui.properties=soapui.properties "-Dsoapui.home=%SOAPUI_HOME%\" -Djava.security.policy=soapui.policy -Djava.security.manager=java.rmi.RMISecurityManager
To run a bit ahead, we need to copy several JAR files to <soapUI home>\bin\ext folder: Now we a good and just need to restart soapUI. The sample project structure is very similar to what we did before:

Respective Groovy test step is built using Spring Framework which significantly simplifies creating RMI stubs and clients by getting rid of the boilerplate code.
import org.springframework.remoting.rmi.RmiProxyFactoryBean
import com.example.RmiServiceInterface

def properties = testRunner.testCase.getTestStepByName( "Properties" )
def invoker = new RmiProxyFactoryBean( 
 serviceUrl: properties.getPropertyValue( "url" ), 
 serviceInterface: com.example.RmiServiceInterface 
)
invoker.afterPropertiesSet()

def service = invoker.object
def token = service.login(
 properties.getPropertyValue( "username" ),
 properties.getPropertyValue( "password" )
)

assert token != null, "Login is not successful"
Now our service is ready for more serious testing. As with web services scenario Properties step just contains username, password and url (RMI) configuration:
I personally found soapUI to be very helpful tool in my developer toolbox and I definitely recommend using it.