Spring Bootの練習日記:6日目

今日もまたサンプルを見ながら、できそうなものを調べていこうと思う。

STSとは?

STSは、Spring Tool Suiteの略で、EclipseベースのIDEの模様。
とりあえず自分には必要なさそうと判断したので放置しよう…。

一応、DL先のURLはこちら。http://spring.io/tools/sts/all

Securing a Web Applicationをやってみる

順序立てて何をやればいいのかがわからなかったので、ざっと眺めてやっておくべきことであろう、セキュアなWebアプリっていう項目に目をつけた。

まずはhtmlを追加。ThymeleafでURLのリンクを書く場合は、th:href=”@{/hello}”などと書くようだ。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
  <title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
</body>
</html>

次に、リンク先のhtmlを記述する。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
  <title>Hello World!</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>

そして、WevMvcConfigurerAdapterを継承したMvcConfigクラスを作る。

package sample;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }
}

addViewControllerを呼び出して、コントローラーをセットし、setViewNameでテンプレートもセットする。これでURLとテンプレートがマッピングされた形か。ちなみにまだlogin.htmlは作っていないが、gradle bootRunは動いた。

http://localhost:8080/homeにアクセスしたら、ちゃんと表示された。リンクも貼られていて、http://localhost:8080/helloに遷移することができた。

Spring Securityをセットアップする

ここからが本題だろう。

このままだと/helloに普通にアクセスできてしまうので、Basic認証をつけよう!ってことらしい。そのためには、build.gradleのdependenciesブロックに以下を追加する。

dependencies {
//    compile("org.springframework.boot:spring-boot-starter-web:1.2.1.RELEASE")
//    compile("org.springframework.boot:spring-boot-starter-actuator:1.2.1.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf:1.2.1.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-security:1.2.1.RELEASE")
    compile("org.webjars:jquery:2.1.3")
    testCompile("org.springframework.boot:spring-boot-starter-test:1.2.1.RELEASE")
}

その後、WebSecurityConfigクラスを作成。

package sample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("user").password("password").roles("USER");
    }
}

@EnableWebMvcSecurityアノテーションを使うことで、Springのセキュリティサポートを利用することができるようになる模様。
configureメソッドをオーバーライドして、URLのアクセス制限を設定していくようだ。ここでは、/と/homeは全部許可しているけれど、それ以外は認証が必要にしてあるように見える。
formLogin以降で、ログインページのURLを設定し、このページは誰でも見られるように設定されている。
configureGlobalメソッドで、ログイン用の情報が設定してあるようだが、てっきりBasic認証だと思っていたら、違った。

とりあえずこのままだとログインページがないので作成する。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
  <title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
  Invalid username and password.
</div>
<div th:if="${param.logout}">
  You have been logged out.
</div>
<form th:action="@{/login}" method="post">
  <div><label> User Name : <input type="text" name="username"/> </label></div>
  <div><label> Password: <input type="password" name="password"/> </label></div>
  <div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>

特に設定しない場合だと、input name=”username”とinput name=”password”を/loginに対してPOSTすれば認証が行われるようだ。楽といえば楽。だが、このままだとサインアウトできないので、hello.htmlを修正してみようってことだったので、やってみる。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
  <title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
  <input type="submit" value="Sign Out"/>
</form>
</body>
</html>

これを見る限りだと、/logoutに対してPOSTでアクセスしたらログアウトのようだ。

Sign Outボタンをクリックしたらちゃんとログアウトできた。Basic認証はわからなかったものの、アクセス制限の方法はだいたいわかった。この認証部分をDBのデータに関連付けたりすれば、一般的なユーザーログイン画面みたいなものが作れるのだろうか?セッション周りのこととかもまだわからないので、調べていきたい。

また今回作ったクラスは自動的に利用されていたようなので、gradleのspring-boot pluginのおかげなんだろう。


heroku pg:diagnose でパフォーマンス確認

データベースの応答が劇的に遅くなるときがあって、原因と解決方法を探そうとしていた。現象が起きているのはHerokuのデモ用の環境だったのだが、本番環境では起きていないのがまだよかったところ…。とりあえず、heroku pg:diagnoseと打つと、健康状態がわかるらしい。

