Freemarker를 사용하면서 내가 자주 실수하고 놓치는 부분을 정리했따. 



1. null 체크는 ??로 한다.  '변수 == null ?'

freemarker에서는 null check는 ?? 로 이용하면 된다. 


<#if userList??>

<#list userList as id >

    ${id}

</#list>

</#if>



else if 는 항상 붙여 쓴다.!!   #elseif



2. conditional statement(if 문)에서는 '='은 두번 쓰지 않는다. 하나의 = 만을 쓴다. 

<#if category = "home">

<#elseif category = "content">

</#if>




3. map 의 모든 요소 표현

Map<String, String> (O)

Map<String, Object> (X)


<#list hashMap?keys as it>

   ${it} = ${hashMap[it]}

</#list>

        


이런 표현식의 주의점은 map의 value 값이 String, Date, Number 타입이 아닌 타입이 들어왔을 가능성이 높다. 아니면, Object 여도 안된다. 

예를 들어 Map 안에 key가 "key" 인데, value가 Collection type이나 Object type이면 아래와 같은 에러가 발생할 것이다. 


ERROR ~ Template processing error: "Error on line 37, column 29 in searchengine.ftl\nExpecting a string, date or number here, Expression hashMap[it] is instead a freemarker.template.TemplateBooleanModel$2"


자세한 내용은 아래 내용을 참조한다. 
http://fmpp.sourceforge.net/freemarker/app_faq.html#faq_question_14



4. List of map 표현

List<Map<String, Object>> userList 



<#if userList??>

<#list userList as user>

    ${user.content}

    ${user.link}

</#list>

</#if>



또는 (역순 출력, 응근히 많이 쓰임)

<#if userList??>

<#list userList?reverse as user>

    ${user.content}

    ${user.link}

</#list>

</#if>


#list 에서 {$user.content}를 사용하고 있지만 if/elseif 문에서는 무장해제를 변수를 시켜야 한다.


// 잘못된 표현

<#if ${user. content} == 'abc'> abc </#if>    


// 좋은 표현

<#if user. content == 'abc'> abc </#if>    



5. List 표현

List<String> userList  

<#if userList??>

<#list userList as activityId >

   ${activityId}

</#list>

</#if>



더 좋은 정보 

http://viralpatel.net/blogs/iterate-hashmap-in-freemarker-ftl/




6. 숫자 관련

high chart 연동하면서 실수한 부분인데..

freemarker는 디폴트로 1000 이상의 숫자는 ,(comma)를 사용한다. 따라서 json의 배열 요소를 나누는 , 와 1,000의 , 가 동시에 사용될 수 있는 단점이 있다.

이를 위해서는 아래와 같이 하면 1,000 대신 1000 이라는 숫자만 출력한다.

${x?c} 


참고로, 아래 내용을 참고한다.







<#assign x = 1000>

${x}                 <#-- 1,000 -->

${x?string}          <#-- 1,000 -->

${x?c}               <#-- 1000 -->

${x?string.computer} <#-- 1000 -->


http://stuartgunter.wordpress.com/2011/09/01/freemarker-default-number-formatting/




7 </#if> 실수


<#if actionTagGuide.objectType != "mp4">11111

<#else>aaa

<#/if>

이것은 컴파일되지 않는다.  #와 /를 거꾸로 쓸 때가 있어서 /이 먼저 그 다음이 #이 와야 한다.



<#if item.objectType != "mp4">11111

<#else>aaa

</#if>





Posted by '김용환'
,


Cassandra Test Case 실행시 아래와 같은 에러가 발생할 수 있다. (나는 cassandra 2.0.2를 사용중이다.)


Caused by: java.lang.UnsatisfiedLinkError: no snappyjava in java.library.path


이 문제는 아래 버그 리포트에도 나타나 있지만. snappy java 문제는 cassandra 가 아니라 snappy java  wkcp 버그이다..

https://issues.apache.org/jira/browse/CASSANDRA-4400

https://github.com/xerial/snappy-java/issues/12


