Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

概要

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

環境

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

参考

ソースコード

まだ未完ですが現時点でのソースコードはsbcl-example にあります。

雛形の作成

雛形はSPRING INITIALIZRで作成しました。とりあえず依存関係には下記の3つを含めました。

  • JDBC
  • MySQL
  • DevTools

si.png

手を入れた後の状態

設定

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=

参考

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>

実装する機能

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

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

引数処理

  • Commons CLI
  • args4j

Commons CLIを使用する場合

https://commons.apache.org/proper/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を使用する場合

http://args4j.kohsuke.org/

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を使用する場合

  1. 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を追加します。

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

MyBatisを使用する場合

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のプラグイン

https://github.com/mybatis/mybatipse

JOOQを使用する場合

spring-boot-starter-jooq

pom.xml

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

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jooq</artifactId>
</dependency>
参考

JPAを使用する場合

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)

https://github.com/spring-projects/spring-boot/tree/master/spring-boot-starters/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)

https://github.com/Ullink/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
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away