参考URL:https://blog.heroku.com/archives/2014/8/12/pg-diagnose

heroku pg:diagnose --app app-name

こうしたところ、REDが出た。

RED: Hit Rate
Name                    Ratio
----------------------  ------------------
overall table hit rate  0.9375946835977065

GREEN: Connection Count
GREEN: Long Queries
GREEN: Idle in Transaction
GREEN: Indexes
GREEN: Bloat
GREEN: Blocking Queries
SKIPPED: Load
  Error Load check not supported on this plan

このHit Rateが0.99以上であることが正常らしい。これでいえば、93%はメモリにキャッシュされているが、7%はキャッシュされていない。これがとても遅くなる原因なので、次に高いプランにするか、速いプランに変えようって書いてあった。

運用コストを削るためにhobby:basicで運用していたのだが、もし本番環境でこれが頻発するようだったらstandard0にしないとまずそうだ。というか本番環境としての運用はstandard0以上がいいって書いてあるんだけれど。しかし、本番環境以外のデモ環境などについてもstandard0にするとなると、運用コストが跳ね上がる。なにかいい方法を考えなければならない。


Spring Bootの練習日記:5日目

前回はhtmlを書くために、Thymeleafを使ったところだった。今度はJSを使ってみたいので、Application development with Spring Boot + JSをやってみようとした。ところが、これはgroovyでのサンプル?のようで、よくわからなかったので、githubのコードを確認したら、readmeにSpring Boot + webjarsへのリンクがあったので、こっちを確認することに。

Spring BootでJSライブラリを扱う(Webjars)

JS開発者はnpmやbowerを使ってライブラリを管理するが、Javaの開発者はMavenを使って管理できるようにWebjarsを使うといい、と書いてあったと思う。Webjarsを使うと、特別な設定なしに、JSライブラリを扱うことができるようだ。
webjars.orgを使って、JSライブラリをインストールするのが、Java系の標準なんだろうか?

以下を、build.gradleのdependenciesブロックに追加した。

dependencies {
//    compile("org.springframework.boot:spring-boot-starter-web:1.2.1.RELEASE")
//    compile("org.springframework.boot:spring-boot-starter-actuator:1.2.1.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf:1.2.1.RELEASE")
    compile("org.webjars:jquery:2.1.3")
    testCompile("org.springframework.boot:spring-boot-starter-test:1.2.1.RELEASE")
}

そして、thymeleafで作ったテンプレート側を以下のように修正。

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>Getting Started: Serving Web Content</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <script src="webjars/jquery/2.1.3/jquery.min.js"></script>
  <script type="text/javascript">
    $(document).ready(function() {
      $('p').animate({
        fontSize: '48px'
      }, "slow");
    });
  </script>
</head>
<body>
  <p th:text="'Hello, ' + ${name} + '!'" />
</body>
</html>

webjars/jqueryの行でJSライブラリを読み込み、その後、ページの読み込みが終わったらフォントが大きくなるアニメーションを追加している。動かしてみたら、うまくいった。

ThymeleafのCacheの削除方法

ホットスワップにしていたので、自動的に削除されると思っていたら、ちゃんと設定しないとテンプレートのキャッシュも削除されないらしい。

http://docs.spring.io/spring-boot/docs/current/reference/html/howto-hotswapping.html#howto-reload-thymeleaf-content

静的ファイルは毎度読み込むのだが、テンプレートは静的ファイルではないので、キャッシュされるってことのようだ。
application.propertiesに以下のように書く。

spring.thymeleaf.cache = false

静的ファイルはどこに置くのか?

Serving Static Web Content with Spring Bootを読んだ。

静的ファイル置き場は、プロジェクト直下にpublicディレクトリやstaticディレクトリなどを作ると自動的にSpring Bootが認識してくれるらしい。なので、先ほどの文字を大きくするアニメーションのJSを、外部ファイル化してみた。

    $(document).ready(function() {
      $('p').animate({
        fontSize: '48px'
      }, "slow");
    });

