謎言語使いの徒然

適当に気になった技術や言語を流すブログ。

DI 入門者向けの話(DI が生まれる背景)

[日記][Tips]DI 入門者向けの話(DI が生まれる背景)

Dependency Injection 日本語訳で、外部依存注入の話をする。

前提知識

オブジェクト指向には多態性なるものがある。
これは、Java でいうところのインターフェースに依存する作りをすることで、その実装を変更できるという論理だ。

割と具体的にコードに落とすと

class AppendHello {
  public String wrap(String name) {
    return "Hello, " + name + " name count are " + this.countValue(name);
  }

  public int countValue(String name) {
    return name == null || name.equals("") ? 0 : name.size();
  }
}


public class Main {
  AppendHello wrapper = new AppendHello();
  public void main() {  // static でないのはサンプルだから
    String result = wrapper.wrap("Azalea");
    System.out.println(result); // ここは変更なし
    return result;
  }
}

ってしてしまうと、Main はあまりに AppendHello にべったりしすぎてしまい、AppendHello でなにか修正を受けると影響がモロに出てしまう。

そこで、本当に必要なインターフェースを用意し、Main から依存するのをインターフェースにしてしまえば、何かの理由で AppendHello に手が入っても、Main 処理に影響を及ぼさないし、何よりインターフェースを継承した別のもので代用できる。
これが多態性だ。

interface StringWrapper {
  String wrap(String value);
}

class HelloWrapper implements StringWrapper {
  @Override
  public String wrap(String name) {
    return "Hello, " + name + " name count are " + this.countValue(name);
  }

  public int countValue(String name) {
    return name == null || name.equals("") ? 0 : name.size();
  }
}

public class Main {
  StringWrapper wrapper = new HelloWrapper();
  public void main() {
    String result = wrapper.wrap("Azalea");
    System.out.println(result);
    return result;
  }
}

例えば、HTML でラップするものに差し替えるとしても

class HtmlWrapper implements StringWrapper {
  @Override
  public String wrap(String name) {
    return String.format("<p>%s</p>", name);
  }
}

public class Main {
  StringWrapper wrapper = new HtmlWrapper(); // ここだけ修正
  public String main() {
    String result = wrapper.wrap("Azalea"); // ここは変更なし
    System.out.println(result);
    return result;
  }
}

つまり、メソッドの呼び出し方と、その応答データの種類に事前に制約をかけて、そのルールでクラスを実装する限り、使用している Main に修正の手が伸びない。
置き換えて色々な事に使用できる。
そういうことだ。

問題点と当初考えられた解決法

しかし、これには半端に2つ問題が発生する。

1: コンパイル時点で固定になる

それは new した時点で固定となり、処理の入れ替えが効かない事である。

public class Main {
  // ↓ここで new するやつ固定じゃん
  StringWrapper wrapper = new HtmlWrapper();
  public void main() {
    System.out.println(wrapper.wrap("Azalea"));
  }
}

つまり、入れ替えしたければ再コンパイルしなければならないという事だ。

2: テストしにくい

  1. Main.main の挙動が、HtmlWrapper の実装に依存しているため、Main.main の応答結果をテストしたければ、最初に HtmlWrapper の動作を考慮しなければならないという問題がある(仮にこれがデータベースを使うものだと考えてみればいい…表面的に関連性のわからないレコードを大量にテスト準備で用意するのはあまりに大変だ)。
  2. HtmlWrapper は別クラスなので、何らかの理由で修正が入るかもしれない、つまりこのテストは HtmlWrapper が修正されるたびに書き直さなければならないのだ。
  3. HelloWrapper 等に置き換える旅にテストを変更しなければならない。これもアフォかと。

オブジェクト指向的に頑張って解決(古典的手段)

この問題を解決するために、オブジェクト指向界ではデザインパターンという名前のあるアルゴリズムのごとく、「こんな時にはこういうデザイン(設計)パターン(型)を適用する」といった設計パターンを作り上げた。

