Quarkus for Spring Boot Developers

Spread the love

Quarkus Overview

Quarkus is a full-stack, Kubernetes-native Java framework made for Java virtual machines (JVMs) and native compilation, optimising Java specifically for containers and enabling it to become an effective platform for serverlesscloud, and Kubernetes environments. Quarkus is designed to work with popular Java standards, frameworks, and libraries like Eclipse MicroProfile and Spring, as well as Apache Kafka, RESTEasy (JAX-RS), Hibernate ORM (JPA), Spring, Infinispan, Camel, and many more.

Container-first

Whether an application is hosted on a public cloud or in an internally hosted Kubernetes cluster, characteristics like fast startup and low memory consumption are important to keeping overall host costs down.

Quarkus was built around a container-first philosophy, meaning it’s optimised for lower memory usage and faster startup times in the following ways:

  • First-class support for Graal/SubstrateVM
  • Build-time metadata processing
  • Reduction in reflection usage
  • Native image preboot

So Quarkus builds applications to consume 1/10th the memory when compared to traditional Java, and has a faster startup time (as much as 300 times faster), both of which greatly reduce the cost of cloud resources.

Quarkus is an effective solution for running Java in this new world of serverless architecture, microservices, containers, Kubernetes, function-as-a-service (FaaS), and cloud because it was created with all these things in mind.

Introduction

Whether an application is hosted on a public cloud or in an internally hosted Kubernetes cluster, characteristics like fast startup and low memory consumption are important to keeping overall host costs down.

Quarkus was built around a container-first philosophy, meaning it’s optimised for lower memory usage and faster startup times in the following ways:

  • First-class support for Graal/SubstrateVM
  • Build-time metadata processing
  • Reduction in reflection usage
  • Native image preboot

So Quarkus builds applications to consume 1/10th the memory when compared to traditional Java, and has a faster startup time (as much as 300 times faster), both of which greatly reduce the cost of cloud resources.

Quarkus is an effective solution for running Java in this new world of serverless architecture, microservices, containers, Kubernetes, function-as-a-service (FaaS), and cloud because it was created with all these things in mind.

Requirements

  • JDK 11+
  • GraalVM
  • Docker

Maven dependencies

<dependencies>
 <dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-reactive</artifactId>
 </dependency>

 <dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
 </dependency>

 <dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-junit5</artifactId>
  <scope>test</scope>
 </dependency>

 <dependency>
  <groupId>io.rest-assured</groupId>
  <artifactId>rest-assured</artifactId>
  <scope>test</scope>
 </dependency>

 <dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-junit5-mockito</artifactId>
  <scope>test</scope>
 </dependency>

 <dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-jdbc-postgresql</artifactId>
 </dependency>

 <dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-spring-data-jpa</artifactId>
 </dependency>

 <dependency>
  <groupId>org.modelmapper</groupId>
  <artifactId>modelmapper</artifactId>
  <version>3.0.0</version>
 </dependency>
</dependencies>

Controller Layer

@Path("/api/users")
public class UserController {

 @Inject
 UserService userService;

 @GET
 @Path("/{id}")
 public User getUser(@PathParam("id") Long id) {
  return userService.getUserById(id);
 }

 @GET
 public List<User> getAllUsers() {
  return userService.getUsers();
 }

 @POST
 public User createUser(final User user) {
  User createdUser = userService.createUser(user);
  return createdUser;
 }

 @PUT
 @Path("/{id}")
 public User updateUser(@RestPath Long id, User user) {
  return userService.updateUser(id, user);
 }

 @PATCH
 @Path("/{id}")
 public User patchUpdateUser(@RestPath Long id, User user) {
  return userService.patchUpdateUser(id, user);
 }

 @DELETE
 @Path("/{id}")
 public void deleteUser(@RestPath Long id) {
  userService.deleteUser(id);
 }
}

Service Layer

@ApplicationScoped
public class UserServiceImpl implements UserService {

 @Inject
 private UsersRepository usersRepository;

 @Override
 public User getUserById(Long id) {
  Optional<User> maybeUser = usersRepository.findById(id);
  return maybeUser.orElseThrow(() -> new RuntimeException("User not found"));
 }

 @Override
 public User createUser(User user) {
  usersRepository.findByEmail(user.getEmail())
  .ifPresent(p -> {
   throw new RuntimeException("User with email " + p.getEmail() + " already exists ");
  });
  return usersRepository.save(user);
 }

