Spring Bootキャンプ ãƒãƒ³ã‚ºã‚ªãƒ³è³‡æ–™Â¶

å‰ææ¡ä»¶Â¶

  • Java SE 8ãŒã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ã„ã‚‹ã“ã¨ã€‚
  • MavenãŒã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ã„ã‚‹ã“ã¨ã‹ã¤åŸºæœ¬çš„ãªã“ã¨ãŒåˆ†ã‹ã‚‹ã“ã¨ã€‚
  • GitãŒã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ã„ã‚‹ã“ã¨ã€‚
  • curlãŒã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ã„ã‚‹ã“ã¨ã€‚
  • OSãŒWindowsã¾ãŸã¯MacOSã§ã‚ã‚‹ã“ã¨ã€‚
  • DIã®åŸºæœ¬çš„ãªçŸ¥è­˜ã‚’有ã—ã¦ã„ã‚‹ã“ã¨ã€‚
  • Javaã§Webプログラミングã®çµŒé¨“ãŒã‚ã‚‹ã“ã¨ã€‚
  • (カメラを使ã†å ´åˆ)PCã«ã‚«ãƒ¡ãƒ©ãŒã¤ã„ã¦ã„ã‚‹ã“ã¨ã€‚
  • (Dockerを使ã†å ´åˆ)boot2dockerãŒã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ã„ã‚‹ã“ã¨ã€ã¾ãŸã¯AWSã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’æŒã£ã¦ã„ã‚‹ã“ã¨ã€‚

作るシステム¶

下図ã®ã‚ˆã†ãªé¡”変æ›ã‚µãƒ¼ãƒ“スを作りã¾ã™ã€‚

_images/system.png

警告

本ãƒãƒ³ã‚ºã‚ªãƒ³ã§ä½œæˆã™ã‚‹ã‚¢ãƒ—リã¯ã‚¸ãƒ§ãƒ¼ã‚¯ã‚¢ãƒ—リã§å®Ÿç”¨æ€§ã‚’考慮ã—ã¦ã„ã¾ã›ã‚“。実際ã®é–‹ç™ºã®å‚考ã«ã™ã‚‹å ´åˆã¯å分気をã¤ã‘ã¦ãã ã•ã„。

目次¶

[事å‰æº–å‚™] Spring Bootã§Hello World¶

Spring BootプロジェクトをMaven Archetypeã‹ã‚‰ä½œã‚Šã¾ã™ã€‚今回ã¯æ‹™ä½œã®spring-boot-docker-blankを使用ã—ã¾ã™ã€‚ ã“ã®Maven Archetypeã«ã¯Dockerデプロイã™ã‚‹ãŸã‚ã®è¨­å®šãŒäºˆã‚è¡Œã‚ã‚Œã¦ã„ã¾ã™ã€‚

以下ã®ã‚³ãƒžãƒ³ãƒ‰ã§ãƒ—ロジェクトを作æˆã—ã¾ã—ょã†ã€‚ターミナルã¾ãŸã¯ã‚³ãƒžãƒ³ãƒ‰ãƒ—ロンプトã«è²¼ä»˜ã‘ã¦ãã ã•ã„。

  • Bashを使ã£ã¦ã„ã‚‹å ´åˆ

    $ mvn archetype:generate -B\
     -DarchetypeGroupId=am.ik.archetype\
     -DarchetypeArtifactId=spring-boot-docker-blank-archetype\
     -DarchetypeVersion=1.0.2\
     -DgroupId=kanjava\
     -DartifactId=kusokora\
     -Dversion=1.0.0-SNAPSHOT
    $ cd kusokora
    
  • コマンドプロンプトを使ã£ã¦ã„ã‚‹å ´åˆ

    $ mvn archetype:generate -B^
     -DarchetypeGroupId=am.ik.archetype^
     -DarchetypeArtifactId=spring-boot-docker-blank-archetype^
     -DarchetypeVersion=1.0.2^
     -DgroupId=kanjava^
     -DartifactId=kusokora^
     -Dversion=1.0.0-SNAPSHOT
    $ cd kusokora
    

生æˆã•ã‚ŒãŸãƒ—ロジェクトã¯ä»¥ä¸‹ã®ã‚ˆã†ãªæ§‹é€ ã«ãªã£ã¦ã„ã¾ã™ã€‚

kusokora/
├── pom.xml
└── src
    ├── main
    │   ├── docker ... Docker用ファイル格ç´ãƒ•ã‚©ãƒ«ãƒ€ã€‚(Dockerデプロイã™ã‚‹ã¨ãã®ã¿ä½¿ã†)
    │   │   ├── Dockerfile.txt
    │   │   └── Dockerrun.aws.json
    │   ├── java
    │   │   └── kanjava
    │   │       └── App.java ... アプリケーションコードを書ãJavaファイル。今回ã®ãƒãƒ³ã‚ºã‚ªãƒ³ã§ã¯åŸºæœ¬çš„ã«ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã—ã‹ä½¿ã‚ãªã„。
    │   └── resources
    │       └── application.yml ... Spring Bootã®è¨­å®šãƒ•ã‚¡ã‚¤ãƒ«ã€‚ç„¡ãã¦ã‚‚良ã„。
    └── test ... テスト用フォルダ。今回ã¯ä½¿ã‚ãªã„。
        ├── java
        └── resources

以下ã®ã‚ˆã†ãª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>kanjava</groupId>
    <artifactId>kusokora</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Spring Boot Docker Blank Project (from https://github.com/making/spring-boot-docker-blank)</name>

    <!-- 最é‡è¦ã€‚Spring Bootã®è«¸ã€…設定を引ã継ããŸã‚ã®è¦ªæƒ…報。 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.1.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>kanjava.App</start-class><!-- mainメソッドã®ã‚るクラスを明示的ã«æŒ‡å®š -->
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- é‡è¦ã€‚Webアプリをã¤ãã‚‹ãŸã‚ã®è¨­å®šã€‚å¿…è¦ãªä¾å­˜é–¢ä¿‚ã¯å®Ÿã¯ã“ã‚Œã ã‘。 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- メトリクスや環境変数を返ã™ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã®è¨­å®šã€‚ã“ã“ã¯ãŠã¾ã‘。 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- テストã®è¨­å®šã€‚今回ã¯ãƒ†ã‚¹ãƒˆã—ãªã„ã®ã§ã€ã“ã“ã¯ãŠã¾ã‘。 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- Spring Bootプラグインã®è¨­å®š(å¿…é ˆ)。Spring Loadedも設定ã—ã¦ã„る。 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <dependencies>
                    <dependency><!-- Java SE 8u40ã ã¨å‹•ãã¾ã›ã‚“。ã“ã“ã®è¨­å®šã‚’削除ã—ã¦ãã ã•ã„ https://github.com/spring-projects/spring-loaded/issues/108 -->
                        <groupId>org.springframework</groupId>
                        <artifactId>springloaded</artifactId>
                        <version>${spring-loaded.version}</version>
                    </dependency>
                </dependencies>
            </plugin>

            <!-- ã“ã“ã‹ã‚‰ä¸‹ã¯Docker用ã®ã¡ã‚‡ã£ã¨ã—ãŸè¨­å®šã§æœ¬è³ªçš„ã§ãªã„。無視ã—ã¦ã‚‚良ã„。 -->
            <!-- Copy Dockerfile -->
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-resources</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${basedir}/target/</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>src/main/docker</directory>
                                    <filtering>true</filtering>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <!-- ã»ã‚“ã¨ã©ã†ã§ã‚‚ã„ã„設定。 -->
            <plugin>
                <groupId>com.coderplus.maven.plugins</groupId>
                <artifactId>copy-rename-maven-plugin</artifactId>
                <version>1.0</version>
                <executions>
                    <execution>
                        <id>rename-file</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>rename</goal>
                        </goals>
                        <configuration>
                            <sourceFile>${basedir}/target/Dockerfile.txt</sourceFile>
                            <destinationFile>${basedir}/target/Dockerfile</destinationFile>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <!-- AWS Elastic BeanStalk用ã®zipを作æˆã€‚ã“ã“も本質的ã§ãªã„。 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
                <version>1.7</version>
                <executions>
                    <execution>
                        <id>zip-files</id>
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <zip destfile="${basedir}/target/app.zip" basedir="${basedir}/target" includes="Dockerfile, Dockerrun.aws.json, ${project.artifactId}.jar" />
                            </target>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

src/main/java/kanjava/App.javaを見ã¦ãã ã•ã„。

package kanjava;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @RequestMapping(value = "/")
    String hello() {
        return "Hello World!";
    }
}

@SpringBootApplicationãŒé­”法ã®ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ã§ã™ã€‚ã“ã®ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ã¯ä»¥ä¸‹ã®3アノテーションを1ã¤ã«ã¾ã¨ã‚ãŸã‚‚ã®ã§ã™ã€‚

アノテーション 説明
@EnableAutoConfiguration
Spring Bootã®è‡ªå‹•è¨­å®šç¾¤ã‚’有効ã«ã—ã¾ã™ã€‚
@ComponentScan
コンãƒãƒ¼ãƒãƒ³ãƒˆã‚¹ã‚­ãƒ£ãƒ³ã‚’è¡Œã†ã€‚ã“ã®ã‚¯ãƒ©ã‚¹ã®ãƒ‘ッケージé…下ã§@Component, @Service, @Repository, @Controller, @RestController, @Configuration,@Namedã¤ãã®ã‚¯ãƒ©ã‚¹ã‚’DIコンテナã«ç™»éŒ²ã—ã¾ã™ã€‚
@Configuration
ã“ã®ã‚¯ãƒ©ã‚¹è‡ªä½“ã‚’Bean定義å¯èƒ½ã«ã—ã¾ã™ã€‚@Beanã‚’ã¤ã‘ãŸãƒ¡ã‚½ãƒƒãƒ‰ã‚’ã“ã®ã‚¯ãƒ©ã‚¹å†…ã«å®šç¾©ã™ã‚‹ã“ã¨ã§ã€DIコンテナã«Beanを登録ã§ãã¾ã™ã€‚

@RestControllerã‚’ã¤ã‘ã‚‹ã“ã¨ã§ã€ã“ã®ã‚¯ãƒ©ã‚¹è‡ªä½“ãŒSpring MVCã®ã‚³ãƒ³ãƒˆãƒ­ãƒ¼ãƒ©ãƒ¼ã«ãªã‚Šã¾ã™ã€‚ ã“ã®ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã¤ã‘ãŸã‚¯ãƒ©ã‚¹ã®ãƒ¡ã‚½ãƒƒãƒ‰ã«@RequestMappingã‚’ã¤ã‘ã‚‹ã¨ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’å—ã‘るメソッドã«ãªã‚Šã€ãã®ãƒ¡ã‚½ãƒƒãƒ‰ã®è¿”り値ãŒãƒ¬ã‚¹ãƒãƒ³ã‚¹ãƒœãƒ‡ã‚£ã«æ›¸ãè¾¼ã¾ã‚Œã¾ã™ã€‚

ã“ã®ä¾‹ã ã¨ã€”/”ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã¨hello()メソッドãŒå‘¼ã°ã‚Œã€”Hello World!”ãŒãƒ¬ã‚¹ãƒãƒ³ã‚¹ãƒœãƒ‡ã‚£ã«æ›¸ãè¾¼ã¾ã‚Œã¾ã™ã€‚Content-Type㯔text/plain”ã«ãªã‚Šã¾ã™ã€‚

mainメソッドを見ã¦ãã ã•ã„。SpringApplication.run(App.class, args)ãŒSpring Bootアプリケーションを起動ã™ã‚‹ãƒ¡ã‚½ãƒƒãƒ‰ã§ã™ã€‚

ã“ã®mainメソッドをIDEã‹ã‚‰å®Ÿè¡Œã—ã¦ã¿ã¦ãã ã•ã„。TomcatãŒç«‹ã¡ä¸ŠãŒã‚Šã€8080番ãƒãƒ¼ãƒˆãŒlistenã•ã‚Œã¾ã™ã€‚ã™ã§ã«8080番ãƒãƒ¼ãƒˆãŒä½¿ç”¨ã•ã‚Œã¦ã„ã‚‹å ´åˆã¯ã€èµ·å‹•ã«å¤±æ•—ã™ã‚‹ã®ã§ä½¿ç”¨ã—ã¦ã„るプロセスを終了ã•ã›ã¦ãã ã•ã„。

http://localhost:8080ã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ãã ã•ã„。「Hello World!ã€ãŒè¡¨ç¤ºã•ã‚Œã¾ã—ãŸã‹ï¼Ÿ

次ã«Mavenプラグインã‹ã‚‰å®Ÿè¡Œã—ã¦ã¿ã¾ã—ょã†ã€‚

$ mvn spring-boot:run

åŒæ§˜ã«èµ·å‹•ã—ã¾ã™ã­ã€‚

注釈

ã“ã®é››å½¢ãƒ—ロジェクトã«ã¯”Spring Boot Actuator”ãŒè¨­å®šã•ã‚Œã¦ãŠã‚Šã€ç’°å¢ƒå¤‰æ•°ã‚„メトリクスã€ãƒ˜ãƒ«ã‚¹ãƒã‚§ãƒƒã‚¯ãªã©éžæ©Ÿèƒ½é¢ã®ã‚µãƒãƒ¼ãƒˆãŒåˆã‚ã‹ã‚‰ã•ã‚Œã¦ã„ã¾ã™ã€‚ 次ã®URLã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ã€è‰²ã€…ãªæƒ…報をå–å¾—ã—ã¦ã¿ã¦ãã ã•ã„。

Chromeを利用ã—ã¦ã„ã‚‹å ´åˆã¯ã€JSONViewをインストールã—ã¦ãŠãã¨ä¾¿åˆ©ã§ã™ã€‚

今度ã¯å®Ÿè¡Œå¯èƒ½jarを作りã¾ã™ã€‚

$ mvn clean package

targetã®ä¸‹ã«kusokora.jarãŒå‡ºæ¥ã¦ã„ã¾ã™ã€‚ã“れを実行ã—ã¦ãã ã•ã„。

$ java -jar target/kusokora.jar

ã“れもåŒæ§˜ã«èµ·å‹•ã—ã¾ã™ã€‚

ã¡ãªã¿ã«ã€ãƒãƒ¼ãƒˆç•ªå·ã‚’変ãˆã‚‹ã¨ãã¯

$ mvn spring-boot:run -Drun.arguments="--server.port=9999"

ã‚„

$ java -jar target/kusokora.jar --server.port=9999

ã§æŒ‡å®šã§ãã¾ã™ã€‚今度ã¯http://localhost:9999ã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã™ã€‚

最後ã«src/main/resources/application.ymlを見ã¦ãã ã•ã„。以下ã®è¨­å®šãŒã•ã‚Œã¦ã„ã¾ã™ã€‚ よã使ã†ã‚‚ã®ãŒäºˆã‚設定ã•ã‚Œã¦ã„ã¾ã™ãŒã€ä»Šå›žã¯ç‰¹ã«å¿…è¦ã§ã¯ã‚ã‚Šã¾ã›ã‚“。気ã«ãªã‚‹ã‚ˆã†ã§ã‚ã‚Œã°å‰Šé™¤ã—ã¦ãã ã•ã„。ファイルã”ã¨æ¶ˆã—ã¦ã‚‚構ã„ã¾ã›ã‚“。

# See http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
spring:
  thymeleaf.cache: false # Thymeleafを使ã£ãŸã¨ãã«ãƒ†ãƒ³ãƒ—レートをキャッシュã•ã›ãªã„(開発用)
  main.show-banner: false # 起動時ã«ãƒãƒŠãƒ¼è¡¨ç¤ºã‚’OFFã«ã™ã‚‹

注釈

Dockerデプロイも試ã—ãŸã„å ´åˆã¯ã€ã€ŒDockerを使ã£ã¦ã¿ã‚‹ã€ã‚’å…ˆã«ã¿ã¦ãã ã•ã„。

本章ã®å†…容を修了ã—ãŸã‚‰ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ#kanjava_sbc #sbc01ã€ã‚’ã¤ã‘ã¦ãƒ„イートã—ã¦ãã ã•ã„。

[事å‰æº–å‚™] Javaã§OpenCVを使ã†Â¶

ç”»åƒå‡¦ç†ã‚’è¡Œã†ãŸã‚ã«ã€è¶…有åãªãƒ©ã‚¤ãƒ–ラリã§ã‚ã‚‹OpenCVを使用ã—ã¾ã™ã€‚Javaã‹ã‚‰OpenCVを扱ã†ãŸã‚ã«ã€ä»Šå›žã¯JavaCVã¨ã„ã†ãƒ©ã‚¤ãƒ–ラリを使ã„ã¾ã™ã€‚

