-
Notifications
You must be signed in to change notification settings - Fork 121
Spring Boot 4 support #4268 #153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9d7a48f
3624076
ba487b6
027c304
833ac80
1b8a5aa
4304e61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package dev.langchain4j.http.client.spring.restclient; | ||
|
|
||
| import org.springframework.http.client.ClientHttpRequestFactory; | ||
|
|
||
| import java.time.Duration; | ||
|
|
||
| /** | ||
| * Strategy interface for creating a {@link ClientHttpRequestFactory} with the appropriate | ||
| * Spring Boot API, depending on the version available on the classpath. | ||
| * | ||
| * @see SpringBoot4HttpClientSettings | ||
| * @see SpringBoot3HttpClientSettings | ||
| */ | ||
| interface HttpClientSettingsStrategy { | ||
|
|
||
| ClientHttpRequestFactory createRequestFactory(Duration connectTimeout, Duration readTimeout); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package dev.langchain4j.http.client.spring.restclient; | ||
|
|
||
| import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; | ||
| import org.springframework.http.client.ClientHttpRequestFactory; | ||
|
|
||
| import java.lang.reflect.Field; | ||
| import java.lang.reflect.Method; | ||
| import java.time.Duration; | ||
|
|
||
| /** | ||
| * The Spring Boot 3.5+ implementation uses reflection because Spring Boot 4 is no longer included in the langchain4J-spring classpath. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Spring Boot 3.5 will reach end of life in June and projects are encouraged to upgrade (see Support page). I would suggest considering if it's desireable to maintain 3.5 support here, even if not supported upstream. If it is, I would recommend considering publishing separate modules, as it's quite risky and complex to support both major version in the same artifact.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Considering that I see a lot of usage of LC4j-SB modules (and given that they support only 3.x), and it is growing, I think it is worth maintaining it untill we see a considerable drop in usage of these modules. Also, as far as I see, enterprise support of 3.5.x is planned untill 2032.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ThomasVitale @dliubarskyi My idea is to exclude all the Spring artefacts and allow the application client to decide which Spring Boot version to use. To do that, we could use the Maven Maven dependency tree on Langchain4j starter side: Maven dependency tree on the Spring Petclinic side: Spring Petclinic startup log: See complete logs: petclinic-logs.log Existing Spring Boot 3.x applications should not be impacted because the Future Spring Boot 4 applications may have to add the @dliubarskyi if you want to test, I could a go ahead and prepare a commit with the described solution.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dliubarskyi @ThomasVitale I have another idea for avoiding the duplication of all the Maven modules. If we take this approach, we must decide about a naming convention on the <profile>
<id>sb3</id>
<properties>
<langchain4j-spring-prefix>sb3</langchain4j-spring-prefix>
<spring.boot.version>3.5.9</spring.boot.version>
</properties>
</profile>At the top of the Open AI Spring boot starter : <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<name>LangChain4j Spring Boot starter for OpenAI</name>
<version>1.12.0-beta20-${langchain4j-spring-prefix}-SNAPSHOT</version>or <artifactId>langchain4j-open-ai-spring-boot-starter-${langchain4j-spring-prefix}</artifactId>
<name>LangChain4j Spring Boot starter for OpenAI</name>
<version>1.12.0-beta20-SNAPSHOT</version>To avoid git tag conflict during the maven release, the first option is maybe the better. With this second solution, I suppose we could have only 2 duplicated modules for the |
||
| */ | ||
| class SpringBoot3HttpClientSettings implements HttpClientSettingsStrategy { | ||
|
|
||
| private static final Class<?> DEPRECATED_SETTINGS_CLASS; | ||
| private static final Class<?> NEW_SETTINGS_CLASS; | ||
| private static final Field DEFAULTS_FIELD; | ||
| private static final Method WITH_CONNECT_TIMEOUT_METHOD; | ||
| private static final Method WITH_READ_TIMEOUT_METHOD; | ||
| private static final Method ADAPT_METHOD; | ||
| private static final Method BUILD_METHOD; | ||
|
|
||
| static { | ||
| try { | ||
| DEPRECATED_SETTINGS_CLASS = Class.forName("org.springframework.boot.web.client.ClientHttpRequestFactorySettings"); | ||
| NEW_SETTINGS_CLASS = Class.forName("org.springframework.boot.http.client.ClientHttpRequestFactorySettings"); | ||
|
|
||
| DEFAULTS_FIELD = DEPRECATED_SETTINGS_CLASS.getField("DEFAULTS"); | ||
| WITH_CONNECT_TIMEOUT_METHOD = DEPRECATED_SETTINGS_CLASS.getMethod("withConnectTimeout", Duration.class); | ||
| WITH_READ_TIMEOUT_METHOD = DEPRECATED_SETTINGS_CLASS.getMethod("withReadTimeout", Duration.class); | ||
| ADAPT_METHOD = DEPRECATED_SETTINGS_CLASS.getDeclaredMethod("adapt"); | ||
| ADAPT_METHOD.setAccessible(true); | ||
|
|
||
| ClientHttpRequestFactoryBuilder<?> builder = ClientHttpRequestFactoryBuilder.detect(); | ||
| BUILD_METHOD = findBuildMethod(builder.getClass(), NEW_SETTINGS_CLASS); | ||
| BUILD_METHOD.setAccessible(true); | ||
| } catch (ReflectiveOperationException e) { | ||
| throw new IllegalStateException("Failed to initialize SpringBoot3HttpClientSettings", e); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public ClientHttpRequestFactory createRequestFactory(Duration connectTimeout, Duration readTimeout) { | ||
| try { | ||
| Object deprecatedSettings = DEFAULTS_FIELD.get(null); | ||
|
|
||
| if (connectTimeout != null) { | ||
| deprecatedSettings = WITH_CONNECT_TIMEOUT_METHOD.invoke(deprecatedSettings, connectTimeout); | ||
| } | ||
| if (readTimeout != null) { | ||
| deprecatedSettings = WITH_READ_TIMEOUT_METHOD.invoke(deprecatedSettings, readTimeout); | ||
| } | ||
|
|
||
| Object newSettings = ADAPT_METHOD.invoke(deprecatedSettings); | ||
| return (ClientHttpRequestFactory) BUILD_METHOD.invoke(ClientHttpRequestFactoryBuilder.detect(), newSettings); | ||
| } catch (ReflectiveOperationException e) { | ||
| throw new IllegalStateException("Failed to create ClientHttpRequestFactory for Spring Boot 3.x", e); | ||
| } | ||
| } | ||
|
|
||
| private static Method findBuildMethod(Class<?> clazz, Class<?> newSettingsClass) throws NoSuchMethodException { | ||
| for (Method method : clazz.getMethods()) { | ||
| if ("build".equals(method.getName()) | ||
| && method.getParameterCount() == 1 | ||
| && method.getParameterTypes()[0].isAssignableFrom(newSettingsClass)) { | ||
| return method; | ||
| } | ||
| } | ||
| if (clazz.getSuperclass() != null) { | ||
| return findBuildMethod(clazz.getSuperclass(), newSettingsClass); | ||
| } | ||
| throw new NoSuchMethodException("No build method accepting " + newSettingsClass.getName()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package dev.langchain4j.http.client.spring.restclient; | ||
|
|
||
| import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; | ||
| import org.springframework.boot.http.client.HttpClientSettings; | ||
| import org.springframework.http.client.ClientHttpRequestFactory; | ||
|
|
||
| import java.time.Duration; | ||
|
|
||
| class SpringBoot4HttpClientSettings implements HttpClientSettingsStrategy { | ||
|
|
||
| @Override | ||
| public ClientHttpRequestFactory createRequestFactory(Duration connectTimeout, Duration readTimeout) { | ||
| HttpClientSettings settings = HttpClientSettings.defaults(); | ||
| if (connectTimeout != null) { | ||
| settings = settings.withConnectTimeout(connectTimeout); | ||
| } | ||
| if (readTimeout != null) { | ||
| settings = settings.withReadTimeout(readTimeout); | ||
| } | ||
| return ClientHttpRequestFactoryBuilder.detect() | ||
| .build(settings); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package dev.langchain4j.http.client.spring.restclient; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.boot.SpringBootVersion; | ||
| import org.springframework.http.client.ClientHttpRequestFactory; | ||
|
|
||
| import java.time.Duration; | ||
|
|
||
| /** | ||
| * Factory that detects the Spring Boot version on the classpath and delegates | ||
| * {@link ClientHttpRequestFactory} creation to the appropriate strategy. | ||
| */ | ||
| public final class SpringBootHttpClientSettingsHelper { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(SpringBootHttpClientSettingsHelper.class); | ||
|
|
||
| private static final HttpClientSettingsStrategy STRATEGY; | ||
|
|
||
| static { | ||
| String version = SpringBootVersion.getVersion(); | ||
| int majorVersion = Integer.parseInt(version.split("\\.")[0]); | ||
| log.debug("Detected Spring Boot major version {}", majorVersion); | ||
|
|
||
| HttpClientSettingsStrategy detected; | ||
| if (majorVersion >= 4) { | ||
| detected = new SpringBoot4HttpClientSettings(); | ||
| } else { | ||
| detected = new SpringBoot3HttpClientSettings(); | ||
| } | ||
| STRATEGY = detected; | ||
| } | ||
|
|
||
| private SpringBootHttpClientSettingsHelper() { | ||
| } | ||
|
|
||
| public static ClientHttpRequestFactory createClientHttpRequestFactory(Duration connectTimeout, Duration readTimeout) { | ||
| return STRATEGY.createRequestFactory(connectTimeout, readTimeout); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.