- vừa được xem lúc

Bài học số 2 - Xây dựng REST API với Quarkus (JAVA framework)

0 0 18

Người đăng: Hoang Minh

Theo Viblo Asia

Architecture layers: Resource/Controller, Service, Repository

Mục tiêu của bài học này là xây dựng một REST API cho thiết bị IoT. REST API sẽ hỗ trợ chức năng CRUD (tạo, đọc, cập nhật và xóa) cơ bản, lưu dữ liệu vào cơ sở dữ liệu quan hệ SQL. Mình thích bắt đầu với Resource Service Repository layering pattern truyền thống, được hiển thị trong hình dưới đây

Trong pattern này, layer Repository trả về một đối tượng Entity đối tượng này được liên kết chặt chẽ với cấu trúc cơ sở dữ liệu bên dưới. Layer Service chấp nhận và trả về các đối tượng Domain và layer Resource/Controller quản lý các REST, ngoài ra có thể xử lý các chuyển đổi dữ liệu bổ sung từ đối tượng Domain sang một đối tượng View cụ thể.

Lombok and MapStruct

Lombok là một thư viện Java giúp rút gọn code trong trong ứng dụng tới mức tối thiểu.

MapStruct là một code generator giúp đơn giản hóa rất nhiều việc triển khai mappings giữa các loại JavaBean.

Lombok và MapStruct cần được định cấu hình trong compiler plugin để đảm bảo rằng cả hai đều được thực thi. Dưới đây là đoạn trích của các thay đổi pom.xml mà bạn cần thực hiện.

<properties>
... <lombok.version>1.18.22</lombok.version> <mapstruct.version>1.4.2.Final</mapstruct.version>
...
</properties> <dependencies>
... <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${mapstruct.version}</version> </dependency>
...
<dependencies> <build> <plugins> ... <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>${compiler-plugin.version}</version> <configuration> <compilerArgs> <arg>-parameters</arg> </compilerArgs> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> ... </plugins>
</build>

Exceptions

Tiếp theo, bạn sẽ triển khai thêm một Exceptions nhanh để sử dụng trong project. Bằng cách sử dụng một ServiceException đơn giản, bạn có thể đơn giản hóa mô hình Exceptions và thêm một số định dạng thông báo để dễ debug.

package org.acme.exception; public class ServiceException extends RuntimeException { public ServiceException(String message) { super(message); } public ServiceException(String format, Object... objects) { super(String.format(format, objects)); } }

Responses

Tiếp theo, bạn sẽ triển khai nhanh một ResponseObject đơn giản.

package org.acme.response; import lombok.Data;
import lombok.NoArgsConstructor; @Data
@NoArgsConstructor public class ResponseObject { private int returnCode; private String message; private Object data; public void getSuccess(Object data) { this.setReturnCode(200); this.setMessage("Fetched data successfully"); this.setData(data); } public void createSuccess(Object data) { this.setReturnCode(201); this.setMessage("Create data successfully"); this.setData(data); } public void updateSuccess(Object data) { this.setReturnCode(204); this.setMessage("Update data successfully"); this.setData(data); } public void deleteSuccess() { this.setReturnCode(204); this.setMessage("Delete data successfully"); } public void getFailed() { this.setReturnCode(404); this.setMessage("Get data failed"); } public void createFailed() { this.setReturnCode(409); this.setMessage("Create data failed"); this.setData(data); } public void updateFailed() { this.setReturnCode(409); this.setMessage("Update data failed"); } public void deleteFailed() { this.setReturnCode(400); this.setMessage("Delete data failed"); } }

Repository layer

Các tương tác cơ sở dữ liệu sẽ được quản lý bởi tiện ích mở rộng Quarkus Panache. Tiện ích mở rộng Panache sử dụng dưới vỏ bọc Hibernate, nhưng cung cấp rất nhiều chức năng để giúp các nhà phát triển làm việc hiệu quả hơn. Trong bài viết này mình sẽ sử dụng cơ sở dữ liệu PostgreSQL và sẽ quản lý schema bằng Flyway, cho phép quản lý phiên bản và mã nguồn của các schema cơ sở dữ liệu. Panache cũng cần được định cấu hình trong compiler plugin Thêm các phần mở rộng này vào tệp pom.xml của dự án.

