Showing posts with label Dynamic Proxy. Show all posts
Showing posts with label Dynamic Proxy. Show all posts

Saturday, 23 March 2024

Say Goodbye to Boilerplate: Annotating Your Way to Powerful API Clients

Ever feel like writing boilerplate code for every single REST API call?

Constructing URLs, managing parameters, setting headers, crafting bodies - it's all time-consuming and repetitive. Wouldn't it be amazing if, like ORMs for databases, there was a way to simplify API interactions?

Well, you're in luck!

This post will introduce you to the concept of annotation-based RPC clients, a powerful tool that can dramatically reduce your API coding burden. We'll explore how annotations can automate the tedious parts of API calls, freeing you to focus on the real logic of your application.


I will use search engine API as example to demonstrate how it will look like.


public interface GoogleSearchService {

@XGET("/customsearch/v1")
@XHeaders({"Content-Type: application/json"})
RpcReply<Map<String, Object>> search(@XQuery("key") String apiKey,
                                @XQuery("cx") String context,
                              @XQuery(value = "q", encoded = true) String searchTerm);




}


This approach uses annotations to declaratively specify everything about the API call. This declarative style offers several advantages:

  • Familiar Programming Experience: The syntax feels like you're interacting with a normal programming API, making it intuitive and easy to learn.
  • Abstraction from Implementation Details: You don't need to worry about the internal names of parameters or the specifics of the underlying protocol. The annotations handle those details for you.
  • Simplified Error Handling: The RpcReply abstraction encapsulates error handling, providing a clean and consistent way to manage potential issues.
  • Seamless Asynchronous Support: Adding asynchronous execution becomes straightforward, allowing you to make non-blocking API calls without extra effort.
  • Enhanced Testability: Everything related to the API interaction is defined through the interface, making unit testing a breeze. You can easily mock the interface behavior to isolate your application logic.

Lets have look at RpcReply abstraction 


public interface RpcReply<T> {

T value();
void execute();
int statusCode();
boolean isSuccess();
Optional<String> reply();
Optional<String> error();
Optional<Exception> exception();
}


The RpcReply object plays a crucial role in this approach. It's a lazy object, meaning its execution is deferred until explicitly requested. This allows you to choose between synchronous or asynchronous execution depending on your needs.

More importantly, RpcReply encapsulates error handling. It takes care of any errors that might occur during the actual API call. You can access the response data or any potential errors through a clean and consistent interface provided by RpcReply. This eliminates the need for manual error handling code within your application logic.


One more thing is missing, reply using map is not neat, it will be better if it is strongly type object like Searchresult.

In essence, using strongly typed objects provides a cleaner, more reliable, and more maintainable way to work with API responses.


Lets add StrongType to our API, it will look something like this.


public interface GoogleSearchService {

@XGET("/customsearch/v1")
@XHeaders({"Content-Type: application/json"})
RpcReply<Map<String, Object>> search(@XQuery("key") String apiKey,
    @XQuery("cx") String context,
     @XQuery(value = "q", encoded = true) String searchTerm);

@XGET("/customsearch/v1")
@XHeaders({"Content-Type: application/json"})
RpcReply<GoogleSearchResult> find(@XQuery("key") String apiKey,
    @XQuery("cx") String context,
    @XQuery(value = "q", encoded = true) String searchTerm);


}


Let's dive into how we can write code that interprets declarative definitions and translates them into actual HTTP API calls.

There are several approaches to generate the code behind this interface:

  • Build Plugin: This method utilizes a build plugin to generate actual code during the compilation process.
  • Dynamic Proxy: This approach employs a dynamic proxy that can leverage the provided metadata to generate real API calls at runtime.

Many frameworks leverage either of these options, or even a combination of both. In the solution I'll share, we'll be using Dynamic Proxy.


I have written about dynamic proxy in past, you can read this if new to concept or need refresher

dynamic-proxy

more-on-dynamic-proxy



High Level Sketch of implementation




In this step, we create a proxy object. This proxy will act as an intermediary between the client application and the real server. It can intercept all outgoing calls initiated by the client app.




During this stage, the client interacts with a remote proxy object. This proxy acts as a transparent intermediary, intercepting all outgoing calls.

