반응형

HttpClient가 필요할 때 Spring에서 제공하는 RestTemplate 을 주로 사용합니다.

그런데 연동해야하는 서버가 EUC-KR Encoding 만 지원하는 경우가 심심치 않게 있습니다.


이럴 때 파라미터를 EUC-KR 로 Encoding 해서 보내야하는데 RestTemplate 로 EUC-KR Encoding 을 못해 고생한 경험이 있어서 그 방법을 공유해 봅니다.



아래 코드를 기준으로 설명 드리겠습니다.


Case1 은 잘못된 Encoding 입니다.

Case2,3 은 정상입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
 
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriTemplateHandler;
 
public class Test {
 
  // RestTemplate을 이용해 EUC-KR 인코딩으로 요청 보내기
  public static void main(String[] args) throws UnsupportedEncodingException {
    String url = "http://google.com?q=" + URLEncoder.encode("테스트""EUC-KR");
    RestTemplate restOperations = new RestTemplate();
 
    // Case 1
    restOperations.getForObject(url, String.class);
 
    // Case2
    restOperations.getForObject(URI.create(url), String.class);
 
    // Case3
    DefaultUriTemplateHandler uriTemplateHandler = (DefaultUriTemplateHandler) restOperations.getUriTemplateHandler();
    uriTemplateHandler.setStrictEncoding(true);
    restOperations.getForObject(url, String.class);
  }
}



Case1

- RestTemplate 은 기본적으로 org.springframework.web.util.DefaultUriTemplateHandler 을 사용해서 String url을 자동으로 Encoding 합니다.

- 그런데 DefaultUriTemplateHandler 는 "UTF-8" 로 Encoding 이 Hard coding 되어 있어서 변경이 불가합니다.

- 그래서 Case1의 경우는 12 Line에서 EUC-KR로 encoding한 문자열을 자동으로 UTF-8로 다시 한번 Encoding 합니다.


Created GET request for "http://google.com?q=%25C5%25D7%25BD%25BA%25C6%25AE"


Case2

- String url로 넘기지않고 URI 객체를 넘기면 자동으로 Encoding 을 하지 않기 때문에 정상 동작합니다.


Created GET request for "http://google.com?q=%C5%D7%BD%BA%C6%AE"


Case3

- String url로 넘기더라도 DefaultUriTemplateHandler 의 설정에서 setStrictEncoding 을 true로 설정하면 자동으로 Encoding을 하지 않기 때문에 정상 동작합니다.


Created GET request for "http://google.com?q=%C5%D7%BD%BA%C6%AE"



** 참고.


Spring RestTemplate Encoding 이라고 Googling을 하면 org.springframework.http.converter.StringHttpMessageConverter 이 많이 출현하는데 이 녀석 때문에 삽질을 많이 했죠. 무식이 죄 ㅜㅜ.

StringHttpMessageConverter 는 Request가 아닌 Response의 Encoding을 처리한다고 보시면 됩니다.


반응형
반응형

Spring boot 실행할 때 args 를 전달해서 사용하는 방법입니다.

Spring boot에서  org.springframework.boot.ApplicationArguments 를 제공하고 있어서 Bean으로 받아서 사용하면 간단하게 쓸 수 있습니다.


1. getSourceArgs

 - 입력한 args 그대로 배열로 받아 옵니다.


2. getOptionNames

 - args 앞에 "--" 를 붙이면 옵션으로 인식 합니다. 옵션 args 사용 형식 --NAME=VALUE 

 - "--fruit=apple" 이렇게 args를 사용하면

 - getOptionName는 fruit 처럼 option name 들의 배열을 받아 옵니다.


3. getNonOptionArgs

- "--" 가 없는 경우 NonOption으로 인식합니다.

- "--" 가 없는 args 들의 값들을 받다 옵니다.


설명보다 아래 sample code를 보시는게 이해가 빠를 것 같습니다.

 


DemoApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
package com.example.demo;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class DemoApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}


Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.example.demo;
 
import java.util.List;
import java.util.Set;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
 
@Component
public class Test implements ApplicationListener<ContextRefreshedEvent> {
  @Autowired
  private ApplicationArguments applicationArguments;
 
