Spring-Mybatis와 Spring 개발환경에서 여러 DB (multiple DB)를 동시에 사용할 때, ibatis처럼 쓰면 문제가 생길 수 있다. 즉 DB가 꼬이거나 SqlSession instance로 인한 문제가 발생할 수 있다. 

(에러 내용)  No unique bean of type [org.apache.ibatis.session.SqlSessionFactory] is defined

(결론) org.mybatis.spring.mapper.MapperScannerConfigurer 클래스 대신 org.mybatis.spring.mapper.MapperFactoryBean을 사용해서야 문제가 해결됨

 



다음과 같은 기본적인 설정을 쓰고 있었다.

 

    <bean id="batch1SqlSession" class="org.mybatis.spring.SqlSessionTemplate" >
          <constructor-arg index="0" ref="batch1SqlSessionFactory" />
    </bean>

        <bean id="batch2SqlSession" class="org.mybatis.spring.SqlSessionTemplate" >
          <constructor-arg index="0" ref="batch2SqlSessionFactory" />
    </bean>

 

 

 

<bean id="xxxxMapper"  class="org.mybatis.spring.mapper.MapperScannerConfigurer">
       <property name="basePackage" value="xxxx" />
       <property name="sqlSessionFactoryBeanName" value="batch1SqlSessionFactory" />
   </bean>

 

 

한 개의 DB 사용시는 문제 없는데, 여러 개의 DB 접속시에 문제가 발생할 수 있다. 

(원인은 잘못된 API 사용이다.)

 

 

즉, org.mybatis.spring.mapper.MapperScannerConfigurer를 사용하는 경우

1) basePackage를 잘못 지정해서 No MyBatis mapper was found in 'xxxx' package. 가 발생한다.

(MapperScannerConfigurer.java:397) No MyBatis mapper was found in 'xxxx' package. Please check your configuration.

 

2) Exception 발생

 

class A1 {

    @Autowired
     SqlSession batch1SqlSession;
}

 

class A2 {

    @Autowired
    SqlSession batch2SqlSession;
}

 

SqlSession을 이용하려다가 No unique bean of type 이라는 exception을 만날 수 있다.

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [org.mybatis.spring.SqlSessionTemplate] is defined: expected single matching bean but found 2: [batch1SqlSession, batch2SqlSession]

  구글 검색하면, Qualifier Annotation 같은 것으로 해결할 수 있다고 하나, 잘못된 API 사용이기 때문에 해결할 수 있다.

 

3) 엉뚱한 DB로 접근한다.

설정을 어떻게 어떻게 바꿨는데..

A db, B db에 접속했는데, A db에 ‘가’ 쿼리가 날아가기를 기대했는데, B db에 ‘나’쿼리가 접속하여 테이블 없다는 Exception이 발생할 수 있다.

 

4) 이런 문제를 해결하기 위해서 약간의 공수를 사용해서 문제를 해결할 수 있다.

  1)번 예제의 xxxxMapper bean id의 basePackage를 엉뚱한데 두면 해결이 된다.

   다만 WARN 에러로그가 남겨진다.

[WARN] No MyBatis mapper was found in ‘XXXX’ package. Please check your configuration.

 

소스상 보면, 요행으로 문제가 되는 코드를 피해갈 뿐이었다.

 

http://jarvana.com/jarvana/view/org/mybatis/mybatis-spring/1.0.2/mybatis-spring-1.0.2-sources.jar!/org/mybatis/spring/mapper/MapperScannerConfigurer.java?format=ok

 

@Override protected Set<BeanDefinitionHolder> doScan(String... basePackages) {

Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

if (beanDefinitions.isEmpty()) {

logger.warn("No MyBatis mapper was found in '" + MapperScannerConfigurer.this.basePackage + "' package. Please check your configuration.");

} else {

}

 

 

=> 해결

Spring-Mybatis 문서를 참조 (http://code.google.com/p/mybatis/wiki/Spring)해서  다시 코딩하기로 했다. 

org.mybatis.spring.mapper.MapperScannerConfigurer 클래스 대신 org.mybatis.spring.mapper.MapperFactoryBean을 사용해서야 문제가 해결되었다.

 

1) bean 설정

 

 

<bean id="googleGatewayDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
….
    </bean>
  

 

    <bean id="googleGatewaySqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="googleGatewayDataSource" />
        <property name="configLocation" value="classpath:storage/google-gateway-service-config.xml" />
    </bean>
   
        <bean id="gatewaySqlSession" class="org.mybatis.spring.SqlSessionTemplate" >
          <constructor-arg index="0" ref="googleGatewaySqlSessionFactory" />
    </bean>

 

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
     <property name="mapperInterface" value=" com.google.center.repository.GoolgeGatewayDAO" />

     <property name="sqlSessionFactory" ref="googleGatewaySqlSessionFactory" />
     <property name="sqlSessionTemplate" ref="googleGatewaySqlSession" />
</bean>     

 

2) 소스 : mybatis mapper 설정과 연동하는 DAO 코드

 

public interface GoolgeGatewayDAO {

