반응형

기본 예제

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

+ Recent posts