概要
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
手を入れた後の状態
設定
pom.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の設定だけ行っています。
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 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>
実装する機能
コマンドラインアプリケーションに必要な機能
- 引数処理
- DB接続
- 多重起動制御
- 処理結果の外部出力
- 通知処理
引数処理
- Commons CLI
- args4j
Commons CLIを使用する場合
pom.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を使用する場合
pom.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を使用する場合
- Data access with JDBC
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html
pom.xml
依存関係にspring-boot-starter-jdbc
を追加します。
<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
を追加します。
<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 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
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 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>
@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 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>
@Mapper
public interface HotelMapper {
Hotel selectHotelById(final Long id);
List<Hotel> selectByCityId(final Long cityId);
}
MyBatipse
Eclipseのプラグイン
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
を追加します。
<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
を追加します。
<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
<!-- 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>
<!-- 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>
<!-- 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
)
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
application.properties
Gmail
# 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
)
pom.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
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
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
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
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コマンドを実行します。
> 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ファイルを実行します。
> 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