<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-flyway</artifactId>
</dependency>

Flyway

Flyway là một công cụ migration cơ sở dữ liệu phổ biến thường được sử dụng trong môi trường JVM. Flyway thường yêu cầu tất cả các tệp migration sẽ nằm trong một folder và đường dẫn lớp có tên là db/migration. Các file SQL sẽ có định dạng như sau:

src/main/resources/db/migration/V1__device_table_create.sql
CREATE TABLE device
( id SERIAL PRIMARY KEY, name TEXT NOT NULL, ipAddress TEXT NOT NULL, macAddress TEXT NOT NULL, status TEXT NOT NULL, type TEXT NOT NULL, version TEXT NOT NULL, CONSTRAINT ipAddress UNIQUE (ipAddress), CONSTRAINT macAddress UNIQUE (macAddress) );
ALTER SEQUENCE device_id_seq RESTART 1000000;

JPA với Panache

Việc sử dụng Java Persistence API (JPA) bắt đầu bằng việc xây dựng một đối tượng Entity. Khi sử dụng Panache, bạn có thể lựa chọn giữa hai pattern: Active Record and Repository. Mình thích pattern Repository hơn vì mình thích single-responsibility principle.

package org.acme.entity; import lombok.Data; import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern; @Entity(name = "Device")
@Table(name = "device")
@Data
public class DeviceEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Integer id; @Column(name = "name") @NotEmpty private String name; @Column(name = "ipAddress") @NotEmpty @Pattern(regexp="^(([0-9]|[1-9][0-9]|1[0-9]" + "{2}|2[0-4][0-9]|25[0-5])\\.)" + "{3}([0-9]|[1-9][0-9]|1[0-9]" + "{2}|2[0-4][0-9]|25[0-5])$", message="{invalid.ipAddress}") private String ipAddress; @Column(name = "macAddress") @NotEmpty @Pattern(regexp="^([0-9A-Fa-f]{2}[:-])" + "{5}([0-9A-Fa-f]{2})|" + "([0-9a-fA-F]{4}\\." + "[0-9a-fA-F]{4}\\." + "[0-9a-fA-F]{4})$", message="{invalid.macAddress}") private String macAddress; @Column(name = "status") @NotEmpty private String status; @Column(name = "type") @NotEmpty private String type; @Column(name = "version") @NotEmpty private String version;
}

@Data là cách dùng nhanh khi bạn muốn thêm tất cả các annotation:

  • @Getter / @Setter
  • @ToString
  • @EqualsAndHashCode
  • @RequiredArgsConstructor

của Lombok vào 1 class.

@Entity: cho phép bạn tạo ra một thực thể Entity để ánh xạ với Table trong cơ sở dữ liệu.

@Pattern: dùng để validate giá trị của một thuộc tính, giá trị chỉ hợp lệ khi nó khớp với một biểu thức chính quy nhất định. Ở đây mình sẽ validate format ipAddress và macAddress có đúng hay không mới cho lưu chúng vào cột tương ứng trong table của cơ sở dữ liệu.

Bước tiếp theo là tạo layer Repository:

package org.acme.repository;
import org.acme.entity.DeviceEntity; import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import javax.enterprise.context.ApplicationScoped; @ApplicationScoped
public class DeviceRepository implements PanacheRepositoryBase<DeviceEntity, Integer> { }

Service layer: Domain object, MapStruct mapper, và Service

Trong ví dụ này, đối tượng Domain khá đơn giản. Về cơ bản, nó là một bản sao của đối tượng thực thể Entity. Tuy nhiên, khi dữ liệu UI muốn lấy khác với dữ liệu lưu trong database, thì lớp bổ sung này sẽ có ích. Điều này giữ cho kiến trúc ứng dụng clean, linh hoạt và dễ dàng mở rộng layer này mà không ảnh hướng đến layer khác.

