Объявлен выпуск версии Forest v1.6.4! В этом выпуске были внесены изменения в интерфейсы обработки сообщений SSE.
Сообщения SSE обычно имеют стандартный многострочный формат name:value
, где каждое сообщение разделено пустыми строками. Пример:
id:1
event:json
data:{"name":"a"}
text:xxx
id:2
event:json
data:{"name":"b"}
text:yyy
Для этого стандарта можно использовать режим с многими строками (MULTI_LINES) или автоматический режим (AUTO).
Forest.get("/sse")
.sse()
.setOnMessage(event -> {
event.id(); // значение поля id сообщения, здесь должно быть 1, 2
event.event(); // json
event.data(); // {"name": "a"}, ...
event.value("text"); // получает значение нестандартного имени поля, например: text, здесь должно быть xxx, yyy
})
.listen(SSELinesMode.MULTI_LINES);
Или используйте режим AUTO
. По умолчанию, если параметр не передается методу listen()
, используется режим AUTO
.
// Режим AUTO автоматически распознает необходимый режим строк
sse.listen(); // по умолчанию режим строки AUTO
Кроме того, существуют различные нестандартные форматы сообщений SSE, такие как каждый JSON-объект на отдельной строке, представляющий собой отдельное сообщение.
{"name":"a"}
{"name":"b"}
{"name":"c"}
Для таких типов сообщений следует использовать режим одной строки (SINGLE_LINE).```java
Forest.get("/sse")
.sse()
.setOnMessage(event -> {
String str = event.value(); // получает строковое значение сообщения
MyUser user = event.value(MyUser.class); // получает значение сообщения и преобразует его в пользовательский тип
})
.listen(SSELinesMode.SINGLE_LINE);
- feat: Добавлена возможность получения объекта body по типу через интерфейс, доступен через `request.body().get(Class)`
- feat: Поддержка указания режима строк сообщений SSE, включая одиночный, множественный и автоматический режим
#### Исправленные проблемы
- fix: Проблема с непредвиденным шифрованием данных body из-за наличия пробелов в части `; charset=utf8` заголовка Content-Type
- fix: Проблема совместимости с предыдущими версиями Forest, связанная с отсутствием сигнатур методов get(url), post(url) и других в новой версии класса Forest, что приводило к ошибкам
EventSource.value(Class<T>)
EventSource.value(TypeReference<T>)
SSEInterceptor.onSSEClose(EventSource)
с параметром EventSource
ForestSSE.setOnClose(Consumer<EventSource>)
с параметром EventSource
EventSource.getValue()
в EventSource.value()
EventSource.getName()
в EventSource.name()
EventSource.getRawData()
в EventSource.rawData()
Версия Forest v1.6.2 выпущена! В этом выпуске были исправлены проблемы, связанные с JsonPath и SSE, а также добавлены новые методы для управления SSE.
ForestSSE.close()
для закрытия слушателяForestSSE.await()
для блокирующего ожидания завершения асинхронного прослушиванияEventSource.close()
Релиз версии Forest v1.6.1! В этом выпуске были исправлены проблемы, связанные с SSE, а также добавлен SSE-интерцептор.
Контроллер ForestSSE
и пользовательский контроллер SSE являются независимыми объектами, которые не являются одиночками, поэтому они не могут быть внедрены в контексте Spring и использоваться для доступа к ресурсам контекста.
Интерфейс SSE-интерцептора SSEInterceptor
, который расширяется от интерцептора Interceptor
, может быть внедрен в контексте Spring и использовать бины этого контекста, но как и обычные интерцепторы, он является одиночкой и не может использовать поля класса для передачи данных между вызовами.
@Component
public class MySSEInterceptor implements SSEInterceptor {
@Override
public void onSuccess(InputStream data, ForestRequest request, ForestResponse response) {
// Аналогично методу onSuccess обычного интерцептора, где data — это поток сообщений SSE; не рекомендуется менять его здесь
}
@Override
public void afterExecute(ForestRequest request, ForestResponse response) {
// Аналогично методу afterExecute обычного интерцептора
}
@Override
public void onSSEOpen(EventSource eventSource) {
// Вызывается при начале прослушивания SSE
}
}
``` @Override
public void onSSEClose(ForestRequest request, ForestResponse response) {
// Вызывается при завершении прослушивания SSE
}
// Слушает сообщение с именем "data"
@SSEDataMessage
public void onData(ForestRequest request, @SSEName String name, @SSEValue String value) {
// Количество параметров в списке параметров не ограничивается, можно произвольно сочетать типы параметров ForestRequest, ForestResponse, EventSource
// name: параметр, помеченный аннотацией @SSEName, передает имя сообщения SSE
// value: параметр, помеченный аннотацией @SSEValue, передает значение сообщения SSE
} // Слушает сообщение с именем "event"
@SSEEventMessage
public void onEvent(EventSource eventSource, @SSEValue String value) {
// Количество параметров в списке параметров не ограничивается, можно произвольно сочетать типы параметров ForestRequest, ForestResponse, EventSource
}
// Через аннотацию @SSEMessage можно указать, какое имя сообщения следует слушать
@SSEMessage("id")
public void onData(EventSource eventSource) {
// Количество параметров в списке параметров не ограничивается, можно произвольно сочетать типы параметров ForestRequest, ForestResponse, EventSource
}
}
Способ привязки аналогичен способу привязки обычного интерцептора:
@Get(url = "/sse", interceptor = MySSEInterceptor.class)
ForestSSE testSSE_withInterceptor();
@SSEName
Forest v1.6.0 версия выпущена! В этом выпуске много новых функций, включая SSE, Jsonpath и цепочное условие.
Основной новостью является поддержка SSE, которая включает два типа интерфейсов SSE: декларативный и программный.
Интерфейс определён следующим образом:
public interface SSEClient {
// Forest SSE контроллер как тип возвращаемого значения SSE интерфейса
@Get("/sse")
ForestSSE testSSE();
// Кастомный SSE контроллер как тип возвращаемого значения SSE интерфейса
@Get("/sse")
MySSEHandler testSSE2();
}
Кастомный SSE контроллер:
public class MySSEHandler extends ForestSSE {
@Override
protected void onOpen(EventSource eventSource) {
// Выполняется при открытии SSE
}
@Override
protected void onClose(ForestRequest request, ForestResponse response) {
// Выполняется при закрытии SSE
}
@SSEDataMessage
public void onHello(@SSEValue String value) {
// Отслеживает событие с названием "data"
// @SSEValue аннотация используется для указания значения события
}
@SSEEventMessage(valueRegex = "\\{.*name.*\\}")
public void onEvent(@SSEValue Contact contact) {
// Отслеживает событие с названием "event"
// И сообщение должно удовлетворять регулярному выражению "\\{.*name.*\\}"
}
}
Вызов интерфейса:
// Вызов интерфейса с ForestSSE в качестве типа возвращаемого значения
sseClient.testSSE().listen();
// Вызов интерфейса с кастомным SSE контроллером в качестве типа возвращаемого значения
sseClient.testSSE2().listen();
```##### Программный SSE интерфейс:
```java
Forest.get("http://localhost:{}/", server.getPort())
.sse() // Указывает запрос как SSE запрос
.setOnOpen(eventSource -> {
// Выполняется при открытии SSE
})
.setOnClose((req, res) -> {
// Выполняется при закрытии SSE
})
.addOnData((eventSource, name, value) -> {
// Выполняется при получении события с названием "data"
})
.addOnEventMatchesPrefix("close", (eventSource, name, value) -> {
// Выполняется при получении события с названием "event" и значением, которое начинается с префикса "close"
eventSource.close(); // Ручное закрытие SSE прослушивания
})
.listen(); // Начало прослушивания
Forest.get("http://localhost:{}", server.getPort())
.addHeader("A", 0)
.cond(b > 100, q -> q.addHeader("B", 100)) // [Если b > 100, то добавить Header B:100]
.cond(c > 200, q -> q.addHeader("C", 100)) // [Если c > 200, то добавить Header C:100]
.ifThen(a > 0, q -> q.addHeader("A", a + 1)) // [Множественные условия] Если a > 0, то добавить Header A:a+1
.elseIfThen(a == 0, q -> q.addHeader("A", 0)) // [Множественные условия] Если a = 0, то добавить Header A:0
.elseIfThen(a == -1, q -> q.addHeader("A", -1)) // [Множественные условия] Если a = -1, то добавить Header A:-1
.elseIfThen(a == -2, q -> q.addHeader("A", -2)) // [Множественные условия] Если a = -2, то добавить Header A:-2
.elseThen(q -> q.addHeader("A", 10)) // [Множественные условия] Иначе добавить Header A:10
.execute();
Декларативные аннотации JsonPath
public interface TestJSONPathClient {
@Get("/test/user")
@JSONPathResult("$.data")
TestUser getSingleUser();
@Get("/test/user")
@JSONPathResult("$.data")
List<TestUser> getListOfUsers();
}
``` @Get("/test/user")
@JSONPathResult("$..data[*].age")
List<Integer> getListOfUserAges();```java
@Test
public void testGetListOfUserAges() {
@Get("/test/user")
@JSONPathResult("$.data[?(@.age > {minAge})].age")
List<Integer> getListOfUserAges(@Var("minAge") int minAge);
}
Программные интерфейсы JsonPath
TestUser user = Forest.get("http://localhost:{}/test/user", server.getPort())
.executeAsResponse()
.getByPath("$.data", TestUser.class);
Forest.get("http://localhost:{}/download/test-img.jpg", server.getPort())
.executeAsStream((in, req, res) -> {
// in — это уже открытый объект InputStream
// здесь нет необходимости вручную открывать и закрывать поток
});
Потоковая обработка больших JSON-данных
List<MyUser> list = Forest.get("http://localhost:{}/user.json", server.getPort())
.executeAsStream((in, req, res) -> {
// Возвращаемое значение из обратного вызова будет использоваться как конечный ответ запроса
return new Gson().fromJson(new JsonReader(new InputStreamReader(in)), MyUser.class);
});
{}
вместо {число}
в качестве местоположителей аргументов@BindingVar
аннотацияonResponse
Optional
как типа возвращаемого значения интерфейсовCompletableFuture
как типа возвращаемого значения интерфейсовLogHandler
, внедренного через Spring Beans- feat: добавление нового интерфейса для безопасной обработки потока ответов#### Исправление проблем
- fix: исправлено проблему с кодировкой процентных символов с помощью URLEncode
- fix: исправлено невозможность переопределения свойства аннотации `@Address`
- fix: исправлено невозможность вызова фильтрационных функций
- fix: исправлено возможную ошибку при закрытии пула соединений
- fix: исправлено ошибку вывода content-type как multipart/form-data, когда body имеет другой тип
- fix: исправлено недействие метода setReturnType в onMethodInitialized (#I8PJ9R)
- fix: исправлено выход за границы стека из-за отсутствия восстановления parentScope (#I9TP9Z)
#### Изменение кода
- update: заменено кэширование Hutool LRU кэшем
- update: обновлен плагин forest-solon-plugin до версии Solon 3.0.1 (совместимость с 2.5.9+)
- update: обновлены зависимости до последней стабильной версии
- refactor: добавлена возможность вывода информации о статусе при ошибке Response: [Network Error]
- opt: добавлена thread-safe поддержка запросов
- reflector: рефакторинг интерфейса ForestAuthenticator
- refactor: рефакторинг логики обработки потока ответов
- add: добавлен метод openStream в ResultGetter
- refactor: изменён формат сообщений об ошибках выражений
#### Вкладчики
- @wittplus (witt)
- @Kiroe (Kiro)
- @noear_admin (Запад)
Forest v1.5.36 версия выпущена! В этом выпуске основное внимание уделено поддержке Fastjson2 и обновлению версий Solon.
response.getContentLength()
возвращает пустое значение (#I90MUX)v2.6.5
v1.5.35
версия выпущена! В этой версии были реализованы возможности конфигурирования размера и времени жизни кеша клиентских объектов в бэкенд.
Настройте максимальный размер кеша и время его истечения.
forest:
backend-client-cache-max-size: 512 # Максимальный размер кеша клиента бэкэнда (в единицах экземпляров; значение по умолчанию — 128)
backend-client-cache-expire-time: 3h # Время истечения кеша клиента бэкэнда (в единицах времени; значение по умолчанию — 6 часов)
версия v1.5.34
выпущена! В этом выпуске исправлена проблема увеличивающегося потребления памяти при множественных запросах к различным доменам, используя кэширование Caffeine как бэкенд для клиентского кэша.
@BaseRequest
или @BaseURL
, где полное URL-адрес метода без указания порта заменяется портом базового адреса вместо использования порта по умолчанию 80@Get()
не могут корректно склеиться, независимо от того, используется ли @Address
или @BaseRequest
(#I7CAYS)/
в конце basePath аннотации @Addrees
и отсутствие /
в начале URL метода, что приводит к двум последовательным слешам в итоговом адресеMultipartRequestBody
(154)refactor: Замена gson на синглтон
refactor: Удаление логов прогресса при скачивании файлов с использованием аннотации @DownloadFile
refactor: Перемещение тестовых случаев для forest-spring-boot3
в forest-test
#### Контрибьюторы
@CHMing
@wittplus
@Moonsets
@noear_admin
Forest v1.5.33 выпущена. В этом выпуске основное внимание уделено поддержке прокси SOCKS и переопределению свойств объединённых аннотаций.
@Body
с массивом параметров некорректно парсится в JSON-массив (#I7UPBR)@JSONBody
, коллекция String codes вызывает ошибку (#I7QLTS)@SocksProxy
@OverrideAttribute
Для использования декларативного прокси SOCKS обратитесь к документации и программной реализации прокси SOCKS.
Для переопределения свойств объединённых аннотаций обратитесь к документации.
в версии v1.5.32 были внесены исправления! Основной акцент сделан на устранение некоторых ошибок.
@Header
(#I7EIAB)opt: Оптимизация параллелизма при инициализации асинхронного пула потоков
Forest v1.5.31 выпущена! Этот выпуск является небольшим обновлением, в котором были исправлены некоторые ошибки.
User-Agent: forest/{версия}
ForestConfiguration
schema
атрибута @Address
как https
(#I6Y6E2)ReflectUtils.getFields
(#I6W9TF)ForestMethod
теперь происходит ленивой загрузкойinterceptor
аннотации запроса принимает только классы, реализующие интерфейс Interceptor
Forest.VERSION
, которое позволяет динамически получать номер версии Forestверсия v1.5.30
была выпущена с существенными изменениями. Теперь она поддерживает и адаптирована к Spring Boot 3 и Solon. В новой версии XML-модуль был вынесен в отдельный модуль, а также были добавлены возможности задержки параметров.
В проекте добавлен модуль forest-spring-boot3-starter
, при интеграции которого следует использовать следующие зависимости:
<!-- Модуль Forest для Spring Boot 3 -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-spring-boot3-starter</artifactId>
<version>1.5.30</version>
</dependency>
<!-- Модуль Forest для Jakarta XML -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-jakarta-xml</artifactId>
<version>1.5.30</version>
</dependency>
Для старых версий Spring Boot зависимость остаётся практически такой же, но XML-модуль теперь требует дополнительной зависимости:
<!-- Модуль Forest для Spring Boot 1 или 2 -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-spring-boot-starter</artifactId>
<version>1.5.30</version>
</dependency>
<!-- Модуль Forest для JAXB -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-jaxb</artifactId>
<version>1.5.30</version>
</dependency>
Подробные примеры можно найти в демонстрационном проекте https://gitee.com/dromara/forest/tree/master/forest-examples
В проекте добавлен модуль forest-solon-plugin
. При интеграции этого модуля следует использовать следующую зависимость:
<!-- Модуль Forest для Solon -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-solon-plugin</artifactId>
<version>1.5.30</version>
</dependency>
```<!-- Одним из JSON-фреймворков может быть Fastjson, Jackson или Gson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<!-- Модуль Forest для JAXB -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-jaxb</artifactId>
<version>1.5.30</version>
</dependency>
Подробные примеры можно найти в демонстрационном проекте [https://gitee.com/dromara/forest/tree/master/forest-examples](https://gitee.com/dromara/forest/tree/master/forest-examples).
#### Об XML-модуле
В этой версии функции сериализации и десериализации XML были перемещены из основного модуля `forest-core` в отдельные модули Forest XML. Выбор конкретного модуля зависит от специфики вашего проекта. Если проект является обычным проектом, не использующим Spring Boot 3 (например, Spring, Spring Boot 1 или 2) с версией JDK ниже 17, то следует использовать зависимость `forest-jaxb`.
```xml
<!-- Поддержка модулей Forest JAXB -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-jaxb</artifactId>
<version>1.5.30</version>
</dependency>
Если ваш проект использует Spring Boot 3 или имеет версию JDK 17 или выше, выберите зависимость forest-jakarta-xml
.
<!-- Поддержка модулей Jakarta XML в Forest -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-jakarta-xml</artifactId>
<version>1.5.30</version>
</dependency>
```#### Опоздание параметров (Параметры lambda)
Существуют ситуации, когда значения параметров заголовков, запросов или тела могут быть получены только непосредственно перед отправкой запроса. Примером может служить ситуация с цифровой подписью (добавление параметра `token` в заголовок, значение которого является результатом шифрования всего тела).- Опоздание параметров заголовка
```java
Forest.post("/test")
.addHeader("Content-Type", "application/json; charset=UTF-8")
// Здесь передается лямбда-выражение, которое будет выполнено за доли секунды до отправки запроса
// Это вычисляет значение, сериализуя весь тело запроса и применяя базовый шифр Base64, затем передает его в заголовок Authorization
.addHeader("Authorization", req -> Base64.encode("Token=" + req.body().encodeToString()))
.addBody("id", "1972664191")
.addBody("name", "XieYu20011008")
.execute();
Forest.post("/test")
.addHeader("Content-Type", "application/json; charset=UTF-8")
.addHeader("_id", "20011008")
.addBody("id", "1972664191")
// Здесь также передается лямбда-выражение, которое будет выполнено за доли секунды до отправки запроса
// Процесс выполнения и принцип работы аналогичны тому, что был описан выше
.addBody("name", req -> "Foo" + req.headerValue("_id"))
.addBody("token", req -> Base64.encode(req.body().encode()))
.execute();
Здесь процесс сериализации тела запроса, вызываемый в лямбда-выражении req.body().encode()
, автоматически исключает само это лямбда-выражение, поэтому нет необходимости беспокоиться о возникновении бесконечной рекурсии.#### Новые возможности
Отдельная благодарность автору Solon (@noear_admin) за поддержку проекта Forest в адаптации к Solon.
версия v1.5.28 выпущена. Это небольшой выпуск, в котором были исправлены некоторые ошибки и добавлена функция обратного вызова onBodyEncode
для промежуточных модулей.
onBodyEncode
для промежуточных модулей (#I4WF5Q)ForestFuture<T>
@HttpClient
и @OkHttp3
, которая была недействительна в версии 1.5.27SpringSSLKeyStore
#RetryWhen
(#I5WEBC)@BaseRequest
не работала для connectTimeout
и readTimeout
(#I5WC6U)@Address
, метод request.basePath()
давал неверный результатверсия v1.5.27
была выпущена. В этой версии были исправлены некоторые ошибки, а также добавлена поддержка корутин Kotlin, усилены API асинхронных запросов и получения ответных данных.
ForestHeaderMap.addCookie
может возникнуть проблема с отсутствием заголовков (100)multipart/form-data
, если параметр @Body
пустой, будет выдано сообщение об ошибке (#I5Y7WJ)ForestRequest.getQueryString()
, если не установлен charset
, будет выдано сообщение об ошибке (#I5RGX4)ForestRequest.getBasePath()
не может получить basePath
, определённый в AddressSource
(#I5RGOY)x-www-form-urlencoded
при URL-кодировании игнорируется символ #
Forest v1.5.26
версия выпущена! В этом выпуске были внесены исправления нескольких ошибок.
5.3.19
2.6.7
Лес v1.5.25
версия выпущена!
Forest v1.5.24 версия выпущена! В этом выпуске были исправлены некоторые ошибки.
Релиз версии Forest v1.5.23! В этом выпуске была проведена глобальная оптимизация производительности выполнения запросов!
Увеличение пропускной способности запросов (QPS) для backend-клиента OkHttp3 более чем в три раза!
Увеличение пропускной способности запросов (QPS) для backend-клиента HttpClient более чем в два раза!
@BackendClient
.fix: Появление NullPointerException при вызове метода ForestResponse.statusIs(xxx) (#I5CWQL)
fix: Утрата действительности базового пути basePath при использовании аннотации @Address (#I5CR15)
fix: Исключение java.lang.IllegalArgumentException: Socket может не быть null при использовании аннотации @HTTPProxy для установки HTTP-прокси для HTTPS-запросов
fix: Невозможность отправки запросов без указания ContentType и BodyType (#I5CML4)
fix: Выполнение метода ForestRequest.addBody(List) один раз вместо каждого цикла#### Изменения кода
refactor: Переработка Cookie (#I5C26U)
refactor: Переработка OkHttpClient
add: Добавление метода ForestRequest#addInterceptor(Class<? extends Interceptor>) для добавления interceptors к запросам
add: Добавление HttpClientFactory
add: Добавление OkHttpClientFactory
Версия 1.5.22
выпущена! В этом выпуске были исправлены проблемы с унифицированным пулом соединений, а также начата поддержка Kotlin, совместимость с версиями Spring 4.x и ниже.
Ранее версии конфигурационные свойства max-connections
и max-route-connections
были настроены для пулов соединений бэкенд-фреймворков OkHttp3 и HttpClient. Это приводило к тому, что запросы от разных бэкендов находились в различных пулах соединений, которые были изолированы друг от друга, и невозможно было установить единое ограничение максимального количества соединений.
Кроме того, ограничение на количество запросов в OkHttp3 находится внутри компонента Dispatcher, который привязывается непосредственно к объекту OkHttpClient, поэтому его сложно настраивать и управлять. Таким образом, свойства max-connections
и max-route-connections
фактически не действуют для OkHttp3.
Новый унифицированный пул соединений позволяет через свойства max-connections
и max-route-connections
управлять максимальным количеством запросов и максимальным количеством запросов на маршрут для всех бэкендов OkHttp и HttpClient, включая асинхронные запросы.##### Определение типа возвращаемого значения
Добавлена аннотация @Return
, которая используется для указания параметра как возвращаемого значения.
/**
* Использует тип Class для определения типа возвращаемого значения
*
* @param clazz Тип возвращаемого значения
* @return экземпляр типа, указанного параметром clazz
* @param <T> неопределённый параметр типа, конкретизируется передачей параметра clazz
*/
@Get("/")
<T> T getGenericClass(@Return Class<T> clazz);
```/**
* Использует тип Type для определения типа возвращаемого значения
*
* @param type Тип возвращаемого значения
* @return Экземпляр типа, указанного параметром type
* @param <T> Непредопределённый параметр типа, конкретизируется передачей параметра type
*/
@Get("/")
<T> T getGenericType(@Return Type type);
/**
* Использует тип TypeReference для определения типа возвращаемого значения
*
* @param typeReference Тип возвращаемого значения
* @return Экземпляр типа, указанного параметром typeReference
* @param <T> Непредопределённый параметр типа, конкретизируется передачей параметра typeReference
*/
@Get("/")
<T> T getGenericTypeReference(@Return TypeReference<T> typeReference);
#
, будет экранироваться, что приведёт к невозможности найти ресурс (#I59O7M)baseURL
в BaseRequest
полный путь запроса с дефолтным портом будет перезаписан, что вызывает ошибку запроса (#I4YBDV)char
и Character
объектами, помеченными аннотацией @Body
, от клиента до сервераRetryWhen
выполняется дважды после последней попытки повтора (#I599BT)#### Другие измененияУчастникам, внесшим свой вклад в этот выпуск
версия v1.5.21
выпущена!
Основные изменения в этой версии направлены на исправление ошибок при парсинге URL в некоторых случаях, а также добавлена поддержка ручной URLEncode для шаблонов строк.