 @Override
 public User updateUser(Long id, User user) {
  User userFound = usersRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
  userFound.setFirstName(user.getFirstName());
  userFound.setLastName(user.getLastName());
  userFound.setEmail(user.getEmail());
  userFound.setFirstLineOfAddress(user.getFirstLineOfAddress());
  userFound.setSecondLineOfAddress(user.getSecondLineOfAddress());
  userFound.setTown(user.getTown());
  userFound.setPostCode(user.getPostCode());
  return usersRepository.save(userFound);
 }

 @Override
 public User patchUpdateUser(Long id, User user) {
  User userFound = usersRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
  ModelMapper modelMapper = new ModelMapper();
  //copy only non null values
  modelMapper.getConfiguration().setSkipNullEnabled(true).setMatchingStrategy(MatchingStrategies.STRICT);
  modelMapper.map(user, userFound);
  return usersRepository.save(userFound);
 }

 @Override
 public void deleteUser(Long id) {
  usersRepository.deleteById(id);
 }

 @Override
 public List<User> getUsers() {
  Iterable<User> userIterable = usersRepository.findAll();
  return StreamSupport.stream(userIterable.spliterator(), false)
  .collect(Collectors.toList());
 }
}

Repository Layer

Entity

Entity/Persistence annotation name remain same as used in spring, but belongs to jakarta package

@Entity(name = "users")
public class User {
 @Id
 @GeneratedValue
 private Long id;

 @Column(nullable = false, length = 50)
 private String firstName;

 @Column(nullable = false, length = 50)
 private String lastName;

 @Column(nullable = false, length = 120)
 private String email;

 @Column(nullable = false, length = 50)
 private String firstLineOfAddress;

 @Column(length = 50)
 private String secondLineOfAddress;

 @Column(nullable = false, length = 50)
 private String town;

 @Column(nullable = false, length = 10)
 private String postCode;

 public Long getId() {
  return id;
 }

 public void setId(Long id) {
  this.id = id;
 }

 public String getFirstName() {
  return firstName;
 }

 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }

 public String getLastName() {
  return lastName;
 }

 public void setLastName(String lastName) {
  this.lastName = lastName;
 }

 public String getEmail() {
  return email;
 }

 public void setEmail(String email) {
  this.email = email;
 }

 public String getPostCode() {
  return postCode;
 }

 public void setPostCode(String postCode) {
  this.postCode = postCode;
 }

 public String getFirstLineOfAddress() {
  return firstLineOfAddress;
 }

 public void setFirstLineOfAddress(String firstLineOfAddress) {
  this.firstLineOfAddress = firstLineOfAddress;
 }

 public String getTown() {
  return town;
 }

 public void setTown(String town) {
  this.town = town;
 }

 public String getSecondLineOfAddress() {
  return secondLineOfAddress;
 }

 public void setSecondLineOfAddress(String secondLineOfAddress) {
  this.secondLineOfAddress = secondLineOfAddress;
 }
}

Repository Interface

Repository interface used is same as in spring

public interface UsersRepository extends CrudRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

Controller Layer Tests

@QuarkusTest
public class UserControllerTest {

    @InjectMock
    private UserService userService;

    private User user;
    private User user2;

    @Inject
    private ObjectMapper objectMapper;

    @BeforeEach
    void setup() {
        user = new User();
        user.setFirstName("Steve");
        user.setLastName("Rob");
        user.setEmail("steverob@gmail.com");
        user.setId(1L);
        user.setFirstLineOfAddress("12 Avenue");
        user.setSecondLineOfAddress("Commercial street");
        user.setTown("London");
        user.setPostCode("HA6 0EW");

        user2 = new User();
        user2.setFirstName("Stev");
        user2.setLastName("Ro");
        user2.setEmail("stevob@gmail.com");
        user2.setId(2L);
        user2.setFirstLineOfAddress("13 Avenue");
        user2.setSecondLineOfAddress("Commercial street");
        user2.setTown("London");
        user2.setPostCode("HA7 0EW");

    }

    @Test
    void testGetUser() throws Exception {
        String expected = objectMapper.writeValueAsString(user);
        when(userService.getUserById(anyLong())).thenReturn(user);
        var response = given().contentType(ContentType.JSON)
                .when()
                .get("/api/users/" + user.getId())
                .then()
                .statusCode(Response.Status.OK.getStatusCode())
                .extract();

        assertEquals(expected, response.asString());
    }