  @Override
  public void onApplicationEvent(ContextRefreshedEvent event) {
    String[] sourceArgs = applicationArguments.getSourceArgs();
    List<String> nonOptionArgs = applicationArguments.getNonOptionArgs();
    Set<String> optionNames = applicationArguments.getOptionNames();
 
    System.out.println("---원본 args---");
    for (String sourceArg : sourceArgs) {
      System.out.println(sourceArg);
    }
    System.out.println("---옵션아닌 args---");
    for (String nonOptionArg : nonOptionArgs) {
      System.out.println(nonOptionArg);
    }
    System.out.println("---옵션 args---");
    for (String optionName : optionNames) {
      List<String> optionValues = applicationArguments.getOptionValues(optionName);
      for (String optionValue : optionValues) {
        System.out.println(optionName + ":" + optionValue);
      }
    }
  }
 
}



Execute

 $JAVA_HOME/bin/java -jar demo.jar HI --MY_NAME=GONI --YOUR_NAME=GUEST --my.hobby=영화 --my.hobby=독서

* option args 에서 name 을 같게 해서 여러번 써서 List로 받을 수 있습니다.


Result

---원본 args---
--spring.output.ansi.enabled=always
HI
--MY_NAME=GONI
--YOUR_NAME=GUEST
--my.hobby=영화
--my.hobby=독서
---옵션아닌 args---
HI
---옵션 args---
spring.output.ansi.enabled:always
MY_NAME:GONI
YOUR_NAME:GUEST
my.hobby:영화
my.hobby:독서


반응형
반응형

Java 1.7 부터  java.nio.file.Files 를 통해서 File 의 생성날짜, 마지막 접근 날짜, 마지막 수정 날짜를 조회할 수 있습니다.


Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
 
public class Test {
  public static void main(String[] args) throws IOException {
    File file = new File("/test/logs/debug.log");
    if (file.isFile()) {
      BasicFileAttributes readAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
      System.out.println(readAttributes.creationTime().toMillis());
      System.out.println(readAttributes.lastAccessTime().toMillis());
      System.out.println(readAttributes.lastModifiedTime().toMillis());
    }
  }
}


Result

1
2
3
1511861243970
1511920327380
1511920793902



* 참고

linux 환경에서는 운용해본 결과 creationTime == lastModifiedTime 값이 같고 lastAccessTime 값이 변하지 않는 가장 과거의 값이었습니다.


* 결론

파일 생성시간을 가져오는 건 불가능해 보입니다. ㅜㅜ


반응형
반응형

Spring boot + Mybatis 로 프로젝트를 진행하고 있었습니다.

그런데 잘 되던 녀석이 갑자기 Exception이 나면서 구동이 안되는 겁니다.

ClassNotFound 라니 정말 당황했습니다. ㄷㄷㄷ


환경(Environment)

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.2.RELEASE</version>
    <relativePath /<!-- lookup parent from repository -->
  </parent>
 
 
<dependencies>
...
    <!-- mybaits -->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.2.1</version>
    </dependency>
...
</dependencies>


mybatis/config.xml

1
2
3
4
  <typeAliases>
    <package name="kr.co.tistory.goni9071.dao" />
    <package name="kr.co.tistory.goni9071.entity" />
  </typeAliases>



문제(Problem)

1. Mybatis 설정에서 TypeAliases의 package 를 이용하면 Entity 파일을 인식하지 못함.

2. 더 당황스러운 건 STS 개발환경에서는 문제가 없으나  executable jar/war 에서만 오류가 발생.


Exception

1
2
3
4
5
6
7
8
9
10
$ java -jar target/xxxx.jar
...
        at org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XMLMapperBuilder.java:118)
        ... 44 more
Caused by: org.apache.ibatis.type.TypeException: Could not resolve type alias 'Message'.  Cause: java.lang.ClassNotFoundException: Cannot find class: Message
        at org.apache.ibatis.type.TypeAliasRegistry.resolveAlias(TypeAliasRegistry.java:120)
        at org.apache.ibatis.builder.BaseBuilder.resolveAlias(BaseBuilder.java:149)
        at org.apache.ibatis.builder.BaseBuilder.resolveClass(BaseBuilder.java:116)
        ... 48 more