이를 해결하기 위해서는 snappy버전을 1.0.5-M4로 올리고, netty 3.x 를 포함시킨후,  snappyjava를 include했던 기존 라이브러리에서 snappyjava를 exclusion을 해야 compile 및 실행이 문제 없다. 


<!-- 추가 -->

<dependency>

  <groupId>org.xerial.snappy</groupId>

  <artifactId>snappy-java</artifactId>

  <version>1.0.5-M4</version>

</dependency>

<dependency>

  <groupId>org.jboss.netty</groupId>

  <artifactId>netty</artifactId>

  <version>3.1.0.GA</version>

</dependency>


<!-- 수정 -->

<dependency>

<groupId>com.netflix.astyanax</groupId>

<artifactId>astyanax</artifactId>

<version>1.56.44</version>

<exclusions

    <exclusion>

        <artifactId>snappy-java</artifactId>

        <groupId>org.xerial.snappy</groupId>

    </exclusion>

</exclusions>

</dependency>

 <dependency>

        <groupId>org.apache.cassandra</groupId>

        <artifactId>cassandra-all</artifactId>

        <version>2.0.2</version>

        <exclusions>  

              <exclusion>

                  <artifactId>snappy-java</artifactId>

                  <groupId>org.xerial.snappy</groupId>

                 </exclusion>

        </exclusions>

</dependency>




참고로 netty를 포함해서 컴파일하지 않으면, 아래 에러가 발생한다. 최신버전은 io.netty로 수정되었기 때문에 버전은 약간 낮은게 낫다. 


java.lang.NoSuchMethodError: org.jboss.netty.handler.codec.frame.LengthFieldBasedFrameDecoder.<init>(IIIIIZ)V


Posted by '김용환'
,



' select * .. query시,  binding하는 java domain object의 필드와 매핑되지 못할 때 어떤 일이 벌어지는가?



Table ABC 에 다음과 같은 데이터가 있.


id | surName | name

0 | kim | yonghwan 

1 | lee | junghwan

2 | yang | jungpil



즉, "select * from ABC" 를 mybatis에서 호출할때 resultType 을 Property 로 저장토록 했다.


Property는 id, name 이렇게 있는 경우라면. 어떻게 될 것 인가? 에 대한 문제이다. 


경험상, 문제없이 동작할 것 같다는 느낌이 온다만, 과연 코드로 어떻게 되어 있는지 체크해본다. 


------------------------------------------------------------------------




<org.apache.ibatis.executor.resultset.DefaultResultSetHandler 클래스>


  private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {

    final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);

    boolean foundValues = false;

    for (String columnName : unmappedColumnNames) {

      String propertyName = columnName;

      if (columnPrefix != null && columnPrefix.length() > 0) {

        // When columnPrefix is specified,

        // ignore columns without the prefix.

        if (columnName.startsWith(columnPrefix)) {

          propertyName = columnName.substring(columnPrefix.length());

        } else {

          continue;

        }

      }

      final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());

  

   //(1)

    if (property != null && metaObject.hasSetter(property)) {

        final Class<?> propertyType = metaObject.getSetterType(property);

        if (typeHandlerRegistry.hasTypeHandler(propertyType)) {

          final TypeHandler<?> typeHandler = rsw.getTypeHandler(propertyType, columnName);

          final Object value = typeHandler.getResult(rsw.getResultSet(), columnName);

          if (value != null || configuration.isCallSettersOnNulls()) { // issue #377, call setter on nulls

            if (value != null || !propertyType.isPrimitive()) {

              metaObject.setValue(property, value);

            }

            foundValues = true;

          }

        }

      }

    }

 // (2)

    return foundValues;

  }



(1) table query로부터 읽을 때 column name이 존재하면 Java domain object의 setter를 호출하여 value를 매핑한다.  (자세히 보면 타입체크와 설정 관련 정보가 연관되어 있지만 패스한다.)

(2) 그러나 만약 column name 이 존재하지 않으면, 그냥 넘어간다. 저장하지 않게 된다. 


