응답 처리시 Header 결과를 변경하려면, ResponseEntity<String>을 사용하고, ResponseEntity<String>를 리턴한다.


ResponseEntity 생성자 사용시 Http Status의 값을 함께 전달한다. 

@RequestMapping(value="/map/get/{id:.+}", method = RequestMethod.GET)
public ResponseEntity<String> getMap(@PathVariable String id) {

// json = ""; // errorJson = ""; if(!StringUtils.isEmpty(url)) {
return new ResponseEntity<String>(json, HttpStatus.OK);
}

return new ResponseEntity<String>(errorJson, HttpStatus.NOT_FOUND);
}


Posted by '김용환'
,




Spring mvc에서 post 혹은 put으로 잘 메소드를 정의한 Restful 서비스를 개발하다가 실수한 부분이 있어서 공유한다.


아래의 URL은 url 옆에 : 이 하나 더 붙어서 POST 처리가 전혀 안되었다. 오타 하나가 삽질을 ㅠㅠ

@RequestMapping(value="/staticmap/put/{id:.+}/{url:}", method = {RequestMethod.POST, RequestMethod.PUT})


 $  curl -XPOST http://localhost:8080/put/1/2

{"timestamp":1442921858855,"status":405,"error":"Method Not Allowed","exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request method 'POST' not supported","path":"/put/1/2"}




참고로,intellij에서는 spring mvc url value 값을 일부 분석하고 에러를 보여주는데, 


 : 에 대한 에러는 발견하지 못하지만,


 아래는 에러로 표시해 준다.

{id:+}


intellij에서는 아래는 이상하다는 느낌으로 보여준다.

{id.+}



Posted by '김용환'
,


spring boot에서 테스트를 진행하려면, spring-boot-starter-test 를 pom.xml에 추가한다.

testCompile("org.springframework.boot:spring-boot-starter-test")


spring boot start test의 maven pom.xml설정으로 보면 junit, mockito, hamcrest, spring-test가 포함되어 있다. 요즘 많이 사용하고 검증된 lib을 쓸 수 있다.

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
</dependencies>


간단히 mockito로 테스트하는 코드를 작성하는데, 간단한 예제를 소개한다.

GatewayService interface, GatewayServiceImpl class, 

GatewayRepository interface, GatewayRepositoryImpl class가 있다.


GatewayServiceImpl 에서 GatewayRepositoryImp 객체를 autowiring 하도록 되어 있다. 


@InjectMocks 의 객체 안에 사용 중인 객체를 @Spy 에서 정의된 객체를 Inject할 수 있다. 클래스로 Inject하는 것이니 interface가 아니면 된다. 

@RunWith(MockitoJUnitRunner.class)
public class GatewayServiceTest {

@Spy
private GatewayRepository mock = new GatewayRepositoryImpl();

@InjectMocks
private GatewayService service = new GatewayServiceImpl();

@Before
public void before() {
// precondition
ImmutableMap<String, String> data = ImmutableMap.of(
"X", "123");

GatewayRepository mock = mock(GatewayRepository.class);
when(mock.get("8.8.8.8", 500)).thenAnswer(invocation -> data);
}

@Test
public void test() {
Data data = service.get("8.8.8.8");

assertEquals("latitude", data.lat, 1.0, 0);
assertEquals("latitude", data.lng, 2.0, 0);
}


