Assuming the actual instance to be a remote service instance, we can use the design pattern to implement a client server model allowing us to segregate the complex business logic as a separate layer.
The proxy design pattern helps us in scenarios where we want to substitute actual instance with a placeholder. Assuming the actual instance to be a remote service instance, we can use the design pattern to implement a client-server model allowing us to segregate the complex business logic as a separate layer.
Another advantage of this implementation is the minimal footprint of code (mostly boilerplate), required to manage request-response, in case there are multiple services with more than 1 method.
The following section provides an overview of the various modules and their implementation:
Defining the service contract: Consider a demo RoomBookingService
that provides the following features:
public interface RoomBookingService {
Boolean isAvailable(LocalDate from, LocalDate to, int count);
Long book(LocalDate from, LocalDate to, int count) throws BookingException;
Boolean cancel(long bookingId) throws BookingException;
}
The package containing the RoomBookingService
is added as a dependency in both service-impl and service-consumer as a dependency.
<dependency>
<groupId>com.jvmaware</groupId>
<artifactId>service-contract</artifactId>
<version>${service.contract.version}</version>
</dependency>
Implementing the service contract: The next step is to implement the contract as a SpringBoot web service. The class BookingEndpoint
has three endpoints corresponding to the methods in RoomBookingService
.
@RestController
@RequestMapping(value = "/booking")
public class BookingEndpoint {
private final RoomBookingService roomBookingService;
public BookingEndpoint(RoomBookingService roomBookingService) {
this.roomBookingService = roomBookingService;
}
@PostMapping(value = "/new/{count}")
public long book(@PathVariable int count,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate from,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate to) {
logger.info("booking request received from: [{}] to: [{}] for [{}] rooms", from, to, count);
return roomBookingService.book(from, to, count);
}
@GetMapping(value = "/check/{count}")
public boolean checkAvailability(@PathVariable int count,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate from,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate to) {
logger.info("checking room [{}] availability from [{}] to [{}]", count, from, to);
return roomBookingService.isAvailable(from, to, count);
}
@PostMapping(value = "/cancel/{bookingId}")
public boolean book(@PathVariable int bookingId) {
logger.info("cancel request received for id: [{}]", bookingId);
return roomBookingService.cancel(bookingId);
}
}
This in turn delegates the requests to RoomBookingServiceImpl
class which provides the concrete implementation for these methods.
public class RoomBookingServiceImpl implements RoomBookingService {
@Override
public Boolean isAvailable(LocalDate from, LocalDate to, int count) {
// some implementation to check the room availability here
// sending a default value for DEMO
logger.info("room availability checked completed");
return true;
}
@Override
public Long book(LocalDate from, LocalDate to, int count) throws BookingException {
// some implementation to book the rooms
// sending a default booking id for DEMO
logger.info("booking completed successfully");
return randomNumberGenerator.nextLong();
}
@Override
public Boolean cancel(long bookingId) throws BookingException {
// some implementation to cancel the room booking
// sending a default value for DEMO
logger.info("booking cancelled successfully");
return true;
}
}
Implementing the service consumer: When implementing the consumer, we create a remote proxy for the RoomBookingService
via ServiceProvider
:
public class ServiceProvider {
private static final ConfigProvider configProvider = new ConfigProvider();
public static RoomBookingService roomBookingService() {
return RoomBookingServiceProvider.INSTANCE;
}
private static class RoomBookingServiceProvider {
private static final RoomBookingService INSTANCE =
(RoomBookingService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{RoomBookingService.class},
new ServiceInvocationHandler(configProvider));
}
}
The ServiceInvocationHandler
is responsible for managing connections to the remote service and result transformation:
public class ServiceInvocationHandler implements InvocationHandler {
private final ConfigProvider configProvider;
private final HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
public ServiceInvocationHandler(ConfigProvider configProvider) {
this.configProvider = configProvider;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
var config = configProvider.getConfig();
var baseUrl = config.getBaseUrl();
var httpMethodAndUrl = config.getMappings().get(method.getName());
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(MessageFormat.format(baseUrl + httpMethodAndUrl.getEndPoint(), args)))
.timeout(Duration.ofMinutes(1))
.header("Content-Type", "application/json")
.method(httpMethodAndUrl.getMethod(), HttpRequest.BodyPublishers.noBody())
.build();
Supplier<?> body = client.send(request, new CustomBodyHandler<>(method.getReturnType())).body();
return body.get();
}
}
The actual endpoint details like resource identifier, method etc are resolved using ConfigProvider
that load these details from a config file.
Finally, the requests are triggered from the client code which is totally abstracted from the underlying remote connections:
private void triggerInvocations() {
var roomBookingService = ServiceProvider.roomBookingService();
var now = LocalDate.now();
var tomorrow = now.plus(1, ChronoUnit.DAYS);
int requestedRoomCount = 2;
// rooms are available; book rooms
if(roomBookingService.isAvailable(now, tomorrow, requestedRoomCount)){
logger.info("[{}] rooms are available for: [{}] and [{}]", requestedRoomCount, now, tomorrow);
roomBookingService.book(now, tomorrow, requestedRoomCount);
}
}
All the method calls on the proxy roomBookingService
are captured by the ServiceInvocationHandler
#invoke which then use the ConfigProvider
to extract the actual endpoint details and the request method. A HttpClient
instance is used to create connections and provide the results back to the client.
From an end-user perspective, the methods are called on a service instance as those are available locally. This allows us to segregate the actual client code from the underlying request-response mechanism.
You can check this repository for more details and code.
Be notified of new posts. Subscribe to the RSS feed.