    @Test
    public void testGetAllUsers() throws Exception {
        List<User> users = Arrays.asList(user, user2);
        String expected = objectMapper.writeValueAsString(users);
        when(userService.getUsers()).thenReturn(users);
        var response = given().contentType(ContentType.JSON)
                .when()
                .get("/api/users")
                .then()
                .statusCode(Response.Status.OK.getStatusCode())
                .extract();
        assertEquals(expected, response.asString());
    }

    @Test
    void testCreateUser() throws Exception {
        String expected = objectMapper.writeValueAsString(user);
        String request = expected.replace("\"id\":1,", "");
        when(userService.createUser(any(User.class))).thenReturn(user);
        var response = given().contentType(ContentType.JSON)
                .accept(ContentType.JSON)
                .body(request)
                .when()
                .post("/api/users")
                .then()
                .statusCode(Response.Status.OK.getStatusCode())
                .extract();
        assertEquals(expected, response.asString());
    }

    @Test
    void testUpdateUser() throws Exception {
        User updateUser = new User();
        updateUser.setFirstName("Steveupdated");
        updateUser.setLastName("Robupdated");
        updateUser.setEmail("steverobupdated@gmail.com");
        updateUser.setFirstLineOfAddress("12 Avenueupdated");
        updateUser.setSecondLineOfAddress("Commercial streetupdated");
        updateUser.setTown("London");
        updateUser.setPostCode("HA6 0EW");
        User updateUserWithId = new User();
        new ModelMapper().map(updateUser, updateUserWithId);
        updateUserWithId.setId(1L);
        //Exclude null values
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        String putBody = objectMapper.writeValueAsString(updateUser);
        String expectedResponse = objectMapper.writeValueAsString(updateUserWithId);
        when(userService.updateUser(anyLong(), any(User.class))).thenReturn(updateUserWithId);
        var response = given().contentType(ContentType.JSON)
                .accept(ContentType.JSON)
                .body(putBody)
                .when()
                .put("/api/users/{userId}", updateUserWithId.getId())
                .then()
                .statusCode(Response.Status.OK.getStatusCode())
                .extract();
        assertEquals(expectedResponse, response.asString());
    }

    @Test
    void testPatchUpdateUser() throws Exception {
        User updateUser = new User();
        updateUser.setFirstName("Steveupdated");
        updateUser.setLastName("Robupdated");
        user.setFirstName(updateUser.getFirstName());
        user.setLastName(updateUser.getLastName());
        //Exclude null values
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        String patchBody = objectMapper.writeValueAsString(updateUser);
        String expectedResponse = objectMapper.writeValueAsString(user);
        when(userService.patchUpdateUser(anyLong(), any(User.class))).thenReturn(user);
        var response = given().contentType(ContentType.JSON)
                .accept(ContentType.JSON)
                .body(patchBody)
                .when()
                .patch("/api/users/{userId}", user.getId())
                .then()
                .statusCode(Response.Status.OK.getStatusCode())
                .extract();
        assertEquals(expectedResponse, response.asString());
    }

    @Test
    void testDeleteUser() throws Exception {
        doNothing().when(userService).deleteUser(anyLong());
        var response = given().contentType(ContentType.JSON)
                .accept(ContentType.JSON)
                .when()
                .delete("/api/users/{userId}", user.getId())
                .then()
                .statusCode(Response.Status.NO_CONTENT.getStatusCode())
                .extract();
        assertEquals("", response.asString());
    }
}

Service Layer Tests

@QuarkusTest
class UserServiceImplTest {

    @InjectMocks
    private UserServiceImpl userService;

    @Mock
    private UsersRepository usersRepository;