JavaCVã¯JavaCPPã¨ã„ã†C++ã®ã‚½ãƒ¼ã‚¹ã‹ã‚‰è‡ªå‹•ç”Ÿæˆã—ã¦ã§ãるブリッジã®ã‚ˆã†ãªã‚‚ã®ã§ä½œã‚‰ã‚Œã¦ã„ã¾ã™ã€‚Mavenã‚„Gradleãªã©ã®ä¾å­˜æ€§è§£æ±ºã®ä»•çµ„ã¿ã§ç°¡å˜ã«åˆ©ç”¨ã§ãã¦ã€æ‰‹è»½ã«ã‚»ãƒƒãƒˆã‚¢ãƒƒãƒ—ã§ãã‚‹ã®ã§ä»Šå›žæŽ¡ç”¨ã—ã¾ã—ãŸã€‚

本章ã§ã¯ã€æ‰‹å…ƒã®ç’°å¢ƒã§JavaCVを利用ã§ãã‚‹ã‹ã©ã†ã‹ã‚’確èªã—ã¾ã™ã€‚(本章ã®å†…容ã¯Spring Bootã¨ã¯ä¸€åˆ‡é–¢ä¿‚ãŒã‚ã‚Šã¾ã›ã‚“)

ã¾ãšã¯å…ˆã»ã©ã®kusokoraディレクトリã®1ã¤ä¸Šã®éšŽå±¤ã«ç§»å‹•ã—ã¦ãã ã•ã„。

$ cd ..

ãã—ã¦ã€ã‚µãƒ³ãƒ—ルプロジェクトをcloneã—ã¾ã™ã€‚

$ git clone https://github.com/making/hello-cv.git
$ cd hello-cv

ãƒã‚§ãƒƒã‚¯ã‚¢ã‚¦ãƒˆã—ãŸãƒ—ロジェクトã®pom.xmlã®å¾ŒåŠã®éƒ¨åˆ†ã‚’見ã¦ãã ã•ã„。

<profiles>
    <profile>
        <id>macosx-x86_64</id>
        <activation>
            <os>
                <family>mac</family>
                <arch>x86_64</arch>
            </os>
        </activation>
        <properties>
            <classifier>macosx-x86_64</classifier>
        </properties>
    </profile>
    <profile>
        <id>linux-x86_64</id>
        <activation>
            <os>
                <family>unix</family>
                <arch>amd64</arch>
            </os>
        </activation>
        <properties>
            <classifier>linux-x86_64</classifier>
        </properties>
    </profile>
    <profile>
        <id>windows-x86_64</id>
        <activation>
            <os>
                <family>windows</family>
                <arch>amd64</arch>
            </os>
        </activation>
        <properties>
            <classifier>windows-x86_64</classifier>
        </properties>
    </profile>
    <profile>
        <id>windows-x86</id>
        <activation>
            <os>
                <family>windows</family>
                <arch>x86</arch>
            </os>
        </activation>
        <properties>
            <classifier>windows-x86</classifier>
        </properties>
    </profile>
</profiles>

実行環境ã«ã‚ˆã‚Šã€ã©ã®ãƒ—ロファイル(アーキテクãƒãƒ£)を使用ã™ã‚‹ã‹ã‚’判断ã—ã¦ã„ã¾ã™ã€‚ã“ã®ãƒ—ロファイルã§å®šç¾©ã•ã‚Œã¦ã„ã‚‹<classifier>プロパティãŒã€

<dependency>
    <groupId>org.bytedeco.javacpp-presets</groupId>
    <artifactId>opencv</artifactId>
    <version>${opencv.version}</version>
    <classifier>${classifier}</classifier>
</dependency>

ã«ä½¿ã‚ã‚Œã€ç’°å¢ƒã«ã‚ã£ãŸãƒã‚¤ãƒ†ã‚£ãƒ–ライブラリをダウンロードã—ã¾ã™ã€‚

ã§ã¯æ—©é€Ÿã‚µãƒ³ãƒ—ルアプリを実行ã—ã¦ã¿ã¾ã—ょã†ã€‚

$ mvn compile exec:java -Dexec.mainClass=com.example.App

次ã®ã‚ˆã†ãªãƒ­ã‚°ãŒã€å‡ºåŠ›ã•ã‚Œã€

path = /Users/maki/tmp/hello-cv/target/classes/lena.png
image = IplImage[width=512,height=512,depth=8,nChannels=3]

下図ã®ã‚ˆã†ã«ã€src/main/resources/lena.pngã®ã‚µã‚¤ã‚ºãŒåŠåˆ†ã«ãªã£ãŸhalf-lena.png(å³å´)ãŒå‡ºæ¥ã‚ãŒã‚Šã¾ã™ã€‚

_images/half.png

ã“ã®ãƒ—ログラムãŒå•é¡Œãªã実行ã§åˆ‡ã‚Œã„ã‚Œã°ã€OpenCVã®å‹•ä½œæ¤œè¨¼ã¯OKã§ã™ã€‚以下ã¯èª­ã¿é£›ã°ã—ã¦ã‚‚構ã„ã¾ã›ã‚“。

注釈

以下ã®ã‚ˆã†ãªã‚¨ãƒ©ãƒ¼ãŒå‡ºã¦ã„ãŸã‚‰ã€ãƒã‚¤ãƒ†ã‚£ãƒ–ライブラリãŒæ­£ã—ã設定ã•ã‚Œã¦ã„ã¾ã›ã‚“。

java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:293)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.UnsatisfiedLinkError: no jniopencv_core in java.library.path
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1857)
    at java.lang.Runtime.loadLibrary0(Runtime.java:870)
    at java.lang.System.loadLibrary(System.java:1119)
    at org.bytedeco.javacpp.Loader.loadLibrary(Loader.java:535)
    at org.bytedeco.javacpp.Loader.load(Loader.java:410)
    at org.bytedeco.javacpp.Loader.load(Loader.java:353)
    at org.bytedeco.javacpp.opencv_core.<clinit>(opencv_core.java:10)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:340)
    at org.bytedeco.javacpp.Loader.load(Loader.java:385)
    at org.bytedeco.javacpp.Loader.load(Loader.java:353)
    at org.bytedeco.javacpp.opencv_highgui.<clinit>(opencv_highgui.java:13)
    at com.example.App.resize(App.java:18)
    at com.example.App.main(App.java:14)
    ... 6 more
Caused by: java.lang.UnsatisfiedLinkError: no opencv_core in java.library.path
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1857)
    at java.lang.Runtime.loadLibrary0(Runtime.java:870)
    at java.lang.System.loadLibrary(System.java:1119)
    at org.bytedeco.javacpp.Loader.loadLibrary(Loader.java:535)
    at org.bytedeco.javacpp.Loader.load(Loader.java:401)
    ... 15 more

ã†ã¾ãã„ã‹ãªã„å ´åˆã¯ã€è‡ªåˆ†ã®ç’°å¢ƒã«åˆã‚ã›ã¦ã€æ¬¡ã®ã‚ˆã†ã«æ˜Žç¤ºçš„ã«ãƒ—ロファイルを指定ã—ã¦ã¿ã¦ãã ã•ã„。

$ mvn compile exec:java -Dexec.mainClass=com.example.App -P<classifier>

<classifier>ã«ã¯ä»¥ä¸‹ã®ã„ãšã‚Œã‹ã®å€¤ãŒå…¥ã‚Šã¾ã™ã€‚

  • windows-x86_64
  • linux-x86_64
  • macosx-x86_64
  • windows-x86
  • linux-x86

å°‘ã—ã ã‘ソースコードを確èªã—ã¾ã—ょã†ã€‚

package com.example;

import java.net.URISyntaxException;
import java.nio.file.Paths;

import static org.bytedeco.javacpp.opencv_core.*;
import static org.bytedeco.javacpp.opencv_highgui.*;
import static org.bytedeco.javacpp.opencv_imgproc.*;

public class App {
    public static void main(String[] args) throws URISyntaxException {
        // 引数ã§ä¸Žãˆã‚‰ã‚ŒãŸãƒ‘スã‹ã‚¯ãƒ©ã‚¹ãƒ‘ス上ã®lena.pngを使用ã™ã‚‹ã€‚
        String filepath = args.length > 0 ? args[0] : Paths.get(
                App.class.getResource("/lena.png").toURI()).toString();
        resize(filepath);
    }

    public static void resize(String filepath) {
        // ç”»åƒã‚’読ã¿è¾¼ã‚“ã§ã€IplImageインスタンスを作æˆã™ã‚‹
        IplImage source = cvLoadImage(filepath, CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR);
        System.out.println("path = " + filepath);
        System.out.println("image = " + source);
        if (source != null) {
            // 変æ›å¾Œã®ç”»åƒã‚’作æˆã™ã‚‹ã€‚å¹…ã¨é«˜ã•ãŒå…ƒç”»åƒã®åŠåˆ†ã«ãªã‚‹ã‚ˆã†ã«ã™ã‚‹
            IplImage dest = cvCreateImage(cvSize(source.width() / 2, source.height() / 2), source.depth(), source.nChannels());
            // リサイズã™ã‚‹
            cvResize(source, dest, CV_INTER_NN);
            // ç”»åƒã‚’ä¿å­˜ã™ã‚‹
            cvSaveImage("half-" + Paths.get(filepath).getFileName().toString(), dest);
            cvReleaseImage(source);
            cvReleaseImage(dest);
        }
    }
}

引数をã¨ã£ã¦ä»»æ„ã®ç”»åƒã‚’リサイズã™ã‚‹å ´åˆã¯ã€ä»¥ä¸‹ã®ã‚ˆã†ã«å®Ÿè¡Œã—ã¦ãã ã•ã„。

$ mvn compile exec:java -Dexec.mainClass=com.example.App -Dexec.args=hoge.png

ã“ã“ã§ã¯å¤ã„OpenCVã®APIを使用ã—ã¾ã—ãŸã€‚

次ã«æ–°ã—ã„OpenCV 2ç³»ã®C++ APIã«å¯¾å¿œã—ãŸJava APIを使用ã—ã¾ã™ã€‚ã¾ãŸã€OpenCVã§ãŠãªã˜ã¿ã®é¡”èªè­˜ã‚’è¡Œã„ã¾ã™ã€‚

注釈

Open CV 2ç³»ã®APIリファレンスã¯ã“ã®ã‚µã‚¤ãƒˆãŒã‚ã‹ã‚Šã‚„ã™ã„ã§ã™ã€‚ã»ã¨ã‚“ã©ã®ã‚³ãƒ¼ãƒ‰ãŒJavaCVã§ã‚‚利用ã§ãã‚‹ã®ã§ã€éŠã‚“ã§ã¿ã¦ãã ã•ã„。

サンプルコードã®ãƒ–ランãƒã‚’dukerã«åˆ‡ã‚Šæ›¿ãˆã¾ã™ã€‚

$ git checkout duker

å†åº¦ã€ã‚µãƒ³ãƒ—ルアプリを実行ã—ã¾ã—ょã†ã€‚

$ mvn compile exec:java -Dexec.mainClass=com.example.App

次ã®ã‚ˆã†ãªãƒ­ã‚°ãŒã€å‡ºåŠ›ã•ã‚Œã€

load /Users/maki/tmp/hello-cv/target/classes/lena.png
1 faces are detected!

下図ã®ã‚ˆã†ã«ã€src/main/resources/lena.pngã®é¡”ã®éƒ¨åˆ†ãŒDukeã®ã‚ˆã†ã«å¤‰æ›ã•ã‚ŒãŸduked-faces.png(å³å´)ãŒå‡ºæ¥ã‚ãŒã‚Šã¾ã™ã€‚

_images/duke.png

プログラムを見ã¦ã¿ã¾ã—ょã†ã€‚

package com.example;

import static org.bytedeco.javacpp.opencv_core.*;
import static org.bytedeco.javacpp.opencv_objdetect.*;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;

import javax.imageio.ImageIO;

public class App {
    public static void main(String[] args) throws URISyntaxException, IOException {
        String filepath = args.length > 0 ? args[0] : Paths.get(
                App.class.getResource("/lena.png").toURI()).toString();
        faceDetect(filepath);
    }

    public static void faceDetect(String filepath) throws URISyntaxException, IOException {
        // 分類器ã®èª­ã¿è¾¼ã¿
        String classifierName = Paths.get(
                App.class.getResource("/haarcascade_frontalface_default.xml")
                        .toURI()).toString();
        CascadeClassifier faceDetector = new CascadeClassifier(classifierName);
        System.out.println("load " + filepath);
        // æ–°ã—ã„APIã§ã¯ç”»åƒãƒ‡ãƒ¼ã‚¿ã‚’æ ¼ç´ã™ã‚‹ãƒ‡ãƒ¼ã‚¿æ§‹é€ ã¨ã—ã¦Matクラスを使用ã™ã‚‹ã€‚
        // ã“ã“ã§ã¯Javaã®ä¸–ç•Œã¨ã‚„ã‚Šã¨ã‚Šã—ã‚„ã™ã„よã†ã«java.awt.image.BufferedImageを経由ã™ã‚‹ã€‚
        Mat source = Mat.createFrom(ImageIO.read(new File(filepath)));
        // é¡”èªè­˜çµæžœ
        Rect faceDetections = new Rect();
        // é¡”èªè­˜å®Ÿè¡Œ
        faceDetector.detectMultiScale(source, faceDetections);
        // èªè­˜ã—ãŸé¡”ã®æ•°
        int numOfFaces = faceDetections.limit();
        System.out.println(numOfFaces + " faces are detected!");
        for (int i = 0; i < numOfFaces; i++) {
            // i番目ã®èªè­˜çµæžœ
            Rect r = faceDetections.position(i);
            int x = r.x(), y = r.y(), h = r.height(), w = r.width();
            // Dukeã®ã‚ˆã†ã«æç”»ã™ã‚‹
            // 上åŠåˆ†ã®é»’四角
            rectangle(source, new Point(x, y), new Point(x + w, y + h / 2),
                    new Scalar(0, 0, 0, 0), -1, CV_AA, 0);
            // 下åŠåˆ†ã®ç™½å››è§’
            rectangle(source, new Point(x, y + h / 2), new Point(x + w, y + h),
                    new Scalar(255, 255, 255, 0), -1, CV_AA, 0);
            // 中央ã®èµ¤ä¸¸
            circle(source, new Point(x + h / 2, y + h / 2), (w + h) / 12,
                    new Scalar(0, 0, 255, 0), -1, CV_AA, 0);
        }

        // æç”»çµæžœã‚’java.awt.image.BufferedImageã§å–å¾—ã™ã‚‹ã€‚
        BufferedImage image = source.getBufferedImage();
        try (OutputStream out = Files.newOutputStream(Paths
                .get("duked-faces.png"))) {
            // ç”»åƒã‚’出力ã™ã‚‹
            ImageIO.write(image, "png", out);
        }
    }
}

引数ã«å¥½ããªç”»åƒã‚’ã¨ã£ã¦Duke化ã§ãã¾ã™ã€‚

$ mvn compile exec:java -Dexec.mainClass=com.example.App -Dexec.args=hoge.png

ã¾ãŸã€ãƒ«ãƒ¼ãƒ—処ç†ãªã„ã®æ画部分を書ãæ›ãˆã¦ã€éŠã‚“ã§ã¿ã¦ãã ã•ã„。æç”»ã®ä¾‹ã¯ã€ã“ã®ãƒªãƒ•ã‚¡ãƒ¬ãƒ³ã‚¹ãŒå½¹ç«‹ã¡ã¾ã™ã€‚

警告

ã“ã®ãƒ—ログラムã¯é€éŽpngを入力画åƒã«ä½¿ã†ã¨ã†ã¾ãæç”»ã§ãã¾ã›ã‚“。

本章ã®å†…容を修了ã—ãŸã‚‰ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ#kanjava_sbc #sbc02ã€ã‚’ã¤ã‘ã¦ãƒ„イートã—ã¦ãã ã•ã„。

次章ã§ã¯ã€ã“ã®ã‚µãƒ³ãƒ—ルã¨Spring Bootã‚’çµ±åˆã—ã¦ã€é¡”変æ›Webサービスを作りã¾ã™ã€‚

顔変æ›ã‚µãƒ¼ãƒ“スã®ä½œæˆÂ¶

本章ã§ã¯ã€Œ[事å‰æº–å‚™] Spring Bootã§Hello Worldã€ã¨ã€Œ[事å‰æº–å‚™] Javaã§OpenCVを使ã†ã€ã§ä½œæˆã—ãŸå†…容を統åˆã—ã¦ã€é¡”変æ›Webサービスを作æˆã—ã¾ã™ã€‚