...



원인(Cause)

1. application.properties 에서 아래와 같이 설정시에는 문제가 없음.

1
2
#mybatis
mybatis.config-location=classpath:mybatis/config.xml


2.  그러나 아래처럼 Java Config를 이용해 수동으로 설정시 오류가 발생.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
@MapperScan(value = Constants.PACKAGE_BASE + ".dao", sqlSessionFactoryRef = "coreSqlSessionFactory")
public class CoreRepositoryConfig {
 
  @Bean(name = "coreSqlSessionFactory")
  @Primary
  public SqlSessionFactory coreSqlSessionFactory(@Qualifier("coreDatasource") DataSource coreDatasource,
      ApplicationContext applicationContext) throws Exception {
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(coreDatasource);
    sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource("classpath:mybatis/config.xml"));
    // 문제의 주범 및 해결책
    sqlSessionFactoryBean.setVfs(SpringBootVFS.class); 
    //
    return sqlSessionFactoryBean.getObject();
  }
}



해결책(Solution)

1. 위의 코드처럼 아래 한줄이 추가되면 문제 해결.

1
sqlSessionFactoryBean.setVfs(SpringBootVFS.class); 




반응형
반응형

mybatis 사용중에 5000건 정도를 조회해서 화면에 표현해야하는 업무가 있었습니다.


툴에서 조회시에는 0.5 초 정도 걸리는데 웹상에서 jdbc를 통해 조회를하면 10초를 넘기는 상황이 발생했습니다.


구글링을 통해 찾아보니 mybatis에 fetchSize라는 옵션이 있더군요. 아래 처럼 추가했더니 0.5 초 이하로 속도가 나옵니다. @.@


1
<select id="selectListByDate" resultMap="logRM" fetchSize="1000">



fetchSize 어떻게 작동하길래 이렇게 속도가 빨라지나 찾아보았습니다.


설정하지 않으면 기본 값은 10 이라고 합니다.


fetchSize 가 10 이면 5000건을 조회하면 실제 db에서 500번을 조회하게 되는 겁니다.


fetchSize 가 1000 이면 5번이면 되는 겁니다.


이해하기 쉽게 예를 들면 같은 결과 값을 조회하는데 쿼리를 500번 날려서 받는 것과 5번만 날려서 받는 것 중에 어떤게 빠를까요?

반응형
반응형

2018/01/29 - [java] - java poi excel write 엑셀 쓰기

 

위 글에서 poi를 이용해 excel write 를 예제를 공유했었습니다.

 

그런데 데이터가 좀 많아지니 "gc overhead limit exceeded" 요런 Out Of Memory 오류가 발생합니다.

 

-Xmx 옵션을 통해서 Heap memory 를 늘리는 것도 한계가 있습니다.

 

이런 경우 해결책으로 org.apache.poi.xssf.usermodel.XSSFWorkbook 대신  org.apache.poi.xssf.streaming.SXSSFWorkbook 를 사용할 수 있습니다.

 

아래 코드를 참조하세요. SXSSFWorkbook 를 사용하면 지정한 row 개수 단위로 끊어서 flush 할 수 있습니다.

 

 

테스트 코드 입니다. 

java vm option을 -Xmx60m 정도 설정하고 SXSSFWorkbook 와 XSSFWorkbook 를 번갈아서 테스트하면 맨 아래 오류 코드를 확인할 수 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
 
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.ss.usermodel.Font;
 