    private User user;
    private User user2;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.initMocks(this);
        user = new User();
        user.setId(1L);
        user.setFirstName("Steve");
        user.setLastName("Rob");
        user.setEmail("steverob@gmail.com");
        user.setFirstLineOfAddress("10 Oliver");
        user.setSecondLineOfAddress("Commercial street");
        user.setTown("London");
        user.setPostCode("HA4 7EW");
        user2 = new User();
        user2.setId(2L);
        user2.setFirstName("Tony");
        user2.setLastName("Taylor");
        user2.setEmail("tonytaylor@gmail.com");
        user2.setFirstLineOfAddress("12 Avenue");
        user2.setSecondLineOfAddress("Avenue street");
        user2.setTown("London");
        user2.setPostCode("HA6 0EW");
    }

    @Test
    void testGetUserById() {
        when(usersRepository.findById(anyLong())).thenReturn(Optional.of(user));
        User userFound = userService.getUserById(user.getId());
        assertNotNull(userFound);
        assertEquals(user.getId(), userFound.getId());
        assertEquals(user.getEmail(), userFound.getEmail());
        assertEquals(user.getFirstName(), userFound.getFirstName());
        assertEquals(user.getLastName(), userFound.getLastName());
        assertEquals(user.getPostCode(), userFound.getPostCode());
        assertEquals(user.getFirstLineOfAddress(), userFound.getFirstLineOfAddress());
        assertEquals(user.getSecondLineOfAddress(), userFound.getSecondLineOfAddress());
        assertEquals(user.getTown(), userFound.getTown());
    }

    @Test
    void shouldThrowExceptionWhenGetUserHasWrongId() {
        when(usersRepository.findById(anyLong())).thenReturn(Optional.empty());
        assertThrows(RuntimeException.class, () -> userService.getUserById(10000L));
    }

    @Test
    void testGetUsers() {
        List<User> users = Arrays.asList(user, user2);
        when(usersRepository.findAll()).thenReturn(users);
        List<User> usersReceived = userService.getUsers();
        assertEquals(users, usersReceived);
        assertEquals(users.size(), usersReceived.size());
        assertEquals(users.get(0).getId(), usersReceived.get(0).getId());
        assertEquals(users.get(0).getFirstName(), usersReceived.get(0).getFirstName());
        assertEquals(users.get(0).getLastName(), usersReceived.get(0).getLastName());
        assertEquals(users.get(0).getEmail(), usersReceived.get(0).getEmail());
        assertEquals(users.get(0).getFirstLineOfAddress(), usersReceived.get(0).getFirstLineOfAddress());
        assertEquals(users.get(0).getSecondLineOfAddress(), usersReceived.get(0).getSecondLineOfAddress());
        assertEquals(users.get(0).getSecondLineOfAddress(), usersReceived.get(0).getSecondLineOfAddress());
    }

    @Test
    void testCreateUser() {
        when(usersRepository.findByEmail(anyString())).thenReturn(Optional.empty());
        when(usersRepository.save(any(User.class))).thenReturn(user);
        User createNewUser = new User();
        createNewUser.setFirstName(user.getFirstName());
        createNewUser.setLastName(user.getLastName());
        createNewUser.setEmail(user.getEmail());
        createNewUser.setPostCode(user.getPostCode());
        createNewUser.setFirstLineOfAddress(user.getFirstLineOfAddress());
        createNewUser.setSecondLineOfAddress(user.getSecondLineOfAddress());
        createNewUser.setTown(user.getTown());
        User createdUser = userService.createUser(createNewUser);
        assertNotNull(createdUser);
        assertEquals(user.getId(), createdUser.getId());
        assertEquals(user.getEmail(), createdUser.getEmail());
        assertEquals(user.getFirstName(), createdUser.getFirstName());
        assertEquals(user.getLastName(), createdUser.getLastName());
        assertEquals(user.getPostCode(), createdUser.getPostCode());
        assertEquals(user.getFirstLineOfAddress(), createdUser.getFirstLineOfAddress());
        assertEquals(user.getSecondLineOfAddress(), createdUser.getSecondLineOfAddress());
        assertEquals(user.getTown(), createdUser.getTown());
    }

    @Test
    void testUpdateUser() {
        User updateUser = new User();
        updateUser.setFirstName("Steve");
        updateUser.setLastName("Rob");
        updateUser.setEmail("steverob@gmail.com");
        updateUser.setPostCode("HA5 2EW");
        updateUser.setFirstLineOfAddress("10 Oliver");
        updateUser.setSecondLineOfAddress("Commercial street");
        updateUser.setTown("London");
        User updateUserWithId = new User();
        new ModelMapper().map(updateUser, updateUserWithId);
        updateUserWithId.setId(1L);
        when(usersRepository.findById(anyLong())).thenReturn(Optional.of(updateUserWithId));
        when(usersRepository.save(any(User.class))).thenReturn(updateUserWithId);
        User savedUser = userService.updateUser(updateUserWithId.getId(), updateUser);
        assertNotNull(savedUser);
        assertEquals(updateUserWithId.getId(), savedUser.getId());
        assertEquals(updateUserWithId.getEmail(), savedUser.getEmail());
        assertEquals(updateUserWithId.getFirstName(), savedUser.getFirstName());
        assertEquals(updateUserWithId.getLastName(), savedUser.getLastName());
        assertEquals(updateUserWithId.getPostCode(), savedUser.getPostCode());
        assertEquals(updateUserWithId.getFirstLineOfAddress(), savedUser.getFirstLineOfAddress());
        assertEquals(updateUserWithId.getSecondLineOfAddress(), savedUser.getSecondLineOfAddress());
        assertEquals(updateUserWithId.getTown(), savedUser.getTown());
    }

    @Test
    void testPatchUpdateUser() {
        User updateUser = new User();
        updateUser.setFirstName("Steveupdated");
        updateUser.setLastName("Robupdated");
        user.setFirstName(updateUser.getFirstName());
        user.setLastName(updateUser.getLastName());
        when(usersRepository.findById(anyLong())).thenReturn(Optional.of(user));
        when(usersRepository.save(any(User.class))).thenReturn(user);
        User savedUser = userService.patchUpdateUser(user.getId(), updateUser);
        assertNotNull(savedUser);
        assertEquals(user.getId(), savedUser.getId());
        assertEquals(user.getEmail(), savedUser.getEmail());
        assertEquals(user.getFirstName(), savedUser.getFirstName());
        assertEquals(user.getLastName(), savedUser.getLastName());
        assertEquals(user.getPostCode(), savedUser.getPostCode());
        assertEquals(user.getFirstLineOfAddress(), savedUser.getFirstLineOfAddress());
        assertEquals(user.getSecondLineOfAddress(), savedUser.getSecondLineOfAddress());
        assertEquals(user.getTown(), savedUser.getTown());
    }

    @Test
    void testDeleteUser() {
        when(usersRepository.findById(anyLong())).thenReturn(Optional.of(user));
        doNothing().when(usersRepository).delete(any(User.class));
        userService.deleteUser(user.getId());
        verify(usersRepository, times(1)).deleteById(user.getId());
    }
}

