Fork me on GitHub

Spring REST HTTP Server Tutorial

This tutorial demonstrates using the OfficeFloor REST Spring Boot Starter to declare REST endpoints as YAML configuration files while retaining the full power of Spring's dependency injection.

Rather than annotating controllers with @RestController and @RequestMapping, each endpoint is a YAML file whose path in the officefloor/rest/ directory mirrors the URL it serves. The YAML file names one or more plain Java classes whose methods handle the request. Multiple methods can be composed (wired together in sequence directly in the YAML) without either class knowing about the other.

The example used in this tutorial is three endpoints:

  • GET /greeting returns {"message":"Hello, World!"} using a Spring bean
  • GET /greeting/{name} returns a personalised greeting using a path parameter
  • POST /greeting validates the request, conditionally routes via a custom flow interface to a build step, then audits. This is via three composed methods, each with its own concern and Spring bean dependencies

Tutorial Source

Maven dependency

Adding a single starter dependency to pom.xml is all that is required:

		<dependency>
			<groupId>net.officefloor.springboot</groupId>
			<artifactId>officefloor-rest-spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

The officefloor-rest-spring-boot-starter auto-configures OfficeFloor into the Spring MVC pipeline. On start-up it scans the classpath for YAML files under officefloor/rest/ and registers each one as a HandlerInterceptor for the corresponding HTTP method and URL path. No additional Java or XML configuration is needed.

Application class

The application entry point is a standard Spring Boot class:

@SpringBootApplication
public class SpringRestApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringRestApplication.class, args);
	}
}

YAML endpoint files — path and method from the file name

Endpoints are declared as YAML files placed under src/main/resources/officefloor/rest/. The file name encodes both the HTTP method and the URL path:

officefloor/rest/
├── greeting.GET.yml          →  GET  /greeting
├── greeting.POST.yml         →  POST /greeting
└── greeting/
    └── {name}.GET.yml        →  GET  /greeting/{name}

The naming convention is {path}.{METHOD}.yml. Directory structure below officefloor/rest/ becomes the URL path, so deeper URLs are produced simply by nesting files in sub-directories. Curly-brace segments such as {name} become URL path parameters. The special filename index.{METHOD}.yml maps to the root path /.

YAML endpoint files — entries are named steps

Inside each YAML file, entries are named steps. The label on each entry is a developer-chosen name used to wire steps together — it is not a keyword. A step entry identifies the Java class that implements it:

myLabel:
  class: com.example.MyLogic

The first entry in the file is always called when the HTTP request arrives.

When a class has only one public method that method is used automatically. When there are multiple public methods the method key selects the right one:

enrich:
  class: com.example.MyLogic
  method: enrich

Spring bean injection — parameters injected by type

The GreetingService is a plain Spring @Service:

@Service
public class GreetingService {

	public String greet(String name) {
		return "Hello, " + name + "!";
	}
}

OfficeFloor registers every bean in the Spring application context as a managed object. Any parameter of a service method whose type matches a Spring bean is injected automatically — no annotation is needed on that parameter.

The handler for GET /greeting receives the bean and uses ObjectResponse to write the JSON response:

public class GetGreetingLogic {

	public void service(GreetingService greetingService, ObjectResponse<GreetingResponse> response) {
		response.send(new GreetingResponse(greetingService.greet("World")));
	}
}

ObjectResponse<T> serialises the object to JSON and writes it to the HTTP response.

Spring annotations on parameters

Service methods can use all the standard Spring MVC parameter annotations. The handler for GET /greeting/{name} uses @PathVariable:

public class GetNamedGreetingLogic {

	public void service(
			@PathVariable(name = "name") String name,
			GreetingService greetingService,
			ObjectResponse<GreetingResponse> response) {
		response.send(new GreetingResponse(greetingService.greet(name)));
	}
}

Both Spring annotations (@PathVariable, @RequestParam, @RequestHeader, @CookieValue, @RequestBody, @ModelAttribute, @RequestPart) and OfficeFloor's own annotations (@HttpPathParameter, @HttpQueryParameter, @HttpHeaderParameter, @HttpObject) are available on any service method parameter.

Composing methods — the POST /greeting pipeline

This is the full YAML for POST /greeting:

validate:
  class: net.officefloor.tutorial.springresthttpserver.ValidateGreetingLogic
  outputs:
    valid: build

build:
  class: net.officefloor.tutorial.springresthttpserver.PostGreetingLogic
  next: audit

audit:
  class: net.officefloor.tutorial.springresthttpserver.AuditGreetingLogic

Three steps, three classes, three distinct concerns. None of these classes imports or references the others — the YAML is the only place that knows about their order and wiring.

