Voorhees is a simple library helping to build JSON RPC 2.0 over HTTP services in Spring applications in a quite fast and seamless way.
- Installation
- Services
- Client (Java)
- Client (Python)
- Exception Handling
- Spring Integration
- Client library generation
It's not yet deployed in public maven repositories so use it as system artifact, or deploy it to your local maven repository.
A service in Voorhees is just a simple class marked as
@JsonRpcService
:
@JsonRpcService(location = "/my")
public class MyService {
public Person getMe() {
return ME;
}
public int getAge(Person person) {
return person.getAge();
}
public Person birthday(Person person) {
person.setAge(person.getAge() + 1);
return person;
}
}
Please note that location
attribute should specify at least one endpoint.
All the public methods of this service class (but not its' superclasses) will be exposed.
If you want to suppress exposing specific method, annotate it with
@DontExpose
.
@JsonRpcService(location = "/my")
public class MyService {
@DontExpose
public Person getMe() {
return ME;
}
// ...
}
In case you want to register services programmatically, there is a way to do that:
@Autowired
private JsonRpcHandlerMapping jsonRpcMapping;
jsonRpcMapping.registerService(new MyService(), "/my");
It could be helpful if you prefer to not use annotations, or it's just impossible 'cause a service is a third-party class.
By default the Java method name is used by JSON RPC. However, it is possible to change it.
For example, one can add prefix to all exposed methods of a particular service:
@JsonRpcService(location = "/my", prefix = "mine")
public class MyService {
public Person getMe() {
return ME;
}
The method name in this case will be "mine.getMe" not "getMe".
To expose some method with a different name one can use @JsonRpcMethod
:
@JsonRpcMethod(name = "persons.me")
public Person getMe() {
return ME;
}
Since JSON RPC supports named parameters for RPC method, Voorhees uses
parameter names of the service method as RPC method parameter names.
Java could strip this information from class files, so sometimes it's
impossible to get parameter name using Java reflection. It returns
names like arg0
, arg1
etc. Sometimes there is additional information
(Kotlin classes metainfo, AspectJ metainfo, etc.) that potentially could
be used in order to get parameter names. And this is exactly what Spring
parameter name discoverer does. Voorhees uses it to find these names.
But there is also other way to name the parameters: Use name
attribute
of the @Param
annotation:
@JsonRpcService(location = "/my")
public class MyService {
public int sum(@Param("left") int x, @Param("right") int y) {
return x + y;
}
// ...
}
In that case left
and right
are being used as names when RPC with named
parameters is executed:
myService.sum(left=3, right=4)
myService.sum(right=4, left=3)
Voorhees support default values for the method parameters. It means that if there is no enough parameters in method call, and the rest of the parameters already have default values, these values are to be used.
For example:
@JsonRpcService(location = "/my")
public class MyService {
public int sum(@Param(defaultValue="0") int x, @Param(defaultValue="1") int y) {
return x + y;
}
// ...
}
We can call it as
myService.sum(3, 4)
myService.sum(3)
myService.sum()
Note that default values are strings keeping JSON representation of the default value. The only exception is string value. So if Voorhees is not able to parse default value, it uses it as is if the type of the parameter is String.
In order to specify the common API prefix for all JSON RPC services use
spring.voorhees.server.api.prefix
Spring property:
spring:
voorhees:
server:
api:
prefix: /api
Note that it should start with slash or be an empty string.
For example, if prefix is /api
and service location is /my
then the
real endpoint is going to be /api/my
.
To use a Voorhees client the service interface is required:
public interface RemoteMyService {
Person getMe();
int getAge(Person person);
Person birthday(Person person);
}
To set up it, we need to know where the server is located and what is the service endpoint:
ServerConfig serverConfig = new ServerConfig("http://localhost:8080/");
JsonRpcClient client = new JsonRpcClient(serverConfig);
The client is thread-safe, we can use it from parallel thread to get the remote services. To get the remote service, provide service's endpoint and interface:
RemoteMyService myService = client.getService("/my", RemoteMyService.class);
That's it! Just use it as a regular java class:
Person me = myService.getMe();
System.out.println(myService.getAge(me));
me = myService.birhtday(me);
System.out.println(myService.getAge(me));
To be updated
Let's take Python as an example of dynamically typed language to show
how easy is to use JSON RPC with it (jsonrpc
library):
from jsonrpc import ServiceProxy
myService = ServiceProxy("http://localhost:8080/my")
me = myService.getMe()
print(myService.getAge(me))
me = myService.birthday(me)
print(myService.getAge(me))
Note that you even don't have to create a remote service stub.
If an exception has been thrown during remote method call, JSON RPC returns error object with a code specific for this error. Depending on this code a client could throw specific exception that is mapped to this code.
There are several predefined errors in JSON RPC:
Code | Description | Exception |
---|---|---|
-32700 | Parse error | ParseErrorException |
-32600 | Invalid Request | InvalidRequestException |
-32601 | Method not found | MethodNotFoundException |
-32602 | Invalid params | InvalidParamsException |
-32603 | Internal error | InternalErrorException |
One can specify their own custom exception. In order to allow Spring
to automatically pick it up, the exception should have static field
int CODE
with the error code this exception is mapped to.
public class FieldException extends JsonRpcException {
private static final int CODE = 214;
public FieldException(String message, List<String> fields) {
super(new Error(CODE, message, userErrorCodes));
}
}
If Spring Boot is not in use, the exception should be registered directly in JSON RPC client:
jsonRpcClient.registerException(FieldException.class)
If this exception is thrown, the JSON RPC client parses it properly, for example if we have response
{
"id": 1,
"jsonrpc": "2.0",
"error": {
"code": 214,
"message": "Some field values are invalid",
"data": [
"confirmation should be same as password",
"email is invalid"
]
}
}
then on the client side this exception will be thrown:
throw new FieldException("Some field values are invalid", listOf(
"confirmation should be same as password",
"email is invalid"
));
There is no need to take any additional step in order to integrate Voorhees. Having the jar dependency in your class path means that Spring Boot application will be configured automatically.
To disable Voorhees auto-configuration, you want to set application
property spring.autoconfigure.exclude
:
spring:
autoconfigure:
exclude: com.hylamobile.voorhees.server.spring.config.VoorheesAutoConfiguration
To enable Voorhees in your Spring applications, use
@EnableVoorhees
@EnableVoorhees
public class JsonRpcConfiguration {
}
If you use client-library generation procedure you may take an advantage of remote services auto-registration:
dependencies {
compile("com.hylamobile:voorhees-client-springboot:3.0.0-RC1")
// ...
}
Instead of manual creating of JSON RPC clients and building services
out of them, just describe them in application.yml
:
voorhees:
client:
basePackage: com.acme.remote
services:
default:
endpoint: http://server.com/api
user-service:
endpoint: http://server.com/user-service
targets: com.acme.remote.user
restTemplate: userRestTemplate
There could be default
client that builds all of the services from
the basePackage
that are not listed in targets
of any other service.
The property target
is a list of service classes and/or packages that
are supposed to be scanned recursively in order to find services for
the current client. E.g. in the example provided above for the client
user-service
package com.acme.remote.user
will be scanned to pick up
all the services from it.
Apparently endpoint
property binds specified endpoint to all belonging
services.
By default Voorhees uses JSON RPC transport that is located in classpath
(see JSON RPC transport). But for auto-registered services if the
property restTemplate
is specified then custom transport will be used.
This transport uses RestTemplate
bean with specified name to get the
data from the JSON RPC server. In the example above for user-service
client userRestTemplate
will be used. It's helpful if you need to
define specific HTTP properties, e.g. authentication etc.
To use a service just autowire it:
@Autowired
private MyService myService;
Voila!
Currently it's possible to generate client library instead of implementing it manually.
Voorhees groovy plugin provides a possibility to generate client library, install it to local maven repository or publish to remote one.
Apply voorhees
in build.gradle. If you want to publish client library
apply maven-publish
plugin as well. For example:
plugins {
id "java"
id "maven-publish"
id "voorhees"
}
Define where you want the plugin to search for remote services in
packagesToScan
attribute of voorhees
extension. If you intend to
publish client library, you want to define artifact
as well as
group
and version
. Note that if group
and version
are not
defined (as in example below) then group
and version
of the current
project will be used:
group = "com.acme"
version = "0.0.1"
voorhees {
packagesToScan = ["com.acme.remote"]
artifact = "myservice-client"
}
Since your application uses voorhees-server
it should be defined in
dependencies:
dependencies {
compile("com.hylamobile:voorhees-server:3.0.0-RC1")
// other deps: spring etc.
}
Currently plugin is not yet deployed to public repositories, so please use your own as in example below:
repositories {
jcenter()
maven {
url mavenRepoUrl
credentials {
username mavenRepoUsername
password mavenRepoPassword
}
}
}
Same for publishing. Just use the repository you want to publish client library to. Currently only maven repository is supported:
publishing {
repositories {
// repository to publish client library
maven {
url mavenRepoUrl
credentials {
username mavenRepoUsername
password mavenRepoPassword
}
}
}
}
Task provided by the plugin:
- generateJsonRpcClient Generates classes in
build/classes/voorhees
directory. - jarJsonRpcClient Archives generated classes to a jar in
build/libs
. - publishJsonRpcClient (only if
maven-publish
is active) Publishes generated jar to maven repository.
Apart from that, maven-publish
itself creates
publishJsonRpcClientPublicationToMavenLocal
and
publishJsonRpcClientPublicationToMavenRepository
.
Please note that only classes from the packages mentioned in
packagesToScan
attribute and their subpackages (recursively) will be
generated for client library.