     public Template getTemplate(@Param("channelId") String channelId, @Param("language") String language);

     public String getI18nChannelName(@Param("channelId") String channelId, @Param("language") String language);

}



또는 간결하게 아래 stackoverflow를 통해 쉽게 해결가능하다. 

http://stackoverflow.com/questions/4746766/spring-and-mybatis-multiple-data-sources-setup


Posted by '김용환'
,

 

 

1. Google의 Spring Cache Annotation과 혼동하면 안됨. 중복 사용으로 인해서 꼬이지 않도록 해야 함

http://code.google.com/p/ehcache-spring-annotations

 

2. Spring Cache를 쓰기로 결정했다면 잘 유의할 내용

(1) pom dependency 추가

// Spring 3 은 기본 추가. .

 

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache-core</artifactId>
    <version>2.5.0</version>
</dependency>

 

(2) Spring 문서를 정독

http://static.springsource.org/spring/docs/3.1.0.M1/spring-framework-reference/html/cache.html

 

(3) 괜찮은 Tutorial

http://sonegy.wordpress.com/2011/12/29/spring-3-1과-ehcache/

여기에 오타가 있어서 bean 정의시 xml schem location에 문제가 있음. 아래와 같이 사용.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:cache="http://www.springframework.org/schema/cache"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd" >

 

(3) Cachable 적용이 안 된다고 있다면,다음을 확인

   1) 그냥 클래스의 method에 추가해서 쓰면 적용이 안됨. Proxy를 이용하기 때문에 interface를 상속하는 클래스의 public method에서만 사용 가능

   2) self-invocation 는 안됨

 

(4) SPEL을 이용해서 key를 정의할 수 있음. 

   1) 특정 파라미터만 이용해서 key 정의 가능

http://stackoverflow.com/questions/11889428/spring-cacheable-with-complex-keys-still-executed

@Cacheable(value="cahceName", key="#p0.concat('-').concat(#p1)")

   자세한 내용은  이곳(http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/cache.html#cache-spel-context) 을 참조

 

   2) 설정 + 클래스 이용

http://stackoverflow.com/questions/6730641/how-can-i-set-a-custom-keygenerator-for-spring-cache

<cache:annotation-driven key-generator="myKeyGenerator"/>
<bean id="myKeyGenerator" class="com.abc.MyKeyGenerator" />

import org.springframework.cache.interceptor.KeyGenerator;
public class MyKeyGenerator implements KeyGenerator {

    public Object generate(Object target, Method method, Object... params) {
}}

 

   3) 복잡한 경우라면. AOP를 사용할 수 있다.

Old

http://springtips.blogspot.kr/2007/06/caching-methods-result-using-spring-and_23.html

new : cache:advice 이용

http://blog.skcc.com/531

 

   4) Default Key Generator 구현 정보

http://www.jarvana.com/jarvana/view/org/springframework/spring-context/3.1.0.RELEASE/spring-context-3.1.0.RELEASE-sources.jar!/org/springframework/cache/interceptor/DefaultKeyGenerator.java?format=ok

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

 

 

(5) Ehcache 설정 중 중요한 부분

overflowToDisk 는 정말 필요할 경우에만.

LRU는 캐쉬의 기본 정책. 안쓰는 녀석은 퇴출(eviction)

maxElementsInMemory 는 얼마나 가지고 있을지(size)

timeToLiveSeconds 는 cache expire 정책.

Posted by '김용환'
,

 

org.springframework.web.servlet.i18n.LocaleChangeInterceptor 를 이용해서 Web browser로부터 httpServletRequest에서 locale 정보 구함.

 

그리고, 다음의 클래스로 잘 포장한다.

org.springframework.web.servlet.i18n.SessionLocaleResolver
org.springframework.context.support.ReloadableResourceBundleMessageSource

 

message는 spring tag를 이용해서 출력한다.

 

다음의 링크를 잘 활용.

 

http://yarbong.tistory.com/33

http://www.mkyong.com/spring-mvc/spring-mvc-internationalization-example/

http://www.devcken.com/?p=226

Posted by '김용환'
,

 

 

${propertyname:defaultValue}

 

예)

 

<property name="port" value="${service.http.port:80}" />

Posted by '김용환'
,

 

Spring Batch 의 Job Repository 생성시 Spring Batch 와 ResourcelessTransactionManager 를 함께 사용할 때 의도치 않게 문제가 된다.

 

따라서, Jdbc template을 바로 사용하는 다음의 방법으로 변경하였다.

 

bean 설정 파일

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch-2.1.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd">
                          
       <bean id="JobRepositoryCreationService" class="jp.naver.talk.channel.noticenter.batch.repository.JobRepositoryServiceImpl">
        <property name="initScripts" value="classpath:/org/springframework/batch/core/schema-mysql.sql" />
    </bean>
   
    <bean id="JobRepositoryDropService" class="jp.naver.talk.channel.noticenter.batch.repository.JobRepositoryServiceImpl">
        <property name="initScripts" value="classpath:/org/springframework/batch/core/schema-drop-mysql.sql" />
    </bean>
   
    <bean id="scriptExecutionDAO" class="jp.naver.talk.channel.noticenter.batch.repository.ScriptExecutionDAOImpl">
        <property name="dataSource" ref="repositoryDataSource" />
    </bean>
   