ã¾ãšã¯ã€Œ[事å‰æº–å‚™] Spring Bootã§Hello Worldã€ã§ä½œæˆã—ãŸpom.xmlã«ã€ã€Œ[事å‰æº–å‚™] Javaã§OpenCVを使ã†ã€ã®å†…容を追記ã—ã¾ã™ã€‚

以下ã®ãƒã‚¤ãƒ©ã‚¤ãƒˆã•ã‚ŒãŸæ–‡ã‚’追加ã—ã¦ãã ã•ã„。

<?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>kanjava</groupId>
    <artifactId>kusokora</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Spring Boot Docker Blank Project (from https://github.com/making/spring-boot-docker-blank)</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.1.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>kanjava.App</start-class>
        <java.version>1.8</java.version>
        <javacv.version>0.10</javacv.version>
        <opencv.version>2.4.10-${javacv.version}</opencv.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv</artifactId>
            <version>${javacv.version}</version>
        </dependency>
        <dependency>
            <groupId>org.bytedeco.javacpp-presets</groupId>
            <artifactId>opencv</artifactId>
            <version>${opencv.version}</version>
        </dependency>
        <dependency>
            <groupId>org.bytedeco.javacpp-presets</groupId>
            <artifactId>opencv</artifactId>
            <version>${opencv.version}</version>
            <classifier>${classifier}</classifier>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <dependencies>
                    <dependency>
                        <groupId>org.springframework</groupId>
                        <artifactId>springloaded</artifactId>
                        <version>${spring-loaded.version}</version>
                    </dependency>
                </dependencies>
            </plugin>

            <!-- Copy Dockerfile -->
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-resources</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${basedir}/target/</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>src/main/docker</directory>
                                    <filtering>true</filtering>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>com.coderplus.maven.plugins</groupId>
                <artifactId>copy-rename-maven-plugin</artifactId>
                <version>1.0</version>
                <executions>
                    <execution>
                        <id>rename-file</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>rename</goal>
                        </goals>
                        <configuration>
                            <sourceFile>${basedir}/target/Dockerfile.txt</sourceFile>
                            <destinationFile>${basedir}/target/Dockerfile</destinationFile>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
                <version>1.7</version>
                <executions>
                    <execution>
                        <id>zip-files</id>
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <zip destfile="${basedir}/target/app.zip" basedir="${basedir}/target"
                                     includes="Dockerfile, Dockerrun.aws.json, ${project.artifactId}.jar"/>
                            </target>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>macosx-x86_64</id>
            <activation>
                <os>
                    <family>mac</family>
                    <arch>x86_64</arch>
                </os>
            </activation>
            <properties>
                <classifier>macosx-x86_64</classifier>
            </properties>
        </profile>
        <profile>
            <id>linux-x86_64</id>
            <activation>
                <os>
                    <family>unix</family>
                    <arch>amd64</arch>
                </os>
            </activation>
            <properties>
                <classifier>linux-x86_64</classifier>
            </properties>
        </profile>
        <profile>
            <id>windows-x86_64</id>
            <activation>
                <os>
                    <family>windows</family>
                    <arch>amd64</arch>
                </os>
            </activation>
            <properties>
                <classifier>windows-x86_64</classifier>
            </properties>
        </profile>
        <profile>
            <id>windows-x86</id>
            <activation>
                <os>
                    <family>windows</family>
                    <arch>x86</arch>
                </os>
            </activation>
            <properties>
                <classifier>windows-x86</classifier>
            </properties>
        </profile>
    </profiles>
</project>

次ã«ã€Œ[事å‰æº–å‚™] Spring Bootã§Hello Worldã€ã§ä½œæˆã—ãŸAppクラスã«ã€ã€Œ[事å‰æº–å‚™] Javaã§OpenCVを使ã†ã€ã§ä½œæˆã—ãŸé¡”変æ›å‡¦ç†ã‚’移æ¤ã—ã¾ã™ã€‚

「[事å‰æº–å‚™] Javaã§OpenCVを使ã†ã€ã§ã¯1メソッドã«ãƒ™ã‚¿æ›¸ãã—ãŸã®ã§ã€ä»Šå›žã¯ä»¥ä¸‹ã®ã‚ˆã†ã«é¡”検出処ç†ã¨é¡”変æ›å‡¦ç†ã‚’分ã‘ã¦ã€ãã‚Œãžã‚Œåˆ¥ã‚¯ãƒ©ã‚¹ã«å®šç¾©ã—ã¾ã™ã€‚

package kanjava;

import static org.bytedeco.javacpp.opencv_core.*;
import static org.bytedeco.javacpp.opencv_objdetect.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.function.BiConsumer;

@SpringBootApplication
@RestController
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @RequestMapping(value = "/")
    String hello() {
        return "Hello World!";
    }
}

@Component // コンãƒãƒ¼ãƒãƒ³ãƒˆã‚¹ã‚­ãƒ£ãƒ³å¯¾è±¡ã«ã™ã‚‹ã€‚@Serviceã§ã‚‚@Namedã§ã‚‚OK
class FaceDetector {
    public void detectFaces(Mat source /* å…¥åŠ›ç”»åƒ */, BiConsumer<Mat, Rect> detectAction /* 顔領域ã«å¯¾å¿œã™ã‚‹å‡¦ç† */) {
        // ã“ã“ã«é¡”検出処ç†ã‚’実装ã™ã‚‹
    }
}

class FaceTranslator {
    public static void duker(Mat source, Rect r) { // Duke化ã™ã‚‹ãƒ¡ã‚½ãƒƒãƒ‰
        // ã“ã“ã«é¡”変æ›å‡¦ç†ã‚’実装ã™ã‚‹
    }
}

実際ã®å‡¦ç†ã‚’埋ã‚ã¾ã—ょã†ã€‚

package kanjava;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.util.function.BiConsumer;

import static org.bytedeco.javacpp.opencv_core.*;
import static org.bytedeco.javacpp.opencv_objdetect.*;

@SpringBootApplication
@RestController
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @RequestMapping(value = "/")
    String hello() {
        return "Hello World!";
    }
}

@Component
class FaceDetector {
    // 分類器ã®ãƒ‘スをプロパティã‹ã‚‰å–å¾—ã§ãるよã†ã«ã™ã‚‹
    @Value("${classifierFile:classpath:/haarcascade_frontalface_default.xml}")
    File classifierFile;

    CascadeClassifier classifier;

    static final Logger log = LoggerFactory.getLogger(FaceDetector.class);

    public void detectFaces(Mat source, BiConsumer<Mat, Rect> detectAction) {
        // é¡”èªè­˜çµæžœ
        Rect faceDetections = new Rect();
        // é¡”èªè­˜å®Ÿè¡Œ
        classifier.detectMultiScale(source, faceDetections);
        // èªè­˜ã—ãŸé¡”ã®æ•°
        int numOfFaces = faceDetections.limit();
        log.info("{} faces are detected!", numOfFaces);
        for (int i = 0; i < numOfFaces; i++) {
            // i番目ã®èªè­˜çµæžœ
            Rect r = faceDetections.position(i);
            // 1件ã”ã¨ã®èªè­˜çµæžœã‚’変æ›å‡¦ç†(関数)ã«ã‹ã‘ã‚‹
            detectAction.accept(source, r);
        }
    }

    @PostConstruct // åˆæœŸåŒ–処ç†ã€‚DIã§ãƒ—ロパティãŒã‚»ãƒƒãƒˆã•ã‚ŒãŸã‚ã¨ã«classifierインスタンスを生æˆã—ãŸã„ã®ã§ã“ã“ã§æ›¸ã。
    void init() throws IOException {
        if (log.isInfoEnabled()) {
            log.info("load {}", classifierFile.toPath());
        }
        // 分類器ã®èª­ã¿è¾¼ã¿
        this.classifier = new CascadeClassifier(classifierFile.toPath()
                .toString());
    }
}

class FaceTranslator {
    public static void duker(Mat source, Rect r) { // BiConsumer<Mat, Rect>ã§æ¸¡ã›ã‚‹ã‚ˆã†ã«ã™ã‚‹
        int x = r.x(), y = r.y(), h = r.height(), w = r.width();
        // Dukeã®ã‚ˆã†ã«æç”»ã™ã‚‹
        // 上åŠåˆ†ã®é»’四角
        rectangle(source, new Point(x, y), new Point(x + w, y + h / 2),
                new Scalar(0, 0, 0, 0), -1, CV_AA, 0);
        // 下åŠåˆ†ã®ç™½å››è§’
        rectangle(source, new Point(x, y + h / 2), new Point(x + w, y + h),
                new Scalar(255, 255, 255, 0), -1, CV_AA, 0);
        // 中央ã®èµ¤ä¸¸
        circle(source, new Point(x + h / 2, y + h / 2), (w + h) / 12,
                new Scalar(0, 0, 255, 0), -1, CV_AA, 0);
    }
}

次ã«ã€ã“ã®ç”»åƒå‡¦ç†ãƒ­ã‚¸ãƒƒã‚¯ã‚’Controllerã‹ã‚‰å©ãã¾ã™ã€‚処ç†çµæžœã®ç”»åƒã‚’レスãƒãƒ³ã‚¹ã¨ã—ã¦è¿”ã™ã®ã«JavaCVã‹ã‚‰æ‰±ã„ã‚„ã™ã„BufferedImageã‚’ãã®ã¾ã¾ã‚·ãƒªã‚¢ãƒ©ã‚¤ã‚ºã•ã›ã¾ã—ょã†ã€‚ BufferedImageã®ã‚·ãƒªã‚¢ãƒ©ã‚¤ã‚ºã¯Spring Bootã®ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã§ã¯å¯¾å¿œã—ã¦ã„ãªã„ã®ã§ã™ãŒã€ç‰¹å®šã®åž‹ã«å¯¾ã™ã‚‹ãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ»ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã‚’扱ã†ãŸã‚ã®HttpMessageConverterã®BufferedImageã¯ç”¨æ„ã•ã‚Œã¦ã„ã¾ã™ã€‚org.springframework.http.converter.BufferedImageHttpMessageConverterã§ã™ã€‚

Spring Bootã§æ–°ã—ã„HttpMessageConverterを追加ã—ãŸã„å ´åˆã€å¯¾è±¡ã®HttpMessageConverterã‚’Bean定義ã™ã‚‹ã ã‘ã§è‰¯ã„ã§ã™ã€‚

Spring Bootã§Bean定義ã™ã‚‹å ´åˆã¯é€šå¸¸ã€@Beanを使ã£ã¦Javaã§å®šç¾©ã—ã¾ã™ã€‚@Configuration (ã¾ãŸã¯ãれを内包ã™ã‚‹@SpringBootApplication) ãŒã¤ã„ãŸã‚¯ãƒ©ã‚¹ã®ä¸­ã§ã€ インスタンスを生æˆã™ã‚‹ãƒ¡ã‚½ãƒƒãƒ‰ã‚’書ãã€ãã®ãƒ¡ã‚½ãƒƒãƒ‰ã«@Beanã‚’ã¤ã‘ã‚Œã°è‰¯ã„ã§ã™ã€‚

今回ã®å ´åˆã€ä»¥ä¸‹ã®ã‚ˆã†ã«ãªã‚Šã¾ã™ã€‚

@Bean
BufferedImageHttpMessageConverter bufferedImageHttpMessageConverter() {
    return new BufferedImageHttpMessageConverter();
}

ãã‚Œã§ã¯ç”»åƒå¤‰æ›ã‚’è¡Œã†Controllerã®å‡¦ç†ã‚’追加ã—ã¾ã—ょã†ã€‚

package kanjava;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.BufferedImageHttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;
import javax.servlet.http.Part;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.function.BiConsumer;

import static org.bytedeco.javacpp.opencv_core.*;
import static org.bytedeco.javacpp.opencv_objdetect.*;

@SpringBootApplication
@RestController
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Autowired // FaceDetectorをインジェクション
    FaceDetector faceDetector;

    @Bean // HTTPã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ»ãƒ¬ã‚¹ãƒãƒ³ã‚¹ãƒœãƒ‡ã‚£ã«BufferedImageを使ãˆã‚‹ã‚ˆã†ã«ã™ã‚‹
    BufferedImageHttpMessageConverter bufferedImageHttpMessageConverter() {
        return new BufferedImageHttpMessageConverter();
    }

    @RequestMapping(value = "/")
    String hello() {
        return "Hello World!";
    }

    // curl -v -F 'file=@hoge.jpg' http://localhost:8080/duker > after.jpg ã¨ã„ã†é¢¨ã«ä½¿ãˆã‚‹ã‚ˆã†ã«ã™ã‚‹
    @RequestMapping(value = "/duker", method = RequestMethod.POST) // POSTã§/dukerã¸ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å¯¾ã™ã‚‹å‡¦ç†
    BufferedImage duker(@RequestParam Part file /* パラメータåfileã®ãƒžãƒ«ãƒãƒ‘ートリクエストã®ãƒ‘ラメータをå–å¾— */) throws IOException {
        Mat source = Mat.createFrom(ImageIO.read(file.getInputStream())); // Part -> BufferedImage -> Matã¨å¤‰æ›
        faceDetector.detectFaces(source, FaceTranslator::duker); // 対象ã®Matã«å¯¾ã—ã¦é¡”èªè­˜ã€‚èªè­˜çµæžœã«å¯¾ã—ã¦duker関数をé©ç”¨ã™ã‚‹ã€‚
        BufferedImage image = source.getBufferedImage(); // Mat -> BufferedImage
        return image;
    }
}

@Component
class FaceDetector {
    @Value("${classifierFile:classpath:/haarcascade_frontalface_default.xml}")
    File classifierFile;

    CascadeClassifier classifier;

    static final Logger log = LoggerFactory.getLogger(FaceDetector.class);

    public void detectFaces(Mat source, BiConsumer<Mat, Rect> detectAction) {
        // é¡”èªè­˜çµæžœ
        Rect faceDetections = new Rect();
        // é¡”èªè­˜å®Ÿè¡Œ
        classifier.detectMultiScale(source, faceDetections);
        // èªè­˜ã—ãŸé¡”ã®æ•°
        int numOfFaces = faceDetections.limit();
        log.info("{} faces are detected!", numOfFaces);
        for (int i = 0; i < numOfFaces; i++) {
            // i番目ã®èªè­˜çµæžœ
            Rect r = faceDetections.position(i);
            // èªè­˜çµæžœã‚’変æ›å‡¦ç†ã«ã‹ã‘ã‚‹
            detectAction.accept(source, r);
        }
    }

    @PostConstruct
    void init() throws IOException {
        if (log.isInfoEnabled()) {
            log.info("load {}", classifierFile.toPath());
        }
        // 分類器ã®èª­ã¿è¾¼ã¿
        this.classifier = new CascadeClassifier(classifierFile.toPath()
                .toString());
    }
}

class FaceTranslator {
    public static void duker(Mat source, Rect r) {
        int x = r.x(), y = r.y(), h = r.height(), w = r.width();
        // Dukeã®ã‚ˆã†ã«æç”»ã™ã‚‹
        // 上åŠåˆ†ã®é»’四角
        rectangle(source, new Point(x, y), new Point(x + w, y + h / 2),
                new Scalar(0, 0, 0, 0), -1, CV_AA, 0);
        // 下åŠåˆ†ã®ç™½å››è§’
        rectangle(source, new Point(x, y + h / 2), new Point(x + w, y + h),
                new Scalar(255, 255, 255, 0), -1, CV_AA, 0);
        // 中央ã®èµ¤ä¸¸
        circle(source, new Point(x + h / 2, y + h / 2), (w + h) / 12,
                new Scalar(0, 0, 255, 0), -1, CV_AA, 0);
    }
}

実行ã™ã‚‹å‰ã«ã€ã€Œ[事å‰æº–å‚™] Javaã§OpenCVを使ã†ã€ã§ä½¿ç”¨ã—ãŸhaarcascade_frontalface_default.xmlã‚’src/main/resourcesã«ã‚³ãƒ”ーã—ã¾ã—ょã†ã€‚以下ã®ã‚ˆã†ã«wgetã—ã¦ã‚‚構ã„ã¾ã›ã‚“。

$ wget https://github.com/making/hello-cv/raw/duker/src/main/resources/haarcascade_frontalface_default.xml

ファイルをコピーã—ãŸã‚‰ã€æ—©é€Ÿèµ·å‹•ã—ã¾ã—ょã†ã€‚

$ mvn spring-boot:run

mainメソッド実行ã§ã‚‚構ã„ã¾ã›ã‚“。

顔画åƒã‚’以下ã®ã‚ˆã†ã«é€ã£ã¦ãã ã•ã„。

$ curl -v -F 'file=@hoge.jpg' http://localhost:8080/duker > after.jpg