public class ExcelUtilTest {
  public static void main(String[] args) throws IOException, InvalidFormatException {
    ExcelUtil excel = new ExcelUtil("시트이름"100); // SXSSFWorkbook
    //ExcelUtil excel = new ExcelUtil("시트이름"); // XSSFWorkbook
    excel.setOffset(11);
    List<String> header = Arrays.asList("번호""이름""제목""기간""기타");
    excel.addRow("f8f8f8", Font.BOLDWEIGHT_BOLD, header);
    for (int i = 0; i < 10000; i++) {
      List<String> body = Arrays.asList(String.valueOf(i), "홍길동""제목입니다.""기간입니다.""기타");
      excel.addRow(body);
      excel.addRow(body);
      excel.addRow(body);
    }
    FileOutputStream fos = new FileOutputStream("/test/엑셀테스트1.xlsx");
    excel.write(fos);
 
  }
}
 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at org.apache.xmlbeans.impl.store.Saver$SynthNamespaceSaver.<init>(Saver.java:883)
    at org.apache.xmlbeans.impl.store.Saver.<init>(Saver.java:121)
    at org.apache.xmlbeans.impl.store.Saver$TextSaver.<init>(Saver.java:916)
    at org.apache.xmlbeans.impl.store.Cursor._xmlText(Cursor.java:546)
    at org.apache.xmlbeans.impl.store.Cursor.xmlText(Cursor.java:2436)
    at org.apache.xmlbeans.impl.values.XmlObjectBase.xmlText(XmlObjectBase.java:1500)
    at org.apache.poi.xssf.model.SharedStringsTable.getKey(SharedStringsTable.java:134)
    at org.apache.poi.xssf.model.SharedStringsTable.addEntry(SharedStringsTable.java:180)
    at org.apache.poi.xssf.usermodel.XSSFCell.setCellType(XSSFCell.java:797)
    at org.apache.poi.xssf.usermodel.XSSFRow.createCell(XSSFRow.java:176)
    at org.apache.poi.xssf.usermodel.XSSFRow.createCell(XSSFRow.java:37)
    at kr.go.seoul.scc.admin.web.util.ExcelUtil.addRow(ExcelUtil.java:117)
    at kr.go.seoul.scc.admin.web.util.ExcelUtil.addRow(ExcelUtil.java:77)
    at kr.go.seoul.scc.admin.web.util.ExcelUtilTest.main(ExcelUtilTest.java:21)
 
반응형
반응형

리눅스에서 Junit 테스트를 해야할 경우가 있는데 테스트 할 때 필요한 라이브러리들 입니다.

스프링 부트 이용시 기준 입니다.

tomcat-embed-core 의 경우 테스트 환경에 servlet이 포함된 경우 필요합니다.


assertj-core-2.5.0.jar  
junit-4.12.jar                              
spring-boot-test-1.4.2.RELEASE.jar                
spring-test-4.3.4.RELEASE.jar
hamcrest-core-1.3.jar   
spring-boot-starter-test-1.4.2.RELEASE.jar  
spring-boot-test-autoconfigure-1.4.2.RELEASE.jar  
tomcat-embed-core-8.5.6.jar



실행시에는 아래와 같이 org.junit.runner.JUnitCore 클래스를 메인 클래스로 args를 테스트할 클래스로 지정하면 됩니다.

TEST_CLASS=com.tistory.goni9071.Test
CP=클래스패스
$JAVA_HOME/bin/java -cp $CP org.junit.runner.JUnitCore $TEST_CLASS


반응형
반응형

분명 어제까지 문제 없던 프로젝트 였는데 오늘 갑자기 빌드 오류가 발생합니다. ㅎㄷㄷ


1
2
The project was not built since its build path is incomplete. Cannot find the class file for org.springframework.core.io.support.PropertySourceFactory. 
The type org.springframework.core.io.support.PropertySourceFactory cannot be resolved. It is indirectly referenced from required .class files    MainApplication.java    


대체 왜? 아무리 생각해도 모르겠습니다.


그렇게 이것 저것 원인을 추측하며 구글링하던 중

1
mvn dependency:purge-local-repository


이 놈을 찾아 내었습니다. 


두둥.


maven 중에 위 명령어를 실행하면 해당 pom.xml 에 명시된 dependency 들에 대해서 Local(~/.m2/repository)에 다운 받아 놓은 라이브러리들을 모두 지우고 다시 받아 줍니다. 


cache를 clean하고 re download 하는 거죠.


그랬더니 거짓말처럼 오류가 사라졌네요. @@

뭘까 대체..

반응형
반응형

스프링 부트의 기본 커넥션 풀이 원래 Tomcat JDBC Connection Pool 이었는데 2.0.0 버전 부터 HikariCP 로 변경 되었습니다.


1.5.9 RELEASE

29.1.2 Connection to a production database