</beans>

 

 

JobRepositoryService

 

public interface JobRepositoryService {
   
    public void execute(String replacePreFix);
   

}

 

 

 

public class JobRepositoryServiceImpl implements JobRepositoryService {
    private static final Log logger = LogFactory.getLog(JobRepositoryServiceImpl.class);
   
    @Autowired
    ScriptExecutionDAO scriptExecutionDAO;

    private Resource scriptResource;
   
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(scriptResource);
    }

    private String stripComments(List<String> list) {
        StringBuffer buffer = new StringBuffer();
        for (String line : list) {
            if (!line.startsWith("//") && !line.startsWith("--")) {
                buffer.append(line + "\n");
            }
        }
        return buffer.toString();
    }

    public void setInitScripts(Resource initScripts) {
        this.scriptResource = initScripts;
    }
   
    @Override
    public void execute(String replacePreFix) {
        String[] scripts;
        try {
            scripts = StringUtils.delimitedListToStringArray(stripComments(IOUtils.readLines(scriptResource.getInputStream())), ";");
        } catch (IOException e) {
            throw new BeanInitializationException("Cannot load script from [" + scriptResource + "]", e);
        }
       
        for (int i = 0; i < scripts.length; i++) {
            String script = scripts[i].trim();
            if (StringUtils.hasText(script)) {
                try {
                    // replace
                    script = script.replaceAll("BATCH_", replacePreFix);
                    script = script.replaceAll("_FK", "_" + replacePreFix + "FK");
                   
                    logger.info(script);
                    scriptExecutionDAO.execute(script);
                } catch (DataAccessException e) {
                    throw e;
                }
            }
        }
    }
}

 

 

 

ScriptExecutionDAO

 

 

public interface ScriptExecutionDAO {
   
    public void execute(String script);
   
}

 

 

public class ScriptExecutionDAOImpl implements ScriptExecutionDAO {
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
   
    @Override
    public void execute(String script) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.execute(script);
    }

}

 

 

실제 Job repository 호출하는 부분

 

@Autowired
private JobRepositoryService JobRepositoryCreationService;

 

@Autowired
private JobRepositoryService JobRepositoryDropService;

 

@RequestMapping(value = "/repository/{type}/{prefix}", method = {RequestMethod.POST, RequestMethod.GET})
@ResponseBody
public String repository(@PathVariable String type, @PathVariable String prefix) throws Exception {
    if (type == null || prefix == null) {
        return "The type or prefix is not given";
    }
   
    if (prefix.length() < 2) {
        return "Prefix length is too shrot.";
    }
   
    if (type.equalsIgnoreCase("create")) {
        JobRepositoryCreationService.execute(prefix);
    } else if (type.equalsIgnoreCase("drop")) {
        JobRepositoryDropService.execute(prefix);
    } else {
        return "unrecognized type : " + type;
    }
    return "success";
}

Posted by '김용환'
,

 

Spring Batch 의 가장 큰 이슈(절대 문제는 아님)는 하나의 Repository를 여러 Job 이 사용하다가 실패될 가능성이 많다. 따라서 가장 확실하게 하나의 Job은 하나의 Repository를 쓰는 것이 가장 안전하다!

 

http://knight76.tistory.com/1191  (관련내용)

 

그러나, 이런 이슈 때문에 매번 Table 이름에 Prefix를 줄 때 상당히 힘이 들 수 밖에 없다. 오타라도 나면. 끔찍하다. 지울 때 잘 못 치우면 지우지도 귀찮은 일이 벌어지기도 한다.

 

mysql 의 예제) Spring Batch lib 에 담겨 있음.

 

schema-mysql.sql

-- Autogenerated: do not edit this file

CREATE TABLE BATCH_JOB_INSTANCE  (

JOB_INSTANCE_ID BIGINT  NOT NULL PRIMARY KEY ,

VERSION BIGINT ,

JOB_NAME VARCHAR(100) NOT NULL,

JOB_KEY VARCHAR(32) NOT NULL,

constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY)

) ENGINE=InnoDB;

CREATE TABLE BATCH_JOB_EXECUTION  (

JOB_EXECUTION_ID BIGINT  NOT NULL PRIMARY KEY ,

VERSION BIGINT  ,

JOB_INSTANCE_ID BIGINT NOT NULL,

CREATE_TIME DATETIME NOT NULL,

START_TIME DATETIME DEFAULT NULL ,

END_TIME DATETIME DEFAULT NULL ,

STATUS VARCHAR(10) ,

EXIT_CODE VARCHAR(100) ,

EXIT_MESSAGE VARCHAR(2500) ,

LAST_UPDATED DATETIME,

constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID)

references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID)

) ENGINE=InnoDB;