ç”»åƒã®ãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆãŒèªè­˜ã•ã‚Œãªã„å ´åˆã¯ã€ãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ‘スã«æ‹¡å¼µå­ã‚’ã¤ã‘ã¦ãƒ¡ãƒ‡ã‚£ã‚¢ã‚¿ã‚¤ãƒ—を明示ã—ã¦ãã ã•ã„。

$ curl -v -F 'file=@hoge.jpg' http://localhost:8080/duker.jpg > after.jpg

変æ›å¾Œã®after.jpgã‚’é–‹ã„ã¦ãã ã•ã„。顔ãŒduke化ã•ã‚Œã¦ã„ã¾ã™ã‹ï¼Ÿ

余裕ãŒã‚ã‚Œã°ã€FaceTranslatorã«ç‹¬è‡ªã®é¡”変æ›ãƒ­ã‚¸ãƒƒã‚¯ã‚’書ã„ã¦ã¿ã¾ã—ょã†ã€‚

class FaceTranslator {
    // ...

    public static void kusokora(Mat source, Rect r) {
        // 変æ›å‡¦ç†
    }
}

Controllerã«ã‚‚以下ã®ãƒ¡ã‚½ãƒƒãƒ‰ã‚’追加ã—ã¾ã—ょã†ã€‚

@RequestMapping(value = "/kusokora", method = RequestMethod.POST)
BufferedImage kusokora(@RequestParam Part file) throws IOException {
    Mat source = Mat.createFrom(ImageIO.read(file.getInputStream()));
    faceDetector.detectFaces(source, FaceTranslator::kusokora);
    BufferedImage image = source.getBufferedImage();
    return image;
}

以上ã§æœ¬ç« ã¯çµ‚了ã§ã™ã€‚

本章ã®å†…容を修了ã—ãŸã‚‰ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ#kanjava_sbc #sbc03ã€ã‚’ã¤ã‘ã¦ãƒ„イートã—ã¦ãã ã•ã„。 是éžå¤‰æ›å¾Œã®ç”»åƒã‚‚ã¤ã‘ã¦ãƒ„イートã—ã¦ãã ã•ã„。

警告

実ã¯ã“ã®ã‚¯ãƒ©ã‚¹(Controller)ã«ã¯ãƒã‚°ãŒã‚ã‚Šã¾ã™ã€‚「JMSã§ç”»åƒå¤‰æ›ã‚’éžåŒæœŸå‡¦ç†ã€ã§ä¿®æ­£ã—ã¾ã™ãŒã€å•é¡Œã«æ°—ã¥ãã¾ã—ãŸã‹ï¼Ÿ

次ã¯ã“ã®é¡”変æ›å‡¦ç†ã‚’éžåŒæœŸã§è¡Œã†ã‚ˆã†ã«ã—ã¾ã™ã€‚次章ã§ã¯ãã®å‰æ®µã¨ã—ã¦ã€Spring Bootã§JMSを使ã†æ–¹æ³•ã‚’å­¦ã³ã¾ã™ã€‚

JMSを使ã£ã¦ã¿ã‚‹Â¶

å‰ç« ã§ã¯ç”»åƒå‡¦ç†ã‚µãƒ¼ãƒãƒ¼ã‚’Spring MVCã§ä½œæˆã—ã¾ã—ãŸã€‚一般的ã«ã€ç”»åƒå‡¦ç†ã®ã‚ˆã†ãªé‡ã„処ç†ã‚’åŒæœŸå®Ÿè¡Œã—ã¦ã„ãã¨ã€ åŒæ™‚リクエストã«ã‚ˆã£ã¦ãƒªã‚¯ã‚¨ã‚¹ãƒˆå‡¦ç†ã‚¹ãƒ¬ãƒƒãƒ‰ãŒæž¯æ¸‡ã—ã‚„ã™ããªã£ã¦ã—ã¾ã„ã¾ã™ã€‚

ãã“ã§ã€ç”»åƒå‡¦ç†ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’å—ã‘ã¦ã‚‚ã™ãã«å‡¦ç†ã¯è¡Œã‚ãšãƒ¬ã‚¹ãƒãƒ³ã‚¹ã ã‘è¿”ã—ã€å®Ÿéš›ã®å‡¦ç†ã¯éžåŒæœŸã§è¡Œã†ã“ã¨ã‚’考ãˆã¾ã—ょã†ã€‚

本章ã§ã¯JMS(Java Message Service)を使用ã—ã¦ã€éžåŒæœŸãƒ—ログラミングを試ã—ã¾ã™ã€‚次章ã§ç”»åƒå‡¦ç†ã‚µãƒ¼ãƒãƒ¼ã®JMS対応を行ã„ã¾ã™ã€‚

HTTPリクエストをå—ã‘ãŸControllerã¯ã€ã™ãã«æœ¬å‡¦ç†ã‚’è¡Œã†ã®ã§ã¯ãªãã€æœ¬å‡¦ç†ã«å¿…è¦ãªãƒ‡ãƒ¼ã‚¿ã‚’è©°ã‚ãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’JMSã«å¯¾å¿œã—ãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚­ãƒ¥ãƒ¼è£½å“ã«é€ä¿¡ã—ã¾ã™ã€‚ メッセージé€ä¿¡ãŒå®Œäº†ã™ã‚Œã°HTTPレスãƒãƒ³ã‚¹ã‚’è¿”å´ã—ã¾ã™ã€‚

_images/jms-send.png

é€ä¿¡ã•ã‚ŒãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã¯å—ä¿¡å´ã«ã‚ˆã£ã¦å–り出ã•ã‚Œã€æœ¬å‡¦ç†ãŒãŠã“ãªã‚Œã¾ã™ã€‚JMSã«ã‚ˆã‚‹ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸å—信方法ã¯è‰²ã€…ã‚ã‚‹ã®ã§ã™ãŒã€ä»Šå›žã¯Message Listenerを使用ã—ã¾ã™ã€‚

_images/jms-receive.png

本ãƒãƒ³ã‚ºã‚ªãƒ³ã§ã¯ã€ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚­ãƒ¥ãƒ¼ã¨ã—ã¦HornetQを使ã„ã¾ã™ã€‚通常メッセージキューã¯åˆ¥ãƒ—ロセスã¨ã—ã¦èµ·å‹•ã•ã›ã¾ã™ãŒã€ä»Šå›žã¯ã‚»ãƒƒãƒˆã‚¢ãƒƒãƒ—ã®æ‰‹é–“ã‚’çœããŸã‚〠組ã¿è¾¼ã¿ã‚¤ãƒ³ãƒ¡ãƒ¢ãƒªHornetQを使ã„ã€ã‚¢ãƒ—リã¨åŒã˜ãƒ—ロセス内ã§èµ·å‹•ã•ã›ã¾ã™ã€‚

ã¾ãŸã€JMS APIを直接使ã‚ãšã€Springã®JMSサãƒãƒ¼ãƒˆæ©Ÿèƒ½ã‚’使用ã—ã¾ã™ã€‚

ã¾ãšã¯ã€ã“ã‚Œã¾ã§ä½œã£ãŸãƒ—ロジェクトã®pom.xmlã«ä»¥ä¸‹ã®ä¾å­˜é–¢ä¿‚を追加ã—ã¦ãã ã•ã„。

<!-- Springã®JMSサãƒãƒ¼ãƒˆã¨HornetQã®ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆãƒ©ã‚¤ãƒ–ラリを追加 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hornetq</artifactId>
</dependency>
<!-- 組ã¿è¾¼ã¿ã‚¤ãƒ³ãƒ¡ãƒ¢ãƒªHornetQ -->
<dependency>
    <groupId>org.hornetq</groupId>
    <artifactId>hornetq-jms-server</artifactId>
</dependency>

次ã«application.ymlã«çµ„ã¿è¾¼ã¿HornetQã®è¨­å®šã‚’è¡Œã„ã¾ã™ã€‚

spring:
  thymeleaf.cache: false
  main.show-banner: false
  hornetq:
    mode: embedded
    embedded:
      enabled: true
      queues: hello # 宛先å

ã“ã®æ®µéšŽã§Appクラスを実行ã—ã¦ã¿ã¦ãã ã•ã„。以下ã®ã‚ˆã†ãªãƒ­ã‚°ãŒå‡ºã¦ã€çµ„ã¿è¾¼ã¿HornetQãŒèµ·å‹•ã—ã¦ã„ã‚‹ã®ãŒã‚ã‹ã‚Šã¾ã™ã€‚

2015-02-28 20:33:14.431  INFO 13107 --- [           main] org.hornetq.core.server                  : HQ221000: live server is starting with configuration HornetQ Configuration (clustered=false,backup=false,sharedStore=true,journalDirectory=/var/folders/9p/hr0h11p124l0z7d3sqpvf5lw0000gn/T/hornetq-data/journal,bindingsDirectory=data/bindings,largeMessagesDirectory=data/largemessages,pagingDirectory=data/paging)
2015-02-28 20:33:14.443  INFO 13107 --- [           main] org.hornetq.core.server                  : HQ221045: libaio is not available, switching the configuration into NIO
2015-02-28 20:33:14.488  INFO 13107 --- [           main] org.hornetq.core.server                  : HQ221043: Adding protocol support CORE
2015-02-28 20:33:14.558  INFO 13107 --- [           main] org.hornetq.core.server                  : HQ221003: trying to deploy queue jms.queue.hello
2015-02-28 20:33:14.642  INFO 13107 --- [           main] org.hornetq.core.server                  : HQ221007: Server is now live
2015-02-28 20:33:14.642  INFO 13107 --- [           main] org.hornetq.core.server                  : HQ221001: HornetQ Server version 2.4.5.FINAL (Wild Hornet, 124) [95281656-bf3d-11e4-aec8-496255b6cee1]

ç°¡å˜ãªJMSプログラミングを行ã„ã¾ã—ょã†ã€‚ã¾ãšã¯é€ä¿¡éƒ¨åˆ†ã‚’作りã¾ã™ã€‚

package kanjava;

// ...
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;

// ...

@SpringBootApplication
@RestController
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    private static final Logger log = LoggerFactory.getLogger(App.class); // 後ã§ä½¿ã†

    @Autowired
    FaceDetector faceDetector;
    @Autowired
    JmsMessagingTemplate jmsMessagingTemplate; // メッセージæ“作用APIã®JMSラッパー

    // ...

    @RequestMapping(value = "/send")
    String send(@RequestParam String msg /* リクエストパラメータmsgã§ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸æœ¬æ–‡ã‚’å—ã‘å–ã‚‹ */) {
        Message<String> message = MessageBuilder
                .withPayload(msg)
                .build(); // メッセージを作æˆ
        jmsMessagingTemplate.send("hello", message); // 宛先helloã«ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡
        return "OK"; // ã¨ã‚Šã‚ãˆãšOKã¨å³æ™‚応答ã—ã¦ãŠã
    }
}

ã“ã‚Œã ã‘ã ã¨é€ã‚Šã£ã±ãªã—ã§ã€ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ã‘å–ã‚Šå´ãŒã„ã¾ã›ã‚“。次ã«å—信部分(MessageListener)を書ãã¾ã—ょã†ã€‚

Spring 4.1ã‹ã‚‰JMSã®MessageListenerã¯ã¨ã¦ã‚‚書ãã‚„ã™ããªã‚Šã€Listenerã¨ãªã‚‹ãƒ¡ã‚½ãƒƒãƒ‰ã«@JmsListenerã‚’ã¤ã‘ã‚‹ã ã‘ã§ã‚ˆããªã‚Šã¾ã—ãŸã€‚ 専用ã®ã‚¯ãƒ©ã‚¹ã‚’作ã£ã¦ã‚‚良ã„ã§ã™ã—ã€Controllerã®ä¸­ã«æ›¸ã„ã¦ã‚‚有効ã§ã™ã€‚

今回ã¯ã‚·ãƒ³ãƒ—ルã«ã™ã‚‹ãŸã‚ã€Appクラス内ã«Listenerメソッドを作æˆã—ã¾ã™ã€‚

package kanjava;

// ...
import org.springframework.jms.annotation.JmsListener;
// ...

@SpringBootApplication
@RestController
public class App {
    // ...

    @RequestMapping(value = "/send")
    String send(@RequestParam String msg) {
        Message<String> message = MessageBuilder
                .withPayload(msg)
                .build();
        jmsMessagingTemplate.send("hello", message);
        return "OK";
    }

    @JmsListener(destination = "hello" /* 処ç†ã™ã‚‹ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã®å®›å…ˆã‚’指定 */)
    void handleHelloMessage(Message<String> message /* é€ä¿¡ã•ã‚ŒãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ã‘å–ã‚‹ */) {
        log.info("received! {}", message);
        log.info("msg={}", message.getPayload());
    }
}

Appクラスを実行ã—ã¦ã€æ¬¡ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’é€ã‚Šã¾ã—ょã†ã€‚

$ curl localhost:8080/send?msg=test
OK

レスãƒãƒ³ã‚¹ãŒå³è¿”ã£ã¦ãã¾ã—ãŸã€‚サーãƒãƒ¼ãƒ­ã‚°ã‚’確èªã—ã¾ã—ょã†ã€‚

2015-02-28 21:09:12.616  INFO 13367 --- [enerContainer-1] kanjava.App                              : received! GenericMessage [payload=test, headers={jms_redelivered=false, jms_deliveryMode=2, JMSXDeliveryCount=1, jms_destination=HornetQQueue[hello], jms_priority=4, id=2eb99131-6f39-f6ca-9214-30221896c891, jms_timestamp=1425125352607, jms_expiration=0, jms_messageId=ID:9b87c38b-bf42-11e4-add4-7d91fe97ae4c, timestamp=1425125352615}]
2015-02-28 21:09:12.616  INFO 13367 --- [enerContainer-1] kanjava.App                              : msg=test

メッセージキューå´ã®ã‚¹ãƒ¬ãƒƒãƒ‰(スレッドå: DefaultMessageListenerContainer-スレッド数)ã§å‡¦ç†ã•ã‚Œã¦ã„ã‚‹ã®ãŒã‚ã‹ã‚Šã¾ã™ã€‚

デフォルトã§ã¯å‡¦ç†ã‚¹ãƒ¬ãƒƒãƒ‰æ•°ã¯1ã§ã™ã€‚スレッド数を変更ã™ã‚‹å ´åˆã¯@JmsListenerã®concurrency属性を設定ã—ã¾ã™ã€‚

@JmsListener(destination = "hello", concurrency = "1-5" /* 最å°1スレッドã€æœ€å¤§5スレッドã«è¨­å®š */)

10リクエストé€ã£ã¦ã¿ã¾ã—ょã†ã€‚

$ for i in `seq 1 10`;do curl localhost:8080/send?msg=test;done
OKOKOKOKOKOKOKOKOKOK

サーãƒãƒ¼ãƒ­ã‚°ã¯ä»¥ä¸‹ã®ã‚ˆã†ã«ãªã‚Šã¾ã™ã€‚