따라서 select * 하는 코드가 안전할 수 있다. 그러나, 좀 더 detail 개발(적확히는 유지보수관점) 을 위해서는 select 문에 필드를 명시적으로 추가하는 것이 좋은 듯 하다. java domain object와 매핑 자체가 편리하다. 



참고로 위 메소드에서 연관된 정보를 소개한다. 



1) configuration.isCallSettersOnNulls() 


과거에는 null 일 때는 호출하지 않았는데, 반드시 초기화가 필요한 부분이 존재 해서 설정할 수 있도록 지정할 수 있게 되어 있다. 


http://mybatis.github.io/mybatis-3/ko/configuration.html


Specifies if setters or map's put method will be called when a retrieved value is null. It is useful when you rely on Map.keySet() or null value initialization. Note primitives such as (int,boolean,etc.) will not be set to null.


<settings> <setting name="callSettersOnNulls" value="true"/>
<settings>



2) configuration.isMapUnderscoreToCamelCase()


db property가 underbar를 많이 쓴다. 이 때 camel case로 바꿔주는 작업을 할지를 결정할 수 있다. 

즉, car_maker 라는 property는 carMaker 라는 java domain object로 만들 수 있다. 


http://mybatis.github.io/mybatis-3/configuration.html#settings


Enables automatic mapping from classic database column names A_COLUMN to camel case classic Java property names aColumn.


<settings> <setting name="mapUnderscoreToCamelCase" value="true"/>

<settings>


Posted by '김용환'
,

mockito, junit, Hamcrest maven dependency 이용시 va.lang.NoSuchMethodError: org/hamcrest/Matcher.describeMismatch 처리 방법


출처 

http://tedvinke.wordpress.com/2013/12/17/mixing-junit-hamcrest-and-mockito-explaining-nosuchmethoderror/



mockito-core-1.9.5와 JUnit 4.11은 각각 내부적으로 의존관계(transitive dependency) 를 가지고 있는 Hamcresto-core 버전이 다르기 때문에 이슈가 된다. 다음과 같이 hamcrest-core 를 exclude 시키고 hamcrest-all을 따로 include 한다. 


<dependency>

    <groupId>org.mockito</groupId>

    <artifactId>mockito-core</artifactId>

    <version>1.9.5</version>

    <exclusions>

        <exclusion>

            <artifactId>hamcrest-core</artifactId>

            <groupId>org.hamcrest</groupId>

        </exclusion>

    </exclusions>

</dependency>

<dependency>

    <groupId>junit</groupId>

    <artifactId>junit</artifactId>

    <version>4.11</version>

    <exclusions>

        <exclusion>

            <artifactId>hamcrest-core</artifactId>

            <groupId>org.hamcrest</groupId>

        </exclusion>

    </exclusions>

</dependency>

<dependency>

    <groupId>org.hamcrest</groupId>

    <artifactId>hamcrest-all</artifactId>

    <version>1.3</version>

</dependency>






Posted by '김용환'
,


MAC/ java 1.7에서 java를 이용해서 SSL 통신시 발생한 SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed 과 InstallCert.java에서 발생한 java.lang.UnsupportedOperationException at InstallCert$SavingTrustManager.getAcceptedIssuers(installcert.java:147) 를 해결하는 방법을 정리했다.



https 서버와 통신해야 하는 상황에서 인증서 없이  HttpsURLConnection 을 사용하면 SSLHandshakeException 이 발생한다. 


javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target


신뢰할 수 있도록 인증서 파일을 java 디렉토리에 두어야 한다. 


InstallCert.java 는 인터넷에 찾으면 다양하게 있다. 원래 소스는 http://blogs.sun.com/andreas/resource/InstallCert.java"

에 있었으나 더이상 운영되고 있지 않아서.. 여기에 올린다.


또한 인터넷에 떠도는 기존의 소스를 사용하면, 아래와 같은 Exception이 발생한다.


javax.net.ssl.SSLException: java.lang.UnsupportedOperationException

at sun.security.ssl.Alerts.getSSLException(Alerts.java:208)

at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1884)