上記であれば、FactoryMethod パターンだ。

interface StringWrapper {
  String wrap(String value);
}

class HelloWrapper implements StringWrapper {
  @Override
  public String wrap(String name) {
    return "Hello, " + name + " name count are " + this.countValue(name);
  }

  public int countValue(String name) {
    return name == null || name.equals("") ? 0 : name.size();
  }
}

abstract class Main {
  abstract protected StringWrapper getWrapper(); // 依存部分だけ切り出す

  public void main() {
    String result = this.getWrapper().wrap("Azalea");
    System.out.println(result);
    return result;
  }
}

public class HelloMain extends Main {
  @Wrapper
  protected StringWrapper getWrapper() {
    return new HelloWrapper();
  }
}

このようにすることで、

  1. Main だけみれば StringWrapper 部分は切り離されているため、使うべき所で継承して用意すればよい。
  2. Main.main は StringWrapper の実装には依存していないので、テスト時には都合のいい偽物にでも置き換えできる。

他にも、 "AbstractFactory":https://ja.wikipedia.org/wiki/Abstract_Factory_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3 なんかもあるので、見てみるといい。

オブジェクト指向だけでの限界

こうする事で、そこそこ動的に入れ替えが効くようにはなるがいくつか問題が起きる。

  1. クラス爆発
    Main に対し、各実装を含む HelloMain とか HtmlMain とか、ともかくクラス数が増える。
    それは保守性や可読性に対してマイナスだ。
  2. 仕組みのためのコード量が多い
    実際に業務上使用したいコードという訳ではないのに、汎用性を求めるための追加コードがやたらと増える。
    そのコードが混ざると、「本来要件的に解決したいコード」が、「仕組みのために必要なコード(非業務要件)」に埋もれてしまい、保守性が下がる。
    (こうした、仕組み上書かなければならない、業務的に意味のない定型コードは「ボイラープレート」と呼ばれる)

インジェクション

そこで、そうした依存は外から設定注入(インジェクション)できるようにすればいいのではないかと考える。
つまり、必要なオブジェクトは使う時に外から指定してやれば良いのではないかと考える。

その案のもっとも原始的方法は、「依存するオブジェクトをコンストラクタで受け取ってしまう」である。

要するにクラスの中で new しなけりゃいいんだろというなんとも簡単な話。

public class Main {
  StringWrapper wrapper;
  
  public Main(StringWrapper wrapper) {
    this.wrapper = wrapper;
  }

  public void main() {
    System.out.println(wrapper.wrap("Azalea"));
  }
}

このクラスだけ見れば、これで問題ない様に見える。
しかし使う側で固定化問題が起こる訳だ。

public UseMain {

  // 使う側でこうなってしまう
  // 結局フレキシブルになりきれない
  Main mainClass = new Main(new HelloWrapper());
  
  public String useMain() {
    String res = mainClass.main();
    // 何か後続処理
  }
}

ならどうするか?
そう考えた時に行き着く技術がある。

リフレクション である。

リフレクションを使ったインジェクション

まずリフレクションについて少し話そう。
通常、Java のオブジェクトとはクラスを書いて、new して初めて利用できる。

このオブジェクトの定義をクラス、new してできた実体をインスタンスと呼んでいる。

ここでいう所のクラスとは何かを考えると、インスタンスが実際に動かすことができるメモリ上のプログラムなら、それを形作る、もしくはその挙動を定義するクラスとは、すなわちメモリ確保時に使用する設計図だ。

普段は new しかしないが、そこに設計図がある。
なら、その設計図から出来上がったインスタンスの中身は、つまりメモリの形が把握できるということ。
なら、設計図を見ながらムリヤリでも書き換えることができるのではないか?

実際にそれを行う機能が「リフレクション」と呼ばれる機能群だ。