2015-02-28 21:19:47.655  INFO 13487 --- [enerContainer-1] kanjava.App                              : received! GenericMessage [payload=test, headers={jms_redelivered=false, jms_deliveryMode=2, JMSXDeliveryCount=1, jms_destination=HornetQQueue[hello], jms_priority=4, id=76de8a0c-fd68-b059-8c91-5165e9845668, jms_timestamp=1425125987654, jms_expiration=0, jms_messageId=ID:160c3ea7-bf44-11e4-a10f-3f034703c26f, timestamp=1425125987655}]
2015-02-28 21:19:47.655  INFO 13487 --- [enerContainer-1] kanjava.App                              : msg=test
2015-02-28 21:19:47.681  INFO 13487 --- [enerContainer-2] kanjava.App                              : received! GenericMessage [payload=test, headers={jms_redelivered=false, jms_deliveryMode=2, JMSXDeliveryCount=1, jms_destination=HornetQQueue[hello], jms_priority=4, id=39870da1-e482-d98d-dba0-24702fa9cac9, jms_timestamp=1425125987680, jms_expiration=0, jms_messageId=ID:16105d5d-bf44-11e4-a10f-3f034703c26f, timestamp=1425125987681}]
2015-02-28 21:19:47.682  INFO 13487 --- [enerContainer-2] kanjava.App                              : msg=test
2015-02-28 21:19:47.705  INFO 13487 --- [enerContainer-3] kanjava.App                              : received! GenericMessage [payload=test, headers={jms_redelivered=false, jms_deliveryMode=2, JMSXDeliveryCount=1, jms_destination=HornetQQueue[hello], jms_priority=4, id=1f30f1b8-9c52-936a-5e8f-935f4f9db38b, jms_timestamp=1425125987703, jms_expiration=0, jms_messageId=ID:1613b8c3-bf44-11e4-a10f-3f034703c26f, timestamp=1425125987705}]
2015-02-28 21:19:47.705  INFO 13487 --- [enerContainer-3] kanjava.App                              : msg=test
2015-02-28 21:19:47.729  INFO 13487 --- [enerContainer-4] kanjava.App                              : received! GenericMessage [payload=test, headers={jms_redelivered=false, jms_deliveryMode=2, JMSXDeliveryCount=1, jms_destination=HornetQQueue[hello], jms_priority=4, id=4694e874-883b-8cb6-0dcf-09cf848bf9f1, jms_timestamp=1425125987727, jms_expiration=0, jms_messageId=ID:16176249-bf44-11e4-a10f-3f034703c26f, timestamp=1425125987729}]
2015-02-28 21:19:47.729  INFO 13487 --- [enerContainer-4] kanjava.App                              : msg=test
2015-02-28 21:19:47.751  INFO 13487 --- [enerContainer-5] kanjava.App                              : received! GenericMessage [payload=test, headers={jms_redelivered=false, jms_deliveryMode=2, JMSXDeliveryCount=1, jms_destination=HornetQQueue[hello], jms_priority=4, id=93c2b05a-6350-4cca-1bf7-2cb6e8e6fef8, jms_timestamp=1425125987749, jms_expiration=0, jms_messageId=ID:161abdaf-bf44-11e4-a10f-3f034703c26f, timestamp=1425125987751}]
2015-02-28 21:19:47.751  INFO 13487 --- [enerContainer-5] kanjava.App                              : msg=test
2015-02-28 21:19:47.775  INFO 13487 --- [enerContainer-1] kanjava.App                              : received! GenericMessage [payload=test, headers={jms_redelivered=false, jms_deliveryMode=2, JMSXDeliveryCount=1, jms_destination=HornetQQueue[hello], jms_priority=4, id=398007e6-70dd-4fef-1954-443fc82269c4, jms_timestamp=1425125987773, jms_expiration=0, jms_messageId=ID:161e6735-bf44-11e4-a10f-3f034703c26f, timestamp=1425125987774}]
2015-02-28 21:19:47.775  INFO 13487 --- [enerContainer-1] kanjava.App                              : msg=test
2015-02-28 21:19:47.803  INFO 13487 --- [enerContainer-2] kanjava.App                              : received! GenericMessage [payload=test, headers={jms_redelivered=false, jms_deliveryMode=2, JMSXDeliveryCount=1, jms_destination=HornetQQueue[hello], jms_priority=4, id=78080611-f326-a5b7-5737-6ff93c58c4b3, jms_timestamp=1425125987800, jms_expiration=0, jms_messageId=ID:162285eb-bf44-11e4-a10f-3f034703c26f, timestamp=1425125987803}]
2015-02-28 21:19:47.803  INFO 13487 --- [enerContainer-2] kanjava.App                              : msg=test
2015-02-28 21:19:47.835  INFO 13487 --- [enerContainer-3] kanjava.App                              : received! GenericMessage [payload=test, headers={jms_redelivered=false, jms_deliveryMode=2, JMSXDeliveryCount=1, jms_destination=HornetQQueue[hello], jms_priority=4, id=a28ddffb-e0ef-a2f9-8b09-5248f78d23d5, jms_timestamp=1425125987834, jms_expiration=0, jms_messageId=ID:1627b611-bf44-11e4-a10f-3f034703c26f, timestamp=1425125987835}]
2015-02-28 21:19:47.836  INFO 13487 --- [enerContainer-3] kanjava.App                              : msg=test
2015-02-28 21:19:47.858  INFO 13487 --- [enerContainer-4] kanjava.App                              : received! GenericMessage [payload=test, headers={jms_redelivered=false, jms_deliveryMode=2, JMSXDeliveryCount=1, jms_destination=HornetQQueue[hello], jms_priority=4, id=641fc649-d29c-ef42-b564-77985b96eb05, jms_timestamp=1425125987856, jms_expiration=0, jms_messageId=ID:162b1177-bf44-11e4-a10f-3f034703c26f, timestamp=1425125987858}]
2015-02-28 21:19:47.858  INFO 13487 --- [enerContainer-4] kanjava.App                              : msg=test
2015-02-28 21:19:47.886  INFO 13487 --- [enerContainer-5] kanjava.App                              : received! GenericMessage [payload=test, headers={jms_redelivered=false, jms_deliveryMode=2, JMSXDeliveryCount=1, jms_destination=HornetQQueue[hello], jms_priority=4, id=43dabfa3-3f07-a75b-41b9-1bfed1b716fb, jms_timestamp=1425125987884, jms_expiration=0, jms_messageId=ID:162f573d-bf44-11e4-a10f-3f034703c26f, timestamp=1425125987886}]
2015-02-28 21:19:47.887  INFO 13487 --- [enerContainer-5] kanjava.App                              : msg=test

5スレッドã§å‡¦ç†ã•ã‚Œã¦ã„ã‚‹ã“ã¨ãŒã‚ã‹ã‚Šã¾ã™ã€‚

注釈

本章ã§ä½¿ç”¨ã—ãŸJmsMessagingTemplateã¯ã€æ˜”ã‹ã‚‰ã‚ã‚‹Springã®JMS APIラッパーã§ã‚ã‚‹JmsTemplateをメッセージング抽象化機構ã§ã•ã‚‰ã«ãƒ©ãƒƒãƒ—ã—ãŸã‚‚ã®ã§ã™ã€‚Spring 4.1ã‹ã‚‰è¿½åŠ ã•ã‚Œã¾ã—ãŸã€‚ メッセージæ“作ã®ç”¨ã®ã‚·ã‚°ãƒ‹ãƒãƒ£(MessageSendingOperationsãªã©)ã‚„é€ä¿¡ã™ã‚‹Messageクラスã¯JMSã«é™ã‚‰ãšã€æ¬¡ã«èª¬æ˜Žã™ã‚‹STOMPãªã©Springã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒ³ã‚°é–¢é€£ã®ãƒ—ログラミングã§ä½¿ç”¨ã§ãã¾ã™ã€‚ ã“ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒ³ã‚°æŠ½è±¡åŒ–プロジェクトã¯spring-messagingã¨å付ã‘られã€Spring 4.0ã‹ã‚‰å…¥ã‚Šã¾ã—ãŸã€‚

Spring 4.1ã§è¿½åŠ ã•ã‚ŒãŸJMS連æºæ©Ÿèƒ½ã‚„spring-messagingã«ã¤ã„ã¦ã¯ã“ã¡ã‚‰ã®è³‡æ–™ã‚’å‚ç…§ã—ã¦ãã ã•ã„。

JMSã®ç°¡å˜ãªä½¿ã„方を学ã³ã¾ã—ãŸã€‚以上ã§æœ¬ç« ã¯çµ‚了ã§ã™ã€‚

本章ã®å†…容を修了ã—ãŸã‚‰ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ#kanjava_sbc #sbc04ã€ã‚’ã¤ã‘ã¦ãƒ„イートã—ã¦ãã ã•ã„。

次ã¯é¡”変æ›å‡¦ç†ã‚’MessageListenerã§è¡Œã†ã‚ˆã†ã«ã—ã¾ã™ã€‚

ãªãŠã€æœ¬ç« ãŒçµ‚ã‚ã£ãŸã‚‰ handleHelloMessageを削除(ã¾ãŸã¯ã‚³ãƒ¡ãƒ³ãƒˆã‚¢ã‚¦ãƒˆ)ã—ã¦ãŠã„ã¦ãã ã•ã„。

JMSã§ç”»åƒå¤‰æ›ã‚’éžåŒæœŸå‡¦ç†Â¶

å‰ç« ã§å­¦ã‚“ã JMSを使ã£ã¦ã€ç”»åƒãƒ‡ãƒ¼ã‚¿ã‚’メッセージã«è©°ã‚ã¦é€ä¿¡ã—ã€ç”»åƒå¤‰æ›å‡¦ç†ã‚’MessageListenerã§éžåŒæœŸå‡¦ç†ã™ã‚‹ã‚ˆã†ã«ã—ã¾ã—ょã†ã€‚

ã¾ãšã¯ç”»åƒå‡¦ç†ç”¨ã®ã‚­ãƒ¥ãƒ¼ã‚’追加ã™ã‚‹ãŸã‚ã«application.ymlを以下ã®ã‚ˆã†ã«ä¿®æ­£ã—ã¾ã™ã€‚

spring:
  thymeleaf.cache: false
  main.show-banner: false
  hornetq:
    mode: embedded
    embedded:
      enabled: true
      queues: hello,faceConverter # 宛先å

次ã«é€ä¿¡å‡¦ç†ã‚’作æˆã—ã¾ã—ょã†ã€‚

JmsMessagingTemplateã¯ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã§ã¯

  • String
  • byte[]
  • Map
  • Serializable

ã®å¤‰æ›ã«å¯¾å¿œã—ã¦ã„ã¾ã™ã€‚今回ã¯éžåŠ¹çŽ‡çš„ã§ã™ãŒã€é€ã‚‰ã‚ŒãŸjavax.servlet.http.Partã‹ã‚‰å–å¾—ã—ãŸç”»åƒãƒ‡ãƒ¼ã‚¿ã‚’byteé…列ã«å¤‰æ›ã—〠MessageListenerå´ã§byteé…列ã‹ã‚‰BufferedImageã«å¤‰æ›ã—ã€OpenCVã«æ¸¡ã—ã¾ã™ã€‚

ã¾ãšã¯Controllerã«ãƒªã‚¯ã‚¨ã‚¹ãƒˆå—付処ç†ã‚’追加ã—ã¾ã—ょã†ã€‚

package kanjava;

// ...
import org.springframework.util.StreamUtils;
// ...

@SpringBootApplication
@RestController
public class App {
    // ...

    @RequestMapping(value = "/queue", method = RequestMethod.POST)
    String queue(@RequestParam Part file) throws IOException {
        byte[] src = StreamUtils.copyToByteArray(file.getInputStream()); // InputStream -> byte[]
        Message<byte[]> message = MessageBuilder.withPayload(src).build(); // byte[]ã‚’æŒã¤Messageを作æˆ
        jmsMessagingTemplate.send("faceConverter", message); // convertAndSend("faceConverter", src)ã§ã‚‚å¯
        return "OK";
    }

    // ...
}

次ã«ã€å®›å…ˆfaceConverterã«å¯¾ã™ã‚‹MessageListenerを追加ã—ã¾ã—ょã†ã€‚

@SpringBootApplication
@RestController
public class App {
    // ...

    @RequestMapping(value = "/queue", method = RequestMethod.POST)
    String queue(@RequestParam Part file) throws IOException {
        byte[] src = StreamUtils.copyToByteArray(file.getInputStream());
        Message<byte[]> message = MessageBuilder.withPayload(src).build();
        jmsMessagingTemplate.send("faceConverter", message);
        return "OK";
    }

    // ...

    @JmsListener(destination = "faceConverter", concurrency = "1-5")
    void convertFace(Message<byte[]> message) throws IOException {
        log.info("received! {}", message);
        try (InputStream stream = new ByteArrayInputStream(message.getPayload())) { // byte[] -> InputStream
            Mat source = Mat.createFrom(ImageIO.read(stream)); // InputStream -> BufferedImage -> Mat
            faceDetector.detectFaces(source, FaceTranslator::duker);
            BufferedImage image = source.getBufferedImage();
            // do nothing...
        }
    }
}

ã“ã“ã¾ã§ã®å†…容を組ã¿åˆã‚ã›ã‚Œã°ã€å†…容をç†è§£ã§ãã‚‹ã¨æ€ã„ã¾ã™ã€‚

$ curl -F 'file=@hoge.jpg' localhost:8080/queue
OK

サーãƒãƒ¼ãƒ­ã‚°ã¯ä»¥ä¸‹ã®ã‚ˆã†ã«ãªã‚Šã¾ã™ã€‚

2015-03-01 00:19:22.366  INFO 14014 --- [enerContainer-1] kanjava.App                              : received! GenericMessage [payload=byte[52075], headers={jms_redelivered=false, jms_deliveryMode=2, JMSXDeliveryCount=1, jms_destination=HornetQQueue[faceConverter], jms_priority=4, id=ba27919f-8758-58fc-9976-99262605295c, jms_timestamp=1425136762365, jms_expiration=0, jms_messageId=ID:2c46ab2c-bf5d-11e4-850e-eff6d41dec3e, timestamp=1425136762366}]
2015-03-01 00:19:22.512  INFO 14014 --- [enerContainer-1] kanjava.FaceDetector                     : 1 faces are detected!

ã“ã®å‡¦ç†ã§ã¯çµæžœãŒã‚ã‹ã‚Šã¾ã›ã‚“ã­ã€‚

次ã«50リクエストをåŒæ™‚ã«é€ã£ã¦ã¿ã¾ã—ょã†ã€‚

$ for i in `seq 1 50`;do curl -F 'file=@hoge.jpg' localhost:8080/queue; done
OKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOKOK

å…¨ã¦ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã¯è¿”ã£ã¦ãã¦ã„ã¾ã™ã€‚サーãƒãƒ¼ãƒ­ã‚°ã¯ã©ã†ã§ã—ょã†ã‹ã€‚

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  [thread 23815 also had an error]
#
# JRE version: Java(TM) SE Runtime Environment (8.0_20-b26) (build 1.8.0_20-b26)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.20-b23 mixed mode bsd-amd64 compressed oops)
# Problematic frame:
# C  [libopencv_objdetect.2.4.dylib+0xe307]  cv::HaarEvaluator::operator()(int) const+0x23
#
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /xxxx/hs_err_pid14014.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.sun.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#

JVMãŒãƒãƒ³ã‚°ã—ã¦ã„ã¾ã™ãƒ»ãƒ»ãƒ»ã€‚

実ã¯ã€é¡”変æ›ã‚µãƒ¼ãƒ“スã®ä½œæˆã®æ®µéšŽã§ãƒã‚°ãŒã‚ã‚Šã¾ã—ãŸã€‚複数リクエストをåŒæ™‚ã«æŒãéš›ã«èµ·ãã¦ã„ã‚‹ãƒã‚°ãªã®ã§ã€ スレッドアンセーフã«ã‚ˆã‚‹ãƒã‚°ã§ã™ã­ã€‚ã©ã“ã§ã—ょã†ã‹ã€‚

JVMãŒè½ã¡ã¦ã„ã‚‹ã“ã¨ã¨ã€cv::HaarEvaluator::operator()(int)ãŒãƒ’ントã§ã™ã€‚OpenCVã®é¡”検出部分ãŒæ€ªã—ã„ã§ã™ã€‚

以下ã®ãƒã‚¤ãƒ©ã‚¤ãƒˆéƒ¨åˆ†ãŒã‚¹ãƒ¬ãƒƒãƒ‰ã‚¢ãƒ³ã‚»ãƒ¼ãƒ•ã§ã™ã€‚

@JmsListener(destination = "faceConverter", concurrency = "1-5")
void convertFace(Message<byte[]> message) throws IOException {
    log.info("received! {}", message);
    try (InputStream stream = new ByteArrayInputStream(message.getPayload())) {
        Mat source = Mat.createFrom(ImageIO.read(stream));
        faceDetector.detectFaces(source, FaceTranslator::duker); // ã“ã®ä¸­ã®å‡¦ç†ãŒã‚¹ãƒ¬ãƒƒãƒ‰ã‚¢ãƒ³ã‚»ãƒ¼ãƒ•!
        BufferedImage image = source.getBufferedImage();
        // do nothing...
    }
}

正確ã«ã¯classifier.detectMultiScale(source, faceDetections);ã®éƒ¨åˆ†ã§ã™ã€‚

classifierãŒã‚¹ãƒ†ãƒ¼ãƒˆãƒ•ãƒ«ãªãŸã‚ã€FaceDetectorをデフォルトã®singletonスコープã§ç™»éŒ²ã—ã¦ã„ã‚‹ã®ãŒå•é¡Œãªã‚ˆã†ã§ã™ã€‚

都度インスタンスを作り直ã™ã€prototypeスコープã«å¤‰æ›´ã—ã¾ã—ょã†ã€‚

以下ã®ã‚ˆã†ã«ã€ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆã‚¹ã‚­ãƒ£ãƒ³å¯¾è±¡ã®ã‚¯ãƒ©ã‚¹ã«@Scopeアノテーションをã¤ã‘ã¦ã‚¹ã‚³ãƒ¼ãƒ—を明示ã—ã¾ã™ã€‚

@Component
@Scope(value = "prototype")
class FaceDetector {
    // ...
}

実ã¯ã“ã‚Œã ã‘ã§ã¯ã€æœŸå¾…通りã«ã¯å‹•ãã¾ã›ã‚“。Springã§ã¯ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ãƒ©ã‚¤ãƒ•ã‚µã‚¤ã‚¯ãƒ«ã¯å¯¿å‘½ã®é•·ã„æ–¹ã«åˆã‚ã›ã‚‰ã‚Œã¾ã™ã€‚