package org.acme.service.device; import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern; @Data
public class Device { private Integer id; @NotEmpty private String name; @NotEmpty @Pattern(regexp="^(([0-9]|[1-9][0-9]|1[0-9]" + "{2}|2[0-4][0-9]|25[0-5])\\.)" + "{3}([0-9]|[1-9][0-9]|1[0-9]" + "{2}|2[0-4][0-9]|25[0-5])$", message="{invalid.ipAddress}") private String ipAddress; @NotEmpty @Pattern(regexp="^([0-9A-Fa-f]{2}[:-])" + "{5}([0-9A-Fa-f]{2})|" + "([0-9a-fA-F]{4}\\." + "[0-9a-fA-F]{4}\\." + "[0-9a-fA-F]{4})$", message="{invalid.macAddress}") private String macAddress; @NotEmpty private String status; @NotEmpty private String type; @NotEmpty private String version; }

Trong layer Service, bạn sẽ cần map giữa entity objects và Domain objects . Đây là nơi MapSturation xuất hiện: để thực hiện ánh xạ cho bạn.

package org.acme.service.device;
import org.acme.entity.DeviceEntity; import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import java.util.List; @Mapper(componentModel = "cdi")
public interface DeviceMapper { List<Device> toDomainList(List<DeviceEntity> entities); Device toDomain(DeviceEntity entity); @InheritInverseConfiguration(name = "toDomain") DeviceEntity toEntity(Device domain); void updateEntityFromDomain(Device domain, @MappingTarget DeviceEntity entity); void updateDomainFromEntity(DeviceEntity entity, @MappingTarget Device domain); }

Bây giờ bạn đã có layer Domain và layer Mapper dựa trên MapStruct cần thiết. Bạn có thể thêm layer Service cho chức năng CRUD (Tạo, Đọc, Cập nhật, Xóa) cơ bản.

package org.acme.service.device; import org.acme.entity.DeviceEntity;
import org.acme.repository.DeviceRepository;
import org.acme.exception.ServiceException;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Objects;
import java.util.Optional; @ApplicationScoped
@AllArgsConstructor
@Slf4j
public class DeviceService { @Inject DeviceRepository deviceRepository; @Inject DeviceMapper deviceMapper; public List<Device> findAll() { log.info("Find all devices: {}"); List<Device> deviceObject = this.deviceMapper.toDomainList(deviceRepository.findAll().list()); if (Objects.isNull(deviceObject)) { throw new ServiceException("Table devices is empty"); } else { return deviceObject; } } public Optional<Device> findById(@NonNull Integer deviceId) { log.info("Find device by id: {}", deviceId); Optional<Device> deviceObject = deviceRepository.findByIdOptional(deviceId).map(deviceMapper::toDomain); if (deviceObject.isEmpty()) { throw new ServiceException("Can not find device for ID: ", deviceId); } else { return deviceObject; } } public Optional<Device> findByMac(@NonNull String macAddress) { log.info("Finding Device By Mac Address: {}", macAddress); DeviceEntity entity = deviceRepository.find("macAddress", macAddress).firstResult(); Optional<Device> deviceObject = Optional.ofNullable(entity).map(deviceMapper::toDomain); if (deviceObject.isEmpty()) { throw new ServiceException("Can not find device for MAC Address: ", macAddress); } else { return deviceObject; } } public void validateAddress(@Valid Device device) { log.info("Validate Mac address and IP address: {}", device.getMacAddress()); DeviceEntity entityMac = deviceRepository.find("macAddress", device.getMacAddress()).firstResult(); Optional<Device> deviceMac = Optional.ofNullable(entityMac).map(deviceMapper::toDomain); log.info("Validate Mac address and IP address: {}", device.getIpAddress()); DeviceEntity entityIp = deviceRepository.find("ipAddress", device.getIpAddress()).firstResult(); Optional<Device> deviceIp = Optional.ofNullable(entityIp).map(deviceMapper::toDomain); if ((deviceMac.isPresent()) && (deviceIp.isPresent())) { throw new ServiceException("IP address & MAC Address is already exists"); } } public void validateUpdate(@Valid Device device) { this.validateId(device); this.validateAddress(device); } public void validateId(@Valid Device device) { log.info("Validate ID: {}", device.getId()); if (Objects.isNull(device.getId())) { throw new ServiceException("ID is null"); } } @Transactional public void save(@Valid Device device) { log.info("Create Device: {}", device); this.validateAddress(device); DeviceEntity entity = deviceMapper.toEntity(device); deviceRepository.persist(entity); deviceMapper.updateDomainFromEntity(entity, device); } @Transactional public void update(@NotNull @Valid Device device) { log.info("Updating Device: {}", device); this.validateId(device); DeviceEntity entity = deviceRepository.findByIdOptional(device.getId()) .orElseThrow(() -> new ServiceException("No Device found for ID[%s]", device.getId())); deviceMapper.updateEntityFromDomain(device, entity); deviceRepository.persist(entity); deviceMapper.updateDomainFromEntity(entity, device); } @Transactional public void deleteAll() { log.debug("Deleting Devices: {}"); deviceRepository.deleteAll(); } }