例えば、setter のない private な文字列フィールドを持つクラスを考える。

class Example {
  private String name;
  
  public Example(String value) {
    this.name = value;
  }
  
  public String getName() {
    return this.name;
  }
}

普通に考えたら、name はコンストラクタからしか設定することができない。
だが、リフレクションを使うとここに値を設定できてしまう。

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

class Example {
    private String name;

    public Example(String value) {
        this.name = value;
    }

    public String getName() {
        return this.name;
    }
}

public class Main {
    public static void main(String[] args)
            throws NoSuchMethodException,
            IllegalAccessException,
            InvocationTargetException,
            InstantiationException,
            NoSuchFieldException {

        // new を使わず設計図からムリヤリインスタンス作成
        Example example = Example.class.getDeclaredConstructor(String.class).newInstance("Hoge");

        System.out.println(example.getName()); // Hoge が表示

        // name の設計図を取得
        Field field = Example.class.getDeclaredField("name");

        // セットアクセス権限を設定
        field.setAccessible(true);

        // example の name をムリヤリ変更
        field.set(example, "Update");

        System.out.println(example.getName()); // Update が表示
    }
}

この機構を使い、設定ファイルか何かでクラスの外からの入力を制御すれば、なんとコードを一切いじらずに実装を変更、挙動を変えることができるのではないかと考える。
クラスの外(外部)から、依存する処理(依存)を流し込む(注入)ことで外部依存注入、DI となるわけだ。

この機能を使ったライブラリを、「public static void main」で初期化し、残りの全てのインスタンス生成してもらえば、あとは全てのクラスで new による直接的な依存がなくなる…すなわち、疎結合とテスタビリティの確保ができるようになる。

気が向いたら続く。

MyBatis の定義がクソ面倒なので、生成を狙う

A5:SQL Mk-2 - フリーの汎用SQL開発ツール/ER図ツール .. 松原正和 使って作ったテーブルのマッピングを書くのがだるくなったので、自動生成を狙った。

まずはこんな感じの ER を書いて、CSV でエクスポートする。

f:id:white-azalea:20171003224641p:plain

一応設定は UTF-8 で吐く。
そうすると、a5m2_COLUMNS.csv なるファイルが出来上がる。

TABLE_CATALOG,TABLE_SCHEMA,TABLE_NAME,COLUMN_NAME,LOGICAL_NAME,ORDINAL_POSITION,COLUMN_DEFAULT,IS_NULLABLE,DATA_TYPE,KEY_POSITION,DESCRIPTION
Example,,example_entity,id,ID,1,,NO,BIGINT IDENTITY,1,
Example,,example_entity,name,名前,2,,NO,NVARCHAR(64),,
Example,,example_entity,price,価格,3,,NO,@INT,,
Example,,example_entity,example_field,その他適当,4,,NO,"@DECIMAL(10, 3)",,"これが
複数行"
Example,,seller,id,ID,1,,NO,BIGINT IDENTITY,1,
Example,,seller,example_entry_id,商品ID,2,,NO,BIGINT,,
Example,,seller,name,店名,3,,NO,NVARCHAR(64),,
Example,,seller,created_at,作成ビ,4,CURRENT_TIMESTAMP,NO,@DATETIME,,
Example,,seller,updated_at,更新日,5,,YES,@DATETIME,,

あとはこれを自動生成できりゃいい。
ちなみに SQL Server 用に設定したけど、他のものを使いたければちょっと改変すればいい。

Python3 で読み込んで、マッピング java bean (Lombok) と mapping 用 XML を吐き出した。

import csv

def to_camel_name(name):
    components = name.split('_')
    return components[0] + "".join(x.title() for x in components[1:])

def to_upper_camel_name(name):
    components = name.split('_')
    return "".join(x.title() for x in components[:])

def starts_with(string_value, arr):
    for v in arr:
        if string_value.startswith(v):
            return True
    return False