at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1842)

at sun.security.ssl.SSLSocketImpl.handleException(SSLSocketImpl.java:1825)

at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1346)

at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323)

at InstallCert.main(installcert.java:63)

Caused by: java.lang.UnsupportedOperationException

at InstallCert$SavingTrustManager.getAcceptedIssuers(installcert.java:147)

at sun.security.ssl.AbstractTrustManagerWrapper.checkAlgorithmConstraints(SSLContextImpl.java:926)

at sun.security.ssl.AbstractTrustManagerWrapper.checkAdditionalTrust(SSLContextImpl.java:872)

at sun.security.ssl.AbstractTrustManagerWrapper.checkServerTrusted(SSLContextImpl.java:814)

at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1323)

at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:153)

at sun.security.ssl.Handshaker.processLoop(Handshaker.java:868)

at sun.security.ssl.Handshaker.process_record(Handshaker.java:804)

at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1016)

at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312)

at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339)


그래서,  http://infposs.blogspot.kr/2013_06_01_archive.html  

에 있는 파일을 받아 사용하면된다. 변경된 내용은 다음과 같다. 



 private static class SavingTrustManager implements X509TrustManager {

...

        public X509Certificate[] getAcceptedIssuers() {

            //throw new UnsupportedOperationException();

                return new X509Certificate[0];

        }


..

}


변경된 파일은 다음과 같다. 

InstallCert.java


이제는 저장소를 만들고 jdk/jre/lib/security 디렉토리 밑에 복사한다. 


$ java InstallCert 1.1.1.1:8000

    jssecacerts 파일이 생성됨


$ sudo cp jssecacerts /Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home/jre/lib/security/




그리고, HttpsURLConnection 을 이용한 서버를 연결하면 문제가 없다. 




Posted by '김용환'
,


spring mvc 406 에러는 세가지 경우이다.


1. jacklib가 없어서 발생

아래 추가..

<dependency>

<groupId>org.codehaus.jackson</groupId>

<artifactId>jackson-mapper-asl</artifactId>

<version>1.9.13</version>

</dependency>


2. spring annotaion 이 빠져 있어서 발생


<mvc:annotation-driven />


http://stackoverflow.com/questions/16335591/spring-mvc-json-406-not-acceptable



3. json으로 표현못하는 Class를 json으로 만들때..

적당하게 map, array, list로 표현할 수 있도록 변환한다.











Posted by '김용환'
,

[tomcat] setenv.sh

general java 2014. 2. 12. 11:40



톰캣에서 환경 변수 추가하는 방법이다. 


catalina.sh에 아래와 같이 환경병수를 읽도록 되어 있다. 


# Ensure that any user defined CLASSPATH variables are not used on startup,

# but allow them to be specified in setenv.sh, in rare case when it is needed.

CLASSPATH=


if [ -r "$CATALINA_BASE/bin/setenv.sh" ]; then

  . "$CATALINA_BASE/bin/setenv.sh"

elif [ -r "$CATALINA_HOME/bin/setenv.sh" ]; then

  . "$CATALINA_HOME/bin/setenv.sh"

fi




setenv.sh 에 내가 원하는 정보를 넣어주면 간단히 된다.

예를 들어.. setenv.sh에 다음과 같이 넣어주면 catalina.sh 수정없이 간단해진다. 



#!/bin/bash


export CATALINA_OPTS="$CATALINA_OPTS -server -Doperation.mode=dev -Dfile.encoding=UTF8 -Dorg.apache.catalina.STRICT_SERVLET_COMPLIANCE=true -Duser.timezone=GMT+9 -Xmx256m -XX:MaxPermSize=52m"






Posted by '김용환'
,


Spring container의  context:property-placeholder 를 이용하여 여러개 이상의 properties 파일을 읽어오는 경우. 다음과 같이 사용하면 하나의 properties만 읽는 현상이 되어 문제가 발생한다. 


<context:property-placeholder location="classpath:properties/jdbc.properties"/>

<context:property-placeholder location="classpath:properties/cassandra.properties"/>



