반응형
기본 예제
Todo
@Data
public class Todo {
private long userId;
private long id;
private String title;
private boolean completed;
}
TodoClient
public interface TodoClient {
@GetExchange("/todos/{id}")
Todo getTodo(@PathVariable long id);
}
TodoClientConfig
@Slf4j
@Configuration
public class TodoClientConfig {
@Bean
public TodoClient todoClient() {
RestClient restClient = RestClient.builder()
.baseUrl("https://jsonplaceholder.typicode.com")
.messageConverters(createMessageConverters())
.requestFactory(createRequestFactory())
.requestInterceptor(createRequestInterceptor())
.defaultStatusHandler(
HttpStatusCode::is4xxClientError,
(request, response) -> {
log.warn("status: {}, contents: {}", response.getStatusCode(), IOUtils.toString(response.getBody(), StandardCharsets.UTF_8));
}
)
.build();
return HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build()
.createClient(TodoClient.class);
}
private Consumer<List<HttpMessageConverter<?>>> createMessageConverters() {
return converters -> {
GsonHttpMessageConverter gsonHttpMessageConverter = new GsonHttpMessageConverter();
gsonHttpMessageConverter.setGson(new GsonBuilder()
.disableHtmlEscaping()
.create());
converters.removeIf(it -> it instanceof MappingJackson2HttpMessageConverter);
converters.add(gsonHttpMessageConverter);
};
}
private ClientHttpRequestFactory createRequestFactory() {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(Duration.ofSeconds(5));
requestFactory.setReadTimeout(Duration.ofSeconds(15));
return requestFactory;
}
private ClientHttpRequestInterceptor createRequestInterceptor() {
return (request, body, execution) -> {
ClientHttpResponse response = execution.execute(request, body);
String responseBody = IOUtils.toString(response.getBody(), StandardCharsets.UTF_8);
log.info("responseBody : {}", responseBody);
return new ClientHttpResponse() {
@Override
public HttpHeaders getHeaders() {
return response.getHeaders();
}
@Override
public InputStream getBody() {
return new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8));
}
@Override
public HttpStatusCode getStatusCode() throws IOException {
return response.getStatusCode();
}
@Override
public String getStatusText() throws IOException {
return response.getStatusText();
}
@Override
public void close() {
// do nothing
}
};
};
}
}
TodoClientTest
@SpringBootTest
public class TodoClientTest {
@Autowired
private TodoClient todoClient;
@Test
public void todoClient() {
Todo expected = new Todo();
expected.setUserId(1);
expected.setId(1);
expected.setTitle("delectus aut autem");
expected.setCompleted(false);
Todo actual = todoClient.getTodo(1);
assertEquals(expected, actual);
}
}
어노테이션으로 Bean 등록하기(ImportBeanDefinitionRegistrar)
HttpClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface HttpClient {
Class<?> configuration() default void.class;
}
HttpClientProperties
@Getter
@Builder
public class HttpClientProperties {
private final String url;
private final Duration connectionTimeout;
private final Duration readTimeout;
}
HttpClientFactoryBean
@RequiredArgsConstructor
public class HttpClientFactoryBean implements ApplicationContextAware, FactoryBean<Object> {
private final Class<?> restClientClass;
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public Class<?> getObjectType() {
return restClientClass;
}
@Override
public Object getObject() {
ApplicationContext configurationContext = createConfigurationContext();
HttpClientProperties properties = configurationContext.getBean(HttpClientProperties.class);
RestClient restClient = RestClient.builder()
.baseUrl(properties.getUrl())
.requestFactory(createRequestFactory(properties.getConnectionTimeout(), properties.getReadTimeout()))
.build();
return HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build()
.createClient(TodoClient.class);
}
private ApplicationContext createConfigurationContext() {
HttpClient httpClient = restClientClass.getAnnotation(HttpClient.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(httpClient.configuration());
context.setClassLoader(applicationContext.getClassLoader());
context.setParent(applicationContext);
context.refresh();
return context;
}
private ClientHttpRequestFactory createRequestFactory(Duration connectionTimeout, Duration readTimeout) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(connectionTimeout);
requestFactory.setReadTimeout(readTimeout);
return requestFactory;
}
}
AnnotationScanningCandidateComponentProvider
class AnnotationScanningCandidateComponentProvider extends ClassPathScanningCandidateComponentProvider {
public AnnotationScanningCandidateComponentProvider(Class<? extends Annotation> annotationType) {
super(false);
addIncludeFilter(new AnnotationTypeFilter(annotationType));
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isIndependent();
}
}
HttpClientBeanRegistrar
public class HttpClientBeanRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
new AnnotationScanningCandidateComponentProvider(HttpClient.class)
.findCandidateComponents("com.example").stream()
.map(it -> findClass(it.getBeanClassName()))
.forEach(restClientClass -> {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(HttpClientFactoryBean.class);
builder.addConstructorArgValue(restClientClass);
registry.registerBeanDefinition(restClientClass.getSimpleName(), builder.getBeanDefinition());
});
}
private Class<?> findClass(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
EnableHttpClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(HttpClientBeanRegistrar.class)
public @interface EnableHttpClient {
}
Application
@EnableHttpClient // 추가
@SpringBootApplication
public class Application {
...
}
TodoClientProperties
@ConfigurationProperties("todo-client")
@Data
public class TodoClientProperties {
private String url;
private Duration connectionTimeout;
private Duration readTimeout;
}
TodoClientConfig
@EnableConfigurationProperties(TodoClientProperties.class)
public class TodoClientConfig {
@Bean
public HttpClientProperties clientProperties(TodoClientProperties properties) {
return HttpClientProperties.builder()
.url(properties.getUrl())
.connectionTimeout(properties.getConnectionTimeout())
.readTimeout(properties.getReadTimeout())
.build();
}
}
TodoClient
@HttpClient(configuration = TodoClientConfig.class)
public interface TodoClient {
@GetExchange("/todos/{id}")
Todo getTodo(@PathVariable long id);
}
참고
반응형
'Development > Spring' 카테고리의 다른 글
[Spring] multi-module 프로젝트 구성하기(kotlin, gradle) (0) | 2024.07.07 |
---|---|
[Spring] Exposed (0) | 2024.04.10 |
[Spring] Elasticsearch (0) | 2024.01.01 |
[Spring] 헥사고날 아키텍처(Hexagonal Architecture) (2) | 2023.11.23 |
[Spring] WireMock (0) | 2023.08.18 |