Spring purpose 2 web frameworks to build an application : the Servlet-based Spring MVC web framework and, in parallel, the Spring WebFlux reactive web framework.
In this course we will focus on Spring MVC because this framework is the most used.
Spring MVC helps you write web applications and takes care of a lot of boilerplate code, so you just have to focus on your application features.
With Spring Web (Spring MVC) you can write screens with a template solution which are used to generate HTML. But we don’t use this solution in this course. We will see how to write REST services. However if you are interested you can read official documentation.
With Spring Web you can expose REST services to another app (web api, JS app, android app…).This is the purpose of this lesson.You will learn how to develop endpoints on a backend application.These REST endpoints will be used later by a JS app or an Android app.
A DTO is an object that carries data between processes. Data need to be serializable to go across the HTTP connection
Serialization is the process of translating data structures or object into a format that can be transmitted
A DTO is often just a POJO (Plain Old Java Object), a bunch of fields and the getters and setters for them. Since Java 16 you can also use Record objects.
A record is a class that has specific characteristics:
this is a final class which cannot be enriched by inheritance from another record or from another class
each element of the description is encapsulated in a private and final field to guarantee immutability
a public getter is proposed for each element
a default equals()
and hashCode()
methods are provided, but you can override them.
public record Sensor(Long id, String name, Double value, SensorType sensorType) {
}
DTO will be used to transfer and to receive data in our REST controllers (entry point in our Java webapp).
You can write an util class to help the DTO creation from an entity or the entity creation from a DTO. This class is called a mapper.
public class SensorMapper {
public static Sensor of(SensorEntity sensor) {
return new Sensor(
sensor.getId(),
sensor.getName(),
sensor.getValue(),
sensor.getSensorType()
);
}
}
Create a record object for your respective entities : SensorEntity
, WindowEntity
, RoomEntity
, …
To prevent cycle in your record you should not map the bidirectional relation between a room and its windows. For example your record for the window can only mapped the room id.
public record Window(Long id, String name, Sensor windowStatus, Long roomId) {
}
Create mappers to create a record from an entity
Write a test for each mapper. As I am nice I will give you 2 useful classes.
One to create fake entities in your tests. FakeEntityBuilder
expose different static methods to create entities
public class FakeEntityBuilder {
public static RoomEntity createRoomEntity(Long id, String name, BuildingEntity building) {
// Sensor is recreated before each test
RoomEntity entity = new RoomEntity(
name,
createSensorEntity(1L, "Temp", TEMPERATURE, 23.2),
1);
entity.setBuilding(building);
entity.setTargetTemperature(26.4);
entity.setId(id);
entity.setWindows(Set.of(
createWindowEntity(id * 10 + 1L, "Window1" + name, entity),
createWindowEntity(id * 10 + 2L, "Window2" + name, entity)
));
entity.setHeaters(Set.of(
createHeaterEntity(id * 10 + 1L, "Heater1" + name, entity),
createHeaterEntity(id * 10 + 2L, "Heater2" + name, entity)
));
return entity;
}
public static WindowEntity createWindowEntity(Long id, String name, RoomEntity roomEntity) {
// Sensor is recreated before each test
WindowEntity windowEntity = new WindowEntity(
name,
createSensorEntity(id * 10 + 1L, "Status" + id, SensorType.STATUS, 0.0),
roomEntity
);
windowEntity.setId(id);
return windowEntity;
}
public static HeaterEntity createHeaterEntity(Long id, String name, RoomEntity roomEntity) {
// Sensor is recreated before each test
HeaterEntity heaterEntity = new HeaterEntity(
name,
createSensorEntity(id * 10 + 1L, "Status" + id, SensorType.STATUS, 0.0),
roomEntity
);
heaterEntity.setId(id);
return heaterEntity;
}
public static SensorEntity createSensorEntity(Long id, String name, SensorType type, Double value) {
// Sensor is recreated before each test
SensorEntity sensorEntity = new SensorEntity(type, name);
sensorEntity.setId(id);
sensorEntity.setValue(value);
return sensorEntity;
}
}
And the class to test the most complicated mapper
class RoomMapperTest {
@Test
void shouldMapRoom() {
// Arrange
RoomEntity roomEntity = FakeEntityBuilder.createBuildingEntity(1L, "Building")
.getRooms()
.stream()
.min(Comparator.comparing(RoomEntity::getName))
.orElseThrow(IllegalArgumentException::new);
// Act
Room room = RoomMapper.of(roomEntity);
// Assert
Room expectedRoom = new Room(
11L,
"Room1Building",
1,
23.2,
26.4,
List.of(
new Window(
111L,
"Window1Room1Building",
WindowStatus.CLOSED,
11L
),
new Window(
112L,
"Window2Room1Building",
WindowStatus.CLOSED,
11L
)
),
List.of(
new Heater(
111L,
"Heater1Room1Building",
HeaterStatus.OFF,
11L
),
new Heater(
112L,
"Heater2Room1Building",
HeaterStatus.OFF,
11L
)
)
);
Assertions.assertThat(room).usingRecursiveAssertion().isEqualTo(expectedRoom);
}
}
The Hypertext Transfer Protocol (HTTP) is an application protocol used for data communication on the World Wide Web.
HTTP defines methods (sometimes referred to as verbs) to indicate the desired action to be performed on the identified resource
A resource can be an image, a video, an HTML page, a JSON document.
To receive a response you have to send a request with a verb in a client an application as Curl, Wget…. or with a website
Each HTTP response has a status identified by a code. This code is sent by the server, by your app
1XX : Wait… request in progress
2XX : Here ! I send you a resource
3XX : Go away !
4XX : You made a mistake
5XX : I made a mistake
HTTP requests are handled by the methods of a REST service. In Spring’s approach a REST service is a controller. It is able to respond to HTTP requests
GET: read resource
POST: creates new record or executing a query
PUT: edit a resource (sometimes we use only a post request)
DELETE: delete a record
Controllers are the link between the web http clients (browsers, mobiles) and your application. They should be lightweight and call other components in your application to perform actual work (DAO for example).
These components are easily identified by the @RestController
annotation.
Example of addressable resources Node Express server listening on http://localhost:4000
Retrieve a sensor list : GET /api/sensors
Retrieve a particular sensor : GET /api/sensors/{sensor_id}
Create a sensor : POST /api/sensors
Update a sensor : PUT /api/sensors/{sensor_id}
Delete a sensor : DELETE /api/sensors/{sensor_id}
This SensorController handles GET requests for /api/sensors
by returning a list of Window.
A complete example to manage sensors
@CrossOrigin
@RestController // (1)
@RequestMapping("/api/sensors") // (2)
@Transactional // (3)
public class SensorController {
private final SensorDao sensorDao;
public SensorController(SensorDao sensorDao) {
this.sensorDao = sensorDao;
}
@GetMapping // (5)
public List<Sensor> findAll() {
return sensorDao.findAll()
.stream()
.map(SensorMapper::of)
.sorted(Comparator.comparing(Sensor::name))
.collect(Collectors.toList()); // (6)
}
@GetMapping(path = "/{id}")
public Sensor findById(@PathVariable Long id) {
return sensorDao.findById(id).map(SensorMapper::of).orElse(null); // (7)
}
@PostMapping // (8)
public ResponseEntity<Sensor> create(@RequestBody SensorCommand sensor) { // (9)
SensorEntity entity = new SensorEntity(sensor.sensorType(), sensor.name());
entity.setValue(sensor.value());
SensorEntity saved = sensorDao.save(entity);
return ResponseEntity.ok(SensorMapper.of(saved));
}
@PutMapping(path = "/{id}") // (10)
public ResponseEntity<Sensor> update(@PathVariable Long id, @RequestBody SensorCommand sensor) {
SensorEntity entity = sensorDao.findById(id).orElse(null);
if (entity == null) {
return ResponseEntity.badRequest().build();
}
entity.setValue(sensor.value());
entity.setName(sensor.name());
entity.setSensorType(sensor.sensorType());
// (11)
return ResponseEntity.ok(SensorMapper.of(entity));
}
@DeleteMapping(path = "/{id}")
public void delete(@PathVariable Long id) {
sensorDao.deleteById(id);
}
}
(1) RestController is a Spring stereotype to mark a class as a rest service
(2) @RequestMapping is used to define a global URL prefix used to manage a resource (in our example all requests that start with /api/sensors
will be handle by this controller)
(3) @Transactional is used to delegate a transaction opening to Spring. Spring will initiate a transaction for each entry point of this controller. This is important because with Hibernate you cannot execute a query outside of a transaction.
(4) DAO used by this controller is injected via constructor
(5) @GetMapping indicates that the following method will respond to a GET request. This method will return a sensor list. We transform our entities SensorEntity
in DTO Sensor
(6) (7) We use Java Stream API to manipulate our data
(8) @PostMapping indicates that the following method will respond to a POST request (for creation).
(9) To return HTTP errors the method return a ResponseEntity
. This object contains different builders to manipulate the HTTP response
(10) @PutMapping indicates that the following method will respond to a PUT request (for creation).
(11) For an update you don’t need to call the DAO save method. The findById
attach the entity to the persistence context and each update will be updated when the transaction will be commited.
Note, that we don’t use the same object for an update or a creation. We often use simples command object where all relationships are flatten. Here the command object is this one
public record SensorCommand(String name, Double value, SensorType sensorType) {
}
If this object has a relationship with another (for example a Room). The object for reading will return a complete Room object. The command object would only contain the data necessary to create/update ie a roomId. Sometimes we can use an object specific to creation, another to update.
To check if Spring MVC controllers are working as expected, use the @WebMvcTest
annotation. @WebMvcTest
auto-configures the Spring MVC infrastructure and the Mock MVC component.
Mock MVC offers a powerful way to quickly test MVC controllers without needing to start a full HTTP server.
Annotation @MockBean
provides mock implementations for required collaborators in place of the real implementations.
With Mock MVC you can perform requests for each HTTP methods
// static import of MockMvcRequestBuilders.*
// a post example
mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));
// you can specify query parameters in URI template style
mockMvc.perform(get("/hotels").param("thing", "somewhere"));
You can define expectations by appending one or more andExpect(..) calls after performing a request, as the following example shows. As soon as one expectation fails, no other expectations will be asserted.
// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*
mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());
You can use Json path expression to check your JSON result. And if you want to test your syntax this website will help you.
You can find several example in the SensorController
test
package com.emse.spring.automacorp.web;
import com.emse.spring.automacorp.dao.SensorDao;
import com.emse.spring.automacorp.model.SensorEntity;
import com.emse.spring.automacorp.model.SensorType;
import com.emse.spring.automacorp.record.Sensor;
import com.emse.spring.automacorp.record.SensorMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import java.util.List;
import java.util.Optional;
@WebMvcTest(SensorController.class)
class SensorControllerTest {
// Spring object to mock call to our app
@Autowired
private MockMvc mockMvc;
// The serializer used by Spring to send and receive data to/from the REST controller
@Autowired
private ObjectMapper objectMapper;
// We choose to mock the DAO used in the REST controller to limit the scope of our test
@MockBean
private SensorDao sensorDao;
@Test
void shouldFindAll() throws Exception {
Mockito.when(sensorDao.findAll()).thenReturn(List.of(
FakeEntityBuilder.createSensorEntity(1L, "Temperature room 1"),
FakeEntityBuilder.createSensorEntity(2L, "Temperature room 2")
));
mockMvc.perform(MockMvcRequestBuilders.get("/api/sensors").accept(MediaType.APPLICATION_JSON))
// check the HTTP response
.andExpect(MockMvcResultMatchers.status().isOk())
// the content can be tested with Json path
.andExpect(
MockMvcResultMatchers
.jsonPath("[*].name")
.value(Matchers.containsInAnyOrder("Temperature room 1", "Temperature room 2"))
);
}
@Test
void shouldReturnNullWhenFindByUnknownId() throws Exception {
Mockito.when(sensorDao.findById(999L)).thenReturn(Optional.empty());
mockMvc.perform(MockMvcRequestBuilders.get("/api/sensors/999").accept(MediaType.APPLICATION_JSON))
// check the HTTP response
.andExpect(MockMvcResultMatchers.status().isOk())
// the content can be tested with Json path
.andExpect(MockMvcResultMatchers.content().string(""));
}
@Test
void shouldFindById() throws Exception {
SensorEntity sensorEntity = FakeEntityBuilder.createSensorEntity(1L, "Temperature room 1");
Mockito.when(sensorDao.findById(999L)).thenReturn(Optional.of(sensorEntity));
mockMvc.perform(MockMvcRequestBuilders.get("/api/sensors/999").accept(MediaType.APPLICATION_JSON))
// check the HTTP response
.andExpect(MockMvcResultMatchers.status().isOk())
// the content can be tested with Json path
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Temperature room 1"));
}
@Test
void shouldNotUpdateUnknownEntity() throws Exception {
SensorEntity sensorEntity = FakeEntityBuilder.createSensorEntity(1L, "Temperature room 1");
SensorCommand expectedSensor = new SensorCommand(sensorEntity.getName(), sensorEntity.getValue(), sensorEntity.getSensorType());
String json = objectMapper.writeValueAsString(expectedSensor);
Mockito.when(sensorDao.findById(1L)).thenReturn(Optional.empty());
mockMvc.perform(
MockMvcRequestBuilders
.put("/api/sensors/1")
.content(json)
.contentType(MediaType.APPLICATION_JSON_VALUE)
)
// check the HTTP response
.andExpect(MockMvcResultMatchers.status().isBadRequest());
}
@Test
void shouldUpdate() throws Exception {
SensorEntity sensorEntity = FakeEntityBuilder.createSensorEntity(1L, "Temperature room 1");
SensorCommand expectedSensor = new SensorCommand(sensorEntity.getName(), sensorEntity.getValue(), sensorEntity.getSensorType());
String json = objectMapper.writeValueAsString(expectedSensor);
Mockito.when(sensorDao.findById(1L)).thenReturn(Optional.of(sensorEntity));
mockMvc.perform(
MockMvcRequestBuilders
.put("/api/sensors/1")
.content(json)
.contentType(MediaType.APPLICATION_JSON_VALUE)
)
// check the HTTP response
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Temperature room 1"))
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"));
}
@Test
void shouldCreate() throws Exception {
SensorEntity sensorEntity = FakeEntityBuilder.createSensorEntity(1L, "Temperature room 1");
SensorCommand expectedSensor = new SensorCommand(sensorEntity.getName(), sensorEntity.getValue(), sensorEntity.getSensorType());
String json = objectMapper.writeValueAsString(expectedSensor);
Mockito.when(sensorDao.existsById(1L)).thenReturn(false);
Mockito.when(sensorDao.save(Mockito.any(SensorEntity.class))).thenReturn(sensorEntity);
mockMvc.perform(
MockMvcRequestBuilders
.post("/api/sensors")
.content(json)
.contentType(MediaType.APPLICATION_JSON_VALUE)
)
// check the HTTP response
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Temperature room 1"))
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"));
}
@Test
void shouldDelete() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.delete("/api/sensors/999"))
.andExpect(MockMvcResultMatchers.status().isOk());
}
}
This is the time to create your first REST controller with Spring.
Create a new class HelloController
in package com.emse.spring.automacorp.api
.
@RestController
@RequestMapping("/api/hello")
@Transactional
public class HelloController {
@GetMapping("/{name}")
public Message welcome(@PathVariable String name) {
return new Message("Hello " + name);
}
public record Message(String message) {
}
}
If your REST service expose an handler for a GET HTTP request, this handler can be tested in a browser.
Launch your app and open the URL http://localhost:8080/api/hello/Guillaume in your browser
When you type an URL in the adress bar, your browser send a GET HTTP request. You should see a response as this one
{"message":"Hello Guillaume}
With a browser you are limited to GET requests. If you want to test PUT, POST or DELETE HTTP requests, you need another tool. We will usehttps://springdoc.org/[springdoc].
The advantage of swagger is that it is very well integrated into the Spring world. Update your build.gradle.kts
file and add these dependencies
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0")
You also need to add this property in your application.properties
file
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
And now you can relaunch your app and open swagger interface http://localhost:8080/swagger-ui/index.html
All your endpoints are available. You can click on one of them to test it
Read the previous examples and create
the REST service SensorController
a rest service which is able to
Retrieve a sensor list via a GET
Retrieve a particular sensor via a GET
Create a sensor via a POST
Update a sensor via a PUT
Delete a window via a DELETE
Use swagger to test your API
create a new sensor
list all the sensor
find the sensor with id -8
update a sensor
deletes this sensor
You can now write WindowController
and RoomController
. These routes must be implemented
You can now create BuildingDto, RoomDtoo, HeaterDto and write services which follow this service
/api/windows (GET) send windows list
/api/windows (POST) add a window
/api/windows/{id} (PUT) update a window
/api/windows/{id} (GET) read a window
/api/windows/{id} (DELETE) delete a window
/api/rooms (GET) send room list
/api/rooms (POST) add or update a room
/api/rooms/{room_id} (GET) read a room
/api/rooms/{room_id} (DELETE) delete a room and all its windows and its heaters
/api/rooms/{room_id}/openWindows switch the room windows to OPEN (status != 0)
/api/rooms/{room_id}/closeWindows switch the room windows to CLOSED (status = 0)
If you need to call remote REST services from your application, you can use the Spring Framework’s RestTemplate class.
A Java method for each HTTP method
DELETE : delete(…)
GET : getForObject(…)
HEAD : headForHeaders(…)
OPTIONS : optionsForAllow(…)
POST : postForObject(…)
PUT : put(…)
any method : exchange(…) or execute(…)
You need to create DTOs to serialize inputs and deserialize outputs
Use RestTemplate
to call the service with the good HTTP method
String result = restTemplate.getForObject(
"http://example.com/hotels/{hotel}/bookings/{booking}",
String.class,
"42",
"21");
will perform a GET on http://example.com/hotels/42/bookings/21.
The map variant expands the template based on variable name, and is therefore more useful when using many variables, or when a single variable is used multiple times. For example:
Map<String, String> vars = Collections.singletonMap("hotel", "42");
String result = restTemplate.getForObject(
"http://example.com/hotels/{hotel}/rooms/{hotel}",
String.class,
vars
);
will perform a GET on http://example.com/hotels/42/rooms/42.
Since RestTemplate instances often need to be customized before being used, Spring Boot does not provide any single auto-configured RestTemplate bean but a builder to help the creation.
@Service
public class SearchService {
private final RestTemplate restTemplate;
public AdressSearchService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.rootUri("https://example.com").build();
}
public ResponseDto findUsers() {
String uri = UriComponentsBuilder.fromUriString("/users/search")
.queryParam("name", "Guillaume")
.build()
.toUriString();
return restTemplate.getForObject(uri, ResponseDto.class);
}
}
will perform a GET on http://example.com/users/search?name=Guillaume
Now we can see how call a remote REST API in a Spring application.
We will test https://adresse.data.gouv.fr/api-doc/adresse
You can test a request in your terminal with the curl tool or in a browser as it’s a GET request.
curl "https://api-adresse.data.gouv.fr/search/?q=cours+fauriel+&limit=15"
You have a JSON as result
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Point", "coordinates": [4.402982, 45.426444] }, "properties": { "label": "Cours Fauriel 42100 Saint-Étienne", "score": 0.8910727272727272, "id": "42218_3390", "name": "Cours Fauriel", "postcode": "42100, "city": "Saint-Étienne", "context": "42, Loire, Auvergne-Rhône-Alpes", "type": "street" } } ], "query": "cours fauriel " }
Now you have to implement a service to call the API.
To help your job you can use these DTOs used to deserialize the returned JSON in Java objects.
ApiGouvResponseDto describes the API response. Inside you will have a list of…
…ApiGouvFeatureDto. Each feature will have different properties …
…ApiGouvAdressDto
public record ApiGouvResponse(
String version,
String query,
Integer limit,
List<ApiGouvFeature> features
) {
}
public record ApiGouvFeature(
String type,
ApiGouvAdress properties
) {
public record ApiGouvAdress(
String id,
String label,
String housenumber,
Double score,
String postcode,
String citycode,
String city,
String context,
String type,
Double x,
Double y
) {
}
Now you are able to write
a service called AdressSearchService
with a constructor in which you will create the restTemplate
add a method to return the List<ApiGouvAdressDto>
this method can have a list of String to define the parameters to send to the API
You can build the URI with this code
String params = String.join("+", keys);
UriComponentsBuilder.fromUriString("/search").queryParam("q", params).queryParam("limit", 15).build().toUriString()`
You can expose a new REST endpoint in a controller to use Swagger to test this API
You can use the @RestClientTest annotation to test REST clients. By default, it auto-configures Jackson, configures a RestTemplateBuilder, and adds support for MockRestServiceServer.
This test should work
package com.emse.spring.automacorp.adress;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.match.MockRestRequestMatchers;
import org.springframework.test.web.client.response.MockRestResponseCreators;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.List;
@RestClientTest(AdressSearchService.class) // (1)
class AdressSearchServiceTest {
@Autowired
private AdressSearchService service;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private MockRestServiceServer server; // (2)
@Test
void shouldFindAdresses() throws JsonProcessingException {
// Arrange
ApiGouvResponse expectedResponse = simulateApiResponse();
String expectedUrl = UriComponentsBuilder
.fromUriString("/search")
.queryParam("q", "cours+fauriel")
.queryParam("limit", 15)
.build()
.toUriString();
this.server
.expect(MockRestRequestMatchers.requestTo(expectedUrl))
.andRespond(
MockRestResponseCreators.withSuccess(
objectMapper.writeValueAsString(expectedResponse),
MediaType.APPLICATION_JSON
)
);
// Act
List<ApiGouvAdress> adresses = this.service.searchAdress(List.of("cours", "fauriel"));
// Assert
Assertions
.assertThat(adresses)
.hasSize(1)
.extracting(ApiGouvAdress::city)
.contains("Saint Etienne");
}
private ApiGouvResponse simulateApiResponse() {
ApiGouvAdress adress = new ApiGouvAdress(
"ad1",
"Cours Fauriel 42100 Saint-Étienne",
"2",
0.98,
"42100",
"42218",
"Saint Etienne",
"context",
"type",
0.0,
0.0
);
ApiGouvFeature feature = new ApiGouvFeature("type", adress);
return new ApiGouvResponse("v1", "cours+fauriel", 15, List.of(feature));
}
}