class Field:
    def __init__(self, name, id, dataType, description):
        self.name = name
        self.id = id
        self.dataType = dataType
        self.description = description

    def db_type(self):
        stn = self.dataType.lower()
        convert = {
            'INTEGER'  : ['@int', 'int'],
            'BIGINT'   : ['bigint', '@bigint'],
            'DECIMAL'  : ['@decimal', 'decimal'],
            'VARCHAR'  : ['@char', 'char', 'varchar'],
            'NVARCHAR' : ['nchar', 'nvarchar'],
            'BOOLEAN'  : ['bit'],
            'TIMESTAMP': ['date', 'datetime', 'timestamp', '@datetime']
        }
        for key in convert.keys():
            if starts_with(stn, convert[key]):
                return key
        return None

    def type_name(self):
        stn = self.dataType.lower()
        convert = {
            'Integer'   : ['@int', 'int'],
            'Long'      : ['bigint', '@bigint'],
            'BigDecimal': ['@decimal', 'decimal'],
            'String'    : ['@char', 'char', 'varchar', 'nchar', 'nvarchar'],
            'Boolean'   : ['bit'],
            'Date'      : ['date', 'datetime', 'timestamp', '@datetime']
        }
        for key in convert.keys():
            if starts_with(stn, convert[key]):
                return key
        return 'Object'

    def get_field_definition(self):
        changed_type = self.type_name()
        field_name = to_camel_name(self.id)
        str = ''
        str = str + f'    /**\n'
        str = str + f'     * {self.name}.\n'
        str = str + f'     *\n'
        str = str + f'     * {self.description}.\n'
        str = str + f'     */\n'
        str = str + f'    private {changed_type} {field_name};\n\n'
        return str

class_field = {}  # クラス辞書

try:
    with open('a5m2_COLUMNS.csv', 'r') as csvfile:
        spamreader = csv.reader(csvfile, delimiter=',', quotechar='"')
        for row in spamreader:
            if len(row) == 0:
                continue
            class_name = to_upper_camel_name(row[2])
            field = Field(row[4], row[3], row[8], row[10])
            if class_name in class_field:
                class_field[class_name].append(field)
            else:
                class_field[class_name] = [field]

# 起こりそうな例外をキャッチ
except FileNotFoundError as e:
    print(e)
except csv.Error as e:
    print(e)

for class_name in class_field.keys():
    file = open(f'{class_name}.java', 'w', encoding='UTF-8')
    file.write('\nimport lombok.*;')
    file.write('\nimport java.util.Date;\n')
    file.write('\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\n')
    file.write('public class ' + class_name + ' {\n')
    for field in class_field[class_name]:
        file.write(field.get_field_definition())
    file.write('}\n')
    file.close()

    file = open(f'{class_name}.mapping.xml', 'w', encoding='UTF-8')
    file.write(f'<resultMap id="{class_name}Map" type="{class_name}">\n')
    for field in class_field[class_name]:
        cur_db_type = field.db_type()
        db_str = ('jdbcType="' + cur_db_type + '"') if cur_db_type != None else ''
        file.write(f'    <result property="{to_camel_name(field.id)}" column="{field.id}" {db_str} />\n')
    file.write('</resultMap>\n')
    file.close()

このスクリプトは好きに使って。

CSV の1行目呼んで変なの出るけどご愛嬌で!

Java9 入れて遊んでみた

まずはここからダウンロード。

Java SE - Downloads | Oracle Technology Network | Oracle

Early Access Releases を選ぶと、JDK9 がダウンロードできる。

インストーラを終えたら .bash_profile を設定する。

export JAVA_HOME=`/usr/libexec/java_home -v 9`

としれっと記述。
後は bash 起動して jsell と叩くとしれっと書くと動き始める。

$ jshell
|  JShellへようこそ -- バージョン9
|  概要については、次を入力してください: /help intro

jshell> System.out.println("Hello jshell");
Hello jshell