@AllArgsConstructor: sẽ sinh ra constructor với tất cả các tham số cho các thuộc tính của class một cách tự động.

Resource/Controller layer

Thêm mở rộng OpenAPI vào tệp pom.xml của project.

<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>

Bây giờ tiện ích mở rộng OpenAPI đã có, bạn có thể triển khai layer DeviceController

package org.acme.controller;
import org.acme.response.ResponseObject;
import org.acme.service.device.Device;
import org.acme.service.device.DeviceService; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag; import javax.inject.Inject;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.List;
import java.util.Optional; @Path("/devices")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "device", description = "Device Operations")
@AllArgsConstructor
@Slf4j
public class DeviceController { @Inject DeviceService deviceService; @GET @APIResponse( responseCode = "200", description = "Get All Devices", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(type = SchemaType.ARRAY, implementation = Device.class) ) ) @APIResponse( responseCode = "404", description = "DB may be not connected", content = @Content(mediaType = MediaType.APPLICATION_JSON) ) public Response getAll() { ResponseObject responseObject = new ResponseObject(); try { List<Device> data = deviceService.findAll(); responseObject.getSuccess(data); return Response.ok(responseObject).build(); } catch (Exception e) { log.error("Failed to get all data", e); responseObject.getFailed(); return Response.serverError().entity(responseObject).build(); } } @GET @Path("/getById/{id}") @APIResponse( responseCode = "200", description = "Get Device by Id", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(type = SchemaType.OBJECT, implementation = Device.class) ) ) @APIResponse( responseCode = "404", description = "DB may be not connected", content = @Content(mediaType = MediaType.APPLICATION_JSON) ) public Response getById(@Parameter(name = "id", required = true) @PathParam("id") Integer deviceId) { ResponseObject responseObject = new ResponseObject(); try { Optional<Device> data = deviceService.findById(deviceId); responseObject.getSuccess(data); return Response.ok(responseObject).build(); } catch (Exception e) { log.error("Failed to get data by id", e); responseObject.getFailed(); return Response.serverError().entity(responseObject).build(); } } @GET @Path("/getByMac/{macAddress}") @APIResponse( responseCode = "200", description = "Get Device by Id", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(type = SchemaType.OBJECT, implementation = Device.class) ) ) @APIResponse( responseCode = "404", description = "Device does not exist for Mac Adress", content = @Content(mediaType = MediaType.APPLICATION_JSON) ) public Response getByMac(@Parameter(name = "macAddress", required = true) @PathParam("macAddress") String deviceMacAddress) { ResponseObject responseObject = new ResponseObject(); try { Optional<Device> data = deviceService.findByMac(deviceMacAddress); responseObject.getSuccess(data); return Response.ok(responseObject).build(); } catch (Exception e) { log.error("Failed to get data by id", e); responseObject.getFailed(); return Response.serverError().entity(responseObject).build(); } } @POST @APIResponse( responseCode = "201", description = "Device Created", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(type = SchemaType.OBJECT, implementation = Device.class) ) ) @APIResponse( responseCode = "409", description = "Invalid Device", content = @Content(mediaType = MediaType.APPLICATION_JSON) ) public Response post(@NotNull @Valid Device device, @Context UriInfo uriInfo) { ResponseObject responseObject = new ResponseObject(); try { deviceService.validateAddress(device); deviceService.save(device); responseObject.createSuccess(device); URI uri = uriInfo.getAbsolutePathBuilder().path(Integer.toString(device.getId())).build(); return Response.created(uri).entity(responseObject).build(); } catch (Exception e) { log.error("Failed to get data by id", e); responseObject.createFailed(); return Response.serverError().entity(responseObject).build(); } } @PUT @APIResponse( responseCode = "204", description = "Device updated", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(type = SchemaType.OBJECT, implementation = Device.class) ) ) @APIResponse( responseCode = "409", description = "Invalid Device", content = @Content(mediaType = MediaType.APPLICATION_JSON) ) public Response put( @NotNull @Valid Device device) { ResponseObject responseObject = new ResponseObject(); try { deviceService.validateUpdate(device); deviceService.update(device); responseObject.updateSuccess(device); return Response.ok(responseObject).build(); } catch (Exception e) { log.error("Failed to get data by id", e); responseObject.updateFailed(); return Response.serverError().entity(responseObject).build(); } } @DELETE @APIResponse( responseCode = "204", description = "Delete All Devices", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(type = SchemaType.ARRAY, implementation = Device.class) ) ) @APIResponse( responseCode = "500", description = "DB may be not connected", content = @Content( mediaType = MediaType.APPLICATION_JSON, schema = @Schema(type = SchemaType.ARRAY, implementation = Device.class) ) ) public Response deleteAll() { ResponseObject responseObject = new ResponseObject(); try { deviceService.deleteAll(); return Response.ok(responseObject).build(); } catch (Exception e) { log.error("Failed to get data by id", e); responseObject.deleteFailed(); return Response.serverError().entity(responseObject).build(); } } }

