версия v1.5.20
выпущена. Из-за занятости другими делами работа над ней несколько затянулась, но благодаря поддержке других участников проекта удалось значительно продвинуться в развитии этой версии, особенно в части совместимости с JDK 17. Отдельное спасибо хочется выразить за это. В данной версии основной акцент сделан на поддержку новых версий JDK, включая все версии от 8 до 17 (особенно проверены популярные версии 11 и 17, а версию 8 поддерживают уже давно).
v1.5.19
версия выпущена, в этом выпуске были исправлены некоторые ошибки.
fix: В нативной среде Spring, интерцепторы не могут быть внедрены в контекст Spring (#I4UE9T)
fix: При параллельном использовании пула соединений HTTPS в бэкенд-части HttpClient данные смешиваются (#I4TYJ1)
fix: При наличии двух заголовков Set-Cookie
, можно получить только последний (#I4TATV)
commons-logging
версия v1.5.17
выпущена. В этом выпуске были исправлены ранее существовавшие ошибки, а также добавлена возможность динамического управления источниками прокси.
feat: Динамическое управление источниками прокси (#I4SYM1)
fix: Исключение: Файл SSL KeyStore пуст (#I4SYGB)
fix: Настройка maxRetryInterval не работает (#I4SV2P)
fix: Ошибка при отсутствии зависимости lang3 в основном проекте (#I4M9DE)
fix: Объединённые аннотации не применяются (#I4N2HC)
refactor: Удаление повторяющихся условий if в util-классе NameUtils
@CHMing
1. 5. 15
версия выпущена, в которой добавлены пользовательские SSL-верификаторы доменного имени, а также исправлена проблема с кодировкой символов ответа Response
и распаковкой gzip
.
Forest принял участие в конкурсе открыток проектов OSC 2021 года. Если вам нравится Forest или вас интересует этот проект, пожалуйста, проголосуйте за него своим голосом. Благодарим!
hostnameVerifier
Определите класс, реализующий интерфейс HostnameVerifier
/**
* Пользовательский SSL-верификатор доменного имени
*/
public class MyHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String s, SSLSession sslSession) {
if ("gitee.com".equals(s)) {
return true;
}
return false;
}
}
Настройте пользовательский SSL-верификатор доменного имени в конфигурационном файле KeyStore
Forest
application.yml
Spring Boot проектаforest:
ssl-key-stores:
- id: keystore1
hostname-verifier: your.site.MyHostnameVerifier
Привязка к методу API
@Post(url = "/something", keyStore = "keystore1")
String postSomething(@Body body);
@SSLHostnameVerifier
Кроме глобальной конфигурации в файле keyStore
, можно использовать аннотацию для прямой привязки к методу API
public class MyHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
// Пропустить запросы только для домена gitee.com
if ("gitee.com".equals(hostname)) {
return true;
}
return false;
}
}
```Привязка через аннотацию `@SSLHostnameVerifier`
```java
@Post(url = "/something")
@SSLHostnameVerifier(TrustAnyHostnameVerifier.class)
String postSomething(@Body body);
@SSLSocketFactoryBuilder
Аналогично, можно настроить SSLSocketFactory
, используя аннотацию @SSLSocketFactoryBuilder
public class MySSLSocketFactoryBuilder implements SSLSocketFactoryBuilder {
@Override
public SSLSocketFactory getSSLSocketFactory(ForestRequest request, String protocol) throws Exception {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null,
new TrustManager[]{new TrustAllManager()},
new SecureRandom());
System.out.println("do MySSLSocketFactoryBuilder");
return sslContext.getSocketFactory();
}
}
Привязка к интерфейсу
@Post(url = "/что-то")
@SSLSocketFactoryBuilder(MySSLSocketFactoryBuilder.class)
String postЧего_то(@Body body);
Этот выпуск был возможен благодаря вкладу участника @designer, за что мы очень благодарны!## Голосование за проект Forest на конкурсе "Китайская открытая лицензионная программа" 2021 года
Проект Forest участвует в голосовании "Китайской открытой лицензионной программы" 2021 года. Если вам нравится Forest или вас интересует этот проект, пожалуйста, проголосуйте за него своим голосом. Большое спасибо!
Голосовать здесь
версия v1.5.14
выпущена! В этом выпуске были исправлены проблемы с установкой размера пула асинхронных потоков и анализом символа @
в URL.
@
при его наличии в URL (#I4J3LU)@Backend
версия v1.5.13
выпущена. В этом выпуске были исправлены некоторые проблемы с парсингом URL и добавлена возможность указывать тип тела запроса и его кодировщик.
@BodyType
для указания типа тела запроса (#I4IF3N)Encoder
в запросе не работает (#I4HNZF)@
(#I4GQWW)BodyType
FastjsonEncoder
GsonEncoder
JacksonEncoder
com.dtflys.forest.http.ForestBodyType
setBodyType
и bodyType
класса ForestRequest
изменён на ForestDataType
версия v1.5.12
выпущена. В этом выпуске произошло substantial重构后台,并解决了若干与URL解析相关的问题。
{variable name}
при ссылке на скрытую переменную (#I4EP04)в версии v1.5.11 были исправлены несколько ошибок
connect-timeout
не может быть распознана в Spring Boot 1.x (#I4ECR3)версия v1.5.10 выпущена; этот выпуск解决了由于前一版本更改而出现的问题
fix: пустой указатель, вызванный переинтерпретацией URL
fix: пустой указатель, вызванный переинтерпретацией URL
Версия v1.5.9 выпущена, в этом обновлении были исправлены проблемы с некорректной работой URL Encoder в некоторых случаях. Для этого был реализован новый подход к парсингу и кодировке URL, отказавшись от использования встроенных объектов URI и класса URLEncoder из Java. Также была внедрена строковая шаблонизация для семантического представления URL.
/**
* Новый вариант позволяет определять {a} и {b} как параметры запроса URL
* Эти параметры будут закодированы согласно требованиям параметров запроса: символ '&' будет экранирован
* В то время как {path} будет распознан как часть пути URL
* Этот параметр будет закодирован согласно требованиям пути URL: символ '&' не будет экранирован
*/
@Get("/data/{path}? a={a}&b={b}")
String getData(@Var("path") String path, @Var("a") String a, @Var("b") String b);
```#### Различие между `{}` и `${}$`
##### `{}` представляет собой параметр запроса
Шаблон параметра `{}`, используемый в контексте `?a={a}`, рассматривается как параметр запроса URL. Даже если значение переменной может содержать несколько параметров, таких как `"1&x=10&y=20"`, это значение будет экранировано как один параметр запроса.
```java
@Get("http://localhost/data? a={a}&b={b}")
String getData(@Var("a") String a, @Var("b") String b)
// Полученный URL будет выглядеть так:
// http://localhost/data? a=1%26x%3D10%26y%3D20&b=hello
// То есть URL будет содержать только два параметра запроса: a и b
myClient.getData("1&x=10&y=20", "hello");
${}$
может содержать несколько параметров запросаШаблон параметра ${}$
можно рассматривать как замену строки, после которой параметры URL будут проанализированы. Поэтому каждый параметр шаблона может содержать несколько параметров, которые затем будут распознаны как отдельные параметры.
@Get("http://localhost/data? a=${a}&b=${b}")
String getData(@Var("a") String a, @Var("b") String b)
// Полученный URL будет выглядеть так:
// http://localhost/data? a=1&x=10&y=20&b=hello
// То есть URL будет содержать четыре параметра запроса: a, x, y и b
myClient.getData("1&x=10&y=20", "hello");
{}
как шаблон параметраУчитывая особенности этих двух типов шаблонов параметров, они могут использоваться в различных ситуациях.Однако обычно рекомендуется использовать {}
поскольку он более структурирован и семантичен, что делает его легче понять и менее склонным к ошибкам, особенно при передаче одного URL в качестве параметра другому URL.
Например, если требуется передать под-URL с параметрами: https://search.gitee.com/?type=repository&q=forest
После получения в родительском URL это будет выглядеть так: http://localhost/data?call={url}
Если же использовать ${url}$
, возникнут проблемы.
@Get("/data?call=${url}")
String getData(@Var("url") String url);
// Последний сгенерированный URL выглядит так
// http://localhost/data?call=https://search.gitee.com/?type=repository&q=forest
На первый взгляд всё в порядке, но последняя часть `&q=forest` будет воспринята как параметр запроса родительского URL, хотя она должна относиться к под-URL.
Если использовать `{url}`, то проблема исчезает, даже если позже будут добавлены другие параметры.
```java
@Get("/data?call={url}&x={x}")
String getData(@Var("url") String url, @Var("x") String x);
// Последний сгенерированный URL выглядит так
// http://localhost/data?call=https://search.gitee.com/?type=repository%26q=forest&x=xxx
Как видно, разделитель параметров запроса &
внутри под-URL был экранирован, что решило конфликт между параметрами под-URL и параметрами родительского URL (например, x
).#### ИСПРАВЛЕННЫЕ ОШИБКИ
Версия v1.5.8 выпущена, в ней исправлены следующие ошибки:
@DownloadFile
для скачивания файлов в некоторых средах происходил блокирующий запрос (#I4DLBI)Версия v1.5.7 выпущена, в основном решена проблема зависимости от пакета google protobuf.
Исправленные ошибки:
версия v1.5.6 выпущена, в этом обновлении были исправлены некоторые ошибки
forest.connect-timeout
не работает (#I45298)Версия v1.5.5 выпущена, в этой версии основное внимание уделено поддержке Protocol Buffers (Protobuf) и исправлению нескольких ошибок.
@Post(
url = "/proto",
contentType = ContentType.APPLICATION_OCTET_STREAM)
ProtobufProto.Data sendProtobufData(@ProtobufBody ProtobufProto.Data data);
// Переключение на okhttp3
@OkHttp3
@Post("/data1")
String sendData1(@Body MyUser user);
// Переключение на HttpClient
@HttpClient
@Post("/data2")
String sendData2(@Body MyUser user);
Forest v1.5.4 выпущена
Основной акцент в этом выпуске сделан на исправление оставленных проблем, а также добавлении новых интерфейсов.
// Каждый ключ-значение пары из Map будет добавлено как параметр запроса
Map<String, Object> map = new LinkedHashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
Forest.get("/")
// Добавляем Map в качестве параметра запроса
.addQuery(map)
// Выполняем запрос
.execute();
// Параметры запроса будут выглядеть так: a=1&b=2&c=3
// Добавление списка в параметры запроса
Forest.get("/")
.addQuery("a", Arrays.asList(1, 2, 3))
.execute();
// Параметры запроса будут выглядеть так: a=1&a=2&a=3
// Добавление массива в параметры запроса
Forest.get("/")
.addQuery("a", new Object[]{1, 2, 3})
.execute();
// Параметры запроса будут выглядеть так: a=1&a=2&a=3
// Добавление списка в параметры запроса с []
Forest.get("/")
.addArrayQuery("a", Arrays.asList(1, 2, 3))
.execute();
// Параметры запроса будут выглядеть так: a[]=1&a[]=2&a[]=3
// Добавление массива в параметры запроса с []
Forest.get("/")
.addArrayQuery("a", new Object[]{1, 2, 3})
.execute();
// Параметры запроса будут выглядеть так: a[]=1&a[]=2&a[]=3
- fix: зависимость от библиотеки Guava (#I4CC9B)
- fix: проблемы сериализации списков при использовании аннотации `@Query` для параметров типа `Map` (#I4C8UC)
- fix: проблемы многопоточности
## Изменения в коде
- update: удалены лишние DEBUG логи
- add: метод `ForestLogHandler.logContent(String content)`
- add: метод `ForestRequest.addQuery(String name, Collection collection)`
- add: метод `ForestRequest.addQuery(String name, Object... array)`
- add: метод `ForestRequest.addArrayQuery(String name, Collection collection)`
- add: метод `ForestRequest.addArrayQuery(String name, Object... array)`
v1.5.3 версия выпущена, в которой было добавлено множество новых функций, среди которых есть значительные обновления.
Ранее при использовании Forest требовалось определять интерфейсный класс, что удовлетворяло большинство ситуаций. Однако это могло усложнять быстрый доступ к URL.
Поэтому в новой версии был добавлен удобный интерфейс, позволяющий немедленно использовать его без необходимости определения интерфейса:```java
// GET запрос
// Получение данных типа String
String str = Forest.get("/").executeAsString();
// POST запрос
// Получение данных типа MyResult
MyResult myResult = Forest.post("/")
.execute(MyResult.class);
// Передача параметров через TypeReference
// Для получения данных с сложными типами
Result<List> userList = Forest.post("/")
.execute(new TypeReference<List<Result<List>>>() {});
// Определение различных параметров
// Получение данных типа Map
Map<String, Object> map = Forest.post("/")
.backend("okhttp3") // Установка бэкэнда как okhttp3
.contentTypeJson() // Установка заголовка Content-Type как application/json
.host("127.0.0.1") // Установка хоста адреса как 127.0.0.1
.port(8080) // Установка порта адреса как 8080
.addBody("a", 1) // Добавление элемента body (пар ключ-значение): a, 1
.addBody("b", 2) // Добавление элемента body (пар ключ-значение): b, 2
.maxRetryCount(3) // Установка максимального количества попыток повторной отправки запроса как 3
.onSuccess((data, req, res) -> { log.info("успешно!"); }) // Установка функции обратного вызова onSuccess
.onError((ex, req, res) -> { log.info("ошибка!"); }) // Установка функции обратного вызова onError
.successWhen((req, res) -> res.noException() && res.statusOk()) // Установка функции обратного вызова successWhen
.executeAsMap(); // Выполнение запроса и получение объекта типа Map
##### 2.1 Аннотация `@Success`
Сначала требуется определить реализацию интерфейса SuccessWhen
```java
public class TestSuccessWhen implements SuccessWhen {
/**
* Условие успешного выполнения запроса
* @param req Объект запроса Forest
* @param res Объект ответа Forest
* @return Успешность выполнения запроса
*/
@Override
public boolean successWhen(ForestRequest req, ForestResponse res) {
// Отсутствие ошибок и состояние кода в нормальном диапазоне и состояние кода не равно 203
// Конечно, здесь можно указать другие условия, например, получить бизнес-данные с помощью res.getData() или res.getContent()
// Затем использовать бизнес-данные для проверки успешности запроса
return res.noException() && res.statusOk() && res.getStatusCode() != 203;
}
}
Добавьте аннотацию @Success
к методу запроса в Forest
@Get("http://localhost:${port}/")
@Success(condition = TestSuccessWhen.class)
String getData();
Если после вызова метода getData()
возвращается код состояния 203, это будет считаться неудачей запроса. Если количество попыток повторной отправки больше нуля, будет выполнена задача повторной отправки. Если нет доступных попыток повторной отправки, будет выполнен процесс обработки ошибок.
@Retry
Сначала определим реализацию интерфейса RetryWhen
.public class TestRetryWhen implements RetryWhen {
/**
* Условие для повторной отправки запроса
* @param request объект запроса Forest
* @param response объект ответа Forest
* @return следует ли повторить отправку
*/
@Override
public boolean retryWhen(ForestRequest request, ForestResponse response) {
// Если код состояния ответа равен 203, то повторяем отправку, хотя сам запрос может быть успешным
// Конечно, здесь можно указать другие условия, например, получить бизнес-данные с помощью res.getData() или res.getConent()
// Затем использовать бизнес-данные для проверки необходимости повторной отправки
return response.statusIs(203);
}
}
Добавьте аннотацию @Retry
к методу запроса в Forest
// maxRetryCount - максимальное количество повторных отправок
// maxRetryInterval - максимальный интервал времени между повторными отправками, в миллисекундах
// condition - условие для повторной отправки, то есть реализация интерфейса RetryWhen
@Get("http://localhost:${port}/")
@Retry(maxRetryCount = "3", maxRetryInterval = "10", condition = TestRetryWhen.class)
String sendData();
При вызове метода sendData()
если возвращается код состояния 203, это будет считаться необходимостью повторной отправки. Если количество попыток повторной отправки больше нуля, будет выполнена задача повторной отправки.
Если нет доступных попыток повторной отправки, будет выполнен процесс обработки успешного запроса.##### 2. 3 Различия между двумя типами повторной отправки
Может возникнуть вопрос, почему используется аннотация RetryWhen
, если можно было бы просто использовать условие успеха в аннотации SuccessWhen
.Механизм повторной отправки в Forest работает следующим образом:
Запросы, повторные попытки которых происходят после неудачной попытки, переходят в состояние ошибки при последней неудачной попытке (например, вызывается метод onError).
Запросы, повторные попытки которых происходят вследствие выполнения условий повторной попытки, считаются успешными, если последняя попытка также завершается успешно, тогда они переходят в состояние успеха (например, вызывается метод onSuccess).
Краткое описание: successWhen — повторяет попытки при неудаче; retryWhen — повторяет попытки даже при успехе.
Независимо от того, какую стратегию повторной попытки используют запросы, перед отправкой повторной попытки всегда будет вызвана функция обратного вызова OnRetry.
Функцию обратного вызова onRetry можно реализовать в интерцепторе.
public class TestRetryInterceptor implements Interceptor<Object> {
/**
* Вызов функции обратного вызова onRetry перед повторной попыткой
*
* @param request объект запроса Forest
* @param response объект ответа Forest
*/
@Override
public void onRetry(ForestRequest request, ForestResponse response) {
Также следует отметить, что в данном тексте используется английский язык, поэтому перевод осуществляется именно на русский язык. // Добавляем текущее количество повторных попыток в атрибуты объекта запроса Forest
request.addAttachment("retry-interceptor", request.getCurrentRetryCount());
}
}
Связываем интерцептор, который реализует метод `onRetry`, с соответствующим интерфейсом.
```java
@BaseRequest(baseURL = "http://localhost:${port}/", interceptor = TestRetryInterceptor.class)
public interface RetryClient {
@Get("/")
@Retry(maxRetryCount = "${0}", maxRetryInterval = "${1}", condition = TestRetryWhen.class)
ForestRequest<String> testRetryRequest(int retryCount, long retryInterval);
}```java
@Get("/")
@Retry(maxRetryCount = "${0}", maxRetryInterval = "${1}", condition = TestRetryWhen.class)
String testRetry(int retryCount, long retryInterval, OnSuccess<String> onSuccess);
}
feat: Быстрые интерфейсы Forest (#I4893Q)
feat: Поддержка глобальных переменных динамического привязывания методов (#I478N2)
feat: Поддержка шаблонов строк с использованием properties (#I3P1QK)
feat: Поддержка получения причинного сообщения ответа, то есть текстового состояния ответа (#I4BJVF)
feat: Создание пользовательских объединённых аннотаций (#I4BISF)
feat: Условия успешности запроса могут быть настроены пользователем (#I4AEMT)
feat: Возможность динамической установки адреса хоста и номера порта (#I4AEJ8)
feat: Настройка условий повторной попытки (#I493N3)
feat: Введение функции обратного вызова OnRetry (#I493N6)
feat: Добавлено аннотация @Headers
feat: Правила наследования для запросов Forest
feat: Автоматическое управление переадресацией
feat: Поддержка глобальных переменных с динамическим связыванием методов
feat: Отображение имени бэкенд-фреймворка в логах запросов
feat: Создание подпроекта forest-mock
fix: При пустом `Map` в POST-запросе невозможно преобразовать его в JSON-строку `{}`
fix: Параметры фильтра всегда являются первым параметром
fix: Пользовательские заголовки `Content-Type` заменяются на верхний регистр
fix: В проектах Spring отсутствие конфигурации конвертора приводит к невозможности найти `Converter`
fix: Ответ без заголовков `Content-Type` и `Content-Encoding` не может быть правильно распознан
fix: При получении ответа 302 запрос 302-го заголовка недоступен
fix: Недоступность инициализации `SpringSSLKeyStore` в Spring
fix: Конверторы с параметрами `ForestConfiguration` не могут быть корректно инициализированы в Spring Boot
fix: Ошибки стека при длительной работе многопоточных запросов на загрузку файлов
fix: Несовместимость интерфейса `BeanPostProcessor` в ранних версиях Spring Boot#### Улучшения:
opt: Улучшение методов класса StringUtils
opt: Улучшение методов класса URLUtils
#### Изменения кода:
add: Класс SpringForestProperties
add: Добавление свойства responseEncoding во все аннотации запросов (например, `@Request`, `@Get`)
add: Класс SpringForestObjectFactory
add: Метод isRedirection в классе ForestResponse
add: Метод getRedirectionLocation в классе ForestResponse
add: Метод redirectionRequest в классе ForestResponse
add: Метод clone в классе ForestHeaderMap
add: Метод clone в классе ForestQueryMap
refactor: Свойство retryCount больше не рекомендуется использовать
update: Удаление MethodLifeCycle
refactor: Изменение логики сканирования типов в Forest
refactor: Преобразование класса TypeReference в абстрактный
#### Благодарности:
Благодарим участников за их вклад, порядок благодарностей не имеет значения
@CHMing
@ifaxin
Отдельное спасибо этим участникам (порядок нумерации не имеет значения).
Этот выпуск является официальным и включает следующие новые возможности и изменения:
ForestConfiguration#isVariableDefined
@Var
ResourceRequestBody
ResourceRequestBodyBuilder
MultipartRequestBody
MultipartRequestBodyBuilder
SupportFormUrlEncoded
DataVariableLifeCycle
в VariableLifeCycle
Body
ForestVariableUndefined.java
в ForestVariableUndefinedException
-修复:#I3B5VH
-修复:#I3B49O
1.5.0-RC6 выпущена, в этом выпуске содержится несколько изменений, которые не совместимы с предыдущими версиями:
(1) Maven зависимость spring-boot-starter-forest
была заменена на forest-spring-boot-starter
.
(2) Пакет, содержащий аннотацию @ForestScan
, был переименован в com.dtflys.forest.springboot
. В коде требуется выполнить импорт пакета заново.
Зависимость в Maven теперь выглядит следующим образом:
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-spring-boot-starter</artifactId>
<version>1.5.0-RC6</version>
</dependency>