-
Notifications
You must be signed in to change notification settings - Fork 7
[en] Tutorial
In this tutorial, we will create a simple RESTful Web serivce for todo list management, and a Restler-based console client for that service.
You can check full source code here
The project consists of 3 parts: API, server and client. The server will implement the API, and the client will depend on it.
Restler Todo Architecture
Restler Todo Project Structure
The root build file contains only a subprojects
section that adds the java plugin, maven repositories and sets the source and target versions to Java 1.8.
subprojects {
apply plugin: 'java'
repositories {
mavenCentral()
maven { url "http://repo.maven.apache.org/maven2" }
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
Let's now consider each of the three project parts in detail.
The API consists of two classes: Todo
and Todos
. Todo
is a simple DTO that is transferred between a client and a server, Todos
describes operations provided by a server.
public class Todo implements Serializable {
public final String id;
public final String name;
public final String description;
public final boolean done;
@JsonCreator
public Todo(String id, String name, String description, boolean done) {
this.id = id;
this.name = name;
this.description = description;
this.done = done;
}
public Todo(String name, String description, boolean done) {
this(null, name, description, done);
}
public Todo withId(String id) {
return new Todo(id, name, description, done);
}
@Override
public String toString() {
return "Todo{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", description='" + description + '\'' +
", done=" + done +
'}';
}
}
The Todo
class doesn't stand out in any way, except that it is immutable, and getters are dropped out in favor of public fields, because any logic requiring a getter may be placed in the constructor.
@RestController("todo")
public interface Todos {
@RequestMapping(value = "", method = RequestMethod.POST)
Todo create(@RequestBody Todo todo);
@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
Todo update(@PathVariable String id, @RequestBody Todo todo);
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
Todo delete(@PathVariable String id);
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
Todo get(@PathVariable String id);
@RequestMapping(value = "", method = RequestMethod.GET)
Todo[] list();
}
Todos
is an interface that will be implemented by the server module. But that class specifies not only a Java interface, but also a REST interface. Specifically, it describes which HTTP requests may be executed on a Todo
resource. For Restler, this solution is of particular importance. because a client doesn’t depend on the server implementation, it only depends on its interface.
But, as everything in the software world, this solution has a flaw: names of interface method parameters are not present in Java class files by default. That means that parameter names cannot be retrieved at run time. There are two ways to avoid that limitation:
- Define parameter names in annotations (e.g.
@PathVariable("id")
) - Add the
"-parameters"
flag to javac arguments to enforce writing of interface parameter names into class files.
In this tutorial we picked the second option.
In the API build file, we add the required dependencies and the compiler flag -parameters
(see the Code section above).
dependencies {
compile 'org.springframework:spring-webmvc:4.1.7.RELEASE',
'com.fasterxml.jackson.core:jackson-databind:2.5.4'
}
compileJava {
options.compilerArgs << '-parameters'
}
The server will be based on Spring Boot, so it can be defined in just two classes:
@EnableAutoConfiguration
@Configuration
@EnableWebMvc
public class Starter extends WebMvcConfigurerAdapter {
@Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
ParanamerModule module = new ParanamerModule();
converters.stream().
filter(c -> c instanceof MappingJackson2HttpMessageConverter).
forEach(c -> ((MappingJackson2HttpMessageConverter) c).getObjectMapper().registerModule(module));
}
@Bean Todos todos() {
return new TodosController();
}
public static void main(String[] args) {
SpringApplication.run(Starter.class, args);
}
}
public class TodosController implements Todos {
private ConcurrentHashMap<String, Todo> todos = new ConcurrentHashMap<>();
@Override
public Todo create(Todo todo) {
String id = UUID.randomUUID().toString();
Todo storedTodo = todo.withId(id);
todos.put(id, storedTodo);
return storedTodo;
}
@Override
public Todo update(String id, Todo todo) {
return todos.put(id, todo.withId(id));
}
@Override
public Todo delete(String id) {
return todos.remove(id);
}
@Override
public Todo get(String id) {
return todos.get(id);
}
@Override
public Todo[] list() {
return todos.values().toArray(new Todo[todos.size()]);
}
}
The Starter
class is the configuration and the server entry point at the same time. TodosController
is a trivial server implementation that uses an in-memory map to store todos.
The required dependencies are specified in the server build file, and the application plugin is applied, so that the server can be packaged and run as an application.
apply plugin: 'application'
mainClassName = 'org.restler.todo.Starter'
dependencies {
compile project(':api'),
'org.springframework.boot:spring-boot-starter-web:1.2.5.RELEASE',
'com.fasterxml.jackson.module:jackson-module-paranamer:2.6.1'
}
Without Restler, we would have to implement the Todos
interface manually, as in the following class:
public class ManualTodosImpl implements Todos {
private static final String baseUrl = "http://localhost:8080/";
private RestTemplate restTemplate = new RestTemplate();
public ManualTodosImpl() {
ParanamerModule module = new ParanamerModule();
restTemplate.getMessageConverters().stream().
filter(c -> c instanceof MappingJackson2HttpMessageConverter).
forEach(c -> ((MappingJackson2HttpMessageConverter) c).getObjectMapper().registerModule(module));
}
@Override
public Todo create(Todo todo) {
return restTemplate.postForObject(baseUrl, todo, Todo.class);
}
@Override
public Todo update(String id, @RequestBody Todo todo) {
try {
return restTemplate.exchange(
new RequestEntity<>(todo, null, HttpMethod.PUT,
new URI(baseUrl + "/" + id)), Todo.class).getBody();
} catch (URISyntaxException e) {
throw new RuntimeException("Unexpected exception", e);
}
}
@Override
public Todo delete(@PathVariable String id) {
try {
return restTemplate.exchange(
new RequestEntity<>(null, null, HttpMethod.DELETE,
new URI(baseUrl + "/" + id)), Todo.class).getBody();
} catch (URISyntaxException e) {
throw new RuntimeException("Unexpected exception", e);
}
}
@Override
public Todo get(@PathVariable String id) {
return restTemplate.getForObject(baseUrl + "/" + id, Todo.class);
}
@Override
public Todo[] list() {
return restTemplate.getForObject(baseUrl, Todo[].class);
}
}
With Restler, the entire client consists of just one class:
public class TodoClient {
private HashMap<String, Function<String[], Result>> handlers;
private Todos todos;
public TodoClient() {
handlers = new HashMap<>();
handlers.put("e", this::exitHandler);
handlers.put("l", this::listHandler);
handlers.put("c", this::createHandler);
handlers.put("u", this::updateHandler);
handlers.put("d", this::deleteHandler);
handlers.put("g", this::getHandler);
}
public static void main(String[] args) throws IOException {
new TodoClient().run();
}
private void run() throws IOException {
// THIS 5 LINES ARE EQUIVALENT TO CLASS FROM PREVIOUS SECTION
SpringMvcSupport springMvcSupport = new SpringMvcSupport();
builder.addJacksonModule(new ParanamerModule()); + springMvcSupport.addJacksonModule(new ParanamerModule());
Restler builder = new Restler("http://localhost:8080", springMvcSupport);
Service todosService = builder.build();
todos = todosService.produceClient(Todos.class);
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in))) {
reader.lines().
map(command -> {
String[] cmdParts = command.split(" ");
return handlers.
getOrDefault(cmdParts[0], this::defaultHandler).
apply(tail(cmdParts));
}).
filter(Result.BREAK::equals).findAny();
}
}
private Result createHandler(String[] args) {
todos.create(todo(args));
return Result.CONTINUE;
}
private Result updateHandler(String[] args) {
todos.update(args[0], todo(tail(args)));
return Result.CONTINUE;
}
private Result deleteHandler(String[] args) {
todos.delete(args[0]);
return Result.CONTINUE;
}
private Result listHandler(String[] args) {
Arrays.stream(todos.list()).
map(todo -> todo.id).
forEach(System.out::println);
return Result.CONTINUE;
}
private Result getHandler(String[] args) {
System.out.println(todos.get(args[0]));
return Result.CONTINUE;
}
private Result exitHandler(String[] args) {
return Result.BREAK;
}
private Result defaultHandler(String[] args) {
return Result.CONTINUE;
}
private Todo todo(String[] args) {
return new Todo(args[0], args[1], Boolean.valueOf(args[2]));
}
private String[] tail(String[] cmdParts) {
return Arrays.copyOfRange(cmdParts, 1, cmdParts.length);
}
private enum Result {CONTINUE, BREAK}
}
The following three lines in the run()
method are of interest to us:
ServiceBuilder builder = new ServiceBuilder("http://localhost:8080");
Service todosService = builder.build();
todos = todosService.produceClient(Todos.class);
In these lines we create an instance of a class that implements the Todos
interface that we can use later to perform todos list operations.
In the client build file, the required dependencies are specified and the application plugin is applied, so thet the client can be packaged and run as application.
apply plugin: 'application'
mainClassName = 'org.restler.todo.client.TodoClient'
dependencies {
compile 'org.restler:restler-spring-mvc:0.4.0',
'com.fasterxml.jackson.module:jackson-module-paranamer:2.6.1',
project(':api')
}
First, we need to build the project:
./gradlew build
Then we need to start the server:
./gradlew :server:run
Running the client is a little bit more complicated, because when Gradle starts an application, there is no console present, and we have to run it manually. But thanks to the application plugin we only need to unzip the created archive and run a script:
unzip client/build/distributions/client-0.1.0-SNAPSHOT.zip
client-0.1.0-SNAPSHOT/bin/client