ã™ãªã‚ã¡singletonスコープã®Appコントローラーã«å¯¾ã—ã¦ã€prototypeスコープã®FaceDetectorをインジェクションã—ã¦ã‚‚〠faceDetectorフィールドã¯å¯¿å‘½ã®é•·ã„singletonスコープã¨ã—ã¦æŒ¯ã‚‹èˆžã„ã¾ã™ã€‚

ã“ã®é–¢ä¿‚を変ãˆã‚‹(faceDetectorフィールドをprototypeスコープã¨ã—ã¦æŒ¯ã‚‹èˆžã‚ã›ã‚‹)ãŸã‚ã«ã€scoped-proxyã¨ã„ã†ä»•çµ„ã¿ã‚’å°Žå…¥ã—ã¾ã™ã€‚

@Scopeã®proxyMode属性ã«ä»¥ä¸‹ã®ã‚ˆã†ãªè¨­å®šã‚’è¡Œã„ã¾ã™ã€‚

@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
class FaceDetector {
    // ...
}

ã“ã‚Œã§FaceDetectorãŒProxyã§ãƒ©ãƒƒãƒ—ã•ã‚ŒãŸçŠ¶æ…‹ã§Appã«ã‚¤ãƒ³ã‚¸ã‚§ã‚¯ã‚·ãƒ§ãƒ³ã•ã‚Œã‚‹ãŸã‚ã€Appã®ã‚¹ã‚³ãƒ¼ãƒ—ã«ã‚ˆã‚‰ãšã€ faceDetectorフィールドã¯prototypeスコープã§ã„られã¾ã™ã€‚

ã“ã®çŠ¶æ…‹ã§Appクラスをå†èµ·å‹•ã—ã€å†åº¦50リクエストをé€ã£ã¦ã¿ã¦ãã ã•ã„。FaceDetectorãŒæ¯Žå›žåˆæœŸåŒ–ã•ã‚Œã€ç„¡äº‹å…¨ã¦ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒæŒã‹ã‚Œã¦ã„ã‚‹ã®ãŒã‚ã‹ã‚‹ã¨æ€ã„ã¾ã™ã€‚

注釈

FaceDetectorã®åˆæœŸåŒ–コストも大ãã„ã®ã§ã€singletonスコープã®ã¾ã¾synchronizedã«ã‚ˆã‚‹åŒæœŸåŒ–ã‚’è¡Œã£ã¦ã‚‚良ã„ã§ã™ã€‚ ã©ã¡ã‚‰ã®æ€§èƒ½ãŒè‰¯ã„ã‹ã¯ã€ã‚µãƒ¼ãƒãƒ¼ã‚¹ãƒšãƒƒã‚¯ã¨åŒæ™‚リクエスト数次第ã§ã™ã€‚

本章ã§ã¯ç”»åƒå‡¦ç†ã‚’éžåŒæœŸã«å®Ÿè¡Œã—ã¾ã—ãŸã€‚ã¾ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ã‚¹ã‚³ãƒ¼ãƒ—ã«ã¤ã„ã¦å­¦ã³ã¾ã—ãŸã€‚以上ã§æœ¬ç« ã¯çµ‚了ã§ã™ã€‚

本章ã®å†…容を修了ã—ãŸã‚‰ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ#kanjava_sbc #sbc05ã€ã‚’ã¤ã‘ã¦ãƒ„イートã—ã¦ãã ã•ã„。

次ã¯éžåŒæœŸã«å®Ÿè¡Œã—ãŸå‡¦ç†çµæžœã‚’通知ã™ã‚‹ãŸã‚ã«ã€STOMPã¨ã„ã†åˆ¥ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒ³ã‚°ãƒ—ロトコルを使用ã—ã¾ã™ã€‚ 次章ã§ã¯ã¾ãšã¯STOMPã‚’ã¤ã‹ã£ã¦ã¿ã¾ã—ょã†ã€‚

STOMPを使ã£ã¦ã¿ã‚‹Â¶

å‰ç« ã§MessageListenerã§éžåŒæœŸã«ç”»åƒå‡¦ç†ã‚’è¡Œã„ã¾ã—ãŸãŒã€å‡¦ç†çµæžœãŒã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã«ã¯è¿”ã£ã¦ãã¦ã„ã¾ã›ã‚“。 今度ã¯å‡¦ç†çµæžœã‚‚メッセージングã§é€ã‚‹ã‚ˆã†ã«ã—ã¾ã—ょã†ã€‚ã“ã“ã§ã¯ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒ³ã‚°ãƒ—ロトコルã¨ã—ã¦STOMPを使用ã—ã¾ã™ã€‚

STOMPã¯ã€ŒSimple (or Streaming) Text Orientated Messaging Protocolã€ã®ç•¥ã§ã€è»½é‡ãªãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒ³ã‚°ãƒ—ロトコルã§ã™ã€‚ TCPã‚„WebSocket上ã§åˆ©ç”¨ã§ãã¾ã™ã€‚

本章ã§ã¯STOMP over WebSocketã®ç°¡å˜ãªä½¿ã„方を学ã³ã¾ã—ょã†ã€‚

ã¾ãšã¯pom.xmlã«ä»¥ä¸‹ã®ä¾å­˜é–¢ä¿‚を追加ã—ã¦ãã ã•ã„。

<!-- WebSocketプログラミングã«å¿…è¦ãªè«¸ã€… -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

Appクラスã«ä»¥ä¸‹ã‚’追加ã—ã¾ã™ã€‚

@SpringBootApplication
@RestController
public class App {
    // ...

    @Configuration
    @EnableWebSocketMessageBroker // WebSocketã«é–¢ã™ã‚‹è¨­å®šã‚¯ãƒ©ã‚¹
    static class StompConfig extends AbstractWebSocketMessageBrokerConfigurer {

        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("endpoint"); // WebSocketã®ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆ
        }

        @Override
        public void configureMessageBroker(MessageBrokerRegistry registry) {
            registry.setApplicationDestinationPrefixes("/app"); // Controllerã«å‡¦ç†ã•ã›ã‚‹å®›å…ˆã®Prefix
            registry.enableSimpleBroker("/topic"); // queueã¾ãŸã¯topicを有効ã«ã™ã‚‹(両方å¯)。queueã¯1対1(P2P)ã€topicã¯1対多(Pub-Sub)
        }
    }

    // ...

    @MessageMapping(value = "/greet" /* 宛先å */) // Controller内ã®@MessageMappingアノテーションをã¤ã‘ãŸãƒ¡ã‚½ãƒƒãƒ‰ãŒã€ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ã‘付ã‘ã‚‹
    @SendTo(value = "/topic/greetings") // 処ç†çµæžœã®é€ã‚Šå…ˆ
    String greet(String name) {
        log.info("received {}", name);
        return "Hello " + name;
    }

    // ※ handleHelloMessageã¯å‰Šé™¤(ã¾ãŸã¯ã‚³ãƒ¡ãƒ³ãƒˆã‚¢ã‚¦ãƒˆ)ã—ã¦ãŠã„ã¦ãã ã•ã„。
    // ...
}

ソースã ã‘ã§ã¯åˆ†ã‹ã‚Šã«ãã„ã¨æ€ã„ã¾ã™ãŒã€ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã®ãƒ•ãƒ­ãƒ¼ã¯ä¸‹å›³ã®ã‚ˆã†ã«ãªã‚Šã¾ã™ã€‚

_images/stomp-message-flow.png

宛先ãŒ/topicã‚„/queueã§å§‹ã¾ã‚‹ã‚‚ã®ã¯ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒ–ローカー(仲介役)ãŒç›´æŽ¥ãƒãƒ³ãƒ‰ãƒªãƒ³ã‚°ã—ã¾ã™ã€‚ 宛先ãŒ/appã‹ã‚‰å§‹ã¾ã‚‹ã‚‚ã®ã¯Controllerã«æ¸¡ã£ã¦å‡¦ç†ã•ã‚Œã€ãã®å‡¦ç†çµæžœãŒãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒ–ローカーã«æ¸¡ã‚Šã¾ã™ã€‚

メッセージブローカーã«ã‚ˆã£ã¦åˆ¶å¾¡ã•ã‚ŒãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã¯ã€ãã®å®›å…ˆã‚’購読ã—ã¦ã„るクライアントã¸ã¨é€ã‚‰ã‚Œã¾ã™ã€‚

次ã«Stomp.jsを使ã£ã¦ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã‚’作りã¾ã—ょã†ã€‚src/main/resources/staticã«hello.htmlを作æˆã—ã¦ãã ã•ã„。

注釈

Spring Bootã§ã¯src/main/resources/static以下ãŒé™çš„リソース置ãå ´ã«ãªã‚Šã¾ã™ã€‚ã“ã®ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã«ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ç½®ãã¨ã€ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆãƒ‘スã‹ã‚‰ç›¸å¯¾çš„ã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã™ã€‚

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello STOMP</title>
</head>
<body>
<div>
    <button id="connect">Connect</button>
    <button id="disconnect" disabled="disabled">Disconnect</button>
</div>
<div>
    <input type="text" id="name" placeholder="Your Name">
    <button id="send" disabled="disabled">Send</button>
    <div id="response"></div>
</div>
</body>
<script src="stomp.js"></script>
<script type="text/javascript">
    /**
     * åˆæœŸåŒ–処ç†
     */
    var HelloStomp = function () {
        this.connectButton = document.getElementById('connect');
        this.disconnectButton = document.getElementById('disconnect');
        this.sendButton = document.getElementById('send');

        // イベントãƒãƒ³ãƒ‰ãƒ©ã®ç™»éŒ²
        this.connectButton.addEventListener('click', this.connect.bind(this));
        this.disconnectButton.addEventListener('click', this.disconnect.bind(this));
        this.sendButton.addEventListener('click', this.sendName.bind(this));
    };

    /**
     * エンドãƒã‚¤ãƒ³ãƒˆã¸ã®æŽ¥ç¶šå‡¦ç†
     */
    HelloStomp.prototype.connect = function () {
        var socket = new WebSocket('ws://' + location.host + '/endpoint'); // エンドãƒã‚¤ãƒ³ãƒˆã®URL
        this.stompClient = Stomp.over(socket); // WebSocketを使ã£ãŸStompクライアントを作æˆ
        this.stompClient.connect({}, this.onConnected.bind(this)); // エンドãƒã‚¤ãƒ³ãƒˆã«æŽ¥ç¶šã—ã€æŽ¥ç¶šã—ãŸéš›ã®ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯ã‚’登録
    };

    /**
     * エンドãƒã‚¤ãƒ³ãƒˆã¸æŽ¥ç¶šã—ãŸã¨ãã®å‡¦ç†
     */
    HelloStomp.prototype.onConnected = function (frame) {
        console.log('Connected: ' + frame);
        // 宛先ãŒ'/topic/greetings'ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’購読ã—ã€ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯å‡¦ç†ã‚’登録
        this.stompClient.subscribe('/topic/greetings', this.onSubscribeGreeting.bind(this));
        this.setConnected(true);
    };

    /**
     * 宛先'/topic/greetings'ãªãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã—ãŸã¨ãã®å‡¦ç†
     */
    HelloStomp.prototype.onSubscribeGreeting = function (message) {
        var response = document.getElementById('response');
        var p = document.createElement('p');
        p.appendChild(document.createTextNode(message.body));
        response.insertBefore(p, response.children[0]);
    };

    /**
     * 宛先'/app/greet'ã¸ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸é€ä¿¡å‡¦ç†
     */
    HelloStomp.prototype.sendName = function () {
        var name = document.getElementById('name').value;
        this.stompClient.send('/app/greet', {}, name); // 宛先'/app/greet'ã¸ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡
    };

    /**
     * 接続切断処ç†
     */
    HelloStomp.prototype.disconnect = function () {
        if (this.stompClient) {
            this.stompClient.disconnect();
            this.stompClient = null;
        }
        this.setConnected(false);
    };

    /**
     * ボタン表示ã®åˆ‡ã‚Šæ›¿ãˆ
     */
    HelloStomp.prototype.setConnected = function (connected) {
        this.connectButton.disabled = connected;
        this.disconnectButton.disabled = !connected;
        this.sendButton.disabled = !connected;
    };

    new HelloStomp();
</script>
</html>

Stomp.jsã‚’src/main/resources/staticã«ãƒ€ã‚¦ãƒ³ãƒ­ãƒ¼ãƒ‰ã—ã¾ã—ょã†ã€‚

$ cd src/main/resources/static
$ wget https://raw.github.com/jmesnil/stomp-websocket/master/lib/stomp.js

Appクラスを起動ã—ã€http://localhost:8080/hello.htmlã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ãã ã•ã„。

_images/hello-html-01.png

「Connectã€ãƒœã‚¿ãƒ³ã‚’押ã—ã¦ã€ãƒ•ã‚©ãƒ¼ãƒ ã«åå‰ã‚’入力ã—ã€ã€ŒSendã€ãƒœã‚¿ãƒ³ã‚’押ã—ã¦ãã ã•ã„。

_images/hello-html-02.png

çµæžœãŒè¿”ã£ã¦ãã¾ã—ãŸã€‚今回ã¯å®›å…ˆã‚’Topicã«ã—ã¦ã„ã‚‹ãŸã‚ã€ä»–ã®ã‚¿ãƒ–ã§åˆ¥é€”Connectã™ã‚Œã°å…¨ã¦ã®ã‚¿ãƒ–ã«çµæžœãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚

注釈

mvn spring-boot:runã§Appクラスを起動ã™ã‚Œã°ã€é™çš„リソースã®å¤‰æ›´ãŒå³å映ã•ã‚Œã‚‹ã®ã§é–‹ç™ºä¸­ã¯ä¾¿åˆ©ã§ã™ã€‚

STOMPã®ç°¡å˜ãªä½¿ã„方を学ã³ã¾ã—ãŸã€‚以上ã§æœ¬ç« ã¯çµ‚了ã§ã™ã€‚

本章ã®å†…容を修了ã—ãŸã‚‰ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ#kanjava_sbc #sbc06ã€ã‚’ã¤ã‘ã¦ãƒ„イートã—ã¦ãã ã•ã„。

次章ã§ã¯JMSã®MessageListenerã®å‡¦ç†çµæžœã‚’STOMPã®å®›å…ˆã«é€ã‚Šã€ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã§è¡¨ç¤ºã•ã›ã¾ã—ょã†ã€‚

STOMP over WebSocketã§éžåŒæœŸå‡¦ç†çµæžœã‚’å—ä¿¡ã™ã‚‹Â¶

本章ã§ã¯JMSã®MessageListenerã®ç”»åƒå‡¦ç†çµæžœã‚’STOMPã®å®›å…ˆã«é€ã‚Šã€ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã§è¡¨ç¤ºã•ã›ã¾ã™ã€‚

STOMPã®çµæžœã‚’é€ä¿¡ã™ã‚‹ãŸã‚ã«SimpMessagingTemplateを使用ã—ã¾ã™ã€‚ã“ã‚Œã¾ã§ä½¿ç”¨ã—ãŸJmsMessagingTemplateã¨ã»ã¼åŒã˜ã‚¤ãƒ³ã‚¿ãƒ¼ãƒ•ã‚§ãƒ¼ã‚¹ã§ã™ã€‚ ç”»åƒã¯HTMLã§è¡¨ç¤ºã—ã‚„ã™ã„よã†ã«byte[]ã«å¤‰æ›ã—ãŸå¾Œã€Base64ã«ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‰ã—ã¦é€ä¿¡ã—ã¾ã™(ã“れもéžåŠ¹çŽ‡)。

以下ã®ã‚³ãƒ¼ãƒ‰ã‚’追加ã—ã¦ãã ã•ã„。

@SpringBootApplication
@RestController
public class App {
    // ...

    @Autowired
    SimpMessagingTemplate simpMessagingTemplate;

    // ...

    @Configuration
    @EnableWebSocketMessageBroker
    static class StompConfig extends AbstractWebSocketMessageBrokerConfigurer {

        // ...

        @Override
        public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
            registration.setMessageSizeLimit(10 * 1024 * 1024); // メッセージサイズã®ä¸Šé™ã‚’10MBã«ä¸Šã’ã‚‹(デフォルトã¯64KB)
        }
    }

    // ...

    @JmsListener(destination = "faceConverter", concurrency = "1-5")
    void convertFace(Message<byte[]> message) throws IOException {
        log.info("received! {}", message);
        try (InputStream stream = new ByteArrayInputStream(message.getPayload())) {
            Mat source = Mat.createFrom(ImageIO.read(stream));
            faceDetector.detectFaces(source, FaceTranslator::duker);
            BufferedImage image = source.getBufferedImage();

            try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { // BufferedImageã‚’byte[]ã«å¤‰æ›
                ImageIO.write(image, "png", baos);
                baos.flush();
                // ç”»åƒã‚’Base64ã«ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‰ã—ã¦ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ä½œæˆã—ã€å®›å…ˆ'/topic/faces'ã¸ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸é€ä¿¡
                simpMessagingTemplate.convertAndSend("/topic/faces",
                        Base64.getEncoder().encodeToString(baos.toByteArray()));
            }
        }
    }
}