다음과 같이 사용하면 된다.



 <context:property-placeholder order="1" ignore-unresolvable="true"  location="classpath:properties/jdbc.properties" />


 <context:property-placeholder order="2" ignore-unresolvable="true"  location="classpath:properties/cassandra.properties" />



Posted by '김용환'
,

 

Common IO(2.1이상) 을 이용해서 File에 특정 내용을 계속 Append 되게 하는 자바 코드

 

File file = new File("d:\\aaaaaaaaaaaaaaaaaaaaa-" + day + ".txt");
FileUtils.writeStringToFile(file, new Date() + "\n", true);
FileUtils.writeStringToFile(file, sql + "\n", true);

Posted by '김용환'
,

 

 

Spring Cache Annotation을 쓰던, AOP를 이용해서 Spring Cache를 이용하든

Spring Cache에 대한 default Key 생성 방법을 알아두면 편리하다.

 

만약 Cache로 사용하기 위해서 아래와 같이 코딩을 했다면, Key의 타입은 바로 int 이다.

 

@Cacheable

public String a() {

}

 

그렇다면, 아래와 같이 코딩했다면. Key의 타입은 long 이 된다.

@Cacheable

public String b(long a) {

}

 

만약 파라미터가 String, String 이면, Key의 타입은 int 이다.

@Cacheable

public String b(String b1, String b2) {

}

 

 

Spring Cache 의 Key default 생성 알고리즘은 코드는 아래와 같다.

 

public class DefaultKeyGenerator implements KeyGenerator {

 

public static final int NO_PARAM_KEY = 0;
public static final int NULL_PARAM_KEY = 53;

 

public Object generate(Object target, Method method, Object... params) {
    if (params.length == 1) {
        return (params[0] == null ? NULL_PARAM_KEY : params[0]);
    }
   if (params.length == 0) {
        return NO_PARAM_KEY;
    }
    int hashCode = 17;
    for (Object object : params) {
        hashCode = 31 * hashCode + (object == null ? NULL_PARAM_KEY : object.hashCode());
    }
    return Integer.valueOf(hashCode);
}

}

 

 

만약 long 값의 범위를 가지는 String 값을 캐쉬로 쓰기 위해서는 자칫 잘못하다가는 int로 변환될 수 있는 단점을 가지고 있다. 또한, 파라미터가 없을 때, cache의 혼동이 올 수 있다.

 

따라서,Spring Cache를 하나의 메소드로 단순하게 사용할 것이 아니면, Custom Key Generation을 넣는 코드를 아래와 같이 넣는 것이 아주 좋다. 명확한 키 생성은 디버그에 훨씬 도움이 된다. (특히 Redis를 쓰는 경우에는… 아주 효과적인듯.)

 

사례1)

@Cacheable(value = "cacheName", key = "T(com.google.cache.KeyCreatorBean).createKey(#p0, #p1)")
@Override
public String getName(String id, String class) {
  ...
}

 

 

package com.google.cache.KeyCreatorBean;

public class KeyCreatorBean  {
    public static Object createKey(Object o1, Object o2) {
        return o1 + ":" + o2;
    }
}

 

사례2)

 

<cache:advice id="localRedisCacheAdvice" cache-manager="localRedisCacheManager">
    <cache:caching>
        <cache:cacheable cache="localCache" method="getReceivable" key="T(com.google.cache.KeyCreatorBean).createKey(#p0, #p1)" />
    </cache:caching>
</cache:advice>

 

 

 

package com.google.cache.KeyCreatorBean;

public class KeyCreatorBean  {
    public static Object createKey(Object o1, Object o2) {
        return o1 + ":" + o2;
    }
}

 

 

참고로. Spring Cache AOP에 대한 정보는 org.springframework.cache.interceptor.CacheAspectSupport  클래스를 참조할 필요가 있다.

 

http://grepcode.com/file/repository.springsource.com/org.springframework/org.springframework.context/3.1.0/org/springframework/cache/interceptor/CacheAspectSupport.java

Posted by '김용환'
,