CREATE TABLE BATCH_JOB_PARAMS  (

JOB_INSTANCE_ID BIGINT NOT NULL ,

TYPE_CD VARCHAR(6) NOT NULL ,

KEY_NAME VARCHAR(100) NOT NULL ,

STRING_VAL VARCHAR(250) ,

DATE_VAL DATETIME DEFAULT NULL ,

LONG_VAL BIGINT ,

DOUBLE_VAL DOUBLE PRECISION ,

constraint JOB_INST_PARAMS_FK foreign key (JOB_INSTANCE_ID)

references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID)

) ENGINE=InnoDB;

CREATE TABLE BATCH_STEP_EXECUTION  (

STEP_EXECUTION_ID BIGINT  NOT NULL PRIMARY KEY ,

VERSION BIGINT NOT NULL,

STEP_NAME VARCHAR(100) NOT NULL,

JOB_EXECUTION_ID BIGINT NOT NULL,

START_TIME DATETIME NOT NULL ,

END_TIME DATETIME DEFAULT NULL ,

STATUS VARCHAR(10) ,

COMMIT_COUNT BIGINT ,

READ_COUNT BIGINT ,

FILTER_COUNT BIGINT ,

WRITE_COUNT BIGINT ,

READ_SKIP_COUNT BIGINT ,

WRITE_SKIP_COUNT BIGINT ,

PROCESS_SKIP_COUNT BIGINT ,

ROLLBACK_COUNT BIGINT ,

EXIT_CODE VARCHAR(100) ,

EXIT_MESSAGE VARCHAR(2500) ,

LAST_UPDATED DATETIME,

constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID)

references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)

) ENGINE=InnoDB;

CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT  (

STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,

SHORT_CONTEXT VARCHAR(2500) NOT NULL,

SERIALIZED_CONTEXT TEXT ,

constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID)

references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID)

) ENGINE=InnoDB;

CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT  (

JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,

SHORT_CONTEXT VARCHAR(2500) NOT NULL,

SERIALIZED_CONTEXT TEXT ,

constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID)

references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)

) ENGINE=InnoDB;

CREATE TABLE BATCH_STEP_EXECUTION_SEQ (ID BIGINT NOT NULL) ENGINE=MYISAM;

INSERT INTO BATCH_STEP_EXECUTION_SEQ values(0);

CREATE TABLE BATCH_JOB_EXECUTION_SEQ (ID BIGINT NOT NULL) ENGINE=MYISAM;

INSERT INTO BATCH_JOB_EXECUTION_SEQ values(0);

CREATE TABLE BATCH_JOB_SEQ (ID BIGINT NOT NULL) ENGINE=MYISAM;

INSERT INTO BATCH_JOB_SEQ values(0);

 

 

Spring Batch Table을 생성 하고 삭제하는 아주 간단한 예제를 공유한다. 버그가 많겠지만.. 대충 쓰기에는 좋을 듯 하다. (돌아갈 정도로만 만들었음..)

 

예제는 Web call로 mysql에 Spring Batch repository table 생성하는 코드이다.  코드는 완전하지 않을 수 있음.

 

 

 

1. 설정파트

 

접속해야 할 DB 정보는 db.properties 에 저장한다.

repository.jdbc.url=…
repository.jdbc.username=…
repository.jdbc.password=…
repository.jdbc.maxActive=2
repository.jdbc.maxIdle=1

 

/beans-batchdb.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch-2.1.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd">
                          
    <bean id="repositoryDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="url" value="${repository.jdbc.url}" />
        <property name="driverClassName" value="net.sf.log4jdbc.DriverSpy"/>
        <property name="username" value="${repository.jdbc.username}" />
        <property name="password" value="${repository.jdbc.password}" />
        <property name="maxActive"             value="${repository.jdbc.maxActive}"/>
        <property name="maxIdle"             value="${repository.jdbc.maxIdle}"/>
        <property name="validationQuery"     value="select 1"/>
        <property name="defaultAutoCommit" value="false"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testWhileIdle" value="true"/>
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <property name="numTestsPerEvictionRun" value="3"/>
        <property name="minEvictableIdleTimeMillis" value="-1"/>
        <property name="maxWait" value="8000"/> 
    </bean>
   
    <bean id="repositoryDataSourceSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="repositoryDataSource" />

       <!—mybatis 설정 추가 -->
        <property name="configLocation" value="classpath:/configfile.xml" />
    </bean>
   
    <bean id="repositorySqlSession" class="org.mybatis.spring.SqlSessionTemplate">
          <constructor-arg index="0" ref="repositoryDataSourceSqlSessionFactory" />
    </bean>
   
</beans>

 

 