HTMLも変更ã—ã¾ã—ょã†ã€‚宛先/topic/facesã¸ã®å‡¦ç†ã‚’追加ã™ã‚‹ãŸã‚ã€å…ˆã»ã©ã®hello.htmlをコピーã—ã¦face.htmlを作æˆã—ã€ä»¥ä¸‹ã®ä¿®æ­£ã‚’加ãˆã¦ãã ã•ã„。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello STOMP</title>
</head>
<body>
<div>
    <button id="connect">Connect</button>
    <button id="disconnect" disabled="disabled">Disconnect</button>
</div>
<div>
    <input type="text" id="name" placeholder="Your Name">
    <button id="send" disabled="disabled">Send</button>
    <div id="response"></div>
</div>
</body>
<script src="stomp.js"></script>
<script type="text/javascript">
    /**
     * åˆæœŸåŒ–処ç†
     */
    var HelloStomp = function () {
        this.connectButton = document.getElementById('connect');
        this.disconnectButton = document.getElementById('disconnect');
        this.sendButton = document.getElementById('send');

        // イベントãƒãƒ³ãƒ‰ãƒ©ã®ç™»éŒ²
        this.connectButton.addEventListener('click', this.connect.bind(this));
        this.disconnectButton.addEventListener('click', this.disconnect.bind(this));
        this.sendButton.addEventListener('click', this.sendName.bind(this));
    };

    /**
     * エンドãƒã‚¤ãƒ³ãƒˆã¸ã®æŽ¥ç¶šå‡¦ç†
     */
    HelloStomp.prototype.connect = function () {
        var socket = new WebSocket('ws://' + location.host + '/endpoint'); // エンドãƒã‚¤ãƒ³ãƒˆã®URL
        this.stompClient = Stomp.over(socket); // WebSocketを使ã£ãŸStompクライアントを作æˆ
        this.stompClient.debug = null; // デãƒãƒƒã‚°ãƒ­ã‚°ã‚’出ã•ãªã„(Base64ã®æ–‡å­—列ãŒå¤§ãã™ã‚‹ãŸã‚)
        this.stompClient.connect({}, this.onConnected.bind(this)); // エンドãƒã‚¤ãƒ³ãƒˆã«æŽ¥ç¶šã—ã€æŽ¥ç¶šã—ãŸéš›ã®ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯ã‚’登録
    };

    /**
     * エンドãƒã‚¤ãƒ³ãƒˆã¸æŽ¥ç¶šã—ãŸã¨ãã®å‡¦ç†
     */
    HelloStomp.prototype.onConnected = function (frame) {
        console.log('Connected: ' + frame);
        // 宛先ãŒ'/topic/greetings'ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’購読ã—ã€ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯å‡¦ç†ã‚’登録
        this.stompClient.subscribe('/topic/greetings', this.onSubscribeGreeting.bind(this));
        // 宛先ãŒ'/topic/faces'ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’購読ã—ã€ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯å‡¦ç†ã‚’登録
        this.stompClient.subscribe('/topic/faces', this.onSubscribeFace.bind(this));
        this.setConnected(true);
    };

    /**
     * 宛先'/topic/greetings'ãªãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã—ãŸã¨ãã®å‡¦ç†
     */
    HelloStomp.prototype.onSubscribeGreeting = function (message) {
        var response = document.getElementById('response');
        var p = document.createElement('p');
        p.appendChild(document.createTextNode(message.body));
        response.insertBefore(p, response.children[0]);
    };

    /**
     * 宛先'/topic/faces'ãªãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã—ãŸã¨ãã®å‡¦ç†
     */
    HelloStomp.prototype.onSubscribeFace = function (message) {
        var response = document.getElementById('response');
        var img = document.createElement('img');
        img.setAttribute("src", "data:image/png;base64," + message.body);
        response.insertBefore(img, response.children[0]);
    };

    /**
     * 宛先'/app/greet'ã¸ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸é€ä¿¡å‡¦ç†
     */
    HelloStomp.prototype.sendName = function () {
        var name = document.getElementById('name').value;
        this.stompClient.send('/app/greet', {}, name); // 宛先'/app/greet'ã¸ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡
    };

    /**
     * 接続切断処ç†
     */
    HelloStomp.prototype.disconnect = function () {
        if (this.stompClient) {
            this.stompClient.disconnect();
            this.stompClient = null;
        }
        this.setConnected(false);
    };

    /**
     * ボタン表示ã®åˆ‡ã‚Šæ›¿ãˆ
     */
    HelloStomp.prototype.setConnected = function (connected) {
        this.connectButton.disabled = connected;
        this.disconnectButton.disabled = !connected;
        this.sendButton.disabled = !connected;
    };

    new HelloStomp();
</script>
</html>

Appクラスをå†èµ·å‹•ã—ã€http://localhost:8080/face.htmlã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã€ã€ŒConnectã€ãƒœã‚¿ãƒ³ã‚’押ã—ã¦ãã ã•ã„。 ãã®å¾Œã€JMSã§ç”»åƒå¤‰æ›ã‚’éžåŒæœŸå‡¦ç†ã§ä½œæˆã—ãŸã‚µãƒ¼ãƒ“スã«å¤‰æ›ã—ãŸã„ç”»åƒã‚’é€ä¿¡ã—ã¾ã™ã€‚

$ curl -F 'file=@lena.png' localhost:8080/queue
OK

é€ä¿¡ã—ãŸå¾Œã€ãƒ–ラウザを確èªã™ã‚‹ã¨ä»¥ä¸‹ã®ã‚ˆã†ã«å¤‰æ›å¾Œã®ç”»åƒãŒè¡¨ç¤ºã•ã‚Œã‚‹ã¯ãšã§ã™ã€‚

_images/face-html-01.png

ç”»åƒã‚µã‚¤ã‚ºãŒå°‘ã—大ããã€è»¢é€é‡ãŒè‚¥å¤§åŒ–ã—ã¦ã—ã¾ã†ãŸã‚ã€ã‚µãƒ¼ãƒãƒ¼ã‚µã‚¤ãƒ‰ã§ãƒªã‚µã‚¤ã‚ºã™ã‚‹ã‚ˆã†ã«ã—ã¾ã—ょã†ã€‚Appクラスを以下ã®ã‚ˆã†ã«å¤‰æ›´ã—ã¦ãã ã•ã„。

// ...
import static org.bytedeco.javacpp.opencv_imgproc.*;

@SpringBootApplication
@RestController
public class App {
    // ...

    @Value("${faceduker.width:200}")
    int resizedWidth; // リサイズ後ã®å¹…

    // ...

    @JmsListener(destination = "faceConverter", concurrency = "1-5")
    void convertFace(Message<byte[]> message) throws IOException {
        log.info("received! {}", message);
        try (InputStream stream = new ByteArrayInputStream(message.getPayload())) {
            Mat source = Mat.createFrom(ImageIO.read(stream));
            faceDetector.detectFaces(source, FaceTranslator::duker);

            // リサイズ
            double ratio = ((double) resizedWidth) / source.cols();
            int height = (int) (ratio * source.rows());
            Mat out = new Mat(height, resizedWidth, source.type());
            resize(source, out, new Size(), ratio, ratio, INTER_LINEAR);

            BufferedImage image = out.getBufferedImage();

            try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                ImageIO.write(image, "png", baos);
                baos.flush();
                // ç”»åƒã‚’Base64ã«ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‰ã—ã¦ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ä½œæˆã—ã€å®›å…ˆ'/topic/faces'ã¸ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸é€ä¿¡
                simpMessagingTemplate.convertAndSend("/topic/faces",
                        Base64.getEncoder().encodeToString(baos.toByteArray()));
            }
        }
    }
}

Appクラスをå†èµ·å‹•ã—ã¦ã€ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã‚’å†æŽ¥ç¶šã—ã¦ãã ã•ã„。ãã—ã¦ç”»åƒå‡¦ç†ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’é€ã‚Šã€ãƒ–ラウザã«ä»¥ä¸‹ã®ã‚ˆã†ã«è¡¨ç¤ºã•ã‚Œã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。

_images/face-html-02.png

以上ã§æœ¬ç« ã¯çµ‚了ã§ã™ã€‚

本章ã®å†…容を修了ã—ãŸã‚‰ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ#kanjava_sbc #sbc07ã€ã‚’ã¤ã‘ã¦ãƒ„イートã—ã¦ãã ã•ã„。

次ã¯ã‚«ãƒ¡ãƒ©ã‚’ã¤ã‹ã£ã¦é¡”ç”»åƒã‚’撮りã€STOMPã§æ’®ã£ãŸç”»åƒã‚’é€ä¿¡ã—ã€ãã®çµæžœã‚’今回åŒæ§˜ã«è¡¨ç¤ºã—ã¾ã—ょã†ã€‚次章ã§ã¯ã¾ãšã¯WebRTCã«ã‚ˆã‚‹ã‚«ãƒ¡ãƒ©ã‚’使ã£ã¦ã¿ã¾ã—ょã†ã€‚

WebRTCを使ã£ã¦ã¿ã‚‹Â¶

本章ã§ã¯ã¾ãšã¯WebRTCã®getUserMedia APIカメラã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ã¿ã¾ã—ょã†ã€‚

WebRTC(Web Real-Time Communication)ã§ã¯ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ ã‚³ãƒŸãƒ¥ãƒ‹ã‚±ãƒ¼ã‚·ãƒ§ãƒ³ç”¨ã®APIãŒç”¨æ„ã•ã‚Œã¦ã„ã¾ã™ãŒã€æœ¬ãƒãƒ³ã‚ºã‚ªãƒ³ã§ã¯getUserMedia APIã®ã¿ä½¿ç”¨ã—ã¾ã™ã€‚

src/main/resources/static/camera.htmlを作æˆã—ã¦ã€ä»¥ä¸‹ã®å†…容を記述ã—ã¦ãã ã•ã„。

<!doctype html>
<html>
<head>
    <title>Cameraテスト</title>
</head>
<body>
<video autoplay width="400" height="300"></video>
<img src="" width="400" height="300">
<canvas style="display:none;" width="400" height="300"></canvas>

<script type="text/javascript">
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia || navigator.msGetUserMedia;
    window.URL = window.URL || window.webkitURL;

    var video = document.querySelector('video');
    var canvas = document.querySelector('canvas');
    var ctx = canvas.getContext('2d');
    var localMediaStream;

    navigator.getUserMedia({video: true, audio: false},
            function (stream) {
                video.src = window.URL.createObjectURL(stream);
                localMediaStream = stream;
            },
            function (error) {
                alert(JSON.stringify(error));
            }
    );

    function takeSnapshot() {
        if (localMediaStream) {
            ctx.drawImage(video, 0, 0, 400, 300);
            document.querySelector('img').src = canvas.toDataURL('image/webp');
        }
    }
    video.addEventListener('click', takeSnapshot, false);
</script>
</body>
</html>

http://localhost:8080/camera.htmlã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ãã ã•ã„。

_images/camera-html-01.png

カメラアクセスã¸ã®è¨±å¯ã‚’確èªã•ã‚Œã¾ã™ã®ã§ã€ã€Œè¨±å¯ã€ã‚’クリックã—ã¦ãã ã•ã„。ãã†ã™ã‚‹ã¨ã‚«ãƒ¡ãƒ©ã®çµæžœãŒå·¦å´ã«è¡¨ç¤ºã•ã‚Œã¾ã™ã€‚

å·¦ã®ã‚«ãƒ¡ãƒ©ç”»åƒã‚’クリックã™ã‚‹ã¨ã€å³å´ã«ã‚¹ãƒŠãƒƒãƒ—ショットã—ã¦è¡¨ç¤ºã•ã‚Œã¾ã™ã€‚

_images/camera-html-02.png

以上ã§æœ¬ç« ã¯çµ‚了ã§ã™ã€‚

本章ã®å†…容を修了ã—ãŸã‚‰ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ#kanjava_sbc #sbc08ã€ã‚’ã¤ã‘ã¦ãƒ„イートã—ã¦ãã ã•ã„。

次章ã§ã¯ã„よã„よカメラ画åƒã‚’サーãƒãƒ¼ã«é€ä¿¡ã—ã€æ’®ã£ãŸç”»åƒãŒå¤‰æ›ã•ã‚Œã¦è¡¨ç¤ºã™ã‚‹ã‚ˆã†ã«ã—ã¾ã™ã€‚

WebRTCã§æ’®ã£ãŸå†™çœŸã‚’顔変æ›ã‚µãƒ¼ãƒ“スã«é€ä¿¡Â¶

ã“ã‚Œã¾ã§å­¦ã‚“ã§ããŸã“ã¨ã‚’çµ±åˆã—ã¾ã—ょã†ã€‚

ã¾ãšã¯face.htmlã«camera.htmlã®å†…容を追加ã—ã¾ã™ã€‚カメラã®ã‚¹ãƒŠãƒƒãƒ—ショットを撮ã£ãŸã‚‰ã€ ãã®ç”»åƒã‚’宛先app/faceConverterã¸é€ã‚Šã¾ã™(サーãƒãƒ¼ã‚µã‚¤ãƒ‰ã®å‡¦ç†ã¯å¾Œã»ã©è¿½åŠ ã—ã¾ã™)。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello STOMP</title>
</head>
<body>
<div>
    <button id="connect">Connect</button>
    <button id="disconnect" disabled="disabled">Disconnect</button>
</div>
<div>
    <video autoplay width="400" height="300"></video>
    <img id="snapshot" src="" width="400" height="300">
    <canvas style="display:none;" width="400" height="300"></canvas>
    <br>

    <div id="response"></div>