jshell> 

とはいえ、Java は Shell のようなものが十全なほど言語仕様は十分か…はちょっとまだ解りませんが…

Bootstrap 以外の css フレームワーク

有名どころ。
個人的にきになったやつのみ

http://foundation.zurb.com/foundation.zurb.com

サイトや email に使用できるフレームワーク
CSS フレームワークというより、HTML の組み方なんかも固定となっている。
やたらと部品が豊富で、一種のラブラリ集と化している雰囲気。

semantic-ui.com

Semantic is a development framework that helps create beautiful, responsive layouts using human-friendly HTML. Human-Friendly な HTML で美しく、レスポンシブレイアウトの開発を支援する開発フレームワーク

サンプルを見てもわかりやすい。
学習コストが低いのはいい事です。

getmdl.io

Google の提唱する MaterialDesign を実装するコンパクトなフレームワーク
コンパクトなだけあって昨日は低めだけど、Material-UI よりはだいぶ安定してるかな?

www.material-ui.com

Goole 製 MaterialDesign フレームワーク
公式で React を謳うとか、Polymer や Angular はどーした?

Google は結構、OSS でさえあれば、自社製に拘らない所があるし、ありえるっちゃありえるか…

v1.0 からマジで React 前提になってる臭い。

https://material-ui-1dab0.firebaseapp.com/getting-started/usage

この辺も参考になるかも?

現時点で Reactを Gulp 上コンパイルして見る

教科書はこれ

qiita.com

package.json をしれっと。

{
  "name": "react-base",
  "version": "0.1.0",
  "description": "React base application template",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "BSD-2-Clause",
  "dependencies": {
    "gulp-util": "^3.0.8",
    "react": "^15.6.1",
    "react-dom": "^15.6.1"
  },
  "devDependencies": {
    "autoprefixer": "^7.1.2",
    "babel": "^6.23.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "babelify": "^7.3.0",
    "browserify": "^14.4.0",
    "cssnano": "^3.10.0",
    "gulp": "^3.9.1",
    "gulp-postcss": "^7.0.0",
    "gulp-sass": "^3.1.0",
    "gulp-sourcemaps": "^2.6.0",
    "gulp-uglify": "^3.0.0",
    "gulp-webserver": "^0.9.1",
    "rimraf": "^2.6.1",
    "vinyl-buffer": "^1.0.0",
    "vinyl-source-stream": "^1.1.0"
  },
  "scripts": {
    "start": "gulp",
    "clean": "gulp clean",
    "compile": "gulp compile"
  }
}

とりあえず各バージョンは上記の通り。
その上で gulpfile.js を下記のようにすればまずコンパイルできる

var gulp = require('gulp');
var gutil = require('gulp-util');

var browserify = require('browserify');
var babelify = require('babelify');

var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var sourcemaps = require('gulp-sourcemaps');

var uglify = require('gulp-uglify');

var webserver = require('gulp-webserver');

let SOURCE_DIR = './src';
let JS_SOURCE_DIR = SOURCE_DIR + '/main';

let DIST_DIR = './dist';

// React コードのコンパイル設定
let jsCompileTask = browserify({
  entries: [ JS_SOURCE_DIR + '/index.js'],
  transform: ['babelify'],
  cache: {},
  packageCache: {}
});

// コンパイル実行箇所
function compileJs() {
  function createErrorHandler(name) {
    return function (err) {
      console.error('Error from ' + name + ' in compress task', err.toString());
    };
  }

  return jsCompileTask.bundle() // ソースをコンパイル
    .on('error', gutil.log.bind(gutil, 'Browserify Error')) // コンパイルエラー出力
    .pipe(source('main.min.js')) // 全てのソースをまとめた JavaScript 出力名
    .pipe(buffer())  // バッファリング
    .pipe(sourcemaps.init({loadMaps: true})) // 既に出てるソースマップ取り込み
    .pipe(uglify())  // コード圧縮
    .on('error', createErrorHandler('uglify'))  // エラーメッセージ
    .pipe(sourcemaps.write('./'))  // デバッグ情報出力
    .pipe(gulp.dest(DIST_DIR + '/js'));  // 出力ディレクトリ
}