Here's what the proxy does:

  • Builds metadata: The proxy can gather additional information about each call, such as timestamps, user IDs, or call identifiers. This metadata can be valuable for debugging, logging, or performance analysis.
  • Makes the server call: Once it has the necessary information, the proxy forwards the request to the actual server.
  • Handles errors: If the server call encounters any issues, the proxy can gracefully handle the error and provide a meaningful response back to the client.
  • Parses the response: The proxy can interpret the server's response and potentially transform it into a format that's easier for the client to understand. This can include type safety checks to ensure the returned data matches the expected format.


Code snippet that build Rpc Stack Trace


HttpCallStack callStack = new HttpCallStack(builder.client());
_processMethodTags(method, callStack);
_processMethodParams(method, args, callStack);
callStack.returnType = returnTypes(method);
return callStack;


Full code of proxy is available at ServiceProxy.java


Lets look at few examples of client service interface.


public interface DuckDuckGoSearch {
@XGET("/ac")
@XHeaders({"Content-Type: application/json"})
RpcReply<List<Map<String, Object>>> suggestions(@XQuery(value = "q", encoded = true) String searchTerm);

@XGET("/html")
@XHeaders({"Content-Type: application/json"})
RpcReply<String> search(@XQuery(value = "q", encoded = true) String searchTerm);

}


public interface DuckDuckGoService {
@XGET("/search.json")
@XHeaders({"Content-Type: application/json"})
RpcReply<Map<String, Object>> search(@XQuery("api_key") String apiKey, @XQuery("engine") String engine, @XQuery(value = "q", encoded = true) String searchTerm);

@XGET("/search.json")
@XHeaders({"Content-Type: application/json"})
RpcReply<DuckDuckGoSearchResult> query(@XQuery("api_key") String apiKey, @XQuery("engine") String engine, @XQuery(value = "q", encoded = true) String searchTerm);
}

public interface GoogleSearchService {

@XGET("/customsearch/v1")
@XHeaders({"Content-Type: application/json"})
RpcReply<Map<String, Object>> search(@XQuery("key") String apiKey, @XQuery("cx") String context, @XQuery(value = "q", encoded = true) String searchTerm);

@XGET("/customsearch/v1")
@XHeaders({"Content-Type: application/json"})
RpcReply<GoogleSearchResult> find(@XQuery("key") String apiKey, @XQuery("cx") String context, @XQuery(value = "q", encoded = true) String searchTerm);


}


Client code is very lean, it looks something like this

RpcBuilder builder = new RpcBuilder().serviceUrl("https://www.googleapis.com");
GoogleSearchService service = builder.create(GoogleSearchService.class);

String key = System.getenv("google_search");

RpcReply<Map<String, Object>> r = service.search(key, "61368983a3efc4386", "large language model");
r.execute();
Map<String, Object> value = r.value();
List<Map<String, Object>> searchResult = (List<Map<String, Object>>) value.get("items");

searchResult.forEach(v -> {
System.out.println(v.get("title") + " -> " + v.get("link"));
});

RpcReply<GoogleSearchResult> searchResults = service.find(key, "61368983a3efc4386", "large language model");

searchResults.execute();

searchResults.value().items.forEach(System.out::println);


Full client code is available @ APIClient.java


Conclusion

Dynamic proxies offer a powerful approach to abstraction, providing several benefits:

  • Protocol Independence: The underlying communication protocol can switch from HTTP to something entirely new (e.g., gRPC, custom protocols) without requiring any changes to the client code. The dynamic proxy acts as an intermediary, insulating the client from the specifics of the protocol being used.
  • Enhanced Functionality: Dynamic proxies can add valuable features to client interactions. This can include:
    • Caching: The proxy can store responses to frequently accessed data, reducing load on the server and improving performance.
    • Throttling: The proxy can limit the rate of calls made to the server to prevent overloading or comply with usage quotas.
    • Telemetry: The proxy can collect data about client-server interactions, providing insights into system performance and user behavior.

By leveraging dynamic proxies, you can achieve a clean separation between the client's core logic and the communication details. This promotes loose coupling, making your code more adaptable, maintainable, and easier to test.