そして、htmlで外部ファイル化したJSを読み込むように修正。

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>Getting Started: Serving Web Content</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <script src="webjars/jquery/2.1.3/jquery.min.js"></script>
  <script src="/js/application.js"></script>
</head>
<body>
  <p th:text="'Hello, ' + ${name} + '!'" />
</body>
</html>

文字が大きくなることが確認できた。


Spring Bootの練習日記:4日目

今日からはSpring Boot Reference Guideをつまみ食い的に読んでいく。

なにはなくとも、まずはHot swappingだろうと思って、これを調べた。ホットスワップとは、毎回毎回アプリを再起動しなくてもよくするための仕組みだ。

build.gradleのbuildscriptブロックに、以下を追加した。IntellJで開発しているので、ideaプラグインも入れてある。

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.1.RELEASE")
        classpath 'org.springframework:springloaded:1.2.0.RELEASE'
    }
}

apply plugin: 'idea'

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

その後、HelloControllerを編集して/fooのマッピングを追加。

package sample;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @RequestMapping("/")
    String index() {
        return "Hello World!";
    }

    @RequestMapping("/foo")
    String foo() {
        return "Hello Foo!";
    }
}

その後、gradle bootRunを実行(spring-bootプラグイン入れてること)。http://localhost:8080/fooにアクセスしてみたら、Hello Foo!と表示される。そのあと、Hello Foo!!!を返すようにHelloController.javaを編集し、またhttp://localhost:8080/fooにアクセスしたら表示が更新され…ない…!!!

原因がわからんなーとtwitterで呟いていたところ、@zephiransasさんに教えてもらった。原因は、HelloController.javaを更新するだけじゃなくて、HelloController.classを更新しないといけないので、再ビルドしないといけない、ということだった。な、なるほど…。RailsやPHPに慣れすぎた自分には全く思いつかない発想だった。Spring Boot自体の再起動が必要ない分だけマシ。Javaのコンパイルは速いから大丈夫。ということのようである。肝に銘じておこう。

サンプルコードを読む

The ‘Spring Web MVC framework’を読んでいこうとしたのだが、結局はspring.io/guideをやってみろ、みたいな感じに思えたので、そっちでSpring MVCでよさそうなのを探すことにした。

Serving Web Content with Spring MVCをやってみる

やはりhtmlを出力できないと色々と困るので、まずはテンプレートエンジンを使う方法を調べようと思った。ガイドの中だとおそらくこれだろう。テンプレートエンジンとしてThymeleafを使っている模様。Javaのテンプレートエンジンについては全然知らないので、まずはこれにしてみようと思う。

build.gradleを編集する

spring-boot-starter-thymeleafをdependenciesプロックに追加するようだ。

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.1.RELEASE")
        classpath 'org.springframework:springloaded:1.2.0.RELEASE'
    }
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'spring-boot'

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

jar {
    baseName = 'spring-boot-practice'
    version = '0.1.0'
}

sourceCompatibility = 1.8
version = '1.0'

repositories {
    mavenCentral()
}

dependencies {
//    compile("org.springframework.boot:spring-boot-starter-web:1.2.1.RELEASE")
//    compile("org.springframework.boot:spring-boot-starter-actuator:1.2.1.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf:1.2.1.RELEASE")
    testCompile("org.springframework.boot:spring-boot-starter-test:1.2.1.RELEASE")
}

sourceSets {
    main {
        java
    }
    test {
        java
    }
}

これでbuildしようとしたら、テストのところでThymeleafのテンプレート置き場がないと言われて落ちたので、ディレクトリsrc/main/resources/templatesを作った。

GreetingControllerを作成する

次に、テンプレートを読み込んで出力するためのControllerとしてGreetingControllerを作成した。
/greetingにマッピングされる。戻り値でテンプレートの場所を指定するようだ。ここで言えば、return “greeting”である。modelに、テンプレートに渡したい変数をaddAttributeで追加する模様。

package sample;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class GreetingController {

    @RequestMapping("/greeting")
    public String greeting(@RequestParam(value="name", required=false, defaultValue="World") String name, Model model) {
        model.addAttribute("name", name);
        return "greeting";
    }

}

