- 投稿日:2019-08-28T22:33:50+09:00
NLP for Java (NLP4J) (2)
以下のような感じで簡単なJavaプログラムを書くとテキストマイニングが楽しめるというコードを作りましたので、近々オープンソースとして公開しようと考えています。
「自然言語処理をやってみたい」「テキストマイニングをしてみたい」という方をターゲットに考えています。形態素解析エンジンにはYahoo JapanのWebサービスを利用しています。
形態素解析の結果を利用して、文書に特徴的なキーワードを出力します処理とInput
List<Document> docs = new ArrayList<Document>(); { docs.add(createDocument("Toyota", "ハイブリッドカーを作っています。")); docs.add(createDocument("Toyota", "ハイブリッドカーを売っています。")); docs.add(createDocument("Toyota", "自動車を作っています。")); docs.add(createDocument("Toyota", "自動車を売っています。")); docs.add(createDocument("Nissan", "EVを作っています。")); docs.add(createDocument("Nissan", "EVを売っています。")); docs.add(createDocument("Nissan", "自動車を売っています。")); docs.add(createDocument("Nissan", "ルノーと提携しています。")); docs.add(createDocument("Nissan", "軽自動車を売っています。")); docs.add(createDocument("Honda", "自動車を作っています。")); docs.add(createDocument("Honda", "自動車を売っています。")); docs.add(createDocument("Honda", "バイクを作っています。")); docs.add(createDocument("Honda", "バイクを売っています。")); docs.add(createDocument("Honda", "軽自動車を売っています。")); docs.add(createDocument("Honda", "軽自動車を作っています。")); } Annotator annotator = new YJpMaAnnotator();{ // 形態素解析処理 annotator.annotate(docs); } Index index = new SimpleDocumentIndex();{ // キーワードインデックス作成処理 index.addDocuments(docs); } { // 共起性の高いキーワードの取得 List<Keyword> kwds = index.getKeywords("名詞", "item=Nissan"); System.out.println("Keywords(名詞) for Nissan"); for (Keyword kwd : kwds) { System.out.println(String.format("%.1f,%s", kwd.getCorrelation(), kwd.getLex())); } } { // 共起性の高いキーワードの取得 List<Keyword> kwds = index.getKeywords("名詞", "item=Toyota"); System.out.println("Keywords(名詞) for Toyota"); for (Keyword kwd : kwds) { System.out.println(String.format("%.1f,%s", kwd.getCorrelation(), kwd.getLex())); } } { // 共起性の高いキーワードの取得 List<Keyword> kwds = index.getKeywords("名詞", "item=Honda"); System.out.println("Keywords(名詞) for Honda"); for (Keyword kwd : kwds) { System.out.println(String.format("%.1f,%s", kwd.getCorrelation(), kwd.getLex())); } } }Output: Nissanに特徴的なキーワードを係数の大きい順に表示する
Keywords for Nissan 3.0,EV 3.0,ルノー 3.0,提携 1.0,軽自動車 0.6,自動車Toyota と Honda はこちら
Keywords(名詞) for Toyota 3.8,ハイブリッド 3.8,カー 1.5,自動車 Keywords(名詞) for Honda 2.5,バイク 1.7,軽自動車 1.0,自動車
- 投稿日:2019-08-28T21:41:04+09:00
Javalinを調べたときのメモ
概要
Javalinとは
JavalinはJavaおよびKotlin用の軽量Web Frameworkです。Spark FrameworkというWeb Frameworkからforkされ、2017年5月にバージョン0.0.1、2019年8月現在は3.4.1がリリースされています。
以下に主な特徴をGitHubから引用します。(日本語訳はグーグル翻訳です)https://github.com/tipsy/javalin
Javalin is more of a library than a framework. Some key points:
- You don't need to extend anything
- There are no @Annotations
- There is no reflection
- There is no other magic; just code.
Javelinは、フレームワークというよりもライブラリです。 いくつかのキーポイント:
- 何も拡張する必要はありません
- @Annotationsはありません
- リフレクションはありません
- 他にマジックはありません。 コードだけ
この記事の前半は、最小構成で"Hello World"を表示するアプリケーションの実装について、後半は簡単なWebアプリケーションの実装で調べたこと/試したことのメモになります。
環境
- Windows 10 Professional 1903
- OpenJDK 12.0.2
- Javalin 3.4.1
- Jetty-server 9.4.19
- Jetty-webapp 9.4.19
- Intellij IDEA 2019.2
参考
- Javalin - A simple web framework for Java and Kotlin
- Spark - A micro framework for creating web applications in Kotlin and Java 8 with minimal effort
- Embedding Jetty
最小構成でHello World
- 参考チュートリアル : Setting up Javalin with Maven
プロジェクト構成
通常のMavenプロジェクトです。
[project_root] | `--- /src | | | `--- /main | | | | | `--- /java | | | | | | | `--- com.example.demo (package) | | | | | | | `--- App.java | | | | | `--- /resources | | | `--- /test | | | `--- /java | | | `--- /resources | `--- pom.xmlpom.xml
最小構成のJavalinアプリケーションに必要な依存関係はjavalinとslf4j-simpleです。
pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>demo-java12-javalin3</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>jar</packaging> <name>demo java12 javalin 3.4.1</name> <description>Demo project for Javalin 3 with Java12</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>12</java.version> <maven.clean.version>3.1.0</maven.clean.version> <maven.resources.version>3.1.0</maven.resources.version> <maven.compiler.version>3.8.1</maven.compiler.version> <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> <maven.compiler.showWarnings>true</maven.compiler.showWarnings> <maven.jar.version>3.1.1</maven.jar.version> <maven.assembly.version>3.1.1</maven.assembly.version> </properties> <dependencies> <dependency> <groupId>io.javalin</groupId> <artifactId>javalin</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.26</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-clean-plugin</artifactId> <version>${maven.clean.version}</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>${maven.resources.version}</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven.compiler.version}</version> <configuration> <compilerArgs> <arg>-Xlint:all</arg> </compilerArgs> <release>${java.version}</release> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>${maven.jar.version}</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>${maven.assembly.version}</version> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <mainClass>com.example.demo.App</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build> </project>アプリケーション起動ポイントの実装
Appクラス(クラス名は任意)のmainメソッドに、Javalinアプリケーションの起動ポイントを実装します。
- (1) : Javalinアプリケーションのインスタンスを初期設定で生成しポート7000で起動します。
- createメソッドにJavalinConfigクラスのインスタンスを渡すことでアプリケーションをカスタマイズできます。
- (2) : Javalinアプリケーションに、パス
/にGETリクエストハンドラを追加します。import io.javalin.Javalin; public class App { public static void main(String ... args) { // (1) Javalin app = Javalin.create().start(7000); // (2) app.get("/", ctx -> ctx.result("Hello World")); } }IDEから起動
App.mainメソッドを実行するとJavalinが起動し、以下のようなログがコンソールに出力されます。ログに出力されている通り私の開発環境では起動にかかる時間は1秒未満でした。
http://localhost:7000にアクセスすると"Hello World"というtext/plainのレスポンスが返ります。[main] INFO io.javalin.Javalin - __ __ _ / /____ _ _ __ ____ _ / /(_)____ __ / // __ `/| | / // __ `// // // __ \ / /_/ // /_/ / | |/ // /_/ // // // / / / \____/ \__,_/ |___/ \__,_//_//_//_/ /_/ https://javalin.io/documentation [main] INFO org.eclipse.jetty.util.log - Logging initialized @827ms to org.eclipse.jetty.util.log.Slf4jLog [main] INFO io.javalin.Javalin - Starting Javalin ... [main] INFO io.javalin.Javalin - Listening on http://localhost:7000/ [main] INFO io.javalin.Javalin - Javalin started in 731ms \o/jarファイルから起動
ビルドするとtargetディレクトリ下に
demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jarというjarファイルが生成されます。
Javalinアプリケーションは埋め込みJettyを利用しているのでjava -jar <jarファイル>でWebアプリケーションを起動することができます。> java --version openjdk 12.0.2 2019-07-16 OpenJDK Runtime Environment (build 12.0.2+10) OpenJDK 64-Bit Server VM (build 12.0.2+10, mixed mode, sharing) > java -jar demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar下記はJVMパラメータでデフォルトロケールを変更する場合の例です。
> java -Duser.language=en -Duser.country=US -jar demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar以上で『最小構成でHello World』のアプリケーションの実装は終了です。
Javalinの特徴について
ここからはJavalinで簡単なWebアプリケーションを実装したときのメモになります。
HTTPハンドラの実装の仕方
上記で書いたGETリクエストハンドラ(下記、パターン1)は、パターン2、パターン3のようにも実装することができます。
パターン1
app.get("/", ctx -> ctx.result("Hello World"));パターン2
※便宜的にAppクラスにメソッドを追加していますが、通常はハンドラクラスを別途実装することになると思います。
public class App { void index2(@NotNull Context ctx) { ctx.result("Hello World2"); } public static void main(String ... args) { Javalin app = Javalin.create().start(7000); App handler = new App(); app.get("/2", handler::index2); } }パターン3
オフィシャルサイトのチュートリアルではstaticフィールドで記述する例もあります。
public class App { static Handler index3 = ctx -> { ctx.result("Hello World3"); }; public static void main(String ... args) { Javalin app = Javalin.create().start(7000); app.get("/3", App.index3); } }HTTPハンドラ
GETの他にPOST、PUT、PATCH、DELETE、HEAD、OPTIONSがあります。
HTTPメソッド シグネチャ GET public Javalin get(@NotNull String path, @NotNull Handler handler) POST public Javalin post(@NotNull String path, @NotNull Handler handler) PUT public Javalin put(@NotNull String path, @NotNull Handler handler) PATCH public Javalin patch(@NotNull String path, @NotNull Handler handler) DELTE public Javalin delete(@NotNull String path, @NotNull Handler handler) HEAD public Javalin head(@NotNull String path, @NotNull Handler handler) OPTIONS public Javalin options(@NotNull String path, @NotNull Handler handler) before / afterエンドポイント
HTTPハンドラの前後で実行するbefore / afterというエンドポイントを設定することができます。
エンドポイント シグネチャ before public Javalin before(@NotNull String path, @NotNull Handler handler) before public Javalin before(@NotNull Handler handler) after public Javalin after(@NotNull String path, @NotNull Handler handler) after public Javalin after(@NotNull Handler handler) HTTPハンドラのグループ化
エンドポイント HTTPメソッド 機能 /api/v1/users GET ユーザ一覧を取得 /api/v1/user/:id GET ID指定でユーザを取得 /api/v1/user POST ユーザを登録 /api/v1/user/:id DELETE ID指定でユーザを削除 /api/v2/users GET ユーザ一覧を取得 /api/v2/user/:id GET ID指定でユーザを取得 /api/v2/user POST ユーザを登録 /api/v2/user/:id DELETE ID指定でユーザを削除 このようなv1とv2の2系統のエンドポイントがあった場合、以下のようにHTTPハンドラをグループ化して定義できます。
UserController v1Controller = /* version 1 の実装 */ UserController v2Controller = /* version 2 の実装 */ app.routes(() -> { // version 1 ApiBuilder.path("/api/v1", () -> { // GET /api/v1/users ApiBuilder.get("/users", v1Controller::findAll); ApiBuilder.path("/user", () -> { // GET /api/v1/user/:id ApiBuilder.get(":id", v1Controller::findById); // POST /api/v1/user ApiBuilder.post(v1Controller::store); // DELETE /api/v1/user/:id ApiBuilder.delete(":id", v1Controller::remove); }); }); // version 2 ApiBuilder.path("/api/v2", () -> { // GET /api/v2/users ApiBuilder.get("/users", v2Controller::findAll); ApiBuilder.path("/user", () -> { // GET /api/v2/user/:id ApiBuilder.get(":id", v2Controller::findById); // POST /api/v2/user ApiBuilder.post(v2Controller::store); // DELETE /api/v2/user/:id ApiBuilder.delete(":id", v2Controller::remove); }); }); });io.javalin.http.Contextについて
HTTPハンドラメソッドのパラメータ
io.javalin.http.Contextクラスは、HTTPリクエストおよびレスポンスに必要な機能とデータを提供します。
以下オフィシャルサイトのドキュメントページからの引用です。https://javalin.io/documentation#context
The Context object provides you with everything you need to handle a http-request. It contains the underlying servlet-request and servlet-response, and a bunch of getters and setters. The getters operate mostly on the request-object, while the setters operate exclusively on the response object.
ContextにはHttpServletRequest、HttpServletResponseのインスタンスを保持するreq、resフィールドもあります。
javax.servlet.http.HttpServletRequest request = ctx.req; javax.servlet.http.HttpServletResponse response = ctx.res;リクエストパラメータの受け取り方
クエリパラメータ
クエリストリングで指定されたパラメータを受け取るメソッド
シグネチャ String queryParam(String key) String queryParam(String key, String default) List<String> queryParams(String key) Map<String, List<String>> queryParamMap() Validator<T> queryParam(String key, Class<T> clazz) フォームパラメータ
フォームもクエリパラメータと同様のメソッドがあります。残念ならがらフォーム全体のフィールドを任意のオブジェクトにバインドすることはできないようです。
シグネチャ String formParam(String key) String formParam(String key, String default) List<String> formParams(String key) Map<String, List<String>> formParamMap() Validator<T> formParam(String key, Class<T> clazz) パスパラメータ
/api/user/1のようなパスからその一部をパラメータとして受け取るメソッド
シグネチャ String pathParam(String key) Map<String, String> pathParamMap() Validator<T> pathParam(String key, Class<T> clazz) リクエストハンドラのパスにコロン(:)を付けた部分がパスパラメータとして識別されます。
app.get("/api/user/:id", ctx -> { Long userId = ctx.pathParam("id", Long.class).get(); });Javalinアプリケーションのコンフィグレーション
- 参考 : Configuration
Javalin.createメソッドにJavalinConfigクラスのインスタンスを渡すことでカスタマイズができます。
以下のようにJavalinConfigのインスタンスを渡す方法の他に、lambda式で記述する方法もあります。インスタンス
configurationJavalinConfig config = new JavalinConfig(); // configuration Javalin .create(config) // ... 省略 ...lambda式
configurationJavalin .create(config -> { // configuration }) // ... 省略 ...主なコンフィグレーション
コンテキストパスの設定 (デフォルトは"/")
config.contextPath = "/app";デフォルトのコンテンツタイプの設定 (デフォルトは"text/plain")
config.defaultContentType = "application/json";ETagの設定 (デフォルトはfalse)
config.autogenerateEtags = true;リクエストログの出力はJavalinConfig.requestLoggerで設定できます。
第二引数(execTime)はリクエストに掛かった時間(ms)です。configurationconfig.requestLogger((ctx, execTime) -> { LOG.debug("[{}] {} - {} ms.", ctx.fullUrl(), ctx.userAgent(), execTime); });開発用コンフィグレーション
開発時に有効にすると便利な機能もあります。
enableDevLogging
configurationconfig.enableDevLogging();情報量が多いので常時有効にしておくというよりは、問題が起きたときの調査時に有効化するといった使い方になると思います。
[qtp2092769598-18] INFO io.javalin.Javalin - JAVALIN REQUEST DEBUG LOG: Request: GET [/overview] Matching endpoint-handlers: [GET=/overview] Headers: {Cookie=Idea-c29ae3c1=53d6d99f-1169-42ed-8d69-a63cfe80a87a, Accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3, Upgrade-Insecure-Requests=1, Connection=keep-alive, User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36, Sec-Fetch-Site=none, Host=localhost:7000, Sec-Fetch-User=?1, DNT=1, Accept-Encoding=gzip, deflate, br, Accept-Language=ja,en-US;q=0.9,en;q=0.8,en-GB;q=0.7, Sec-Fetch-Mode=navigate} Cookies: {Idea-c29ae3c1=53d6d99f-1169-42ed-8d69-a63cfe80a87a} Body: QueryString: null QueryParams: {} FormParams: {} Response: [200], execution took 26.37 ms Headers: {Server=Javalin, Content-Encoding=gzip, Date=Wed, 21 Aug 2019 13:44:46 GMT, Content-Type=text/html} Body is gzipped (4464 bytes, not logged) ----------------------------------------------------------------------------------RouteOverviewPlugin
configurationconfig.registerPlugin(new RouteOverviewPlugin("/overview"));このプラグインを登録して
http://localhost:7000/overviewにアクセスするとJavalinアプリケーションに追加されているリクエストハンドラの一覧を確認することができます。
エラーレスポンスのカスタマイズ
JavalinのHttpResponseExceptionを継承したHTTPステータスに対応する例外クラスが用意されています。
Status 例外クラス メッセージ 302 RedirectResponse Redirected 400 BadRequestResponse Bad request 401 UnauthorizedResponse Unauthorized 403 ForbiddenResponse Forbidden 404 NotFoundResponse Not found 405 MethodNotAllowedResponse Method not allowed 409 ConflictResponse Conflict 410 GoneResponse Gone 500 InternalServerErrorResponse Internal server error 502 BadGatewayResponse Bad gateway 503 ServiceUnavailableResponse Service unavailable 504 GatewayTimeoutResponse Gateway timeout 固有の例外をハンドリングする
アプリケーション固有の例外をハンドリングして任意のエラーレスポンスを返すことができます。
下記のAppServiceExceptionというアプリケーション固有の例外がスローされた場合、AppServiceExceptionpublic class AppServiceException extends RuntimeException { public AppServiceException() { } public AppServiceException(String message) { super(message); } public AppServiceException(String message, Throwable cause) { super(message, cause); } }この例外をハンドリングしてHTTPステータス返すにはJavalinアプリケーションに例外をマッピングします。
この例では500ステータスだけを返します。app.exception(AppServiceException.class, (e, ctx) -> { ctx.status(500); });任意のエラーレスポンスメッセージとHTTPステータスを返すには以下のように実装します。
resultメソッドではなくjsonメソッドを使用すればjsonフォーマットのメッセージを返すこともできます。app.exception(AppServiceException.class, (e, ctx) -> { ctx.result("Application Error : " + e.getMessage()).status(500); });リクエスト / レスポンスでjsonを扱う
リクエスト / レスポンスでjsonを扱えるように依存関係にjacksonを追加します。
2番目のjackson-datatype-jsr310はJava8 Date and Time APIを扱う場合に必要です。pom.xml<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.10.0.pr1</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>2.10.0.pr1</version> </dependency>jsonをレスポンスするGETリクエストハンドラを実装します。jsonをレスポンスするにはjsonメソッドを使用します。
public static void main(String ... args) { // ... 省略 // (1) ObjectMapper objectMapper = new ObjectMapper(); objectMapper.findAndRegisterModules(); objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")); // (2) JavalinJackson.configure(objectMapper); // (3) app.get("/json", ctx -> { Map<String, Object> model = new HashMap<>(); model.put("message", "Hello World"); model.put("now", LocalDateTime.now()); //jsonメソッドを使用します。 ctx.json(model); }); // (4) app.post("/json", ctx -> { Map<String, Object> model = ctx.bodyAsClass(Map.class); // HTTP Status 200を返します。 ctx.status(200); }); }
- (1) : ObjectMapperのインスタンスをカスタマイズします。
- (2) : カスタマイズしたインスタンスをJavalinJacksonクラスを使って適用します。
- (3) : jsonを返すGETリクエストハンドラを追加します。
- (4) : jsonを受け取るPOSTリクエストハンドラを追加します。
静的ファイルを配信する
クラスパス上に配置したファイルを配信するにはJavalinConfigで設定を行います。
[project_root] | `--- /src | `--- /main | `--- /resources | `--- /public | `--- index.html上記の場所に配置した
index.htmlを配信するにはJavalinConfig.addStaticFilesでパスを指定します。configurationconfig.addStaticFiles("/public");『最小構成でHello World』のアプリケーションで追加した
/に対するGETリクエストハンドラとパスが被るので、以下のコードをコメントアウトします。// app.get("/", ctx -> ctx.result("Hello World"));この状態でJavalinアプリケーションを起動し、
http://localhost:7000/index.htmlまたはhttp://localhost:7000にアクセスするとpublic/index.htmlが返されます。また、クラスパス外にあるディレクトリを扱うこともできます。
例えば以下のようなC:\var\staticディレクトリ以下のファイルを扱いたい場合は、C:\var | `--- \static | `--- \images | `--- sample_1.jpgJavalinConfig.addStaticFilesで絶対パスを指定します。なお、ここで指定したパスにアクセスできないとアプリケーション起動時にエラーが発生します。
configurationconfig.addStaticFiles("C:\\var\\static", Location.EXTERNAL);htmlファイルで下記のようにパスを指定します。
<img src="/images/sample_1.jpg" />WebJarsを利用する
pom.xmlの依存関係に利用したいライブラリを追加します。
この例ではjQueryを利用しました。pom.xml<dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.4.1</version> </dependency>これだけではjQueryは有効になりません。Webjarsをアプリケーションで利用できるようにするためにJavalinConfig.enableWebjarsで有効化します。
configurationconfig.enableWebjars();htmlファイルで下記のようにパスを指定します。
<script src="/webjars/jquery/3.4.1/jquery.min.js"></script>ロギングにlogbackを利用する
ロギングにslf4jとlogbackを利用します。
slf4j-apiはすでにJavalinが依存しているので追加するのはlogback-classicだけになります。
また最初に追加していたslf4j-simpleはコメントアウトします。pom.xml<!-- <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.26</version> </dependency> --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> <scope>runtime</scope> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> </exclusions> </dependency>logbackの設定ファイルlogback.xmlを以下の場所に作成します。
[project_root] | `--- /src | `--- /main | `--- /resources | `--- logback.xmllogback.xml
logback.xml<?xml version="1.0" encoding="UTF-8"?> <configuration status="INFO"> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <Pattern>%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Pattern> </encoder> </appender> <logger name="com.example.demo" level="debug" additivity="false"> <appender-ref ref="CONSOLE"/> </logger> <logger name="org.jooq" level="info" additivity="false"> <appender-ref ref="CONSOLE"/> </logger> <logger name="org.thymeleaf" level="info" additivity="false"> <appender-ref ref="CONSOLE"/> </logger> <root level="info"> <appender-ref ref="CONSOLE" /> </root> </configuration>アプリケーションログの出力
ログを出力したいクラスに以下のようにロガーを定義します。
import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class App { static final Logger LOG = LoggerFactory.getLogger(App.class); // ...省略... }テンプレートエンジンにThymeleafを利用する
JavalinではVelocity、Freemarker、Thymeleafなどのテンプレートエンジンを簡単に利用できるようになっています。
この例ではThymeleafを利用します。pom.xml<dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf</artifactId> <version>3.0.9.RELEASE</version> </dependency>以下の場所に
hello_world.htmlというテンプレートファイルを配置します。同じファイル名のプロパティファイルはメッセージリソースです。[project_root] | `--- /src | `--- /main | `--- /resources | `--- /WEB-INF | `--- /templates | `--- hello_world.html `--- hello_world.properties `--- hello_world_ja.properties `--- hello_world_en.propertieshello_world.html
<!doctype html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <script th:src="@{/webjars/jquery/3.4.1/jquery.min.js}"></script> <title>Hello World</title> </head> <body> <div> <p th:text="${message}">message</p> <p th:text="${now}">now</p> <p th:text="#{msg.hello}">message</p> </div> </body> </html>メッセージリソースの定義
hello_world.propertiesmsg.hello = Hello World (default)hello_world_en.propertiesmsg.hello = Hello Worldhello_world_ja.propertiesmsg.hello = ハローワールドテンプレートエンジンを利用してレスポンスするにはrenderメソッドを使用します。
なお、どのテンプレートエンジンが利用されるかはテンプレートファイルの拡張子で決まります。Thymeleafの場合は.html、.tl、.thyme、.thymeleafが該当します。app.get("/hello", ctx -> { Map<String, Object> model = new HashMap<>(); model.put("message", "Hello World"); model.put("now", LocalDateTime.now()); ctx.render("/WEB-INF/templates/hello_world.html", model); });
localhost:7000/helloにアクセスするとテンプレートエンジンで描画されたhtmlページが表示されます。ブラウザのAccept-Languageでメッセージを切り替える
デフォルトでは、アプリケーション起動時のデフォルトロケールでメッセージリソースが決まります。
ある言語のメッセージリソースを利用するには、Javalinアプリケーション起動時のJVMオプションにロケールを指定します。
例えばenのメッセージリソースを利用するには以下のオプションを指定します。> java -Duser.language=en -Duser.country=US -jar <jarファイル>Spring BootのようにブラウザのAccept-Languageに対応するメッセージリソースを利用するには、FileRendererを実装したクラスを作ります。
CustomThymeleafRendererimport io.javalin.http.Context; import io.javalin.plugin.rendering.FileRenderer; import org.jetbrains.annotations.NotNull; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.WebContext; import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect; import org.thymeleaf.templatemode.TemplateMode; import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; import org.thymeleaf.templateresolver.ITemplateResolver; import java.util.Map; import java.util.concurrent.TimeUnit; public class CustomThymeleafRenderer implements FileRenderer { private final TemplateEngine templateEngine; public CustomThymeleafRenderer() { templateEngine = templateEngine(); } @Override public String render(@NotNull String filePath, @NotNull Map<String, Object> model, @NotNull Context ctx) { WebContext context = new WebContext(ctx.req, ctx.res, ctx.req.getServletContext(), ctx.req.getLocale()); context.setVariables(model); return templateEngine.process(filePath, context); } private TemplateEngine templateEngine() { TemplateEngine templateEngine = new TemplateEngine(); templateEngine.setTemplateResolver(templateResolver()); templateEngine.addDialect(new Java8TimeDialect()); return templateEngine; } private ITemplateResolver templateResolver() { ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver(); templateResolver.setPrefix("/WEB-INF/templates/"); templateResolver.setSuffix(".html"); templateResolver.setCharacterEncoding("UTF-8"); templateResolver.setCacheable(true); templateResolver.setCacheTTLMs(TimeUnit.MINUTES.toMillis(30)); templateResolver.setTemplateMode(TemplateMode.HTML); return templateResolver; } }このCustomThymeleafRendererをJavalinアプリケーションに登録します。
JavalinRenderer.register(new CustomThymeleafRenderer(), ".html");このカスタマイズを行うと、テンプレートファイルのパスを指定する必要は無くなるのでハンドラの実装は以下のようになります。
// ctx.render("/WEB-INF/templates/hello_world.html", model); ctx.render("hello_world.html", model);ちなみにデフォルトで登録されているrendererは以下のとおりです。
JavalinRendererobject JavalinRenderer { init { register(JavalinVelocity, ".vm", ".vtl") register(JavalinFreemarker, ".ftl") register(JavalinMustache, ".mustache") register(JavalinJtwig, ".jtwig", ".twig", ".html.twig") register(JavalinPebble, ".peb", ".pebble") register(JavalinThymeleaf, ".html", ".tl", ".thyme", ".thymeleaf") register(JavalinCommonmark, ".md", ".markdown") } // ...省略... }データベースを利用する
データベースにMySQL 8.0、ORMにjOOQ、コネクションプールにHikariCPを利用しました。
※2019年8月現在、JavalinにはjOOQに関するプラグインやユーティリティ、コンフィグレーションクラスはないようです。なのでjOOQによるデータベースアクセス周りの実装は自前で行う必要があります。
データベースの準備
Javalinアプリケーションで使用する
sample_dbデータベースとtest_userユーザーを管理者権限を持つユーザーで作成します。CREATE DATABASE IF NOT EXISTS sample_db CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ; CREATE USER IF NOT EXISTS 'test_user'@'localhost' IDENTIFIED BY 'test_user' PASSWORD EXPIRE NEVER ; GRANT ALL ON sample_db.* TO 'test_user'@'localhost';次に
test_userユーザーでuserテーブルを作成します。DROP TABLE IF EXISTS user; CREATE TABLE user ( id BIGINT AUTO_INCREMENT COMMENT 'ユーザーID', nick_name VARCHAR(60) NOT NULL COMMENT 'ニックネーム', sex CHAR(1) NOT NULL COMMENT '性別 M:男性 F:女性', prefecture_id TINYINT(1) NOT NULL DEFAULT 0 COMMENT '都道府県 0:不明、1:北海道 - 8:九州・沖縄', email VARCHAR(120) COMMENT 'メールアドレス', memo TEXT COMMENT '備考欄', create_at DATETIME NOT NULL DEFAULT NOW(), update_at DATETIME NOT NULL DEFAULT NOW(), PRIMARY KEY (id) ) ENGINE = INNODB DEFAULT CHARSET = UTF8MB4 COMMENT = 'ユーザーテーブル'; CREATE INDEX idx_sex on user (sex) USING BTREE; CREATE INDEX idx_pref on user (prefecture_id) USING BTREE;依存関係を追加
pom.xmlを編集して以下の依存関係を追加します。
pom.xml<dependency> <groupId>org.jooq</groupId> <artifactId>jooq</artifactId> <version>3.11.11</version> </dependency> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>3.3.1</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.17</version> <scope>runtime</scope> </dependency>コードの自動生成
jOOQには
jooq-codegen-mavenプラグインというデータベーススキーマからモデル(エンティティ)のコードを自動生成する機能があります。
- Running the code generator with Maven
- Configuration and setup of the generator
- jooq-codegen-3.11.0.xsd
<plugin> <groupId>org.jooq</groupId> <artifactId>jooq-codegen-maven</artifactId> <version>3.11.11</version> <executions> <execution> <id>jooq-codegen</id> <phase>generate-sources</phase> <goals> <goal>generate</goal> </goals> </execution> </executions> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.17</version> </dependency> </dependencies> <configuration> <jdbc> <url>jdbc:mysql://localhost:3306</url> <user>test_user</user> <password>test_user</password> </jdbc> <generator> <database> <name>org.jooq.meta.mysql.MySQLDatabase</name> <includes>.*</includes> <inputSchema>sample_db</inputSchema> <!-- Configure type overrides for generated fields, attributes, sequences, parameters. --> <forcedTypes> <forcedType> <!--Specify the Java type of your custom type. This corresponds to the Converter's <U> type. --> <userType>com.example.demo.converter.Prefecture</userType> <!-- A converter implementation for the {@link #getUserType(). --> <converter>com.example.demo.converter.PrefectureConverter</converter> <!-- regex to match the column name --> <expression>PREFECTURE_ID</expression> <types>.*</types> </forcedType> <forcedType> <userType>com.example.demo.converter.Sex</userType> <converter>com.example.demo.converter.SexConverter</converter> <expression>SEX</expression> <types>.*</types> </forcedType> </forcedTypes> </database> <target> <packageName>com.example.demo.model</packageName> <directory>src/main/java</directory> </target> <generate> <!-- A flag indicating whether Java 8's java.time types should be used by the source code generator, rather than JDBC's java.sql types. --> <javaTimeTypes>true</javaTimeTypes> <!-- Generate index information --> <indexes>true</indexes> <!-- Primary key / foreign key relations should be generated and used. This is a prerequisite for various advanced features --> <relations>true</relations> <!-- Generate deprecated code for backwards compatibility --> <deprecated>false</deprecated> <!-- Generate the {@link javax.annotation.Generated} annotation to indicate jOOQ version used for source code --> <generatedAnnotation>false</generatedAnnotation> <!-- Generate Sequence classes --> <sequences>true</sequences> <!-- Generate Key classes --> <keys>true</keys> <!-- Generate Table classes --> <tables>true</tables> <!-- Generate TableRecord classes --> <records>true</records> <!-- Generate POJOs --> <pojos>true</pojos> <!-- Generate basic equals() and hashCode() methods in POJOs --> <pojosEqualsAndHashCode>true</pojosEqualsAndHashCode> <!-- Generate basic toString() methods in POJOs --> <pojosToString>true</pojosToString> <!-- Generate serializable POJOs --> <serializablePojos>true</serializablePojos> <!-- Generated interfaces to be implemented by records and/or POJOs --> <interfaces>false</interfaces> <!-- Turn off generation of all global object references --> <globalObjectReferences>true</globalObjectReferences> <!-- Turn off generation of global catalog references --> <globalCatalogReferences>true</globalCatalogReferences> <!-- Turn off generation of global schema references --> <globalSchemaReferences>true</globalSchemaReferences> <!-- Turn off generation of global table references --> <globalTableReferences>true</globalTableReferences> <!-- Turn off generation of global sequence references --> <globalSequenceReferences>true</globalSequenceReferences> <!-- Turn off generation of global UDT references --> <globalUDTReferences>true</globalUDTReferences> <!-- Turn off generation of global routine references --> <globalRoutineReferences>true</globalRoutineReferences> <!-- Turn off generation of global queue references --> <globalQueueReferences>true</globalQueueReferences> <!-- Turn off generation of global database link references --> <globalLinkReferences>true</globalLinkReferences> <!-- Turn off generation of global key references --> <globalKeyReferences>true</globalKeyReferences> <!-- Generate fluent setters in records, POJOs, interfaces --> <fluentSetters>true</fluentSetters> </generate> </generator> </configuration> </plugin>以下のmvnコマンドでコード生成を行います。(mvn packageでも生成されます)
> mvn jooq-codegen:generateコードは
com.example.demo.modelパッケージに出力されます。今回の例では以下のコードが生成されました。[project_root] | `--- /src | `--- /main | `--- /java | `--- com.example.demo.model | | | `--- DefaultCatalog.java | `--- Indexes.java | `--- Keys.java | `--- SampleDb.java | `--- Tables.java | `--- com.example.demo.model.tables | | | `--- User.java // (1) | `--- com.example.demo.model.tables.records | | | `--- UserRecord.java // (2) | `--- com.example.demo.model.tables.pojos | `--- User.java // (3)
- (1) : userテーブルに対するエンティティクラスです。
- jOOQで操作するためのメソッドが備わっています。
- (2) : userテーブルのカラムに対するエンティティレコードクラスです。
- jOOQで操作するためのメソッドが備わっています。
- (3) : userテーブルに対するPojoクラス(オプション)です。この記事ではjOOQで検索したエンティティはPojoへ変換して呼び出し元(コントローラ)へ返すように実装しました。
Javalinアプリケーション側の実装
接続プロパティファイル
接続情報はプロパティファイルで管理します。
datasource.propertiesdatasource.jdbcUrl = jdbc:mysql://localhost:3306/sample_db datasource.userName = test_user datasource.password = test_userDbPropertyLoaderimport java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.util.Properties; public class DbPropertyLoader { private final Properties prop; private static final String JDBC_URL = "datasource.jdbcUrl"; private static final String USER_NAME = "datasource.userName"; private static final String PASSWORD = "datasource.password"; public DbPropertyLoader(String fileName) { prop = loadProperties(fileName); } public String getJdbcUrl() { return prop.getProperty(JDBC_URL); } public String getUserName() { return prop.getProperty(USER_NAME); } public String getPassword() { return prop.getProperty(PASSWORD); } private Properties loadProperties(String fileName) { try (InputStream stream = getClass().getClassLoader().getResourceAsStream(fileName)) { if (stream == null) { throw new IOException("Not Found : " + fileName); } Properties prop = new Properties(); prop.load(stream); return prop; } catch (IOException e) { throw new UncheckedIOException(e); } } }DSLContextの作成
jOOQでデータベースアクセスするにはjOOQのDSLContextというクラスのインスタンスが必要です。
以下のようにDSLContextのインスタンスを保持するシングルトンのホルダークラスを実装しました。
なお、DSLContextはスレッドセーフです。DslConfigureimport org.jooq.Configuration; import org.jooq.ConnectionProvider; import org.jooq.DSLContext; import org.jooq.SQLDialect; import org.jooq.TransactionProvider; import org.jooq.conf.Settings; import org.jooq.impl.DefaultConfiguration; import javax.sql.DataSource; import java.util.Optional; public interface DslConfigure { DSLContext getDslContext(); DataSource getDataSource(); SQLDialect getSqlDialect(); Settings getSettings(); // optional settings Optional<ConnectionProvider> getConnectionProvider(); Optional<TransactionProvider> getTransactionProvider(); default Configuration configuration() { Configuration config = new DefaultConfiguration(); config .set(getSqlDialect()) .set(getSettings()); getConnectionProvider().ifPresentOrElse(cp -> { config.set(cp); getTransactionProvider().ifPresent(tp -> { config.set(tp); }); }, () -> { config.set(getDataSource()); }); return config; } }DslContextHolderimport com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.jooq.ConnectionProvider; import org.jooq.DSLContext; import org.jooq.SQLDialect; import org.jooq.TransactionProvider; import org.jooq.conf.Settings; import org.jooq.conf.StatementType; import org.jooq.impl.DSL; import org.jooq.impl.DataSourceConnectionProvider; import org.jooq.impl.DefaultTransactionProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.sql.DataSource; import java.io.Serializable; import java.util.Optional; public class DslContextHolder implements DslConfigure, Serializable { private static final long serialVersionUID = -8276859195238889686L; private static final Logger LOG = LoggerFactory.getLogger(DslContextHolder.class); private static DslContextHolder instance; private final DSLContext dslContext; private final DataSource dataSource; private final Settings settings; private final ConnectionProvider connectionProvider; private final TransactionProvider transactionProvider; private DslContextHolder() { dataSource = createDataSource(); settings = createSettings(); connectionProvider = new DataSourceConnectionProvider(dataSource); transactionProvider = new DefaultTransactionProvider(connectionProvider, true); // transactionProvider = new ThreadLocalTransactionProvider(connectionProvider, true); dslContext = DSL.using(configuration()); } public static DslContextHolder getInstance() { if (instance == null) { synchronized (DslContextHolder.class) { if (instance == null) { instance = new DslContextHolder(); LOG.debug("DSL Context create : {}", instance.getDslContext().toString()); } } } return instance; } public static void destroy() { if (instance != null) { synchronized (DslContextHolder.class) { if (instance != null) { LOG.debug("DSL Context destroy : {}", instance.getDslContext().toString()); instance.dslContext.close(); instance = null; } } } } DataSource createDataSource() { DbPropertyLoader prop = new DbPropertyLoader("datasource.properties"); HikariConfig config = new HikariConfig(); config.setJdbcUrl(prop.getJdbcUrl()); config.setUsername(prop.getUserName()); config.setPassword(prop.getPassword()); config.setPoolName("hikari-cp"); config.setAutoCommit(false); config.setConnectionTestQuery("select 1"); config.setMaximumPoolSize(10); DataSource dataSource = new HikariDataSource(config); return dataSource; } Settings createSettings() { return new Settings() .withStatementType(StatementType.STATIC_STATEMENT) .withQueryTimeout(10) .withMaxRows(1000) .withFetchSize(20) .withExecuteLogging(true); } @Override public DSLContext getDslContext() { return this.dslContext; } @Override public DataSource getDataSource() { return this.dataSource; } @Override public SQLDialect getSqlDialect() { return SQLDialect.MYSQL_8_0; } @Override public Settings getSettings() { return this.settings; } @Override public Optional<ConnectionProvider> getConnectionProvider() { // return Optional.ofNullable(this.connectionProvider); } @Override public Optional<TransactionProvider> getTransactionProvider() { // return Optional.ofNullable(this.transactionProvider); } }サービスの実装例
jOOQのDSLContextと自動生成したコード(エンティティクラス)を利用してデータベースアクセスを行うサービスクラスの実装例です。
※通常はビジネスロジックを記述することろですが、サンプルコードなのでリポジトリのような実装になってしまっています。UserServiceimport com.example.demo.model.tables.pojos.User; import java.util.List; import java.util.Optional; public interface UserService { Optional<User> findById(Long id); List<User> findByNickName(String nickName); List<User> findAll(); User save(User user); void remove(Long id); }UserServiceImplimport com.example.demo.AppServiceException; import com.example.demo.model.tables.pojos.User; import com.example.demo.model.tables.records.UserRecord; import com.example.demo.service.UserService; import org.jooq.DSLContext; import org.jooq.Result; import org.jooq.impl.DSL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import static com.example.demo.model.Tables.USER; public class UserServiceImpl implements UserService { private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class); private final DSLContext dsl; public UserServiceImpl(DSLContext dsl) { this.dsl = dsl; } @Override public Optional<User> findById(Long id) { LOG.debug("find by Id : {}", id); UserRecord record = dsl .selectFrom(USER) .where(USER.ID.eq(id)) .fetchOne(); if (record != null) { return Optional.of(record.into(User.class)); } return Optional.empty(); } @Override public List<User> findByNickName(String nickName) { LOG.debug("find by nickName : {}", nickName); Result<UserRecord> result = dsl .selectFrom(USER) .where(USER.NICK_NAME.contains(nickName)) .orderBy(USER.NICK_NAME.asc()) .fetch(); return result.into(User.class); } @Override public List<User> findAll() { LOG.debug("findAll"); Result<UserRecord> result = dsl .selectFrom(USER) .orderBy(USER.ID.desc()) .fetch(); return result.into(User.class); } @Override public User save(User user) { LOG.debug("save : {}", user); UserRecord result = dsl.transactionResult(conf -> { if (user.getId() == null || user.getId().equals(0L)) { return DSL.using(conf) .insertInto(USER, USER.NICK_NAME, USER.SEX, USER.PREFECTURE_ID, USER.EMAIL, USER.MEMO) .values( user.getNickName(), user.getSex(), user.getPrefectureId(), user.getEmail(), user.getMemo()) .returning() .fetchOne(); } else { return DSL.using(conf) .update(USER) .set(USER.NICK_NAME, user.getNickName()) .set(USER.SEX, user.getSex()) .set(USER.PREFECTURE_ID, user.getPrefectureId()) .set(USER.EMAIL, user.getEmail()) .set(USER.MEMO, user.getMemo()) .set(USER.UPDATE_AT, LocalDateTime.now()) .where(USER.ID.eq(user.getId())) .returning() .fetchOne(); } }); LOG.debug("save result : {}", result); if (result == null) { throw new AppServiceException("[save] Not Found saved user record."); } return result.into(User.class); } @Override public void remove(Long id) { LOG.debug("remove by id : {}", id); int result = dsl.transactionResult(conf -> { int count = DSL.using(conf) .selectCount() .from(USER) .where(USER.ID.eq(id)) .execute(); if (count != 1) { return count; } return DSL.using(conf) .deleteFrom(USER) .where(USER.ID.eq(id)) .execute(); }); LOG.debug("remove result : {}", result); if (result == 0) { throw new AppServiceException("[remove] Not Found delete user record."); } } }コントローラの実装例
上記のサービスを利用するコントローラの実装例です。
UserControllerimport com.example.demo.model.tables.pojos.User; import com.example.demo.service.UserService; import io.javalin.Javalin; import io.javalin.apibuilder.ApiBuilder; import io.javalin.http.Context; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; public class UserController { private static final Logger LOG = LoggerFactory.getLogger(UserController.class); private final UserService userService; public UserController(UserService userService) { this.userService = userService; } void findById(@NotNull Context ctx) { Long id = ctx.pathParam("id", Long.class).get(); userService.findById(id).ifPresentOrElse(user -> { ctx.json(user); },() -> { ctx.status(404); }); } void findAll(@NotNull Context ctx) { List<User> users = userService.findAll(); ctx.json(users); } void store(@NotNull Context ctx) { User user = ctx.bodyAsClass(User.class); User storedUser = userService.save(user); ctx.json(storedUser); } void remove(@NotNull Context ctx) { Long id = ctx.pathParam("id", Long.class).get(); userService.remove(id); ctx.status(200); } public void bindEndpoints(@NotNull Javalin app) { app.routes(() -> { ApiBuilder.path("api/user", () -> { // GET /api/user ApiBuilder.get(this::findAll); // GET /api/user/:id ApiBuilder.get(":id", this::findById); // POST /api/user ApiBuilder.post(this::store); // DELETE /api/user/:id ApiBuilder.delete(":id", this::remove); }); }); } }ハンドラのバインドはJavalinアプリケーションの起動ポイントで行いました。
public static void main(String... args) { Javalin app = Javalin .create(config -> { // configuration }) .start(7000); // ... 省略 ... // DSLContextの生成 final DSLContext dsl = DslContextHolder.getInstance().getDslContext(); // アプリケーションにハンドラをバインドする new UserController(new UserServiceImpl(dsl)).bindEndpoints(app); // ... 省略 ... }テストコード
テスティングフレームワークにJUnit 5、アサーションにAssertJを利用しました。
依存関係を追加
pom.xmlを編集して以下の依存関係を追加します。
pom.xml<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.5.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <!--<version>1.4.199</version>--> <version>1.4.193</version> <scope>test</scope> </dependency>サービスクラスのテスト
サービスクラスのテストは単体テストとして実装します。サービスが依存する外部リソースのデータベースにはH2を使用します。
テスト開始前にスキーマを作成、テストメソッド毎にテストデータをロード、テスト終了後にスキーマを削除するという方法でテスト環境を準備します。テスト用の接続プロパティファイル
test-datasource.properties#datasource.driver = org.h2.Driver datasource.jdbcUrl = jdbc:h2:mem:sample_db;MODE=MySQL;DB_CLOSE_DELAY=-1; #datasource.jdbcUrl = jdbc:h2:tcp://localhost:9092/./sample_db;MODE=MySQL;DATABASE_TO_UPPER=false datasource.userName = sa datasource.password =テスト用DSLContextの作成
H2用のDSLContextを作成するコードを実装します。
TestDslContextHolderimport com.example.demo.config.DbPropertyLoader; import com.example.demo.config.DslConfigure; import org.h2.jdbcx.JdbcDataSource; import org.jooq.ConnectionProvider; import org.jooq.DSLContext; import org.jooq.SQLDialect; import org.jooq.TransactionProvider; import org.jooq.conf.Settings; import org.jooq.conf.StatementType; import org.jooq.impl.DSL; import org.jooq.impl.DataSourceConnectionProvider; import org.jooq.impl.DefaultTransactionProvider; import javax.sql.DataSource; import java.io.Serializable; import java.util.Optional; public class TestDslContextHolder implements DslConfigure, Serializable { private static final long serialVersionUID = -5728144007357037196L; private static TestDslContextHolder instance; private final DSLContext dslContext; private final DataSource dataSource; private final Settings settings; private final ConnectionProvider connectionProvider; private final TransactionProvider transactionProvider; public TestDslContextHolder() { dataSource = createDataSource(); settings = createSettings(); connectionProvider = new DataSourceConnectionProvider(dataSource); transactionProvider = new DefaultTransactionProvider(connectionProvider, true); // transactionProvider = new ThreadLocalTransactionProvider(connectionProvider, true); dslContext = DSL.using(configuration()); } public static TestDslContextHolder getInstance() { if (instance == null) { synchronized (TestDslContextHolder.class) { if (instance == null) { instance = new TestDslContextHolder(); } } } return instance; } public static void destroy() { if (instance != null) { synchronized (TestDslContextHolder.class) { if (instance != null) { instance.dslContext.close(); instance = null; } } } } DataSource createDataSource() { DbPropertyLoader prop = new DbPropertyLoader("test-datasource.properties"); JdbcDataSource dataSource = new JdbcDataSource(); dataSource.setURL(prop.getJdbcUrl()); dataSource.setUser(prop.getUserName()); dataSource.setPassword(prop.getPassword()); return dataSource; } Settings createSettings() { return new Settings() .withStatementType(StatementType.STATIC_STATEMENT) .withQueryTimeout(10) .withMaxRows(1000) .withFetchSize(20) .withExecuteLogging(true) .withDebugInfoOnStackTrace(true) .withRenderFormatted(true); } @Override public DSLContext getDslContext() { return this.dslContext; } @Override public DataSource getDataSource() { return this.dataSource; } @Override public SQLDialect getSqlDialect() { return SQLDialect.H2; } @Override public Settings getSettings() { return this.settings; } @Override public Optional<ConnectionProvider> getConnectionProvider() { // return Optional.ofNullable(this.connectionProvider); } @Override public Optional<TransactionProvider> getTransactionProvider() { // return Optional.ofNullable(this.transactionProvider); } }サービスのテストクラス
このコードはインメモリのH2を使用するパターンです。(サーバーモードのH2を利用するパターンは後述します。)TestServiceimport org.jooq.DSLContext; import org.jooq.Queries; import org.jooq.Query; import org.jooq.Schema; import org.jooq.Table; import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; import java.util.List; public interface TestService { DSLContext getDSLContext(); void setDSLContext(DSLContext dsl); default void createSchema(Schema schema) { Queries queries = getDSLContext().ddl(schema); for (Query query : queries.queries()) { getDSLContext().execute(query); } } default void dropSchema(Schema schema) { List<Table<?>> tables = schema.getTables(); for (Table<?> table : tables) { getDSLContext().dropTableIfExists(table).execute(); } getDSLContext().dropSchemaIfExists(schema).execute(); } default void cleanUp(List<Table<?>> tables) { getDSLContext().transaction(conf -> { for (Table table : tables) { DSL.using(conf).deleteFrom(table).execute(); } }); } default void testDataRollback() { throw new DataAccessException("rollback"); } default void isFailed(RuntimeException e) { if (!e.getMessage().equals("rollback")) { System.err.println(e); throw e; } } }UserServiceImplTestsimport com.example.demo.TestDslContextHolder; import com.example.demo.converter.Prefecture; import com.example.demo.converter.Sex; import com.example.demo.model.tables.pojos.User; import com.example.demo.model.tables.records.UserRecord; import com.example.demo.service.TestService; import com.example.demo.service.UserService; import org.assertj.core.groups.Tuple; import org.jooq.DSLContext; import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static com.example.demo.model.SampleDb.SAMPLE_DB; import static com.example.demo.model.Tables.*; @TestInstance(value = TestInstance.Lifecycle.PER_CLASS) public class UserServiceImplTests implements TestService { private DSLContext dsl; @Override public DSLContext getDSLContext() { return dsl; } @Override public void setDSLContext(DSLContext dsl) { this.dsl = dsl; } @BeforeAll public void setupSchema() { System.out.println("setup class"); setDSLContext(TestDslContextHolder.getInstance().getDslContext()); createSchema(SAMPLE_DB); } @AfterAll public void tearDownSchema() { System.out.println("teardown class"); dropSchema(SAMPLE_DB); TestDslContextHolder.destroy(); } @BeforeEach public void setup() { System.out.println("setup"); } @AfterEach public void tearDown() { System.out.println("teardown"); cleanUp(List.of(USER)); } @Test public void findById() { System.out.println("findById"); try { dsl.transaction(conf -> { // setup test data DSL.using(conf) .insertInto(USER, USER.ID, USER.NICK_NAME, USER.SEX, USER.PREFECTURE_ID, USER.EMAIL, USER.MEMO) .values(1L, "nick name AAA", Sex.FEMALE, Prefecture.HOKKAIDO, "test.user_a@example.com", "memo aaa") .values(2L, "nick name BBB", Sex.MALE, Prefecture.TOHOKU, "test.user_b@example.com", null) .values(3L, "nick name CCC", Sex.FEMALE, Prefecture.KANTOU, "test.user_c@example.com", "memo ccc") .execute(); // exercise UserService sut = new UserServiceImpl(DSL.using(conf)); Optional<User> actual = sut.findById(1L); // verify assertThat(actual.isPresent()).isTrue(); actual.ifPresent(user -> { assertThat(user) .extracting( "id", "nickName", "sex", "prefectureId", "email", "memo") .contains( 1L, "nick name AAA", Sex.FEMALE, Prefecture.HOKKAIDO, "test.user_a@example.com", "memo aaa"); }); testDataRollback(); }); } catch (DataAccessException e) { isFailed(e); } } @Test public void findByNickName() { System.out.println("findByNickName"); try { dsl.transaction(conf -> { // setup test data DSL.using(conf) .insertInto(USER, USER.ID, USER.NICK_NAME, USER.SEX, USER.PREFECTURE_ID, USER.EMAIL, USER.MEMO) .values(1L, "nick name AAA", Sex.FEMALE, Prefecture.HOKKAIDO, "test.user_a@example.com", "memo aaa") .values(2L, "nick name BBB", Sex.MALE, Prefecture.TOHOKU, "test.user_b@example.com", null) .values(3L, "nick name CCC", Sex.FEMALE, Prefecture.KANTOU, "test.user_c@example.com", "memo ccc") .execute(); // exercise UserService sut = new UserServiceImpl(DSL.using(conf)); List<User> actual = sut.findByNickName("BBB"); // verify assertThat(actual).hasSize(1); assertThat(actual) .extracting( "id", "nickName", "sex", "prefectureId", "email", "memo") .contains(Tuple.tuple(2L, "nick name BBB", Sex.MALE, Prefecture.TOHOKU, "test.user_b@example.com", null)); testDataRollback(); }); } catch (DataAccessException e) { isFailed(e); } } @Test public void save() { try { dsl.transaction(conf -> { // exercise User testData = new User(null, "nick name DDD", Sex.MALE, Prefecture.KANTOU, "test.user_d@example.com", null, null, null); UserService sut = new UserServiceImpl(DSL.using(conf)); User expected = sut.save(testData); // verify Optional<User> actual = sut.findById(expected.getId()); assertThat(actual.isPresent()).isTrue(); actual.ifPresent(user -> { assertThat(user) .extracting( "id", "nickName", "sex", "prefectureId", "email", "memo") .contains( expected.getId(), expected.getNickName(), expected.getSex(), expected.getPrefectureId(), expected.getEmail(), expected.getMemo()); }); testDataRollback(); }); } catch (DataAccessException e) { isFailed(e); } } @Test public void remove() { try { dsl.transaction(conf -> { // setup Long userId = 1L; DSL.using(conf) .insertInto(USER, USER.ID, USER.NICK_NAME, USER.SEX, USER.PREFECTURE_ID, USER.EMAIL, USER.MEMO) .values( userId, "nick name EEE", Sex.MALE, Prefecture.CHUBU, "test.user_e@example.com", "memo eee") .execute(); // exercise UserService sut = new UserServiceImpl(DSL.using(conf)); sut.remove(userId); // verify UserRecord actual = DSL.using(conf) .selectFrom(USER) .where(USER.ID.eq(userId)) .fetchOne(); assertThat(actual).isNull(); testDataRollback(); }); } catch (DataAccessException e) { isFailed(e); } } }H2をサーバモードで使用する
サーバーモードではスキーマやテーブル、テストデータは以下の場所に配置したSQLスクリプトを実行して作成します。
テストを実行すると、H2データベースのデータベースファイルがsample_db.mv.dbファイルとして作成されます。[project_root] | `--- /h2 | `--- /sql | | | `--- 01_schema.sql //スキーマを作成する | `--- 02_table.sql //テーブルを作成する | `--- 03_data.sql //データを挿入する | `--- 04_table.sql //テーブルを削除する | `--- 05_schema.sql //スキーマを削除する | `--- sample_db.mv.dbTestH2Serverimport com.example.demo.config.DbPropertyLoader; import org.h2.tools.RunScript; import org.h2.tools.Server; import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; public class TestH2Server implements AutoCloseable { private final Server server; private final String jdbcUrl; private final String userName; private final String password; public TestH2Server() { DbPropertyLoader prop = new DbPropertyLoader("test-datasource.properties"); jdbcUrl = prop.getJdbcUrl(); userName = prop.getUserName(); password = prop.getPassword(); String[] params = new String[] {"-tcp", "-tcpPort", "9092", "-baseDir", "./h2"}; try { server = Server.createTcpServer(params); } catch (SQLException e) { System.err.println(e.getMessage()); throw new RuntimeException(e); } } public void start() { System.out.println("server start"); try { server.start(); status(); Runtime.getRuntime().addShutdownHook(new Thread(() -> shutdown())); } catch (SQLException e) { throw new RuntimeException(e); } } public void runScript(List<String> scripts) { try { for (String script : scripts) { System.out.println("run script : " + script); RunScript.execute(jdbcUrl, userName, password, script, StandardCharsets.UTF_8, false); } } catch (SQLException e) { System.err.println("run script : " + e.getMessage()); throw new RuntimeException(e); } } public void shutdown() { if (server.isRunning(true)) { System.out.println("server shutdown"); server.shutdown(); } } public void status() { System.out.println(server.getStatus()); } // for test public static void main(String ... args) { try (TestH2Server h2 = new TestH2Server()) { h2.start(); h2.status(); h2.runScript(List.of("./h2/sql/01_schema.sql", "./h2/sql/02_table.sql", "./h2/sql/03_data.sql")); h2.test(); h2.runScript(List.of("./h2/sql/04_table.sql", "./h2/sql/05_schema.sql")); } System.exit(0); } public void test() { System.out.println("test"); try (Connection conn = DriverManager.getConnection(jdbcUrl, userName, password)) { PreparedStatement ps = conn.prepareStatement("select * from sample_db.user"); ResultSet rs = ps.executeQuery(); while (rs.next()) { System.out.println("id : " + rs.getLong("id")); System.out.println("nick_name : " + rs.getString("nick_name")); System.out.println("sex : " + rs.getString("sex")); System.out.println("prefecture_id : " + rs.getInt("prefecture_id")); System.out.println("email : " + rs.getString("email")); System.out.println("memo : " + rs.getString("memo")); System.out.println("createAt : " + rs.getTimestamp("create_at")); System.out.println("updateAt : " + rs.getTimestamp("update_at")); } } catch (SQLException e) { System.err.println("test error : " + e.getMessage()); throw new RuntimeException(e); } } @Override public void close() { shutdown(); } }サービスのテストクラスをH2サーバーを利用するように修正します。修正する箇所はsetupSchema、tearDownSchemaメソッドです。
@TestInstance(value = TestInstance.Lifecycle.PER_CLASS) public class UserServiceImplTests implements TestService { private DSLContext dsl; private TestH2Server h2; private List<String> initScripts = List.of( "./h2/sql/01_schema.sql", "./h2/sql/02_table.sql" ); private List<String> clearScripts = List.of( "./h2/sql/04_table.sql", "./h2/sql/05_schema.sql" ); @Override public DSLContext getDSLContext() { return dsl; } @Override public void setDSLContext(DSLContext dsl) { this.dsl = dsl; } @BeforeAll public void setupSchema() { System.out.println("setup class"); h2 = new TestH2Server(); h2.start(); h2.runScript(initScripts); setDSLContext(TestDslContextHolder.getInstance().getDslContext()); } @AfterAll public void tearDownSchema() { System.out.println("teardown class"); TestDslContextHolder.destroy(); h2.runScript(clearScripts); h2.shutdown(); } //... 省略... }コントローラクラスのテスト
コントローラクラスのテストは結合テストとして実装します。データベースは実環境と同じMySQLを使用するため、テスト実行時はMySQLサーバにアクセスできる必要があります。
コントローラのテストクラス
import com.example.demo.config.DslContextHolder; import com.example.demo.config.JacksonConfig; import com.example.demo.converter.Prefecture; import com.example.demo.converter.Sex; import com.example.demo.model.tables.pojos.User; import com.example.demo.service.impl.UserServiceImpl; import com.fasterxml.jackson.databind.ObjectMapper; import io.javalin.Javalin; import io.javalin.plugin.json.JavalinJackson; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import static org.assertj.core.api.Assertions.assertThat; @TestInstance(value = TestInstance.Lifecycle.PER_CLASS) public class UserControllerTests { Javalin app; int port; ObjectMapper objectMapper; @BeforeAll public void setup() { port = 7000; app = Javalin.create().start(port); objectMapper = JacksonConfig.getObjectMapper(); JavalinJackson.configure(objectMapper); } @AfterAll public void tearDown() { app.stop(); } @Test public void findById() throws Exception { // setup new UserController(new UserServiceImpl(DslContextHolder.getInstance().getDslContext())) .bindEndpoints(app); // exercise Long userId = 1L; String testTargetUrl = String.format("http://localhost:%d/api/user/%d", port, userId); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(testTargetUrl)) .header("Accept", "application/json") .GET() .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); // verify assertThat(response.statusCode()).isEqualTo(200); User actual = objectMapper.readValue(response.body(), User.class); assertThat(actual) .extracting( "id", "nickName", "sex", "prefectureId", "email", "memo") .contains( 1L, "test user 1", Sex.MALE, Prefecture.HOKKAIDO, "test.user1@example.com", "memo 1" ); } }
- 投稿日:2019-08-28T21:35:43+09:00
Spring がリクエストパラメータを処理する流れを理解したい
@ModelAttributeを利用したリクエストパラメータをオブジェクトにバインドする処理ってどうなってるんだっけ、と調べることがあったので結果をまとめておく。
あまりドキュメントにも記載がないことが多く、ソースコードを読んだ結果をまとめている。なお、
@RequestParamでもリクエストパラメータを取得できるが、今回は対象外。環境
- Spring Boot 2.1.6.RELEASE
@ModelAttributeが利用できる箇所
- メソッド
- Handler メソッドの引数
実は、Handler メソッドの引数には
@ModelAttribute付与しなくてもよかったりする。
デフォルトの挙動では、Handler メソッドの引数にBeanUtils.isSimplePropertyでfalseと判定されるクラスを指定すると、@ModelAttributeが付与された場合と同様の動作となる。
具体的には、プリミティブ型とそのラッパー、Enum、String、CharSequence、Number、Date、URI、URL、Locale、Class、配列、以外のクラス。
ただ、優先度は一番低いので、他のHandlerMethodArgumentResolverが引数を解決できるのであれば、そちらで解決される。
ModelAttributeMethodProcessorSpring では、
HandlerAdapterが Handler メソッドを呼び出す。いくつかの実装クラスがあるが、@RequestMapiingを利用している場合は、RequestMappingHandlerAdapterが Handler メソッドの呼び出しを担う。
このクラスが Handler メソッドの引数に渡す値を解決したり、戻り値をハンドリングしたりしていて、Handler メソッドの引数にModelやRedirectAttributesを指定するといい感じに処理できるのは、このクラスが頑張っているから。で、実際に引数に渡す値を解決するのは
HandlerMethodArgumentResolverの実装クラスの役割で、@ModelAttributeが指定された場合にはModelAttributeMethodProcessorが処理を行っている。メソッドに
@ModelAttributeを付与した場合の挙動ここは本題ではなかったが、
@ModelAttributeを指定したメソッドの動作もまとめておく。メソッドの戻り値を
Modelに格納する処理が Handler メソッドの実行前に処理が行われる。
(厳密にはModelAndViewContainer内のModelMapだが、ややこしくなりそうなのでModelということにしておく。)
例えば以下のような Controller を作ったとする。@Controller public class HelloController {} @ModelAttribute public User setUp() { return new User(); } @GetMapping("/") public String index() { return "hello"; } }その際の挙動は以下のようなイメージになる。(あくまでもイメージで厳密には違う。)
@Controller public class HelloController { @GetMapping("/") public String index(Model model) { model.addAttribute(new User()); return "hello"; } }以下のように
@ModelAttriubteに属性名を指定した場合は、@ModelAttribute("hoge") public User setUp() { return new User(); }
model.addAttribute()の第一引数に追加されるイメージ。@GetMapping("/") public String index(Model model) { model.addAttribute("hoge", new User()); return "index"; }ちなみに、
@ModelAttributeが付与されたメソッドを呼び出しているのは、ModelFactoryというクラス。Handler メソッドの引数に
@ModelAttributeを付与した場合の挙動ここからが本題。
オブジェクトの取得、生成
まず、
Modelから指定されたオブジェクトを取得する。@ModelAttributeにvalueornameを指定した場合はModelから取得する際の key を指定できる。省略した場合は、クラス名から Spring が自動的に決定する。
Modelから取得できるパターンはいくつかあり、前述の@ModelAttributeを付与したメソッドでModelに格納していたり、リダイレクト時に Flash スコープを利用していたり、@SessionAttributeで Session スコープを利用したりしていると、Handler メソッド実行前(というより引数に渡す値の解決前)にModelに格納されていることがある。
Modelからオブジェクトが取得できなかった場合は、オブジェクトを生成する。
コンストラクタが 1 つの場合はそれを利用し、複数ある場合はデフォルトコンストラクタを利用するが、このときデフォルトコンストラクタが無い場合は死ぬ。
引数ありコンストラクタを利用する場合は、リクエストパラメータをバインドする処理が挟まるが、これは後述。リクエストパラメータのバインド
取得 or 生成したオブジェクトに対して、リクエストパラメータの値をバインドする。
リクエストパラメータの名前と一致するオブジェクトのフィールドに対して値がバインドされる。オブジェクトの値を上書きするが、リクエストパラメータに含まれない値に対しては何も行わない。
@Controller public class HelloController { @ModelAttribute public User setUp() { User user = new User(); user.setName("なまえ"); user.setEmail("メール"); return user; } @GetMapping("/") public String index(User user) { return "hello"; } }
Userがnameと
で、以下のようにnameだけをリクエストパラメータに付与して送信する。curl http://localhost:8080?name=hogehoge
Userオブジェクトのnameはhogehogeに、メールとなり、nullで上書きされることはない。パラメータありのコンストラクタでオブジェクトを生成する場合
パラメータありのコンストラクタを用いてオブジェクトを生成する場合は、そのタイミングでデータのバインドを試みる。
この場合、フィールド名ではなくコンストラクタのパラメータ名 or@ConstructorPropertiesで指定された値と一致するリクエストパラメータをバインドする。public class User { private String name; private String email; public User(String n, String e) { this.name = n; this.email = e; } public String getName() { return name; } public String getEmail() { return email; } }上記のようなコンストラクタの場合、リクエストパラメータ名は
nやeにしないといけない。public class User { private String name; private String email; @ConstructorProperties({"name", "email"}) public User(String n, String e) { this.name = n; this.email = e; } public String getName() { return name; } public String getEmail() { return email; } }上記のように
@ConstructorPropertiesを使えば、リクエストパラメータ名はnameや@ConstructorPropertiesがある場合はそちらが優先されてしまうため、nやeでは動作しなくなる。ただ、オブジェクトの生成が完了した後に、通常と同様のリクエストパラメータをバインドする処理が実行されるので、setter がある場合や、ダイレクトフィールドアクセスが可能な
DataBinderを利用している場合は、そちらの結果が優先されてしまう。例えば、以下のようなクラスを作った場合、
public class User { private String name; private String email; public User(String name, String email) { this.name = name + "hoge"; this.email = email + "fuga"; } public String getName() { return name; } public String getEmail() { return email; } public void setName(String name) { this.name = name; } public void setEmail(String email) { this.email = email; } }コンストラクタでは、
hogeやfugaを付与してフィールドに保持しているが、その後 setter が実行されてしまうので、最終的にバインドされる値には、hogeやfugaが付与されていない。リクエストパラメータのバインドを抑止する
@ModelAttributeのbindingをfalseにすることで、リクエストパラメータのバインドを抑止することができる。@Controller public class HelloController { @ModelAttribute public User setUp() { User user = new User(); user.setName("なまえ"); user.setEmail("メール"); return user; } @GetMapping("/") public String index(@ModelAttribute(binding = false) User user) { return "hello"; } }上記のような状態で以下のリクエストを送信しても
Userのオブジェクトのnameはなまえ、メールのままとなる。curl http://localhost:8080?name=hogehoge&email=fugafugaバリデーション
引数に
@Validatedが付与されるなど、バリデーションが必要な場合は実施する。バリデーションの結果、エラーがある場合でかつ Handler メソッドの直後の引数に
Errorsがない場合、BindingExceptionがスローされ、ある場合はBindingResultとして結果を保持する。(ErrorsはBindingResultの親インターフェース)ちなみに Handler メソッドで
BindingResultを引数に設定する際に、順番に気を付けなければならないのは、ここの実装によるもの。話が横道にそれるが Handler メソッドの引数に
BindingResultを設定した場合、ErrorsMethodArgumentResolverというクラスが引数の値を解決する。
このクラスはModelに格納された最後の要素がBindingResultかどうかをチェックし、そうであれば引数に設定するという処理を行っている。
そのため、バリデーションを行ってBindingResultをModelに格納した直後に実行されないと上手く動作しない可能性がある。
なので、Handler メソッドの引数には@Validatedを付与した引数の直後にBindingResultが無いといけないっぽい。これを調べて思ったが、以下のようにリクエストパラメータをバインドするオブジェクトを 2 つに分けて、それぞれのバリデーション結果を取得するようなコードも実現できる。
Userのバリデーション結果はresult1に、OtherObjのバリデーション結果はresult2に格納される。@Controller public class HelloController { @GetMapping("/") public String index(@Validated User user, BindingResult result1, @Validated OtherObj other, BindingResult result2) { return "index"; } }パラメータありのコンストラクタを利用する場合
パラメータありのコンストラクタを用いる場合は、オブジェクトを生成するタイミングで必要に応じて型変換が行われる。
その際、型変換ができないフィールドが含まれていた場合はその結果をBindingResultとして保持するが、それ以降のバインド処理、バリデーションが実施されない。
つまり、setter によるリクエストパラメータのバインド処理や、Bean Validation によるフィールドチェックが実施されない。public class User { @NotEmpty private String name; private Integer age; public User(String name, Integer age) { this.name = name; this.age = age; } // getter 省略 } @Controller public class HelloController { @GetMapping("/") public String index(@Validated User user, BindingResult result) { return "index"; } }上記のようなクラスを作成した場合に、以下のようなリクエストを送る。
curl http://localhost:8080?age=hogehoge本来期待しているバリデーション結果は、
nameが空であること、ageがIntegerに変換できないことだが、後者しか検出することができない。
パラメータありのコンストラクタを削除し、デフォルトコンストラクタと setter を利用するように変更すると、どちらも検出できる。Model へ格納
上記までの処理で生成したオブジェクトと
BindingResultをModelに格納する。
@ModelAttributeで属性名が指定されている場合は、その値を key として利用する。さいごに
なるほどー。
パラメータありのコンストラクタを利用する場合に、微妙に動作が異なることがあるので注意しないといけない。
- 投稿日:2019-08-28T21:33:12+09:00
SpringBootのログをjson形式で出力する
参考
JSON Logging for Spring Boot
以降、上記の記事に沿って実装した内容を載せています
上記の記事の方が情報量が多いので英語が苦でなければそちらを参照することをお勧めします実行環境
- Java12
- SpirngBoot 2.1.7.RELEASE
- Gradle 5.6
準備
依存関係にlogstashを追加
build.gradledependencies { implementation 'net.logstash.logback:logstash-logback-encoder:6.1' }logbackの設定を追加
src/main/resources/logback-spring.xml<?xml version="1.0" encoding="UTF-8"?> <configuration> <property resource="application.yml" /> <contextName>${spring.application.name}</contextName> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <providers> <timestamp> <fieldName>timeStamp</fieldName> <timeZone>UTC</timeZone> </timestamp> <loggerName> <fieldName>logger</fieldName> </loggerName> <logLevel> <fieldName>level</fieldName> </logLevel> <threadName> <fieldName>thread</fieldName> </threadName> <mdc /> <message> <fieldName>message</fieldName> </message> <arguments> <includeNonStructuredArguments>false</includeNonStructuredArguments> </arguments> <stackTrace> <fieldName>stack</fieldName> </stackTrace> </providers> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE" /> </root> </configuration>実装
ControllerクラスでINFOログを出力する例
※Loggerの取得はlombokの@Slf4jアノテーションにおまかせTestController.javapackage com.exsample.controller; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import static net.logstash.logback.argument.StructuredArguments.v; @Slf4j @RestController @RequestMapping("/test") public class TestController { @GetMapping public void get() { MDC.put("MDC key","MDC value"); log.info("JSON Log",v("arg key","arg value")); } }結果
以下のログが出力される
{"timeStamp":"2019-08-28T08:52:22.173Z","logger":"com.exsample.controller.TestController","level":"INFO","thread":"main","MDC key":"MDC value","message":"JSON Log","arg key":"arg value"}補足
logback-spring.xmlの<encoder>タグ内で設定した内容が上から順に出力される(抜粋)logback-spring.xml<providers> <timestamp> <fieldName>timeStamp</fieldName> <timeZone>UTC</timeZone> </timestamp> <loggerName> <fieldName>logger</fieldName> </loggerName> <logLevel> <fieldName>level</fieldName> </logLevel> <threadName> <fieldName>thread</fieldName> </threadName> <mdc /> <message> <fieldName>message</fieldName> </message> <arguments> <includeNonStructuredArguments>false</includeNonStructuredArguments> </arguments> <stackTrace> <fieldName>stack</fieldName> </stackTrace> </providers>ログ内容{ "timeStamp": "2019-08-28T08:52:22.173Z", "logger": "com.exsample.controller.TestController", "level": "INFO", "thread": "main", "MDC key": "MDC value", "message": "JSON Log", "arg key": "arg value" }
MDC.put()した内容は<mdc />タグの位置に出力される(抜粋)TestController.javaMDC.put("MDC key","MDC value");ログ抜粋"MDC key": "MDC value"
log.info()の第一引数に渡した内容は<message>タグの位置に出力される(抜粋)TestController.javalog.info("JSON Log",v("arg key","arg value"));ログ抜粋"message": "JSON Log"
log.info()の第二引数以降にStructuredArguments.v(key,value)を渡すと、<arguments>タグの位置にその内容が出力される(抜粋)TestController.javalog.info("JSON Log",v("arg key","arg value"));ログ抜粋"arg key": "arg value"
- 投稿日:2019-08-28T20:18:58+09:00
型がある言語で DDD してたらテストはいらないの?
前にとある DDD の座談会で「静的言語で DDD をしているならテストはいらない?」という質問が出て、思ったより多くの人が「そうだ / そうかも」の空気になって驚いたので、ちょっと整理してみることにした。
結論から言うと、僕の持論は「もちろんコンパイルの恩恵はとてもあるんだけど、思考停止で全てが解決するわけではないし、学びも多いので、書いた方が良い」です。
条件分岐は型があってもバグになりやすい
例えば
else ifが増えるような修正は、型があるだけだと割と危険。Plan.javapublic enum Plan { PLAN_1, PLAN_2, PLAN_3; }Price.javapublic class Price { private final int value; public static Price of(Plan plan) { if (plan == Plan.PLAN_1) return new Price(100); else if (plan == Plan.PLAN_2) return new Price(200); else return new Price(300); } }これに
PLAN_4を足した。値段は 400 円にしたかったけど、
else ifを書き足し忘れた。public enum Plan { PLAN_1, PLAN_2, PLAN_3, + PLAN_4; }当然コンパイルは通り、
PLAN_4は 300 円になる。怖い。
とても怖い。余談
くだらない例だけど、冗談ではないよ。
Planに応じて値段、明細品目、メールの文面、システムコード等々、else ifを忘れずに書き足さないといけないプロダクトなんて珍しくないはず。
大規模でこの手の漏れを策なく防ぐのは割と無謀。じゃあどうすんの
テストを書けば少しはマシになるはず。
enum の全要素 (
Plan.values()) とかをテストコードにうまく絡めていると、検知できたりする。PriceTest.groovy// テストコードは groovy と spock を用いた class PriceTest extends Specification { def "#plan -> #exp"() { expect: Price.of(plan) == new Price(exp) where: plan || exp Plan.PLAN_1 || 100 Plan.PLAN_2 || 200 Plan.PLAN_3 || 300 } def guard() { expect: Plan.values().length == 3 } }シンプルだけど、
Planに依存してるテストに== 3が入っていると、Planが増減した時に気づける。
elseを値段判定に使わない、とかもアリ。Price.javaelse if (plan == Plan.PLAN_3) return new Price(300); else throw new RuntimeException("match error");テストコードがあればこれでも気づける。
整理: ここが怖いよ
- 例えば条件分岐の書き方によってはコンパイルが通ってもバグる可能性があるよ
- 一番最初に書くときは簡単なので「まさかバグんないだろ〜」と思っても、保守と追記で地獄を見るよ
引数不正なんかは恩恵が得られる、かも
逆に、しっかりやればこんなバグはバッチリ防げる
例1
MailService.javapublic class MailService { public void send(String itemName, String userName) { String mailBody = MailBodyFactory.create(itemName, userName); ... } }例2
FooService.javapublic class FooService { FooRepository fooRepository; public void replace(Foo usingOne) { Foo newOne = Foo.newOne(); fooRepository.replace(newOne, usingOne); } }どこが間違ってるかというと...
例1
MailFactory.javapublic class MailBodyFactory { public static String create(String userName, String itemName) { return String.format("%s 様 %s のお買い上げありがとうございます", userName, itemName); } }例2
FooRepository.javapublic interface FooRepository { void replace(Foo usingOne, Foo newOne); }引数の順番が逆でした。
また余談
「知るかよ!」って思った?
でも大体の場合プロダクトコードの引数順なんて覚えてないし、気をつけながら実装するのは案外とても疲れる。さらに、こういうのは下手に動くだけにとてもタチが悪い。
じゃあどうすんの
Stringは乱用しないでUserNameとItemNameのクラスを作ろう。
そしたら絶対に逆にできない。
Fooの方はドメインクラスを作ってみたみたいだけど、例えば状態ごとにわけてみよう。public interface FooRepository { - void replace(Foo usingOne, Foo newOne); + void replace(UsingFoo usingFoo, NewFoo newFoo); }これで絶対に逆にならない。
(状態別クラスについては勢いで一緒にこんな記事を書いて見ました → 全部のステータスを1クラスで表現する神エンティティをやめよう!)大体の目安だけど、変数名で説明を頑張ってる箇所は、型にしてしまった方が良いと思う。
赤線になってくれた方がよっぽど楽だし、エディタも補完候補とかを賢く考えてくれる。
ちなみに、似た様な話は以前も書いたので興味があればそちらも見てみてください。
同じテーブルの値でも違うクラスを用意すると良い感じ整理: ここが怖いよ
- 型があると言っても、全部
Stringじゃあ恩恵を全然享受できない- ドメインクラスを作ってみても、それがちゃんと設計されていなければ恩恵は大きくならない
テストがあると使いづらさに気づく、かも
例えばこんなコード
CampaignCode.javapublic enum CampaignCode { CODE_A, CODE_B; public static CampaignCode create() { if (LocalDateTime.now().getDayOfMonth() < 15) { return CODE_A; } else { return CODE_B; } } }テストコードを書こうとすると
nowが邪魔で「あれ、テスト書けないぞ...??」ってなるはずなんだ。これはつまり作ったプロダクトも
nowでしか動かないので、他の部分のテストを邪魔したり、モッキングや結合試験とかで融通が効かなかったりする。テストコードは最初にそのメソッドを使うので、こういう「あれ?このメソッド使いづらいな?」みたいなのに気づくチャンスに、結構なる。
(この例は
nowは引数で渡した方が良い)まとめ
- 静的言語で DDD をやっているからと言って、全てのバグが抜けるわけではないよ
- 条件分岐の例とか
- 他にすぐ思いつくのだと配列操作とか(前のどこかの処理が間違ってて index out of range が出るとか、よくあるよね)
- 同じく前処理のどっかがおかしくて
Optional.getしたらemptyだった、とか- イテレータのカーソルが思った位置にないとか
- java でたまに見るのは
Streamの終端操作を2回しちゃうとか- ドメインクラスを作ってるからと言って、思考停止で型の恩恵が得られるわけではないよ
Foo usingの例とか- テストコードは作った処理の使いづらさとかに気づく大事なチャンス
- 品質面では当然
- すぐ学習と経験のフィードバックが得られるという意味でも、とても大事
- (忘れてたのでさらっと追記)リファクタリングを促進するためにも、テストはあった方が良い
- モデルのアップデートをかけてドメイン層をどんどん直せるためにも、テストコードは大事
- ただし不必要に書きすぎると逆にリファクタリングを阻害することにもなる
おわりに
例えば enum の件なんて言語によっては警告やコンパイルエラーが出るんだけど、会場の大きな流れとしては「コンパイルはテストコードを不要にするか」って感じだと思ったので、思い立ってアンチテーゼとして書いて見ました。
本記事はあくまで僕個人の持論です。
最近聞いたある人の言葉を真似るなら「テストいらないって聞いたんだけどマジ?」って思った人は、まずは素直にテストを書きなさい。
と僕は思います。おしまい。
- 投稿日:2019-08-28T20:18:29+09:00
全部のステータスを1クラスで表現する神エンティティをやめよう!
勢いだけで書いたよ!
大体タイトルの通り!いきなりだけど、勉強会で話を聞いたり twitter を見たりしてると、とりあえず見つけた名詞をクラスにして周辺のデータっぽいやつを全部そこに放り込んでいるエンティティがよくある気がするよ。
僕も初学者の頃書いたw早速コード
UserStatus.javapublic enum UserStatus { Applying, // 申請中 Using, // 利用中 Leaved, // 退会済 BlackListed, // ブラックリスト Erased, // 抹消済 }(たった)これくらいの状態を持つ
Userってクラスを1つで表現してしまった!「DDD だから User ってクラスを作って、処理はそこに書くんだ〜」って段階によくあると思う。
きっと最初は状態が少なかったんだけど、分析だかサービス成長だかがしていく間にヤベェことになってしまったんだね、わかるよ。ウンウンUser.javapublic class User { private UserId userId; private UserCourse userCourse; private UserStatus userStatus; private ApplyDate applyDate; private Optional<UseStartDate> useStartDate; private Optional<LeaveDate> leaveDate; private Optional<EraseDate> eraseDate; public User apply(UserCourse userCourse) { return new User( new UserId(), userCourse, Applying, new ApplyDate(), Optional.empty(), Optional.empty(), Optional.empty() ); } public User useStart() { if (userStatus != Applying) throw new RuntimeException("not applying"); return new User( userId, userCourse, Using, applyDate, Optional.of(new UseStartDate()), leaveDate, eraseDate ); } public User courseChange(UserCourse userCourse) { if (userStatus != Using) throw new RuntimeException("not using"); return new User( userId, userCourse, userStatus, applyDate, useStartDate, leaveDate, eraseDate ); } public User leave() { if (userStatus != Using) throw new RuntimeException("not using"); return new User( userId, userCourse, Leaved, applyDate, useStartDate, Optional.of(new LeaveDate()), eraseDate ); } public User blackListIn() { if (userStatus != Using) throw new RuntimeException("not using"); return new User( userId, userCourse, BlackListed, applyDate, useStartDate, leaveDate, eraseDate ); } public User blackListOut() { if (userStatus != BlackListed) throw new RuntimeException("not black listed"); return new User( userId, userCourse, Using, applyDate, useStartDate, leaveDate, eraseDate ); } public User erase() { if (userStatus != BlackListed) throw new RuntimeException("not black listed"); return new User( userId, userCourse, Erased, applyDate, useStartDate, leaveDate, Optional.of(new EraseDate()) ); } }使うコードはこんな
UserService.javapublic class UserService { public void leave(UserId userId) { User usingUser = userRepository.find(userId); User leavedUser = usingUser.leave(); userRepository.leave(leavedUser); billing.stop(leavedUser.getLeaveDate().get()); ... } }うへぇ〜辛ェ〜
コードが長い!
コンパイル通っても実行例外が超出そう!
ってかドメインロジックってナニ!これほとんど DTO なんじゃあないのォ〜?
状態ごとにクラスを分けよう!
実装は割愛するけど、状態ごとにクラスを分けよう。
ポイントは2つ!
- ステータスで頑張らずにもっと細かく分ける
- 特定の状態でしか持たない値は極力持たない、持つときは必ず持つようにする
Optionalを極力排除するということ主なメリットは!
- ステータス不正で実行してしまう実行例外がなくなる
leavedUser.getLeaveDate().get()みたいな実行例外の可能性がなくなる- 特定の状態でできる処理が明瞭になる
- 何から何に遷移するかが明瞭になる
簡単だけど超効果あるからやってみるべし!
もちろんテク先行ではなくて、モデリングありきだよ
例で状態を用いたけど、何で分けるかはモデリング次第。
ユースケースとかビジネス影響度とか、きっとまちまちのはず。それから、この分割は「これが DDD だぞ!」ではなくて「ただの起点」だよ!
こうやってモデルを小さくして、そこから分析をしてロジックを選り分けて集めるんだ!例えば今回の例だと、
LeavedUserを取っ掛かりに請求停止(billing.stop(...)の箇所)のモデリングをする、とか。ちょっと気をぬくとエンティティを作ったつもりでもただの DTO になってしまうので、気をつけよう!
ちなみに、2年弱前に似た様な記事を書いてるよ、そちらもよければどうぞ
同じテーブルの値でも違うクラスを用意すると良い感じ#ケース2-1つのクラスの状態更新をする
- 投稿日:2019-08-28T18:37:04+09:00
Kotlin の後置・前置インクリメントと演算子オーバーロード (C, Java, C++ との比較)
概要
- a++ のような後置インクリメント演算子: 値が関数に渡された後にインクリメントされる
- ++b のような前置インクリメント演算子: 値が関数に渡される前にインクリメントされる
C言語、C++、Java、JavaScriptなどでは、インクリメント演算子(増量子)「++」が用意されている。前置インクリメントと後置インクリメントの2種類がある。字句は同じ「++」だが前置演算子として使うか(例: ++x)後置演算子として使うか(例: x++)で意味が違う。オペランドの値が整数の場合は1、ポインタの場合は指し示す対象1個ぶん変わるのはどちらも同じだが、式としての値が、前置の場合はインクリメントした後の値になる(この意味は += 1 と同じ)、後置の場合はインクリメントする前の値になる。
環境
- kotlinc-jvm 1.3.10 (JRE 11.0.1+13-LTS)
Kotlin で後置・前置インクリメント
Kotlin のソースコード。
var a = 1 var b = 1 println(a++) println(++b)実行結果。
a++ の場合、値が +1 される前に関数に渡されるため 1 が出力される。
++b の場合、値が +1 された後に関数に渡されるため 2 が出力される。$ kotlinc -script hello.kts 1 2Kotlin のインクリメント演算子は C や Java と同様の挙動をしている。
C のソースコード。
#include <stdio.h> int main(int argc, char *args[]) { int a = 1; int b = 1; printf("%d\n", a++); printf("%d\n", ++b); return 0; }実行結果。
$ gcc hello.c -o hello $ ./hello 1 2Java のソースコード。
public class Hello { public static void main(String[] args) { int a = 1; int b = 1; System.out.println(a++); System.out.println(++b); } }実行結果。
$ javac Hello.java $ java Hello 1 2Kotlin で演算子オーバーロードしてインクリメント
Kotlin のインクリメント演算子オーバーロードは手軽に書ける。
operator fun inc でインクリメント演算子をオーバーロードできる。data class Counter(val num: Int) { operator fun inc(): Counter { return Counter(num + 1) } } var a = Counter(1) var b = Counter(1) println(a++) println(++b)実行結果。
a++ の場合、num の値が +1 される前に関数に渡されるため、num=1 の状態が出力される。
++b の場合、num の値が +1 された後に関数に渡されるため、num=2 の状態が出力される。$ kotlinc -script counter.kts Counter(num=1) Counter(num=2)同じようなインクリメント演算子の挙動を C++ で書いてみる。
C++ では後置と前置で別の関数を書く必要がある。#include <iostream> class Counter { private: int num; public: Counter(int num) : num(num) {} // Prefix increment operator Counter& operator ++() { this->num++; return *this; } // Postfix increment operator Counter operator ++(int) { Counter c = *this; this->num++; return c; } friend std::ostream& operator<<(std::ostream& os, const Counter& c) { os << c.num; return os; } }; int main(int argc, char *argv[]) { Counter a(1); Counter b(1); std::cout << a++ << std::endl; std::cout << ++b << std::endl; return 0; }実行結果。
$ g++ hello.cpp -o hellocpp $ ./hellocpp 1 2参考資料
- Operator overloading - Kotlin Programming Language
- 後置インクリメント演算子と後置デクリメント演算子: ++ および -- | Microsoft Docs
- Prefix Increment and Decrement Operators: ++ and -- | Microsoft Docs
- 前置インクリメント演算子と前置デクリメント演算子: ++ および -- | Microsoft Docs
- Increment and Decrement Operator Overloading (C++) | Microsoft Docs
- インクリメント演算子とデクリメント演算子のオーバーロード (C++) | Microsoft Docs
- 投稿日:2019-08-28T17:56:46+09:00
JavaでNotification Hubsを使用してpush通知を送信する
azureのNotification Hubsを使用してpush通知を送信することになったので備忘録。
サーバ側の開発です。(スマホアプリはわかりません。)Java SDK入手
ここからJava SDKを入手
一応公式サイトとのことです。コンパイルとビルド
mavenコマンドを使用するので、mavenインストールしていない場合はここを参照
pom.xmlがあるフォルダでmavenコマンドを実行するだけcd NotificationHubs mvn packageそうするとtarget配下にNotification-Hubs-java-sdk-0.1.0.jarできます。
これをパスが通ったフォルダに置いてください。使い方
タグを指定して送信することを想定しています。
事前にトークンとタグをNotification Hubsに登録していることが前提です。トークンはメールアドレスみたいなイメージで
タグはメールアドレスみたいなイメージです。タグを指定してpush通知を送信すると、そのタグに紐づいているトークンすべてにpush通知を送信してくれます。
push通知送信(即時送信)
String connectionString = "Endpoint=sb://xxxxx.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=~~~~~"; String hubPath = "Notification hubsの名前"; NotificationHub hub = new NotificationHub(connectionString, hubPath); String message = "push通知"; String body = String.format("{\"data\":{\"message\":\"%s\"}}", message); // android端末用 Notification notifiation = Notification.createFcmNotifiation(body); hub.sendNotification(notifiation, "タグ名");push通知送信(日時指定送信)
String connectionString = "Endpoint=sb://xxxxx.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=~~~~~"; String hubPath = "Notification hubsの名前"; NotificationHub hub = new NotificationHub(connectionString, hubPath); String message = "push通知"; String body = String.format("{\"data\":{\"message\":\"%s\"}}", message); // android端末用 Notification notifiation = Notification.createFcmNotifiation(body); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date = dateFormat.parse("2020-01-01 00:00:00"); hub.scheduleNotification(notifiation, "タグ名", date);トークンとタグの登録
事前にトークンとタグをNotification Hubsに登録していることが前提ですと記載しましたが、
サーバ側でもトークンとタグを登録できます。(トークンとタグがわかれば)// Android端末用 Registration reg = new FcmRegistration("トークン"); reg.getTags().add("タグ名"); NotificationHub hub = new NotificationHub(connectionString, hubPath); Registration registreg = hub.createRegistration(reg);トークンとタグの削除
トークンとタグを削除する場合はタグを指定するか、RegistrationIdを指定します。
・タグで削除する場合は意図しないトークンも削除されるため、注意が必要です。
・RegistrationIdはトークンとタグの登録で登録結果(上のソースではregistreg )の中に詰まっています。
この値でトークンとタグを一意に識別することができます。NotificationHub hub = new NotificationHub(connectionString, hubPath); // タグを指定して削除 CollectionResult collectionResult = hub.getRegistrationsByTag(tag); for (Registration registration:collectionResult.getRegistrations()) { hub.deleteRegistration(registration.getRegistrationId()); } // RegistrationIdを指定して削除 hub.deleteRegistration(registreg.getRegistrationId());
- 投稿日:2019-08-28T16:57:14+09:00
私は如何にして JMockit の returns(Object) を安全に置換したか?
テストで使っているJMockitというモックライブラリが古いバージョンだったので、最新バージョンにアップデートする作業をしたのですが、その際にちょっとしたハックをしたので紹介します。
なお、JMockit自体の変更点などについては別記事にしているので、JMockitを使っている方はそちらもご覧ください(→JMockit と共に生きる者ためのメモ - Qiita)
どんな問題があったのか?
JMockit(の旧バージョン)では以下のように
Expectationsのインスタンスイニシャライザでメソッドを差し替えます。戻り値はresultまたはreturnsで指定します。new Expectations() {{ intList.get(0); result = 1; // intList.get(0) が常に1を返すようになる intList.get(0); returns(1); // ↑ の別記法(1.30で廃止) intList.get(1); returns(-1, 1); // intList.get(1) が、-1 と 1 を交互に返すようになる }};ここで、1引数版の
returns(v)は JMockit 1.30 で廃止されてしまいました。
そのため、テストコード中のreturns(v)はresult = vに書き換えなくてはなりません。まず、sed で置換しようとしましたが、returnsには1引数版と2引数以上版があるので単純に置換することはできません。
sed -e 's/returns/result =' // 2引数版も置換してしまう。-e 以外の引数は省略正規表現を工夫したりもしましたが「returnsの引数として別のメソッド呼び出しを指定する」コードがある等して、上手くいきませんでした。
次に IntelliJ のリファクタリング機能を探しましたが、
- インライン化
- メソッド名変更
などはあるのですが「メソッド呼び出しを代入文に変更」という変更はできませんでした1。
どうやって解決したか?
まず、
returnsの呼び出しを自作クラスのメソッドの呼び出しに置換しました。sed -e 's/returns(/test.MyDummy.returns(/g'テストコードはこんな形になります。
new Expectations() {{ intList.get(0); test.MyDummy.returns(1); // returns が自作メソッドのものに置換されている intList.get(1); test.MyDummy.returns(-1, 1); // 2引数版も置換される }};さらに、自作クラスに以下のようにメソッドを定義します。この時点で、JavaコードとしてはValidになるので、IntellJのリファクタリング機能を使えるようになります。
class MyDummy { public static void returns(Object o) { result = o; } public static void returns(Object o, Object o2, Object... args) { } }IntelliJ で、MyDummy.returnsにカーソルを合わせ
Refactor This → inlineでインライン化します。すると、IntelliJ は、メソッドオーバーロードを理解しているので、1引数版の returns のみをインライン化してくれます。
new Expectations() {{ intList.get(0); result = 1; intList.get(1); test.MyDummy.returns(-1, 1); }};これで「1引数の
returns→result」は完了です。残ったtest.MyDummy.returnsは、再びsed で置換して、returnsに戻します。sed -e 's/test.MyDummy.returns(/returns(g'これで、1引数の
returnsのみが書き換えられた状態になりました。new Expectations() {{ intList.get(0); result = 1; intList.get(1); returns(-1, 1); }};かかった時間は、考える時間も含めて1・2時間ほどでした(測ってないけど多分)。
returnsは何千箇所もあったので、手作業では何日もかかったことでしょう。また、使った変換は全て機械的なものだったので、バグは生じませんでした(本当にバグっていないかは、実際にテストをコンパイル・実行して確認できました)。
商用版では出来たりするかもしれませんが、私が使っていたIntelliJ IDEAでは出来ませんでした。 ↩
- 投稿日:2019-08-28T16:53:07+09:00
IT業界転職活動し始めて自分の目指す方向と違う方向に行ってしまっていた事に気づき始め少し絶望感
業界・職種未経験で転職活動をし始めた話。現在進行形です。
まず僕のスペックから
概要
住所 : 都内在住
年齢 : 29歳
学歴 : 高卒
業種 : 未経験
職種 : 未経験
前職 : 弱電設備の設置施工、サービスエンジニア
前々職 : 電気工事・電気通信工事のエンジニア来歴
高校卒業後
↓
アルバイトしながら生活
↓
上京
↓
またアルバイトしながら生活
↓
とりあえず元々自分で持っている資格があったため就職
↓
少し興味を抱いた仕事があったため転職
↓
かなり前から興味があったが本格的に仕事してみたいと思い退職
↓
無職ITスキル
学習方法 : 某オンラインスクールにて
学習言語 : Java課程修了レベル目指したい所
自社開発・受託開発を行っているWeb系のエンジニアになりたい
オンラインスクールでJavaを選んだ理由
転職サポートのついているコースがJavaだったため
→この時点でもう少し調べておくべきでした。
転職活動をはじめて
※詳しい社名は伏せます。
某転職エージェント業界最王手へ登録
とりあえず誰もが知る転職エージェントへの登録し担当のエージェントに会いに行きました。会ってみた感想としては確かに業界No.1の求人数を誇るようでしたがIT業界への転職に対してあまり強くなさそうな所に加えて未経験だと紹介しづらいとの事。にも関わらず担当のエージェントの方がSESの会社を何件か紹介してもらいました。
IT転職業界No.1のエージェント登録
次にIT業界に特化した転職エージェントへ登録しエージェントに会いに行きました。こちらは担当者がかなり若く私と話している時もそわそわしている状況。おそらく会社の指示だとは思うが自分のための転職相談に来ているのに「お友達紹介キャンペーンやってまして...」との話が始まりちょっとそれはないんじゃないかと思いました。
その話の流れで無料のスクールやっているとのこと。話を聞く流れだととりあえず気持ち勉強した後→スクールのスポンサーになっている企業への紹介→面接→就職という流れでした。とりあえず参加。が、ここに落とし穴が...。(後ほど記載)●無料のスクールに入って感じたこと
当たり前かもしれませんがまずそもそも他の有料オンラインスクールを受講している人間だと必要がない。スクールで作った成果物やポートフォリオを提げて転職活動し始めた方がいい。そしてここが本題。紹介してくる企業がSESの会社しかない。転職活動し始めてサイトに登録するとSESメインでやっている会社からかなりオファーがくる。ってことは転職エージェント経由で紹介してもらう必要がない。スクールにも個々に担当者がつく。担当者曰く「未経験だとSESしかいけない。しかも年齢的にも29歳だときつい」と事あるごとに言われる。実際どうなのかなと疑念を抱き始める。
とりあえず面接に行って感じた事
●SES事業/受託開発など色々やっている結構大きい会社
こちらは通常の面接。
採用担当者との挨拶が済み個人情報取り扱いについての紙を記入している途中おそらく社長らしき人も入室。
挨拶するも反応なし。採用担当者が一般的な流れの面接開始。その途中社長の開口一番「この年齢だとね〜」から始まり「ITパスポートとかないと話にならないんだよね〜」とのこと。求人情報やホームページをかなり見てから来たがITパスポートの記載は一切なし。というかそもそも29歳業界完全未経験という情報を公開しての応募しているわけであって書類選考の時点で落とすべき。その後も「あなたの今までの経験だとインフラの方が向いてるよ」「なんで今更プログラマーなの?」と上げていくとキリがない。今思い返してみるとかなりの圧迫面接。
こちらの企業まだ合否の通知は来ていないが仮に採用の通知もらってもいくつもりはありません。おそらく入社しても一ヶ月もたないかも。笑●自社開発/受託開発を行っているWeb系のベンチャー
採用面談という形での面接とのこと。
流れとして担当者からの企業概要の紹介(30分ほど)から面談者から自分に対しての質問(30分)その他質問(10分)。前述の会社から180°代わり担当者と色々な話ができました。結果不採用でしたが、面談担当者から添えられたメッセージがとてもためになりました。
内容は、
ITの勉強をはじめてスクールでも大変だと思いますが、もう少し、ITに対する他のアクションが欲しかったです。 たとえば、ブログに勉強したことを公開するとか、アプリ作ってストアにあげてみるとか、 そういった、経験がエンジニアの成長には不可欠となってきます。 まだ、勉強を開始して間もないと思いますが、いろいろ自分から動いて、いろいろなことを吸収してみてください。 きっと素晴らしいエンジニアになれると思います。 また、年齢のことを気にしていましたが、あまり気にしなくて良いと思います。 〇〇様より年上で未経験の方もたくさん会ってきました。 重要なのは行動と情熱、それを維持することだと思います。
実は会う前のメッセージで技術的な興味や行動を見たいとの話をいただいていましたが自分の理解が足らずそのまま面談の場へ行ってしまいました。おそらく担当者も話しているうちに完全未経験で取るべき行動がわかってないと悟ったのだと思います。こちらこそご期待に添えられない状態でお伺いしたこと大変申し訳ありませんでした。
今現時点で思うこと(反省点)
そもそもJavaだとWeb系がきつい?
今更だけど市場で需要のある言語はRuby、Python、PHPなのかなと最近思う。
スキルコレクターみたいになるつもりはないけど言語学び直そうかな...。SESだと逆に年齢が重要?
最悪SESでも問題ないかなとは思っていたけどSESやっている会社からしてみたら完全未経験で29歳はきついのかな。
言語の勉強をすればエンジニアになれると思っていたが違った
実際は勉強中、勉強後の行動が重要
今現時点で思うこと(よかった点)
退職
退職したおかげでかなり時間ができて今まで見えてこなかったことが見えてくるようになった。
おそらく仕事しながら勉強して、転職活動してだとろくな結果をうまない。
お金に困りそうだったらウーバーイーツの配達でなんとかなる。Webベンチャーの面談担当の人と話ができた
今現時点で一番の収穫。かなり勉強になりました。
今後の行動
アウトプットを頑張っていく
・とりあえずQiitaで投稿
ポートフォリオの作り直し
・持っていくのに恥ずかしいくらいのはりぼてポートフォリオなため最初から作り直し
・なんらかの形でQiitaに投稿しながら、Gitにもプッシュしながら
・Web上で公開言語の学び直し(検討中)
・実際どうなのか要検討
とりあえず転職活動は休止
最後に
同じような境遇のかたに情報共有になればと思います。
質問があるかた気軽にどうぞ。
- 投稿日:2019-08-28T16:03:23+09:00
JMockit と共に生きる者ためのメモ
先日、某Java製プロダクトの、ユニットテストで使っている、JMockitのバージョンを上げる作業をしました(1.13 → 1.46)。その際に分かったことをメモします。
主に以下の2種類の人に役立つことを期待します。
- 今まで古いJMockitを使っていたが、最新版を使う人(つまり私と私の同僚)
- これからJMockitをアップデートする人
JMockit とは
JMockit は Java用のモックライブラリです。ユニットテストの中で、既存のメソッドの動作を差し替える(モックする)ことができます。
JMockitの公式ページ → The JMockit testing toolkit
差し替えは、インスタンスイニシャライザと特殊なインスタンス変数
resultやtimesを使って定義します。new Expectations() {{ obj.method(arg1, arg2, arg3); result = value; times = 2; }};かなり強力な操作もサポートしており、以下のような差し替えもできます(詳しくは公式ページ参照)。
- staticメソッドの差し替え
- オブジェクトの特定のメソッドのみ差し替え(Partial Mocking)
newが返すオブジェクトの差し替えJMockit のバージョンアップの特徴
2・3か月ごとにマイナーバージョンが上がります。過去のリリースはこちらのページから確認できます:
履歴からわかるように、JMockitは古い機能はどんどん切り捨てるポリシーです。例えば
Deencapsulation.setFieldというメソッドは2ヶ月・2バージョンで非推奨指定→廃止されています。Version 1.45 (Jan 27, 2019):
Removed the Deencapsulation.setField methods, which were deprecated in version 1.44.Version 1.44 (Nov 25, 2018):
Deprecated the Deencapsulation.setField methods, in favor of @Tested and @Injectable.したがって、一気にバージョンを上げようとすると、大量の変更が必要になり、作業が辛くなります。
- JMockitのバージョンを上げる(常に最新版に追従する)
- JMockit以外のライブラリに移行する
の、どちらかをお勧めします。
JMockit より Mockito の方がオススメ!
JMockitのライバルにはMockito があります。Mockito の方がユーザー数が多く、記法も自然です(特にRubyのRSpecに慣れた身としては)。
また、Mockito単体では通常のメソッドの差し替えしかできませんが、Powermock と組み合わせれば、JMockitと同様の強力な「汚い」変更もできるようになります。
// mockito の記法のイメージ when(obj.method(arg1, arg2, arg3)).thenReturn(value);したがって、可能ならJMockitからMockitoに移行することをお勧めします。
なお、今回のプロダクトでは、JMockitを使ったテストが大量にあったためMockitoに移行するのは諦めました。
バージョンアップ作業の流れ
以下のような作業でバージョンアップできます。
- JMockitのバージョンを上げる
- バージョンアップで廃止された機能などを書き換える
- コンパイルする
- コンパイルが通るまでコードを修正する
- テスト実行
- テストが通るまでコードを修正する
ただし「最新版では避けるべき書き方」で説明するように、コンパイルは通るが、意図した通りに動作しなくなるケースがあります。テストが失敗する場合は、そこを疑ってください。
なお、古いバージョンから一足飛びに最新版に上げるのは、変更点が多くなりすぎ破綻するので、オススメしません。
また、
- バージョン 1.x で廃止された機能が 1.y で復活しているので、1.xを飛ばして 1.yに上げるべき
- バージョン 1.(x+1)には後方互換のための機能があるので、1.xを飛ばして1.(x+1)に上げるべき
といったショートカットパスも無いと思います。地道に1バージョンずつ上げていくことをオススメします。
最新版のチュートリアルを読む
バージョンアップしたりテストを書いたりする前に最新版のチュートリアルを読みましょう。
JMockitはそもそもクセが強いライブラリです。「書き方が違うだけでMockitoと同じだろ」と思うと痛い目に会います。例えば
@Mockedであるクラスのモックオブジェクトを取得すると、そのクラスの全インスタンスがモックになります。また、上述のように機能が大胆に改廃されおり、書き方がだいぶ変わっています。JMockitの経験者もコードに触る前にに目を通してください。
主な廃止機能と書き換え方法
最新版のチュートリアルに目を通しましたか?
OK!
過去のバージョンの変更点のうち、影響が大きそうなものを説明します(細かい機能は申し訳ないが、リリースノートなどで調べてください)。
テストの実行方法
従来は JUnitのテストクラスに
@RunWith(JMockit.class)アノテーションをつけるとJMockitが読み込まれていました。現在は方法が変わり、ユニットテストの実行時に、VMのオプションとして以下のように javaagent として JMockit の jar ファイルを指定すると、JMockitが読み込まれます。
-javaagent:/path/to/jmockit-1.46.jarIntelliJ なら以下の場所で設定します。
「ツールバーの実行ボタン横のモジュール名」 → Edit Configurations → Templates → JUnit → Configurations → VM options
@RunWith(JMockit.class)アノテーション上述のように廃止されたので単に削除してください。
1引数の
returns1引数の
returnsは、代わりにresult =を使ってください。2引数以上のreturnsは引き続き使えます。なお
returnsを1個1個書き換えるのは大変ですが、それについては別記事に書いたので参考にしてください(→ 私は如何にして JMockit の returns(Object) を安全に置換したか?)。// Old new Expectations() {{ obj.method(arg1, arg2, arg3); returns(value); }}; // New new Expectations() {{ obj.method(arg1, arg2, arg3); result = value; }};NonStrictExpectations
NonStrictExpectationsは、Expectationsの「差し替えたメソッドが呼び出されなくてもエラーにならないバージョン」でした。以下のように
Expectationsに置き換えた上でminTimes = 0で最小呼び出し回数を0回に設定してください。new Expectations() {{ mock.get(0); result = "1"; minTimes = 0; }};なお、「メソッドを差し替えたのに、それが呼び出されていない」のは、
- 実はそのメソッドを差し替える必要が無かった
- 意図した動作をしていない
のどちらかの場合が多いので、テストケースを見直してみてください。
StrictExpectations
単に Expectations に書き換えてください。
Deencapsulation
Deencapsulation は private なフィールドを参照したり変更したりする機能でした。
1. フィールドを
package-privateないしpublicに変更する
Deencapsulationで変更していたフィールドのアクセス制限を緩め、テストからは.でアクセスするようにします。フィールドには Guava の@VisibleForTestingアノテーションを付けます。本体側コードに変更を加えることになりますが、アクセス修飾子の変更がバグに繋がることは普通は無いはずです1。
// Old // 本体コード class Spam { // ... private Foo field; // ... } // テストコード x = Deencapsulation.getField(obj, "field"); // New // Old // 本体コード class Spam { // ... @VisibleForTesting Foo field; // ... } // テストコード x = obj.field;2.
@Testedと@Injectableを使うJMockitにはテスト用のDI機構があります。
Deencapsulation.setFieldをテスト対象オブジェクトにモックをセットするために使っている場合は、DIに置き換えられます。4 Instantiation and injection of tested classes
.newMockInstance()モックインスタンスを @Mocked で取得するようにします。単純な書き換えはできません。
Systemなどのメソッドはモック古いバージョンでは JMockit はどんなメソッドでも差し替えられましたが、
現在のバージョンではSystem.currentTimeMillisのようなネイティブメソッドは差し替えられません。テストを再設計し、差し替え自体を不要にするか、ネイティブではないメソッドを差し替える形にしましょう。
今回バージョンアップしたプロダクトの、
System.currentTimeMillisを使っている部分は、代わりにnew Dateを差し替える形に変えました。最新版では避けるべき書き方
ドキュメントには明示されていませんが、旧バージョンでは問題なく動いていたし、コンパイルも通るのに、新バージョンでは動かなくなるパターンがあります。
引数・戻り値に別のメソッド呼び出しを書く
Expectationsの中で、メソッドの引数や戻り値として別のメソッド呼び出しを書くと、うまく差し替わらない場合があります。引数・戻り値はExpectationsの外で、一旦一時変数に代入してください。可読性の観点からも分けた方がよいでしょう。// NG new Expectations() {{ foo.method(getX(), getY()); result = getHogeList(); }}; // OK X x = getX(); Y y = getY(); HogeList hogeList = getHogeList(); new Expectations() {{ foo.method(x, y); result = hogeList; }};引数あり
Expectationsの中に無関係なメソッドを書くクラスの特定のメソッドだけを差し替えたい時や、特定のインスタンスのメソッドだけを差し替えたい時には、
Expectationsに引数にクラスやインスタンスを指定します。このとき、
Expectationsの引数と無関係なメソッド呼び出しを混ぜてしまうと、うまくモックできないことがあります。無関係なメソッド呼び出しは、別のExpectationsに分けましょう。可読性の観点からも分けた方がよいでしょう。// NG new Expectations(foo) {{ foo.method(x, y); result = v; other.othersMethod(); result = value; // このメソッドが刺し変わらない }}; // OK new Expectations(foo) {{ foo.method(x, y); result = v; }}; new Expectations() {{ // Expectations を分ける other.othersMethod(); result = value; }};
もちろん、テスト用に public にしているフィールド(本来はprivateにしたい)に本番コードでアクセスされると困りますが、
@VisibleForTestingを使えばアクセスしてよいフィールドかどうかは区別できるはずです。 ↩
- 投稿日:2019-08-28T15:13:53+09:00
ガチ初心者がKubernetesでコンテナを立てる?まで・・・
「Java Küche」のイベントに参加した!
2019/08/23の実施されたイベント「Kubernetes ハッカソン~Docker と Kubernetes にどっぷり浸かる二日間! ~」を振り返る記事。
講師
Microsoft・アドボケイト: 寺田 佳央さんありがとうございました。m(_ _)m
流れ
今回は、踏み台用の環境としてのUbuntu、そしてDockerfileのpush先、さらにKubernetesを動かす環境の、計3つのリソースをAzureで作成する。
内容としては、
UbuntuでDockerイメージ作成。
Azure Container Registry (以下、ACR) にpushする。
Azure Kubernetes Service (以下、AKS) で、コンテナを起動する。
までを紹介する。必要なファイルや情報は、寺田さんが大変綺麗にまとめてくださってます。
GitHubリンク: https://github.com/yoshioterada/k8s-Azure-Container-Service-AKS--on-Azureよし!やっていこう!
Outlookアカウント作成
リンク: https://signup.live.com/signup
※この時に、hotmail.comでないとAzureアカウントの作成でエラーする??Azureアカウント作って、クレジットカード登録する
Azureのログインページ: https://portal.azure.com/#home
※クレジット登録してないと、Ubuntuのリソースを作ろうとしたときに再足される。Ubuntuのリソースを作成する
Azureのコンソール画面にて、「Create a resource」をクリック。
Ubuntu 18.04 LTS版を作る
Resource gropは新規で作成!仮想マシン名も同じ。
リージョンは、「東日本」
SSH公開キーを「パスワード」
sshの許可する作ったUbuntu環境にsshでアクセスする。
※resourceの詳細画面で、sshするときのip(パブリックIP)を調べとく。
例)ssh userName@ipAdressDockerをインストール
参考記事: https://qiita.com/myyasuda/items/cb8e076f4dba5c41afbc
sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable test edge"
sudo apt-get update
sudo apt-get install docker-ce
docker --versionAzure CLIのインストール
この後、ACR, AKSと連携したい。ので、AzureCLIをインストールするぞ!
参考記事: http://aka.ms/az-cli-install-jp
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash// Dockerfileの作成と、Dockerイメージの違いが・・・
※Dockerfileは、Dockerイメージの為のレシピ。作り方、材料が書いてある。
※docker buildでイメージが作れる。らしい。Dockerfileを、Git Cloneする
※今回は寺田さんが、GitHubで公開してくれているやつを使わえてもらう。
めっちゃ感謝してます!!!!!
(>ω<)移動
cd FrontServiceDockerfileを眺めてみる
cat 0-Dockerfile-for-Maven※たくさん使うライブラリ郡を毎回ダウンロードするのは大変なので、それらを一度ダウンロードして、イメージとして作っておく。次からは、そのイメージを読み込むだけで良い。よく依存しそうなものは、イメージを作っておくのをオススメする。らしい。
docker build -t maven-include-localrepo:1.1 . -f 0-Dockerfile-for-Maven
※sudo su で入っておいた方が良いかも。さっきのbuildできたか、確認。
docker imagesAzure Container Registry の作成
// Kubernetesは、ACR経由でDockerイメージを取りに来るイメージ。だと思う。
// docker hubのプライベート版てきなやつかな?作ったイメージを、さっき言ったAzure Continer Registryにpushしたいので、先に作っておく。
参考記事: https://github.com/yoshioterada/DEIS-k8s-ACS/blob/master/CreateAzureContainerRegistry.md
ブラウザにてリソースを作成
レジストリ名は、適当に決める。
リソースグループは新規で適当に決める。
管理ユーザーは有効にしといた。続けて、ACRへログイン
※docker login する。コマンドの引数として、user名とACRのドメインを与える。
docker login -u [user名] [xxx.azurecr.io]
の後に、パスワードを入力する※必要な情報は、詳細画面でアクセスキーで確認できる。
※詳細画面で得られる情報 {
user : 〇〇
login-server: 〇〇〇〇.azurecr.io
pass: 〇〇〇〇〇〇〇〇
}Dockerイメージ作成とpushの準備
// ACRへの認証はやった。(docker loginで。)
※イメージを作ると、コンテナが作れる。動いてる実態はコンテナ。Dockerイメージの作成と、ACRへのpushを、.shファイル(2-build-create.sh)に寺田さんがまとめてくれてる。ので、shをコマンドで起動させれば良いらしい。が、そのpushするイメージの指定と、push先のリポジトリの2箇所はviで編集してね!
※ viでの編集箇所
vi 2-build-create.sh
DOCKER_IMAGE=〇〇〇〇/front-spring-service
DOCKER_REPOSITORY=〇〇〇〇.azurecr.io
※ DOCKER_IMAGEには、ACRのuser名: fullund2。/の後には、imagesの名前(ここで付けた名前がイメージの名前になるっぽい。今回は、front-spring-serviceで行こう!)ACRへpushする
※コマンドを打つときに、バージョン指定を引数で受け取れるので・・・もし、2回目とかなら、2.0とかにしてね!
./2-build-create.sh [x.x]※ブラウザで、pushされているか確認してね!
今回は試しに少しソースを編集してv2としてイメージ作成してみよう!
vi src/main/java/com/yoshio3/frontspring/FrontController.java
の一番した、returnの文字列を少し変更してみよう!よし、変更したソースを元に、作成しよう
./2-build-create.sh 2.0
※引数のバージョンは、2.0にしとく。動作確認
イメージの起動
docker run -p 8080:8080 -it [REPOSITORY名]:[x.x(TAGの数字)]別タブで新たに開いて、curlでアクセスして確認してみよう!
curl http://localhost:8080/sample/hello次の目標: AKSでコンテナを立てたい。まずは、ACRにあるイメージを取りに行く!
Azureページで、Create Kubernetes cluster を作る。
またリソースグループは、新規で!クラスター名は一緒かな。
リージョンは、東日本
Node count 1にする。
仮想ノードは有効HTTP application ・・・ Yes
Kubectl コマンドのインストール
az aks install-cliaz-loginしようぜ
az login
※ブラウザ経由でログインできるAKS接続用の資格情報の取得
az aks get-credentials --resource-group [xxxx] --name [xxxx]
※これで、Kubernetesに繋がるようになった。試しにコマンドが使えるか確認してみるか
kubectl cluster-info
とか
kubectl get nodeACRにあるイメージを、Kubernetesに持ってきたい
今のままだと、KubernetesがACRへアクセスできない。
なので、3-create-secret4reg.shをviで編集する。※具体的には、ACRへAKSからpullの通信を送りたい。その認証を、3-create-secret4reg.shファイルに書いてある感じ。
kubectl create secret docker-registry docker-reg-credential --docker-server=〇〇〇〇.azurecr.io --docker-username=〇〇 --docker-password="〇〇〇〇" --docker-email=hoge@example.com※うまくいったら、
kubectl get secretで確認してみよう!よし、これでイメージを取りに行く事は出来そう!
取ってくるイメージ名を修正する
※作成したACRサーバー名、コンテナのイメージ名に変更する
vi 4-create-deployment-svc.yaml
※修正箇所 30行目。入力する内容としては、docker imagesでの長いやつ。/の後には、verを書けばよい。
image:
〇〇〇〇.azurecr.io/〇〇/front-spring-service:1.0
※少し下も修正する。cpuの0.6を100mに変更。2箇所
※1.0を取りにいくなら、上の
template:
metadata:
labels:
app: spring-front-service
version: [ここを、"v1"としておく]よし!認証と取りに行くイメージは指定できた。
実行
※applyコマンドを使って、ACRからイメージを取ってきて、コンテナを立てる事が出来る。
kubectl apply -f 4-create-deployment-svc.yaml起動しているか確認
kubectl get po -w動作確認
※実態はKubernetes側で動いてるから、lcoalhostへフォワードするコマンド
kubectl port-forward [spring-front-service-〇〇〇〇] 8080:8080
※もし上手くいかなかったら、docker stopで動いてるの止める。別タブで・・・
curl localhost:8080/sample/hello
# 結果が表示されるv2を作って、applyする
4-create-deployment-svc.yamlを複製して、4-create-deployment-svc-v2.yamlとする。
vi 4-create-deployment-svc-v2.yamlで編集
・4行目の name: spring-front-serviceも末尾に、"-v2"を付けとく。
・imagesをいじる。:2.0にすると、v2のイメージを見ることになる。
・versionを、v2にする。
kubectl apply -f 4-create-deployment-svc-v2.yaml
※なんか、sudo suに入らないと出来なかった。v2の起動確認
kubectl get po// 2つめも、port-forwardして確認すると良いかも。
これで、僕らはk8sで2つのコンテナを立てることに成功した!やった!!
フィルタリングしてみよう!
kubectl get po --selector app=[],version=[v1]
-> つまり、selectorを書いて、どのpodへアクセスするか制御できる!!!次に、Kubernetesでv2のコンテナを起動させて、v1をv2にアップデートさせる方法をやる。
serviceについて
serviceってのを作って、それを起動させよう。
serviceがpodへのアクセスを仲介する感じ??podを実行すると、固定でipが生成されるよね。それってどう管理します?手動って大変。
実はip以外にもpodにアクセスする方法がある。
ipが頻繁に変化することになるのは、良くないですよね。デプロイの肝
=> serviceを作って、この一意のIPアドレスを覚えてさえいれば、あとは、serviceのselectorでpodを変更できる。
11-Service.yaml を確認してみよう!serviceの起動
kubectl apply -f 11-Service.yaml確認
kubectl get svcメリット
正常に動いているpodを立てたまま、新たなpodに以降できる。
直ぐに戻す事も出来る!
これが、kubernetesの凄く良いところ!!!!
自分自身でやろうとすると、大変。
yaml編集するだけ。ちなみに
通常は、serviceはtype: ClusterIP!!
まぁ一瞬だけでも、どうしても外部に公開したいなら、type: LoadBalancerにする。
でも、よくない。危険。注意してね。--> serviceが複数ある時、Ingressでまとめて、そのIngressの設定で、pathを設定する事で、URLに応じて呼び出すserviceを分岐できる。
まとめ: Ingressを外部に公開すると良い。
IngressでURLを分けて!serviceでpodを分岐する。
イメージは、serviceのルーティング。受付嬢!!マイクロサービスを運営する上で、便利な機能。
Ingressを修正して立ち上げてみる
vi 12-Ingress.yaml
修正箇所:- host: front-service.[〇〇〇〇.aksapp.io]
この修正の中身は、ブラウザの、リソースの「HTTP アプリケーションのルーティング ドメイン」から貼り付け
※場所的には、ホーム > リソース グループ > [k8s] > [k8s] かな?立ち上げ
kubectl apply -f 12-Ingress.yaml確認
kubectl get ingアクセス
http://[front-service.〇〇〇〇.japaneast.aksapp.io]/sample/hello一応、イベントの内容はここまで。
デザインオブフェアーという考え
壊れる事が前提で、物事を作っていくマインドがないと、kubernetesは触らない方が良い。
落ちる。影響範囲を小さくする。
落ちないようにする努力よりも、落ちる前提で考えた方が良い。
落ちた時の再構築の流れを考える。どこまでデザインオブフェアーをやり通すか。必ず落ちる。最後に
本当に寺田さん、ありがとうございました!
正直、Dockerは30分は触ってたので何となく分かったけど、Kubernetesは無知でした。
モブプロだった事も幸いし、なんとか最後まで出来た!!(´∀`)
後日、知り合いと3人で同じ内容をした。
本記事は、その時の成功例を元に作成した。繰り返すって大事だなぁ。
Kubernetes、、、というかマイクロサービスをちょっと感じれて良かった。
またやりたい!(^・^)
- 投稿日:2019-08-28T12:54:23+09:00
線形探索(番兵法)
番兵法とは
線形探索でコストを抑えるための方法。
探索するキーと同じ値を配列の最後に格納する。
このときに格納するデータを番兵(sentinel)と呼ぶ。SenlinSearch.java/* 線形探索(番兵法) */ import java.util.Scanner; public class SenlinSearch{ static int linSearch(int[] arr,int n,int key){ int i=0; arr[n]=key; while(true){ if(arr[i]==key){ break; } i++; } return i == n ? -1: i; } public static void main(String[]args){ Scanner sc = new Scanner(System.in); System.out.print("要素数を入力: "); int n=sc.nextInt(); int arr[]=new int[n+1]; for(int i=0;i<n;i++){ System.out.print(i+"個目: "); arr[i]=sc.nextInt(); } System.out.print("探す値を入力: "); int key=sc.nextInt(); arr[n]=key; int ans=linSearch(arr,n,key); if(ans==-1){ System.out.println("見つかりませんでした"); }else{ System.out.println(ans+"つ目に見つかりました"); } } }実行例
要素数を入力: 5 0個目: 2 1個目: 4 2個目: 5 3個目: 6 4個目: 7 探す値を入力: 6 3つ目に見つかりました 要素数を入力: 5 0個目: 2 1個目: 4 2個目: 5 3個目: 6 4個目: 7 探す値を入力: 8 見つかりませんでした
- 投稿日:2019-08-28T12:31:42+09:00
springを勉強するうちに、Neither BindingResult nor plain target object for bean name 'command' available as request attribute エラー対処
springmvcのフォームredirectの練習をしたとき、jsp中にspringのformタグを使い時、こんなエラーが発生しました。
Stacktrace:] with root cause
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'command' available as request attribute
at org.springframework.web.servlet.support.BindStatus.(BindStatus.java:141)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.getBindStatus(AbstractDataBoundFormElementTag.java:178)調べたところ、
エラーが発生した理由:
spring:bindのソースの中身を見ると
Object target = requestContext.getModelObject(beanName);
if (target == null) {
throw new IllegalStateException("Neither BindingResult nor plain target object for bean name '" +
beanName + "' available as request attribute");
}
ようは、ある画面にリクエストする時、リクエストにオブジェクトのcommandがないので、エラーが発生する解決方法:
JSPに、/jsp:useBeanを追加します
- 投稿日:2019-08-28T11:02:24+09:00
[個人メモ] すでにJavaが入っているAmazonLinuxに、最新のJavaをインストールしてバージョンを切り替える
- 投稿日:2019-08-28T02:08:53+09:00
【Java】バイト数指定で文字列を半角スペースパディングする
import java.nio.charset.Charset; public class Main { public static void main(String[] args) throws Exception { int size = 10; String[] charsetNames = {"utf-8", "sjis"}; String[] targets = {"あ", "aiueo", "あいうえお", "?"}; for (String charsetName: charsetNames) { System.out.println("--- " + charsetName + " ---"); for (String target: targets) { System.out.println("[" + Main.padRight(target, size, charsetName) + "]"); } } // 実行結果: // --- utf-8 --- // [あ ] // [aiueo ] // [あいうえお] // [? ] // --- sjis --- // [あ ] // [aiueo ] // [あいうえお] // [? ] } /** * 文字列の末尾を半角スペースでパディングする。 * 文字列が指定バイト数を超過している場合、渡された文字列をそのまま返却する。 * * @param target パディング対象の文字列 * @param size パディング後のバイト数 * @param charsetName 文字コード名 * @return パディング後の文字列 */ private static String padRight(String target, int size, String charsetName) { Charset charset = Charset.forName(charsetName); int blankSize = " ".getBytes(charset).length; int paddingLen = (size - target.getBytes(charset).length) / blankSize; StringBuilder sb = new StringBuilder(target); for (int i = 0; i < paddingLen; i ++) { sb.append(" "); } return sb.toString(); } }
- 投稿日:2019-08-28T02:08:53+09:00
[Java]バイト数指定で文字列を半角スペースパディングする
import java.nio.charset.Charset; public class Main { public static void main(String[] args) throws Exception { int size = 10; String[] charsetNames = {"utf-8", "sjis"}; String[] targets = {"あ", "aiueo", "あいうえお", "?"}; for (String charsetName: charsetNames) { System.out.println("--- " + charsetName + " ---"); for (String target: targets) { System.out.println("[" + Main.padRight(target, size, charsetName) + "]"); } } // 実行結果: // --- utf-8 --- // [あ ] // [aiueo ] // [あいうえお] // [? ] // --- sjis --- // [あ ] // [aiueo ] // [あいうえお] // [? ] } /** * 文字列の末尾を半角スペースでパディングする。 * 文字列が指定バイト数を超過している場合、渡された文字列をそのまま返却する。 * * @param target パディング対象の文字列 * @param size パディング後のバイト数 * @param charsetName 文字コード名 * @return パディング後の文字列 */ private static String padRight(String target, int size, String charsetName) { Charset charset = Charset.forName(charsetName); int blankSize = " ".getBytes(charset).length; int paddingLen = (size - target.getBytes(charset).length) / blankSize; StringBuilder sb = new StringBuilder(target); for (int i = 0; i < paddingLen; i ++) { sb.append(" "); } return sb.toString(); } }