This approach leads us towards a concept similar to API Relational Mapping (ARM) (though this term isn't widely used). Think of it as a specialized layer that translates between API calls and the underlying functionalities they trigger.


Full client library is available @ rpcclient.

Sunday, 11 December 2022

More on Dynamic proxy

This is a follow up post from dynamic proxy to share about more realistic examples of dynamic proxy.

Today's software development is heavily reliant on system observability. Observability helps us to understand when the system degrades or misbehaves so that we can take proactive measures to fix it. 

  
Toy service for this example will be FXService 

public interface FXService {

double convert(String from, String to, int amount);
}

This service will use https://api.exchangerate.host API for FX conversion.




Lets look at what types of dynamic proxy we can create on top of this.

Method Timing

It will keep track of method execution time and make it available later for analysis or other purposes. Using this proxy, X slow-running methods will be provided.
In FXService, there is only one method, so this proxy will create a method key with the method name and parameters.


Method timing proxy will show top X slow running method for eg. 


Method convert( SGD,IDR,1 ) took 1041 ms
Method convert( SGD,GBP,1 ) took 994 ms
Method convert( SGD,USD,1 ) took 983 ms
Method convert( SGD,IDR,1 ) took 672 ms
Method convert( SGD,INR,1 ) took 650 ms
Method convert( SGD,USD,1 ) took 593 ms
Method convert( SGD,JPY,1 ) took 593 ms
Method convert( SGD,GBP,1 ) took 582 ms
Method convert( SGD,USD,1 ) took 580 ms
Method convert( SGD,INR,1 ) took 566 ms

Such type of proxy is very helpful in identifying outage or degradation in API.

Stand In Processing

Proxy services such as this can be used to provide stand-in processing when the underlying service is down. For example, when a real FX service is down, this proxy can answer queries from the last successful call.




It is useful for not only enhancing availability but also improving latency, since such a service can answer queries from the local cache right away. Additionally, it may be possible to save some costs if the underlying API is charged by usage.


Chain of proxy

Nice thing about proxies is that multiple proxies can be composed together to create a complex chain of proxy. For example, we can chain Stand In & Method timing together to get features of both.



Below code snippet is creating chain of proxy

FXService core = new FXServiceAPI("https://api.exchangerate.host", 1);
FXService timeRecorderProxy = create(FXService.class, new TimeRecorderProxy(core, tracker));
FXService standInProxy = create(FXService.class, new StandInProcessingProxy(timeRecorderProxy, cache));
FXService fx = standInProxy; 


Full code using all the proxy

List<String> currency = new ArrayList<String>() {{
add("USD");
add("INR");
add("GBP");
add("IDR");
add("JPY");
add("CAD");
}};

IntStream.range(0, 100).forEach($ -> {
Collections.shuffle(currency);
currency.parallelStream().forEach(code -> {
try {
Double d = fx.convert("SGD", code, 1);
System.out.println(d);
} catch (Exception e) {
System.out.println("Failed for " + code);
}
});
});

tracker.dumpSlowRequests(10);
cache.prettyPrint();



Code used in this post is available @ fx service

Conculsion

A dynamic proxy is a powerful tool that is part of Java's ecosystem. It can be a very useful tool for writers of libraries or frameworks, since a proxy's primary purpose is to extend the functionality of an underlying service/api. Therefore, special precautions must be taken to ensure that it does not negatively impact the underlying service.


 

Dynamic Proxy

In software design, a proxy pattern is one of the popular GOF design patterns. 

A proxy is a class functioning as an interface to something else, it could be an interface to some business logic, network, file, or anything else.

This can also be seen as a wrapper around something core or real, main goal of a proxy is to add abstraction to get something extra it could be logging, permission check, cache, metric collection, etc.

We interact with proxies in real life. Take a example of bank interaction via ATM





ATM acts like a proxy to the bank branch, it allows to do almost everything that can be done at a branch.


In software we see many variations of a proxy, some of the examples are in IO API in java




Another variation is chain of responsibility.  




In java proxy can be of 2 types it can be static and dynamic, lets look at a static proxy example.


Static Proxy

We are building Big Collection that allows to store unlimited data, our big collection interface looks like

public interface BigCollection<V> {
void add(V value);

boolean exists(V value);

void forEach(Consumer<V> c);
}


Static proxy will have same interface as original interface and will manually delegate calls to real object, it will look something like below.


public class BigCollectionProxy<V> implements BigCollection<V> {

private final Supplier<BigCollection<V>> supplier;
private final BigCollection<V> realObject;

public BigCollectionProxy(Supplier<BigCollection<V>> supplier) {
this.supplier = supplier;
this.realObject = supplier.get();
}

@Override
public void add(V value) {
realObject.add(value);
}

@Override
public boolean exists(V value) {
return realObject.exists(value);
}

@Override
public void forEach(Consumer<V> c) {
realObject.forEach(c);
}
}


Client API for using the proxy will look something like below 


BigCollection<String> collection = new BigCollectionProxy<>(AwsCollection::new);

collection.add("Value1");
collection.add("Value2");

collection.forEach(System.out::println);

System.out.println("Exists " + collection.exists("Value2"));

Static proxy is easy to implement but it has got few problems 

  • Manual delegation is painful and very verbose.
  • Any changes in interface required proxy also to implement changes.
  • Special treatment to functions that are part of language ecosystem like equals, hashcode,getClass etc. 
  • and as name suggest it is static, can't change the behavior at runtime.

Dynamic proxy solves issue with static proxy, lets look at dynamic proxy.

Dynamic Proxy

Dynamic proxy creates proxy at runtime, it is very flexible and convenient.

 JDK has dynamic proxy API since 1.3 that allows to create dynamic proxy using very simple API

Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
                                          new Class[] { Foo.class },
                                          handler);
