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");
}
}




Posted by '김용환'
,