Repository Layer Tests

@QuarkusTest
public class UserRepositoryTest {
    @Inject
    private UsersRepository usersRepository;

    @Inject
    UserService userService;

    @Test
    void testFndByEmail() {
        //Given
        User user = new User();
        user.setFirstName("Steve");
        user.setLastName("Rob");
        user.setEmail(UUID.randomUUID().toString() + "@gmail.com");
        user.setFirstLineOfAddress("10 Oliver");
        user.setSecondLineOfAddress("Commercial street");
        user.setTown("London");
        user.setPostCode("HA4 7EW");
        userService.createUser(user);
        //When
        User found = usersRepository.findByEmail(user.getEmail()).get();
        //Then
        assertEquals(user.getEmail(), found.getEmail());
    }
}

Integration Tests

@QuarkusTest
public class UserRestIT {

    private User user;

    @Inject
    private ObjectMapper objectMapper;

    @BeforeEach
    void setup() {
        user = new User();
        user.setFirstName("Steve");
        user.setLastName("Rob");
        user.setEmail(UUID.randomUUID().toString() + "@gmail.com");
        user.setId(1L);
        user.setFirstLineOfAddress("12 Avenue");
        user.setSecondLineOfAddress("Commercial street");
        user.setTown("London");
        user.setPostCode("HA6 0EW");

    }

    @Test
    public void testCreateUser() throws Exception {
        String expected = objectMapper.writeValueAsString(user);
        String request = expected.replace("\"id\":1,", "");
        given().contentType(ContentType.JSON)
                .accept(ContentType.JSON)
                .body(request)
                .when()
                .post("/api/users")
                .then()
                .statusCode(Response.Status.OK.getStatusCode())
                .body("firstName", is(user.getFirstName()))
                .body("lastName", is(user.getLastName()))
                .body("email", is(user.getEmail()))
                .body("firstLineOfAddress", is(user.getFirstLineOfAddress()))
                .body("secondLineOfAddress", is(user.getSecondLineOfAddress()))
                .body("town", is(user.getTown()))
                .body("postCode", is(user.getPostCode()));
    }
}

Code Location

https://github.com/ranjesh1/quarkus-rest-services/


Spread the love

9 Replies to “Quarkus for Spring Boot Developers”

Leave a Reply

Your email address will not be published. Required fields are marked *