Production database connections can also be auto-configured using a pooling DataSource. Here’s the algorithm for choosing a specific implementation:

  • We prefer the Tomcat pooling DataSource for its performance and concurrency, so if that is available we always choose it.
  • Otherwise, if HikariCP is available we will use it.
  • If neither the Tomcat pooling datasource nor HikariCP are available and if Commons DBCP is available we will use it, but we don’t recommend it in production and its support is deprecated.
  • Lastly, if Commons DBCP2 is available we will use it.

If you use the spring-boot-starter-jdbc or spring-boot-starter-data-jpa ‘starters’ you will automatically get a dependency to tomcat-jdbc.



2.0.0 RELEASE

29.1.2 Connection to a Production Database

Production database connections can also be auto-configured by using a pooling DataSource. Spring Boot uses the following algorithm for choosing a specific implementation:

  1. We prefer HikariCP for its performance and concurrency. If HikariCP is available, we always choose it.
  2. Otherwise, if the Tomcat pooling DataSource is available, we use it.
  3. If neither HikariCP nor the Tomcat pooling datasource are available and if Commons DBCP2 is available, we use it.

If you use the spring-boot-starter-jdbc or spring-boot-starter-data-jpa “starters”, you automatically get a dependency to HikariCP.


그래서 2.0 버전 부터는 Tomcat JDBC Connection Pool 을 쓰려면 몇 가지 설정 추가해야 합니다.


application.properties 에서 어떤 데이터소스를 사용할지 명시해야 합니다.

1
spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource


그리고 tomcat-jdbc 라이브러리도 가 추가 되어야합니다. 

<maven - pom.xml>

1
2
3
4
5
6
    <!-- tomcat-jdbc -->
    <dependency>
      <groupId>org.apache.tomcat</groupId>
      <artifactId>tomcat-jdbc</artifactId>
      <scope>provided</scope>
    </dependency>



application.properties 에 tomcat connection pool 설정입니다. 위의 2가지를 설정하지 않으면 아래 설정이 동작하지 않습니다.

1
2
3
4
5
6
7
8
9
spring.datasource.tomcat.max-active=50
spring.datasource.tomcat.max-idle=50
spring.datasource.tomcat.min-idle=10
spring.datasource.tomcat.max-wait=-1
spring.datasource.tomcat.initial-size=10
spring.datasource.tomcat.test-on-borrow=true
spring.datasource.tomcat.test-while-idle=true
spring.datasource.tomcat.validation-query=select 'GONI'
spring.datasource.tomcat.time-between-eviction-runs-millis=3000


반응형
반응형

fasterxml 을 이용해 JSON 문자열을 객체로 변환하는데 단순 Snake Case 가 아닌 대문자 Snake Case 인 경우 아래 코드처럼 참고해 네이밍 전략을 수정해주면 됩니다.


2018/06/18 - [기타] - 스네이크 표기법 Snake Case



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
 
import java.io.IOException;
 
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy.SnakeCaseStrategy;
 
 
public class Test{
  private static UpperCaseStrategy UPPER_CASE = new UpperCaseStrategy();
 
  public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
    ObjectMapper mapper = new ObjectMapper();
    mapper.setPropertyNamingStrategy(UPPER_CASE);
    Code code = mapper.readValue("{\"CODE\": \"A01\", \"CODE_MSG\":\"청하\"}", Code.class);
    System.out.println("code : " + code.getCode());
    System.out.println("codeMsg : " + code.getCodeMsg());
  }
 
  public static class Code {
    private String code;
    private String codeMsg;
 
    public String getCode() {
      return code;
    }
 
    public void setCode(String code) {
      this.code = code;
    }
 
    public String getCodeMsg() {
      return codeMsg;
    }
 
    public void setCodeMsg(String codeMsg) {
      this.codeMsg = codeMsg;
    }
 
  }
 
  public static class UpperCaseStrategy extends SnakeCaseStrategy {
    /**
     * 
     */
    private static final long serialVersionUID = -284516094725996654L;
 
    @Override
    public String translate(String input) {
      return super.translate(input).toUpperCase();
    }
  }
 
}
 


반응형

+ Recent posts