[WIP] Spring Bootで作るCommand Lineアプリケーションの雛形

# 概要

Spring Bootを使ったコマンドラインアプリケーションの雛形を作成する記事です。（まだ未完です）
このアプリケーションの用途はcronやjenkinsから定期的に実行されるバッチ処理を想定しています。
（バッチ処理向けのSpring Batchというフレームワークがありますがこの例では使用しません。）

**環境**

* Windows10
* Java 1.8.0_92
* Spring-Boot 1.4.0
 * Spring JDBC
 * MyBatis
 * MySQL 5.6

**参考**

* [Spring Bootでコマンドラインアプリを作る時の注意点] (http://qiita.com/tag1216/items/898348a7fc3465148bc8)
* [mybatis-spring-boot-starterの使い方] (http://qiita.com/kazuki43zoo/items/ea79e206d7c2e990e478)


**ソースコード**

まだ未完ですが現時点でのソースコードは[sbcl-example] (https://github.com/rubytomato/sbcl-example) にあります。

## 雛形の作成

雛形は[SPRING INITIALIZR] (https://start.spring.io/)で作成しました。とりあえず依存関係には下記の3つを含めました。

* JDBC
* MySQL
* DevTools

![si.png](https://qiita-image-store.s3.amazonaws.com/0/22772/0c93866a-d534-d030-8290-d9b8410adc12.png)


## 手を入れた後の状態

### 設定

#### pom.xml

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>sbcl-example</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>sbcl-example</name>
	<description>CommandLine Application project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.4.0.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
        	</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
	
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
             </plugin>
        </plugins>
    </build>

</project>
```

#### application.properties

MySQLに接続する処理を想定してdatasourceの設定だけ行っています。

```text
spring.profiles.active				= local

# DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url				= jdbc:mysql://localhost/test
spring.datasource.username			= test_user
spring.datasource.password			= test_user
spring.datasource.driver-class-name		= com.mysql.jdbc.Driver

spring.datasource.schema			= import.sql

# MyBatis
mybatis.config-location				= mybatis-config.xml
# mybatis.mapper-locations			= 
# mybatis.type-aliases-package			= 
# mybatis.type-handlers-package			= 
# mybatis.executor-type				= SIMPLE
# mybatis.configuration				= 

# JOOQ (JooqAutoConfiguration)
# spring.jooq.sql-dialect=
```

*参考*

* [Appendix A. Common application properties] (http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html)



#### logback-spring.xml

```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <property name="LOG_DIR" value="D:/logs" />

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MMM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{35} - %msg %n</pattern>
    </encoder>
  </appender>
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
     <file>${LOG_DIR}/sbcl-example.log</file>
     <encoder>
       <charset>UTF-8</charset>
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] - %msg %n</pattern>
     </encoder>
  </appender>

  <logger name="com.example" level="DEBUG" />

  <logger name="org.springframework" level="INFO"/>
  <root>
    <appender-ref ref="STDOUT" />
    <appender-ref ref="FILE" />
  </root>
</configuration>
```

## 実装する機能

コマンドラインアプリケーションに必要な機能

1. 引数処理
1. DB接続
1. 多重起動制御
1. 処理結果の外部出力
1. 通知処理


### 引数処理

* Commons CLI
* args4j

#### Commons CLIを使用する場合

https://commons.apache.org/proper/commons-cli/

##### pom.xml

```xml
<!-- https://mvnrepository.com/artifact/commons-cli/commons-cli -->
<dependency>
    <groupId>commons-cli</groupId>
    <artifactId>commons-cli</artifactId>
    <version>1.3.1</version>
</dependency>
```



#### args4j parentを使用する場合

http://args4j.kohsuke.org/

##### pom.xml

```xml
<!-- https://mvnrepository.com/artifact/args4j/args4j -->
<dependency>
    <groupId>args4j</groupId>
    <artifactId>args4j</artifactId>
    <version>2.33</version>
</dependency>
```



### DB接続

* Spring JDBC (spring-boot-starter-jdbc)
* MyBatis (mybatis-spring-boot-starter)
* JOOQ (spring-boot-starter-jooq)
* JPA (spring-boot-starter-data-jpa)


#### Spring JDBCを使用する場合

19. Data access with JDBC
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html

https://github.com/spring-projects/spring-boot/tree/master/spring-boot-starters/spring-boot-starter-jdbc

##### pom.xml

依存関係に`spring-boot-starter-jdbc`を追加します。

```xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
```

#### MyBatisを使用する場合

[mybatis-spring-boot-starter] (https://github.com/mybatis/spring-boot-starter)

##### pom.xml

依存関係に`mybatis-spring-boot-starter`を追加します。

```xml
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>1.1.1</version>
</dependency>
```

##### mybatis-config.xml

src/java/resources/mybatis-config.xml

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <package name="com.example.domain"/>
    </typeAliases>
    <mappers>
        <mapper resource="mapper/CityMapper.xml"/>
        <mapper resource="mapper/HotelMapper.xml"/>
    </mappers>
</configuration>
```

##### import.sql

src/java/resources/import.sql

```sql
drop table if exists city;
drop table if exists hotel;

create table city (
   `id` int not null
 , `name` varchar(30)
 , `state` varchar(6)
 , `country` varchar(6)
 , primary key(id)
) engine = INNODB;

create table hotel (
    `id` int not null
  , `city` int not null
  , `name` varchar(60)
  , `address` varchar(60)
  , `zip` varchar(12)
  , primary key(id)
) engine = INNODB;

insert into city (id, name, state, country) values (1, 'San Francisco', 'CA', 'US');
insert into city (id, name, state, country) values (2, 'New York City', 'NY', 'US');
insert into city (id, name, state, country) values (3, 'Los Angeles', 'CA', 'US');
insert into city (id, name, state, country) values (4, 'Chicago', 'IL', 'US');
insert into city (id, name, state, country) values (5, 'Houston', 'TX', 'US');

insert into hotel(id, city, name, address, zip) values (1, 1, 'Conrad Treasury Place', 'William & George Streets', '4001');
insert into hotel(id, city, name, address, zip) values (2, 1, 'The Westin St. Francis San Francisco on Union Square', 'Powell Street, Union Square, San Francisco, CA', '335');
insert into hotel(id, city, name, address, zip) values (3, 1, 'The Fairmont San Francisco', 'Mason Street, San Francisco, CA', '950');
```

##### mapper/CityMapper.xml

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.CityMapper">
    <select id="selectCityById" resultType="City">
        SELECT * FROM city WHERE id = #{id}
    </select>
</mapper>
```

```java
@Mapper
public interface CityMapper {

    City selectCityById(final Long id);

    @Select("SELECT * FROM city WHERE state = #{state}")
    List<City> findByState(@Param("state") String state);

}
```

##### mapper/HotelMapper.xml

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.HotelMapper">

    <select id="selectHotelById" resultType="Hotel">
        SELECT * FROM hotel WHERE id = #{id}
    </select>

    <resultMap id="selectByCityIdMap" type="Hotel">
      <id property="id" column="ホテルID" />
      <result property="city" column="都市ID" />
      <result property="name" column="ホテル名" />
      <result property="address" column="住所" />
      <result property="zip" column="郵便番号" />
    </resultMap>

    <select id="selectByCityId" resultMap="selectByCityIdMap">
        SELECT * FROM hotel WHERE city = #{id}
    </select>
    
</mapper>
```

```java
@Mapper
public interface HotelMapper {

	Hotel selectHotelById(final Long id);

	List<Hotel> selectByCityId(final Long cityId);

}
```

##### MyBatipse

Eclipseのプラグイン

https://github.com/mybatis/mybatipse



#### JOOQを使用する場合

[spring-boot-starter-jooq] (https://github.com/spring-projects/spring-boot/tree/master/spring-boot-starters/spring-boot-starter-jooq)

##### pom.xml

依存関係に`spring-boot-starter-jooq`を追加します。

```xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jooq</artifactId>
</dependency>
```

##### 参考

* [SpringBoot : Working with JOOQ] (http://sivalabs.in/2016/03/springboot-working-with-jooq/)



#### JPAを使用する場合

[spring-boot-starter-data-jpa] (https://github.com/spring-projects/spring-boot/tree/master/spring-boot-starters/spring-boot-starter-data-jpa)

##### pom.xml

依存関係に`spring-boot-starter-data-jpa`を追加します。

```xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
```


### 多重起動制御

#### FileChannelを使用する場合




### 処理結果の外部出力

#### Google Sheetsに出力する場合

Google Sheets API
https://developers.google.com/sheets/

##### pom.xml

必要なライブラリは

* google-api-client
* google-oauth-client-jetty
* google-api-services-sheets

```xml
<!-- https://mvnrepository.com/artifact/com.google.api-client/google-api-client -->
<dependency>
    <groupId>com.google.api-client</groupId>
    <artifactId>google-api-client</artifactId>
    <version>1.22.0</version>
</dependency>
```

```xml
<!-- https://mvnrepository.com/artifact/com.google.oauth-client/google-oauth-client-jetty -->
<dependency>
    <groupId>com.google.oauth-client</groupId>
    <artifactId>google-oauth-client-jetty</artifactId>
    <version>1.22.0</version>
</dependency>
```

```xml
<!-- https://mvnrepository.com/artifact/com.google.apis/google-api-services-sheets -->
<dependency>
    <groupId>com.google.apis</groupId>
    <artifactId>google-api-services-sheets</artifactId>
    <version>v4-rev21-1.22.0</version>
</dependency>
```



### 通知処理

想定するのは、処理の「開始、経過、終了、異常発生」の日時や結果などを通知する処理。または常駐型の場合の死活通知など。

* spring-boot-starter-mail
* Simple Slack API

#### メールで通知する場合(`spring-boot-starter-mail`)

https://github.com/spring-projects/spring-boot/tree/master/spring-boot-starters/spring-boot-starter-mail

##### pom.xml

```xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
```

##### application.properties

Gmail

```text
# Email (MailProperties)
spring.mail.default-encoding = UTF-8
spring.mail.host = smtp.gmail.com
#spring.mail.jndi-name= # Session JNDI name. When set, takes precedence to others mail settings.
spring.mail.password = ********** # Google App password
spring.mail.port = 578
#spring.mail.properties.*= # Additional JavaMail session properties.
spring.mail.protocol = smtp
spring.mail.test-connection = false
spring.mail.username = *****.*****.*****@gmail.com # Google account mail address
```


#### Slackで通知する場合(`Simple Slack API`)

https://github.com/Ullink/simple-slack-api

##### pom.xml

```xml
<!-- https://mvnrepository.com/artifact/com.ullink.slack/simpleslackapi -->
<dependency>
    <groupId>com.ullink.slack</groupId>
    <artifactId>simpleslackapi</artifactId>
    <version>0.6.0</version>
</dependency>
```



## 実装コード

### Application.java

```java
package com.example;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import com.example.service.ServiceAlpha;
import com.example.service.ServiceBeta;
import com.example.service.ServiceGamma;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@SpringBootApplication
public class Application {

	@Autowired
	ServiceAlpha alpha;

	@Autowired
	ServiceBeta beta;

	@Autowired
	ServiceGamma gamma;

	public static void main(String[] args) {
		log.debug(">>> call main");
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Application.class, args)) {
        	Application app = ctx.getBean(Application.class);
        	app.run(args);
        }
        log.debug("<<< main end");
	}

	@PostConstruct
	public void init() {
		log.debug("application init");
	}

	@PreDestroy
	public void destory() {
		log.debug("application finish");
	}

	public void run(final String... args) {
		log.debug("application run");
		alpha.execute();
		beta.execute(args);
		gamma.execute();
	}
	
}
```

#### ServiceAlpha

```java
package com.example.service;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class ServiceAlpha {

    @Autowired
    private JdbcTemplate jdbc;

    public void execute() {
    	log.debug("*** alpha in ***");

    	if (jdbc == null) {
    		throw new RuntimeException("JdbcTemplate is null");
    	}
    	Map<String, Object> result = jdbc.queryForMap("select now() as current");
    	if (result == null) {
    		log.debug("result null");
    		return;
    	}
    	result.forEach((k,v)->{
    		log.debug("key:{} value:{}", k, v);
    	});

    	log.debug("*** alpha out ***");
    }
}
```

#### ServiceBeta

```java
package com.example.service;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class ServiceBeta {

    public void execute(final String... args) {
    	log.debug("*** beta in ***");

		if (args!= null && args.length > 0) {
			log.debug("UpperCase:{}", StringUtils.join(args, ",").toUpperCase());
		}

    	log.debug("*** beta out ***");
    }
}
```

#### ServiceGamma

```java
package com.example.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.example.domain.City;
import com.example.domain.Hotel;
import com.example.mapper.CityMapper;
import com.example.mapper.HotelMapper;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class ServiceGamma {

	@Autowired
	private CityMapper cityMapper;

	@Autowired
	private HotelMapper hotelMapper;

    public void execute() {
    	log.debug("*** gamma in ***");

    	City city = cityMapper.selectCityById(1L);
    	log.debug("city:{}", city);

    	List<City> cityList = cityMapper.findByState("CA");
    	if (cityList != null) {
    		cityList.forEach(c -> {
    			log.debug("city:{}",c);
    		});
    	} else {
    		log.debug("city list is null");
    	}

    	Hotel hotel = hotelMapper.selectHotelById(1L);
    	log.debug("hotel:{}", hotel);

    	List<Hotel> hotelList = hotelMapper.selectByCityId(1L);
    	if (hotelList != null) {
    		hotelList.forEach(h -> {
    			log.debug("hotel:{}",h);
    		});
    	} else {
    		log.debug("hotel list is null");
    	}

    	log.debug("*** gamma out ***");
    }

}
```



### ビルドと実行

プロジェクトのルートディレクトリで下記のmvnコマンドを実行します。

```bat
> mvn clean package

...省略...

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO]
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ sbcl-example ---
[INFO] Building jar: D:\dev\eclipse-jee-neon-workspace\sbcl-example\target\sbcl-example-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:1.4.0.RELEASE:repackage (default) @ sbcl-example ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 8.619 s
[INFO] Finished at: 2016-08-30T21:44:51+09:00
[INFO] Final Memory: 27M/269M
[INFO] ------------------------------------------------------------------------
```

targetディレクトリ下に作成されているjarファイルを実行します。

```bat
> java -jar .\target\sbcl-example-0.0.1-SNAPSHOT.jar abc xyz

...省略...

DEBUG [main] com.example.Application - application init
INFO  [main] o.s.j.e.a.AnnotationMBeanExporter - Registering beans for JMX exposure on startup
INFO  [main] com.example.Application - Started Application in 3.771 seconds (JVM running for 4.699)
DEBUG [main] com.example.Application - application run
DEBUG [main] com.example.service.ServiceAlpha - *** alpha in ***
DEBUG [main] com.example.service.ServiceAlpha - key:current value:2016-08-30 21:46:11.0
DEBUG [main] com.example.service.ServiceAlpha - *** alpha out ***
DEBUG [main] com.example.service.ServiceBeta - *** beta in ***
DEBUG [main] com.example.service.ServiceBeta - UpperCase:ABC,XYZ
DEBUG [main] com.example.service.ServiceBeta - *** beta out ***
DEBUG [main] com.example.Application - application finish
DEBUG [main] com.example.Application - <<< main end
```