</div>
</body>
<script src="stomp.js"></script>
<script type="text/javascript">
    // camera
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia || navigator.msGetUserMedia;
    window.URL = window.URL || window.webkitURL;

    /**
     * åˆæœŸåŒ–処ç†
     */
    var HelloStomp = function () {
        this.connectButton = document.getElementById('connect');
        this.disconnectButton = document.getElementById('disconnect');
        // this.sendButton = document.getElementById('send'); ã“ã®è¡Œã¯å‰Šé™¤
        this.video = document.querySelector('video');
        this.canvas = document.querySelector('canvas');
        this.canvasContext = this.canvas.getContext('2d');
        this.snapshot = document.getElementById('snapshot');
        this.canvasContext.globalAlpha = 1.0;

        // イベントãƒãƒ³ãƒ‰ãƒ©ã®ç™»éŒ²
        this.connectButton.addEventListener('click', this.connect.bind(this));
        this.disconnectButton.addEventListener('click', this.disconnect.bind(this));
        // this.sendButton.addEventListener('click', this.sendName.bind(this)); ã“ã®è¡Œã¯å‰Šé™¤
        this.video.addEventListener('click', this.takeSnapshot.bind(this));

        // getUserMedia API
        navigator.getUserMedia({video: true, audio: false},
                function (stream) {
                    this.video.src = window.URL.createObjectURL(stream);
                    this.localMediaStream = stream;
                }.bind(this),
                function (error) {
                    alert(JSON.stringify(error));
                }
        );
    };

    /**
     * エンドãƒã‚¤ãƒ³ãƒˆã¸ã®æŽ¥ç¶šå‡¦ç†
     */
    HelloStomp.prototype.connect = function () {
        var socket = new WebSocket('ws://' + location.host + '/endpoint'); // エンドãƒã‚¤ãƒ³ãƒˆã®URL
        this.stompClient = Stomp.over(socket); // WebSocketを使ã£ãŸStompクライアントを作æˆ
        this.stompClient.debug = null; // デãƒãƒƒã‚°ãƒ­ã‚°ã‚’出ã•ãªã„
        this.stompClient.connect({}, this.onConnected.bind(this)); // エンドãƒã‚¤ãƒ³ãƒˆã«æŽ¥ç¶šã—ã€æŽ¥ç¶šã—ãŸéš›ã®ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯ã‚’登録
    };

    /**
     * エンドãƒã‚¤ãƒ³ãƒˆã¸æŽ¥ç¶šã—ãŸã¨ãã®å‡¦ç†
     */
    HelloStomp.prototype.onConnected = function (frame) {
        console.log('Connected: ' + frame);
        // 宛先ãŒ'/topic/greetings'ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’購読ã—ã€ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯å‡¦ç†ã‚’登録
        this.stompClient.subscribe('/topic/greetings', this.onSubscribeGreeting.bind(this));
        // 宛先ãŒ'/topic/faces'ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’購読ã—ã€ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯å‡¦ç†ã‚’登録
        this.stompClient.subscribe('/topic/faces', this.onSubscribeFace.bind(this));
        this.setConnected(true);
    };

    /**
     * 宛先'/topic/greetings'ãªãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã—ãŸã¨ãã®å‡¦ç†
     */
    HelloStomp.prototype.onSubscribeGreeting = function (message) {
        var response = document.getElementById('response');
        var p = document.createElement('p');
        p.appendChild(document.createTextNode(message.body));
        response.insertBefore(p, response.children[0]);
    };

    /**
     * 宛先'/topic/faces'ãªãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã—ãŸã¨ãã®å‡¦ç†
     */
    HelloStomp.prototype.onSubscribeFace = function (message) {
        var response = document.getElementById('response');
        var img = document.createElement('img');
        img.setAttribute("src", "data:image/png;base64," + message.body); // Base64エンコードã•ã‚ŒãŸç”»åƒã‚’ãã®ã¾ã¾è¡¨ç¤ºã™ã‚‹
        response.insertBefore(img, response.children[0]);
    };

    /**
     * 宛先'/app/greet'ã¸ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸é€ä¿¡å‡¦ç†
     */
    HelloStomp.prototype.sendName = function () {
        var name = document.getElementById('name').value;
        this.stompClient.send('/app/greet', {}, name); // 宛先'/app/greet'ã¸ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡
    };

    /**
     * 接続切断処ç†
     */
    HelloStomp.prototype.disconnect = function () {
        if (this.stompClient) {
            this.stompClient.disconnect();
            this.stompClient = null;
        }
        this.setConnected(false);
    };

    /**
     * ボタン表示ã®åˆ‡ã‚Šæ›¿ãˆ
     */
    HelloStomp.prototype.setConnected = function (connected) {
        this.connectButton.disabled = connected;
        this.disconnectButton.disabled = !connected;
        // this.sendButton.disabled = !connected; ã“ã®è¡Œã¯å‰Šé™¤
    };

    /**
     * カメラã®ã‚¹ãƒŠãƒƒãƒ—ショットをå–å¾—
     */
    HelloStomp.prototype.takeSnapshot = function () {
        this.canvasContext.drawImage(this.video, 0, 0, 400, 300);
        var dataUrl = this.canvas.toDataURL('image/jpeg');
        this.snapshot.src = dataUrl;
        this.sendFace(dataUrl);
    };

    /**
     * 顔画åƒã®é€ä¿¡
     */
    HelloStomp.prototype.sendFace = function (dataUrl) {
        if (this.stompClient) {
            this.stompClient.send("/app/faceConverter", {}, dataUrl.replace(/^.*,/, '')); // 宛先'/app/faceConverter'ã¸ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡
        } else {
            alert('not connected!');
        }
    };

    new HelloStomp();
</script>
</html>

サーãƒãƒ¼ã‚µã‚¤ãƒ‰ã«ã€å®›å…ˆapp/faceConverterã«å¯¾ã™ã‚‹å‡¦ç†ã‚’追加ã—ã¾ã—ょã†ã€‚

Base64ã§ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‰ã•ã‚ŒãŸç”»åƒã‚’byte[]ã«ãƒ‡ã‚³ãƒ¼ãƒ‰ã—ã¦ã€JMSã®MessageListenerã¸é€ä¿¡ã™ã‚‹ã ã‘ã§ã™ã€‚

@SpringBootApplication
@RestController
public class App {
    // ...

    @MessageMapping(value = "/faceConverter")
    void faceConverter(String base64Image) {
        Message<byte[]> message = MessageBuilder.withPayload(Base64.getDecoder().decode(base64Image)).build();
        jmsMessagingTemplate.send("faceConverter", message);
    }

    // ...
}

Appクラスをå†èµ·å‹•ã—ã€http://localhost:8080/face.htmlã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ãã ã•ã„。

「Connectã€ãƒœã‚¿ãƒ³ã‚’押ã—ã¦æŽ¥ç¶šã—ãŸã‚‰ã€ã‚«ãƒ¡ãƒ©ã®ç”»åƒã‚’クリックã—ã¦ãã ã•ã„。スナップショットãŒä¿å­˜ã•ã‚Œã€ã‚µãƒ¼ãƒãƒ¼ã¸é€ä¿¡ã•ã‚Œã¾ã™ã€‚ ã—ã°ã‚‰ãã™ã‚‹ã¨å‡¦ç†çµæžœã‚’å—ä¿¡ã—ã€è¡¨ç¤ºã—ã¾ã™ã€‚連続ã—ã¦ã‚¯ãƒªãƒƒã‚¯ã—ã¦ã‚‚スムーズã«å‡¦ç†ã•ã‚Œã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。

最後ã«ã€ã‚«ãƒ¡ãƒ©ç”»åƒã ã‘ã§ãªãã€ãƒ­ãƒ¼ã‚«ãƒ«ãƒ•ã‚¡ã‚¤ãƒ«ã‚‚é€ä¿¡ã§ãるよã†ã«æ©Ÿèƒ½è¿½åŠ ã—ã¾ã—ょã†ã€‚face.htmlを以下ã®ã‚ˆã†ã«ä¿®æ­£ã—ã¦ãã ã•ã„。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello STOMP</title>
</head>
<body>
<div>
    <button id="connect">Connect</button>
    <button id="disconnect" disabled="disabled">Disconnect</button>
</div>
<div>
    <video autoplay width="400" height="300"></video>
    <img id="snapshot" src="" width="400" height="300">
    <canvas style="display:none;" width="400" height="300"></canvas>
    <br>
    <input id="files" type="file" disabled="disabled" multiple>

    <div id="response"></div>
</div>
</body>
<script src="stomp.js"></script>
<script type="text/javascript">
    // camera
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia || navigator.msGetUserMedia;
    window.URL = window.URL || window.webkitURL;

    /**
     * åˆæœŸåŒ–処ç†
     */
    var HelloStomp = function () {
        this.connectButton = document.getElementById('connect');
        this.disconnectButton = document.getElementById('disconnect');
        this.files = document.getElementById('files');
        this.video = document.querySelector('video');
        this.canvas = document.querySelector('canvas');
        this.canvasContext = this.canvas.getContext('2d');
        this.snapshot = document.getElementById('snapshot');
        this.canvasContext.globalAlpha = 1.0;

        // イベントãƒãƒ³ãƒ‰ãƒ©ã®ç™»éŒ²
        this.connectButton.addEventListener('click', this.connect.bind(this));
        this.disconnectButton.addEventListener('click', this.disconnect.bind(this));
        this.video.addEventListener('click', this.takeSnapshot.bind(this));
        this.files.addEventListener('change', this.sendFiles.bind(this));

        // getUserMedia API
        navigator.getUserMedia({video: true, audio: false},
                function (stream) {
                    this.video.src = window.URL.createObjectURL(stream);
                    this.localMediaStream = stream;
                }.bind(this),
                function (error) {
                    alert(JSON.stringify(error));
                }
        );
    };

    /**
     * エンドãƒã‚¤ãƒ³ãƒˆã¸ã®æŽ¥ç¶šå‡¦ç†
     */
    HelloStomp.prototype.connect = function () {
        var socket = new WebSocket('ws://' + location.host + '/endpoint'); // エンドãƒã‚¤ãƒ³ãƒˆã®URL
        this.stompClient = Stomp.over(socket); // WebSocketを使ã£ãŸStompクライアントを作æˆ
        this.stompClient.debug = null; // デãƒãƒƒã‚°ãƒ­ã‚°ã‚’出ã•ãªã„
        this.stompClient.connect({}, this.onConnected.bind(this)); // エンドãƒã‚¤ãƒ³ãƒˆã«æŽ¥ç¶šã—ã€æŽ¥ç¶šã—ãŸéš›ã®ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯ã‚’登録
    };

    /**
     * エンドãƒã‚¤ãƒ³ãƒˆã¸æŽ¥ç¶šã—ãŸã¨ãã®å‡¦ç†
     */
    HelloStomp.prototype.onConnected = function (frame) {
        console.log('Connected: ' + frame);
        // 宛先ãŒ'/topic/greetings'ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’購読ã—ã€ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯å‡¦ç†ã‚’登録
        this.stompClient.subscribe('/topic/greetings', this.onSubscribeGreeting.bind(this));
        // 宛先ãŒ'/topic/faces'ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’購読ã—ã€ã‚³ãƒ¼ãƒ«ãƒãƒƒã‚¯å‡¦ç†ã‚’登録
        this.stompClient.subscribe('/topic/faces', this.onSubscribeFace.bind(this));
        this.setConnected(true);
    };

    /**
     * 宛先'/topic/greetings'ãªãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã—ãŸã¨ãã®å‡¦ç†
     */
    HelloStomp.prototype.onSubscribeGreeting = function (message) {
        var response = document.getElementById('response');
        var p = document.createElement('p');
        p.appendChild(document.createTextNode(message.body));
        response.insertBefore(p, response.children[0]);
    };

    /**
     * 宛先'/topic/faces'ãªãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã—ãŸã¨ãã®å‡¦ç†
     */
    HelloStomp.prototype.onSubscribeFace = function (message) {
        var response = document.getElementById('response');
        var img = document.createElement('img');
        img.setAttribute("src", "data:image/png;base64," + message.body); // Base64エンコードã•ã‚ŒãŸç”»åƒã‚’ãã®ã¾ã¾è¡¨ç¤ºã™ã‚‹
        response.insertBefore(img, response.children[0]);
    };

    /**
     * 宛先'/app/greet'ã¸ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸é€ä¿¡å‡¦ç†
     */
    HelloStomp.prototype.sendName = function () {
        var name = document.getElementById('name').value;
        this.stompClient.send('/app/greet', {}, name); // 宛先'/app/greet'ã¸ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡
    };

    /**
     * 接続切断処ç†
     */
    HelloStomp.prototype.disconnect = function () {
        if (this.stompClient) {
            this.stompClient.disconnect();
            this.stompClient = null;
        }
        this.setConnected(false);
    };

    /**
     * ボタン表示ã®åˆ‡ã‚Šæ›¿ãˆ
     */
    HelloStomp.prototype.setConnected = function (connected) {
        this.connectButton.disabled = connected;
        this.disconnectButton.disabled = !connected;
        this.files.disabled = !connected;
    };

    /**
     * カメラã®ã‚¹ãƒŠãƒƒãƒ—ショットをå–å¾—
     */
    HelloStomp.prototype.takeSnapshot = function () {
        this.canvasContext.drawImage(this.video, 0, 0, 400, 300);
        var dataUrl = this.canvas.toDataURL('image/jpeg');
        this.snapshot.src = dataUrl;
        this.sendFace(dataUrl);
    };

    /**
     * 顔画åƒã®é€ä¿¡
     */
    HelloStomp.prototype.sendFace = function (dataUrl) {
        if (this.stompClient) {
            this.stompClient.send("/app/faceConverter", {}, dataUrl.replace(/^.*,/, ''));
        } else {
            alert('not connected!');
        }
    };

    /**
     * é¸æŠžã—ãŸç”»åƒãƒ•ã‚¡ã‚¤ãƒ«ã‚’é€ä¿¡
     */
    HelloStomp.prototype.sendFiles = function (event) {
        var input = event.target;
        for (var i = 0; i < input.files.length; i++) {
            var file = input.files[i];
            var reader = new FileReader();
            reader.onload = function (event) {
                var dataUrl = event.target.result;
                this.sendFace(dataUrl);
            }.bind(this);
            reader.readAsDataURL(file);
        }
    };

    new HelloStomp();
</script>
</html>

å†åº¦ã€http://localhost:8080/face.htmlã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ãƒ•ã‚¡ã‚¤ãƒ«ã‚¢ãƒƒãƒ—ロードã—ã¦ã¿ã¦ãã ã•ã„。複数ファイルを一度ã«é€ä¿¡ã§ãã¾ã™ã€‚

以上ã§æœ¬ç« ã¯çµ‚了ã§ã™ã€‚

本章ã®å†…容を修了ã—ãŸã‚‰ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ#kanjava_sbc #sbc09ã€ã‚’ã¤ã‘ã¦ãƒ„イートã—ã¦ãã ã•ã„。

Dockerを使ã£ã¦ã¿ã‚‹Â¶

ã„ã¤ã‹æ›¸ã。基本的ã«ã¯ã“ã¡ã‚‰ã®å†…容。

「[事å‰æº–å‚™] Spring Bootã§Hello Worldã€ã®å†…容ã«æˆ»ã£ã¦ã€AWS Elastic Beanstalkã¸ãƒ‡ãƒ—ロイã—ã¦ã¿ã¦ãã ã•ã„。

本章ã®å†…容を修了ã—ãŸã‚‰ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ#kanjava_sbc #sbc10ã€ã‚’ã¤ã‘ã¦ãƒ„イートã—ã¦ãã ã•ã„。

顔変æ›ã‚µãƒ¼ãƒ“スをDocker化¶

src/main/docker/Dockerfile.txtã‚’ã¿ã¦ãã ã•ã„。

FROM dockerfile/java:oracle-java8

ADD kusokora.jar /opt/kusokora/
EXPOSE 8080
WORKDIR /opt/kusokora/
CMD ["java", "-Xms512m", "-Xmx1g", "-jar", "kusokora.jar"]

実行å¯èƒ½jarをコピーã—ã¦å®Ÿè¡Œã—ã¦ã„ã‚‹ã ã‘ã§ã™ã€‚通常ã¯ã“ã‚Œã§OKãªã®ã§ã™ãŒã€ä»Šå›žã¯OpenCVを使ã†ãŸã‚ã€OpenCV用ã®æº–å‚™ãŒå¿…è¦ã§ã™ã€‚

src/main/docker/Dockerfile.txt以下ã®ã‚ˆã†ã«æ›¸ãæ›ãˆã¦ãã ã•ã„。

FROM centos:centos7
RUN yum -y update && yum clean all
RUN yum -y install wget glibc gtk2 gstreamer gstreamer-plugins-base libv4l
RUN wget -c -O /tmp/jdk-8u31-linux-x64.rpm --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u31-b13/jdk-8u31-linux-x64.rpm
RUN yum -y localinstall /tmp/jdk-8u31-linux-x64.rpm
RUN rm -f /tmp/jdk-8u31-linux-x64.rpm

ADD kusokora.jar /opt/kusokora/
ADD classes/haarcascade_frontalface_default.xml /opt/kusokora/
EXPOSE 8080
WORKDIR /opt/kusokora/
CMD ["java", "-Xms512m", "-Xmx1g", "-jar", "kusokora.jar", "--classifierFile=haarcascade_frontalface_default.xml"]

ã¾ãŸã€JavaCVã®åˆ¶é™ã§haarcascade_frontalface_default.xmlã‚’jarã®ä¸­ã‹ã‚‰èª­ã¿è¾¼ã‚ãªã„ãŸã‚ã€å¤–出ã—ã—ã¦æ¸¡ã™å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ haarcascade_frontalface_default.xmlã‚’AWSデプロイ用ã®zipã«å«ã‚ã‚‹ãŸã‚ã«pom.xmlã®ä»¥ä¸‹ã®éƒ¨åˆ†ã‚’修正ã—ã¾ã™ã€‚

<execution>
    <id>zip-files</id>
    <phase>package</phase>
    <goals>
        <goal>run</goal>
    </goals>
    <configuration>
        <target>
            <!-- classes/haarcascade_frontalface_default.xmlを追加 -->
            <zip destfile="${basedir}/target/app.zip" basedir="${basedir}/target" includes="Dockerfile, Dockerrun.aws.json, ${project.artifactId}.jar, classes/haarcascade_frontalface_default.xml" />
        </target>
    </configuration>
</execution>

Docker上ã¯Linux(CentOS 7)を使用ã™ã‚‹ã®ã§ã€Linux用ã«ã‚¢ãƒ—リケーションをビルドã—ã¾ã™ã€‚

$ mvn clean package -Plinux-x86_64

target以下ã«app.zipãŒã§ãã‚‹ã®ã§ã€ã“れを「Dockerを使ã£ã¦ã¿ã‚‹ã€ã®ã‚ˆã†ã«ã€AWSã«ãƒ‡ãƒ—ロイã—ãŸã‚Šã€ ローカルã®boot2dockerã§è©¦ã™ãªã‚Šã—ã¦ã¿ã¦ãã ã•ã„。

本章ã®å†…容を修了ã—ãŸã‚‰ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã€Œ#kanjava_sbc #sbc11ã€ã‚’ã¤ã‘ã¦ãƒ„イートã—ã¦ãã ã•ã„。