Lets create dynamic proxy for BigCollection class.

(BigCollection<V>) Proxy.newProxyInstance(BigCollection.class.getClassLoader(),
new Class<?>[]{BigCollection.class},
new BigCollectionDynamicProxy(supplier.get()));

This proxy looks exactly like BigCollection implementation and can be passed around. This also does not have the verbosity of static/hand crafted proxy, full proxy looks something like below


public class BigCollectionDynamicProxy implements InvocationHandler {
private final Object realObject;

public BigCollectionDynamicProxy(Object realObject) {
this.realObject = realObject;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(realObject, args);
}

public static <V> BigCollection<V> create(Supplier<BigCollection<V>> supplier) {
return (BigCollection<V>) Proxy.newProxyInstance(BigCollection.class.getClassLoader(),
new Class<?>[]{BigCollection.class},
new BigCollectionDynamicProxy(supplier.get()));
}

} 


Java reflection makes it easy to delegate calls to underlying real object.


Dynamic Proxy Use case

Lets look at some use case where dynamic proxy will come handy.

 - Timing of method execution

Elapsed time calculation is one of the cross-cutting concerns and proxy comes in handy for such use cases without the need of adding time tracking code all over the place.
 
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.nanoTime();
try {
return method.invoke(realObject, args);
} finally {
long total = System.nanoTime() - start;
System.out.println(String.format("Function %s took %s nano seconds", method.getName(), total));
}
}


 - Single thread execution

Many time some use case need single thread access to critical data structure. Dynamic proxy can add synchronization at the higher level, core code is not worrying about language level synchronization APIs.

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
synchronized (realObject) {
return method.invoke(realObject, args);
}
}

 - Asynchronous Execution.

Asynchronous execution is one of the common techniques to get best out of  cores of underlying machine. Java has made parallel computation very easy with Completable future, parallel streams etc. Newer JDK version will have support for fibers and that will add concept of light weight threads.


Dynamic proxy can be used to convert synchronous API to asynchronous with simple code.  

Code snippet for async exeuction.

es.submit(() -> {
try {
System.out.println("Using thread " + Thread.currentThread().getName());
method.invoke(realObject, args);
} catch (Exception e) {
e.printStackTrace();
}
}); 

Few things to understand for such type of scenario
 - Value is not return, client API has to use some mechanism like call back handler or reply API to get value.
 - Exception handling.


 - Logging.

This is straight forward use case and very popular one. 

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.nanoTime();
try {
return method.invoke(realObject, args);
} finally {
long total = System.nanoTime() - start;
System.out.println(String.format("Function %s took %s nano seconds", method.getName(), total));
}
}

Dynamic Proxy Tradeoff

Nothing comes for free, dynamic proxy has below tradeoff.

- Reflection cost 
- Parameter box/unboxing
- Array overhead for parameters.

Reflection related cost are not that much of issue in JDK8 onwards. I wrote about reflection in few post.

Methodhandle returns back in java 8

Boxing & Unboxing overhead are really hard to resolve and this will be always overhead for dynamic proxy unless some code generation technique is used.

Dynamic proxy can be used to build many useful things. In next part of this post i will have more advance usage of dynamic proxy.


Code used in this post is available @ proxy github project