Jest는 팩토리 패턴을 사용하는 클래스이다. 그래서 Spring에서 Jest 사용시 FactoryBean으로 생성하여 JestClient를 생성할 수 있다. FactoryBean으로 코딩을 했는데, 최근 유행이 @Configuration을 쓴다고 해서 써봤다.
FactoryBean 을 이용하여 JestClientFactoryBean을 생성한 예이다.
public class JestClientFactoryBean implements FactoryBean<JestClient>, InitializingBean, DisposableBean {
private String endPoint;
private JestClient jestClient;
private int maxTotalConnection = 10;
private int connectionTimeout = 1000;
private int maxTotalConnectionPerRoute = 1000;
private int readTimeout = 3000;
public void setMaxTotalConnection(int maxTotalConnection) {
this.maxTotalConnection = maxTotalConnection;
}
public void setConnectionTimeout(int connectionTimeout) {
this.connectionTimeout = connectionTimeout;
}
public void setSocketTimeout(int socketTimeout) {
this.readTimeout = socketTimeout;
}
public void setEndPoint(String endPoint) {
this.endPoint = endPoint;
}
public void setMaxTotalConnectionPerRoute(int maxTotalConnectionPerRoute) {
this.maxTotalConnectionPerRoute = maxTotalConnectionPerRoute;
}
@Override
public JestClient getObject() throws Exception {
return jestClient;
}
@Override
public Class<?> getObjectType() {
return JestClient.class;
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(endPoint, "end point should be configured");
HttpClientConfig clientConfig = new HttpClientConfig.Builder(endPoint)
.multiThreaded(true)
.maxTotalConnection(maxTotalConnection)
.connTimeout(connectionTimeout)
.readTimeout(readTimeout)
.defaultMaxTotalConnectionPerRoute(maxTotalConnectionPerRoute)
.build();
JestClientFactory factory = new JestClientFactory();
factory.setHttpClientConfig(clientConfig);
jestClient = factory.getObject();
}
@Override
public void destroy() throws Exception {
if (jestClient != null) {
jestClient.shutdownClient();
}
}
}
<bean id="searchJestClient" class="com.google.elasticsearch.JestClientFactoryBean">
<property name="endPoint" value="http://es-search.google.com:9200"/>
<property name="maxTotalConnection" value="1000" />
<property name="maxTotalConnectionPerRoute" value="1000"/>
<property name="connectionTimeout" value="3000" />
<property name="socketTimeout" value="3000" />
</bean>
JestClientConfiguration 클래스이다. JestClient Bean을 생성한다. 참고로 DisposableBean의 destroy와 같은 역할은 @Bean(destroyMethod = "shutdownClient") 이 할 수 있다. 코드가 많이 간결해진 느낌이다.
@Configuration은 그 자체로 빈으로 역할을 하지 못해서, @Bean 메소드만 빈 생성을 위해서 사용할 수 있다.
import io.searchbox.client.JestClient;
import io.searchbox.client.JestClientFactory;
import io.searchbox.client.config.HttpClientConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JestClientConfiguration {
// @Value("${elasticsearch.search.endpoint}")
private String endPoint = "http://es-search.google.com:9200/";
// @Value("${elasticsearch.search.max_connection}")
private int maxTotalConnection = 10;
// @Value("${elasticsearch.search.conn_timeout
private int connTimeout = 1000;
// @Value("${elasticsearch.search.read_timeout
private int readTimeout = 3000;
@Bean(destroyMethod = "shutdownClient")
public JestClient jestClient(){
Assert.notNull(endPoint, "end point should be configured");
HttpClientConfig clientConfig = new HttpClientConfig.Builder(endPoint)
.multiThreaded(true)
.maxTotalConnection(maxTotalConnection)
.connTimeout(connTimeout)
.readTimeout(readTimeout)
.build();
JestClientFactory factory = new JestClientFactory();
factory.setHttpClientConfig(clientConfig);
JestClient client = factory.getObject();
return client;
}
// 다른 property를 이용하여 또 다른 Bean을 생성할 수 있다.
@Bean(destroyMethod = "shutdownClient")
public JestClient anotherJestClient(){
Assert.notNull(endPoint, "end point should be configured");
HttpClientConfig clientConfig = new HttpClientConfig.Builder(endPoint)
.multiThreaded(true)
.maxTotalConnection(maxTotalConnection)
.connTimeout(connTimeout)
.readTimeout(readTimeout)
.build();
JestClientFactory factory = new JestClientFactory();
factory.setHttpClientConfig(clientConfig);
JestClient client = factory.getObject();
return client;
}
}
SearchService 이다. Jest를 통해 간단한 질의를 하고 POJO로 바인딩한다. JestClientConfiguration이 두 개의 Bean이 있어서 @Inject 대신 @Autowired @Qualifier를 동시에 사용한다.
@Service
public class SearchService {
// JestClientConfiguration이 하나의 Bean만 가진다면, JestClient Bean을 바로 생성할 수 있다.
// @Inject
// private JestClient jestClient;
@Autowired
@Qualifier("jestClient")
private JestClient jestClient;
// SearchObject는 POJO이며 Jest의 Json을 객체로 Marshalling해준다.public List<SearchObject> search(String queryString) {
if (StringUtils.isEmpty(queryString)) {
return Collections.emptyList();
}
String query = "{\n"
+ " \"query\": {\n"
+ " \"filtered\" : {\n"
+ " \"query\" : {\n"
+ " \"query_string\" : {\n"
+ " \"query\" : \"" + queryString + "\"\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " },\n"
+ " \"size\" : 10"
+ "}";
Search.Builder searchBuilder = new Search.Builder(query).addIndex(INDEX_NAME).addType(TYPE_NAME);
Search search = searchBuilder.build();
List<SearchObject> searchObjects = Lists.newArrayList();
try {
SearchResult result = execute(search);
if (result == null) {
return Collections.emptyList();
}
List<SearchResult.Hit<SearchObject, Void>> hits = result.getHits(SearchObject.class);
for (SearchResult.Hit< SearchObject, Void> hit: hits) {
SearchObject objectSource = hit.source;
searchObjects.add(objectSource);
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return searchObjects;
}
private SearchResult execute(Search action) {
try {
SearchResult result = jestClient.execute(action);
if (!result.isSucceeded()) {
logger.warn("Failed to search elasticSearch action: " + result.getErrorMessage()
+ " " + result.getJsonString());
}
return result;
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return null;
}
@Autowired, @Qualifier 사용하는 대신 아래 코드처럼 @Autowired 만 사용할 수 있다.
또한, JestClient를 Java8의 Optional 객체로 감쌀 수 있다. maven 빌드는 문제 없으나 Intellij IDEA14.1.x에서는 에러를 표시하지만 잘 돌아간다.
(Could not autowired..라는 에러가 보이지만, 이는 IDEA에서 Java8 연동을 완벽하지 않은 것으로 보인다.)
Optional을 사용하면 객체가 바로 null이 아닌 null을 포함한 객체가 되므로 NPE를 최대한 방지 할 수 있다. Optional 클래스를 이용하면 isPresent()를 통해 null 체크를 할 수 있다.
@Autowired
private Optional<JestClient> jestClient;
...
private SearchResult execute(Search action) {
if (!jestClient.isPresent()) {
return null;
}
try {
SearchResult result = jestClient.get().execute(action);
logger.error(result.getJsonString());
if (!result.isSucceeded()) {
logger.warn("Failed to search ElasticSearch action: " + result.getErrorMessage()
+ " " + result.getJsonString());
}
return result;
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return null;
}
그리고, 빈 생성이 되는지 테스트는 이렇게 할 수 있다.
public class JestClientConfigurationTest {
@Test
public void test() {
AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext(JestClientConfiguration.class);
JestClient jestClient = (JestClient) appContext.getBean("jestClient");
if (jestClient != null) System.out.println("OK");
}
}