 그리고, RestTemplate의 integration test용 TestRestTemplate 클래스를 사용할 수 있다.

private RestTemplate template = new TestRestTemplate();
@Test
public void productionTest() {
String body = template.getForEntity("http://internal.google.com/coord/8.8.8.8", String.class).getBody();
assertThat(body, containsString("latlng"));
}



참고

http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html

Posted by '김용환'
,


zookeeper는 ant 빌드라서, mvn assembly를 쉽게 할 수 없다.

그래서 딱 필요한 정보만 모아서 zookeeper client로 사용 가능하게 할 수 있다.


$ tree

.

├── bin

│   ├── zkCli.sh

│   └── zkEnv.sh

├── lib

│   ├── jline-0.9.94.jar

│   └── log4j-1.2.15.jar

├── version.txt

└── zookeeper-3.3.6.jar


2 directories, 6 files


3.3.6 zk 클라이언트를 쓸 수 있는 파일만 모아놨다. 


zk-3.3.6.zip




실행은 압축을 풀어 zkCli.sh을 사용 가능한 문법으로 사용한다.

$ ./bin/zkCli.sh -server localhost:2181 'ls /'

Posted by '김용환'
,



devtools(cool restart)을 적용하려면 spring boot 1.3부터 사용할 수 있다.

devtools 의존 lib를 추가하고, gradlew를 생성한 후 bootrun 매개변수를 추가한다.




1. build.gradle에 다음을 추가한다.

compile("org.springframework.boot:spring-boot-devtools")



2. gradlew 실행



$ ./gradlew bootrun



(참고로 gradlew 명령어를 살펴보려면 다음을 실행한다. 

$ ./gradlew task)





(참고)


devtools을 적용할 수 없지만, 간단히 실행할 수 있다.


$ cat _run.sh


#!/bin/sh


./gradlew clean build && java -jar build/libs/{spring-boot-파일}.jar





Posted by '김용환'
,


Google place api 사용시 Http Rest 사용하는 것이 좋다.  rest api가 언제든지 바뀔 수 있고..

java api로 처음 뜨는 아래 java client는 테스트해보니 문제가 많다. null 체크 및 위경도가 안맞아서 쓰지 않은 것이 좋다. 

RestTemplate과 SimpleJson(또는 Jackson)으로 처리하는 것이 좋을 듯..



https://github.com/windy1/google-places-api-java

Posted by '김용환'
,




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 '김용환'
,




java에서 Jest 사용시 다음 에러가 발생한다면 common-httpclient 버전이 이슈가 된 것이다. 

java.lang.NoClassDefFoundError: org/apache/http/impl/conn/PoolingClientConnectionManager



httpclient을 다음과 같이 버전업을 하니 문제가 사라졌다.

    <dependency>

      <groupId>org.apache.httpcomponents</groupId>

      <artifactId>httpclient</artifactId>

      <version>4.4.1</version>

    </dependency>


Posted by '김용환'
,



@JsonAutoDetect를 사용하면, JSON de/serialization 시, Spring 3/Jaskson lib로부터 Getter, Setter없이 POJO를 쉽게 사용할 수 있다.


import com.fasterxml.jackson.annotation.JsonAutoDetect;

@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
public class POJO {
private String id;

private String title;
private String address; }



Posted by '김용환'
,


아래 싸이트의예제를 바탕으로 assertj 예제를 사용한다.

http://knight76.tistory.com/entry/Java8-Function%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EA%B0%9D%EC%B2%B4-%EB%B3%B5%EC%82%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%98%88%EC%A0%9C
 


java 8에서 동작할 수 있도록 assert-core 3.x 이상 버전에에 의존한다. java7 이하를 쓸 경우는 2.x 버전을 이용한다.
난 java8을 사용해서 3.1.0을 사용했다.

<pom.xml>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>



 소스는 다음과 같다. 보라색으로 칠한 것을 주의깊게 본다.
import static org.assertj.core.api.Assertions.assertThat;
import java.util.stream.Stream;
import java.util.Arrays;
import java.util.List;

public class ConvertObjectTest { @Test
public void convertObjectListWithFunctionTest() {
// given
OneMember samuel = new OneMember(1, "Samuel");
OneMember daisy = new OneMember(2, "Daisy");
OneMember matt = new OneMember(3, "Matt");

// when
List<OnePerson> persons = Stream.of(samuel, daisy, matt)
.map(OnePerson::new)
.sorted(comparing(OnePerson::getId))
.collect(toList());

List<OnePerson> list = Arrays.asList(new OnePerson(1, "Samuel"), new OnePerson(2, "Daisy"), new OnePerson(3, "Matt"));
assertThat(persons).hasSameElementsAs(list);
}

일반적으로 List와 같은 Collection 에 대한 테스트 코드가 상당히 verbose 하다. 첫 번째는 x, 두 번째는 y 이렇게 assert equal로 테스트를 해야 하는데, assertJ의 가장 특징은 이런 verbose한 코드를 줄일 수 있다는 점에서 참 좋았다.


예제와 같이 List<OnePerson> 객체 두개를 한 줄에서 assert로 체크해 볼 수 있어서 좋다.


관련 URL 


http://knight76.tistory.com/entry/AspectJ-Array-List-Map%EC%9D%84-%EC%89%BD%EA%B2%8C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EC%98%88%EC%A0%9C



Posted by '김용환'
,