- Overview
- Recommended code organization
- Usage
- Exception marshalling / demarshalling
- Demo
- TODO
- Contributing
- Copyright and license
A java rest client auto generator with stubs and proxy code mimicking your webservices.
Specifically, Alchemy rest client generator
- generates a runtime proxy for a restful webservice class, useful for unit / integration testing.
- has an ant task to generates webservice stubs and proxy implementations. This can be part of your build scripts, generating a new client every time your webservice code changes. This would constitute your client library.
- uses guice for dependency injection, leading to cleaner and more loosely coupled code
- supports query, path, matrix, header, cookie and form parameters for the rest methods
- can marshall and demarshall exceptions thrown by services so that client can see the exception as if they were thrown by local methods.
Alchemy rest client does not
- generate client side transfer objects as yet. For example if have a webservice as follows.
@GET
@PATH("/history")
public CommitInfo getHistory(String objectURI) {
....
}
The generated client will not mimic / stub the CommitInfo object. The transfer object classes / jars will need to shared with the client. This seems reasonable at the moment.
We recommend you split your service code into at least three components
-
common containing
-
direct transfer objects / dependencies or dependencies to modules having these transfer objects.
-
exception marshalling / demarshlling code as discussed above
-
jackson json modules if any for using jackson
-
client containing
-
auto generated code
-
custom client bindings for server URL, credentials etc.
-
server containing
-
webservices and dependent server side module dependencies
compile 'com.strandls.alchemy:alchemy-rest-client-generator:0.9'
<dependency>
<groupId>com.strandls.alchemy</groupId>
<artifactId>alchemy-rest-client-generator</artifactId>
<version>0.9</version>
</dependency>
package com.strandls.alchemy.restclient.demo;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import lombok.extern.slf4j.Slf4j;
/**
* A simpleton echo service.
*/
@Path("/echo")
@Slf4j
public class EchoService {
/**
* Echoes back the input string.
*
* @param input
* the input string
* @return the input string
*/
@GET
@Path("{toEcho}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String echo(@PathParam("toEcho") String input) {
log.debug("Echo service invoked with input '{}'", input);
return input;
}
}
The following junit test EchoServiceTest
- starts the EchoService in a Grizzly webcontainer.
- uses the client generator to create a runtime proxy for the webservice
- tests to ensure the service works
Code relevant to using the runtime proxy is show below.
public class EchoServiceTest extends JerseyTest {
.
.
.
/**
* Inject the client factory.
*/
private AlchemyRestClientFactory clientFactory;
/**
* Test the echo service.
*
* @throws Exception
*/
@Test
public void testEcho() throws Exception {
EchoService echoClient = clientFactory.getInstance(EchoService.class);
String testinput = "Hello World !!!!";
assertEquals(testinput, echoClient.echo(testinput));
}
}
We supply the base uri for the webservice to the generated client proxy via guice bindings. The follows Guice module is an example for how to do this.
package com.strandls.alchemy.restclient.demo;
import javax.ws.rs.client.Client;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.name.Names;
import com.strandls.alchemy.rest.client.AlchemyRestClientFactory;
/**
* The client side guice module.
*/
public class ClientBindingModule extends AbstractModule {
/*
* (non-Javadoc)
* @see com.google.inject.AbstractModule#configure()
*/
@Override
protected void configure() {
// bind the URI.
bind(String.class).annotatedWith(
Names.named(AlchemyRestClientFactory.BASE_URI_NAMED_PARAM)).toInstance("http://localhost:9999");
// bind the client.
bind(Client.class).toProvider(JaxRsClientProvider.class);
}
}
The proxy code uses Jersey client behind the scenes to make http calls.
The following code set's up the Jersey client. Notice the use of JacksonJsonProvider to transparently convert to and from json input parameters and return values.
package com.strandls.alchemy.restclient.demo;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import lombok.RequiredArgsConstructor;
import org.eclipse.net4j.util.security.ICredentialsProvider;
import org.eclipse.net4j.util.security.IPasswordCredentials;
import org.eclipse.net4j.util.security.PasswordCredentialsProvider;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.filter.HttpBasicAuthFilter;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
/**
* Provides the {@link Client} after applying the jackson bindings.
*/
@Singleton
@RequiredArgsConstructor(onConstructor = @_(@Inject))
public class JaxRsClientProvider implements Provider<Client> {
/**
* The jackson json provider.
*/
private final JacksonJsonProvider jsonProvider;
/*
* (non-Javadoc)
* @see javax.inject.Provider#get()
*/
@Override
@Singleton
public Client get() {
final ClientConfig clientConfig = new ClientConfig();
// register the json provider
clientConfig.register(jsonProvider);
return ClientBuilder.newClient(clientConfig);
}
}
Once you setup the client provider and a module to bind the base URI, all you need to do is inject AlchemyRestClientFactory into your code.
The rest client code generator requires the webservice classes to be in its classpath. The generated client code however does not depend on the webservice classes.
Note: Client will still need in its classpath, classes / jars for custom input paramter and return types.
You can setup basic http authentication on the server side by following [JerseyAuthDoc][Jersey documentation] or by looking up the demo code.
The client needs to implement an the interface com.strandls.alchemy.rest.client.request.RequestBuilderFilter to plugin http authentication. The implementation could also be used to set other headers before the http request is made.
Example client request builder filter that sets up http credentials
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.ws.rs.client.Invocation.Builder;
import lombok.RequiredArgsConstructor;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import com.strandls.alchemy.rest.client.request.RequestBuilderFilter;
import com.strandls.alchemy.webservices.common.auth.Credentials;
/**
* Request builder that sets autntication credentials.
*
* @author Ashish Shinde
*
*/
@Singleton
@RequiredArgsConstructor(onConstructor = @_(@Inject))
public class AuthRequestBuilderFilter implements RequestBuilderFilter {
/**
* The credentials provider.
*/
private final Provider<Credentials> credentialsProvider;
/*
* (non-Javadoc)
* @see
* com.strandls.alchemy.rest.client.request.RequestBuilderFilter#apply(javax
* .ws.rs.client.Invocation.Builder)
*/
@Override
public void apply(final Builder builder) {
final Credentials credentials = credentialsProvider.get();
String user = "James Bond";
String password = "007";
builder.property(HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_USERNAME, user)
.property(HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_PASSWORD, password);
}
}
Alchemy rest client enables the client to raise exceptions that were raised by the server as if they were generated locally. This can be setup by adding the following classed to both the server and client. Ideally you could create a common module and have the server and client depend on both
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.Path;
import javax.ws.rs.WebApplicationException;
import lombok.extern.slf4j.Slf4j;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.strandls.alchemy.inject.AlchemyModule;
import com.strandls.alchemy.inject.AlchemyModule.Environment;
import com.strandls.alchemy.reflect.JavaTypeQueryHandler;
import com.strandls.alchemy.rest.client.NotRestInterfaceException;
import com.strandls.alchemy.rest.client.RestInterfaceAnalyzer;
import com.strandls.alchemy.rest.client.exception.ThrowableMaskMixin;
import com.strandls.alchemy.rest.client.exception.ThrowableObjectMapper;
/**
* Binding for {@link ObjectMapper} used for server side error conversions.
*
* @author Ashish Shinde
*
*/
@Slf4j
@AlchemyModule(Environment.All)
public class ExceptionObjectMapperModule extends AbstractModule {
/**
* The jax rs package root.
*/
private static final String JAVAX_WS_RS_PACKAGE = "javax.ws.rs";
/**
* Elixir web service package root.
*/
private static final String ALCHEMY_SERVICE_PACKAGE = "com.strandls.alchemy";
/*
* (non-Javadoc)
* @see com.google.inject.AbstractModule#configure()
*/
@Override
protected void configure() {
}
/**
* Binding for throwable exception mapper.
*
* @param mapper
* @return
*/
@Provides
@Singleton
@ThrowableObjectMapper
@Inject
public ObjectMapper getExceptionObjectMapper(final ObjectMapper mapper,
final RestInterfaceAnalyzer restInterfaceAnalyzer,
final JavaTypeQueryHandler typeQueryHandler) {
// can't copy owing to bug -
// https://github.com/FasterXML/jackson-databind/issues/245
final ObjectMapper exceptionMapper = mapper;
exceptionMapper.registerModule(new SimpleModule() {
/**
* The serial version id.
*/
private static final long serialVersionUID = 1L;
/*
* (non-Javadoc)
* @see
* com.fasterxml.jackson.databind.module.SimpleModule#setupModule
* (com.fasterxml.jackson.databind.Module.SetupContext)
*/
@Override
public void setupModule(final SetupContext context) {
// find exceptions thrown by webservices
final Set<Class<?>> serviceClasses =
typeQueryHandler.getTypesAnnotatedWith(ALCHEMY_SERVICE_PACKAGE, Path.class);
final Set<Class<?>> exceptionsUsed = new HashSet<Class<?>>();
for (final Class<?> serviceClass : serviceClasses) {
// get hold of all rest methods and hence exception
try {
final Set<Method> restMethods =
restInterfaceAnalyzer.analyze(serviceClass).getMethodMetaData()
.keySet();
for (final Method method : restMethods) {
for (final Class<?> exceptionClass : method.getExceptionTypes()) {
exceptionsUsed.add(exceptionClass);
}
}
} catch (final NotRestInterfaceException e) {
log.error("Error geting exception classes for methods from {}",
serviceClass);
throw new RuntimeException(e);
}
}
// add the mixin to all jaxrs classes as well.
exceptionsUsed.addAll(typeQueryHandler.getSubTypesOf(JAVAX_WS_RS_PACKAGE,
WebApplicationException.class));
for (final Class<?> exceptionClass : exceptionsUsed) {
// add a mixin to prevent server stack trace from showing up
// to the client.
log.debug("Applied mixin mask to {}", exceptionClass);
context.setMixInAnnotations(exceptionClass, ThrowableMaskMixin.class);
}
}
});
exceptionMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return exceptionMapper;
}
}
The code above also ensures that the stacktrace is not send to the client, which could be a security requirement.
You need some code on the server side to marshall exceptions raised by services into the response body. This is done by adding the class below to the server
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.strandls.alchemy.rest.client.exception.ThrowableToResponseMapper;
/**
* Mapper for {@link Exception}s generated from the webservices.
*
* @author Ashish Shinde
*
*/
@Provider
@RequiredArgsConstructor(onConstructor = @_(@Inject))
@Singleton
@Slf4j
public class AlchemyExceptionMapper implements javax.ws.rs.ext.ExceptionMapper<Exception> {
/**
* The response mapper.
*/
private final ThrowableToResponseMapper responseMapper;
/*
* (non-Javadoc)
* @see javax.ws.rs.ext.ExceptionMapper#toResponse(java.lang.Throwable)
*/
@Override
public Response toResponse(final Exception exception) {
log.error("{}", exception);
return responseMapper.apply(exception);
}
}
You might need to bind add a guice binding to your Servlet module this class, to get the exception mapper to work.
/*
* (non-Javadoc)
* @see com.google.inject.servlet.ServletModule#configureServlets()
*/
@Override
protected void configureServlets() {
.
.
bind(AlchemyExceptionMapper.class);
.
.
}
The Alchemy Rest Client Demo project is a good demostration of real life use of this module.
Generate client from WADL.
Please refer to Contribution Guidlines if you are not familiar with contributing to open source projects.
The gist for making a contibution is
- Fork
- Create a topic branch -
git checkout -b <your branch>
- Make your changes
- Push to your branch -
git push origin <your branch>
- Create an [Issue] with a link to your branch
Run
gradle/gradlew eclipse
Import alchemy inject to eclipse using File > Import > Existing Projects into Workspace
The project has been setup to auto format the code via eclipse save actions. Please try not to disturb this.
Code and documentation copyright 2015 Strand Life Sciences. Code released under the Apache License 2.0. Docs released under Creative Commons.