/job/create-repository/benas-create-repository.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch-2.1.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd">
                          
    <bean id="dataSourceInitializerForCreation" class="com.google.batch.repository.DataSourceInitializer">
        <property name="dataSource" ref="repositoryDataSource" />
        <property name="initScripts" value="classpath:/org/springframework/batch/core/schema-mysql.sql" />
    </bean>
   
    <bean id="dataSourceInitializerForDrop" class="com.google.batch.repository.DataSourceInitializer">
        <property name="dataSource" ref="repositoryDataSource" />
        <property name="initScripts" value="classpath:/org/springframework/batch/core/schema-drop-mysql.sql" />
    </bean>
   
     <job id="repositoryJobTableCreation" parent="repository_simpleJob" xmlns="http://www.springframework.org/schema/batch">
        <step id="step1" parent="repository_simpleStep">
            <tasklet ref="repositoryTasklet" />
        </step>
    </job>
   
    <job id="repositoryJobTableDrop" parent="repository_simpleJob" xmlns="http://www.springframework.org/schema/batch">
        <step id="step2" parent="repository_simpleStep">
            <tasklet ref="repositoryDropTasklet" />
        </step>
    </job>
    
     <bean id="repositoryJobExecutionListener" class="com.google.batch.repository.RepositoryJobExecutionListener"/>
    
     <bean id="repository_simpleJob" class="org.springframework.batch.core.job.SimpleJob" abstract="true">
        <property name="jobRepository" ref="jobRepository" />
         <property name="jobExecutionListeners" ref="repositoryJobExecutionListener" />
    </bean>

    <bean id="repository_simpleStep" class="org.springframework.batch.core.step.item.SimpleStepFactoryBean" abstract="true">
        <property name="jobRepository" ref="jobRepository" />
        <property name="transactionManager" ref="resourcelessTransactionManager"></property>
        <property name="commitInterval" value="1"/>
    </bean>
    
     <bean id="repositoryTasklet" class="com.google.batch.repository.RepositoryTasklet">
         <property name="dataSourceInitializer" ref="dataSourceInitializerForCreation"></property>
     </bean>
    
      <bean id="repositoryDropTasklet" class="com.google.batch.repository.RepositoryTasklet">
         <property name="dataSourceInitializer" ref="dataSourceInitializerForDrop"></property>
     </bean>
   
     <bean id="jobRepository" class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
        <property name="transactionManager" ref="resourcelessTransactionManager"/>
    </bean>
   
    <bean id="resourcelessTransactionManager" class="org.springframework.batch.support.transaction.ResourcelessTransactionManager" />
   
    <bean id="repositoryJobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
        <property name="jobRepository" ref="jobRepository" />
    </bean>
   
   
</beans>

 

 

beans.xml


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

<import resource="/beans-batchdb.xml"/>
   
<context:annotation-config />
   
<context:component-scan base-package="com.google"/> 

 

<!--  job #1  : create repository -->
<import resource="classpath:/job/create-repository/beans-create-repository.xml" />

 

 

2. 자바 코드

com.google.batch.repository.DataSourceInitailizer

 

/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.batch.repository;

 

import java.io.IOException;
import java.util.List;

import javax.sql.DataSource;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
* Wrapper for a {@link DataSource} that can run scripts on start up and shut
* down.  Us as a bean definition <br/><br/>
*
* Run this class to initialize a database in a running server process.
* Make sure the server is running first by launching the "hsql-server" from the
* <code>hsql.server</code> project. Then you can right click in Eclipse and
* Run As -&gt; Java Application. Do the same any time you want to wipe the
* database and start again.
*
* @author Dave Syer
* @author Kim, Yong Hwan
*
*/
public class DataSourceInitializer implements InitializingBean, DisposableBean {

    private static final Log logger = LogFactory.getLog(DataSourceInitializer.class);

    private Resource[] initScripts;

    private DataSource dataSource;

    private String replacePreFix;

    public String getReplacePreFix() {
        return replacePreFix;
    }

    public void setReplacePreFix(String replacePreFix) {
        this.replacePreFix = replacePreFix;
    }

    protected void finalize() throws Throwable {
        super.finalize();
        logger.debug("finalize called");
    }

    public void destroy() {
        // no use
    }

    public void afterPropertiesSet() throws Exception {
        Assert.notNull(dataSource);
    }

    public void initialize() {
            destroy();
            if (initScripts != null) {
                for (int i = 0; i < initScripts.length; i++) {
                    Resource initScript = initScripts[i];
                    logger.debug("Initializing with script: " + initScript);
                    doExecuteScript(initScript);
                }
            }
    }

    @SuppressWarnings("unchecked")
    private void doExecuteScript(final Resource scriptResource) {
        if (scriptResource == null || !scriptResource.exists())
            return;
        TransactionTemplate transactionTemplate = new TransactionTemplate(new DataSourceTransactionManager(dataSource));
        transactionTemplate.execute(new TransactionCallback() {

            public Object doInTransaction(TransactionStatus status) {
                JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
                String[] scripts;
                try {
                    scripts = StringUtils.delimitedListToStringArray(stripComments(IOUtils.readLines(scriptResource.getInputStream())), ";");
                } catch (IOException e) {
                    throw new BeanInitializationException("Cannot load script from [" + scriptResource + "]", e);
                }
                for (int i = 0; i < scripts.length; i++) {
                    String script = scripts[i].trim();
                    if (StringUtils.hasText(script)) {
                        try {
                           // replace
                            script = script.replaceAll("BATCH_", replacePreFix);
                            script = script.replaceAll("_FK", "_" + replacePreFix + "FK");
                           
                            logger.info(script);
                            jdbcTemplate.execute(script);
                        } catch (DataAccessException e) {
                            if (script.toLowerCase().contains("drop")) {
                                logger.debug("DROP script failed (ignoring): " + script);
                            } else {
                                throw e;
                            }
                        }
                    }
                }
                return null;
            }

        });

    }