function runServer() {
  gulp.src(DIST_DIR)
    .pipe(webserver({
      livereload: true,
      open: true,
      fallback: '/index.html'
    }));  
}

function watch() {
  gulp.watch(JS_SOURCE_DIR + '/**/*.js', ['compileJs']);
}

gulp.task('compileJs', compileJs);

gulp.task('compile', ['compileJs'])

gulp.task('watch', watch);
gulp.task('debug', ['compile', 'watch'], runServer);

gulp.task('default', ['debug']);

後は scss でもなんでもすればおk

ただし、この構成は欠点があって、CSS ファイルを include すると死ぬ。
多分 babel-preset-react の問題だと思う。

Gradle4 でマルチプロジェクト

教科書はこれ。 第57章 マルチプロジェクトのビルド

まずは Jersey の設定を作って、分離するものを指定する。

Jerseyの設定2(web.xmlとかApplicationクラスとか) - edgegram

サーブレット設定をソースで

web.xml ファイルは昔は役に立ったアーキテクチャだ。
一つの Tomcat で複数のサーブレットをデプロイするにあたって、そのデプロイ設定を行うにはリーズナブルだったろう。

だが、はっきり言おう、今日日1サーバにそう何個もデプロイなんざしねーよと(汗
MVC ができてルーティングができるようになった時から、その役目を失ったと言える。

ということで設定

package net.white_azalea

import org.glassfish.jersey.server.ResourceConfig
import org.glassfish.jersey.server.ServerProperties
import javax.ws.rs.ApplicationPath

@ApplicationPath("/")
class ApplicationSetting : ResourceConfig {

    constructor() {
        packages("net.white_azalea")
        property(ServerProperties.PROVIDER_SCANNING_RECURSIVE, true)
    }
}

そしてこれがあると、設定ファイル( web.xml )いらずなのです。

そしてプロジェクト分離

設定一つで、管理しようと思う。
何って完全分離とか難しすぎた

目的はこんな感じに配置すること。

  • ROOT
    • build.gradle
    • settings.gradle
    • src/main/kotlin/net/white_azalea/ApplicationSetting.kt
      プロジェクト設定を含むソース
    • settings
      サブプロジェクト
      • src/main/kotlin/net/white_azalea/api/Example.kt ソース

まずは ROOT/settings.gradle

include 'settings'

次に、ROOT/build.gradle を編集

apply plugin: 'maven'
apply plugin: 'war'
apply plugin: 'org.akhikhl.gretty'
apply plugin: 'kotlin'

sourceCompatibility = 1.8
def jerseyVersion = '2.25.1'

buildscript {
  repositories {
    jcenter()
    mavenCentral()
  }

  dependencies {
    classpath 'org.akhikhl.gretty:gretty:+'
    classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.3'
  }
}

def thisGroup = 'net.white-azalea'
def thisVersion = '1.0'

allprojects {
    apply plugin: 'maven'
    apply plugin: 'war'

    group = thisGroup
    version = thisVersion

    repositories {
        jcenter()
        mavenCentral()
        mavenLocal()
    }

    dependencies {
        compile 'javax.ws.rs:javax.ws.rs-api:2.0.1'
        compile "org.glassfish.jersey.containers:jersey-container-servlet:${jerseyVersion}"
        testCompile 'junit:junit:4.+'
    }
}

ext {
    appName = 'example'
    project.version = thisVersion
    project.group = thisGroup
    project.description = 'example gradle application.'
}

dependencies {
    compile project(':settings')
}

ここまで書いたら後は settings ディレクトリを掘ってファイルを配置すればおk

Gradle4 + Jersey2 + Kotlin でRESTサービスを作ってみる

SPA でアプリ作ろう思って、サーバに当初 Spring 考えてたけど、こっちの方が API サーバが楽そうだった。

で、Kotlin は個人的な好み

教科書はこれ

JAX-RS(Jersey)+GradleでWebアプリを作る - Olivinecafe - blog

Gradle

汎用ビルドツール。
Java 専用ではないので、Java 書きたければ基本はプラグイン
独自の DSL で記述するが、Java ライブラリとか普通に叩けた。

個人的に好きなのは、gradle wrapper コマンドで、これを実行するとカレントディレクトリに gradlew コマンドを吐き出す。
こいつは単独実行可能な gradle コマンドなので、いちいち使う人に Gradle のインストールを強要しない。

sbt も大概 jar 一つでやってのけるのだが、こういう仕様がないと、複数人での開発がだるくて仕方ない。

とりあえず、build.gradle はこんな感じで書いた。

apply plugin: 'maven'
apply plugin: 'war'
apply plugin: 'org.akhikhl.gretty'
apply plugin: 'kotlin'

def jerseyVersion = '2.25.1'

sourceCompatibility = 1.7

buildscript {
  repositories {
    jcenter()
    mavenCentral()
  }

  dependencies {
    classpath 'org.akhikhl.gretty:gretty:+'
    classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.3'
  }
}

ext {
    project.version = '0.0.1'
    project.group = 'net.white-azalea'
    appName = 'example'
    project.description = 'example gradle application/'
}

configurations {
     all*.exclude module: 'servlet-api'
}

dependencies {
    providedCompile 'javax.ws.rs:javax.ws.rs-api:2.0.1'
    compile "org.glassfish.jersey.containers:jersey-container-servlet:${jerseyVersion}"
    testCompile 'junit:junit:4.+'
}

repositories {
    jcenter()
    mavenCentral()
    mavenLocal()
}

とてもざっくり説明すると、

  • リリース時は war でビルドするよ
  • gretty プラグインを使って、デバック実行時に jetty サーバ使うよ
  • Kotlin 言語でやるよ
    しかも Java プラグイン入れてないので、Java 書けないよ
  • servlet-api は war には含まない様にしてるよ
    これが含まれてると、Tomcat とかのサーバにデプロイした時バージョン衝突とかめんどくさいことになる場合がある
  • Jersey2.25.1 を必要とするよ

これを gradlew appRun すると、依存する jar とか、実行サーバとか諸々勝手にダウンロードしてくる。
開発マシンにわざわざ手動で Tomcat 入れさせる環境とか、ネットワーク接続禁止環境だけでしょ…Web アプリ作るのにネットワーク禁止とかこれいかに?

jersey2

JAX-RS 実装らしいがなんのことはない。
アノテーションでパス指定すれば色々できる。

まずは、src/main/webapp/WEB-INF/web.xml を記述する。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    version="2.5">
    <servlet>
        <servlet-name>jersey-example</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>net.white_azalea</param-value>
        </init-param>
        <init-param>
            <param-name>jersey.config.server.provider.scanning.recursive</param-name>
            <param-value>true</param-value>
        </init-param>
        <load-on-startup>10</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>jersey-example</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

設定は全て Gradle にお任せモードする web.xml だ。

お次はソース。src/main/kotlin/net.white-azalea/Example.kt を作成して以下を記述

package net.white_azalea

import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.Produces
import javax.ws.rs.core.MediaType

@Path("/hello")
class Example {

    @GET
    @Produces(MediaType.TEXT_HTML)
    fun hello(): String = "Hello!"
}

ここまできたら全部おしまい。

./gradlew appRun して、http://localhost:8080/jersey-example/hello にアクセスできれば OK.

jar 依存がだいぶ足りてないので、オブジェクト返すとコケる。
まぁその辺は この辺 みて自力解決しよう。