Kết Luận

Sự thay đổi của hệ sinh thái Quarkus trong những năm gần đây thực sự đáng kinh ngạc. Mặc dù framework vẫn nhằm mục đích nhẹ và nhanh, nhưng tiện tích hỗ trợ và khả năng tích hợp của nó dường như nằm trên một Đồ thị hàm số mũ. Nếu bạn đang tìm kiếm một framework Java như một phần trong nỗ lực hiện đại hóa ứng dụng hoặc đang bắt đầu hành trình phát triển ứng dụng Kubernetes hay Cloud native, thì Quarkus chắc chắn sẽ nằm trong danh sách lựa chọn của bạn.

Bình luận

Bài viết tương tự

- vừa được xem lúc

Deploying A Containerized Web Application On Kubernetes

1. Overview. Kubernetes is an open source project (available on kubernetes.io) which can run on many different environments, from laptops to high-availability multi-node clusters; from public clouds to on-premise deployments; from virtual machines to bare metal.

0 0 26

- vừa được xem lúc

Kubernetes - Học cách sử dụng Kubernetes Namespace cơ bản

Namespace trong Kubernetes là gì. Tại sao nên sử dụng namespace.

0 0 96

- vừa được xem lúc

[Kubernetes] Kubectl và các command cơ bản

Mở đầu. Kubectl là công cụ quản trị Kubernetes thông qua giao diện dòng lệnh, cho phép bạn thực thi các câu lệnh trong Kubernetes cluster.

0 0 39

- vừa được xem lúc

Triển khai EFK Stack trên Kubernetes

EFK stack on K8S. Giới thiệu. Một hệ thống có thể chạy nhiều dịch vụ hoặc ứng dụng khác nhau, vì vậy việc. theo dõi hệ thống là vô cùng cần thiết.

0 0 54

- vừa được xem lúc

Thực hành Kubernetes (K8S) bằng cách sử dụng lệnh Command

Bài hướng dẫn hôm nay sẽ hướng dẫn sử dụng K8S bằng cách sử dụng câu lệnh thay vì UI trên web. Có 2 lựa chọn để thực hiện:. . Sử dụng Cloud Shell.

0 0 42

- vừa được xem lúc

Kubernetes best practices - Liveness và Readiness Health checks

Mở đầu. Kubernetes cung cấp cho bạn một framework để chạy các hệ phân tán một cách mạnh mẽ.

0 0 35