    private String stripComments(List<String> list) {
        StringBuffer buffer = new StringBuffer();
        for (String line : list) {
            if (!line.startsWith("//") && !line.startsWith("--")) {
                buffer.append(line + "\n");
            }
        }
        return buffer.toString();
    }

    public void setInitScripts(Resource[] initScripts) {
        this.initScripts = initScripts;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }


}

 

 

 

 

com.google.batch.repository.RepositoryJobExecutionListener


package com.google.batch.repository;

 

import java.util.Map;

import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.batch.core.JobParameter;
import org.springframework.batch.core.JobParameters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;

public class RepositoryJobExecutionListener implements JobExecutionListener {

    private ConfigurableApplicationContext context;
   
    @Autowired
    public void setContext(ConfigurableApplicationContext ctx){
        this.context = ctx;
    }
   
    @Override
    public void beforeJob(JobExecution paramJobExecution) {
        JobParameters params = paramJobExecution.getJobInstance().getJobParameters();
        Map<String, JobParameter> map = params.getParameters();
        String replacePreFix = map.get("prefix").toString();
        if (!replacePreFix.endsWith("_")) {
            replacePreFix = replacePreFix + "_";
        }

        DataSourceInitializer dsinit =(DataSourceInitializer)context.getBean("dataSourceInitializerForCreation");
        dsinit.setReplacePreFix(replacePreFix);
       
        DataSourceInitializer dsinit2 =(DataSourceInitializer)context.getBean("dataSourceInitializerForDrop");
        dsinit2.setReplacePreFix(replacePreFix);
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        if( jobExecution.getStatus() == BatchStatus.COMPLETED ){
            //job success
            System.out.println("completed");
        }
        else if(jobExecution.getStatus() == BatchStatus.FAILED){
            //job failure
            System.out.println("failed");
        }
    }

}

 

 

com.google.batch.repository.RepositoryTasklet

package com.google.batch.repository;

 

import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;

 

public class RepositoryTasklet implements Tasklet {
    private DataSourceInitializer dataSourceInitializer;
   
    public DataSourceInitializer getDataSourceInitializer() {
        return dataSourceInitializer;
    }

    public void setDataSourceInitializer(DataSourceInitializer dataSourceInitializer) {
        this.dataSourceInitializer = dataSourceInitializer;
    }
   
    @Override
    public RepeatStatus execute(StepContribution contribution,
            ChunkContext chunkContext) throws Exception {
        // creation or deletion table
        dataSourceInitializer.initialize();
        return RepeatStatus.FINISHED;
    }
}

 

 

 

JobRepositoryController

 

package com.google.batch.controller;

 

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.converter.DefaultJobParametersConverter;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

 

@Controller
public class JobRepositoryController {
    @Autowired
    private JobLauncher repositoryJobLauncher;

    @Autowired
    private Job repositoryJobTableCreation;

    @Autowired
    private Job repositoryJobTableDrop;
   
    @RequestMapping(value = "/repository/{type}/{prefix}", method = {RequestMethod.POST, RequestMethod.GET})
    @ResponseBody
    public String repository(@PathVariable String type, @PathVariable String prefix) throws Exception {
        if (type == null || prefix == null) {
            throw new WrongParamException();
        }
       
        if (prefix.length() < 2) {
            throw new WrongParamException();
        }

        String[] parameters = new String[2];
        parameters[0] = "prefix=" + prefix;

         Properties props = StringUtils.splitArrayElementsIntoProperties(parameters, "=");
JobParametersBuilder builder = new JobParametersBuilder();

         builder.addLong("currTime", System.currentTimeMillis());

for(Object okey : props.keySet() ) {
     String key = (String) okey; 
             builder.addString(key, (String) props.get(key));
        }

JobParameters jobParameters = builder.toJobParameters();



        if (type.equalsIgnoreCase("create")) {
            repositoryJobLauncher.run(repositoryJobTableCreation, jobParameters);
        } else if (type.equalsIgnoreCase("drop")) {
            repositoryJobLauncher.run(repositoryJobTableDrop, jobParameters);
        } else {
            throw new WrongParamException();
        }

        return "success";
    }
    
   

}

 

 

3. 실행 테스트

 

(1) table 생성

$ curl -X GET http://localhost/batch/repository/create/itemid_
success

 

 

 

 

 

(2) table 삭제

$ curl -X GET http://localhost/batch/repository/drop/itemid_
success



* ResourcelessTransactionManager  클래스의 단점이 있다.

위의 예제는 하나의 JVM에서 동작할 때 하나의 Job만 동작하는 경우에 쓸모가 있다. 즉, 다른 Job과 동시에 작업하는 Batch Job으로 동작하는 경우, ResourcelessTransactionManager 는 동작하지 않는다. 