Composing methods — conditional routing with a custom flow interface

The validate step decides whether the request is usable. It declares a @Flow-annotated parameter whose type is a custom functional interface:

public class ValidateGreetingLogic {

	@FunctionalInterface
	public interface ValidGreetingFlow {
		void flow(GreetingRequest request);
	}

	public void service(
			@RequestBody GreetingRequest request,
			@Flow("valid") ValidGreetingFlow valid,
			ObjectResponse<GreetingResponse> response) {
		if (request.getName() == null || request.getName().isBlank()) {
			response.send(new GreetingResponse("Hello, World!"));
		} else {
			valid.flow(request);
		}
	}
}

OfficeFloor inspects ValidGreetingFlow.flow(GreetingRequest) at start-up and records that calling this interface passes a GreetingRequest as the parameter to the receiving step. Can use any single-abstract-method interface whose method has at most two parameters (an optional argument to target method followed by an optional FlowCallback).

The outputs map in the YAML connects the @Flow name to the receiving step:

validate:
  class: ...ValidateGreetingLogic
  outputs:
    valid: build

When valid.flow(request) is called, OfficeFloor routes execution to the build step and makes request available as a @Parameter there. When the invalid branch fires instead (response.send(...)), the build and audit steps are never invoked for that request.

The build step receives the validated request as a @Parameter — the value that was passed to ValidGreetingFlow.flow(). It builds and returns a GreetingResponse:

public class PostGreetingLogic {

	public GreetingResponse service(
			@Parameter GreetingRequest request,
			GreetingService greetingService) {
		return new GreetingResponse(greetingService.greet(request.getName()));
	}
}

Composing methods — sequential continuation with next

When a step returns a value, OfficeFloor passes that value as a @Parameter to whichever step is named by next. Here, the GreetingResponse returned by build becomes the @Parameter received by audit:

public class AuditGreetingLogic {

	public void service(
			@Parameter GreetingResponse greeting,
			AuditService auditService,
			ObjectResponse<GreetingResponse> response) {
		auditService.record(greeting.getMessage());
		response.send(greeting);
	}
}

The AuditService is a plain Spring bean:

@Service
public class AuditService {

	private final List<String> entries = new ArrayList<>();

	public void record(String message) {
		entries.add(message);
	}

	public List<String> getEntries() {
		return Collections.unmodifiableList(entries);
	}
}

The request and response POJOs are:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class GreetingRequest {

	private String name;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GreetingResponse {

	private String message;
}

Testing

The application is a standard Spring Boot application so tests use MockMvc. The postGreeting test injects AuditService from the application context to confirm that the audit step ran. The postGreetingWithBlankName test confirms the validate step short-circuited the chain:

@SpringBootTest
@AutoConfigureMockMvc
public class SpringRestHttpServerTest {

	@Autowired
	private MockMvc mvc;

	@Autowired
	private ObjectMapper mapper;

	@Autowired
	private AuditService auditService;

	@Test
	public void getGreeting() throws Exception {
		mvc.perform(MockMvcRequestBuilders.get("/greeting")
				.accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andExpect(content().json(mapper.writeValueAsString(new GreetingResponse("Hello, World!"))));
	}

	@Test
	public void getNamedGreeting() throws Exception {
		mvc.perform(MockMvcRequestBuilders.get("/greeting/OfficeFloor")
				.accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andExpect(content().json(mapper.writeValueAsString(new GreetingResponse("Hello, OfficeFloor!"))));
	}

	@Test
	public void postGreeting() throws Exception {
		mvc.perform(post("/greeting")
				.accept(MediaType.APPLICATION_JSON)
				.contentType(MediaType.APPLICATION_JSON)
				.content(mapper.writeValueAsString(new GreetingRequest("Daniel"))))
				.andExpect(status().isOk())
				.andExpect(content().json(mapper.writeValueAsString(new GreetingResponse("Hello, Daniel!"))));

		assertTrue(auditService.getEntries().contains("Hello, Daniel!"),
				"Greeting should be recorded by AuditService");
	}

	@Test
	public void postGreetingWithBlankName() throws Exception {
		mvc.perform(post("/greeting")
				.accept(MediaType.APPLICATION_JSON)
				.contentType(MediaType.APPLICATION_JSON)
				.content(mapper.writeValueAsString(new GreetingRequest(""))))
				.andExpect(status().isOk())
				.andExpect(content().json(mapper.writeValueAsString(new GreetingResponse("Hello, World!"))));
	}
}

Next

The Function Injection tutorial explores function injection more deeply: a three-step pipeline with conditional branching, typed data passing between steps, and independent Spring service injection at each step.