@RequestParamアノテーションは、変数とパラメータをバインディングするために使うみたいだ。

src/main/resources/templates/greeting.htmlを作成する

呼び出されるテンプレートを作る。
html xmlns:thでthymeleafの定義をロードしてるっぽい。
そして、pタグ内の要素th:textで変数を展開しているみたいだ。nameは、GreetingControllerでmodel.addAttributeで渡されたものが入る。

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>Getting Started: Serving Web Content</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'Hello, ' + ${name} + '!'" />
</body>
</html>
起動する

Gradle taskのbootRunを実行してSpring Bootを起動する。

gradle bootRun

http://localhost:8080/greeting にアクセスすると、Hello World!と表示される。
http://localhost:8080/greeting?name=Okayama として、パラメータを渡すと、Hello Okayama!と表示された。

とりあえず成功した模様。

本日はここまでとして、あとはUnderstanding View Templatesを読んでおこうと思う。


Spring Bootの練習日記:3日目

今日もBuilding an Application with Spring Bootの続きを読む。今日は単体テストのところあたりから。

単体テスト

まずは単体テストを行う。
build.gradleのdependenciesブロックに以下を追加する。testCompileにテスト用のライブラリを渡すことで、テスト時にこれが使えるようになる。

testCompile("org.springframework.boot:spring-boot-starter-test:1.2.1.RELEASE")

そして、次のコードがテストコード。

package sample;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockServletContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MockServletContext.class)
@WebAppConfiguration
public class HelloControllerTest {
    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        mvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
    }

    @Test
    public void getHello() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.TEXT_PLAIN))
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo("Hello World!")));
    }
}

@SpringApplicationConfigurationに渡しているMockServletContextが空のWebApplicationContextを作り、@BeforeのsetUpメソッドでHelloControllerをMockMvcとして生成することでテストが可能となっている。あとはMockMvcRequestBuildersで擬似的にアクセスしてテスト結果を照合すればOK。しかし、アノテーションが多い。流儀に従えばいいだけなんだろうけれど、サラサラと出てくるもんだろうか?

インテグレーションテスト

さきほどの単体テストではMockに置き換えていたが、インテグレーションテストも比較的簡単に行えるようだ。

package sample;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.client.RestTemplate;

import java.net.URL;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@IntegrationTest({"server.port=0"})
public class HelloControllerIT {
    @Value("${local.server.port}")
    private int port;

    private URL base;
    private RestTemplate template;

    @Before
    public void setUp() throws Exception {
        this.base = new URL("http://localhost:" + port + "/");
        this.template = new TestRestTemplate();
    }

    @Test
    public void getHello() throws Exception {
        ResponseEntity<String> response = template.getForEntity(base.toString(), String.class);
        assertThat(response.getBody(), equalTo("Hello World!"));
    }
}

@IntegrationTestアノテーションでポートを指定しておいて、privateなメンバ変数portに@Valueアノテーションで渡すとよい、と理解した。
この例はシンプルなものだったので、簡単そうに見えた。

Production Gradeのサービスを追加する

ウェブサイトを作ったら監視は付き物。Spring Bootはいくつかの監視用モジュールが準備されている。

build.gradleのdependenciesブロックに以下を追加する。

compile("org.springframework.boot:spring-boot-starter-actuator:1.2.1.RELEASE")

そしてSpring Bootを再ビルドしたあと、起動すると、様々な状態監視用のルートがマッピングされている。
エンドポイントの詳細についてはEndpointsを参照するといい。

設定は、application.propertiesファイルに記述することで、できるようである。

あとはSpring Boot Starterを見よ

そして、全てのサンプルソースコードが見られるようだ。

Spring BootはGroovyもサポートしてるよ

Groovyでも書けるらしい。あとは実行可能なjarにまとめることができると。これは最初のほうに書いてあったような…。

読み終わったよ!

あとはSpring Bootのオンラインドキュメントを読みながら深く掘り下げてみよう!だそうである。
そうしよう。とりあえず今日はここまで。