다른 Job의 TransactionManager로 바뀌는 부분이 존재한다. 따라서, 위의 예제는 하나의 JVM 인스턴스로 동작하는 경우에서만 사용할 수 있다. 다른 Job 과 동시에 사용해야 하는 경우 위의 예제를 사용할 수 없다.

다음 내용으로 변경해서 해결했다. 

http://knight76.tistory.com/entry/Mysql-에서-Spring-Batch를-이용하여-Job-Repository-Table-생성-또는-삭제하는-예제-2




Posted by '김용환'
,

 

Spring 실행시 다음과 같은 에러가 났다

NoClassDefFoundError: org/springframework/core/env/EnvironmentCapable

 

pom.xml 파일에서 hierachy를 살펴보니. Spring Batch 를 사용하면서 Spring 하위 버전을 쓰는 부분 떄문에 conflict 가 발생하고, Spring 이 하위버전으로 자동 resolve되었다.

 

 

아래와 같이 Spring Batch 사용시 Spring 관련 lib에 대해서 exclusion하니 작동이 되었다.

 

 

<dependency>
            <groupId>org.springframework.batch</groupId>
            <artifactId>spring-batch-infrastructure</artifactId>
            <version>${spring.batch.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-aop</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-beans</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-context</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-jdbc</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-test</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-tx</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

 

        <dependency>
            <groupId>org.springframework.batch</groupId>
            <artifactId>spring-batch-core</artifactId>
            <version>${spring.batch.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-aop</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-beans</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-context</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-jdbc</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-test</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                      <artifactId>spring-tx</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

Posted by '김용환'
,

 

 

아래 코드에서 참조했으나.. 버그가 있어서 좀 수정.

http://www.michaelyagudaev.com/programming/sql/7

 

 

public class HtmlEntities {
    private static Map<String, Character> map = new LinkedHashMap<String, Character>();

    static {
        map.put("&amp;", (char)38);
        map.put("&lt;", (char)60);
        map.put("&gt;", (char)62);
       
        map.put("&quot;", (char)34);
        map.put("&nbsp;", (char)160);
        map.put("&iexcl;", (char)161);
        map.put("&cent;", (char)162);
        map.put("&pound;", (char)163);
        map.put("&curren;", (char)164);
        map.put("&yen;", (char)165);
        map.put("&brvbar;", (char)166);
        map.put("&sect;", (char)167);
        map.put("&uml;", (char)168);
        map.put("&copy;", (char)169);
        map.put("&ordf;", (char)170);
        map.put("&laquo;", (char)171);
        map.put("&not;", (char)172);
        map.put("&shy;", (char)173);
        map.put("&reg;", (char)174);
        map.put("&macr;", (char)175);
        map.put("&deg;", (char)176);
        map.put("&plusmn;", (char)177);
        map.put("&sup2;", (char)178);
        map.put("&sup3;", (char)179);
        map.put("&acute;", (char)180);
        map.put("&micro;", (char)181);
        map.put("&para;", (char)182);
        map.put("&middot;", (char)183);
        map.put("&cedil;", (char)184);
        map.put("&sup1;", (char)185);
        map.put("&ordm;", (char)186);
        map.put("&raquo;", (char)187);
        map.put("&frac14;", (char)188);
        map.put("&frac12;", (char)189);
        map.put("&frac34;", (char)190);
        map.put("&iquest;", (char)191);
        map.put("&times;", (char)215);
        map.put("&divide;", (char)247);
        map.put("&Agrave;", (char)192);
        map.put("&Aacute;", (char)193);
        map.put("&Acirc;", (char)194);
        map.put("&Atilde;", (char)195);
        map.put("&Auml;", (char)196);
        map.put("&Aring;", (char)197);
        map.put("&AElig;", (char)198);
        map.put("&Ccedil;", (char)199);
        map.put("&Egrave;", (char)200);
        map.put("&Eacute;", (char)201);
        map.put("&Ecirc;", (char)202);
        map.put("&Euml;", (char)203);
        map.put("&Igrave;", (char)204);
        map.put("&Iacute;", (char)205);
        map.put("&Icirc;", (char)206);
        map.put("&Iuml;", (char)207);
        map.put("&ETH;", (char)208);
        map.put("&Ntilde;", (char)209);
        map.put("&Ograve;", (char)210);
        map.put("&Oacute;", (char)211);
        map.put("&Ocirc;", (char)212);
        map.put("&Otilde;", (char)213);
        map.put("&Ouml;", (char)214);
        map.put("&Oslash;", (char)216);
        map.put("&Ugrave;", (char)217);
        map.put("&Uacute;", (char)218);
        map.put("&Ucirc;", (char)219);
        map.put("&Uuml;", (char)220);
        map.put("&Yacute;", (char)221);
        map.put("&THORN;", (char)222);
        map.put("&szlig;", (char)223);
        map.put("&agrave;", (char)224);
        map.put("&aacute;", (char)225);
        map.put("&acirc;", (char)226);
        map.put("&atilde;", (char)227);
        map.put("&auml;", (char)228);
        map.put("&aring;", (char)229);
        map.put("&aelig;", (char)230);
        map.put("&ccedil;", (char)231);
        map.put("&egrave;", (char)232);
        map.put("&eacute;", (char)233);
        map.put("&ecirc;", (char)234);
        map.put("&euml;", (char)235);
        map.put("&igrave;", (char)236);
        map.put("&iacute;", (char)237);
        map.put("&icirc;", (char)238);
        map.put("&iuml;", (char)239);
        map.put("&eth;", (char)240);
        map.put("&ntilde;", (char)241);
        map.put("&ograve;", (char)242);
        map.put("&oacute;", (char)243);
        map.put("&ocirc;", (char)244);
        map.put("&otilde;", (char)245);
        map.put("&ouml;", (char)246);
        map.put("&oslash;", (char)248);
        map.put("&ugrave;", (char)249);
        map.put("&uacute;", (char)250);
        map.put("&ucirc;", (char)251);
        map.put("&uuml;", (char)252);
        map.put("&yacute;", (char)253);
        map.put("&thorn;", (char)254);
        map.put("&yuml;", (char)255);
        
    }

    private static String fromHtmlEntity(String str) {
        Character ch = map.get(str);
        return (ch != null) ? ch.toString() : str;
    }

    private static String findValue(char value) {
        boolean found = false;
        String result = null;
        for (String key : map.keySet()) {
            if (map.get(key).charValue() == value) {
                found = true;
                result = key;
            }
           
            if (found == true) {
                return result;
            }
        }

        return result;
    }

    public static String encode(String encode) {
        StringBuilder str = new StringBuilder(encode);
        String key;
        int i = 0;

        while (i < str.length()) {
            key = findValue(str.charAt(i));
            if (key != null) {
                str.replace(i, i + 1, key);
                i += key.length();
            } else {
                i++;
            }
        }

        return str.toString();
    }

    public static String decode(String decode){
        StringBuilder str = new StringBuilder(decode);
        Matcher m = Pattern.compile("&[A-Za-z]+;").matcher(str);
        String replaceStr = null;

        int matchPointer = 0;
        while (m.find(matchPointer)) {
            replaceStr = fromHtmlEntity(m.group());
            str.replace(m.start(), m.end(), replaceStr);
            matchPointer = m.start() + replaceStr.length();
        }

        return str.toString();
    }
}

Posted by '김용환'
,

 

json 데이터를 post로 보내서 테스트하고 싶은 경우에 사용시..

 

 

 

from net.grinder.script import Test
from net.grinder.plugin.http import HTTPRequest

from net.grinder.script.Grinder import grinder

from HTTPClient import NVPair
from java.util import Random, Date

test1 = Test(1, "JSON POST TEST")

requestGet = test1.wrap(HTTPRequest(url="http://1.1.1.1:1111"))
logger = grinder.logger

 

class TestRunner:
    def __init__(self):
        print "Init"

    def __call__(self):
        grinder.statistics.delayReports = 1
        url = "/event"
        json="{\"result\" :  [{  \"to\":[\"1111\"]  }] }"
        result = requestGet.POST(url, json,   ( NVPair('Content-Type', 'application/json'), ))
        grinder.sleep(1000)
       
        logger.output(str(result.statusCode))

        if result.statusCode > 200 or result.text == "null":
            grinder.statistics.forLastTest.success = 0
        else :
            grinder.statistics.forLastTest.success = 1




아래는 그냥 간단한 테스트 샘플


from net.grinder.script import Test
from net.grinder.plugin.http import HTTPRequest
from HTTPClient import NVPair
from net.grinder.script.Grinder import grinder
from java.util import Random, Date

test1 = Test(1, "test")

requestGet = test1.wrap(HTTPRequest(url="http://1.1.1.1"))
logger = grinder.logger

params = []
params.append("a=111&n=222")
params.append("a=222&n=222")
params.append("a=333&n=222")
params.append("a=444&n=222")
params.append("a=555&n=222")

        
class TestRunner:
    def __init__(self):
        print "Init"

    def __call__(self):
    
            random = Random()

            randomInt = random.nextInt(len(params))
            
            grinder.statistics.delayReports = 1
            url = "/count?" +  params[randomInt]
          
            result = requestGet.GET(url)
    
               
            grinder.sleep(1000)
                               
            
            if result.statusCode > 200 or result.text == "null":
               grinder.statistics.forLastTest.success = 0
            
            else : 
                grinder.statistics.forLastTest.success = 1
                
                





Posted by '김용환'
,


interface를 client, server에 배포해두고. 서버에서  interface를 실제 구현한  클래스를 정의한다. 
Http를 이용해서 entity에 추가해서 송수신하는 message format을 정의한다. serialization은  protostuff를 사용할 수 있다. client에서는 단순히 interface에 정의된 것만 호출함으로 rpc 효과를 얻을 수 있다. 

protobuff가 성능도 용량도 작아서 많이 쓸만해 보임
 
http://code.google.com/p/protostuff/


Protostuff is the stuff that leverages google's protobuf.

A serialization library with built-in support for forward-backward compatibility (schema evolution) and validation.

Posted by '김용환'
,