OpenFeignでDMM.comと通信する

## 目次
* [概要](#概要)
* [環境](#環境)
* [構成](#構成)
* [各種サービスの用意](#各種サービスの用意)
* [実装内容](#実装内容)
* [実行結果](#実行結果)
* [所感](#所感)
* [参考資料](#参考資料)

## 概要
OpenFeignを使用して、DMM.comと通信するアプリを作る一連について記載しています。
画面については、[ChatGPT](https://openai.com/chatgpt)にベースを作成してもらい、後は頑張って[Thymeleaf](https://www.thymeleaf.org/)化しました。
ソースコードだけ見たい方は[こちら](https://github.com/he23inw3/openfeign-example)から。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534209/9ae41b31-eddd-f28e-cf95-607360e51c2e.png)


## 環境
* Kotlin 1.8.22
* SpringBoot 3.1.5

## 構成
アプリからキーワードを連携し、商品情報を取得し表示しています。
商品検索の機能は、DMM.com提供の商品検索APIを使用しています。
APIの仕様は[こちら](https://affiliate.dmm.com/api/v3/itemlist.html)をご確認ください。

```mermaid
sequenceDiagram
    autonumber
    actor user as 利用者
    participant screen as アプリ
    participant dmm as DMM
    user->>screen: キーワード送信
    screen ->> screen: 入力チェック
    opt 入力エラーの場合
        screen->>user: 入力エラー内容返信
    end
    screen->>dmm: キーワード送信
    dmm->>screen: 商品検索結果の返信
    screen->>user: 商品検索結果の返信
```

## 各種サービスの用意
APIを使用するにあたって、DMM.comの会員登録とアフィリエイトの登録とAPI利用申請が必要になります。
詳しくは[こちら](https://affiliate.dmm.com/api/)を参照お願いします。

## 実装内容
OpenFeignは、以下のようにアノテーションで通信方法を記述することができます。
各種設定については、Configクラスに設定しそれを指定することで設定が反映されます。
アノテーションの仕様は、[こちら](https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/)を参照お願いします。
※以下のコードはクエリパラメータをPOJOで渡しています。
　[RequestParam](https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestParam.html)で各値を引数とし、個別に渡すこともできます。
　個人的にはDTOの方が管理しやすいと思うので、そちらで記載しております。

```DmmClient.kt
package com.example.openfeignexample.client

import com.example.openfeignexample.client.dto.GetItemsRequest
import com.example.openfeignexample.client.dto.GetItemsResponse
import com.example.openfeignexample.config.interceptor.DmmApiConfig
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.cloud.openfeign.SpringQueryMap
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping

@FeignClient(name = "DMMClient", url = "\${dmm-api.url}", configuration = [DmmApiConfig::class])
interface DmmClient {

	@GetMapping("/ItemList")
	fun getItems(@SpringQueryMap request: GetItemsRequest): ResponseEntity<GetItemsResponse>
}

```

以下が商品検索APIの、リクエスト内容です。
APIキーとアフィリエイトIDが必要ですが、共通で必要なものについてはDTO管理外としています。

```GetItemsRequest.kt
package com.example.openfeignexample.client.dto

data class GetItemsRequest(
	val site: String = "DMM.com",
	val keyword: String,
	val hits: Int = 100,
	val sort: String = "date"
)
```

Configクラスで設定しているのは以下の3つです。
Beanで登録していますが、ログレベル等は設定ファイルで設定する方法もありそうです。
* APIキー等の付与。
* リトライ設定。
* ログ出力設定。

```DmmApiConfig.kt
package com.example.openfeignexample.config.interceptor

import com.example.openfeignexample.client.constant.DmmHeaders
import com.example.openfeignexample.constant.DmmApiConfigProperty
import com.example.openfeignexample.util.ConfigUtil
import feign.Logger
import feign.Request
import feign.RequestInterceptor
import feign.Response
import feign.RetryableException
import feign.Retryer
import feign.codec.ErrorDecoder
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Bean
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import java.io.InputStream
import java.lang.Exception
import java.net.URLDecoder

class DmmApiConfig(
	private val property: DmmApiConfigProperty,
	private val configUtil: ConfigUtil
) {

	private val log = LoggerFactory.getLogger(this::class.java)

	@Bean
	fun requestInterceptor(): RequestInterceptor {
		return RequestInterceptor { requestTemplate ->
			requestTemplate.query(DmmHeaders.DMM_API_ID, property.apiId)
			requestTemplate.query(DmmHeaders.AFFILIATE_ID, property.affiliateId)

			requestTemplate.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
			requestTemplate.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
		}
	}

	@Bean
	fun retry(): Retryer {
		return Retryer.Default(
			property.retryPeriod,
			property.retryDuration,
			property.retryMaxCount
		)
	}

	@Bean
	fun errorDecoder(): ErrorDecoder {
		return object : ErrorDecoder {

			override fun decode(methodKey: String?, response: Response?): Exception {
				val status = response?.status()
				if (status == HttpStatus.GATEWAY_TIMEOUT.value()) {
					val request = response.request()
					val method = request?.httpMethod()
					return RetryableException(status, "504: Gateway Timeout.", method, null, request)
				}

				return ErrorDecoder.Default().decode(methodKey, response)
			}
		}
	}

	@Bean
	fun logLevel(): Logger.Level {
		return Logger.Level.FULL
	}

	@Bean
	fun logger(): Logger {
		return object : Logger() {

			override fun log(configKey: String?, format: String?, vararg args: Any?) {
				// Not Used
			}

			override fun logRetry(configKey: String?, logLevel: Level?) {
				log.info(configKey, "RETRYING!!")
			}

			override fun logRequest(configKey: String?, logLevel: Level?, request: Request?) {
				if (logLevel != Level.FULL) {
					return
				}

				val url = request?.requestTemplate()?.path()
				val method = request?.httpMethod()
				val query = buildQuery(request?.requestTemplate()?.queries() ?: emptyMap())
				val body = buildBody(request?.body()?.inputStream())
				log.info(configUtil.getLogMessage("BE8001"), url, method, query, body)
			}

			override fun logAndRebufferResponse(configKey: String?, logLevel: Level?, response: Response?, elapsedTime: Long): Response {
				if (logLevel != Level.FULL) {
					return response!!
				}

				val request = response?.request()
				val url = request?.requestTemplate()?.path()
				val method = request?.httpMethod()
				val query = buildQuery(request?.requestTemplate()?.queries() ?: emptyMap())
				val body = buildBody(request?.body()?.inputStream())
				val status = response?.status()
				log.info(configUtil.getLogMessage("BE8002"), url, method, query, body, status, elapsedTime)
				return response!!
			}

			private fun buildQuery(queryMap: Map<String, Collection<String>>): Map<String, Collection<String>> {
				return queryMap.filterNot {
					it.key == DmmHeaders.DMM_API_ID ||
						it.key == DmmHeaders.AFFILIATE_ID
				}.mapValues {
					it.value.map { value ->
						URLDecoder.decode(value, Charsets.UTF_8)
					}
				}
			}

			private fun buildBody(body: InputStream?): String {
				if (body == null) {
					return "{}"
				}

				return IOUtils.toString(body, Charsets.UTF_8)
			}
		}
	}
}
```

接続先情報とAPIキーは、環境変数で管理しています。

```application.yml
spring:
  application:
    name: OpenFeign-Example
  main:
    banner-mode: off
    log-startup-info: false
  messages:
    basename: messages
    encoding: UTF-8
    fallback-to-system-locale: false

server:
  tomcat:
    max-threads: ${TOMCAT_MAX_THREADS:200}
    min-spare-threads: ${TOMCAT_MIN_THREADS:20}
  shutdown: graceful
  port: ${PORT:5000}

dmm-api:
  url: https://api.dmm.com/affiliate/v3
  api-id: ${DMM_API_ID}
  affiliate-id: ${DMM_API_AFFILIATE_ID}
  retry-max-count: ${DMM_API_RETRY_MAX_COUNT:5}
  retry-period: ${DMM_API_RETRY_PERIOD:100}
  retry-duration: ${DMM_API_RETRY_DURATION:1000}

logging:
  level:
    root: ${LOG_LEVEL:INFO}
```

```DmmApiConfigProperty.kt
package com.example.openfeignexample.constant

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration

@Configuration
class DmmApiConfigProperty {

	@Value("\${dmm-api.url}")
	val url: String = ""

	@Value("\${dmm-api.api-id}")
	val apiId: String = ""

	@Value("\${dmm-api.affiliate-id}")
	val affiliateId: String = ""

	@Value("\${dmm-api.retry-max-count}")
	val retryMaxCount: Int = 0

	@Value("\${dmm-api.retry-period}")
	val retryPeriod: Long = 0

	@Value("\${dmm-api.retry-duration}")
	val retryDuration: Long = 0
}
```

ログメッセージの設定

```messages.properties
# 8XXX: 外部通信関連
BE8001=SEND: URL={}, Method={}, Query={}, Body={}
BE8002=RECV: URL={}, Method={}, Query={}, Body={}, Status={}, Time={}ms
```

## 実行結果
* 画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534209/53c79cc3-03dc-458c-0f5e-d9210ed284c2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534209/8b02d1ad-5281-3bcc-663e-8862391f2523.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534209/c0662742-04db-6937-0a14-1e4df22e803a.png)


* ログ
```実行結果
"C:\Program Files\Java\jdk-17\bin\java.exe" "-javaagent:C:\Users\golde\AppData\Local\Programs\IntelliJ IDEA Community Edition\lib\idea_rt.jar=61403:C:\Users\golde\AppData\Local\Programs\IntelliJ IDEA Community Edition\bin" -Dfile.encoding=UTF-8 -classpath C:\Projects\IdeaProjects\openfeign-example\build\classes\kotlin\main;C:\Projects\IdeaProjects\openfeign-example\build\resources\main;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlin\kotlin-stdlib-jdk8\1.8.22\b25c86d47d6b962b9cf0f8c3f320c8a10eea3dd1\kotlin-stdlib-jdk8-1.8.22.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-starter-web\3.1.5\3674e5603cc25bc4dccf73b97d50234332fc3d72\spring-boot-starter-web-3.1.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.core\jackson-databind\2.11.0\8f5aaf3878b0647ff3a16610af53b1a5c05d9f15\jackson-databind-2.11.0.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.module\jackson-module-kotlin\2.15.3\36ea7813ada694e67f562ff1dc6f3b47883e373\jackson-module-kotlin-2.15.3.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlin\kotlin-reflect\1.8.22\b52be44bc57cb6fd2169a29aefa4507c4e49c858\kotlin-reflect-1.8.22.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.cloud\spring-cloud-starter-openfeign\4.0.4\51d8b8ea8698bce969dd3afad8420f04a7251dde\spring-cloud-starter-openfeign-4.0.4.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-starter-aop\3.1.5\2ac1f8bf4c2bce13b46d04abafc69a4f1523a08\spring-boot-starter-aop-3.1.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-starter-thymeleaf\3.1.5\3d5f1bff03e5e58b065ca230b7ee24b84bf7fe75\spring-boot-starter-thymeleaf-3.1.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-starter-validation\3.1.5\f653e6661ec3da08003789f4a6583d1674e333b1\spring-boot-starter-validation-3.1.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\nz.net.ultraq.thymeleaf\thymeleaf-layout-dialect\3.2.1\526f2e950a4d67ea49198f9ea5b4ec303ed002c2\thymeleaf-layout-dialect-3.2.1.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlin\kotlin-stdlib-jdk7\1.8.22\4dabb8248310d833bb6a8b516024a91fd3d275c\kotlin-stdlib-jdk7-1.8.22.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlin\kotlin-stdlib\1.8.22\636bf8b320e7627482771bbac9ed7246773c02bd\kotlin-stdlib-1.8.22.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-starter-json\3.1.5\36ce79c1a5c63ffd72b092eb312cb35ac6edb128\spring-boot-starter-json-3.1.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-starter\3.1.5\a14cd17b86261933929566775d80c65b9f7440fc\spring-boot-starter-3.1.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-starter-tomcat\3.1.5\4b362b923ec1ca080556c4e55fe4ae73cf5a84d7\spring-boot-starter-tomcat-3.1.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework\spring-webmvc\6.0.13\df1230a7dde55b9ce9d72670b42d621dc2e481df\spring-webmvc-6.0.13.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework\spring-web\6.0.13\c374fcaf34d82d735a1dcf2c496f064488aa2b20\spring-web-6.0.13.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.core\jackson-annotations\2.15.3\79baf4e605eb3bbb60b1c475d44a7aecceea1d60\jackson-annotations-2.15.3.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.core\jackson-core\2.15.3\60d600567c1862840397bf9ff5a92398edc5797b\jackson-core-2.15.3.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.cloud\spring-cloud-openfeign-core\4.0.4\30883d013fe1586e06e9f995020124ba6202a317\spring-cloud-openfeign-core-4.0.4.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.cloud\spring-cloud-starter\4.0.4\7a90112c371183668b51e657a1f9113ed38f126f\spring-cloud-starter-4.0.4.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.cloud\spring-cloud-commons\4.0.4\b8fd08d1066deb7dd7e924d913b21f8c2ff44e70\spring-cloud-commons-4.0.4.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\io.github.openfeign\feign-slf4j\12.4\25a7a8249a2b9cb9ae3ca4eeba9a57050551913b\feign-slf4j-12.4.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\io.github.openfeign\feign-core\12.4\7acf105cbe60a58af10fcaa610a41ceec9a4fa9d\feign-core-12.4.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework\spring-aop\6.0.13\aae1a18033787c9d324322f4470b12264e773e83\spring-aop-6.0.13.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.aspectj\aspectjweaver\1.9.20\da562407e43f74c0f8f5f5df4065d85ec1736d01\aspectjweaver-1.9.20.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.thymeleaf\thymeleaf-spring6\3.1.2.RELEASE\6030c7b4e260c41336f378e53da6e8531263f24b\thymeleaf-spring6-3.1.2.RELEASE.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.apache.tomcat.embed\tomcat-embed-el\10.1.15\49e784ef48d5fc7bc48d7e2a7ceb57e88b4d58b7\tomcat-embed-el-10.1.15.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.hibernate.validator\hibernate-validator\8.0.1.Final\e49e116b3d3928060599b176b3538bb848718e95\hibernate-validator-8.0.1.Final.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.apache.groovy\groovy\4.0.15\1fa486007f31bd7f245ba4674fb8afb173bea745\groovy-4.0.15.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlin\kotlin-stdlib-common\1.8.22\1a8e3601703ae14bb58757ea6b2d8e8e5935a586\kotlin-stdlib-common-1.8.22.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.jetbrains\annotations\13.0\919f0dfe192fb4e063e7dacadee7f8bb9a2672a9\annotations-13.0.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.datatype\jackson-datatype-jdk8\2.15.3\80158cb020c7bd4e4ba94d8d752a65729dc943b2\jackson-datatype-jdk8-2.15.3.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.datatype\jackson-datatype-jsr310\2.15.3\4a20a0e104931bfa72f24ef358c2eb63f1ef2aaf\jackson-datatype-jsr310-2.15.3.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.module\jackson-module-parameter-names\2.15.3\8d251b90c5358677e7d8161e0c2488e6f84f49da\jackson-module-parameter-names-2.15.3.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-autoconfigure\3.1.5\42a5b2ee98f700fba8d8c88d4af7b23266f1de0f\spring-boot-autoconfigure-3.1.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot\3.1.5\c188015a5a79f5df65e876dcfdef16148c45fe2c\spring-boot-3.1.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-starter-logging\3.1.5\8d8a91061baa4347d97a8fe15f3337d943badab\spring-boot-starter-logging-3.1.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\jakarta.annotation\jakarta.annotation-api\2.1.1\48b9bda22b091b1f48b13af03fe36db3be6e1ae3\jakarta.annotation-api-2.1.1.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework\spring-core\6.0.13\cd565c2408e37d2026822b871cd43e69da8ec40e\spring-core-6.0.13.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.yaml\snakeyaml\1.33\2cd0a87ff7df953f810c344bdf2fe3340b954c69\snakeyaml-1.33.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.apache.tomcat.embed\tomcat-embed-websocket\10.1.15\14cc0999d4a5128dc9bb8862487aa8ed4cd7fcd8\tomcat-embed-websocket-10.1.15.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.apache.tomcat.embed\tomcat-embed-core\10.1.15\e3a725405f494abc0fd4f30263c2e6ad87052de1\tomcat-embed-core-10.1.15.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework\spring-context\6.0.13\4c49af6dde7fce9602049f952b45ca29f30e2a37\spring-context-6.0.13.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework\spring-beans\6.0.13\5b205c9f2fb07c1367db144ce7ab305f94300604\spring-beans-6.0.13.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework\spring-expression\6.0.13\2bedffa4a3850bbbb652a31c47671824b17fbe01\spring-expression-6.0.13.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\io.micrometer\micrometer-observation\1.11.5\803f341121416365d1c438fe2a7290b146fadc92\micrometer-observation-1.11.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\io.github.openfeign.form\feign-form-spring\3.8.0\71059f6e16ddbaf0e42f1b1aa0bb60cc2f823e4d\feign-form-spring-3.8.0.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\commons-fileupload\commons-fileupload\1.5\ad4ad2ab2961b4e1891472bd1a33fabefb0385f3\commons-fileupload-1.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.cloud\spring-cloud-context\4.0.4\6962fd8ddc995cea96b2499cfbc44df181c41b47\spring-cloud-context-4.0.4.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.security\spring-security-rsa\1.0.12.RELEASE\2c6aca9e7eee11aee196a29eeea35d7aa8692398\spring-security-rsa-1.0.12.RELEASE.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.security\spring-security-crypto\6.1.5\971a767ef1ef6bea94486ac944f0e17b143dc680\spring-security-crypto-6.1.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.slf4j\slf4j-api\2.0.9\7cf2726fdcfbc8610f9a71fb3ed639871f315340\slf4j-api-2.0.9.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.thymeleaf\thymeleaf\3.1.2.RELEASE\273997509a4c7aef86cee0521750140c587d9be2\thymeleaf-3.1.2.RELEASE.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\jakarta.validation\jakarta.validation-api\3.0.2\92b6631659ba35ca09e44874d3eb936edfeee532\jakarta.validation-api-3.0.2.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.jboss.logging\jboss-logging\3.5.3.Final\c88fc1d8a96d4c3491f55d4317458ccad53ca663\jboss-logging-3.5.3.Final.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\com.fasterxml\classmate\1.5.1\3fe0bed568c62df5e89f4f174c101eab25345b6c\classmate-1.5.1.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\ch.qos.logback\logback-classic\1.4.11\54450c0c783e896a1a6d88c043bd2f1daba1c382\logback-classic-1.4.11.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.apache.logging.log4j\log4j-to-slf4j\2.20.0\d37f81f8978e2672bc32c82712ab4b3f66624adc\log4j-to-slf4j-2.20.0.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.slf4j\jul-to-slf4j\2.0.9\9ef7c70b248185845f013f49a33ff9ca65b7975\jul-to-slf4j-2.0.9.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework\spring-jcl\6.0.13\91ea90f2de4c71dac3cff04882156b00cdca3e0d\spring-jcl-6.0.13.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\io.micrometer\micrometer-commons\1.11.5\3e708f737ba2674823201a836b5858482b183902\micrometer-commons-1.11.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\io.github.openfeign.form\feign-form\3.8.0\3083889996b18443c275a352d3ab4bc6c8321598\feign-form-3.8.0.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\commons-io\commons-io\2.11.0\a2503f302b11ebde7ebc3df41daebe0e4eea3689\commons-io-2.11.0.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.bouncycastle\bcpkix-jdk18on\1.73\fd41dae0f564a93888ed5ade426281de94824717\bcpkix-jdk18on-1.73.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.attoparser\attoparser\2.0.7.RELEASE\e5d0e988d9124139d645bb5872b24dfa23e283cc\attoparser-2.0.7.RELEASE.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.unbescape\unbescape\1.1.6.RELEASE\7b90360afb2b860e09e8347112800d12c12b2a13\unbescape-1.1.6.RELEASE.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\ch.qos.logback\logback-core\1.4.11\2f9f280219a9922a74200eaf7138c4c17fb87c0f\logback-core-1.4.11.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.apache.logging.log4j\log4j-api\2.20.0\1fe6082e660daf07c689a89c94dc0f49c26b44bb\log4j-api-2.20.0.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.bouncycastle\bcutil-jdk18on\1.73\73a680acd04b249a6773f49200092cadb670bf0\bcutil-jdk18on-1.73.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.bouncycastle\bcprov-jdk18on\1.73\4bd3de48e5153059fe3f80cbcf86ea221795ee55\bcprov-jdk18on-1.73.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.springframework.boot\spring-boot-devtools\3.1.5\e7725476fbcd154d7ee5d85446cac2e16db1d818\spring-boot-devtools-3.1.5.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\nz.net.ultraq.groovy\groovy-extensions\2.1.0\c869dc033867e8861957ff3ff4e1ce0a00260e3a\groovy-extensions-2.1.0.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\nz.net.ultraq.thymeleaf\thymeleaf-expression-processor\3.1.1\81360e5289e8b4551733708c0ef11799d2716e54\thymeleaf-expression-processor-3.1.1.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\ognl\ognl\3.3.4\1904789bdd96f226ad252a02f230be1015f4462b\ognl-3.3.4.jar;C:\Users\golde\.gradle\caches\modules-2\files-2.1\org.javassist\javassist\3.29.0-GA\d3959fa7e00bf04dbe519228a23213d2afb625d8\javassist-3.29.0-GA.jar com.example.openfeignexample.OpenfeignExampleApplicationKt
2023-11-03T23:57:03.569+09:00  INFO 10908 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2023-11-03T23:57:03.573+09:00  INFO 10908 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2023-11-03T23:57:04.978+09:00  INFO 10908 --- [  restartedMain] o.s.cloud.context.scope.GenericScope     : BeanFactory id=750cf38b-3d33-3f45-a8b2-68bc6615318c
2023-11-03T23:57:05.017+09:00  INFO 10908 --- [  restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'com.example.openfeignexample.client.DmmClient' of type [org.springframework.cloud.openfeign.FeignClientFactoryBean] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2023-11-03T23:57:05.900+09:00  INFO 10908 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 5000 (http)
2023-11-03T23:57:05.919+09:00  INFO 10908 --- [  restartedMain] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-11-03T23:57:05.919+09:00  INFO 10908 --- [  restartedMain] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.15]
2023-11-03T23:57:06.025+09:00  INFO 10908 --- [  restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-11-03T23:57:06.027+09:00  INFO 10908 --- [  restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2451 ms
2023-11-03T23:57:06.509+09:00  INFO 10908 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2023-11-03T23:57:07.581+09:00  INFO 10908 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 5000 (http) with context path ''
2023-11-03T23:57:10.266+09:00  INFO 10908 --- [nio-5000-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-11-03T23:57:10.267+09:00  INFO 10908 --- [nio-5000-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2023-11-03T23:57:10.268+09:00  INFO 10908 --- [nio-5000-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2023-11-03T23:57:23.035+09:00  INFO 10908 --- [nio-5000-exec-4] c.e.o.config.interceptor.DmmApiConfig    : SEND: URL=https://api.dmm.com/affiliate/v3/ItemList, Method=GET, Query={hits=[100], site=[DMM.com], sort=[date], keyword=[バビル二世]}, Body={}
2023-11-03T23:57:23.609+09:00  INFO 10908 --- [nio-5000-exec-4] c.e.o.config.interceptor.DmmApiConfig    : RECV: URL=https://api.dmm.com/affiliate/v3/ItemList, Method=GET, Query={hits=[100], site=[DMM.com], sort=[date], keyword=[バビル二世]}, Body={}, Status=200, Time=568ms
```

## 所感
[前回](https://qiita.com/he23inw3/items/9a5f733b84d1b5ce2397)はRestTemplateを使用して通信していましたが、OpenFeignの方が直感的かなと思います。
仕事では圧倒的にRestTemplateを使う方が多いですが、技術選定できる機会あれば押してみようかと思います。

## 参考資料
https://affiliate.dmm.com/api/

https://cloud.spring.io/spring-cloud-openfeign/reference/html/