📧 Email: danilarassokhin@gmail.com
💻 GitHub: @CrissNamon
📑 LinkedIn: Danila Rassokhin
🐦 Twitter: @KpekepSalt
📚 Medium: @danilarassokhin
📜 Блог
by Danila Rassokhin
В этой статье я расскажу как использовать Proxy
из пакета java.lang.reflect
на примере создания http-клиента на основе интерфейсов, аналогичного Retrofit.
Чтобы вы поняли конечный результат, я покажу пример.
Создадим некоторый интерфейс для хранения наших запросов в одном месте:
public interface BookClient {
// Get request to SOME_BASE_URL/book
@GET("/book")
List<Book> getBooks();
}
Затем мы просто создадим экземпляр этого интерфейса с нашим специальным классом-создателем:
BookClient bookClient = WebClient.of(BookClient.class)
.baseUrl("https://63c306edb0c286fbe5f7e9d4.mockapi.io/api/v1")
.create();
List<Book> books = bookClient.getBooks();
System.out.println(books);
Вывод будет таким:
[Book{id=1, title='Sherry Waelchi'}, Book{id=3, title='Mr. Nathan Labadie'}, ...]
Как это работает?
Когда мы вызываем WebClient.of(BookClient.class)
, он использует Proxy
для создания прокси-объекта нашего интерфейса, который будет возвращен в результате. Все вызовы методов нашего прокси-объекта будут проксированы к нашей реализации InvocationHandler
, которая будет собирать информацию из аннотаций и отправлять запрос с использованием стандартного HttpClient
. Ответ из JSON будем парсить с использованием библиотеки Gson, а затем возвращать как результат вызванного метода.
Переходим к коду!
Сначала мы создадим простую аннотацию, чтобы указать метод запроса и URL-адрес ресурса:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GET {
String value();
}
Нам также нужно некоторое перечисление доступных методов Http:
public enum RequestMethod {
GET
}
Теперь нам нужен AnnotationProcessor, чтобы найти наши аннотации:
public class AnnotationProcessor {
/**
* Этот метод получит все аннотации из данного
* метода и найдет аннотацию с заданным типом
**/
public static <T extends Annotation> T extractMethodAnnotation(Method method,
Class<T> annotationClasses) {
return Arrays.stream(method.getDeclaredAnnotations())
.filter(annotation -> annotation.annotationType().equals(annotationClasses))
.map(annotation -> (T) annotation)
.findFirst().orElse(null);
}
}
Для построения запроса создадим класс RequestCreator
с одним статическим методом.:
public class RequestCreator {
/**
* Создать запрос на baseUrl + path методом requestMethod
**/
public static HttpRequest create(String baseUrl, String path,
RequestMethod requestMethod) {
HttpRequest.Builder baseRequest = baseRequest(baseUrl + path);
switch (requestMethod) {
case GET:
baseRequest = baseRequest.GET();
break;
default:
throw new RuntimeException("Method " + requestMethod + " is not supported");
}
return baseRequest.build();
}
/**
* Создает базовый запрос с base url и версией Http.
**/
private static HttpRequest.Builder baseRequest(String url) {
try {
return HttpRequest.newBuilder()
.uri(new URI(url))
.version(HttpClient.Version.HTTP_2);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Чтобы перехватывать вызовы методов нашего прокси-объекта, нам нужна реализация интерфейса InvocationHandler
, поэтому давайте создадим RequestMethodHandler
:
public class RequestMethodHandler implements InvocationHandler {
private final HttpClient httpClient;
private final String baseUrl;
public RequestMethodHandler(HttpClient httpClient, String baseUrl) {
this.httpClient = httpClient;
this.baseUrl = baseUrl;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Получить аннотацию с проксированного метода
GET get = AnnotationProcessor.extractMethodAnnotation(method, GET.class);
String url = get.value();
// Создать запрос
HttpRequest request = RequestCreator.create(baseUrl, url, RequestMethod.GET);
return sendRequest(request, method);
}
/**
* Отправит запрос с использованием HttpClient и распарсит JSON-ответ через Gson
**/
private Object sendRequest(HttpRequest httpRequest, Method method) {
try {
HttpResponse<String> httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString());
// Важно: method.getGenericReturnType()
// вернет тип возвращаемого значения метода вместе с дженериками
// Например, если возвращаемый тип - List<Book>, тогда мы молучи именно этот тип,
// но method.getReturnType() вернет только List.class
return new Gson().fromJson(httpResponse.body(), method.getGenericReturnType());
} catch (JsonSyntaxException | IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}
Наконец, мы можем создать класс WebClient для создания наших прокси-объектов:
public class WebClient<T> {
private final Class<T> clientClass;
private String baseUrl;
private HttpClient httpClient = HttpClient.newBuilder().build();
private WebClient(Class<T> clientClass) {
this.clientClass = clientClass;
}
public static <T> WebClient<T> of(Class<T> clientClass) {
return new WebClient<>(clientClass);
}
public WebClient<T> baseUrl(String baseUrl) {
this.baseUrl = baseUrl;
return this;
}
@SuppressWarnings("unchecked")
public T create() {
return (T) Proxy.newProxyInstance(clientClass.getClassLoader(), new Class[]{clientClass},
getDefaultHandler()
);
}
private InvocationHandler getDefaultHandler() {
return new RequestMethodHandler(httpClient, baseUrl);
}
}
В методе T create()
мы вызываем статический метод Proxy.newProxyInstance
для создания прокси-объекта. Он принимает 3 аргумента:
InvocationHandler
И теперь мы можем создать какой-нибудь клиент и протестировать его на mock API. Вот и все.
Это всего лишь базовый пример, но вы можете добавить дополнительные методы запроса, параметры пути и многое другое. Смотрите мою версию клиента со всеми полезными функциями на GitHub: https://github.com/CrissNamon/http-interface-client