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}”などと書くようだ。

src/main/resources/templates/home.html
01
02
03
04
05
06
07
08
09
10
11
12
<!DOCTYPE html>
      xmlns:th="http://www.thymeleaf.org"
<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を記述する。

src/main/resources/templates/hello.html
01
02
03
04
05
06
07
08
09
10
11
<!DOCTYPE html>
      xmlns:th="http://www.thymeleaf.org"
<head>
  <title>Hello World!</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>

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

src/sample/MvcConfig.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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ブロックに以下を追加する。

build.gradle
1
2
3
4
5
6
7
8
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クラスを作成。

sample/WebSecurityConfig.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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認証だと思っていたら、違った。

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

src/resources/templates/login.html
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
      xmlns:th="http://www.thymeleaf.org"
<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を修正してみようってことだったので、やってみる。

src/resources/templates/hello.html
01
02
03
04
05
06
07
08
09
10
11
12
13
14
<!DOCTYPE html>
      xmlns:th="http://www.thymeleaf.org"
<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

1
heroku pg:diagnose --app app-name

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

01
02
03
04
05
06
07
08
09
10
11
12
13
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ブロックに追加した。

build.gradle
1
2
3
4
5
6
7
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で作ったテンプレート側を以下のように修正。

src/main/resources/templates/greeting.html
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<!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に以下のように書く。

application.properties
1
spring.thymeleaf.cache = false

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

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

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

public/js/application.js
1
2
3
4
5
$(document).ready(function() {
  $('p').animate({
    fontSize: '48px'
  }, "slow");
});

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

src/main/resources/templates/greeting.html
01
02
03
04
05
06
07
08
09
10
11
12
<!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プラグインも入れてある。

build.gradle
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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のマッピングを追加。

sample/HelloController
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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プロックに追加するようだ。

build.gradle
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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で追加する模様。

sample/GreetingController
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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で渡されたものが入る。

src/main/resources/templates/greeting.html
01
02
03
04
05
06
07
08
09
10
<!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を起動する。

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にテスト用のライブラリを渡すことで、テスト時にこれが使えるようになる。

build.gradleのdependenciesブロック
1
testCompile("org.springframework.boot:spring-boot-starter-test:1.2.1.RELEASE")

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

sample/HelloControllerTest
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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に置き換えていたが、インテグレーションテストも比較的簡単に行えるようだ。

sample/HelloControllerIT
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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ブロックに以下を追加する。

build.gradleのdependenciesブロック
1
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のオンラインドキュメントを読みながら深く掘り下げてみよう!だそうである。
そうしよう。とりあえず今日はここまで。