├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── lombok.config ├── okhttp-spring-boot-autoconfigure ├── src │ ├── test │ │ ├── resources │ │ │ └── keystore.jks │ │ └── java │ │ │ └── io │ │ │ └── freefair │ │ │ └── spring │ │ │ └── okhttp │ │ │ ├── DurationPropertiesIT.java │ │ │ ├── metrics │ │ │ └── OkHttpMetricsAutoConfigurationTest.java │ │ │ ├── client │ │ │ └── OkHttpClientRequestFactoryTest.java │ │ │ ├── ssl │ │ │ └── SslBundleTest.java │ │ │ ├── OkHttp3AutoConfigurationIT.java │ │ │ ├── OkHttpRestTemplateAutoConfigurationTest.java │ │ │ ├── OkHttpRestClientAutoConfigurationTest.java │ │ │ ├── logging │ │ │ └── OkHttp3LoggingInterceptorAutoConfigurationTest.java │ │ │ └── OkHttp3AutoConfigurationTest.java │ └── main │ │ ├── java │ │ └── io │ │ │ └── freefair │ │ │ └── spring │ │ │ └── okhttp │ │ │ ├── OkHttp3Configurer.java │ │ │ ├── autoconfigure │ │ │ ├── logging │ │ │ │ ├── OkHttp3LoggingInterceptorProperties.java │ │ │ │ └── OkHttp3LoggingInterceptorAutoConfiguration.java │ │ │ ├── OkHttpClientRequestFactoryBuilderAutoConfiguration.java │ │ │ ├── metrics │ │ │ │ ├── OkHttpMetricsProperties.java │ │ │ │ └── OkHttpMetricsAutoConfiguration.java │ │ │ ├── OkHttpProperties.java │ │ │ ├── OkHttpClientRequestFactoryBuilder.java │ │ │ └── OkHttp3AutoConfiguration.java │ │ │ ├── ApplicationInterceptor.java │ │ │ └── NetworkInterceptor.java │ │ └── resources │ │ └── META-INF │ │ └── spring │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports └── build.gradle ├── okhttp-spring-client ├── src │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── freefair │ │ │ └── spring │ │ │ └── okhttp │ │ │ ├── client │ │ │ ├── package-info.java │ │ │ ├── OkHttpClientRequestFactory.java │ │ │ ├── StreamingBodyRequestBody.java │ │ │ ├── OkHttpClientResponse.java │ │ │ └── OkHttpClientRequest.java │ │ │ ├── OkHttpUtils.java │ │ │ ├── OkHttpResponseResource.java │ │ │ ├── ResourceRequestBody.java │ │ │ └── OkHttpResponseBodyResource.java │ └── test │ │ └── java │ │ └── io │ │ └── freefair │ │ └── spring │ │ └── okhttp │ │ ├── TestServer.java │ │ ├── OkHttpUtilsTest.java │ │ ├── ResourceRequestBodyTest.java │ │ └── OkHttpResponseResourceTest.java └── build.gradle ├── .editorconfig ├── starters ├── okhttp-spring-boot-starter │ └── build.gradle ├── okhttp4-spring-boot-starter │ ├── src │ │ └── test │ │ │ └── java │ │ │ └── io │ │ │ └── freefair │ │ │ └── okhttp │ │ │ └── starter4 │ │ │ ├── OkHttp4TestApplication.java │ │ │ └── OkHttp4Test.java │ └── build.gradle ├── okhttp5-spring-boot-starter │ ├── src │ │ └── test │ │ │ └── java │ │ │ └── io │ │ │ └── freefair │ │ │ └── okhttp │ │ │ └── starter5 │ │ │ ├── OkHttp5TestApplication.java │ │ │ └── OkHttp5Test.java │ └── build.gradle └── build.gradle ├── .github ├── workflows │ ├── gradle-wrapper-validation.yml │ ├── dependency-submission.yml │ ├── gradle-deploy.yml │ ├── gradle.yml │ └── gradle-publish.yml ├── CONTRIBUTING.md └── dependabot.yml ├── settings.gradle ├── okhttp-spring-boot-dependencies └── build.gradle ├── LICENSE ├── README.md ├── .gitignore ├── config └── checkstyle │ └── checkstyle.xml ├── gradlew.bat └── gradlew /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | org.gradle.caching=true 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freefair/okhttp-spring-boot/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | config.stopBubbling = true 2 | lombok.addLombokGeneratedAnnotation = true 3 | lombok.nonnull.exceptiontype=IllegalArgumentException -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/test/resources/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freefair/okhttp-spring-boot/HEAD/okhttp-spring-boot-autoconfigure/src/test/resources/keystore.jks -------------------------------------------------------------------------------- /okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullApi 2 | package io.freefair.spring.okhttp.client; 3 | 4 | import org.springframework.lang.NonNullApi; 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*.{java,gradle}] 7 | charset = utf-8 8 | insert_final_newline = true 9 | indent_style = space 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /starters/okhttp-spring-boot-starter/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "io.freefair.maven-publish-java" 2 | apply plugin: "java-library" 3 | 4 | description = "OkHttp Spring Boot Starter" 5 | 6 | dependencies { 7 | api project(':starters:okhttp4-spring-boot-starter') 8 | } 9 | -------------------------------------------------------------------------------- /starters/okhttp4-spring-boot-starter/src/test/java/io/freefair/okhttp/starter4/OkHttp4TestApplication.java: -------------------------------------------------------------------------------- 1 | package io.freefair.okhttp.starter4; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class OkHttp4TestApplication { 7 | } 8 | -------------------------------------------------------------------------------- /starters/okhttp5-spring-boot-starter/src/test/java/io/freefair/okhttp/starter5/OkHttp5TestApplication.java: -------------------------------------------------------------------------------- 1 | package io.freefair.okhttp.starter5; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class OkHttp5TestApplication { 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper-validation.yml: -------------------------------------------------------------------------------- 1 | name: "Validate Gradle Wrapper" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | validation: 9 | name: "Validation" 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v6 13 | - uses: gradle/actions/wrapper-validation@v5 -------------------------------------------------------------------------------- /starters/build.gradle: -------------------------------------------------------------------------------- 1 | subprojects { Project starterProject -> 2 | starterProject.apply plugin: "java-library" 3 | 4 | starterProject.tasks.named('jar', Jar) { 5 | manifest { 6 | attributes 'Spring-Boot-Jar-Type': 'dependencies-starter' 7 | } 8 | } 9 | 10 | test.useJUnitPlatform() 11 | } 12 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/OkHttp3Configurer.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import okhttp3.OkHttpClient; 4 | 5 | /** 6 | * @author Lars Grefer 7 | */ 8 | @FunctionalInterface 9 | public interface OkHttp3Configurer { 10 | 11 | void configure(OkHttpClient.Builder object); 12 | } 13 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | io.freefair.spring.okhttp.autoconfigure.OkHttp3AutoConfiguration 2 | io.freefair.spring.okhttp.autoconfigure.OkHttpClientRequestFactoryBuilderAutoConfiguration 3 | io.freefair.spring.okhttp.autoconfigure.metrics.OkHttpMetricsAutoConfiguration 4 | io.freefair.spring.okhttp.autoconfigure.logging.OkHttp3LoggingInterceptorAutoConfiguration -------------------------------------------------------------------------------- /.github/workflows/dependency-submission.yml: -------------------------------------------------------------------------------- 1 | name: Dependency Submission 2 | 3 | on: 4 | push: 5 | branches: [ 'main' ] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | dependency-submission: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v6 15 | - uses: actions/setup-java@v5 16 | with: 17 | distribution: temurin 18 | java-version: 17 19 | 20 | - name: Generate and submit dependency graph 21 | uses: gradle/actions/dependency-submission@v5 22 | -------------------------------------------------------------------------------- /starters/okhttp4-spring-boot-starter/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "io.freefair.maven-publish-java" 2 | apply plugin: "java-library" 3 | 4 | description = "OkHttp 4 Spring Boot Starter" 5 | 6 | dependencies { 7 | api platform(project(":okhttp-spring-boot-dependencies")) 8 | 9 | api 'org.springframework.boot:spring-boot-starter' 10 | api project(':okhttp-spring-boot-autoconfigure') 11 | api project(':okhttp-spring-client') 12 | api 'com.squareup.okhttp3:okhttp' 13 | 14 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 15 | } 16 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | ## Branching 4 | 5 | When starting to work on a pull request make sure to start your work on the correct branch. 6 | This repository uses the same branching model as _Spring Boot_ and the _Spring Framework_. 7 | 8 | The `master` branch always contains the latest _bleeding edge_ code, while the older versions live in their own branches. 9 | 10 | __For example:__ If you wan't to fix a bug in Version `1.x.y`, make sure to start your work on the `1.x` branch 11 | and not on the `master` branch. When opening the pull request make also sure, to set `1.x` as _base_. 12 | -------------------------------------------------------------------------------- /okhttp-spring-client/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id "io.freefair.lombok" 4 | id "io.freefair.maven-publish-java" 5 | } 6 | 7 | description = "OkHttp Spring Client" 8 | 9 | dependencies { 10 | implementation platform(project(":okhttp-spring-boot-dependencies")) 11 | 12 | api "com.squareup.okhttp3:okhttp" 13 | api "org.springframework:spring-web" 14 | 15 | testImplementation "org.springframework.boot:spring-boot-starter-test" 16 | testImplementation "org.springframework.boot:spring-boot-starter-web" 17 | } 18 | 19 | tasks.named("test", Test) { 20 | useJUnitPlatform() 21 | } 22 | -------------------------------------------------------------------------------- /starters/okhttp5-spring-boot-starter/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "io.freefair.maven-publish-java" 2 | apply plugin: "java-library" 3 | 4 | description = "OkHttp 5 Spring Boot Starter" 5 | 6 | dependencies { 7 | api platform(project(":okhttp-spring-boot-dependencies")) 8 | api platform('com.squareup.okhttp3:okhttp-bom:5.3.2') 9 | 10 | api 'org.springframework.boot:spring-boot-starter' 11 | api project(':okhttp-spring-boot-autoconfigure') 12 | api project(':okhttp-spring-client') 13 | api 'com.squareup.okhttp3:okhttp' 14 | 15 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 16 | } 17 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/logging/OkHttp3LoggingInterceptorProperties.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.autoconfigure.logging; 2 | 3 | import lombok.Data; 4 | import okhttp3.logging.HttpLoggingInterceptor; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | 7 | /** 8 | * @author Lars Grefer 9 | */ 10 | @Data 11 | @ConfigurationProperties(prefix = "okhttp.logging") 12 | public class OkHttp3LoggingInterceptorProperties { 13 | 14 | /** 15 | * The level at which the HttpLoggingInterceptor logs. 16 | */ 17 | private HttpLoggingInterceptor.Level level = HttpLoggingInterceptor.Level.NONE; 18 | } 19 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/ApplicationInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import org.springframework.beans.factory.annotation.Qualifier; 4 | 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.Target; 7 | 8 | import static java.lang.annotation.ElementType.*; 9 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 10 | 11 | /** 12 | * A {@link Qualifier} annotation for {@link okhttp3.Interceptor OkHttp3-Interceptors}. 13 | * 14 | * @author Lars Grefer 15 | * @see NetworkInterceptor 16 | */ 17 | @Target({METHOD, FIELD, CONSTRUCTOR, TYPE}) 18 | @Retention(RUNTIME) 19 | @Qualifier 20 | public @interface ApplicationInterceptor { 21 | } 22 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/NetworkInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import org.springframework.beans.factory.annotation.Qualifier; 4 | 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.Target; 7 | 8 | import static java.lang.annotation.ElementType.*; 9 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 10 | 11 | /** 12 | * A {@link Qualifier} annotation for {@link okhttp3.Interceptor OkHttp3-Interceptors}. 13 | * 14 | * @author Lars Grefer 15 | * @see ApplicationInterceptor 16 | */ 17 | @SuppressWarnings("WeakerAccess") 18 | @Target({METHOD, FIELD, CONSTRUCTOR, TYPE}) 19 | @Retention(RUNTIME) 20 | @Qualifier 21 | public @interface NetworkInterceptor { 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/gradle-deploy.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Deploy Docs 5 | 6 | on: 7 | push: 8 | branches-ignore: 9 | - 'dependabot**' 10 | 11 | jobs: 12 | deploy-docs: 13 | runs-on: ubuntu-latest 14 | env: 15 | ORG_GRADLE_PROJECT_freefairDocsUser: ${{ secrets.FREEFAIR_DOCS_USER }} 16 | ORG_GRADLE_PROJECT_freefairDocsPass: ${{ secrets.FREEFAIR_DOCS_PASS }} 17 | steps: 18 | - uses: actions/checkout@v6 19 | - name: Set up JDK 17 20 | uses: actions/setup-java@v5 21 | with: 22 | java-version: 17 23 | distribution: 'temurin' 24 | - name: Setup Gradle 25 | uses: gradle/actions/setup-gradle@v5 26 | - run: ./gradlew uploadDocumentation -s 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gradle" 9 | directory: "/" 10 | target-branch: "3.4.x" 11 | schedule: 12 | interval: "daily" 13 | - package-ecosystem: "gradle" 14 | directory: "/" 15 | target-branch: "3.5.x" 16 | schedule: 17 | interval: "daily" 18 | - package-ecosystem: "gradle" 19 | directory: "/" 20 | target-branch: "4.0.x" 21 | schedule: 22 | interval: "daily" 23 | 24 | # Maintain dependencies for GitHub Actions 25 | - package-ecosystem: "github-actions" 26 | directory: "/" 27 | schedule: 28 | interval: "weekly" 29 | -------------------------------------------------------------------------------- /okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequestFactory.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.client; 2 | 3 | import okhttp3.OkHttpClient; 4 | import org.springframework.http.HttpMethod; 5 | import org.springframework.http.client.ClientHttpRequest; 6 | import org.springframework.http.client.ClientHttpRequestFactory; 7 | import org.springframework.lang.NonNull; 8 | 9 | import java.net.URI; 10 | 11 | /** 12 | * OkHttp based {@link ClientHttpRequestFactory} implementation. 13 | *

14 | * Serves as replacement for the deprecated {@link org.springframework.http.client.OkHttp3ClientHttpRequestFactory}. 15 | * 16 | * @author Lars Grefer 17 | */ 18 | public record OkHttpClientRequestFactory(@NonNull OkHttpClient okHttpClient) implements ClientHttpRequestFactory { 19 | 20 | @Override 21 | public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) { 22 | return new OkHttpClientRequest(okHttpClient, uri, httpMethod); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | - push 8 | - pull_request 9 | 10 | jobs: 11 | build: 12 | name: build 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | java: [ 17 ] 18 | steps: 19 | - uses: actions/checkout@v6 20 | 21 | - name: Set up JDK ${{ matrix.java }} 22 | uses: actions/setup-java@v5 23 | with: 24 | java-version: ${{ matrix.java }} 25 | distribution: 'temurin' 26 | 27 | - name: Setup Gradle 28 | uses: gradle/actions/setup-gradle@v5 29 | 30 | - run: ./gradlew -V assemble -s 31 | - run: ./gradlew -V check -s 32 | - name: Upload Coverage Reports 33 | run: | 34 | ./gradlew jacocoTestReport 35 | bash <(curl -s https://codecov.io/bash) 36 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven { url = 'https://repo.spring.io/milestone' } 5 | } 6 | } 7 | 8 | plugins { 9 | id "io.freefair.settings.plugin-versions" version "9.1.0" 10 | id "com.gradle.develocity" version "4.3" 11 | } 12 | 13 | boolean isCiServer = System.getenv().containsKey("CI") 14 | 15 | develocity { 16 | buildScan { 17 | publishing.onlyIf { isCiServer } 18 | termsOfUseUrl = "https://gradle.com/help/legal-terms-of-use" 19 | termsOfUseAgree = "yes" 20 | } 21 | } 22 | 23 | dependencyResolutionManagement { 24 | repositories { 25 | mavenCentral() 26 | maven { url = 'https://repo.spring.io/milestone' } 27 | } 28 | } 29 | 30 | include 'okhttp-spring-client' 31 | 32 | include 'okhttp-spring-boot-dependencies' 33 | include 'okhttp-spring-boot-autoconfigure' 34 | include 'starters:okhttp-spring-boot-starter' 35 | include 'starters:okhttp4-spring-boot-starter' 36 | include 'starters:okhttp5-spring-boot-starter' 37 | 38 | rootProject.name = 'okhttp-spring-boot' 39 | -------------------------------------------------------------------------------- /okhttp-spring-boot-dependencies/build.gradle: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.plugin.SpringBootPlugin 2 | 3 | plugins { 4 | id "java-platform" 5 | id "maven-publish" 6 | } 7 | 8 | javaPlatform { 9 | allowDependencies() 10 | } 11 | 12 | dependencies { 13 | api platform(SpringBootPlugin.BOM_COORDINATES) 14 | api platform('com.squareup.okhttp3:okhttp-bom:4.12.0') 15 | 16 | constraints { 17 | api project(":okhttp-spring-client") 18 | api project(":okhttp-spring-boot-autoconfigure") 19 | api project(":starters:okhttp-spring-boot-starter") 20 | api project(":starters:okhttp4-spring-boot-starter") 21 | api project(":starters:okhttp5-spring-boot-starter") 22 | } 23 | } 24 | 25 | publishing { 26 | publications { 27 | bom(MavenPublication) { 28 | from components.javaPlatform 29 | 30 | pom { 31 | name = 'OkHttp Spring Boot Dependencies' 32 | description = 'OkHttp Spring Boot Dependencies' 33 | } 34 | 35 | signing.sign it 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /okhttp-spring-client/src/test/java/io/freefair/spring/okhttp/TestServer.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.util.FileCopyUtils; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import java.io.IOException; 11 | 12 | @SpringBootApplication 13 | @RestController 14 | public class TestServer { 15 | 16 | @RequestMapping("/echo") 17 | public void echo(HttpServletRequest request, HttpServletResponse response) throws IOException { 18 | 19 | request.getHeaderNames().asIterator().forEachRemaining(header -> { 20 | request.getHeaders(header).asIterator().forEachRemaining(value -> { 21 | response.addHeader(header, value); 22 | }); 23 | }); 24 | 25 | FileCopyUtils.copy(request.getInputStream(), response.getOutputStream()); 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2023 Lars Grefer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/OkHttpUtils.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import kotlin.Pair; 4 | import lombok.experimental.UtilityClass; 5 | import okhttp3.Headers; 6 | import org.springframework.http.HttpHeaders; 7 | import org.springframework.util.Assert; 8 | 9 | @UtilityClass 10 | public class OkHttpUtils { 11 | 12 | public static HttpHeaders toSpringHeaders(Headers okhttpHeaders) { 13 | Assert.notNull(okhttpHeaders, "Headers must not be null"); 14 | HttpHeaders springHeaders = new HttpHeaders(); 15 | 16 | for (Pair okhttpHeader : okhttpHeaders) { 17 | springHeaders.add(okhttpHeader.getFirst(), okhttpHeader.getSecond()); 18 | } 19 | 20 | return springHeaders; 21 | } 22 | 23 | public static Headers toOkHttpHeaders(HttpHeaders springHeaders) { 24 | Headers.Builder builder = new Headers.Builder(); 25 | 26 | springHeaders.forEach((name, values) -> { 27 | values.forEach(value -> builder.add(name, value)); 28 | }); 29 | 30 | return builder.build(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot Starters for [OkHttp](http://square.github.io/okhttp/) 2 | 3 | ![Maven Central](https://img.shields.io/maven-central/v/io.freefair.okhttp-spring-boot/okhttp-spring-boot-starter.svg) 4 | [![GitHub license](https://img.shields.io/github/license/freefair/okhttp-spring-boot.svg)](https://github.com/freefair/okhttp-spring-boot/blob/master/LICENSE) 5 | [![codecov](https://codecov.io/gh/freefair/okhttp-spring-boot/branch/master/graph/badge.svg)](https://codecov.io/gh/freefair/okhttp-spring-boot) 6 | [![Build Status](https://travis-ci.com/freefair/okhttp-spring-boot.svg?branch=master)](https://travis-ci.com/freefair/okhttp-spring-boot) 7 | 8 | ## Available Starters 9 | 10 | - `okhttp4-spring-boot-starter` 11 | - `okhttp5-spring-boot-starter` 12 | - `okhttp-spring-boot-starter` (Alias for `okhttp4-spring-boot-starter`) 13 | 14 | ## Using Gradle 15 | ```gradle 16 | dependencies { 17 | compile "io.freefair.okhttp-spring-boot:okhttp-spring-boot-starter:$version" 18 | } 19 | ``` 20 | ## Using Maven 21 | ```xml 22 | 23 | io.freefair.okhttp-spring-boot 24 | okhttp-spring-boot-starter 25 | ${version} 26 | 27 | ``` 28 | -------------------------------------------------------------------------------- /.github/workflows/gradle-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v6 12 | - name: Set up JDK 17 13 | uses: actions/setup-java@v5 14 | with: 15 | java-version: 17 16 | distribution: 'temurin' 17 | - name: Setup Gradle 18 | uses: gradle/actions/setup-gradle@v5 19 | - name: Publish local 20 | run: ./gradlew publish 21 | env: 22 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.FREEFAIR_SIGNING_KEY }} 23 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.FREEFAIR_SIGNING_PASSWORD }} 24 | - name: Publish package 25 | run: ./gradlew --no-parallel uploadDocumentation jreleaserDeploy 26 | env: 27 | JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.MAVENCENTRAL_USERNAME }} 28 | JRELEASER_MAVENCENTRAL_TOKEN: ${{ secrets.MAVENCENTRAL_PASSWORD }} 29 | JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | ORG_GRADLE_PROJECT_freefairDocsUser: ${{ secrets.FREEFAIR_DOCS_USER }} 31 | ORG_GRADLE_PROJECT_freefairDocsPass: ${{ secrets.FREEFAIR_DOCS_PASS }} 32 | -------------------------------------------------------------------------------- /starters/okhttp4-spring-boot-starter/src/test/java/io/freefair/okhttp/starter4/OkHttp4Test.java: -------------------------------------------------------------------------------- 1 | package io.freefair.okhttp.starter4; 2 | 3 | import okhttp3.OkHttpClient; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.boot.test.context.assertj.AssertableApplicationContext; 8 | import org.springframework.context.ConfigurableApplicationContext; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) 13 | public class OkHttp4Test { 14 | 15 | @Autowired 16 | private ConfigurableApplicationContext applicationContext; 17 | 18 | @Test 19 | void contextLoads() { 20 | assertThat(applicationContext).isNotNull(); 21 | } 22 | 23 | @Test 24 | void hasClientBean() { 25 | AssertableApplicationContext context = AssertableApplicationContext.get(() -> applicationContext); 26 | 27 | assertThat(context).hasSingleBean(OkHttpClient.class); 28 | OkHttpClient okHttpClient = context.getBean(OkHttpClient.class); 29 | 30 | assertThat(okHttpClient).isNotNull(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /starters/okhttp5-spring-boot-starter/src/test/java/io/freefair/okhttp/starter5/OkHttp5Test.java: -------------------------------------------------------------------------------- 1 | package io.freefair.okhttp.starter5; 2 | 3 | import okhttp3.OkHttpClient; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.boot.test.context.assertj.AssertableApplicationContext; 8 | import org.springframework.context.ConfigurableApplicationContext; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) 13 | public class OkHttp5Test { 14 | 15 | @Autowired 16 | private ConfigurableApplicationContext applicationContext; 17 | 18 | @Test 19 | void contextLoads() { 20 | assertThat(applicationContext).isNotNull(); 21 | } 22 | 23 | @Test 24 | void hasClientBean() { 25 | AssertableApplicationContext context = AssertableApplicationContext.get(() -> applicationContext); 26 | 27 | assertThat(context).hasSingleBean(OkHttpClient.class); 28 | OkHttpClient okHttpClient = context.getBean(OkHttpClient.class); 29 | 30 | assertThat(okHttpClient).isNotNull(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /okhttp-spring-client/src/test/java/io/freefair/spring/okhttp/OkHttpUtilsTest.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import okhttp3.Headers; 4 | import org.assertj.core.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.http.HttpHeaders; 7 | 8 | import java.nio.charset.StandardCharsets; 9 | import java.time.Instant; 10 | import java.time.temporal.ChronoUnit; 11 | import java.util.List; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | class OkHttpUtilsTest { 16 | 17 | 18 | @Test 19 | void headerConversion() { 20 | 21 | HttpHeaders springHeaders = new HttpHeaders(); 22 | Instant date = Instant.now(); 23 | 24 | springHeaders.set("key1", "value1"); 25 | springHeaders.setAcceptCharset(List.of(StandardCharsets.UTF_8)); 26 | springHeaders.setDate(date); 27 | 28 | Headers okHttpHeaders = OkHttpUtils.toOkHttpHeaders(springHeaders); 29 | 30 | assertThat(okHttpHeaders.get("key1")).isEqualTo("value1"); 31 | assertThat(okHttpHeaders.getInstant("Date")).isCloseTo(date, Assertions.within(1, ChronoUnit.SECONDS)); 32 | assertThat(okHttpHeaders.get("Accept-Charset")).isEqualToIgnoringCase("UTF-8"); 33 | 34 | assertThat(OkHttpUtils.toSpringHeaders(okHttpHeaders)).isEqualTo(springHeaders); 35 | 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /okhttp-spring-client/src/test/java/io/freefair/spring/okhttp/ResourceRequestBodyTest.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import okhttp3.MediaType; 4 | import okio.ByteString; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.core.io.ByteArrayResource; 7 | import org.springframework.util.MimeType; 8 | import org.springframework.util.MimeTypeUtils; 9 | 10 | import java.nio.charset.StandardCharsets; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | 15 | class ResourceRequestBodyTest { 16 | 17 | 18 | @Test 19 | void testContentTypes() { 20 | 21 | ByteArrayResource resource = new ByteArrayResource(ByteString.encodeUtf8("hello world").toByteArray()); 22 | 23 | assertThat(new ResourceRequestBody(resource).contentType()) 24 | .isNull(); 25 | 26 | assertThat(new ResourceRequestBody(resource, MimeTypeUtils.IMAGE_PNG).contentType().toString()) 27 | .isEqualTo("image/png"); 28 | 29 | assertThat(new ResourceRequestBody(resource, new MimeType("application", "foo", StandardCharsets.UTF_16)).contentType().toString()) 30 | .isEqualTo("application/foo;charset=UTF-16"); 31 | 32 | assertThat(new ResourceRequestBody(resource, MediaType.parse("text/plain")).contentType().toString()) 33 | .isEqualTo("text/plain"); 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/StreamingBodyRequestBody.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.client; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import okhttp3.MediaType; 5 | import okhttp3.RequestBody; 6 | import okio.BufferedSink; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.springframework.http.StreamingHttpOutputMessage; 9 | import org.springframework.lang.Nullable; 10 | 11 | import java.io.IOException; 12 | 13 | /** 14 | * {@link StreamingHttpOutputMessage.Body} based {@link RequestBody} implementation. 15 | * 16 | * @author Lars Grefer 17 | * @see OkHttpClientRequest 18 | */ 19 | @RequiredArgsConstructor 20 | class StreamingBodyRequestBody extends RequestBody { 21 | 22 | private final StreamingHttpOutputMessage.Body streamingBody; 23 | 24 | private final MediaType contentType; 25 | 26 | @Nullable 27 | private final long contentLength; 28 | 29 | @Nullable 30 | @Override 31 | public MediaType contentType() { 32 | return contentType; 33 | } 34 | 35 | @Override 36 | public long contentLength() { 37 | return contentLength; 38 | } 39 | 40 | @Override 41 | public void writeTo(@NotNull BufferedSink bufferedSink) throws IOException { 42 | streamingBody.writeTo(bufferedSink.outputStream()); 43 | } 44 | 45 | @Override 46 | public boolean isOneShot() { 47 | return !streamingBody.repeatable(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/OkHttpClientRequestFactoryBuilderAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.autoconfigure; 2 | 3 | import io.freefair.spring.okhttp.client.OkHttpClientRequestFactory; 4 | import okhttp3.OkHttpClient; 5 | import org.springframework.boot.autoconfigure.AutoConfiguration; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 8 | import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; 9 | import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; 10 | import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; 11 | import org.springframework.context.annotation.Bean; 12 | 13 | /** 14 | * @author Lars Grefer 15 | * @see RestTemplateAutoConfiguration 16 | */ 17 | @AutoConfiguration(before = RestTemplateAutoConfiguration.class, after = HttpMessageConvertersAutoConfiguration.class) 18 | @ConditionalOnClass({ClientHttpRequestFactoryBuilder.class, OkHttpClientRequestFactory.class}) 19 | public class OkHttpClientRequestFactoryBuilderAutoConfiguration { 20 | 21 | @Bean 22 | @ConditionalOnMissingBean(ClientHttpRequestFactoryBuilder.class) 23 | public OkHttpClientRequestFactoryBuilder okHttpClientRequestFactoryBuilder(OkHttpClient okHttpClient) { 24 | return new OkHttpClientRequestFactoryBuilder(okHttpClient); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/test/java/io/freefair/spring/okhttp/DurationPropertiesIT.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import io.freefair.spring.okhttp.autoconfigure.OkHttpProperties; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.SpringBootConfiguration; 7 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | 10 | import java.time.Duration; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | 15 | /** 16 | * @author Lars Grefer 17 | */ 18 | @SpringBootTest( 19 | properties = { 20 | "okhttp.connectTimeout=10s", 21 | "okhttp.readTimeout=1d", 22 | "okhttp.writeTimeout=5" 23 | } 24 | ) 25 | public class DurationPropertiesIT { 26 | 27 | @Autowired 28 | private OkHttpProperties okHttpProperties; 29 | 30 | @Test 31 | public void getConnectTimeout() { 32 | assertThat(okHttpProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(10)); 33 | } 34 | 35 | @Test 36 | public void getReadTimeout() { 37 | assertThat(okHttpProperties.getReadTimeout()).isEqualTo(Duration.ofDays(1)); 38 | } 39 | 40 | @Test 41 | public void getWriteTimeout() { 42 | assertThat(okHttpProperties.getWriteTimeout()).isEqualTo(Duration.ofMillis(5)); 43 | } 44 | 45 | @SpringBootConfiguration 46 | @EnableConfigurationProperties(OkHttpProperties.class) 47 | static class Config { 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/test/java/io/freefair/spring/okhttp/metrics/OkHttpMetricsAutoConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.metrics; 2 | 3 | import io.freefair.spring.okhttp.autoconfigure.metrics.OkHttpMetricsAutoConfiguration; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import okhttp3.EventListener; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; 9 | import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; 10 | import org.springframework.boot.autoconfigure.AutoConfigurations; 11 | import org.springframework.boot.test.context.runner.ApplicationContextRunner; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | class OkHttpMetricsAutoConfigurationTest { 16 | 17 | private ApplicationContextRunner applicationContextRunner; 18 | 19 | @BeforeEach 20 | public void setUp() { 21 | applicationContextRunner = new ApplicationContextRunner() 22 | .withConfiguration(AutoConfigurations.of( 23 | OkHttpMetricsAutoConfiguration.class, 24 | MetricsAutoConfiguration.class, 25 | CompositeMeterRegistryAutoConfiguration.class 26 | )); 27 | } 28 | 29 | @Test 30 | public void testEventListener() { 31 | applicationContextRunner.run(context -> { 32 | assertThat(context).hasSingleBean(MeterRegistry.class); 33 | assertThat(context).hasSingleBean(EventListener.class); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/build.gradle: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.plugin.SpringBootPlugin 2 | 3 | plugins { 4 | id "checkstyle" 5 | id "jacoco" 6 | id "io.freefair.lombok" 7 | id "java-library" 8 | id "io.freefair.maven-optional" 9 | id "io.freefair.maven-publish-java" 10 | } 11 | 12 | description = "OkHttp Spring Boot AutoConfigure" 13 | 14 | tasks.named('compileJava') { 15 | inputs.files(processResources) 16 | } 17 | 18 | tasks.named('jacocoTestReport', JacocoReport) { 19 | reports.xml.required = true 20 | } 21 | 22 | dependencies { 23 | api platform(SpringBootPlugin.BOM_COORDINATES) 24 | annotationProcessor platform(SpringBootPlugin.BOM_COORDINATES) 25 | 26 | annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' 27 | annotationProcessor 'org.springframework.boot:spring-boot-autoconfigure-processor' 28 | 29 | api 'org.springframework.boot:spring-boot-autoconfigure' 30 | implementation 'org.slf4j:slf4j-api' 31 | 32 | optional project(':okhttp-spring-client') 33 | optional 'org.springframework:spring-web' 34 | optional 'org.springframework.boot:spring-boot-starter-actuator' 35 | 36 | optional platform('com.squareup.okhttp3:okhttp-bom:4.12.0') 37 | optional 'com.squareup.okhttp3:okhttp' 38 | optional 'com.squareup.okhttp3:logging-interceptor' 39 | 40 | testImplementation 'org.springframework.boot:spring-boot-starter-web' 41 | testImplementation('org.springframework.boot:spring-boot-starter-test') { 42 | exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' 43 | } 44 | } 45 | 46 | tasks.named('test', Test) { 47 | useJUnitPlatform() 48 | } 49 | -------------------------------------------------------------------------------- /okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/OkHttpResponseResource.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import okhttp3.Response; 4 | import org.springframework.http.ContentDisposition; 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.http.HttpMessage; 7 | import org.springframework.lang.NonNull; 8 | import org.springframework.lang.Nullable; 9 | import org.springframework.util.StringUtils; 10 | 11 | import java.net.URI; 12 | import java.net.URL; 13 | 14 | public class OkHttpResponseResource extends OkHttpResponseBodyResource implements HttpMessage { 15 | 16 | private final Response response; 17 | 18 | public OkHttpResponseResource(Response response) { 19 | super(response.body()); 20 | this.response = response; 21 | } 22 | 23 | @Override 24 | @NonNull 25 | public String getDescription() { 26 | return "OkHttpResponse [ " + response + " ]"; 27 | } 28 | 29 | @Override 30 | @Nullable 31 | public String getFilename() { 32 | 33 | String contentDisposition = response.header("Content-Disposition"); 34 | if (StringUtils.hasText(contentDisposition)) { 35 | return ContentDisposition.parse(contentDisposition).getFilename(); 36 | } 37 | 38 | return super.getFilename(); 39 | } 40 | 41 | @Override 42 | @NonNull 43 | public URL getURL() { 44 | return response.request().url().url(); 45 | } 46 | 47 | @Override 48 | @NonNull 49 | public URI getURI() { 50 | return response.request().url().uri(); 51 | } 52 | 53 | @Override 54 | @NonNull 55 | public HttpHeaders getHeaders() { 56 | return OkHttpUtils.toSpringHeaders(response.headers()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | secring.gpg 3 | 4 | 5 | # Created by https://www.gitignore.io/api/intellij,gradle 6 | 7 | ### Intellij ### 8 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 9 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 10 | 11 | # User-specific stuff: 12 | .idea/workspace.xml 13 | .idea/tasks.xml 14 | 15 | # Sensitive or high-churn files: 16 | .idea/dataSources/ 17 | .idea/dataSources.ids 18 | .idea/dataSources.xml 19 | .idea/dataSources.local.xml 20 | .idea/sqlDataSources.xml 21 | .idea/dynamic.xml 22 | .idea/uiDesigner.xml 23 | 24 | # Gradle: 25 | .idea/gradle.xml 26 | .idea/libraries 27 | 28 | # Mongo Explorer plugin: 29 | .idea/mongoSettings.xml 30 | 31 | ## File-based project format: 32 | *.iws 33 | 34 | ## Plugin-specific files: 35 | 36 | # IntelliJ 37 | /out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Crashlytics plugin (for Android Studio and IntelliJ) 46 | com_crashlytics_export_strings.xml 47 | crashlytics.properties 48 | crashlytics-build.properties 49 | fabric.properties 50 | 51 | ### Intellij Patch ### 52 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 53 | 54 | # *.iml 55 | # modules.xml 56 | # .idea/misc.xml 57 | # *.ipr 58 | 59 | 60 | ### Gradle ### 61 | .gradle 62 | build/ 63 | 64 | # Ignore Gradle GUI config 65 | gradle-app.setting 66 | 67 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 68 | !gradle-wrapper.jar 69 | 70 | # Cache of project 71 | .gradletasknamecache 72 | 73 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 74 | # gradle/wrapper/gradle-wrapper.properties 75 | 76 | # End of https://www.gitignore.io/api/intellij,gradle 77 | -------------------------------------------------------------------------------- /okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientResponse.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.client; 2 | 3 | import io.freefair.spring.okhttp.OkHttpUtils; 4 | import lombok.RequiredArgsConstructor; 5 | import okhttp3.Response; 6 | import okhttp3.ResponseBody; 7 | import org.springframework.http.HttpHeaders; 8 | import org.springframework.http.HttpStatusCode; 9 | import org.springframework.http.client.ClientHttpResponse; 10 | 11 | import java.io.InputStream; 12 | 13 | /** 14 | * OkHttp based {@link ClientHttpResponse} implementation. 15 | * 16 | * @author Lars Grefer 17 | * @see OkHttpClientRequest 18 | */ 19 | @RequiredArgsConstructor 20 | public class OkHttpClientResponse implements ClientHttpResponse { 21 | 22 | private final Response okHttpResponse; 23 | 24 | private HttpHeaders springHeaders; 25 | 26 | @Override 27 | public HttpStatusCode getStatusCode() { 28 | return HttpStatusCode.valueOf(okHttpResponse.code()); 29 | } 30 | 31 | @Override 32 | public String getStatusText() { 33 | return okHttpResponse.message(); 34 | } 35 | 36 | @Override 37 | public void close() { 38 | ResponseBody body = okHttpResponse.body(); 39 | if (body != null) { 40 | body.close(); 41 | } 42 | } 43 | 44 | @Override 45 | public InputStream getBody() { 46 | ResponseBody body = okHttpResponse.body(); 47 | if (body != null) { 48 | return body.byteStream(); 49 | } else { 50 | return InputStream.nullInputStream(); 51 | } 52 | } 53 | 54 | @Override 55 | public HttpHeaders getHeaders() { 56 | if (springHeaders == null) { 57 | springHeaders = OkHttpUtils.toSpringHeaders(okHttpResponse.headers()); 58 | } 59 | 60 | return springHeaders; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/logging/OkHttp3LoggingInterceptorAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.autoconfigure.logging; 2 | 3 | import io.freefair.spring.okhttp.ApplicationInterceptor; 4 | import io.freefair.spring.okhttp.autoconfigure.OkHttp3AutoConfiguration; 5 | import okhttp3.logging.HttpLoggingInterceptor; 6 | import org.springframework.beans.factory.ObjectProvider; 7 | import org.springframework.boot.autoconfigure.AutoConfiguration; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 11 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 12 | import org.springframework.context.annotation.Bean; 13 | 14 | /** 15 | * @author Lars Grefer 16 | */ 17 | @AutoConfiguration(before = OkHttp3AutoConfiguration.class) 18 | @ConditionalOnClass(HttpLoggingInterceptor.class) 19 | @EnableConfigurationProperties(OkHttp3LoggingInterceptorProperties.class) 20 | public class OkHttp3LoggingInterceptorAutoConfiguration { 21 | 22 | @Bean 23 | @ApplicationInterceptor 24 | @ConditionalOnMissingBean 25 | @ConditionalOnProperty(value = "okhttp.logging.enabled", havingValue = "true", matchIfMissing = true) 26 | public HttpLoggingInterceptor okHttp3LoggingInterceptor( 27 | OkHttp3LoggingInterceptorProperties properties, 28 | ObjectProvider logger 29 | ) { 30 | HttpLoggingInterceptor.Logger actualLogger = logger.getIfUnique(() -> HttpLoggingInterceptor.Logger.DEFAULT); 31 | 32 | HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(actualLogger); 33 | 34 | httpLoggingInterceptor.setLevel(properties.getLevel()); 35 | 36 | return httpLoggingInterceptor; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /okhttp-spring-client/src/test/java/io/freefair/spring/okhttp/OkHttpResponseResourceTest.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import io.freefair.spring.okhttp.OkHttpResponseResource; 4 | import okhttp3.*; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.boot.test.web.server.LocalServerPort; 9 | import org.springframework.core.io.Resource; 10 | import org.springframework.http.ContentDisposition; 11 | 12 | import java.io.IOException; 13 | import java.nio.charset.StandardCharsets; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 18 | class OkHttpResponseResourceTest { 19 | 20 | @LocalServerPort 21 | private int port; 22 | 23 | private OkHttpClient client; 24 | 25 | @BeforeEach 26 | public void setUp() { 27 | client = new OkHttpClient.Builder().build(); 28 | } 29 | 30 | @Test 31 | void testResource() throws IOException { 32 | 33 | Request request = new Request.Builder().url("http://localhost:" + port + "/echo") 34 | .post(RequestBody.create("Hello World", MediaType.parse("text/plain"))) 35 | .addHeader("Content-Disposition", ContentDisposition.attachment().filename("foo.txt").build().toString()) 36 | .build(); 37 | 38 | try (Response execute = client.newCall(request).execute()){ 39 | 40 | Resource resource = new OkHttpResponseResource(execute); 41 | 42 | assertThat(resource.isFile()).isFalse(); 43 | assertThat(resource.exists()).isTrue(); 44 | assertThat(resource.getFilename()).isEqualTo("foo.txt"); 45 | assertThat(resource.contentLength()).isEqualTo(11); 46 | assertThat(resource.getContentAsString(StandardCharsets.UTF_8)).isEqualTo("Hello World"); 47 | 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/metrics/OkHttpMetricsProperties.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.autoconfigure.metrics; 2 | 3 | import io.micrometer.core.instrument.binder.okhttp3.OkHttpConnectionPoolMetrics; 4 | import io.micrometer.core.instrument.binder.okhttp3.OkHttpMetricsEventListener; 5 | import lombok.Data; 6 | import okhttp3.Request; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.boot.context.properties.NestedConfigurationProperty; 9 | 10 | import java.util.ArrayList; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | /** 16 | * @author Lars Grefer 17 | * @see OkHttpMetricsEventListener 18 | * @see OkHttpMetricsAutoConfiguration 19 | */ 20 | @Data 21 | @ConfigurationProperties("okhttp.metrics") 22 | public class OkHttpMetricsProperties { 23 | 24 | private boolean enabled = true; 25 | 26 | /** 27 | * Name for the metrics. 28 | */ 29 | private String name = "okhttp"; 30 | 31 | /** 32 | * Whether to include the {@code host} tag. 33 | * 34 | * @see OkHttpMetricsEventListener.Builder#includeHostTag(boolean) 35 | */ 36 | private boolean includeHostTag = true; 37 | 38 | /** 39 | * Tag keys for {@link Request#tag()} or {@link Request#tag(Class)}. 40 | * 41 | * @see OkHttpMetricsEventListener.Builder#requestTagKeys(Iterable) 42 | */ 43 | private List requestTagKeys = new ArrayList<>(); 44 | 45 | /** 46 | * @see OkHttpMetricsEventListener.Builder#tags(Iterable) 47 | */ 48 | private Map tags = new HashMap<>(); 49 | 50 | @NestedConfigurationProperty 51 | private final ConnectionPoolMetricsProperties pool = new ConnectionPoolMetricsProperties(); 52 | 53 | /** 54 | * @see OkHttpConnectionPoolMetrics 55 | */ 56 | @Data 57 | public static class ConnectionPoolMetricsProperties { 58 | 59 | private boolean enabled = true; 60 | 61 | /** 62 | * @see OkHttpConnectionPoolMetrics#namePrefix 63 | */ 64 | private String namePrefix = "okhttp.pool"; 65 | 66 | /** 67 | * @see OkHttpConnectionPoolMetrics#tags 68 | */ 69 | private Map tags = new HashMap<>(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/test/java/io/freefair/spring/okhttp/client/OkHttpClientRequestFactoryTest.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.client; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.SpringBootConfiguration; 7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.test.web.server.LocalServerPort; 10 | import org.springframework.boot.web.client.RestTemplateBuilder; 11 | import org.springframework.http.HttpHeaders; 12 | import org.springframework.web.bind.annotation.*; 13 | import org.springframework.web.client.RestTemplate; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 18 | class OkHttpClientRequestFactoryTest { 19 | 20 | RestTemplate restTemplate; 21 | 22 | @Autowired 23 | RestTemplateBuilder restTemplateBuilder; 24 | 25 | @LocalServerPort 26 | private int port; 27 | 28 | @BeforeEach 29 | void setUp() { 30 | restTemplate = restTemplateBuilder 31 | .rootUri("http://localhost:" + port) 32 | .build(); 33 | } 34 | 35 | @Test 36 | void get() { 37 | String response = restTemplate.getForObject("/user-agent", String.class); 38 | 39 | assertThat(response).contains("okhttp"); 40 | } 41 | 42 | @Test 43 | void put() { 44 | restTemplate.put("/put", "foo"); 45 | } 46 | 47 | @Test 48 | void post() { 49 | String response = restTemplate.postForObject("/post", "foobar", String.class); 50 | 51 | assertThat(response).contains("foobar"); 52 | } 53 | 54 | @SpringBootConfiguration 55 | @EnableAutoConfiguration 56 | @RestController 57 | static class Config { 58 | 59 | @GetMapping("/user-agent") 60 | public String getUserAgent(@RequestHeader(HttpHeaders.USER_AGENT) String userAgent) { 61 | return userAgent; 62 | } 63 | 64 | @PutMapping("/put") 65 | public String put(@RequestBody String body) { 66 | return body; 67 | } 68 | 69 | @PostMapping("/post") 70 | public String post(@RequestBody String body) { 71 | return body; 72 | } 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/ResourceRequestBody.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import okhttp3.MediaType; 4 | import okhttp3.RequestBody; 5 | import okio.BufferedSink; 6 | import okio.Okio; 7 | import org.springframework.core.io.ByteArrayResource; 8 | import org.springframework.core.io.FileSystemResource; 9 | import org.springframework.core.io.InputStreamResource; 10 | import org.springframework.core.io.Resource; 11 | import org.springframework.lang.Nullable; 12 | import org.springframework.util.MimeType; 13 | 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | 17 | /** 18 | * @author Lars Grefer 19 | */ 20 | public class ResourceRequestBody extends RequestBody { 21 | 22 | private final Resource resource; 23 | 24 | @Nullable 25 | private final MediaType mediaType; 26 | 27 | public ResourceRequestBody(Resource resource) { 28 | this.resource = resource; 29 | this.mediaType = null; 30 | } 31 | 32 | public ResourceRequestBody(Resource resource, MimeType springMimeType) { 33 | this.resource = resource; 34 | this.mediaType = MediaType.parse(springMimeType.toString()); 35 | } 36 | 37 | public ResourceRequestBody(Resource resource, MediaType okhttpMediaType) { 38 | this.resource = resource; 39 | this.mediaType = okhttpMediaType; 40 | } 41 | 42 | @Override 43 | @Nullable 44 | public MediaType contentType() { 45 | return mediaType; 46 | } 47 | 48 | @Override 49 | public void writeTo(BufferedSink bufferedSink) throws IOException { 50 | try (InputStream inputStream = resource.getInputStream()) { 51 | bufferedSink.writeAll(Okio.source(inputStream)); 52 | } 53 | } 54 | 55 | @Override 56 | public long contentLength() throws IOException { 57 | if (resource instanceof InputStreamResource && resource.getClass().equals(InputStreamResource.class)) { 58 | return -1; 59 | } 60 | return resource.contentLength(); 61 | } 62 | 63 | @Override 64 | public boolean isOneShot() { 65 | if (resource instanceof InputStreamResource) { 66 | return true; 67 | } 68 | 69 | if (resource instanceof ByteArrayResource) { 70 | return false; 71 | } 72 | 73 | if (resource instanceof FileSystemResource) { 74 | return false; 75 | } 76 | 77 | return super.isOneShot(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/OkHttpResponseBodyResource.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import okhttp3.ResponseBody; 6 | import org.springframework.core.io.AbstractResource; 7 | import org.springframework.lang.Nullable; 8 | 9 | import java.io.Closeable; 10 | import java.io.FileNotFoundException; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.nio.charset.Charset; 14 | 15 | @RequiredArgsConstructor 16 | public class OkHttpResponseBodyResource extends AbstractResource implements Closeable { 17 | 18 | @Nullable 19 | @Getter 20 | private final ResponseBody responseBody; 21 | private boolean closed = false; 22 | 23 | @Override 24 | public byte[] getContentAsByteArray() throws IOException { 25 | if (responseBody == null) { 26 | throw new FileNotFoundException("Response body is null"); 27 | } 28 | return responseBody.bytes(); 29 | } 30 | 31 | @Override 32 | public String getContentAsString(Charset charset) throws IOException { 33 | if (responseBody == null) { 34 | throw new FileNotFoundException("Response body is null"); 35 | } 36 | return responseBody.string(); 37 | } 38 | 39 | @Override 40 | public String getDescription() { 41 | return "OkHttp ResponseBody " + responseBody; 42 | } 43 | 44 | @Override 45 | public InputStream getInputStream() throws FileNotFoundException { 46 | if (responseBody == null) { 47 | throw new FileNotFoundException("Response body is null"); 48 | } 49 | return responseBody.byteStream(); 50 | } 51 | 52 | @Override 53 | public long contentLength() throws FileNotFoundException { 54 | if (responseBody == null) { 55 | throw new FileNotFoundException("Response body is null"); 56 | } 57 | return responseBody.contentLength(); 58 | } 59 | 60 | @Override 61 | public boolean exists() { 62 | return responseBody != null; 63 | } 64 | 65 | @Override 66 | public boolean isOpen() { 67 | return exists() && !closed; 68 | } 69 | 70 | @Override 71 | public boolean isReadable() { 72 | return isOpen(); 73 | } 74 | 75 | @Override 76 | public void close() { 77 | if (responseBody != null) { 78 | responseBody.close(); 79 | } 80 | closed = true; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/test/java/io/freefair/spring/okhttp/ssl/SslBundleTest.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.ssl; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.ssl.*; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.boot.test.web.server.LocalServerPort; 9 | import org.springframework.boot.web.client.RestTemplateBuilder; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | import org.springframework.web.client.RestTemplate; 15 | 16 | import javax.net.ssl.*; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { 21 | "server.ssl.enabled=true", 22 | "server.ssl.key-store=classpath:keystore.jks", 23 | "server.ssl.key-store-password=changeit", 24 | "server.ssl.key-alias=server-alias", 25 | "server.ssl.key-password=changeit", 26 | "spring.ssl.bundle.jks.a.truststore.location=classpath:keystore.jks", 27 | "spring.ssl.bundle.jks.a.truststore.password=changeit" 28 | }) 29 | public class SslBundleTest { 30 | 31 | @Autowired 32 | private RestTemplateBuilder restTemplateBuilder; 33 | 34 | @Autowired 35 | private SslBundles sslBundles; 36 | 37 | @LocalServerPort 38 | private int port; 39 | 40 | @Test 41 | void testSsl() { 42 | 43 | SslBundle ssl = sslBundles.getBundle("a"); 44 | 45 | RestTemplate restTemplate = restTemplateBuilder 46 | .sslBundle(ssl) 47 | .rootUri("https://localhost:" + port) 48 | .build(); 49 | 50 | String result = restTemplate.getForObject("/foo", String.class); 51 | assertThat(result).isEqualTo("bar"); 52 | 53 | } 54 | 55 | @Configuration 56 | @SpringBootApplication 57 | @RestController 58 | public static class TestController { 59 | 60 | 61 | @GetMapping("/foo") 62 | public String foo() { 63 | return "bar"; 64 | } 65 | 66 | @Bean 67 | public HostnameVerifier hostnameVerifier() { 68 | return (hostname, session) -> true; 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/test/java/io/freefair/spring/okhttp/OkHttp3AutoConfigurationIT.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import okhttp3.Interceptor; 4 | import okhttp3.OkHttpClient; 5 | import okhttp3.Response; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | import java.io.IOException; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.assertj.core.api.Assertions.fail; 17 | 18 | /** 19 | * @author Lars Grefer 20 | */ 21 | @SpringBootTest 22 | public class OkHttp3AutoConfigurationIT { 23 | 24 | @Autowired 25 | private OkHttpClient okHttpClient; 26 | 27 | @Test 28 | public void testContextLoads() { 29 | assertThat(okHttpClient).isNotNull(); 30 | } 31 | 32 | @Test 33 | public void testApplicationInterceptor() { 34 | assertThat(okHttpClient.interceptors()).size().isGreaterThan(0); 35 | 36 | for (Interceptor interceptor : okHttpClient.interceptors()) { 37 | if (interceptor instanceof MyInterceptor) { 38 | return; 39 | } 40 | } 41 | fail("MyInterceptor not found"); 42 | } 43 | 44 | @Test 45 | public void testNetworkInterceptor() { 46 | assertThat(okHttpClient.networkInterceptors()).size().isGreaterThan(0); 47 | 48 | for (Interceptor interceptor : okHttpClient.networkInterceptors()) { 49 | if (interceptor instanceof MyInterceptor) { 50 | return; 51 | } 52 | } 53 | fail("MyInterceptor not found"); 54 | } 55 | 56 | @Configuration 57 | @EnableAutoConfiguration 58 | static class TestConfig { 59 | 60 | @Bean 61 | @ApplicationInterceptor 62 | public Interceptor appInterceptor() { 63 | return new MyInterceptor(); 64 | } 65 | 66 | @Bean 67 | @NetworkInterceptor 68 | public Interceptor netInterceptor() { 69 | return new MyInterceptor(); 70 | } 71 | } 72 | 73 | static class MyInterceptor implements Interceptor { 74 | 75 | @Override 76 | public Response intercept(Chain chain) throws IOException { 77 | return chain.proceed(chain.request()); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/test/java/io/freefair/spring/okhttp/OkHttpRestTemplateAutoConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import io.freefair.spring.okhttp.client.OkHttpClientRequestFactory; 4 | import okhttp3.OkHttpClient; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.SpringBootConfiguration; 8 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.boot.web.client.RestTemplateBuilder; 11 | import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper; 12 | import org.springframework.http.client.ClientHttpRequestFactory; 13 | import org.springframework.web.client.RestTemplate; 14 | 15 | import java.time.Duration; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, 20 | properties = { 21 | "okhttp.read-timeout=21s", 22 | "okhttp.connect-timeout=21s", 23 | "okhttp.write-timeout=21s" 24 | }) 25 | class OkHttpRestTemplateAutoConfigurationTest { 26 | 27 | @Autowired 28 | private RestTemplateBuilder restTemplateBuilder; 29 | 30 | @Test 31 | void testTimeouts() throws NoSuchFieldException, IllegalAccessException { 32 | RestTemplate restTemplate = restTemplateBuilder.connectTimeout(Duration.ofSeconds(42)).build(); 33 | 34 | OkHttpClient client = extractClient(restTemplate); 35 | 36 | assertThat(client.connectTimeoutMillis()).isEqualTo(Duration.ofSeconds(42).toMillis()); 37 | assertThat(client.readTimeoutMillis()).isEqualTo(Duration.ofSeconds(21).toMillis()); 38 | assertThat(client.writeTimeoutMillis()).isEqualTo(Duration.ofSeconds(21).toMillis()); 39 | } 40 | 41 | private OkHttpClient extractClient(RestTemplate restTemplate) { 42 | ClientHttpRequestFactory requestFactory = restTemplate.getRequestFactory(); 43 | 44 | while (requestFactory instanceof AbstractClientHttpRequestFactoryWrapper) { 45 | requestFactory = ((AbstractClientHttpRequestFactoryWrapper) requestFactory).getDelegate(); 46 | } 47 | 48 | assertThat(requestFactory).isInstanceOf(OkHttpClientRequestFactory.class); 49 | 50 | return ((OkHttpClientRequestFactory)requestFactory).okHttpClient(); 51 | } 52 | 53 | @SpringBootConfiguration 54 | @EnableAutoConfiguration 55 | public static class TestConfiguration { 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/test/java/io/freefair/spring/okhttp/OkHttpRestClientAutoConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import io.freefair.spring.okhttp.client.OkHttpClientRequestFactory; 4 | import okhttp3.OkHttpClient; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.SpringBootConfiguration; 8 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper; 11 | import org.springframework.http.client.ClientHttpRequestFactory; 12 | import org.springframework.web.client.RestClient; 13 | 14 | import java.lang.reflect.Field; 15 | import java.time.Duration; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, 20 | properties = { 21 | "okhttp.read-timeout=21s", 22 | "okhttp.connect-timeout=21s", 23 | "okhttp.write-timeout=21s" 24 | }) 25 | class OkHttpRestClientAutoConfigurationTest { 26 | 27 | @Autowired 28 | private RestClient.Builder restClientBuilder; 29 | 30 | @Test 31 | void testTimeouts() throws NoSuchFieldException, IllegalAccessException { 32 | RestClient restTemplate = restClientBuilder.build(); 33 | 34 | OkHttpClient client = extractClient(restTemplate); 35 | 36 | assertThat(client.connectTimeoutMillis()).isEqualTo(Duration.ofSeconds(21).toMillis()); 37 | assertThat(client.readTimeoutMillis()).isEqualTo(Duration.ofSeconds(21).toMillis()); 38 | assertThat(client.writeTimeoutMillis()).isEqualTo(Duration.ofSeconds(21).toMillis()); 39 | } 40 | 41 | private OkHttpClient extractClient(RestClient restClient) throws NoSuchFieldException, IllegalAccessException { 42 | 43 | Field clientRequestFactoryField = restClient.getClass().getDeclaredField("clientRequestFactory"); 44 | clientRequestFactoryField.setAccessible(true); 45 | ClientHttpRequestFactory requestFactory = (ClientHttpRequestFactory) clientRequestFactoryField.get(restClient); 46 | 47 | while (requestFactory instanceof AbstractClientHttpRequestFactoryWrapper) { 48 | requestFactory = ((AbstractClientHttpRequestFactoryWrapper) requestFactory).getDelegate(); 49 | } 50 | 51 | assertThat(requestFactory).isInstanceOf(OkHttpClientRequestFactory.class); 52 | 53 | return ((OkHttpClientRequestFactory)requestFactory).okHttpClient(); 54 | } 55 | 56 | @SpringBootConfiguration 57 | @EnableAutoConfiguration 58 | public static class TestConfiguration { 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /config/checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/test/java/io/freefair/spring/okhttp/logging/OkHttp3LoggingInterceptorAutoConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.logging; 2 | 3 | import io.freefair.spring.okhttp.autoconfigure.logging.OkHttp3LoggingInterceptorAutoConfiguration; 4 | import okhttp3.logging.HttpLoggingInterceptor; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.boot.autoconfigure.AutoConfigurations; 8 | import org.springframework.boot.test.context.runner.ApplicationContextRunner; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | /** 15 | * @author Lars Grefer 16 | */ 17 | public class OkHttp3LoggingInterceptorAutoConfigurationTest { 18 | 19 | private ApplicationContextRunner applicationContextRunner; 20 | 21 | @BeforeEach 22 | public void setUp() { 23 | applicationContextRunner = new ApplicationContextRunner() 24 | .withConfiguration(AutoConfigurations.of(OkHttp3LoggingInterceptorAutoConfiguration.class)); 25 | } 26 | 27 | @Test 28 | public void testDefaultLogger() { 29 | applicationContextRunner 30 | .run(context -> { 31 | assertThat(context).hasSingleBean(HttpLoggingInterceptor.class); 32 | 33 | assertThat(context).getBean(HttpLoggingInterceptor.class) 34 | .hasFieldOrPropertyWithValue("logger", HttpLoggingInterceptor.Logger.DEFAULT); 35 | }); 36 | } 37 | 38 | @Test 39 | public void testDefaultLogger_enabled() { 40 | applicationContextRunner 41 | .withPropertyValues("okhttp.logging.enabled=true") 42 | .run(context -> { 43 | assertThat(context).hasSingleBean(HttpLoggingInterceptor.class); 44 | 45 | assertThat(context).getBean(HttpLoggingInterceptor.class) 46 | .hasFieldOrPropertyWithValue("logger", HttpLoggingInterceptor.Logger.DEFAULT); 47 | }); 48 | } 49 | 50 | @Test 51 | public void testDefaultLogger_disabled() { 52 | applicationContextRunner 53 | .withPropertyValues("okhttp.logging.enabled=false") 54 | .run(context -> { 55 | assertThat(context).doesNotHaveBean(HttpLoggingInterceptor.class); 56 | }); 57 | } 58 | 59 | @Test 60 | public void testCustomLogger() { 61 | 62 | applicationContextRunner 63 | .withUserConfiguration(CustomLoggerConfiguration.class) 64 | .run(context -> { 65 | assertThat(context).hasSingleBean(HttpLoggingInterceptor.class); 66 | 67 | assertThat(context).getBean(HttpLoggingInterceptor.class) 68 | .hasFieldOrPropertyWithValue("logger", CustomLoggerConfiguration.LOGGER); 69 | }); 70 | } 71 | 72 | @Configuration 73 | static class CustomLoggerConfiguration { 74 | 75 | public static final HttpLoggingInterceptor.Logger LOGGER = message -> { }; 76 | 77 | @Bean 78 | public HttpLoggingInterceptor.Logger customLogger() { 79 | return LOGGER; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/OkHttpProperties.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.autoconfigure; 2 | 3 | import lombok.Data; 4 | import okhttp3.Protocol; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.boot.context.properties.NestedConfigurationProperty; 7 | import org.springframework.util.unit.DataSize; 8 | 9 | import java.io.File; 10 | import java.time.Duration; 11 | import java.util.List; 12 | 13 | /** 14 | * @author Lars Grefer 15 | */ 16 | @Data 17 | @ConfigurationProperties(prefix = "okhttp") 18 | public class OkHttpProperties { 19 | 20 | /** 21 | * The default connect timeout for new connections. 22 | */ 23 | private Duration connectTimeout = Duration.ofSeconds(10); 24 | 25 | /** 26 | * The default read timeout for new connections. 27 | */ 28 | private Duration readTimeout = Duration.ofSeconds(10); 29 | 30 | /** 31 | * The default write timeout for new connections. 32 | */ 33 | private Duration writeTimeout = Duration.ofSeconds(10); 34 | 35 | /** 36 | * The interval between web socket pings initiated by this client. Use this to 37 | * automatically send web socket ping frames until either the web socket fails or it is closed. 38 | * This keeps the connection alive and may detect connectivity failures early. No timeouts are 39 | * enforced on the acknowledging pongs. 40 | * 41 | *

The default value of 0 disables client-initiated pings. 42 | */ 43 | private Duration pingInterval = Duration.ZERO; 44 | 45 | @NestedConfigurationProperty 46 | private CacheProperties cache = new CacheProperties(); 47 | 48 | /** 49 | * Whether to follow redirects from HTTPS to HTTP and from HTTP to HTTPS. 50 | */ 51 | private boolean followSslRedirects = true; 52 | 53 | /** 54 | * Whether to follow redirects. 55 | */ 56 | private boolean followRedirects = true; 57 | 58 | /** 59 | * Whether to retry or not when a connectivity problem is encountered. 60 | */ 61 | private boolean retryOnConnectionFailure = true; 62 | 63 | /** 64 | * Configure the protocols used by this client to communicate with remote servers. 65 | */ 66 | private List protocols = null; 67 | 68 | @NestedConfigurationProperty 69 | private final ConnectionPoolProperties connectionPool = new ConnectionPoolProperties(); 70 | 71 | /** 72 | * @author Lars Grefer 73 | * @see okhttp3.Cache 74 | */ 75 | @Data 76 | public static class CacheProperties { 77 | 78 | private boolean enabled; 79 | 80 | /** 81 | * The maximum number of bytes this cache should use to store. 82 | */ 83 | private DataSize maxSize = DataSize.ofMegabytes(10); 84 | 85 | /** 86 | * The path of the directory where the cache should be stored. 87 | */ 88 | private File directory; 89 | } 90 | 91 | /** 92 | * @see okhttp3.ConnectionPool 93 | */ 94 | @Data 95 | public static class ConnectionPoolProperties { 96 | 97 | /** 98 | * The maximum number of idle connections for each address. 99 | */ 100 | private int maxIdleConnections = 5; 101 | 102 | private Duration keepAliveDuration = Duration.ofMinutes(5); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/OkHttpClientRequestFactoryBuilder.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.autoconfigure; 2 | 3 | import io.freefair.spring.okhttp.client.OkHttpClientRequestFactory; 4 | import lombok.RequiredArgsConstructor; 5 | import okhttp3.ConnectionSpec; 6 | import okhttp3.OkHttpClient; 7 | import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; 8 | import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; 9 | import org.springframework.boot.ssl.SslBundle; 10 | import org.springframework.boot.ssl.SslOptions; 11 | import org.springframework.lang.Nullable; 12 | import org.springframework.util.Assert; 13 | 14 | import javax.net.ssl.SSLContext; 15 | import javax.net.ssl.SSLSocketFactory; 16 | import javax.net.ssl.TrustManager; 17 | import javax.net.ssl.X509TrustManager; 18 | import java.time.Duration; 19 | import java.util.List; 20 | 21 | /** 22 | * @author Lars Grefer 23 | */ 24 | @RequiredArgsConstructor 25 | public class OkHttpClientRequestFactoryBuilder implements ClientHttpRequestFactoryBuilder { 26 | 27 | private final OkHttpClient okHttpClient; 28 | 29 | @Override 30 | public OkHttpClientRequestFactory build() { 31 | return this.build(null); 32 | } 33 | 34 | @Override 35 | public OkHttpClientRequestFactory build(@Nullable ClientHttpRequestFactorySettings settings) { 36 | OkHttpClient.Builder builder = okHttpClient.newBuilder(); 37 | if (settings == null) { 38 | return new OkHttpClientRequestFactory(builder.build()); 39 | } 40 | 41 | Duration connectTimeout = settings.connectTimeout(); 42 | if (connectTimeout != null) { 43 | builder.connectTimeout(connectTimeout); 44 | } 45 | 46 | Duration readTimeout = settings.readTimeout(); 47 | if (readTimeout != null) { 48 | builder.readTimeout(readTimeout); 49 | } 50 | 51 | SslBundle sslBundle = settings.sslBundle(); 52 | if (sslBundle != null) { 53 | 54 | SslOptions sslOptions = sslBundle.getOptions(); 55 | if (sslOptions.isSpecified()) { 56 | ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true) 57 | .cipherSuites(sslOptions.getCiphers()) 58 | .tlsVersions(sslOptions.getEnabledProtocols()) 59 | .build(); 60 | 61 | builder.connectionSpecs(List.of(connectionSpec)); 62 | } 63 | 64 | SSLContext sslContext = sslBundle.createSslContext(); 65 | SSLSocketFactory socketFactory = sslContext.getSocketFactory(); 66 | 67 | TrustManager[] trustManagers = sslBundle.getManagers().getTrustManagers(); 68 | Assert.state(trustManagers.length == 1, 69 | "Trust material must be provided in the SSL bundle for OkHttp3ClientHttpRequestFactory"); 70 | 71 | builder.sslSocketFactory(socketFactory, (X509TrustManager) trustManagers[0]); 72 | } 73 | 74 | ClientHttpRequestFactorySettings.Redirects redirects = settings.redirects(); 75 | if (redirects != null) { 76 | switch (redirects) { 77 | case FOLLOW_WHEN_POSSIBLE, FOLLOW -> { 78 | builder.followRedirects(true); 79 | builder.followSslRedirects(true); 80 | } 81 | case DONT_FOLLOW -> { 82 | builder.followRedirects(false); 83 | builder.followSslRedirects(true); 84 | } 85 | default -> {} 86 | } 87 | } 88 | 89 | return new OkHttpClientRequestFactory(builder.build()); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/metrics/OkHttpMetricsAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.autoconfigure.metrics; 2 | 3 | import io.freefair.spring.okhttp.autoconfigure.OkHttpProperties; 4 | import io.micrometer.core.instrument.binder.okhttp3.OkHttpConnectionPoolMetrics; 5 | import io.micrometer.core.instrument.binder.okhttp3.OkHttpMetricsEventListener; 6 | import io.micrometer.core.instrument.MeterRegistry; 7 | import io.micrometer.core.instrument.Tag; 8 | import okhttp3.ConnectionPool; 9 | import okhttp3.OkHttpClient; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; 12 | import org.springframework.boot.autoconfigure.AutoConfiguration; 13 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 14 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 15 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 16 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 17 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 18 | import org.springframework.context.annotation.Bean; 19 | import org.springframework.util.CollectionUtils; 20 | 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.stream.Collectors; 24 | 25 | @AutoConfiguration(after = CompositeMeterRegistryAutoConfiguration.class) 26 | @ConditionalOnBean(MeterRegistry.class) 27 | @ConditionalOnClass({MeterRegistry.class, OkHttpMetricsEventListener.class, OkHttpClient.class}) 28 | @EnableConfigurationProperties(OkHttpMetricsProperties.class) 29 | public class OkHttpMetricsAutoConfiguration { 30 | 31 | @Autowired 32 | private OkHttpMetricsProperties properties; 33 | 34 | @Bean 35 | @ConditionalOnProperty(value = "okhttp.metrics.enabled", matchIfMissing = true, havingValue = "true") 36 | @ConditionalOnMissingBean 37 | public OkHttpMetricsEventListener okHttpMetricsEventListener(MeterRegistry meterRegistry) { 38 | OkHttpMetricsEventListener.Builder builder = OkHttpMetricsEventListener 39 | .builder(meterRegistry, properties.getName()) 40 | .includeHostTag(properties.isIncludeHostTag()); 41 | 42 | List requestTagKeys = properties.getRequestTagKeys(); 43 | if (!CollectionUtils.isEmpty(requestTagKeys)) { 44 | builder = builder.requestTagKeys(requestTagKeys); 45 | } 46 | 47 | Map tags = properties.getTags(); 48 | if (!CollectionUtils.isEmpty(tags)) { 49 | builder = builder.tags(getTags(tags)); 50 | } 51 | 52 | return builder.build(); 53 | } 54 | 55 | @Bean 56 | @ConditionalOnProperty(value = "okhttp.metrics.pool.enabled", matchIfMissing = true, havingValue = "true") 57 | @ConditionalOnBean(ConnectionPool.class) 58 | @ConditionalOnMissingBean 59 | public OkHttpConnectionPoolMetrics okHttpConnectionPoolMetrics(ConnectionPool connectionPool, OkHttpProperties okHttpProperties) { 60 | OkHttpMetricsProperties.ConnectionPoolMetricsProperties poolProperties = properties.getPool(); 61 | return new OkHttpConnectionPoolMetrics(connectionPool, 62 | poolProperties.getNamePrefix(), 63 | getTags(poolProperties.getTags()), 64 | okHttpProperties.getConnectionPool().getMaxIdleConnections() 65 | ); 66 | } 67 | 68 | private List getTags(Map tags) { 69 | return tags.entrySet() 70 | .stream() 71 | .map(entry -> Tag.of(entry.getKey(), entry.getValue())) 72 | .collect(Collectors.toList()); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequest.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.client; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import okhttp3.*; 5 | import okio.Buffer; 6 | import okio.ByteString; 7 | import org.springframework.http.HttpHeaders; 8 | import org.springframework.http.HttpMethod; 9 | import org.springframework.http.StreamingHttpOutputMessage; 10 | import org.springframework.http.client.AbstractClientHttpRequest; 11 | import org.springframework.http.client.ClientHttpRequest; 12 | import org.springframework.http.client.ClientHttpResponse; 13 | import org.springframework.lang.Nullable; 14 | import org.springframework.util.Assert; 15 | import org.springframework.util.StringUtils; 16 | 17 | import java.io.IOException; 18 | import java.io.OutputStream; 19 | import java.net.MalformedURLException; 20 | import java.net.URI; 21 | 22 | /** 23 | * OkHttp based {@link ClientHttpRequest} implementation. 24 | * 25 | * @author Lars Grefer 26 | * @see OkHttpClientRequestFactory 27 | */ 28 | @RequiredArgsConstructor 29 | public class OkHttpClientRequest extends AbstractClientHttpRequest implements StreamingHttpOutputMessage { 30 | 31 | private final OkHttpClient okHttpClient; 32 | 33 | private final URI uri; 34 | 35 | private final HttpMethod method; 36 | 37 | 38 | @Nullable 39 | private Body streamingBody; 40 | 41 | @Nullable 42 | private Buffer bufferBody; 43 | 44 | 45 | @Override 46 | public HttpMethod getMethod() { 47 | return method; 48 | } 49 | 50 | @Override 51 | public URI getURI() { 52 | return uri; 53 | } 54 | 55 | @Override 56 | public void setBody(Body body) { 57 | Assert.notNull(body, "body must not be null"); 58 | assertNotExecuted(); 59 | Assert.state(bufferBody == null, "getBody has already been used."); 60 | this.streamingBody = body; 61 | } 62 | 63 | @Override 64 | protected OutputStream getBodyInternal(HttpHeaders headers) { 65 | Assert.state(this.streamingBody == null, "setBody has already been used."); 66 | 67 | if (bufferBody == null) { 68 | bufferBody = new Buffer(); 69 | } 70 | 71 | return bufferBody.outputStream(); 72 | } 73 | 74 | @Override 75 | protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException { 76 | 77 | Request okHttpRequest = buildRequest(headers); 78 | 79 | Response okHttpResponse = this.okHttpClient.newCall(okHttpRequest).execute(); 80 | 81 | return new OkHttpClientResponse(okHttpResponse); 82 | } 83 | 84 | private Request buildRequest(HttpHeaders headers) throws MalformedURLException { 85 | 86 | Request.Builder builder = new Request.Builder(); 87 | 88 | builder.url(uri.toURL()); 89 | 90 | MediaType contentType = null; 91 | 92 | String contentTypeHeader = headers.getFirst(HttpHeaders.CONTENT_TYPE); 93 | if (StringUtils.hasText(contentTypeHeader)) { 94 | contentType = MediaType.parse(contentTypeHeader); 95 | } 96 | 97 | RequestBody body = null; 98 | 99 | if (bufferBody != null) { 100 | ByteString bodyData = bufferBody.readByteString(); 101 | if (headers.getContentLength() < 0) { 102 | headers.setContentLength(bodyData.size()); 103 | } 104 | body = RequestBody.create(bodyData, contentType); 105 | } else if (streamingBody != null) { 106 | body = new StreamingBodyRequestBody(streamingBody, contentType, headers.getContentLength()); 107 | } else if (okhttp3.internal.http.HttpMethod.requiresRequestBody(method.name())) { 108 | body = RequestBody.create(new byte[0], contentType); 109 | } 110 | 111 | builder.method(getMethod().name(), body); 112 | 113 | headers.forEach((name, values) -> { 114 | for (String value : values) { 115 | builder.addHeader(name, value); 116 | } 117 | }); 118 | 119 | return builder.build(); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/test/java/io/freefair/spring/okhttp/OkHttp3AutoConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp; 2 | 3 | import io.freefair.spring.okhttp.autoconfigure.OkHttp3AutoConfiguration; 4 | import okhttp3.Cache; 5 | import okhttp3.Dns; 6 | import okhttp3.OkHttpClient; 7 | import okhttp3.Protocol; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.boot.autoconfigure.AutoConfigurations; 11 | import org.springframework.boot.test.context.runner.ApplicationContextRunner; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | 15 | import java.io.File; 16 | import java.util.List; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.mockito.Mockito.mock; 20 | 21 | 22 | /** 23 | * @author Lars Grefer 24 | */ 25 | public class OkHttp3AutoConfigurationTest { 26 | 27 | private ApplicationContextRunner applicationContextRunner; 28 | 29 | @BeforeEach 30 | public void setUp() { 31 | applicationContextRunner = new ApplicationContextRunner() 32 | .withConfiguration(AutoConfigurations.of(OkHttp3AutoConfiguration.class)); 33 | } 34 | 35 | @Test 36 | public void testDefaultClient() { 37 | applicationContextRunner.run(context -> { 38 | assertThat(context).hasSingleBean(OkHttpClient.class); 39 | assertThat(context.getBean(OkHttpClient.class).cache()).isNotNull(); 40 | assertThat(context).doesNotHaveBean(Dns.class); 41 | }); 42 | } 43 | 44 | @Test 45 | public void testDns() { 46 | applicationContextRunner 47 | .withUserConfiguration(DnsConfiguration.class) 48 | .run(context -> { 49 | assertThat(context).hasSingleBean(OkHttpClient.class); 50 | assertThat(context).hasSingleBean(Dns.class); 51 | 52 | assertThat(context.getBean(OkHttpClient.class).dns()).isNotNull(); 53 | }); 54 | } 55 | 56 | @Test 57 | public void testNoCache() { 58 | applicationContextRunner 59 | .withPropertyValues("okhttp.cache.enabled=false") 60 | .run(context -> { 61 | assertThat(context).hasSingleBean(OkHttpClient.class); 62 | assertThat(context).doesNotHaveBean(Cache.class); 63 | 64 | assertThat(context.getBean(OkHttpClient.class).cache()).isNull(); 65 | }); 66 | } 67 | 68 | @Test 69 | public void testCustomCache() { 70 | applicationContextRunner 71 | .withUserConfiguration(CustomCacheConfiguration.class) 72 | .run(context -> { 73 | assertThat(context).hasSingleBean(OkHttpClient.class); 74 | 75 | assertThat(context.getBean(OkHttpClient.class).cache()).isEqualTo(CustomCacheConfiguration.CACHE); 76 | }); 77 | } 78 | 79 | @Test 80 | public void testProtocols() { 81 | applicationContextRunner 82 | .withPropertyValues("okhttp.protocols=H2_PRIOR_KNOWLEDGE") 83 | .run(context -> { 84 | assertThat(context).hasSingleBean(OkHttpClient.class); 85 | 86 | List protocols = context.getBean(OkHttpClient.class).protocols(); 87 | assertThat(protocols).containsExactly(Protocol.H2_PRIOR_KNOWLEDGE); 88 | }); 89 | } 90 | 91 | @Test 92 | public void testProtocols_invalid() { 93 | applicationContextRunner 94 | .withPropertyValues("okhttp.protocols=Foo") 95 | .run(context -> { 96 | assertThat(context).hasFailed(); 97 | }); 98 | } 99 | 100 | @Test 101 | public void testProtocols_empty() { 102 | applicationContextRunner 103 | .withPropertyValues("okhttp.protocols=") 104 | .run(context -> { 105 | assertThat(context).hasSingleBean(OkHttpClient.class); 106 | 107 | List protocols = context.getBean(OkHttpClient.class).protocols(); 108 | assertThat(protocols).contains(Protocol.HTTP_1_1, Protocol.HTTP_2); 109 | }); 110 | } 111 | 112 | @Configuration 113 | static class CustomCacheConfiguration { 114 | 115 | static final Cache CACHE = new Cache(new File(""), 1); 116 | 117 | @Bean 118 | public Cache okHttp3Cache() { 119 | return CACHE; 120 | } 121 | } 122 | 123 | @Configuration 124 | static class DnsConfiguration { 125 | @Bean 126 | public Dns dns() { 127 | return mock(Dns.class); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/OkHttp3AutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.freefair.spring.okhttp.autoconfigure; 2 | 3 | import io.freefair.spring.okhttp.ApplicationInterceptor; 4 | import io.freefair.spring.okhttp.NetworkInterceptor; 5 | import io.freefair.spring.okhttp.OkHttp3Configurer; 6 | import jakarta.annotation.PreDestroy; 7 | import lombok.extern.slf4j.Slf4j; 8 | import okhttp3.*; 9 | import org.springframework.beans.factory.ObjectProvider; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.autoconfigure.AutoConfiguration; 12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 13 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 14 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 15 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 16 | import org.springframework.context.annotation.Bean; 17 | import org.springframework.util.CollectionUtils; 18 | import org.springframework.util.FileSystemUtils; 19 | 20 | import javax.net.ssl.HostnameVerifier; 21 | import java.io.File; 22 | import java.io.IOException; 23 | import java.nio.file.Files; 24 | import java.time.Duration; 25 | import java.util.concurrent.TimeUnit; 26 | 27 | /** 28 | * @author Lars Grefer 29 | */ 30 | @Slf4j 31 | @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") 32 | @AutoConfiguration 33 | @ConditionalOnClass(OkHttpClient.class) 34 | @EnableConfigurationProperties(OkHttpProperties.class) 35 | public class OkHttp3AutoConfiguration { 36 | 37 | @Autowired 38 | private OkHttpProperties okHttpProperties; 39 | 40 | @Autowired 41 | private ObjectProvider configurers; 42 | 43 | @Autowired 44 | @ApplicationInterceptor 45 | private ObjectProvider applicationInterceptors; 46 | 47 | @Autowired 48 | @NetworkInterceptor 49 | private ObjectProvider networkInterceptors; 50 | 51 | private File tempDirCache = null; 52 | 53 | @Bean 54 | @ConditionalOnMissingBean 55 | public OkHttpClient okHttp3Client( 56 | ObjectProvider cache, 57 | ObjectProvider cookieJar, 58 | ObjectProvider dns, 59 | ObjectProvider hostnameVerifier, 60 | ObjectProvider certificatePinner, 61 | ConnectionPool connectionPool, 62 | ObjectProvider eventListener 63 | ) { 64 | OkHttpClient.Builder builder = new OkHttpClient.Builder(); 65 | 66 | cache.ifUnique(builder::cache); 67 | 68 | eventListener.ifUnique(builder::eventListener); 69 | 70 | builder.connectTimeout(okHttpProperties.getConnectTimeout()); 71 | builder.readTimeout(okHttpProperties.getReadTimeout()); 72 | builder.writeTimeout(okHttpProperties.getWriteTimeout()); 73 | builder.pingInterval(okHttpProperties.getPingInterval()); 74 | 75 | cookieJar.ifUnique(builder::cookieJar); 76 | 77 | dns.ifUnique(builder::dns); 78 | 79 | hostnameVerifier.ifUnique(builder::hostnameVerifier); 80 | certificatePinner.ifUnique(builder::certificatePinner); 81 | 82 | builder.connectionPool(connectionPool); 83 | 84 | builder.followRedirects(okHttpProperties.isFollowRedirects()); 85 | builder.followSslRedirects(okHttpProperties.isFollowSslRedirects()); 86 | builder.retryOnConnectionFailure(okHttpProperties.isRetryOnConnectionFailure()); 87 | 88 | if (!CollectionUtils.isEmpty(okHttpProperties.getProtocols())) { 89 | builder.protocols(okHttpProperties.getProtocols()); 90 | } 91 | 92 | applicationInterceptors.forEach(builder::addInterceptor); 93 | 94 | networkInterceptors.forEach(builder::addNetworkInterceptor); 95 | 96 | configurers.forEach(configurer -> configurer.configure(builder)); 97 | 98 | return builder.build(); 99 | } 100 | 101 | @Bean 102 | @ConditionalOnMissingBean 103 | public ConnectionPool okHttp3ConnectionPool() { 104 | int maxIdleConnections = okHttpProperties.getConnectionPool().getMaxIdleConnections(); 105 | Duration keepAliveDuration = okHttpProperties.getConnectionPool().getKeepAliveDuration(); 106 | return new ConnectionPool(maxIdleConnections, keepAliveDuration.toNanos(), TimeUnit.NANOSECONDS); 107 | } 108 | 109 | @Bean 110 | @ConditionalOnMissingBean 111 | @ConditionalOnProperty(value = "okhttp.cache.enabled", havingValue = "true", matchIfMissing = true) 112 | public Cache okHttp3Cache() throws IOException { 113 | File directory = okHttpProperties.getCache().getDirectory(); 114 | if (directory == null) { 115 | tempDirCache = Files.createTempDirectory("okhttp-cache").toFile(); 116 | directory = tempDirCache; 117 | } 118 | return new Cache(directory, okHttpProperties.getCache().getMaxSize().toBytes()); 119 | } 120 | 121 | @PreDestroy 122 | public void deleteTempCache() { 123 | if (tempDirCache != null) { 124 | log.debug("Deleting the temporary OkHttp Cache at {}", tempDirCache.getAbsolutePath()); 125 | try { 126 | FileSystemUtils.deleteRecursively(tempDirCache); 127 | } catch (Exception e) { 128 | log.warn("Failed to delete the temporary OkHttp Cache at {}", tempDirCache.getAbsolutePath()); 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | --------------------------------------------------------------------------------